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 { /// The default: free OpenStreetMap vector tiles from OpenFreeMap. No API key, /// no registration, no request limits. Owes the OpenMapTiles and /// OpenStreetMap credits. openFreeMap, /// A style URL supplied through `MAP_STYLE_URL`. Assumed to be OSM-derived, /// since every practical alternative is, so it owes the same credits. custom, /// 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. class MapStyle { const MapStyle({required this.kind, required this.styleString}); /// OpenFreeMap's Positron style. /// /// Chosen over Liberty and Bright because a radar overlay has to be the /// loudest thing on screen. Positron is a desaturated grey base designed for /// exactly this — the precipitation colours read against it instead of /// competing with road casings and landuse fills. /// /// Free, unmetered and key-less: . static const String openFreeMapStyleUrl = 'https://tiles.openfreemap.org/styles/positron'; factory MapStyle.forRegion(RegionConfig region) { if (Env.wantsOfflineStyle) { return MapStyle( kind: BaseMapKind.offlineFallback, styleString: buildFallbackStyle(region), ); } if (Env.hasCustomMapStyle) { return MapStyle(kind: BaseMapKind.custom, styleString: Env.mapStyleUrl); } return const MapStyle( kind: BaseMapKind.openFreeMap, styleString: openFreeMapStyleUrl, ); } 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 layer adds its own on top of these. /// /// OpenFreeMap's own credit is optional by their terms, so it lives on the /// Sources screen rather than in the always-visible bar; the OpenMapTiles and /// OpenStreetMap credits are mandatory and go in the bar. Set get attributionIds => switch (kind) { BaseMapKind.openFreeMap || BaseMapKind.custom => const {'openmaptiles', '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); } }