Skip to content

Commit

Permalink
Enhance offline maps support
Browse files Browse the repository at this point in the history
  • Loading branch information
Xennis committed Jul 20, 2024
1 parent cab8643 commit 31a4bd5
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 118 deletions.
12 changes: 11 additions & 1 deletion green_walking/lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,18 @@
"optionDarkTheme": "Dunkel",
"optionLightTheme": "Hell",
"optionSystem": "System",
"offlineMapsPage": "Offline Karten (Beta)",
"offlineMapsPage": "Offline-Karten (Beta)",
"downloadNewOfflineMapButton": "Neue Karte herunterladen",
"downloadMapTitle": "Offline-Karte herunterladen",
"downloadMapButton": "Gebiet herunterladen",
"offlineMapCardTitle": "Karte",
"offlineMapCardDownloadTime": "Heruntergeladen: {dateTime}",
"offlineMapStyleCardTitle": "Kartenstil: Outdoor",
"offlineMapStyleCardSubtitle": "Stil (z.B. Schriftart, Icons) der Karte.",
"downloadMapDialogTitle": "Gebiet herunterladen?",
"downloadMapDialogCancel": "Abbruch",
"downloadMapDialogConfirm": "Herunterladen",
"downloadMapDialogEstimateLoading": "Speicherplatz wird geschätzt.",
"downloadMapDialogEstimateResult": "Schätzung:\n{storageSize} Speicherplatz\n{networkTransferSize} Netzwerk-Transfer",
"language": "Sprache"
}
58 changes: 57 additions & 1 deletion green_walking/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,63 @@
},
"downloadMapTitle": "Download offline map",
"@downloadMapTitle": {
"description": "Title for download map page"
"description": "Title for download map page."
},
"downloadMapButton": "Download area",
"@downloadMapButton": {
"description": "Title for download map button."
},
"offlineMapCardTitle": "Map",
"@offlineMapCardTitle": {
"description": "Title of a offline map card."
},
"offlineMapCardDownloadTime": "Download: {dateTime}",
"@offlineMapCardDownloadTime": {
"description": "Label of the displayed download time.",
"placeholders": {
"dateTime": {
"type": "String",
"example": "2024-07-20 12:11:02"
}
}
},
"offlineMapStyleCardTitle": "Map Style: Outdoor",
"@offlineMapStyleCardTitle": {
"description": "Title of a offline map style card."
},
"offlineMapStyleCardSubtitle": "Style (e.g. fonts, icons) of the map.",
"@offlineMapStyleCardSubtitle": {
"description": "Subtitle of a offline map style card."
},
"downloadMapDialogTitle": "Download area?",
"@downloadMapDialogTitle": {
"description": "Title of the download map dialog."
},
"downloadMapDialogCancel": "Cancel",
"@downloadMapDialogCancel": {
"description": "Action button to cancel the dialog."
},
"downloadMapDialogConfirm": "Download",
"@downloadMapDialogConfirm": {
"description": "Action button to confirm the dialog."
},
"downloadMapDialogEstimateLoading": "Estimate storage space.",
"@downloadMapDialogEstimateLoading": {
"description": "Text shown while estimating the required storage space."
},
"downloadMapDialogEstimateResult": "Estimation:\n{storageSize} storage\n{networkTransferSize} network transfer",
"@downloadMapDialogEstimateResult": {
"description": "Text shown while estimating the required storage space.",
"placeholders": {
"storageSize": {
"type": "String",
"example": "102 MB"
},
"networkTransferSize": {
"type": "String",
"example": "80 MB"
}
}
},
"language": "Language"
}
45 changes: 45 additions & 0 deletions green_walking/lib/library/offline.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:developer' show log;

import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

import './map_utils.dart';

class OfflineMapMetadata {
static const String downloadTime = "download-time";

static DateTime? parseMetadata(Map<String, Object> metadata) {
try {
final downloadTime = metadata[OfflineMapMetadata.downloadTime] as int;
return DateTime.fromMillisecondsSinceEpoch(downloadTime);
} catch (e) {
log('failed to parse metadata: $e');
}
return null;
}

static Map<String, Object> createMetadata({required DateTime downloadTime}) {
return {OfflineMapMetadata.downloadTime: downloadTime.millisecondsSinceEpoch};
}
}

Future<TileRegionLoadOptions> createRegionLoadOptions(MapboxMap mapboxMap) async {
final (String styleURI, CoordinateBounds cameraBounds, CameraBounds bounds) =
await (mapboxMap.style.getStyleURI(), mapboxMap.getCameraBounds(), mapboxMap.getBounds()).wait;
return TileRegionLoadOptions(
geometry: _coordinateBoundsToPolygon(cameraBounds).toJson(),
descriptorsOptions: [TilesetDescriptorOptions(styleURI: styleURI, minZoom: 6, maxZoom: bounds.maxZoom.floor())],
acceptExpired: true,
metadata: OfflineMapMetadata.createMetadata(downloadTime: DateTime.now()),
networkRestriction: NetworkRestriction.DISALLOW_EXPENSIVE);
}

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)),
]
]);
}
49 changes: 9 additions & 40 deletions green_walking/lib/pages/download_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

import '../library/map_utils.dart';
import '../config.dart';
import '../widgets/download_map_dialog.dart';
import '../library/offline.dart';
import '../widgets/offline/download_map_dialog.dart';

