From 16d6f1bdaf84640a82a8ca12b75995657ad64a3f Mon Sep 17 00:00:00 2001 From: Ryotaro Onoue <73390859+YumNumm@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:58:54 +0900 Subject: [PATCH] fix: vxse51 crash bug (#548) * update flutter to 3.19.0-0.4-beta * fix --- .fvm/fvm_config.json | 2 +- ios/Runner.xcodeproj/project.pbxproj | 12 +- ios/Runner/Info.plist | 2 +- .../intenisty/jma_lg_intensity_icon.dart | 131 +-- lib/core/extension/lat_lng_bounds_list.dart | 13 + .../capture/intensity_icon_render.dart | 28 + .../capture/intensity_icon_render.g.dart | 35 + .../earthquake_history_config_provider.dart | 15 +- .../earthquake_history_config_provider.g.dart | 2 +- .../earthquake_history_config_model.dart | 28 +- ...rthquake_history_config_model.freezed.dart | 103 +- .../earthquake_history_config_model.g.dart | 27 +- lib/core/provider/map/map_style.dart | 2 - .../page/earthquake_history.dart | 6 + .../use_case/earthquake_history_use_case.dart | 2 +- .../component/earthquake_map.dart | 918 +++++++++++++----- .../screen/earthquake_history_details.dart | 20 +- .../render/intensity_renderer_widget.dart | 60 ++ .../home/features/map/view/main_map_view.dart | 11 + .../earthquake_history_config_page.dart | 276 +++++- 20 files changed, 1296 insertions(+), 397 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index dbec753e8..79421ad57 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.19.0-0.2.pre@beta", + "flutterSdkVersion": "3.19.0-0.4.pre@beta", "flavors": {} } \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 85f5680dd..49341c07a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_BITCODE = NO; @@ -559,7 +559,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -601,7 +601,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -642,7 +642,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -788,7 +788,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_BITCODE = NO; @@ -820,7 +820,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1110; + CURRENT_PROJECT_VERSION = 1113; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CPL7H8SHVM; ENABLE_BITCODE = NO; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 51c287cb1..86b388931 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -23,7 +23,7 @@ CFBundleSignature ???? CFBundleVersion - 1110 + 1113 LSApplicationCategoryType FLTEnableImpeller diff --git a/lib/core/component/intenisty/jma_lg_intensity_icon.dart b/lib/core/component/intenisty/jma_lg_intensity_icon.dart index fbf3da268..53d9c669a 100644 --- a/lib/core/component/intenisty/jma_lg_intensity_icon.dart +++ b/lib/core/component/intenisty/jma_lg_intensity_icon.dart @@ -24,68 +24,85 @@ class JmaLgIntensityIcon extends ConsumerWidget { final intensityColorModel = ref.watch(intensityColorProvider); final colorScheme = intensityColorModel.fromJmaLgIntensity(intensity); final (fg, bg) = (colorScheme.foreground, colorScheme.background); - // 震度の整数部分 - final intensityMainText = - intensity.type.replaceAll('-', '').replaceAll('+', ''); - // 震度の弱・強の表記 - final intensitySubText = intensity.type.contains('-') - ? '弱' - : intensity.type.contains('+') - ? '強' - : ''; - return SizedBox( - height: 50, - width: 50, - child: DecoratedBox( - decoration: BoxDecoration( - color: bg, - // 角丸にする - borderRadius: BorderRadius.circular(10), - ), - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - if (customText != null) - Text( - customText!, - style: TextStyle( - color: fg, - fontSize: 100, - fontWeight: FontWeight.w900, - fontFamily: FontFamily.jetBrainsMono, - ), - ) - else ...[ - Text( - intensityMainText, - style: TextStyle( - color: fg, - fontSize: 100, - fontWeight: FontWeight.w900, - fontFamily: FontFamily.jetBrainsMono, - ), - ), - Text( - intensitySubText, - style: TextStyle( - color: fg, - fontSize: 50, - fontWeight: FontWeight.w900, - fontFamily: FontFamily.jetBrainsMono, - fontFamilyFallback: const [FontFamily.notoSansJP], + final borderColor = Color.lerp( + bg, + fg, + 0.3, + )!; + return switch (type) { + IntensityIconType.small => SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: bg, + border: Border.all( + color: borderColor, + width: 5, + ), + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + intensity.type, + style: TextStyle( + color: fg, + fontSize: 100, + fontWeight: FontWeight.w900, + fontFamily: FontFamily.jetBrainsMono, + ), ), + ], + ), + ), + ), + ), + ), + IntensityIconType.smallWithoutText => SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: bg, + border: Border.all( + color: borderColor, + width: 5, + ), + ), + ), + ), + IntensityIconType.filled => SizedBox( + height: 50, + width: 50, + child: DecoratedBox( + decoration: BoxDecoration( + color: bg, + // 角丸にする + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + customText!, + style: TextStyle( + color: fg, + fontSize: 100, + fontWeight: FontWeight.w900, + fontFamily: FontFamily.jetBrainsMono, ), - ], - ], + ), + ), ), ), ), - ), - ); + }; } } diff --git a/lib/core/extension/lat_lng_bounds_list.dart b/lib/core/extension/lat_lng_bounds_list.dart index fb08be543..d26bfb8dd 100644 --- a/lib/core/extension/lat_lng_bounds_list.dart +++ b/lib/core/extension/lat_lng_bounds_list.dart @@ -21,3 +21,16 @@ extension LatLngBoundsListEx on List { ); } } + +extension LatLngBoundsEx on LatLngBounds { + LatLngBounds add(LatLng latLng) { + final minLat = min(southWest.lat, latLng.lat); + final minLng = min(southWest.lng, latLng.lng); + final maxLat = max(northEast.lat, latLng.lat); + final maxLng = max(northEast.lng, latLng.lng); + return LatLngBounds( + southWest: LatLng(lat: minLat, lng: minLng), + northEast: LatLng(lat: maxLat, lng: maxLng), + ); + } +} diff --git a/lib/core/provider/capture/intensity_icon_render.dart b/lib/core/provider/capture/intensity_icon_render.dart index e6d2c6c79..971818dc0 100644 --- a/lib/core/provider/capture/intensity_icon_render.dart +++ b/lib/core/provider/capture/intensity_icon_render.dart @@ -29,6 +29,30 @@ class IntensityIconFillRender extends _$IntensityIconFillRender { Map build() => {}; } +@Riverpod(keepAlive: true) +class LpgmIntensityIconRender extends _$LpgmIntensityIconRender { + void onRendered(Uint8List data, JmaLgIntensity intensity) { + state = {...state, intensity: data}; + } + + Uint8List? get(JmaLgIntensity intensity) => state.getOrNull(intensity); + + @override + Map build() => {}; +} + +@Riverpod(keepAlive: true) +class LpgmIntensityIconFillRender extends _$LpgmIntensityIconFillRender { + void onRendered(Uint8List data, JmaLgIntensity intensity) { + state = {...state, intensity: data}; + } + + Uint8List? get(JmaLgIntensity intensity) => state.getOrNull(intensity); + + @override + Map build() => {}; +} + @Riverpod(keepAlive: true) class HypocenterIconRender extends _$HypocenterIconRender { @override @@ -50,3 +74,7 @@ class HypocenterLowPreciseIconRender extends _$HypocenterLowPreciseIconRender { extension IntensityIconRenderEx on Map { bool isAllRendered() => length == JmaIntensity.values.length; } + +extension LpgmIntensityIconRenderEx on Map { + bool isAllRendered() => length == JmaLgIntensity.values.length; +} diff --git a/lib/core/provider/capture/intensity_icon_render.g.dart b/lib/core/provider/capture/intensity_icon_render.g.dart index 0849e99a5..e31515047 100644 --- a/lib/core/provider/capture/intensity_icon_render.g.dart +++ b/lib/core/provider/capture/intensity_icon_render.g.dart @@ -42,6 +42,41 @@ final intensityIconFillRenderProvider = NotifierProvider< ); typedef _$IntensityIconFillRender = Notifier>; +String _$lpgmIntensityIconRenderHash() => + r'44e23825e54c81cde3fff5109e75be2910db02c9'; + +/// See also [LpgmIntensityIconRender]. +@ProviderFor(LpgmIntensityIconRender) +final lpgmIntensityIconRenderProvider = NotifierProvider< + LpgmIntensityIconRender, Map>.internal( + LpgmIntensityIconRender.new, + name: r'lpgmIntensityIconRenderProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$lpgmIntensityIconRenderHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LpgmIntensityIconRender = Notifier>; +String _$lpgmIntensityIconFillRenderHash() => + r'34bd69625f805e15eebfa7192e71532f19e3c94d'; + +/// See also [LpgmIntensityIconFillRender]. +@ProviderFor(LpgmIntensityIconFillRender) +final lpgmIntensityIconFillRenderProvider = NotifierProvider< + LpgmIntensityIconFillRender, Map>.internal( + LpgmIntensityIconFillRender.new, + name: r'lpgmIntensityIconFillRenderProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$lpgmIntensityIconFillRenderHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LpgmIntensityIconFillRender + = Notifier>; String _$hypocenterIconRenderHash() => r'76f8f7104e9c1218646b56290778b15bed6973c0'; diff --git a/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.dart b/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.dart index 30f8d820f..6efea211e 100644 --- a/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.dart +++ b/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.dart @@ -42,13 +42,20 @@ class EarthquakeHistoryConfig extends _$EarthquakeHistoryConfig { } } - void updateListConfig(EarthquakeHistoryListConfig config) { + Future updateListConfig(EarthquakeHistoryListConfig config) async { state = state.copyWith(list: config); - _save(); + return _save(); } - void updateDetailConfig(EarthquakeHistoryDetailConfig config) { + Future updateDetailConfig(EarthquakeHistoryDetailConfig config) async { state = state.copyWith(detail: config); - _save(); + return _save(); + } + + Future updateIntensityIcon({required bool value}) async { + state = state.copyWith( + detail: state.detail.copyWith(showIntensityIcon: value), + ); + await _save(); } } diff --git a/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.g.dart b/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.g.dart index bbbfd8ed6..f0480df2c 100644 --- a/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.g.dart +++ b/lib/core/provider/config/earthquake_history/earthquake_history_config_provider.g.dart @@ -9,7 +9,7 @@ part of 'earthquake_history_config_provider.dart'; // ************************************************************************** String _$earthquakeHistoryConfigHash() => - r'b1fdfbaa45d3af3f74336154f5d1f905bd84e51c'; + r'453f49269fbf832c29a5b1363665b4b05884eb3f'; /// See also [EarthquakeHistoryConfig]. @ProviderFor(EarthquakeHistoryConfig) diff --git a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart index b48a2db93..f0f528c56 100644 --- a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart +++ b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart @@ -32,26 +32,24 @@ class EarthquakeHistoryListConfig with _$EarthquakeHistoryListConfig { class EarthquakeHistoryDetailConfig with _$EarthquakeHistoryDetailConfig { const factory EarthquakeHistoryDetailConfig({ /// 震度の表示方法 - @Default(IntensityDisplayMode.fillCity) - IntensityDisplayMode intensityDisplayMode, + @Default(IntensityFillMode.fillCity) IntensityFillMode intensityFillMode, + + /// 震度観測点のアイコン表示 + @Default(true) bool showIntensityIcon, + + /// fromJsonでは、常にfalseを返す + @Default(false) bool showingLpgmIntensity, }) = _EarthquakeHistoryDetailConfig; factory EarthquakeHistoryDetailConfig.fromJson(Map json) => - _$EarthquakeHistoryDetailConfigFromJson(json); + _$EarthquakeHistoryDetailConfigFromJson(json).copyWith( + showingLpgmIntensity: false, + ); } /// 地震履歴詳細画面における震度の表示方法 -enum IntensityDisplayMode { - /// 震度観測点のアイコンを表示 - icon, - - /// 市区町村レベルの震度塗りつぶし +enum IntensityFillMode { fillCity, - - /// 都道府県レベルの震度塗りつぶし - fillPrefecture, - - /// 震度観測点のアイコンと都道府県レベルの震度塗りつぶし - iconAndFillPrefecture, - ; + fillRegion, + none; } diff --git a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.freezed.dart b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.freezed.dart index 302325f3e..4b07f8712 100644 --- a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.freezed.dart +++ b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.freezed.dart @@ -399,8 +399,13 @@ EarthquakeHistoryDetailConfig _$EarthquakeHistoryDetailConfigFromJson( /// @nodoc mixin _$EarthquakeHistoryDetailConfig { /// 震度の表示方法 - IntensityDisplayMode get intensityDisplayMode => - throw _privateConstructorUsedError; + IntensityFillMode get intensityFillMode => throw _privateConstructorUsedError; + + /// 震度観測点のアイコン表示 + bool get showIntensityIcon => throw _privateConstructorUsedError; + + /// fromJsonでは、常にfalseを返す + bool get showingLpgmIntensity => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -416,7 +421,10 @@ abstract class $EarthquakeHistoryDetailConfigCopyWith<$Res> { _$EarthquakeHistoryDetailConfigCopyWithImpl<$Res, EarthquakeHistoryDetailConfig>; @useResult - $Res call({IntensityDisplayMode intensityDisplayMode}); + $Res call( + {IntensityFillMode intensityFillMode, + bool showIntensityIcon, + bool showingLpgmIntensity}); } /// @nodoc @@ -433,13 +441,23 @@ class _$EarthquakeHistoryDetailConfigCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? intensityDisplayMode = null, + Object? intensityFillMode = null, + Object? showIntensityIcon = null, + Object? showingLpgmIntensity = null, }) { return _then(_value.copyWith( - intensityDisplayMode: null == intensityDisplayMode - ? _value.intensityDisplayMode - : intensityDisplayMode // ignore: cast_nullable_to_non_nullable - as IntensityDisplayMode, + intensityFillMode: null == intensityFillMode + ? _value.intensityFillMode + : intensityFillMode // ignore: cast_nullable_to_non_nullable + as IntensityFillMode, + showIntensityIcon: null == showIntensityIcon + ? _value.showIntensityIcon + : showIntensityIcon // ignore: cast_nullable_to_non_nullable + as bool, + showingLpgmIntensity: null == showingLpgmIntensity + ? _value.showingLpgmIntensity + : showingLpgmIntensity // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -453,7 +471,10 @@ abstract class _$$EarthquakeHistoryDetailConfigImplCopyWith<$Res> __$$EarthquakeHistoryDetailConfigImplCopyWithImpl<$Res>; @override @useResult - $Res call({IntensityDisplayMode intensityDisplayMode}); + $Res call( + {IntensityFillMode intensityFillMode, + bool showIntensityIcon, + bool showingLpgmIntensity}); } /// @nodoc @@ -469,13 +490,23 @@ class __$$EarthquakeHistoryDetailConfigImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? intensityDisplayMode = null, + Object? intensityFillMode = null, + Object? showIntensityIcon = null, + Object? showingLpgmIntensity = null, }) { return _then(_$EarthquakeHistoryDetailConfigImpl( - intensityDisplayMode: null == intensityDisplayMode - ? _value.intensityDisplayMode - : intensityDisplayMode // ignore: cast_nullable_to_non_nullable - as IntensityDisplayMode, + intensityFillMode: null == intensityFillMode + ? _value.intensityFillMode + : intensityFillMode // ignore: cast_nullable_to_non_nullable + as IntensityFillMode, + showIntensityIcon: null == showIntensityIcon + ? _value.showIntensityIcon + : showIntensityIcon // ignore: cast_nullable_to_non_nullable + as bool, + showingLpgmIntensity: null == showingLpgmIntensity + ? _value.showingLpgmIntensity + : showingLpgmIntensity // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -485,7 +516,9 @@ class __$$EarthquakeHistoryDetailConfigImplCopyWithImpl<$Res> class _$EarthquakeHistoryDetailConfigImpl implements _EarthquakeHistoryDetailConfig { const _$EarthquakeHistoryDetailConfigImpl( - {this.intensityDisplayMode = IntensityDisplayMode.fillCity}); + {this.intensityFillMode = IntensityFillMode.fillCity, + this.showIntensityIcon = true, + this.showingLpgmIntensity = false}); factory _$EarthquakeHistoryDetailConfigImpl.fromJson( Map json) => @@ -494,11 +527,21 @@ class _$EarthquakeHistoryDetailConfigImpl /// 震度の表示方法 @override @JsonKey() - final IntensityDisplayMode intensityDisplayMode; + final IntensityFillMode intensityFillMode; + + /// 震度観測点のアイコン表示 + @override + @JsonKey() + final bool showIntensityIcon; + + /// fromJsonでは、常にfalseを返す + @override + @JsonKey() + final bool showingLpgmIntensity; @override String toString() { - return 'EarthquakeHistoryDetailConfig(intensityDisplayMode: $intensityDisplayMode)'; + return 'EarthquakeHistoryDetailConfig(intensityFillMode: $intensityFillMode, showIntensityIcon: $showIntensityIcon, showingLpgmIntensity: $showingLpgmIntensity)'; } @override @@ -506,13 +549,18 @@ class _$EarthquakeHistoryDetailConfigImpl return identical(this, other) || (other.runtimeType == runtimeType && other is _$EarthquakeHistoryDetailConfigImpl && - (identical(other.intensityDisplayMode, intensityDisplayMode) || - other.intensityDisplayMode == intensityDisplayMode)); + (identical(other.intensityFillMode, intensityFillMode) || + other.intensityFillMode == intensityFillMode) && + (identical(other.showIntensityIcon, showIntensityIcon) || + other.showIntensityIcon == showIntensityIcon) && + (identical(other.showingLpgmIntensity, showingLpgmIntensity) || + other.showingLpgmIntensity == showingLpgmIntensity)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, intensityDisplayMode); + int get hashCode => Object.hash( + runtimeType, intensityFillMode, showIntensityIcon, showingLpgmIntensity); @JsonKey(ignore: true) @override @@ -533,8 +581,9 @@ class _$EarthquakeHistoryDetailConfigImpl abstract class _EarthquakeHistoryDetailConfig implements EarthquakeHistoryDetailConfig { const factory _EarthquakeHistoryDetailConfig( - {final IntensityDisplayMode intensityDisplayMode}) = - _$EarthquakeHistoryDetailConfigImpl; + {final IntensityFillMode intensityFillMode, + final bool showIntensityIcon, + final bool showingLpgmIntensity}) = _$EarthquakeHistoryDetailConfigImpl; factory _EarthquakeHistoryDetailConfig.fromJson(Map json) = _$EarthquakeHistoryDetailConfigImpl.fromJson; @@ -542,7 +591,15 @@ abstract class _EarthquakeHistoryDetailConfig @override /// 震度の表示方法 - IntensityDisplayMode get intensityDisplayMode; + IntensityFillMode get intensityFillMode; + @override + + /// 震度観測点のアイコン表示 + bool get showIntensityIcon; + @override + + /// fromJsonでは、常にfalseを返す + bool get showingLpgmIntensity; @override @JsonKey(ignore: true) _$$EarthquakeHistoryDetailConfigImplCopyWith< diff --git a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.g.dart b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.g.dart index 6b6863a17..99e4828e8 100644 --- a/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.g.dart +++ b/lib/core/provider/config/earthquake_history/model/earthquake_history_config_model.g.dart @@ -65,11 +65,15 @@ _$EarthquakeHistoryDetailConfigImpl json, ($checkedConvert) { final val = _$EarthquakeHistoryDetailConfigImpl( - intensityDisplayMode: $checkedConvert( - 'intensityDisplayMode', + intensityFillMode: $checkedConvert( + 'intensityFillMode', (v) => - $enumDecodeNullable(_$IntensityDisplayModeEnumMap, v) ?? - IntensityDisplayMode.fillCity), + $enumDecodeNullable(_$IntensityFillModeEnumMap, v) ?? + IntensityFillMode.fillCity), + showIntensityIcon: $checkedConvert( + 'showIntensityIcon', (v) => v as bool? ?? true), + showingLpgmIntensity: $checkedConvert( + 'showingLpgmIntensity', (v) => v as bool? ?? false), ); return val; }, @@ -78,13 +82,14 @@ _$EarthquakeHistoryDetailConfigImpl Map _$$EarthquakeHistoryDetailConfigImplToJson( _$EarthquakeHistoryDetailConfigImpl instance) => { - 'intensityDisplayMode': - _$IntensityDisplayModeEnumMap[instance.intensityDisplayMode]!, + 'intensityFillMode': + _$IntensityFillModeEnumMap[instance.intensityFillMode]!, + 'showIntensityIcon': instance.showIntensityIcon, + 'showingLpgmIntensity': instance.showingLpgmIntensity, }; -const _$IntensityDisplayModeEnumMap = { - IntensityDisplayMode.icon: 'icon', - IntensityDisplayMode.fillCity: 'fillCity', - IntensityDisplayMode.fillPrefecture: 'fillPrefecture', - IntensityDisplayMode.iconAndFillPrefecture: 'iconAndFillPrefecture', +const _$IntensityFillModeEnumMap = { + IntensityFillMode.fillCity: 'fillCity', + IntensityFillMode.fillRegion: 'fillRegion', + IntensityFillMode.none: 'none', }; diff --git a/lib/core/provider/map/map_style.dart b/lib/core/provider/map/map_style.dart index 1b03c2786..428200692 100644 --- a/lib/core/provider/map/map_style.dart +++ b/lib/core/provider/map/map_style.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'dart:io'; import 'package:eqmonitor/core/provider/map/map_config.dart'; @@ -160,7 +159,6 @@ class MapStyle { }, ], }; - log(jsonEncode(json)); return _saveStyleJson(json, 'maplibre-$isDark'); } } diff --git a/lib/feature/earthquake_history/page/earthquake_history.dart b/lib/feature/earthquake_history/page/earthquake_history.dart index b130d7963..090a5bb9f 100644 --- a/lib/feature/earthquake_history/page/earthquake_history.dart +++ b/lib/feature/earthquake_history/page/earthquake_history.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:eqmonitor/core/provider/config/earthquake_history/earthquake_history_config_provider.dart'; import 'package:eqmonitor/core/router/router.dart'; import 'package:eqmonitor/feature/earthquake_history/component/earthquake_history_tile_widget.dart'; @@ -179,6 +180,7 @@ class _ListBottomWidget extends ConsumerWidget { ), ); } + final exception = error.error; return Padding( padding: const EdgeInsets.symmetric( vertical: 40, @@ -200,6 +202,10 @@ class _ListBottomWidget extends ConsumerWidget { Text( error.error.toString(), ), + switch (exception) { + DioException() => Text(exception.response.toString()), + _ => Text(exception.runtimeType.toString()), + }, ], ), ); diff --git a/lib/feature/earthquake_history/use_case/earthquake_history_use_case.dart b/lib/feature/earthquake_history/use_case/earthquake_history_use_case.dart index 0871608c5..8fcab56ff 100644 --- a/lib/feature/earthquake_history/use_case/earthquake_history_use_case.dart +++ b/lib/feature/earthquake_history/use_case/earthquake_history_use_case.dart @@ -37,7 +37,7 @@ class EarthquakeHistoryUseCase { if (e.response?.statusCode == 429) { throw Exception('レートリミットに達しました。10秒後に再度お試しください。'); } - throw Exception(e.message); + rethrow; } } } diff --git a/lib/feature/earthquake_history_details/component/earthquake_map.dart b/lib/feature/earthquake_history_details/component/earthquake_map.dart index 02228a07f..c0bd37877 100644 --- a/lib/feature/earthquake_history_details/component/earthquake_map.dart +++ b/lib/feature/earthquake_history_details/component/earthquake_map.dart @@ -1,11 +1,14 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; +import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:eqapi_types/eqapi_types.dart'; import 'package:eqmonitor/core/extension/lat_lng_bounds_list.dart'; import 'package:eqmonitor/core/provider/capture/intensity_icon_render.dart'; +import 'package:eqmonitor/core/provider/config/earthquake_history/earthquake_history_config_provider.dart'; +import 'package:eqmonitor/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart'; import 'package:eqmonitor/core/provider/config/theme/intensity_color/intensity_color_provider.dart'; import 'package:eqmonitor/core/provider/config/theme/intensity_color/model/intensity_color_model.dart'; import 'package:eqmonitor/core/provider/jma_parameter/jma_parameter.dart'; @@ -13,10 +16,12 @@ import 'package:eqmonitor/core/provider/map/jma_map_provider.dart'; import 'package:eqmonitor/core/provider/map/map_style.dart'; import 'package:eqmonitor/feature/earthquake_history/model/state/earthquake_history_item.dart'; import 'package:extensions/extensions.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:jma_map/jma_map.dart' as jma_map; import 'package:jma_parameter_api_client/jma_parameter_api_client.dart'; import 'package:maplibre_gl/maplibre_gl.dart' as map_libre; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -63,14 +68,21 @@ class EarthquakeMapWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final stopWatch = Stopwatch()..start(); + log('EarthquakeMapWidget build start'); final earthquake = item.earthquake; final intensity = earthquake.intensity; + final colorModel = ref.watch(intensityColorProvider); final earthquakeParams = ref.watch(jmaParameterProvider).valueOrNull?.earthquake; final intensityIconData = ref.watch(intensityIconRenderProvider); - final hypocenterIconRender = ref.watch(hypocenterIconRenderProvider); final intensityIconFillData = ref.watch(intensityIconFillRenderProvider); + final lpgmIntensityIconData = ref.watch(lpgmIntensityIconRenderProvider); + final lpgmIntensityIconFillData = + ref.watch(lpgmIntensityIconFillRenderProvider); + + final hypocenterIconRender = ref.watch(hypocenterIconRenderProvider); final jmaMap = ref.watch(jmaMapProvider).valueOrNull; final isDark = Theme.of(context).brightness == Brightness.dark; final mapStyle = ref.watch(mapStyleProvider); @@ -83,9 +95,12 @@ class EarthquakeMapWidget extends HookConsumerWidget { ); final styleJsonFuture = useFuture(styleJsonFutureing); final path = styleJsonFuture.data; + if (earthquakeParams == null || !intensityIconData.isAllRendered() || !intensityIconFillData.isAllRendered() || + !lpgmIntensityIconData.isAllRendered() || + !lpgmIntensityIconFillData.isAllRendered() || jmaMap == null || hypocenterIconRender == null || path == null) { @@ -95,101 +110,47 @@ class EarthquakeMapWidget extends HookConsumerWidget { ), ); } + log('EarthquakeMapWidget build phase 1 ${stopWatch.elapsedMilliseconds}ms'); - final citiesItem = useMemoized( - () => intensity?.cities - ?.groupListsBy((e) => e.maxInt) - .entries - .where((e) => e.key != null) - .map( - (e) => ( - color: colorModel.fromJmaIntensity(e.key!), - codes: e.value.map((e) => e.code).toList(), - intensity: intensity.maxInt, - ), - ) - .toList(), - [intensity], - ); - final regionsItem = useMemoized( - () => intensity?.regions - .groupListsBy((e) => e.maxInt) - .entries - .where((e) => e.key != null) - .map( - (e) => ( - color: colorModel.fromJmaIntensity(e.key!), - codes: e.value.map((e) => e.code.padLeft(3, '0')).toList(), - intensity: intensity.maxInt, - ), - ) - .toList(), - [intensity], - ); - final regionsLpgmItem = useMemoized( - () => intensity?.regions - .groupListsBy((e) => e.maxLgInt) - .entries - .where((e) => e.key != null) - .map( - (e) => ( - color: colorModel.fromJmaLgIntensity(e.key!), - codes: e.value.map((e) => e.code.padLeft(3, '0')).toList(), - intensity: intensity, - ), - ) - .toList(), - [intensity], - ); - final stationsItem = useMemoized( + final itemCalcurateFutureing = useMemoized( () { - final stations = intensity?.stations; - if (stations == null) { - return null; - } - final allStations = earthquakeParams.regions - .map( - (region) => region.cities.map( - (city) => city.stations, - ), - ) - .flattened - .flattened; - final stationsParamMerged = stations.map( - (e) => ( - item: e, - param: allStations.firstWhereOrNull( - (element) => element.code == e.code, - ), - ), - ); - final grouped = stationsParamMerged.groupListsBy((e) => e.item.maxInt); - return grouped; + return _compute(colorModel, earthquake, earthquakeParams); }, - [intensity], + [earthquake, jmaMap], ); + final itemCalcurateFuture = useFuture(itemCalcurateFutureing); + final result = itemCalcurateFuture.data; + if (itemCalcurateFuture.hasError) { + return Scaffold( + body: Center( + child: Text('地図情報の取得に失敗しました\nエラー: ${itemCalcurateFuture.error}'), + ), + ); + } + if (result == null) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + } + final ( + regionsItem, + citiesItem, + stationsItem, + regionsLpgmItem, + stationsLpgmItem + ) = result; + log('EarthquakeMapWidget build phase 2 ${stopWatch.elapsedMilliseconds}ms'); final bbox = useMemoized( () { - // 最大震度5弱以上の場合、最大震度4以上の地域を表示する final maxInt = intensity?.maxInt; if (maxInt == null || regionsItem == null) { return null; } - final List codes; - if (maxInt.index >= JmaIntensity.fiveLower.index) { - codes = regionsItem - .where((e) => e.intensity.index >= JmaIntensity.four.index) - .map((e) => e.codes) - .flattened - .toList(); - } else { - codes = regionsItem - .where((e) => e.intensity.index >= maxInt.index) - .map((e) => e.codes) - .flattened - .toList(); - } + // 最大震度5弱以上の場合、最大震度4以上の地域を表示する + final codes = regionsItem.map((e) => e.codes).flattened.toList(); final bboxs = codes .map( (e) => jmaMap[JmaMapType.areaForecastLocalE]! @@ -198,7 +159,14 @@ class EarthquakeMapWidget extends HookConsumerWidget { ) .whereNotNull() .toList(); - final bbox = bboxs.marge(); + var bbox = bboxs.marge(); + // 震源地を含める + final hypocenter = earthquake.earthquake?.hypocenter.coordinate; + if (hypocenter != null) { + bbox = bbox.add( + jma_map.LatLng(lat: hypocenter.lat, lng: hypocenter.lon), + ); + } return bbox; }, [regionsItem], @@ -207,19 +175,123 @@ class EarthquakeMapWidget extends HookConsumerWidget { final mapController = useState(null); final cameraUpdate = useMemoized( - () => CameraUpdate.newLatLngBounds( - LatLngBounds( - southwest: LatLng( - bbox!.southWest.lat, - bbox.southWest.lng, + () { + if (bbox == null) { + final hypocenter = earthquake.earthquake?.hypocenter.coordinate; + if (hypocenter != null) { + return CameraUpdate.newLatLngZoom( + map_libre.LatLng(hypocenter.lat, hypocenter.lon), + 2, + ); + } else { + return CameraUpdate.newLatLngZoom( + const map_libre.LatLng(35, 139), + 6, + ); + } + } + return CameraUpdate.newLatLngBounds( + map_libre.LatLngBounds( + southwest: map_libre.LatLng( + bbox.southWest.lat, + bbox.southWest.lng, + ), + northeast: map_libre.LatLng( + bbox.northEast.lat, + bbox.northEast.lng, + ), ), - northeast: LatLng( - bbox.northEast.lat, - bbox.northEast.lng, + bottom: 10, + left: 10, + right: 10, + top: 10, + ); + }, + [bbox, earthquake], + ); + + // * Display mode related + List<_Action> getActions(EarthquakeHistoryDetailConfig config) => [ + _HypocenterAction( + earthquake: earthquake, ), - ), + if (config.showingLpgmIntensity) ...[ + if (config.intensityFillMode == IntensityFillMode.fillRegion) + _FillRegionLpgmIntensityAction( + regionsItem: regionsLpgmItem ?? [], + ), + if (config.showIntensityIcon) + _StationIntensityLpgmAction( + stations: stationsLpgmItem ?? {}, + colorModel: colorModel, + ), + ] else ...[ + if (config.intensityFillMode == IntensityFillMode.fillCity) + if (citiesItem != null) + _FillCityAction( + citiesItem: citiesItem, + ) + else + _FillRegionAction( + regionsItem: regionsItem ?? [], + ), + if (config.intensityFillMode == IntensityFillMode.fillRegion) + _FillRegionAction( + regionsItem: regionsItem ?? [], + ), + if (config.showIntensityIcon) + _StationAction( + stations: stationsItem ?? {}, + colorModel: colorModel, + ), + ], + // TODO + /* if (config.intensityFillMode == + intensityFillMode.fillLpgmRegion) + _FillLpgmAction( + regionsLpgmItem: regionsLpgmItem ?? [], + ), + */ + ]; + final config = ref.watch( + earthquakeHistoryConfigProvider.select((value) => value.detail), + ); + final currentActions = useState>(getActions(config)); + + Future disposeActions(List<_Action> actions) async { + for (final action in actions) { + await action.dispose(mapController.value!); + } + } + + Future initActions(List<_Action> actions) async { + await actions + .map>( + (e) => e.init(mapController.value!), + ) + .wait; + } + + Future onDisplayModeChanged({ + required map_libre.MaplibreMapController controller, + required EarthquakeHistoryDetailConfig config, + }) async { + // まずはdispose + await disposeActions(currentActions.value); + // 次にinit + final actions = getActions(config); + + // 状態更新 + currentActions.value = actions; + await initActions(actions); + } + + ref.listen( + earthquakeHistoryConfigProvider.select((v) => v.detail), + (_, next) async => onDisplayModeChanged( + controller: mapController.value!, + config: next, ), - [bbox], ); useEffect( @@ -239,93 +311,306 @@ class EarthquakeMapWidget extends HookConsumerWidget { }, [], ); + final maxZoomLevel = useState(6); return RepaintBoundary( child: MaplibreMap( initialCameraPosition: CameraPosition( - target: LatLng( + target: map_libre.LatLng( earthquake.earthquake?.hypocenter.coordinate?.lat ?? 35, earthquake.earthquake?.hypocenter.coordinate?.lon ?? 139, ), - zoom: 6, + zoom: 7, ), styleString: path, - minMaxZoomPreference: const MinMaxZoomPreference(0, 10), - onMapCreated: (controller) { + minMaxZoomPreference: MinMaxZoomPreference(0, maxZoomLevel.value), + onMapCreated: (controller) async { mapController.value = controller; }, onStyleLoadedCallback: () async { final controller = mapController.value!; - await controller.moveCamera(cameraUpdate); await [ addImageFromBuffer( controller, 'hypocenter', hypocenterIconRender, ), - for (final intensity in JmaIntensity.values) + for (final intensity in JmaIntensity.values) ...[ addImageFromBuffer( controller, 'intensity-${intensity.type}', intensityIconData.getOrNull(intensity)!, ), - for (final intensity in JmaIntensity.values) addImageFromBuffer( controller, 'intensity-${intensity.type}-fill', intensityIconFillData.getOrNull(intensity)!, ), + ], + for (final intensity in JmaLgIntensity.values) ...[ + addImageFromBuffer( + controller, + 'lpgm-intensity-${intensity.type}', + lpgmIntensityIconData.getOrNull(intensity)!, + ), + addImageFromBuffer( + controller, + 'lpgm-intensity-${intensity.type}-fill', + lpgmIntensityIconFillData.getOrNull(intensity)!, + ), + ], ].wait; + await initActions(currentActions.value); + await controller.moveCamera(cameraUpdate); + maxZoomLevel.value = 12; + }, + rotateGesturesEnabled: false, + tiltGesturesEnabled: false, + ), + ); + } - if (citiesItem != null) { - await _FillCityAction( - citiesItem: citiesItem, - ).init( - controller, - ); - } else if (regionsItem != null) { - await _FillRegionAction( - regionsItem: regionsItem, - ).init( - controller, - ); - } - if (stationsItem != null) { - await _StationAction( - stations: stationsItem, - colorModel: colorModel, - ).init( - controller, - ); + Future< + ( + List< + ({ + List codes, + TextColorModel color, + JmaIntensity intensity + })>?, + List< + ({ + List codes, + TextColorModel color, + JmaIntensity intensity + })>?, + Map< + JmaIntensity?, + List< + ({ + RegionIntensity item, + EarthquakeParameterStationItem? param + })>>?, + List< + ({ + List codes, + TextColorModel color, + JmaLgIntensity intensity + })>?, + Map< + JmaLgIntensity?, + List< + ({ + RegionIntensity item, + EarthquakeParameterStationItem? param + })>>? + )> _compute( + IntensityColorModel colorModel, + EarthquakeData earthquake, + EarthquakeParameter earthquakeParams, + ) { + return compute( + (arg) { + final earthquake = arg.$1; + final earthquakeParam = arg.$2; + final earthquakeParams = arg.$3; + final colorModel = arg.$4; + final regionsItem = earthquake.intensity?.regions + .groupListsBy((e) => e.maxInt) + .entries + .where((e) => e.key != null) + .map( + (e) => ( + color: colorModel.fromJmaIntensity(e.key!), + codes: e.value.map((e) => e.code.padLeft(3, '0')).toList(), + intensity: e.key!, + ), + ) + .toList(); + final citiesItem = earthquake.intensity?.cities + ?.groupListsBy((e) => e.maxInt) + .entries + .where((e) => e.key != null) + .map( + (e) => ( + color: colorModel.fromJmaIntensity(e.key!), + codes: e.value.map((e) => e.code).toList(), + intensity: e.key!, + ), + ) + .toList(); + + final stationsItem = () { + final stations = earthquake.intensity?.stations; + if (stations == null) { + return null; } + final allStations = earthquakeParams.regions + .map( + (region) => region.cities.map( + (city) => city.stations, + ), + ) + .flattened + .flattened; + final stationsParamMerged = stations.map( + (e) => ( + item: e, + param: allStations.firstWhereOrNull( + (element) => element.code == e.code, + ), + ), + ); + final grouped = + stationsParamMerged.groupListsBy((e) => e.item.maxInt); + return grouped; + }(); - await _HypocenterAction( - earthquake: earthquake, - ).init( - controller, + final regionsLpgmItem = earthquake.lgIntensity?.regions + .groupListsBy((e) => e.maxLgInt) + .entries + .where((e) => e.key != null) + .map( + (e) => ( + color: colorModel.fromJmaLgIntensity(e.key!), + codes: e.value.map((e) => e.code.padLeft(3, '0')).toList(), + intensity: e.key!, + ), + ) + .toList(); + final stationsLpgmItem = () { + final stations = earthquake.lgIntensity?.stations; + if (stations == null) { + return null; + } + final allStations = earthquakeParams.regions + .map( + (region) => region.cities.map( + (city) => city.stations, + ), + ) + .flattened + .flattened; + final stationsParamMerged = stations.map( + (e) => ( + item: e, + param: allStations.firstWhereOrNull( + (element) => element.code == e.code, + ), + ), ); - }, - rotateGesturesEnabled: false, - tiltGesturesEnabled: false, + final grouped = + stationsParamMerged.groupListsBy((e) => e.item.maxLgInt); + return grouped; + }(); + return ( + regionsItem, + citiesItem, + stationsItem, + regionsLpgmItem, + stationsLpgmItem, + ); + }, + ( + earthquake, + jmaMap, + earthquakeParams, + colorModel, ), ); } } -class _FillCityAction { +sealed class _Action { + Future init(map_libre.MaplibreMapController controller); + Future dispose(map_libre.MaplibreMapController controller); +} + +class _FillRegionAction extends _Action { + _FillRegionAction({ + required this.regionsItem, + }); + + final List<_RegionColorItem> regionsItem; + + static const name = 'areaForecastLocalE'; + static String getLineLayerName(JmaIntensity intensity) => + '$name-line-${intensity.type}-${intensity.hashCode}'; + static String getFillLayerName(JmaIntensity intensity) => + '$name-fill-${intensity.type}-${intensity.hashCode}'; + + @override + Future init(map_libre.MaplibreMapController controller) async { + await dispose(controller); + await [ + for (final item in regionsItem) ...[ + controller.addLayer( + 'eqmonitor_map', + getFillLayerName(item.intensity), + FillLayerProperties( + fillColor: item.color.background.toHexStringRGB(), + ), + sourceLayer: name, + belowLayerId: BaseLayer.areaForecastLocalEewLine.name, + filter: [ + 'in', + ['get', 'code'], + [ + 'literal', + item.codes, + ], + ], + ), + controller.addLayer( + 'eqmonitor_map', + getLineLayerName(item.intensity), + LineLayerProperties( + lineWidth: 0.4, + lineColor: item.color.foreground.toHexStringRGB(), + lineOpacity: 0.8, + ), + sourceLayer: name, + belowLayerId: BaseLayer.areaForecastLocalEewLine.name, + filter: [ + 'in', + ['get', 'regioncode'], + [ + 'literal', + item.codes, + ], + ], + ), + ], + ].wait; + } + + @override + Future dispose(map_libre.MaplibreMapController controller) => [ + for (final item in regionsItem) ...[ + controller.removeLayer( + getLineLayerName(item.intensity), + ), + controller.removeLayer( + getFillLayerName(item.intensity), + ), + ], + ].wait; +} + +class _FillCityAction extends _Action { _FillCityAction({ required this.citiesItem, }); final List<_RegionColorItem> citiesItem; + @override Future init(map_libre.MaplibreMapController controller) async { - /// 震度分布塗りつぶし (市区町村) await dispose(controller); for (final item in citiesItem) { await controller.addLayer( 'eqmonitor_map', - '$name-fill-${item.hashCode}', + getFillLayerName(item.intensity), FillLayerProperties( fillColor: item.color.background.toHexStringRGB(), ), @@ -342,7 +627,7 @@ class _FillCityAction { ); await controller.addLayer( 'eqmonitor_map', - '$name-line-${item.hashCode}', + getLineLayerName(item.intensity), LineLayerProperties( lineWidth: 0.4, lineColor: item.color.foreground.toHexStringRGB(), @@ -362,77 +647,28 @@ class _FillCityAction { } } - Future dispose(map_libre.MaplibreMapController controller) async => [ - for (final type in ['fill', 'line']) - for (final item in citiesItem) - controller.removeLayer( - '$name-$type-${item.hashCode}', - ), - ].wait; - - static const name = 'areaInformationCityQuake'; -} - -class _FillRegionAction { - _FillRegionAction({ - required this.regionsItem, - }); - - final List<_RegionColorItem>? regionsItem; - - Future init(map_libre.MaplibreMapController controller) async { - if (regionsItem != null) { - const name = 'areaForecastLocalE'; - for (final item in regionsItem!) { - await controller.removeLayer( - '$name-fill-${item.color.background.toHexStringRGB()}-' - '${item.intensity.type}', - ); - await controller.addLayer( - 'eqmonitor_map', - '$name-fill-${item.color.background.toHexStringRGB()}-' - '${item.intensity.type}', - FillLayerProperties( - fillColor: item.color.background.toHexStringRGB(), + @override + Future dispose(map_libre.MaplibreMapController controller) => [ + for (final item + in citiesItem.groupListsBy((e) => e.intensity).entries) ...[ + controller.removeLayer( + getLineLayerName(item.key), ), - sourceLayer: name, - belowLayerId: BaseLayer.areaForecastLocalEewLine.name, - filter: [ - 'in', - ['get', 'code'], - [ - 'literal', - item.codes, - ], - ], - ); - await controller.addLayer( - 'eqmonitor_map', - '$name-line-${item.color.foreground.toHexStringRGB()}${item.intensity.type}', - LineLayerProperties( - lineWidth: 0.4, - lineColor: item.color.foreground.toHexStringRGB(), - lineOpacity: 0.8, + controller.removeLayer( + getFillLayerName(item.key), ), - sourceLayer: name, - belowLayerId: BaseLayer.areaForecastLocalEewLine.name, - filter: [ - 'in', - ['get', 'regioncode'], - [ - 'literal', - item.codes, - ], - ], - ); - } - } - } + ], + ].wait; - Future dispose(map_libre.MaplibreMapController controller) async {} + static String getLineLayerName(JmaIntensity intensity) => + '$name-line-${intensity.type}${intensity.hashCode}'; + static String getFillLayerName(JmaIntensity intensity) => + '$name-fill-${intensity.type}${intensity.hashCode}'; + + static const name = 'areaInformationCityQuake'; } -class _StationAction { +class _StationAction extends _Action { _StationAction({ required this.stations, required this.colorModel, @@ -443,9 +679,11 @@ class _StationAction { stations; final IntensityColorModel colorModel; + @override Future init( map_libre.MaplibreMapController controller, ) async { + await dispose(controller); await controller.setSymbolIconAllowOverlap(true); await controller.setSymbolIconIgnorePlacement(true); await controller.addGeoJsonSource( @@ -483,7 +721,6 @@ class _StationAction { }, ); for (final intensity in JmaIntensity.values) { - await controller.removeLayer('station-intensity-${intensity.type}'); await controller.addLayer( 'station-intensity', 'station-intensity-${intensity.type}', @@ -543,23 +780,48 @@ class _StationAction { sourceLayer: 'station-intensity', ); } + + await controller.addSymbolLayer( + 'station-intensity', + 'station-intensity-symbol', + SymbolLayerProperties( + textField: ['get', 'name'], + textSize: 12, + textHaloColor: Colors.white.toHexStringRGB(), + textHaloWidth: 0.5, + textOffset: [ + map_libre.Expressions.literal, + [0, 2], + ], + ), + sourceLayer: 'station-intensity', + minzoom: 10, + ); } - Future dispose(map_libre.MaplibreMapController controller) => [ - controller.removeSource('station-intensity'), - controller.removeLayer('station-intensity'), - for (final intensity in JmaIntensity.values) - controller.removeLayer('station-intensity-${intensity.type}'), - ].wait; + @override + Future dispose(map_libre.MaplibreMapController controller) async { + // Layer + await [ + for (final intensity in JmaIntensity.values) + controller.removeLayer('station-intensity-${intensity.type}'), + for (final intensity in JmaIntensity.values) + controller.removeLayer('station-intensity-${intensity.type}-circle'), + controller.removeLayer('station-intensity-symbol'), + ].wait; + // Source + await controller.removeSource('station-intensity'); + } } -class _HypocenterAction { +class _HypocenterAction extends _Action { _HypocenterAction({ required this.earthquake, }); final EarthquakeData earthquake; + @override Future init(map_libre.MaplibreMapController controller) async { /// 震源地 final hypocenter = earthquake.earthquake?.hypocenter; @@ -616,76 +878,222 @@ class _HypocenterAction { } } - Future dispose(map_libre.MaplibreMapController controller) => ( - controller.removeSource('hypocenter'), - controller.removeLayer('hypocenter') - ).wait; + @override + Future dispose(map_libre.MaplibreMapController controller) async { + await controller.removeLayer('hypocenter'); + await controller.removeSource('hypocenter'); + } } -class _FillLpgmAction { - _FillLpgmAction({ - required this.regionsLpgmItem, +class _FillRegionLpgmIntensityAction extends _Action { + _FillRegionLpgmIntensityAction({ + required this.regionsItem, }); - final List<_RegionLpgmColorItem> regionsLpgmItem; + final List<_RegionLpgmColorItem> regionsItem; static const name = 'areaForecastLocalE'; + static String getLineLayerName(JmaLgIntensity intensity) => + '$name-LPGM-line-${intensity.type}-${intensity.hashCode}'; + static String getFillLayerName(JmaLgIntensity intensity) => + '$name-LPGM-fill-${intensity.type}-${intensity.hashCode}'; + @override Future init(map_libre.MaplibreMapController controller) async { - /// 震度分布塗りつぶし (市区町村) - for (final item in regionsLpgmItem) { - await controller.removeLayer( - '$name-fill-${item.color.background.toHexStringRGB()}-' - '${item.intensity.type}', - ); + await dispose(controller); + await [ + for (final item in regionsItem) ...[ + controller.addLayer( + 'eqmonitor_map', + getFillLayerName(item.intensity), + FillLayerProperties( + fillColor: item.color.background.toHexStringRGB(), + ), + sourceLayer: name, + belowLayerId: BaseLayer.areaForecastLocalEewLine.name, + filter: [ + 'in', + ['get', 'code'], + [ + 'literal', + item.codes, + ], + ], + ), + controller.addLayer( + 'eqmonitor_map', + getLineLayerName(item.intensity), + LineLayerProperties( + lineWidth: 0.4, + lineColor: item.color.foreground.toHexStringRGB(), + lineOpacity: 0.8, + ), + sourceLayer: name, + belowLayerId: BaseLayer.areaForecastLocalEewLine.name, + filter: [ + 'in', + ['get', 'regioncode'], + [ + 'literal', + item.codes, + ], + ], + ), + ], + ].wait; + } + + @override + Future dispose(map_libre.MaplibreMapController controller) => [ + for (final item in regionsItem) ...[ + controller.removeLayer( + getLineLayerName(item.intensity), + ), + controller.removeLayer( + getFillLayerName(item.intensity), + ), + ], + ].wait; +} + +class _StationIntensityLpgmAction extends _Action { + _StationIntensityLpgmAction({ + required this.stations, + required this.colorModel, + }); + + final Map> + stations; + final IntensityColorModel colorModel; + + @override + Future init( + map_libre.MaplibreMapController controller, + ) async { + await dispose(controller); + await controller.setSymbolIconAllowOverlap(true); + await controller.setSymbolIconIgnorePlacement(true); + await controller.addGeoJsonSource( + 'station-lpgm-intensity', + { + 'type': 'FeatureCollection', + 'features': stations.entries + .map((e) { + final color = colorModel.fromJmaLgIntensity(e.key!); + return e.value.map( + (point) => { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [ + point.param?.longitude ?? 0, + point.param?.latitude ?? 0, + ], + }, + 'properties': { + 'color': color.background.toHexStringRGB(), + 'lgIntensity': e.key?.type, + 'name': point.param?.name, + }, + }, + ); + }) + .flattened + .toList(), + }, + ); + for (final intensity in JmaLgIntensity.values) { await controller.addLayer( - 'eqmonitor_map', - '$name-fill-${item.color.background.toHexStringRGB()}-' - '${item.intensity.type}-lpgm', - FillLayerProperties( - fillColor: item.color.background.toHexStringRGB(), + 'station-lpgm-intensity', + 'station-lpgm-intensity-${intensity.type}', + SymbolLayerProperties( + iconImage: 'lpgm-intensity-${intensity.type}', + iconSize: [ + 'interpolate', + ['linear'], + ['zoom'], + 6, + 1, + 20, + 2, + ], + iconOpacity: [ + 'step', + ['zoom'], + 0.0, + 7, + 1.0, + ], + textAllowOverlap: true, + iconAllowOverlap: true, ), - sourceLayer: name, - belowLayerId: 'areaForecastLocalEew_line', filter: [ - 'in', - ['get', 'code'], - [ - 'literal', - item.codes, - ], + '==', + 'lgIntensity', + intensity.type, ], + sourceLayer: 'station-lpgm-intensity', + minzoom: 7, ); + await controller.addLayer( - 'eqmonitor_map', - '$name-line-${item.color.foreground.toHexStringRGB()}${item.intensity.type}', - LineLayerProperties( - lineWidth: 0.4, - lineColor: item.color.foreground.toHexStringRGB(), - lineOpacity: 0.8, + 'station-lpgm-intensity', + 'station-lpgm-intensity-${intensity.type}-circle', + SymbolLayerProperties( + iconImage: 'intensity-${intensity.type}-fill', + iconSize: [ + 'interpolate', + ['linear'], + ['zoom'], + 3, + 0.3, + 20, + 2, + ], + textAllowOverlap: true, + iconAllowOverlap: true, ), - sourceLayer: name, - belowLayerId: 'areaForecastLocalEew_line', + maxzoom: 7, filter: [ - 'in', - ['get', 'regioncode'], - [ - 'literal', - item.codes, - ], + '==', + 'lgIntensity', + intensity.type, ], + sourceLayer: 'station-lpgm-intensity', ); } + + await controller.addSymbolLayer( + 'station-lpgm-intensity', + 'station-lpgm-intensity-symbol', + SymbolLayerProperties( + textField: ['get', 'name'], + textSize: 12, + textHaloColor: Colors.white.toHexStringRGB(), + textHaloWidth: 0.5, + textOffset: [ + map_libre.Expressions.literal, + [0, 2], + ], + ), + sourceLayer: 'station-intensity', + minzoom: 10, + ); } + @override Future dispose(map_libre.MaplibreMapController controller) async { + // Layer await [ - for (final prefix in ['$name-fill', '$name-line']) - for (final item in regionsLpgmItem) - controller.removeLayer( - '$prefix-${item.color.background.toHexStringRGB()}-' - '${item.intensity.type}-lpgm', - ), + for (final intensity in JmaIntensity.values) + controller.removeLayer('station-lpgm-intensity-${intensity.type}'), + for (final intensity in JmaIntensity.values) + controller + .removeLayer('station-lpgm-intensity-${intensity.type}-circle'), + controller.removeLayer('station-lpgm-intensity-symbol'), ].wait; + // Source + await controller.removeSource('station-lpgm-intensity'); } } diff --git a/lib/feature/earthquake_history_details/screen/earthquake_history_details.dart b/lib/feature/earthquake_history_details/screen/earthquake_history_details.dart index c5a951b59..1643f13b1 100644 --- a/lib/feature/earthquake_history_details/screen/earthquake_history_details.dart +++ b/lib/feature/earthquake_history_details/screen/earthquake_history_details.dart @@ -12,6 +12,7 @@ import 'package:eqmonitor/feature/earthquake_history/model/state/earthquake_hist import 'package:eqmonitor/feature/earthquake_history_details/component/earthquake_map.dart'; import 'package:eqmonitor/feature/earthquake_history_details/component/prefecture_intensity.dart'; import 'package:eqmonitor/feature/earthquake_history_details/component/prefecture_lpgm_intensity.dart'; +import 'package:eqmonitor/feature/settings/children/config/earthquake_history/earthquake_history_config_page.dart'; import 'package:eqmonitor/gen/fonts.gen.dart'; import 'package:extensions/extensions.dart'; import 'package:flutter/material.dart'; @@ -66,7 +67,7 @@ class EarthquakeHistoryDetailsPage extends HookConsumerWidget { ) .join('・'), style: theme.textTheme.displayLarge!.copyWith( - color: colorScheme.error, + color: colorScheme.onError, fontWeight: FontWeight.bold, ), ), @@ -78,8 +79,23 @@ class EarthquakeHistoryDetailsPage extends HookConsumerWidget { hasAppBar: false, controller: sheetController, fab: [ + // layer controller + if (data.earthquake.intensity != null) + FloatingActionButton.small( + heroTag: 'earthquake_history_details_layer_fab', + tooltip: '地図の表示レイヤーを切り替える', + onPressed: () => showEarthquakeHistoryDetailConfigDialog( + context, + showCitySelector: + data.earthquake.intensity?.cities != null, + hasLpgmIntensity: data.earthquake.lgIntensity != null, + ), + elevation: 4, + child: const Icon(Icons.layers), + ), FloatingActionButton.small( heroTag: 'earthquake_history_details_fab', + tooltip: '表示領域を地図に合わせる', onPressed: () { if (navigateToHomeFunction.value != null) { navigateToHomeFunction.value!.call(); @@ -475,6 +491,6 @@ class _EarthquakeCommentWidget extends StatelessWidget { ), ); } - return Container(); + return const SizedBox.shrink(); } } diff --git a/lib/feature/home/component/render/intensity_renderer_widget.dart b/lib/feature/home/component/render/intensity_renderer_widget.dart index ddee46834..433fce433 100644 --- a/lib/feature/home/component/render/intensity_renderer_widget.dart +++ b/lib/feature/home/component/render/intensity_renderer_widget.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:eqapi_types/eqapi_types.dart'; import 'package:eqmonitor/core/component/intenisty/intensity_icon_type.dart'; import 'package:eqmonitor/core/component/intenisty/jma_intensity_icon.dart'; +import 'package:eqmonitor/core/component/intenisty/jma_lg_intensity_icon.dart'; import 'package:eqmonitor/core/provider/capture/intensity_icon_render.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -39,6 +40,24 @@ class IntensityRendererWidget extends HookConsumerWidget { }, type: type, ), + for (final intensity in JmaLgIntensity.values) + for (final type in [ + IntensityIconType.small, + IntensityIconType.smallWithoutText, + ]) + _LpgmIntensityRender( + intensity: intensity, + onRendered: (data) => switch (type) { + IntensityIconType.small => ref + .read(lpgmIntensityIconRenderProvider.notifier) + .onRendered(data, intensity), + IntensityIconType.smallWithoutText => ref + .read(lpgmIntensityIconFillRenderProvider.notifier) + .onRendered(data, intensity), + _ => null, + }, + type: type, + ), ], ); } @@ -84,3 +103,44 @@ class _IntensityRender extends HookConsumerWidget { ); } } + +class _LpgmIntensityRender extends HookConsumerWidget { + _LpgmIntensityRender({ + required this.onRendered, + required this.intensity, + required this.type, + }) : super() { + _key = GlobalObjectKey((intensity, type)); + } + + final void Function(Uint8List data) onRendered; + final JmaLgIntensity intensity; + final IntensityIconType type; + + late final GlobalObjectKey _key; + + @override + Widget build(BuildContext context, WidgetRef ref) { + useEffect( + () { + WidgetsBinding.instance.endOfFrame.then((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + final boundary = + _key.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final image = await boundary.toImage(); + final byte = await image.toByteData(format: ImageByteFormat.png); + onRendered.call(byte!.buffer.asUint8List()); + }); + return null; + }, + [], + ); + return RepaintBoundary( + key: _key, + child: JmaLgIntensityIcon( + intensity: intensity, + type: type, + ), + ); + } +} diff --git a/lib/feature/home/features/map/view/main_map_view.dart b/lib/feature/home/features/map/view/main_map_view.dart index fe4470cd1..6ba81c95b 100644 --- a/lib/feature/home/features/map/view/main_map_view.dart +++ b/lib/feature/home/features/map/view/main_map_view.dart @@ -79,6 +79,17 @@ class MainMapView extends HookConsumerWidget { !images.intenistyIcon.isAllRendered() || !images.intensityIconFill.isAllRendered() || !hasTravelTimeDepthMapValue) { + log('stylePath.value: ${stylePath.value != null}'); + log('images.hypocenterIcon: ${images.hypocenterIcon != null}'); + log( + 'images.hypocenterLowPreciseIcon: ${images.hypocenterLowPreciseIcon != null}', + ); + log('images.intenistyIcon: ${images.intenistyIcon.isAllRendered()}'); + log( + 'images.intensityIconFill: ${images.intensityIconFill.isAllRendered()}', + ); + log('hasTravelTimeDepthMapValue: $hasTravelTimeDepthMapValue'); + return const Scaffold( body: Center( child: CircularProgressIndicator.adaptive(), diff --git a/lib/feature/settings/children/config/earthquake_history/earthquake_history_config_page.dart b/lib/feature/settings/children/config/earthquake_history/earthquake_history_config_page.dart index a45acc4ba..1c8405ad5 100644 --- a/lib/feature/settings/children/config/earthquake_history/earthquake_history_config_page.dart +++ b/lib/feature/settings/children/config/earthquake_history/earthquake_history_config_page.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:eqmonitor/core/provider/config/earthquake_history/earthquake_history_config_provider.dart'; import 'package:eqmonitor/core/provider/config/earthquake_history/model/earthquake_history_config_model.dart'; import 'package:eqmonitor/feature/settings/component/settings_section_header.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -20,13 +23,11 @@ class EarthquakeHistoryConfigPage extends StatelessWidget { text: '地震履歴一覧', ), _EarthquakeHistoryListConfigWidget(), - if (kDebugMode) ...[ - Divider(), - SettingsSectionHeader( - text: '地震履歴詳細', - ), - _EarthquakeHistoryDetailConfigWidget(), - ], + Divider(), + SettingsSectionHeader( + text: '地震履歴詳細', + ), + _EarthquakeHistoryDetailConfigWidget(), ], ), ); @@ -99,10 +100,10 @@ class _EarthquakeHistoryDetailConfigWidget extends ConsumerWidget { children: [ ListTile( title: const Text('震度の表示方法'), - trailing: Text(state.intensityDisplayMode.name), + trailing: Text(state.intensityFillMode.name), onTap: () async { //bottomSheetで選択する - final result = await showModalBottomSheet( + final result = await showModalBottomSheet( context: context, clipBehavior: Clip.antiAlias, builder: (context) { @@ -111,11 +112,11 @@ class _EarthquakeHistoryDetailConfigWidget extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ sheetBar, - for (final mode in IntensityDisplayMode.values) + for (final mode in IntensityFillMode.values) RadioListTile.adaptive( title: Text(mode.name), value: mode, - groupValue: state.intensityDisplayMode, + groupValue: state.intensityFillMode, onChanged: (value) => Navigator.pop(context, value), ), ], @@ -124,11 +125,11 @@ class _EarthquakeHistoryDetailConfigWidget extends ConsumerWidget { }, ); if (result != null) { - ref + await ref .read(earthquakeHistoryConfigProvider.notifier) .updateDetailConfig( state.copyWith( - intensityDisplayMode: result, + intensityFillMode: result, ), ); } @@ -139,11 +140,250 @@ class _EarthquakeHistoryDetailConfigWidget extends ConsumerWidget { } } -extension _IntensityDisplayModeEx on IntensityDisplayMode { +extension _IntensityDisplayModeEx on IntensityFillMode { String get name => switch (this) { - IntensityDisplayMode.icon => '震度アイコン', - IntensityDisplayMode.fillCity => '市区町村を塗りつぶし', - IntensityDisplayMode.fillPrefecture => '都道府県を塗りつぶし', - IntensityDisplayMode.iconAndFillPrefecture => '震度アイコンと市区町村を塗りつぶし', + IntensityFillMode.fillCity => '市区町村', + IntensityFillMode.fillRegion => '細分化地域', + IntensityFillMode.none => '塗りつぶしなし', }; } + +Future showEarthquakeHistoryDetailConfigDialog( + BuildContext context, { + required bool showCitySelector, + required bool hasLpgmIntensity, +}) async { + final result = await showModalBottomSheet( + context: context, + clipBehavior: Clip.antiAlias, + builder: (context) => _EarthquakeHistoryDetailConfigBody( + showCitySelector: showCitySelector, + hasLpgmIntensity: hasLpgmIntensity, + ), + ); + return result; +} + +class _EarthquakeHistoryDetailConfigBody extends HookConsumerWidget { + const _EarthquakeHistoryDetailConfigBody({ + required this.showCitySelector, + required this.hasLpgmIntensity, + }); + final bool showCitySelector; + final bool hasLpgmIntensity; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showingLpgmIntensity = ref.watch( + earthquakeHistoryConfigProvider + .select((value) => value.detail.showingLpgmIntensity), + ); + final theme = Theme.of(context); + final sheetBar = Container( + margin: const EdgeInsets.symmetric(vertical: 8), + width: 36, + height: 4, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: theme.colorScheme.onBackground, + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 12), + ], + ), + ); + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: sheetBar), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SettingsSectionHeader( + text: showingLpgmIntensity ? '長周期地震動階級の塗りつぶし' : '震度の塗りつぶし', + ), + ), + Center( + child: _IntensityFillModeSegmentedControl( + showCitySelector: showCitySelector && !showingLpgmIntensity, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SettingsSectionHeader( + text: showingLpgmIntensity ? '長周期地震動階級のアイコン' : '震度のアイコン', + ), + ), + const _IntensityStationIconModeSegmentedButton(), + if (hasLpgmIntensity) ...[ + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: SettingsSectionHeader( + text: '震度・長周期地震動階級の表示切り替え', + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: _IntensityModeSegmentedControl( + selected: showingLpgmIntensity + ? _IntensityMode.lpgm + : _IntensityMode.intensity, + onSelected: (value) => ref + .read(earthquakeHistoryConfigProvider.notifier) + .updateDetailConfig( + ref + .watch(earthquakeHistoryConfigProvider) + .detail + .copyWith( + showingLpgmIntensity: value == _IntensityMode.lpgm, + ), + ), + ), + ), + ], + ], + ), + ); + } +} + +class _IntensityFillModeSegmentedControl extends ConsumerStatefulWidget { + const _IntensityFillModeSegmentedControl({ + required this.showCitySelector, + }); + final bool showCitySelector; + + @override + ConsumerState createState() => + __IntensityFillModeSegmentedControlState(); +} + +class __IntensityFillModeSegmentedControlState + extends ConsumerState<_IntensityFillModeSegmentedControl> { + @override + Widget build(BuildContext context) { + final state = ref + .watch(earthquakeHistoryConfigProvider.select((value) => value.detail)); + final showCitySelector = widget.showCitySelector; + const choices = IntensityFillMode.values; + + if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { + return CupertinoSlidingSegmentedControl( + groupValue: state.intensityFillMode, + padding: const EdgeInsets.all(4), + children: { + for (final mode in choices) + mode: Text( + mode.name, + style: showCitySelector || mode != IntensityFillMode.fillCity + ? null + : const TextStyle(color: Colors.grey), + ), + }, + onValueChanged: (value) { + if (value != null) { + final isAllowed = + showCitySelector || value != IntensityFillMode.fillCity; + if (!isAllowed) { + // treeの再構築をトリガーすることで、 + // 選択できない選択肢が選択されている状態を解除する + // (CupertinoSlidingSegmentedControlが内部的に管理している選択状況を、 + // groupValueから再取得させることで、想定された状態に戻す) + setState(() {}); + return; + } + ref + .read(earthquakeHistoryConfigProvider.notifier) + .updateDetailConfig( + state.copyWith(intensityFillMode: value), + ); + } + }, + ); + } else { + return SegmentedButton( + selected: {state.intensityFillMode}, + onSelectionChanged: (p0) => ref + .read(earthquakeHistoryConfigProvider.notifier) + .updateDetailConfig( + state.copyWith(intensityFillMode: p0.first), + ), + segments: [ + for (final mode in choices) + ButtonSegment( + label: Text(mode.name), + value: mode, + enabled: showCitySelector || mode != IntensityFillMode.fillCity, + ), + ], + ); + } + } +} + +enum _IntensityMode { + intensity('震度'), + lpgm('長周期地震動階級'), + ; + + const _IntensityMode(this.name); + final String name; +} + +class _IntensityModeSegmentedControl extends StatelessWidget { + const _IntensityModeSegmentedControl({ + required this.onSelected, + required this.selected, + }); + final void Function(_IntensityMode) onSelected; + final _IntensityMode selected; + + @override + Widget build(BuildContext context) { + if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { + return CupertinoSlidingSegmentedControl( + groupValue: selected, + children: { + for (final mode in _IntensityMode.values) mode: Text(mode.name), + }, + onValueChanged: (value) { + if (value != null) { + onSelected(value); + } + }, + ); + } else { + return SegmentedButton( + selected: {selected}, + onSelectionChanged: (p0) => onSelected(p0.first), + segments: [ + for (final mode in _IntensityMode.values) + ButtonSegment( + label: Text(mode.name), + value: mode, + ), + ], + ); + } + } +} + +class _IntensityStationIconModeSegmentedButton extends ConsumerWidget { + const _IntensityStationIconModeSegmentedButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref + .watch(earthquakeHistoryConfigProvider.select((value) => value.detail)); + final isShowingLpgmIntensity = state.showingLpgmIntensity; + return SwitchListTile.adaptive( + title: Text(isShowingLpgmIntensity ? '長周期地震動階級アイコンを表示する' : '震度アイコンを表示する'), + value: state.showIntensityIcon, + onChanged: (value) => + ref.read(earthquakeHistoryConfigProvider.notifier).updateDetailConfig( + state.copyWith(showIntensityIcon: value), + ), + ); + } +}