diff --git a/assets/sprites.json b/assets/sprites.json deleted file mode 100644 index 8236e73b8..000000000 --- a/assets/sprites.json +++ /dev/null @@ -1 +0,0 @@ -{"intensity-7-dark":{"x":288,"y":72,"width":64,"height":64,"pixelRatio":1},"intensity-6-dark":{"x":288,"y":136,"width":64,"height":64,"pixelRatio":1},"lightning-1-10":{"x":288,"y":200,"width":64,"height":64,"pixelRatio":1},"gps":{"x":288,"y":0,"width":72,"height":72,"pixelRatio":1},"lightning-1-5":{"x":0,"y":288,"width":64,"height":64,"pixelRatio":1},"wind-high":{"x":64,"y":288,"width":64,"height":64,"pixelRatio":1},"intensity-5":{"x":128,"y":288,"width":64,"height":64,"pixelRatio":1},"intensity-4":{"x":192,"y":288,"width":64,"height":64,"pixelRatio":1},"intensity-6":{"x":256,"y":288,"width":64,"height":64,"pixelRatio":1},"lightning-0-30":{"x":360,"y":0,"width":64,"height":64,"pixelRatio":1},"intensity-7":{"x":360,"y":64,"width":64,"height":64,"pixelRatio":1},"intensity-1-dark":{"x":360,"y":128,"width":64,"height":64,"pixelRatio":1},"cross-8":{"x":0,"y":0,"width":96,"height":96,"pixelRatio":1},"intensity-3":{"x":360,"y":192,"width":64,"height":64,"pixelRatio":1},"intensity-2":{"x":360,"y":256,"width":64,"height":64,"pixelRatio":1},"cross-9":{"x":96,"y":0,"width":96,"height":96,"pixelRatio":1},"lightning-1-60":{"x":0,"y":352,"width":64,"height":64,"pixelRatio":1},"lightning-0-5":{"x":64,"y":352,"width":64,"height":64,"pixelRatio":1},"wind-low":{"x":128,"y":352,"width":64,"height":64,"pixelRatio":1},"intensity-1":{"x":192,"y":352,"width":64,"height":64,"pixelRatio":1},"cross-7":{"x":0,"y":96,"width":96,"height":96,"pixelRatio":1},"cross-6":{"x":96,"y":96,"width":96,"height":96,"pixelRatio":1},"wind-5":{"x":256,"y":352,"width":64,"height":64,"pixelRatio":1},"cross-4":{"x":192,"y":0,"width":96,"height":96,"pixelRatio":1},"lightning-0-10":{"x":320,"y":352,"width":64,"height":64,"pixelRatio":1},"cross-5":{"x":192,"y":96,"width":96,"height":96,"pixelRatio":1},"wind-middle":{"x":424,"y":0,"width":64,"height":64,"pixelRatio":1},"wind-4":{"x":424,"y":64,"width":64,"height":64,"pixelRatio":1},"cross-1":{"x":0,"y":192,"width":96,"height":96,"pixelRatio":1},"wind-1":{"x":424,"y":128,"width":64,"height":64,"pixelRatio":1},"intensity-4-dark":{"x":424,"y":192,"width":64,"height":64,"pixelRatio":1},"intensity-5-dark":{"x":424,"y":256,"width":64,"height":64,"pixelRatio":1},"wind-3":{"x":424,"y":320,"width":64,"height":64,"pixelRatio":1},"cross-2":{"x":96,"y":192,"width":96,"height":96,"pixelRatio":1},"intensity-9":{"x":0,"y":416,"width":64,"height":64,"pixelRatio":1},"intensity-8":{"x":64,"y":416,"width":64,"height":64,"pixelRatio":1},"cross-3":{"x":192,"y":192,"width":96,"height":96,"pixelRatio":1},"wind-2":{"x":128,"y":416,"width":64,"height":64,"pixelRatio":1},"intensity-9-dark":{"x":192,"y":416,"width":64,"height":64,"pixelRatio":1},"intensity-8-dark":{"x":256,"y":416,"width":64,"height":64,"pixelRatio":1},"lightning-1-30":{"x":320,"y":416,"width":64,"height":64,"pixelRatio":1},"intensity-3-dark":{"x":384,"y":416,"width":64,"height":64,"pixelRatio":1},"intensity-2-dark":{"x":488,"y":0,"width":64,"height":64,"pixelRatio":1},"lightning-0-60":{"x":488,"y":64,"width":64,"height":64,"pixelRatio":1}} \ No newline at end of file diff --git a/assets/sprites.png b/assets/sprites.png deleted file mode 100644 index cf82708f0..000000000 Binary files a/assets/sprites.png and /dev/null differ diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index a20936a7c..0c536b632 100644 --- a/lib/app/map/page.dart +++ b/lib/app/map/page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:dpip/utils/extensions/maplibre.dart'; import 'package:flutter/material.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -223,32 +224,10 @@ class _MapPageState extends State with TickerProviderStateMixin { setState(() => _activeLayers = newLayers); } - void _hideBaseMapLayers() { - _controller.setLayerVisibility(BaseMapLayerIds.exptechGlobalFill, false); - _controller.setLayerVisibility(BaseMapLayerIds.exptechTownFill, false); - _controller.setLayerVisibility(BaseMapLayerIds.exptechCountyFill, false); - _controller.setLayerVisibility(BaseMapLayerIds.osmGlobalRaster, false); - _controller.setLayerVisibility(BaseMapLayerIds.googleGlobalRaster, false); - } - Future setBaseMapType(BaseMapType baseMapType) async { if (!mounted) return; - _hideBaseMapLayers(); - - switch (baseMapType) { - case BaseMapType.exptech: - await _controller.setLayerVisibility(BaseMapLayerIds.exptechGlobalFill, true); - await _controller.setLayerVisibility(BaseMapLayerIds.exptechTownFill, true); - await _controller.setLayerVisibility(BaseMapLayerIds.exptechCountyFill, true); - await _controller.setLayerVisibility(BaseMapLayerIds.exptechCountyOutline, true); - - case BaseMapType.osm: - await _controller.setLayerVisibility(BaseMapLayerIds.osmGlobalRaster, true); - - case BaseMapType.google: - await _controller.setLayerVisibility(BaseMapLayerIds.googleGlobalRaster, true); - } + await _controller.setBaseMap(baseMapType); setState(() => _baseMapType = baseMapType); } @@ -283,7 +262,6 @@ class _MapPageState extends State with TickerProviderStateMixin { _setupWeatherLayerTimeSync(); - setBaseMapType(_baseMapType); setLayers(_activeLayers); } @@ -299,7 +277,7 @@ class _MapPageState extends State with TickerProviderStateMixin { return Scaffold( body: Stack( children: [ - DpipMap(onMapCreated: onMapCreated), + DpipMap(baseMapType: _baseMapType, onMapCreated: onMapCreated), PositionedLayerButton( activeLayers: _activeLayers, currentBaseMap: _baseMapType, diff --git a/lib/utils/extensions/maplibre.dart b/lib/utils/extensions/maplibre.dart new file mode 100644 index 000000000..f8ab01564 --- /dev/null +++ b/lib/utils/extensions/maplibre.dart @@ -0,0 +1,34 @@ +import 'package:maplibre_gl/maplibre_gl.dart'; + +import 'package:dpip/widgets/map/map.dart'; + +extension MapLibreMapControllerExtension on MapLibreMapController { + Future setBaseMap(BaseMapType baseMapType) async { + await Future.wait([ + setOSMVisibility(baseMapType == BaseMapType.osm), + setGoogleVisibility(baseMapType == BaseMapType.google), + setExptechVisibility(baseMapType == BaseMapType.exptech), + ]); + } + + Future setOSMVisibility(bool visible) async { + final layers = (await getLayerIds()).cast(); + final osmLayers = layers.where((v) => v.startsWith('osm-')); + + await Future.wait(osmLayers.map((v) => setLayerVisibility(v, visible))); + } + + Future setGoogleVisibility(bool visible) async { + final layers = (await getLayerIds()).cast(); + final googleLayers = layers.where((v) => v.startsWith('google-')); + + await Future.wait(googleLayers.map((v) => setLayerVisibility(v, visible))); + } + + Future setExptechVisibility(bool visible) async { + final layers = (await getLayerIds()).cast(); + final exptechLayers = layers.where((v) => v.startsWith('exptech-')); + + await Future.wait(exptechLayers.map((v) => setLayerVisibility(v, visible))); + } +} diff --git a/lib/widgets/map/map.dart b/lib/widgets/map/map.dart index 3ac70774f..d8c7ecb8d 100644 --- a/lib/widgets/map/map.dart +++ b/lib/widgets/map/map.dart @@ -1,18 +1,17 @@ -import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:flutter/material.dart'; + +import 'package:maplibre_gl/maplibre_gl.dart'; + import 'package:dpip/core/ios_get_location.dart'; import 'package:dpip/core/providers.dart'; import 'package:dpip/utils/constants.dart'; -import 'package:dpip/utils/extensions/build_context.dart'; import 'package:dpip/utils/extensions/latlng.dart'; import 'package:dpip/utils/geojson.dart'; import 'package:dpip/utils/log.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:dpip/widgets/map/style.dart'; enum BaseMapType { exptech, osm, google } @@ -36,9 +35,6 @@ class BaseMapLayerIds { static const exptechCountyFill = 'exptech-county'; static const exptechCountyOutline = 'exptech-county-outline'; - static const osmGlobalRaster = 'osm-global'; - static const googleGlobalRaster = 'google-global'; - static const userLocation = 'user-location'; static Iterable values() sync* { @@ -72,11 +68,12 @@ class DpipMap extends StatefulWidget { final bool focusUserLocationOnValueUpdate; static const kTaiwanCenter = LatLng(23.60, 120.85); + static const kTaiwanZoom = 6.4; const DpipMap({ super.key, this.baseMapType = BaseMapType.exptech, - this.initialCameraPosition = const CameraPosition(target: kTaiwanCenter, zoom: 6.4), + this.initialCameraPosition = const CameraPosition(target: kTaiwanCenter, zoom: kTaiwanZoom), this.onMapCreated, this.onMapClick, this.onMapIdle, @@ -94,106 +91,8 @@ class DpipMap extends StatefulWidget { @override State createState() => DpipMapState(); -} -class DpipMapState extends State { - String _getStyleJson(String spritePath) { - final colors = context.colors; - - return jsonEncode({ - 'version': 8, - 'name': 'ExpTech Studio', - 'center': [120.85, 23.10], - 'zoom': 6.2, - 'sources': { - 'map': { - 'type': 'vector', - 'url': 'https://lb.exptech.dev/api/v1/map/tiles/tiles.json', - 'tileSize': 512, - 'buffer': 64, - }, - 'osm': { - 'type': 'raster', - 'tiles': ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], - 'tileSize': 256, - 'attribution': '© OpenStreetMap Contributors', - 'maxzoom': 19, - }, - 'google': { - 'type': 'raster', - 'tiles': ['https://mts1.google.com/vt/lyrs=p&hl=zh-TW&x={x}&y={y}&z={z}'], - 'tileSize': 256, - 'attribution': '© Google Maps', - 'maxzoom': 19, - }, - }, - 'sprite': spritePath, - 'glyphs': 'https://cdn.jsdelivr.net/gh/exptechtw/map-glyph/{fontstack}/{range}.pbf', - 'layers': [ - { - 'id': 'background', - 'type': 'background', - 'paint': {'background-color': colors.surface.toHexStringRGB()}, - }, - { - 'id': BaseMapLayerIds.osmGlobalRaster, - 'type': 'raster', - 'source': 'osm', - 'layout': {'visibility': widget.baseMapType == BaseMapType.osm ? 'visible' : 'none'}, - }, - { - 'id': BaseMapLayerIds.googleGlobalRaster, - 'type': 'raster', - 'source': 'google', - 'layout': {'visibility': widget.baseMapType == BaseMapType.google ? 'visible' : 'none'}, - }, - { - 'id': BaseMapLayerIds.exptechGlobalFill, - 'type': 'fill', - 'source': 'map', - 'source-layer': 'global', - 'paint': {'fill-color': colors.surfaceContainer.toHexStringRGB(), 'fill-opacity': 1}, - 'layout': {'visibility': widget.baseMapType == BaseMapType.exptech ? 'visible' : 'none'}, - }, - { - 'id': BaseMapLayerIds.exptechCountyFill, - 'type': 'fill', - 'source': 'map', - 'source-layer': 'city', - 'paint': {'fill-color': colors.surfaceContainerHigh.toHexStringRGB(), 'fill-opacity': 1}, - 'layout': {'visibility': widget.baseMapType == BaseMapType.exptech ? 'visible' : 'none'}, - }, - { - 'id': BaseMapLayerIds.exptechTownFill, - 'type': 'fill', - 'source': 'map', - 'source-layer': 'town', - 'paint': {'fill-color': colors.surfaceContainerHigh.toHexStringRGB(), 'fill-opacity': 1}, - 'layout': {'visibility': widget.baseMapType == BaseMapType.exptech ? 'visible' : 'none'}, - }, - { - 'id': BaseMapLayerIds.exptechCountyOutline, - 'source': 'map', - 'source-layer': 'city', - 'type': 'line', - 'paint': {'line-color': colors.outline.toHexStringRGB()}, - 'layout': {'visibility': 'visible'}, - }, - { - 'id': 'tsunami', - 'type': 'line', - 'source': 'map', - 'source-layer': 'tsunami', - 'paint': {'line-opacity': 0, 'line-width': 3, 'line-join': 'round'}, - }, - ], - }); - } - - MapLibreMapController? _controller; - String? styleAbsoluteFilePath; - - double adjustedZoom(double zoom) { + static double adjustedZoom(BuildContext context, double zoom) { final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio; const double baseZoomAdjustment = 1.0; const double mediumZoomAdjustment = 0.3; @@ -208,6 +107,11 @@ class DpipMapState extends State { return zoom + baseZoomAdjustment; } } +} + +class DpipMapState extends State { + MapLibreMapController? _controller; + late Future styleAbsoluteFilePath = MapStyle(context, baseMap: widget.baseMapType).save(); Future _updateUserLocation() async { if (!mounted) return; @@ -286,69 +190,45 @@ class DpipMapState extends State { super.initState(); GlobalProviders.location.$coordinates.addListener(_updateUserLocation); - - getApplicationDocumentsDirectory().then((dir) async { - final documentDir = dir.path; - final mapDir = '$documentDir/map'; - - await Directory(mapDir).create(recursive: true); - - // Copy sprite.png - final spritePngData = await rootBundle.load('assets/sprites.png'); - final spritePngFile = File('$mapDir/sprites.png'); - await spritePngFile.writeAsBytes(spritePngData.buffer.asUint8List()); - final spritePngFile2x = File('$mapDir/sprites@2x.png'); - await spritePngFile2x.writeAsBytes(spritePngData.buffer.asUint8List()); - - // Copy sprite.json - final spriteJsonData = await rootBundle.load('assets/sprites.json'); - final spriteJsonFile = File('$mapDir/sprites.json'); - await spriteJsonFile.writeAsBytes(spriteJsonData.buffer.asUint8List()); - final spriteJsonFile2x = File('$mapDir/sprites@2x.json'); - await spriteJsonFile2x.writeAsBytes(spriteJsonData.buffer.asUint8List()); - - final spriteUri = '${spriteJsonFile.parent.uri}sprites'; - TalkerManager.instance.info('Sprite is $spriteUri'); - - // Create style.json - final styleJsonData = _getStyleJson(spriteUri); - final styleJsonFile = File('$mapDir/style.json'); - await styleJsonFile.writeAsString(styleJsonData); - - setState(() => styleAbsoluteFilePath = styleJsonFile.uri.toFilePath()); - }); } @override Widget build(BuildContext context) { - if (styleAbsoluteFilePath == null) { - return const Center(child: CircularProgressIndicator()); - } + final double adjustedZoomValue = DpipMap.adjustedZoom(context, widget.initialCameraPosition.zoom); - final double adjustedZoomValue = adjustedZoom(widget.initialCameraPosition.zoom); - - return MapLibreMap( - minMaxZoomPreference: widget.minMaxZoomPreference ?? const MinMaxZoomPreference(4, 12.5), - trackCameraPosition: true, - initialCameraPosition: CameraPosition(target: widget.initialCameraPosition.target, zoom: adjustedZoomValue), - styleString: styleAbsoluteFilePath!, - tiltGesturesEnabled: widget.tiltGesturesEnabled ?? false, - scrollGesturesEnabled: widget.scrollGesturesEnabled ?? true, - rotateGesturesEnabled: widget.rotateGesturesEnabled ?? false, - zoomGesturesEnabled: widget.zoomGesturesEnabled ?? true, - doubleClickZoomEnabled: widget.doubleClickZoomEnabled ?? true, - dragEnabled: widget.dragEnabled ?? true, - attributionButtonMargins: const Point(-100, -100), - onMapCreated: (controller) { - _controller = controller; - widget.onMapCreated?.call(controller); - }, - onMapClick: widget.onMapClick, - onMapIdle: widget.onMapIdle, - onMapLongClick: widget.onMapLongClick, - onStyleLoadedCallback: () { - _initMap(); - widget.onStyleLoadedCallback?.call(); + return FutureBuilder( + future: styleAbsoluteFilePath, + builder: (context, snapshot) { + final styleString = snapshot.data; + + if (styleString == null) { + return const Center(child: CircularProgressIndicator()); + } + + return MapLibreMap( + minMaxZoomPreference: widget.minMaxZoomPreference ?? const MinMaxZoomPreference(4, 12.5), + trackCameraPosition: true, + initialCameraPosition: CameraPosition(target: widget.initialCameraPosition.target, zoom: adjustedZoomValue), + styleString: styleString, + tiltGesturesEnabled: widget.tiltGesturesEnabled ?? false, + scrollGesturesEnabled: widget.scrollGesturesEnabled ?? true, + rotateGesturesEnabled: widget.rotateGesturesEnabled ?? false, + zoomGesturesEnabled: widget.zoomGesturesEnabled ?? true, + doubleClickZoomEnabled: widget.doubleClickZoomEnabled ?? true, + dragEnabled: widget.dragEnabled ?? true, + attributionButtonMargins: const Point(-100, -100), + onMapCreated: (controller) { + _controller = controller; + widget.onMapCreated?.call(controller); + }, + onMapClick: widget.onMapClick, + onMapIdle: widget.onMapIdle, + onMapLongClick: widget.onMapLongClick, + onStyleLoadedCallback: () { + _initMap(); + widget.onStyleLoadedCallback?.call(); + }, + ); }, ); } diff --git a/lib/widgets/map/style.dart b/lib/widgets/map/style.dart new file mode 100644 index 000000000..9fabfd240 --- /dev/null +++ b/lib/widgets/map/style.dart @@ -0,0 +1,1123 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:crypto/crypto.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:dpip/utils/extensions/build_context.dart'; +import 'package:dpip/utils/extensions/latlng.dart'; +import 'package:dpip/widgets/map/map.dart'; + +class MapStyle { + late Map json; + + MapStyle(BuildContext context, {required BaseMapType baseMap}) { + json = { + 'version': 8, + 'name': 'DPIP Map', + 'center': DpipMap.kTaiwanCenter.asGeoJsonCooridnate, + 'zoom': DpipMap.adjustedZoom(context, DpipMap.kTaiwanZoom), + 'font-faces': { + 'Noto Sans TC Regular': + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/blob/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf', + 'Noto Sans TC Bold': + 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk/blob/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Bold.otf', + }, + 'glyphs': 'https://cdn.jsdelivr.net/gh/exptechtw/map-assets/{fontstack}/{range}.pbf', + 'sprite': 'https://cdn.jsdelivr.net/gh/exptechtw/map-assets/sprites', + 'sources': {...osmSource(), ...googleSource(), ...exptechSource()}, + 'layers': [ + background(context.colors), + ...osmLayers(context.colors, visible: baseMap == BaseMapType.osm), + ...googleLayers(visible: baseMap == BaseMapType.google), + ...exptechLayers(context.colors, visible: baseMap == BaseMapType.exptech), + ], + }; + } + + Future save() async { + final cache = await getApplicationCacheDirectory(); + final cachePath = cache.path; + + final data = jsonEncode(json); + final hash = md5.convert(utf8.encode(data)).toString(); + + final styleJsonFile = File('$cachePath/map-$hash.json'); + + if (!styleJsonFile.existsSync()) { + await styleJsonFile.writeAsString(data); + } + + return styleJsonFile.path; + } + + static Map osmSource() => { + 'ne2_shaded': { + 'maxzoom': 6, + 'tileSize': 256, + 'tiles': ['https://tiles.openfreemap.org/natural_earth/ne2sr/{z}/{x}/{y}.png'], + 'type': 'raster', + }, + 'openmaptiles': {'type': 'vector', 'url': 'https://tiles.openfreemap.org/planet', 'volatile': true}, + }; + + static Map background(ColorScheme colors) => { + 'id': 'background', + 'type': 'background', + 'layout': {'visibility': 'visible'}, + 'paint': {'background-color': colors.surface.toHexStringRGB()}, + }; + + static List> osmLayers(ColorScheme colors, {bool visible = false}) => [ + { + 'id': 'osm-background', + 'type': 'background', + 'layout': {'visibility': visible ? 'visible' : 'none'}, + 'paint': {'background-color': colors.surfaceContainerHigh.toHexStringRGB()}, + }, + { + 'id': 'osm-landcover-glacier', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'landcover', + 'filter': [ + '==', + ['get', 'subclass'], + 'glacier', + ], + 'paint': { + 'fill-color': '#fff', + 'fill-opacity': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.9, + 10, + 0.3, + ], + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-park', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'park', + 'filter': [ + 'match', + ['geometry-type'], + ['MultiPolygon', 'Polygon'], + true, + false, + ], + 'paint': { + 'fill-color': '#d8e8c8', + 'fill-opacity': [ + 'interpolate', + ['exponential', 1.8], + ['zoom'], + 9, + if (colors.brightness == Brightness.dark) 0.05 else 0.6, + 12, + if (colors.brightness == Brightness.dark) 0.004 else 0.25, + ], + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-landcover-wood', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'landcover', + 'filter': [ + '==', + ['get', 'class'], + 'wood', + ], + 'paint': { + 'fill-antialias': [ + 'step', + ['zoom'], + false, + 9, + true, + ], + 'fill-color': '#66aa44', + 'fill-opacity': 0.1, + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-landcover-grass', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'landcover', + 'filter': [ + '==', + ['get', 'class'], + 'grass', + ], + 'paint': {'fill-color': '#d2e8c2', 'fill-opacity': colors.brightness == Brightness.dark ? 0.2 : 1}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-landcover-grass-park', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'park', + 'filter': [ + '==', + ['get', 'class'], + 'public_park', + ], + 'paint': {'fill-color': '#d8e8c8', 'fill-opacity': colors.brightness == Brightness.dark ? 0.4 : 0.8}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway_tunnel', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'minzoom': 14, + 'filter': [ + 'all', + [ + 'match', + ['get', 'class'], + ['canal', 'river', 'stream'], + true, + false, + ], + [ + '==', + ['get', 'brunnel'], + 'tunnel', + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-dasharray': [2, 4], + 'line-width': [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 13, + 0.5, + 20, + 6, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-other', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + 'match', + ['get', 'class'], + ['canal', 'river', 'stream'], + false, + true, + ], + [ + '==', + ['get', 'intermittent'], + 0, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-width': [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 13, + 0.5, + 20, + 2, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-other-intermittent', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + 'match', + ['get', 'class'], + ['canal', 'river', 'stream'], + false, + true, + ], + [ + '==', + ['get', 'intermittent'], + 1, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-dasharray': [4, 3], + 'line-width': [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 13, + 0.5, + 20, + 2, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-stream-canal', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + 'match', + ['get', 'class'], + ['canal', 'stream'], + true, + false, + ], + [ + '!=', + ['get', 'brunnel'], + 'tunnel', + ], + [ + '==', + ['get', 'intermittent'], + 0, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-width': [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 13, + 0.5, + 20, + 6, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-stream-canal-intermittent', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + 'match', + ['get', 'class'], + ['canal', 'stream'], + true, + false, + ], + [ + '!=', + ['get', 'brunnel'], + 'tunnel', + ], + [ + '==', + ['get', 'intermittent'], + 1, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-dasharray': [4, 3], + 'line-width': [ + 'interpolate', + ['exponential', 1.3], + ['zoom'], + 13, + 0.5, + 20, + 6, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-river', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'river', + ], + [ + '!=', + ['get', 'brunnel'], + 'tunnel', + ], + [ + '!=', + ['get', 'intermittent'], + 1, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-width': [ + 'interpolate', + ['exponential', 1.2], + ['zoom'], + 10, + 0.8, + 20, + 6, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway-river-intermittent', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'river', + ], + [ + '!=', + ['get', 'brunnel'], + 'tunnel', + ], + [ + '==', + ['get', 'intermittent'], + 1, + ], + ], + 'paint': { + 'line-color': '#a0c8f0', + 'line-opacity': colors.brightness == Brightness.dark ? 0.4 : 1, + 'line-dasharray': [3, 2.5], + 'line-width': [ + 'interpolate', + ['exponential', 1.2], + ['zoom'], + 10, + 0.8, + 20, + 6, + ], + }, + 'layout': {'line-cap': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-water', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'water', + 'filter': [ + 'all', + [ + '!=', + ['get', 'intermittent'], + 1, + ], + [ + '!=', + ['get', 'brunnel'], + 'tunnel', + ], + ], + 'paint': {'fill-color': colors.brightness == Brightness.dark ? colors.surface.toHexStringRGB() : '#AECFE2'}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-water-intermittent', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'water', + 'filter': [ + '==', + ['get', 'intermittent'], + 1, + ], + 'paint': {'fill-color': '#CFE6F7', 'fill-opacity': colors.brightness == Brightness.dark ? 0.3 : 0.7}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-landcover-ice-shelf', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'landcover', + 'filter': [ + '==', + ['get', 'subclass'], + 'ice_shelf', + ], + 'paint': { + 'fill-color': colors.brightness == Brightness.dark ? colors.surfaceContainerHigh.toHexStringRGB() : '#fff', + 'fill-opacity': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.9, + 10, + 0.3, + ], + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-landcover-sand', + 'type': 'fill', + 'source': 'openmaptiles', + 'source-layer': 'landcover', + 'filter': [ + '==', + ['get', 'class'], + 'sand', + ], + 'paint': {'fill-color': '#f5eebc', 'fill-opacity': colors.brightness == Brightness.dark ? 0.6 : 1}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-boundary_3', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'boundary', + 'minzoom': 4, + 'filter': [ + 'all', + [ + '>=', + ['get', 'admin_level'], + 3, + ], + [ + '<=', + ['get', 'admin_level'], + 6, + ], + [ + '!=', + ['get', 'maritime'], + 1, + ], + [ + '!=', + ['get', 'disputed'], + 1, + ], + [ + '!', + ['has', 'claimed_by'], + ], + ], + 'paint': { + 'line-color': colors.outline.toHexStringRGB(), + 'line-dasharray': [1, 1], + 'line-width': [ + 'interpolate', + ['linear', 1], + ['zoom'], + 7, + 1, + 11, + 2, + ], + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-boundary_2', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'boundary', + 'filter': [ + 'all', + [ + '==', + ['get', 'admin_level'], + 2, + ], + [ + '!=', + ['get', 'maritime'], + 1, + ], + [ + '!=', + ['get', 'disputed'], + 1, + ], + [ + '!', + ['has', 'claimed_by'], + ], + ], + 'paint': { + 'line-color': colors.outlineVariant.toHexStringRGB(), + 'line-opacity': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.4, + 4, + 1, + ], + 'line-width': [ + 'interpolate', + ['linear'], + ['zoom'], + 3, + 1, + 5, + 1.2, + 12, + 3, + ], + }, + 'layout': {'line-cap': 'round', 'line-join': 'round', 'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-boundary_disputed', + 'type': 'line', + 'source': 'openmaptiles', + 'source-layer': 'boundary', + 'filter': [ + 'all', + [ + '!=', + ['get', 'maritime'], + 1, + ], + [ + '==', + ['get', 'disputed'], + 1, + ], + ], + 'paint': { + 'line-color': colors.outlineVariant.toHexStringRGB(), + 'line-dasharray': [1, 2], + 'line-width': [ + 'interpolate', + ['linear'], + ['zoom'], + 3, + 1, + 5, + 1.2, + 12, + 3, + ], + }, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': 'osm-waterway_line_label', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'waterway', + 'minzoom': 10, + 'filter': [ + 'match', + ['geometry-type'], + ['LineString', 'MultiLineString'], + true, + false, + ], + 'paint': { + 'text-color': '#74aee9', + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1.5, + }, + 'layout': { + 'symbol-placement': 'line', + 'symbol-spacing': 350, + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Regular'], + 'text-letter-spacing': 0.2, + 'text-max-width': 5, + 'text-size': 14, + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-water_name_line_label', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'water_name', + 'filter': [ + 'match', + ['geometry-type'], + ['LineString', 'MultiLineString'], + true, + false, + ], + 'paint': { + 'text-color': '#495e91', + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1.5, + }, + 'layout': { + 'symbol-placement': 'line', + 'symbol-spacing': 350, + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Regular'], + 'text-letter-spacing': 0.2, + 'text-max-width': 5, + 'text-size': 14, + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_country_2', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'maxzoom': 9, + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'country', + ], + [ + '==', + ['get', 'rank'], + 2, + ], + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Bold'], + 'text-max-width': 6.25, + 'text-size': [ + 'interpolate', + ['linear'], + ['zoom'], + 2, + 9, + 5, + 17, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_town', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'minzoom': 6, + 'filter': [ + '==', + ['get', 'class'], + 'town', + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'icon-allow-overlap': true, + 'icon-image': [ + 'step', + ['zoom'], + 'circle_11_black', + 10, + '', + ], + 'icon-optional': false, + 'icon-size': 0.2, + 'text-anchor': 'bottom', + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Regular'], + 'text-max-width': 8, + 'text-size': [ + 'interpolate', + ['exponential', 1.2], + ['zoom'], + 7, + 12, + 11, + 14, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_state', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'minzoom': 5, + 'maxzoom': 8, + 'filter': [ + '==', + ['get', 'class'], + 'state', + ], + 'paint': { + 'text-color': colors.onSurfaceVariant.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Regular'], + 'text-letter-spacing': 0.2, + 'text-max-width': 9, + 'text-size': [ + 'interpolate', + ['linear'], + ['zoom'], + 5, + 10, + 8, + 14, + ], + 'text-transform': 'uppercase', + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_city', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'minzoom': 3, + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'city', + ], + [ + '!=', + ['get', 'capital'], + 2, + ], + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'icon-allow-overlap': true, + 'icon-image': [ + 'step', + ['zoom'], + 'circle_11_black', + 9, + '', + ], + 'icon-optional': false, + 'icon-size': 0.4, + 'text-anchor': 'bottom', + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Regular'], + 'text-max-width': 8, + 'text-offset': [0, -0.1], + 'text-size': [ + 'interpolate', + ['exponential', 1.2], + ['zoom'], + 4, + 11, + 7, + 13, + 11, + 18, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_city_capital', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'minzoom': 3, + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'city', + ], + [ + '==', + ['get', 'capital'], + 2, + ], + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'icon-allow-overlap': true, + 'icon-image': [ + 'step', + ['zoom'], + 'circle_11_black', + 9, + '', + ], + 'icon-optional': false, + 'icon-size': 0.5, + 'text-anchor': 'bottom', + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Bold'], + 'text-max-width': 8, + 'text-offset': [0, -0.2], + 'text-size': [ + 'interpolate', + ['exponential', 1.2], + ['zoom'], + 4, + 12, + 7, + 14, + 11, + 20, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_country_3', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'minzoom': 2, + 'maxzoom': 9, + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'country', + ], + [ + '>=', + ['get', 'rank'], + 3, + ], + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Bold'], + 'text-max-width': 6.25, + 'text-size': [ + 'interpolate', + ['linear'], + ['zoom'], + 3, + 9, + 7, + 17, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + { + 'id': 'osm-label_country_1', + 'type': 'symbol', + 'source': 'openmaptiles', + 'source-layer': 'place', + 'maxzoom': 9, + 'filter': [ + 'all', + [ + '==', + ['get', 'class'], + 'country', + ], + [ + '==', + ['get', 'rank'], + 1, + ], + ], + 'paint': { + 'text-color': colors.onSurface.toHexStringRGB(), + 'text-halo-blur': 1, + 'text-halo-color': colors.outlineVariant.toHexStringRGB(), + 'text-halo-width': 1, + }, + 'layout': { + 'text-field': [ + 'coalesce', + ['get', 'name:nonlatin'], + ['get', 'name'], + ], + 'text-font': ['Noto Sans TC Bold'], + 'text-max-width': 6.25, + 'text-size': [ + 'interpolate', + ['linear'], + ['zoom'], + 1, + 9, + 4, + 17, + ], + 'visibility': visible ? 'visible' : 'none', + }, + }, + ]; + + static Map googleSource() => { + 'google': { + 'type': 'raster', + 'tiles': ['https://mt1.google.com/vt/lyrs=s&hl=zh-TW&x={x}&y={y}&z={z}'], + 'tileSize': 256, + 'attribution': '© Google Maps', + 'maxzoom': 19, + }, + }; + + static List> googleLayers({bool visible = false}) => [ + { + 'id': 'google-raster', + 'type': 'raster', + 'source': 'google', + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + ]; + + static Map exptechSource() => { + 'exptech': { + 'type': 'vector', + 'url': 'https://lb.exptech.dev/api/v1/map/tiles/tiles.json', + 'tileSize': 512, + 'buffer': 64, + }, + }; + + static List> exptechLayers(ColorScheme colors, {bool visible = false}) => [ + { + 'id': BaseMapLayerIds.exptechGlobalFill, + 'type': 'fill', + 'source': 'exptech', + 'source-layer': 'global', + 'paint': {'fill-color': colors.surfaceContainer.toHexStringRGB(), 'fill-opacity': 1}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': BaseMapLayerIds.exptechCountyFill, + 'type': 'fill', + 'source': 'exptech', + 'source-layer': 'city', + 'paint': {'fill-color': colors.surfaceContainerHigh.toHexStringRGB(), 'fill-opacity': 1}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': BaseMapLayerIds.exptechTownFill, + 'type': 'fill', + 'source': 'exptech', + 'source-layer': 'town', + 'paint': {'fill-color': colors.surfaceContainerHigh.toHexStringRGB(), 'fill-opacity': 1}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + { + 'id': BaseMapLayerIds.exptechCountyOutline, + 'type': 'line', + 'source': 'exptech', + 'source-layer': 'city', + 'paint': {'line-color': colors.outline.toHexStringRGB()}, + 'layout': {'visibility': visible ? 'visible' : 'none'}, + }, + ]; +} diff --git a/pubspec.lock b/pubspec.lock index a0a351cb2..762629257 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -258,7 +258,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -1607,6 +1607,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 14645052e..a23675aba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: ip_country_lookup: ^1.0.0 flutter_icmp_ping: ^3.1.3 geojson_vi: ^2.2.5 + crypto: ^3.0.6 dependency_overrides: intl: 0.19.0 @@ -89,5 +90,3 @@ flutter: - assets/notify_test.json - assets/time.json - assets/box.json - - assets/sprites.png - - assets/sprites.json