class DownloadMapPage extends StatefulWidget {
const DownloadMapPage({super.key, required this.tileStore});
Expand All @@ -20,8 +20,6 @@ class DownloadMapPage extends StatefulWidget {
class _DownloadMapPageState extends State<DownloadMapPage> {
late MapboxMap _mapboxMap;

bool _isLoading = false;

@override
Widget build(BuildContext context) {
final AppLocalizations locale = AppLocalizations.of(context)!;
Expand All @@ -36,17 +34,9 @@ class _DownloadMapPageState extends State<DownloadMapPage> {
ButtonBar(
alignment: MainAxisAlignment.center,
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"),
ElevatedButton(
onPressed: _onDownloadPressed,
child: Text(locale.downloadMapButton),
),
],
),
Expand Down Expand Up @@ -76,36 +66,15 @@ class _DownloadMapPageState extends State<DownloadMapPage> {
}

Future<void> _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));
final regionLoadOptions = await createRegionLoadOptions(_mapboxMap);

if (!mounted) return;
final shouldDownload =
await showDialog<bool>(context: context, builder: (context) => const DownloadMapDialog(estimateRegion: null));
setState(() => _isLoading = false);
final shouldDownload = await showDialog<bool>(
context: context,
builder: (context) => DownloadMapDialog(tileStore: widget.tileStore, regionLoadOptions: regionLoadOptions));
if (shouldDownload != null && shouldDownload) {
if (!mounted) return;
Navigator.of(context).pop(regionLoadOptions);
}
}
}

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)),
]
]);
}
51 changes: 19 additions & 32 deletions green_walking/lib/pages/offline_maps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
import 'package:path_provider/path_provider.dart';

import '../widgets/offline/offline_map_card.dart';
import 'download_map.dart';

class OfflineMapsPage extends StatefulWidget {
Expand All @@ -18,6 +19,7 @@ class OfflineMapsPage extends StatefulWidget {
class _OfflineMapsPageState extends State<OfflineMapsPage> {
late OfflineManager _offlineManager;
late TileStore _tileStore;

List<StylePack> _stylePacks = [];
List<TileRegion> _regions = [];
StreamController<double>? _downloadProgress;
Expand All @@ -29,12 +31,9 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
}

void _setAsyncState() async {
final offlineManager = await OfflineManager.create();
final tmpDir = await getTemporaryDirectory();
final tileStore = await TileStore.createAt(tmpDir.uri);

final stylePacks = await offlineManager.allStylePacks();
final regions = await tileStore.allTileRegions();
final (offlineManager, tileStore) = await (OfflineManager.create(), TileStore.createAt(tmpDir.uri)).wait;
final (stylePacks, regions) = await (offlineManager.allStylePacks(), tileStore.allTileRegions()).wait;

setState(() {
_offlineManager = offlineManager;
Expand Down Expand Up @@ -74,15 +73,10 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
itemCount: _regions.length,
itemBuilder: (context, index) {
final region = _regions[index];
//final expire = DateTime.fromMillisecondsSinceEpoch(pack.expires!)
return Card(
child: ListTile(
title: Text('Map: ${region.id}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _onDeleteRegion(region.id, index),
),
),
return OfflineMapCard(
region: region,
tileStore: _tileStore,
onDelete: () => _onDeleteRegion(region.id, index),
);
},
),
Expand All @@ -92,18 +86,10 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
itemCount: _stylePacks.length,
itemBuilder: (context, index) {
final pack = _stylePacks[index];
//final name = pack.styleURI == CustomMapboxStyles.outdoor ? "outdoor" : "satellite";
//final expire = DateTime.fromMillisecondsSinceEpoch(pack.expires!)
return Card(
child: ListTile(
title: const Text("Map Style: Outdoor"),
subtitle: const Text("Style (e.g. fonts, icons) of the map."),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: _regions.isEmpty ? () => _onDeleteStylePack(pack.styleURI, index) : null,
),
),
);
return OfflineMapStyleCard(
stylePack: pack,
canBeDeleted: _regions.isEmpty,
onDelete: () => _onDeleteStylePack(pack.styleURI, index));
},
),
],
Expand All @@ -122,6 +108,7 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
initialData: 0.0,
builder: (context, snapshot) {
return Column(mainAxisSize: MainAxisSize.min, children: [
// TODO: Translate
Text("Progress: ${(snapshot.requireData * 100).toStringAsFixed(0)}%"),
LinearProgressIndicator(
value: snapshot.requireData,
Expand All @@ -144,23 +131,21 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
if (styleURI == null) {
return;
}
final regionId = DateTime.now().millisecondsSinceEpoch.toString();
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);
final tileRegion = await _downloadTileRegion(regionId: regionId, regionLoadOptions: regionLoadOptions);
setState(() {
_stylePacks = stylePacks;
_regions = [..._regions, tileRegion];
_downloadProgress = null;
});
} catch (e) {
log('failed to download map: $e');
_displaySnackBar("Failed to map");
// TODO: Translate
_displaySnackBar("Failed to download map");
setState(() => _downloadProgress = null);
}
}
Expand Down Expand Up @@ -205,6 +190,7 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
setState(() {
_stylePacks.removeAt(index);
});
// TODO: Add translation
_displaySnackBar("Deleted map style");
} catch (e) {
log('failed to delete downloaded style pack: $e');
Expand All @@ -218,6 +204,7 @@ class _OfflineMapsPageState extends State<OfflineMapsPage> {
setState(() {
_regions.removeAt(index);
});
// TODO: Add translation
_displaySnackBar("Deleted map");
} catch (e) {
log('failed to remove downloaded region: $e');
Expand Down
44 changes: 0 additions & 44 deletions green_walking/lib/widgets/download_map_dialog.dart

This file was deleted.

Loading

0 comments on commit 31a4bd5

Please sign in to comment.