diff --git a/green_walking/lib/l10n/app_de.arb b/green_walking/lib/l10n/app_de.arb index f9e56e7..008905d 100644 --- a/green_walking/lib/l10n/app_de.arb +++ b/green_walking/lib/l10n/app_de.arb @@ -52,7 +52,6 @@ "openLocationDetailsSemanticLabel": "Details anzeigen", "followLocationToast": "Position folgen", "followLocationOffToast": "Position folgen aus", - "optionDarkTheme": "Dark Theme", "optionTheme": "Design", "optionDarkTheme": "Dunkel", "optionLightTheme": "Hell", diff --git a/green_walking/lib/l10n/app_en.arb b/green_walking/lib/l10n/app_en.arb index 68d191d..c426799 100644 --- a/green_walking/lib/l10n/app_en.arb +++ b/green_walking/lib/l10n/app_en.arb @@ -346,7 +346,7 @@ "description": "Toast message if the map stops following the users location." }, "optionTheme": "Theme", - "@optionDarkTheme": { + "@optionTheme": { "description": "Option to select the theme of the application." }, "optionDarkTheme": "Dark", diff --git a/green_walking/lib/library/map_utils.dart b/green_walking/lib/library/map_utils.dart index 225e4ab..17f5878 100644 --- a/green_walking/lib/library/map_utils.dart +++ b/green_walking/lib/library/map_utils.dart @@ -13,8 +13,8 @@ class PuckLocation { extension MapboxMapPosition on MapboxMap { Future getCameraPosition() async { try { - final CameraState mapCameraState = await getCameraState(); - return mapCameraState.center.coordinates; + final CameraState cameraState = await getCameraState(); + return cameraState.center.coordinates; } catch (e) { log('failed to get camera position: $e'); return null; @@ -33,4 +33,10 @@ extension MapboxMapPosition on MapboxMap { return null; } } + + Future getCameraBounds() async { + final CameraState cameraState = await getCameraState(); + return coordinateBoundsForCamera(CameraOptions( + center: cameraState.center, zoom: cameraState.zoom, bearing: cameraState.bearing, pitch: cameraState.pitch)); + } } diff --git a/green_walking/lib/pages/download_map.dart b/green_walking/lib/pages/download_map.dart index 7d3b075..2144b48 100644 --- a/green_walking/lib/pages/download_map.dart +++ b/green_walking/lib/pages/download_map.dart @@ -1,47 +1,26 @@ import 'dart:async'; -import 'dart:developer' show log; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; -import 'package:path_provider/path_provider.dart'; +import '../library/map_utils.dart'; import '../config.dart'; -import '../library/util.dart'; +import '../widgets/download_map_dialog.dart'; class DownloadMapPage extends StatefulWidget { - const DownloadMapPage({super.key}); + const DownloadMapPage({super.key, required this.tileStore}); + + final TileStore tileStore; @override State createState() => _DownloadMapPageState(); } class _DownloadMapPageState extends State { - // Only the outdoor map can be downloaded - static const STYLE_URI = CustomMapboxStyles.outdoor; - - final StreamController _stylePackProgress = StreamController.broadcast(); - final StreamController _tileRegionLoadProgress = StreamController.broadcast(); - late OfflineManager _offlineManager; - late TileStore _tileStore; late MapboxMap _mapboxMap; - TileRegionEstimateResult? _estimateResult; - - @override - void initState() { - super.initState(); - _setAsyncState(); - } - void _setAsyncState() async { - final offlineManager = await OfflineManager.create(); - final tmpDir = await getTemporaryDirectory(); - final tileStore = await TileStore.createAt(tmpDir.uri); - setState(() { - _offlineManager = offlineManager; - _tileStore = tileStore; - }); - } + bool _isLoading = false; @override Widget build(BuildContext context) { @@ -53,146 +32,80 @@ class _DownloadMapPageState extends State { ), body: Center( child: Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(20), - child: MapWidget( - key: const ValueKey("downloadMapWidget"), - styleUri: CustomMapboxStyles.outdoor, - cameraOptions: CameraOptions(center: Point(coordinates: Position(9.8682, 53.5519)), zoom: 11.0), - //onCameraChangeListener: (cameraChangedEventData) { - // setState(() { - // _estimateResult = null; - // }); - //}, - onMapCreated: (MapboxMap mapboxMap) { - _mapboxMap = mapboxMap; - _mapboxMap.scaleBar.updateSettings(ScaleBarSettings(enabled: false)); - _mapboxMap.compass.updateSettings(CompassSettings(enabled: false)); - _mapboxMap.gestures.updateSettings(GesturesSettings(rotateEnabled: false, pitchEnabled: false)); - })), - ), + children: [ ButtonBar( alignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: _estimateTileRegion, - child: const Text("Estimate size"), - ), - ElevatedButton( - onPressed: _onDownload, - child: const Text("Download (WiFi only)"), + children: [ + ElevatedButton.icon( + icon: _isLoading + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(2.0), + child: const CircularProgressIndicator(), + ) + : const Icon(Icons.download), + onPressed: _isLoading ? null : _onDownloadPressed, + label: const Text("Download area"), ), ], ), - Text(_estimateResult != null - ? 'Storage: ${formatBytes(_estimateResult!.storageSize, 0)}, Network transfer: ${formatBytes(_estimateResult!.transferSize, 0)}' - : ""), - StreamBuilder( - stream: _tileRegionLoadProgress.stream, - initialData: 0.0, - builder: (context, snapshot) { - return Column(mainAxisSize: MainAxisSize.min, children: [ - Text("Progress: ${(snapshot.requireData * 100).toStringAsFixed(0)}%"), - LinearProgressIndicator( - value: snapshot.requireData, - ) - ]); - }), + Expanded( + child: Padding(padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: _mapWidget()), + ), ], ), ), ); } - Future _cameraBoundingBox() async { - final cameraState = await _mapboxMap.getCameraState(); - final cameraBounds = await _mapboxMap.coordinateBoundsForCamera( - CameraOptions(center: cameraState.center, zoom: cameraState.zoom, pitch: cameraState.pitch)); - - return Polygon.fromPoints(points: [ - [ - cameraBounds.southwest, - Point( - coordinates: Position.named( - lng: cameraBounds.southwest.coordinates.lng, lat: cameraBounds.northeast.coordinates.lat)), - cameraBounds.northeast, - Point( - coordinates: Position.named( - lng: cameraBounds.northeast.coordinates.lng, lat: cameraBounds.southwest.coordinates.lat)), - ] - ]); + Widget _mapWidget() { + return MapWidget( + key: const ValueKey("downloadMapWidget"), + styleUri: CustomMapboxStyles.outdoor, + cameraOptions: CameraOptions(center: Point(coordinates: Position(9.8682, 53.5519)), zoom: 11.0), + onMapCreated: (MapboxMap mapboxMap) { + _mapboxMap = mapboxMap; + // Disable + _mapboxMap.scaleBar.updateSettings(ScaleBarSettings(enabled: false)); + _mapboxMap.compass.updateSettings(CompassSettings(enabled: false)); + _mapboxMap.gestures.updateSettings(GesturesSettings(rotateEnabled: false, pitchEnabled: false)); + // Bounds (minZoom: zoomed out towards globe) + _mapboxMap.setBounds(CameraBoundsOptions(maxZoom: 14, minZoom: 10)); + }); } - Future _regionLoadOptions() async { - final bounds = await _cameraBoundingBox(); - return TileRegionLoadOptions( - geometry: bounds.toJson(), - descriptorsOptions: [TilesetDescriptorOptions(styleURI: STYLE_URI, minZoom: 0, maxZoom: 16)], + Future _onDownloadPressed() async { + setState(() => _isLoading = true); + final styleURI = await _mapboxMap.style.getStyleURI(); + final cameraBounds = await _mapboxMap.getCameraBounds(); + final regionLoadOptions = TileRegionLoadOptions( + geometry: _coordinateBoundsToPolygon(cameraBounds).toJson(), + descriptorsOptions: [TilesetDescriptorOptions(styleURI: styleURI, minZoom: 0, maxZoom: 16)], acceptExpired: true, networkRestriction: NetworkRestriction.DISALLOW_EXPENSIVE); + // FIXME: Catch errors here? + // FIXME: Esimate is somehow notworking anymore; + // final estimateRegion = await widget.tileStore.estimateTileRegion("", regionLoadOptions, null, null).timeout(const Duration(seconds: 2)); + + if (!mounted) return; + final shouldDownload = + await showDialog(context: context, builder: (context) => const DownloadMapDialog(estimateRegion: null)); + setState(() => _isLoading = false); + if (shouldDownload != null && shouldDownload) { + if (!mounted) return; + Navigator.of(context).pop(regionLoadOptions); + } } +} - Future _estimateTileRegion() async { - setState(() { - _estimateResult = null; - }); - final estimated = await _tileStore.estimateTileRegion("", await _regionLoadOptions(), null, null); - setState(() { - _estimateResult = estimated; - }); - } - - Future _onDownload() async { - final cameraState = await _mapboxMap.getCameraState(); - final id = - '${cameraState.center.coordinates.lat.toStringAsFixed(6)},${cameraState.center.coordinates.lng.toStringAsFixed(6)}'; - await _downloadStylePack(); - await _downloadTileRegion(regionId: id); - } - - Future _downloadStylePack() async { - final stylePackLoadOptions = StylePackLoadOptions( - glyphsRasterizationMode: GlyphsRasterizationMode.IDEOGRAPHS_RASTERIZED_LOCALLY, - // metadata: {"tag": "test"}, - acceptExpired: false); - _offlineManager.loadStylePack(STYLE_URI, stylePackLoadOptions, (progress) { - final percentage = progress.completedResourceCount / progress.requiredResourceCount; - if (!_stylePackProgress.isClosed) { - _stylePackProgress.sink.add(percentage); - } - }).then((value) { - _stylePackProgress.sink.add(1); - _stylePackProgress.sink.close(); - }).onError((error, _) { - log('failed to download style pack: $error'); - _displaySnackBar("Failed to map style download"); - }); - } - - Future _downloadTileRegion({required String regionId}) async { - _tileStore.loadTileRegion(regionId, await _regionLoadOptions(), (progress) { - final percentage = progress.completedResourceCount / progress.requiredResourceCount; - if (!_tileRegionLoadProgress.isClosed) { - _tileRegionLoadProgress.sink.add(percentage); - } - }).then((value) { - _tileRegionLoadProgress.sink.add(1); - _tileRegionLoadProgress.sink.close(); - }).onError((error, _) { - log('failed to download tile region: $error'); - _displaySnackBar("Failed to download map"); - }); - } - - void _displaySnackBar(String text) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(text), - // Otherwise you can't click the custom floating button. - // dismissDirection: DismissDirection.none, - duration: const Duration(seconds: 1, milliseconds: 200), - behavior: SnackBarBehavior.floating, - margin: const EdgeInsets.fromLTRB(20.0, 0.0, 92.0, 22.0))); - } +Polygon _coordinateBoundsToPolygon(CoordinateBounds bounds) { + return Polygon.fromPoints(points: [ + [ + bounds.southwest, + Point(coordinates: Position.named(lng: bounds.southwest.coordinates.lng, lat: bounds.northeast.coordinates.lat)), + bounds.northeast, + Point(coordinates: Position.named(lng: bounds.northeast.coordinates.lng, lat: bounds.southwest.coordinates.lat)), + ] + ]); } diff --git a/green_walking/lib/pages/offline_maps.dart b/green_walking/lib/pages/offline_maps.dart index 2826b84..13232f9 100644 --- a/green_walking/lib/pages/offline_maps.dart +++ b/green_walking/lib/pages/offline_maps.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' show log; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -5,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:path_provider/path_provider.dart'; -import '../routes.dart'; +import 'download_map.dart'; class OfflineMapsPage extends StatefulWidget { const OfflineMapsPage({super.key}); @@ -19,6 +20,7 @@ class _OfflineMapsPageState extends State { late TileStore _tileStore; List _stylePacks = []; List _regions = []; + StreamController? _downloadProgress; @override void initState() { @@ -28,10 +30,12 @@ class _OfflineMapsPageState extends State { void _setAsyncState() async { final offlineManager = await OfflineManager.create(); - final stylePacks = await offlineManager.allStylePacks(); final tmpDir = await getTemporaryDirectory(); final tileStore = await TileStore.createAt(tmpDir.uri); + + final stylePacks = await offlineManager.allStylePacks(); final regions = await tileStore.allTileRegions(); + setState(() { _offlineManager = offlineManager; _stylePacks = stylePacks; @@ -57,12 +61,13 @@ class _OfflineMapsPageState extends State { alignment: MainAxisAlignment.center, children: [ ElevatedButton( - onPressed: () => Navigator.of(context).pushNamed(Routes.downloadMap), + onPressed: _onDownloadNewMap, child: Text(locale.downloadNewOfflineMapButton), ), ], ), const Divider(), + _downloadProgressWidget(), ListView.builder( scrollDirection: Axis.vertical, shrinkWrap: true, @@ -107,6 +112,93 @@ class _OfflineMapsPageState extends State { ); } + Widget _downloadProgressWidget() { + final progress = _downloadProgress; + if (progress == null) { + return Container(); + } + return StreamBuilder( + stream: progress.stream, + initialData: 0.0, + builder: (context, snapshot) { + return Column(mainAxisSize: MainAxisSize.min, children: [ + Text("Progress: ${(snapshot.requireData * 100).toStringAsFixed(0)}%"), + LinearProgressIndicator( + value: snapshot.requireData, + ) + ]); + }); + } + + Future _onDownloadNewMap() async { + final regionLoadOptions = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DownloadMapPage( + tileStore: _tileStore, + ))); + if (regionLoadOptions == null) { + return; + } + final styleURI = regionLoadOptions.descriptorsOptions?.first?.styleURI; + if (styleURI == null) { + return; + } + try { + await _downloadStylePack(styleURI); + // Get all style packs because we most likely downloaded the same again. + final stylePacks = await _offlineManager.allStylePacks(); + //setState(() { + // _stylePacks = stylePacks; + //}); + final tileRegion = await _downloadTileRegion( + regionId: DateTime.now().millisecondsSinceEpoch.toString(), regionLoadOptions: regionLoadOptions); + setState(() { + _stylePacks = stylePacks; + _regions = [..._regions, tileRegion]; + _downloadProgress = null; + }); + } catch (e) { + log('failed to download map: $e'); + _displaySnackBar("Failed to map"); + setState(() => _downloadProgress = null); + } + } + + Future _downloadStylePack(String styleURI) async { + final stylePackLoadOptions = StylePackLoadOptions( + glyphsRasterizationMode: GlyphsRasterizationMode.IDEOGRAPHS_RASTERIZED_LOCALLY, + // metadata: {"tag": "test"}, + acceptExpired: false); + + final downloadProgress = StreamController.broadcast(); + setState(() => _downloadProgress = downloadProgress); + final stylePack = await _offlineManager.loadStylePack(styleURI, stylePackLoadOptions, (progress) { + final percentage = progress.completedResourceCount / progress.requiredResourceCount; + if (!downloadProgress.isClosed) { + downloadProgress.sink.add(percentage); + } + }); + downloadProgress.sink.add(1); + downloadProgress.sink.close(); + return stylePack; + } + + Future _downloadTileRegion( + {required String regionId, required TileRegionLoadOptions regionLoadOptions}) async { + final downloadProgress = StreamController.broadcast(); + setState(() => _downloadProgress = downloadProgress); + final tileRegion = await _tileStore.loadTileRegion(regionId, regionLoadOptions, (progress) { + final percentage = progress.completedResourceCount / progress.requiredResourceCount; + if (!downloadProgress.isClosed) { + downloadProgress.sink.add(percentage); + } + }); + downloadProgress.sink.add(1); + downloadProgress.sink.close(); + return tileRegion; + } + Future _onDeleteStylePack(String styleURI, int index) async { try { await _offlineManager.removeStylePack(styleURI); diff --git a/green_walking/lib/routes.dart b/green_walking/lib/routes.dart index a683570..8634821 100644 --- a/green_walking/lib/routes.dart +++ b/green_walking/lib/routes.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'pages/download_map.dart'; import 'pages/feedback.dart'; import 'pages/legal_notice.dart'; import 'pages/map.dart'; @@ -8,7 +7,6 @@ import 'pages/offline_maps.dart'; import 'pages/settings.dart'; class Routes { - static const String downloadMap = 'download-map'; static const String feedback = 'feedback'; static const String legalNotice = 'legal-notice'; static const String map = 'map'; @@ -19,7 +17,6 @@ class Routes { Map getRoutes(BuildContext context) { return { - Routes.downloadMap: (_) => const DownloadMapPage(), Routes.feedback: (_) => const FeedbackPage(), Routes.legalNotice: (_) => const LegalNoticePage(), Routes.map: (_) => const MapPage(), diff --git a/green_walking/lib/widgets/download_map_dialog.dart b/green_walking/lib/widgets/download_map_dialog.dart new file mode 100644 index 0000000..ac27ec0 --- /dev/null +++ b/green_walking/lib/widgets/download_map_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' show TileRegionEstimateResult; + +import '../library/util.dart'; + +class DownloadMapDialog extends StatelessWidget { + const DownloadMapDialog({super.key, required this.estimateRegion}); + + final TileRegionEstimateResult? estimateRegion; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Download area?"), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text(_contentText()), + ], + ), + ), + actions: [ + TextButton( + child: Text("cancel".toUpperCase()), + onPressed: () { + Navigator.of(context).pop(false); + }), + TextButton( + child: Text("download".toUpperCase()), + onPressed: () { + Navigator.of(context).pop(true); + }), + ], + ); + } + + String _contentText() { + final estimate = estimateRegion; + if (estimate == null) { + return 'No estimate'; + } + return 'Estimation:\n${formatBytes(estimate.storageSize, 0)} storage\n${formatBytes(estimate.transferSize, 0)} network transfer'; + } +}