import 'dart:convert'; import '../../core/config/env.dart'; import '../../core/region/region_config.dart'; /// Which base map the app is showing, and therefore which credits are owed. enum BaseMapKind { /// A real vector base map from [Env.mapStyleUrl]. Carries OpenStreetMap data, /// so the ODbL credit is mandatory. configured, /// The bundled fallback: a flat background and the region outline, drawn from /// the region config. Contains no third-party map data, so crediting /// OpenStreetMap here would be a false attribution. offlineFallback, } /// Resolves the MapLibre style the app should load. /// /// With no `MAP_STYLE_URL` configured the app must still run — offline, in /// tests, and on a fresh clone with no credentials — so it falls back to a /// style generated from the region's own bounding box. class MapStyle { const MapStyle({required this.kind, required this.styleString}); factory MapStyle.forRegion(RegionConfig region) { if (Env.hasMapStyle) { return MapStyle( kind: BaseMapKind.configured, styleString: Env.mapStyleUrl, ); } return MapStyle( kind: BaseMapKind.offlineFallback, styleString: buildFallbackStyle(region), ); } final BaseMapKind kind; /// Either a style URL or an inline MapLibre style document; the map widget /// accepts both. final String styleString; /// Attribution ids owed by the base map itself, as they appear in the region /// config. The radar and forecast layers add their own on top of these. Set get attributionIds => switch (kind) { BaseMapKind.configured => const {'osm'}, BaseMapKind.offlineFallback => const {}, }; /// Builds a self-contained MapLibre style: a flat background plus the region /// bounding box, with no network sources at all. /// /// The outline is deliberately the *bounding box*, not the administrative /// boundary. A real boundary would need a third-party dataset with its own /// licence, and would make the fallback look like a finished map when it is /// a placeholder. static String buildFallbackStyle(RegionConfig region) { final b = region.bounds; final ring = >[ [b.west, b.south], [b.east, b.south], [b.east, b.north], [b.west, b.north], [b.west, b.south], ]; final style = { 'version': 8, 'name': 'Nuvolari offline fallback', 'sources': { 'region-extent': { 'type': 'geojson', 'data': { 'type': 'Feature', 'properties': {'id': region.id}, 'geometry': { 'type': 'Polygon', 'coordinates': [ring], }, }, }, }, 'layers': [ { 'id': 'background', 'type': 'background', 'paint': {'background-color': '#0B1720'}, }, { 'id': 'region-extent-fill', 'type': 'fill', 'source': 'region-extent', 'paint': { 'fill-color': '#13303F', 'fill-opacity': 0.7, }, }, { 'id': 'region-extent-outline', 'type': 'line', 'source': 'region-extent', 'paint': { 'line-color': '#4FA3D1', 'line-width': 1.5, }, }, ], }; return jsonEncode(style); } }