Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import 'package:dpip/core/settings/experimental_settings.dart';
import 'package:dpip/core/settings/locale_controller.dart';
import 'package:dpip/core/settings/map_layer_order_controller.dart';
import 'package:dpip/core/settings/map_layer_visibility_controller.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/core/settings/onboarding_store.dart';
import 'package:dpip/core/astro/tle_store.dart';
import 'package:dpip/core/settings/setting_keys.dart';
Expand Down Expand Up @@ -226,6 +227,7 @@ Future<void> bootstrap() async {
final defaultMapLayer = DefaultMapLayerController(settings);
final mapLayerOrder = MapLayerOrderController(settings);
final mapLayerVisibility = MapLayerVisibilityController(settings);
final mapReferenceOutline = MapReferenceOutlineController(settings);
final cache = await cacheFuture;
final dio = createDio(etagCache: cache?.etag, usage: cache?.usage);
final endpointHealth = EndpointHealthMonitor();
Expand Down Expand Up @@ -378,6 +380,7 @@ Future<void> bootstrap() async {
defaultMapLayer: defaultMapLayer,
mapLayerOrder: mapLayerOrder,
mapLayerVisibility: mapLayerVisibility,
mapReferenceOutline: mapReferenceOutline,
meshtastic: meshtastic,
meshLink: meshLink,
meshAlerts: meshAlerts,
Expand Down
4 changes: 4 additions & 0 deletions lib/core/di/core_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:dpip/core/settings/experimental_settings.dart';
import 'package:dpip/core/settings/locale_controller.dart';
import 'package:dpip/core/settings/map_layer_order_controller.dart';
import 'package:dpip/core/settings/map_layer_visibility_controller.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/core/settings/onboarding_store.dart';
import 'package:dpip/core/settings/region_store.dart';
import 'package:dpip/core/settings/color_vision_controller.dart';
Expand Down Expand Up @@ -60,6 +61,9 @@ List<SingleChildWidget> coreProviders(SharedDeps deps) => [
ChangeNotifierProvider<MapLayerVisibilityController>.value(
value: deps.mapLayerVisibility,
),
ChangeNotifierProvider<MapReferenceOutlineController>.value(
value: deps.mapReferenceOutline,
),
Provider<SettingsStore>.value(value: deps.settings),
Provider<AppDatabase>.value(value: deps.database),
Provider<TleStore>.value(value: deps.tleStore),
Expand Down
6 changes: 6 additions & 0 deletions lib/core/di/shared_deps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'package:dpip/core/settings/experimental_settings.dart';
import 'package:dpip/core/settings/locale_controller.dart';
import 'package:dpip/core/settings/map_layer_order_controller.dart';
import 'package:dpip/core/settings/map_layer_visibility_controller.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/core/settings/onboarding_store.dart';
import 'package:dpip/core/settings/settings_store.dart';
import 'package:dpip/core/settings/region_store.dart';
Expand Down Expand Up @@ -70,6 +71,7 @@ class SharedDeps {
required this.defaultMapLayer,
required this.mapLayerOrder,
required this.mapLayerVisibility,
required this.mapReferenceOutline,
required this.meshtastic,
required this.meshLink,
required this.meshAlerts,
Expand Down Expand Up @@ -158,6 +160,10 @@ class SharedDeps {
/// The map layers the user hid (also provided).
final MapLayerVisibilityController mapLayerVisibility;

/// The shared reference-chrome toggles every raster layer offers — admin
/// borders and scan range (also provided).
final MapReferenceOutlineController mapReferenceOutline;

/// LoRa mesh (Meshtastic) over BLE — off-grid emergency messaging.
final MeshtasticService meshtastic;

Expand Down
69 changes: 69 additions & 0 deletions lib/core/settings/map_reference_outline_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// Persisted, shared state for a raster layer's reference chrome — the
/// admin-border outlines (國界 / 縣市 / 鄉鎮) and the scan-range coverage
/// outline every weather raster can redraw over itself.
library;

import 'dart:async';

import 'package:dpip/core/settings/setting_keys.dart';
import 'package:dpip/core/settings/settings_store.dart';
import 'package:flutter/foundation.dart';

/// One shared preference per toggle, used by every raster layer that offers
/// it (radar, QPESUMS, satellite, wind forecast) — see `AdminOutlineChrome`
/// and `ScanRangeOverlayChrome`. A layer instance is rebuilt per map surface,
/// so without one shared, persisted source of truth each rebuild forgot the
/// choice and every layer kept its own copy, out of sync with the others.
///
/// All four ship on: a blank area reads as "not observed", not "no weather",
/// and an unidentified county is one a reader cannot act on.
class MapReferenceOutlineController extends ChangeNotifier {
MapReferenceOutlineController(this._settings)
: _global = _settings.getBool(SettingKeys.mapShowGlobalOutline) ?? true,
_county = _settings.getBool(SettingKeys.mapShowCountyOutline) ?? true,
_town = _settings.getBool(SettingKeys.mapShowTownOutline) ?? true,
_scanRange = _settings.getBool(SettingKeys.mapShowScanRange) ?? true;

final SettingsStore _settings;

bool _global;
bool _county;
bool _town;
bool _scanRange;

bool get showGlobalOutline => _global;
bool get showCountyOutline => _county;
bool get showTownOutline => _town;
bool get showScanRange => _scanRange;

void setShowGlobalOutline(bool value) {
if (!_changed(_global, value)) return;
_global = value;
_persist(SettingKeys.mapShowGlobalOutline, value);
}

void setShowCountyOutline(bool value) {
if (!_changed(_county, value)) return;
_county = value;
_persist(SettingKeys.mapShowCountyOutline, value);
}

void setShowTownOutline(bool value) {
if (!_changed(_town, value)) return;
_town = value;
_persist(SettingKeys.mapShowTownOutline, value);
}

void setShowScanRange(bool value) {
if (!_changed(_scanRange, value)) return;
_scanRange = value;
_persist(SettingKeys.mapShowScanRange, value);
}

bool _changed(bool current, bool next) => current != next;

void _persist(SettingKey<bool> key, bool value) {
unawaited(_settings.setBool(key, value));
notifyListeners();
}
}
41 changes: 41 additions & 0 deletions lib/core/settings/setting_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,47 @@ abstract final class SettingKeys {
static const SettingKey<List<String>> mapLayerHiddenIds =
SettingKey<List<String>>._('map.layerHiddenIds');

/// Whether the OSM street/building overlay is on (absent = off, except the
/// disaster-prevention layer's own forced default). See
/// `GsiOverlayController`.
static const SettingKey<bool> mapGsiEnabled = SettingKey<bool>._(
'map.gsiEnabled',
);

/// Enabled OSM overlay sub-layer groups ([GsiLayerGroup] names; absent =
/// every group except the ones [gsiDefaultDisabledGroups] starts off). See
/// `GsiOverlayController`.
static const SettingKey<List<String>> mapGsiEnabledGroups =
SettingKey<List<String>>._('map.gsiEnabledGroups');

/// Whether the base map's terrain-relief hillshade is shown (absent =
/// true). See `MapScaffold`.
static const SettingKey<bool> mapShowTerrain = SettingKey<bool>._(
'map.showTerrain',
);

/// Whether the base map's township-name labels are shown (absent = true).
/// See `MapScaffold`.
static const SettingKey<bool> mapShowTownLabels = SettingKey<bool>._(
'map.showTownLabels',
);

/// The four reference-chrome toggles a raster layer (radar, QPESUMS,
/// satellite, wind forecast) redraws over itself — one shared preference
/// for every layer, all absent = true. See `MapReferenceOutlineController`.
static const SettingKey<bool> mapShowGlobalOutline = SettingKey<bool>._(
'map.showGlobalOutline',
);
static const SettingKey<bool> mapShowCountyOutline = SettingKey<bool>._(
'map.showCountyOutline',
);
static const SettingKey<bool> mapShowTownOutline = SettingKey<bool>._(
'map.showTownOutline',
);
static const SettingKey<bool> mapShowScanRange = SettingKey<bool>._(
'map.showScanRange',
);

/// Saved Home township codes (ordered list). See `RegionStore`.
static const SettingKey<List<String>> savedRegionCodes =
SettingKey<List<String>>._('home.savedRegionCodes');
Expand Down
66 changes: 37 additions & 29 deletions lib/features/map/presentation/layers/admin_outline_chrome.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:async';
import 'package:dpip/app/theme/app_spacing.dart';
import 'package:dpip/core/a11y/color_vision.dart';
import 'package:dpip/core/logging/log.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/l10n/gen/app_localizations.dart';
import 'package:dpip/shared/color_hex.dart';
import 'package:dpip/shared/map/admin_outline.dart';
Expand All @@ -28,22 +29,27 @@ import 'package:maplibre_gl/maplibre_gl.dart';
/// consumer; only the ids come from the base style's own source, so a map
/// showing two rasters at once draws two independent border sets.
mixin AdminOutlineChrome on RasterTimelineLayer {
/// The persisted, shared toggle state — one instance for every raster layer
/// that mixes this in, so a choice made on one layer's menu is already in
/// effect (and already saved) on every other one, live.
MapReferenceOutlineController get referenceOutline;

/// Whether 國界 (world country borders) are redrawn above the raster.
///
/// On by default: the base style's own borders are covered by the raster
/// everywhere the weather shows, and the world frame is the reference a
/// reader keeps when the map zooms past a county mesh — so every weather
/// layer ships with it, and a reader who finds it noise can turn it off.
final ValueNotifier<bool> showGlobalOutline = ValueNotifier(true);
bool get showGlobalOutline => referenceOutline.showGlobalOutline;

/// Whether 縣市 borders are redrawn above the raster.
final ValueNotifier<bool> showCountyOutline = ValueNotifier(true);
bool get showCountyOutline => referenceOutline.showCountyOutline;

/// Whether 鄉鎮 borders are redrawn above the raster. Separate from the
/// county toggle: they are different questions ("which city" vs "which
/// township"), and the finer mesh is the first thing a reader wants gone when
/// studying a single cell.
final ValueNotifier<bool> showTownOutline = ValueNotifier(true);
bool get showTownOutline => referenceOutline.showTownOutline;

/// The live map controller, set on attach and cleared on detach. Shared with
/// chrome mixins layered above this one, so it is protected rather than
Expand All @@ -53,49 +59,47 @@ mixin AdminOutlineChrome on RasterTimelineLayer {
final Set<AdminBoundary> _boundariesShown = {};

/// All admin-chrome listenables, for a legend that follows the toggles.
Listenable get adminChromeListenable =>
Listenable.merge([showGlobalOutline, showCountyOutline, showTownOutline]);
Listenable get adminChromeListenable => referenceOutline;

/// Which boundary sets should currently be on the map.
Set<AdminBoundary> get _wantedBoundaries => {
// Inserted finest → coarsest so the later, coarser frame wins where two
// run together along a coastline.
if (showTownOutline.value) AdminBoundary.town,
if (showCountyOutline.value) AdminBoundary.county,
if (showGlobalOutline.value) AdminBoundary.global,
if (showTownOutline) AdminBoundary.town,
if (showCountyOutline) AdminBoundary.county,
if (showGlobalOutline) AdminBoundary.global,
};

/// Turns the 國界 borders on/off, applying it to a live map immediately.
void setShowGlobalOutline(bool value) {
if (showGlobalOutline.value == value) return;
showGlobalOutline.value = value;
unawaited(_syncBoundaries());
}
/// Turns the 國界 borders on/off, applying it to every attached layer
/// sharing [referenceOutline] immediately.
void setShowGlobalOutline(bool value) =>
referenceOutline.setShowGlobalOutline(value);

/// Turns the 縣市 borders on/off, applying it to a live map immediately.
void setShowCountyOutline(bool value) {
if (showCountyOutline.value == value) return;
showCountyOutline.value = value;
unawaited(_syncBoundaries());
}
/// Turns the 縣市 borders on/off, applying it to every attached layer
/// sharing [referenceOutline] immediately.
void setShowCountyOutline(bool value) =>
referenceOutline.setShowCountyOutline(value);

/// Turns the 鄉鎮 borders on/off, applying it to a live map immediately.
void setShowTownOutline(bool value) {
if (showTownOutline.value == value) return;
showTownOutline.value = value;
unawaited(_syncBoundaries());
}
/// Turns the 鄉鎮 borders on/off, applying it to every attached layer
/// sharing [referenceOutline] immediately.
void setShowTownOutline(bool value) =>
referenceOutline.setShowTownOutline(value);

@override
Future<void> onAttached(MapLibreMapController controller) async {
super.onAttached(controller);
this.controller = controller;
// A shared preference can change from another attached layer's menu, not
// just this one's — resync whenever it does, for as long as this layer is
// actually on the map to resync.
referenceOutline.addListener(_onReferenceOutlineChanged);
await _syncBoundaries();
}

@override
Future<void> onDetached(MapLibreMapController controller) async {
super.onDetached(controller);
referenceOutline.removeListener(_onReferenceOutlineChanged);
this.controller = null;
// Only undo what was actually added — tearing down an overlay that was
// never mounted issues removals the map never asked for.
Expand All @@ -105,6 +109,10 @@ mixin AdminOutlineChrome on RasterTimelineLayer {
}
}

void _onReferenceOutlineChanged() {
unawaited(_syncBoundaries());
}

@override
void onStyleReset() {
// The style reload dropped every runtime layer, these included; forget them
Expand Down Expand Up @@ -150,7 +158,7 @@ mixin AdminOutlineChrome on RasterTimelineLayer {
List<SymbolLegendItem> adminLegendItems(BuildContext context) {
final l10n = AppLocalizations.of(context);
return [
if (showGlobalOutline.value)
if (showGlobalOutline)
SymbolLegendItem(
swatch: LineSwatch(
color: colorFromHexRgb(AdminOutline.lineColor)!,
Expand All @@ -162,7 +170,7 @@ mixin AdminOutlineChrome on RasterTimelineLayer {
),
label: l10n.radarGlobalOutline,
),
if (showCountyOutline.value)
if (showCountyOutline)
SymbolLegendItem(
swatch: LineSwatch(
color: colorFromHexRgb(AdminOutline.lineColor)!,
Expand All @@ -174,7 +182,7 @@ mixin AdminOutlineChrome on RasterTimelineLayer {
),
label: l10n.radarCountyOutline,
),
if (showTownOutline.value)
if (showTownOutline)
SymbolLegendItem(
swatch: LineSwatch(
color: colorFromHexRgb(AdminOutline.lineColor)!,
Expand Down
6 changes: 5 additions & 1 deletion lib/features/map/presentation/layers/qpesums_layer.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:dpip/core/a11y/color_vision.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/features/map/presentation/layers/admin_outline_chrome.dart';
import 'package:dpip/features/map/presentation/layers/qpesums_scan_range.dart';
import 'package:dpip/features/map/presentation/layers/scan_range_overlay_chrome.dart';
Expand All @@ -25,7 +26,10 @@ import 'package:flutter/material.dart';
/// union of range circles.
class QpesumsMapLayer extends RasterTimelineLayer
with AdminOutlineChrome, ScanRangeOverlayChrome {
QpesumsMapLayer(QpesumsRepository super.repository);
QpesumsMapLayer(QpesumsRepository super.repository, this.referenceOutline);

@override
final MapReferenceOutlineController referenceOutline;

/// Distinct from radar's ids: both layers can be on the map at once, and each
/// draws its own outline instead of clashing over one source/layer pair.
Expand Down
6 changes: 5 additions & 1 deletion lib/features/map/presentation/layers/radar_layer.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:dpip/core/a11y/color_vision.dart';
import 'package:dpip/core/settings/map_reference_outline_controller.dart';
import 'package:dpip/features/map/presentation/layers/admin_outline_chrome.dart';
import 'package:dpip/features/map/presentation/layers/radar_scan_range.dart';
import 'package:dpip/features/map/presentation/layers/scan_range_overlay_chrome.dart';
Expand All @@ -24,7 +25,10 @@ import 'package:flutter/material.dart';
/// uninterrupted raster.
class RadarMapLayer extends RasterTimelineLayer
with AdminOutlineChrome, ScanRangeOverlayChrome {
RadarMapLayer(RadarRepository super.repository);
RadarMapLayer(RadarRepository super.repository, this.referenceOutline);

@override
final MapReferenceOutlineController referenceOutline;

/// The radar composite's own ids — the default geometry and layer naming.
@override
Expand Down
Loading
Loading