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 300fb82 commit cab8643
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 162 deletions.
1 change: 0 additions & 1 deletion green_walking/lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"openLocationDetailsSemanticLabel": "Details anzeigen",
"followLocationToast": "Position folgen",
"followLocationOffToast": "Position folgen aus",
"optionDarkTheme": "Dark Theme",
"optionTheme": "Design",
"optionDarkTheme": "Dunkel",
"optionLightTheme": "Hell",
Expand Down
2 changes: 1 addition & 1 deletion green_walking/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions green_walking/lib/library/map_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class PuckLocation {
extension MapboxMapPosition on MapboxMap {
Future<Position?> 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;
Expand All @@ -33,4 +33,10 @@ extension MapboxMapPosition on MapboxMap {
return null;
}
}

Future<CoordinateBounds> getCameraBounds() async {
final CameraState cameraState = await getCameraState();
return coordinateBoundsForCamera(CameraOptions(
center: cameraState.center, zoom: cameraState.zoom, bearing: cameraState.bearing, pitch: cameraState.pitch));
}
}
217 changes: 65 additions & 152 deletions green_walking/lib/pages/download_map.dart
Original file line number Diff line number Diff line change
@@ -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<DownloadMapPage> createState() => _DownloadMapPageState();
}

class _DownloadMapPageState extends State<DownloadMapPage> {
// Only the outdoor map can be downloaded
static const STYLE_URI = CustomMapboxStyles.outdoor;

final StreamController<double> _stylePackProgress = StreamController.broadcast();
final StreamController<double> _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) {
Expand All @@ -53,146 +32,80 @@ class _DownloadMapPageState extends State<DownloadMapPage> {
),
body: Center(
child: Column(
children: <Widget>[
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: <Widget>[
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<Polygon> _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<TileRegionLoadOptions> _regionLoadOptions() async {
final bounds = await _cameraBoundingBox();
return TileRegionLoadOptions(
geometry: bounds.toJson(),
descriptorsOptions: [TilesetDescriptorOptions(styleURI: STYLE_URI, minZoom: 0, maxZoom: 16)],
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));

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

Future<void> _estimateTileRegion() async {
setState(() {
_estimateResult = null;
});
final estimated = await _tileStore.estimateTileRegion("", await _regionLoadOptions(), null, null);
setState(() {
_estimateResult = estimated;
});
}

Future<void> _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<void> _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<void> _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)),
]
]);
}
Loading

0 comments on commit cab8643

Please sign in to comment.