Add OSM as alternative map provider

This commit is contained in:
Ming Ming 2021-11-17 22:41:11 +08:00
parent d57be5e77a
commit a63d3e43a1
15 changed files with 290 additions and 38 deletions

BIN
assets/2.0x/gps_map_pin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
assets/3.0x/gps_map_pin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
assets/gps_map_pin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -352,6 +352,7 @@
"settingsScreenBrightnessDescription": "Override system brightness level", "settingsScreenBrightnessDescription": "Override system brightness level",
"settingsForceRotationTitle": "Ignore rotation lock", "settingsForceRotationTitle": "Ignore rotation lock",
"settingsForceRotationDescription": "Rotate the screen even when auto rotation is disabled", "settingsForceRotationDescription": "Rotate the screen even when auto rotation is disabled",
"settingsMapProviderTitle": "Map provider",
"settingsAlbumTitle": "Album", "settingsAlbumTitle": "Album",
"settingsAlbumDescription": "Customize albums", "settingsAlbumDescription": "Customize albums",
"settingsAlbumPageTitle": "Album settings", "settingsAlbumPageTitle": "Album settings",

View file

@ -8,6 +8,7 @@
"settingsShareFolderDialogDescription", "settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription", "settingsShareFolderPickerDescription",
"settingsServerAppSectionTitle", "settingsServerAppSectionTitle",
"settingsMapProviderTitle",
"settingsAlbumTitle", "settingsAlbumTitle",
"settingsAlbumDescription", "settingsAlbumDescription",
"settingsAlbumPageTitle", "settingsAlbumPageTitle",
@ -59,6 +60,7 @@
"settingsShareFolderDialogDescription", "settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription", "settingsShareFolderPickerDescription",
"settingsServerAppSectionTitle", "settingsServerAppSectionTitle",
"settingsMapProviderTitle",
"settingsAlbumTitle", "settingsAlbumTitle",
"settingsAlbumDescription", "settingsAlbumDescription",
"settingsAlbumPageTitle", "settingsAlbumPageTitle",
@ -132,6 +134,7 @@
"settingsScreenBrightnessDescription", "settingsScreenBrightnessDescription",
"settingsForceRotationTitle", "settingsForceRotationTitle",
"settingsForceRotationDescription", "settingsForceRotationDescription",
"settingsMapProviderTitle",
"settingsAlbumTitle", "settingsAlbumTitle",
"settingsAlbumDescription", "settingsAlbumDescription",
"settingsAlbumPageTitle", "settingsAlbumPageTitle",
@ -235,6 +238,10 @@
"defaultButtonLabel" "defaultButtonLabel"
], ],
"es": [
"settingsMapProviderTitle"
],
"fr": [ "fr": [
"collectionsTooltip", "collectionsTooltip",
"settingsAccountTitle", "settingsAccountTitle",
@ -252,6 +259,7 @@
"settingsScreenBrightnessDescription", "settingsScreenBrightnessDescription",
"settingsForceRotationTitle", "settingsForceRotationTitle",
"settingsForceRotationDescription", "settingsForceRotationDescription",
"settingsMapProviderTitle",
"settingsAlbumTitle", "settingsAlbumTitle",
"settingsAlbumDescription", "settingsAlbumDescription",
"settingsAlbumPageTitle", "settingsAlbumPageTitle",
@ -344,6 +352,7 @@
"settingsShareFolderDialogDescription", "settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription", "settingsShareFolderPickerDescription",
"settingsServerAppSectionTitle", "settingsServerAppSectionTitle",
"settingsMapProviderTitle",
"settingsAlbumTitle", "settingsAlbumTitle",
"settingsAlbumDescription", "settingsAlbumDescription",
"settingsAlbumPageTitle", "settingsAlbumPageTitle",

View file

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class GpsMap extends StatelessWidget { class GoogleGpsMap extends StatelessWidget {
const GpsMap({ const GoogleGpsMap({
Key? key, Key? key,
required this.center, required this.center,
required this.zoom, required this.zoom,
@ -41,7 +41,6 @@ class GpsMap extends StatelessWidget {
); );
} }
/// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center; final Tuple2<double, double> center;
final double zoom; final double zoom;
final VoidCallback? onTap; final VoidCallback? onTap;

View file

@ -1,5 +1,5 @@
export 'db_util.dart'; export 'db_util.dart';
export 'download.dart'; export 'download.dart';
export 'file_saver.dart'; export 'file_saver.dart';
export 'gps_map.dart'; export 'google_gps_map.dart';
export 'universal_storage.dart'; export 'universal_storage.dart';

View file

@ -133,6 +133,11 @@ class Pref {
Future<bool> setNewSharedAlbum(bool value) => Future<bool> setNewSharedAlbum(bool value) =>
provider.setBool(PrefKey.newSharedAlbum, value); provider.setBool(PrefKey.newSharedAlbum, value);
int? getGpsMapProvider() => provider.getInt(PrefKey.gpsMapProvider);
int getGpsMapProviderOr(int def) => getGpsMapProvider() ?? def;
Future<bool> setGpsMapProvider(int value) =>
provider.setInt(PrefKey.gpsMapProvider, value);
bool? isLabEnableSharedAlbum() => bool? isLabEnableSharedAlbum() =>
provider.getBool(PrefKey.labEnableSharedAlbum); provider.getBool(PrefKey.labEnableSharedAlbum);
bool isLabEnableSharedAlbumOr(bool def) => isLabEnableSharedAlbum() ?? def; bool isLabEnableSharedAlbumOr(bool def) => isLabEnableSharedAlbum() ?? def;
@ -308,6 +313,7 @@ enum PrefKey {
isSlideshowShuffle, isSlideshowShuffle,
isSlideshowRepeat, isSlideshowRepeat,
isAlbumBrowserShowDate, isAlbumBrowserShowDate,
gpsMapProvider,
} }
extension on PrefKey { extension on PrefKey {
@ -353,6 +359,8 @@ extension on PrefKey {
return "isSlideshowRepeat"; return "isSlideshowRepeat";
case PrefKey.isAlbumBrowserShowDate: case PrefKey.isAlbumBrowserShowDate:
return "isAlbumBrowserShowDate"; return "isAlbumBrowserShowDate";
case PrefKey.gpsMapProvider:
return "gpsMapProvider";
} }
} }
} }

View file

@ -6,8 +6,8 @@ import 'package:/nc_photos/mobile/ui_hack.dart' if (dart.library.html) 'dart:ui'
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class GpsMap extends StatefulWidget { class GoogleGpsMap extends StatefulWidget {
const GpsMap({ const GoogleGpsMap({
Key? key, Key? key,
required this.center, required this.center,
required this.zoom, required this.zoom,
@ -15,15 +15,14 @@ class GpsMap extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
createState() => _GpsMapState(); createState() => _GoogleGpsMapState();
/// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center; final Tuple2<double, double> center;
final double zoom; final double zoom;
final void Function()? onTap; final void Function()? onTap;
} }
class _GpsMapState extends State<GpsMap> { class _GoogleGpsMapState extends State<GoogleGpsMap> {
@override @override
initState() { initState() {
super.initState(); super.initState();
@ -45,5 +44,5 @@ class _GpsMapState extends State<GpsMap> {
static const _apiKey = ""; static const _apiKey = "";
String get viewType => String get viewType =>
"mapIframe(${widget.center.item1},${widget.center.item2})"; "googleMapIframe(${widget.center.item1},${widget.center.item2})";
} }

View file

@ -1,5 +1,5 @@
export 'db_util.dart'; export 'db_util.dart';
export 'download.dart'; export 'download.dart';
export 'file_saver.dart'; export 'file_saver.dart';
export 'gps_map.dart'; export 'google_gps_map.dart';
export 'universal_storage.dart'; export 'universal_storage.dart';

124
lib/widget/gps_map.dart Normal file
View file

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:nc_photos/pref.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
enum GpsMapProvider {
// the order must not be changed
google,
osm,
}
extension GpsMapProviderExtension on GpsMapProvider {
String toUserString() {
switch (this) {
case GpsMapProvider.google:
return "Google Maps";
case GpsMapProvider.osm:
return "OpenStreetMap";
}
}
}
class GpsMap extends StatelessWidget {
const GpsMap({
Key? key,
required this.center,
required this.zoom,
this.onTap,
}) : super(key: key);
@override
build(BuildContext context) {
if (GpsMapProvider.values[Pref().getGpsMapProviderOr(0)] ==
GpsMapProvider.osm) {
return _OsmGpsMap(
center: center,
zoom: zoom,
onTap: onTap,
);
} else {
return _GoogleGpsMap(
center: center,
zoom: zoom,
onTap: onTap,
);
}
}
/// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center;
final double zoom;
final void Function()? onTap;
}
typedef _GoogleGpsMap = platform.GoogleGpsMap;
class _OsmGpsMap extends StatelessWidget {
const _OsmGpsMap({
Key? key,
required this.center,
required this.zoom,
this.onTap,
}) : super(key: key);
@override
build(BuildContext context) {
const double pinSize = 48;
final centerLl = LatLng(center.item1, center.item2);
return GestureDetector(
onTap: () {
launch(
"https://www.openstreetmap.org/?mlat=${center.item1}&mlon=${center.item2}#map=${zoom.toInt()}/${center.item1}/${center.item2}");
},
behavior: HitTestBehavior.opaque,
// IgnorePointer is needed to prevent FlutterMap absorbing all pointer
// events
child: IgnorePointer(
child: FlutterMap(
options: MapOptions(
center: centerLl,
zoom: zoom,
allowPanning: false,
enableScrollWheel: false,
interactiveFlags: InteractiveFlag.none,
),
layers: [
TileLayerOptions(
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attributionBuilder: (_) {
return const Text(
"© OpenStreetMap contributors",
style: TextStyle(color: Colors.black),
);
},
),
MarkerLayerOptions(
markers: [
Marker(
width: pinSize,
height: pinSize,
point: centerLl,
anchorPos: AnchorPos.align(AnchorAlign.top),
builder: (context) => const Image(
image: AssetImage("gps_map_pin.png"),
),
),
],
),
],
),
),
);
}
final Tuple2<double, double> center;
final double zoom;
final void Function()? onTap;
}

View file

@ -17,6 +17,7 @@ import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/fancy_option_picker.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart';
import 'package:nc_photos/widget/gps_map.dart';
import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/root_picker.dart'; import 'package:nc_photos/widget/root_picker.dart';
import 'package:nc_photos/widget/share_folder_picker.dart'; import 'package:nc_photos/widget/share_folder_picker.dart';
@ -105,17 +106,16 @@ class _SettingsState extends State<Settings> {
label: L10n.global().settingsAccountTitle, label: L10n.global().settingsAccountTitle,
builder: () => AccountSettingsWidget(account: widget.account), builder: () => AccountSettingsWidget(account: widget.account),
), ),
if (platform_k.isMobile) _buildSubSettings(
_buildSubSettings( context,
context, leading: Icon(
leading: Icon( Icons.view_carousel_outlined,
Icons.view_carousel_outlined, color: AppTheme.getUnfocusedIconColor(context),
color: AppTheme.getUnfocusedIconColor(context),
),
label: L10n.global().settingsViewerTitle,
description: L10n.global().settingsViewerDescription,
builder: () => _ViewerSettings(),
), ),
label: L10n.global().settingsViewerTitle,
description: L10n.global().settingsViewerDescription,
builder: () => _ViewerSettings(),
),
_buildSubSettings( _buildSubSettings(
context, context,
leading: Icon( leading: Icon(
@ -715,6 +715,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
super.initState(); super.initState();
_screenBrightness = Pref().getViewerScreenBrightnessOr(-1); _screenBrightness = Pref().getViewerScreenBrightnessOr(-1);
_isForceRotation = Pref().isViewerForceRotationOr(false); _isForceRotation = Pref().isViewerForceRotationOr(false);
_gpsMapProvider = GpsMapProvider.values[Pref().getGpsMapProviderOr(0)];
} }
@override @override
@ -738,19 +739,27 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
SwitchListTile( if (platform_k.isMobile)
title: Text(L10n.global().settingsScreenBrightnessTitle), SwitchListTile(
subtitle: title: Text(L10n.global().settingsScreenBrightnessTitle),
Text(L10n.global().settingsScreenBrightnessDescription), subtitle:
value: _screenBrightness >= 0, Text(L10n.global().settingsScreenBrightnessDescription),
onChanged: (value) => value: _screenBrightness >= 0,
_onScreenBrightnessChanged(context, value), onChanged: (value) =>
), _onScreenBrightnessChanged(context, value),
SwitchListTile( ),
title: Text(L10n.global().settingsForceRotationTitle), if (platform_k.isMobile)
subtitle: Text(L10n.global().settingsForceRotationDescription), SwitchListTile(
value: _isForceRotation, title: Text(L10n.global().settingsForceRotationTitle),
onChanged: (value) => _onForceRotationChanged(value), subtitle:
Text(L10n.global().settingsForceRotationDescription),
value: _isForceRotation,
onChanged: (value) => _onForceRotationChanged(value),
),
ListTile(
title: Text(L10n.global().settingsMapProviderTitle),
subtitle: Text(_gpsMapProvider.toUserString()),
onTap: () => _onMapProviderTap(context),
), ),
], ],
), ),
@ -789,7 +798,8 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
onChangeEnd: (value) async { onChangeEnd: (value) async {
brightness = value; brightness = value;
try { try {
await ScreenBrightness().setScreenBrightness(value); await ScreenBrightness()
.setScreenBrightness(value);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.severe("Failed while setScreenBrightness", e, _log.severe("Failed while setScreenBrightness", e,
stackTrace); stackTrace);
@ -830,6 +840,44 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
void _onForceRotationChanged(bool value) => _setForceRotation(value); void _onForceRotationChanged(bool value) => _setForceRotation(value);
Future<void> _onMapProviderTap(BuildContext context) async {
final oldValue = _gpsMapProvider;
final newValue = await showDialog<GpsMapProvider>(
context: context,
builder: (context) => FancyOptionPicker(
items: GpsMapProvider.values
.map((provider) => FancyOptionPickerItem(
label: provider.toUserString(),
isSelected: provider == oldValue,
onSelect: () {
_log.info(
"[_onMapProviderTap] Set map provider: ${provider.toUserString()}");
Navigator.of(context).pop(provider);
},
))
.toList(),
),
);
if (newValue == null || newValue == oldValue) {
return;
}
setState(() {
_gpsMapProvider = newValue;
});
try {
await Pref().setGpsMapProvider(newValue.index);
} catch (e, stackTrace) {
_log.severe("[_onMapProviderTap] Failed writing pref", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().writePreferenceFailureNotification),
duration: k.snackBarDurationNormal,
));
setState(() {
_gpsMapProvider = oldValue;
});
}
}
Future<void> _setScreenBrightness(int value) async { Future<void> _setScreenBrightness(int value) async {
final oldValue = _screenBrightness; final oldValue = _screenBrightness;
setState(() { setState(() {
@ -866,6 +914,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
late int _screenBrightness; late int _screenBrightness;
late bool _isForceRotation; late bool _isForceRotation;
late GpsMapProvider _gpsMapProvider;
static final _log = Logger("widget.settings._ViewerSettingsState"); static final _log = Logger("widget.settings._ViewerSettingsState");
} }

View file

@ -23,8 +23,6 @@ import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/notified_action.dart';
import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
@ -36,6 +34,7 @@ import 'package:nc_photos/use_case/remove_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/album_picker_dialog.dart'; import 'package:nc_photos/widget/album_picker_dialog.dart';
import 'package:nc_photos/widget/gps_map.dart';
import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart'; import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -248,7 +247,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
if (features.isSupportMapView && _gps != null) if (features.isSupportMapView && _gps != null)
SizedBox( SizedBox(
height: 256, height: 256,
child: platform.GpsMap( child: GpsMap(
center: _gps!, center: _gps!,
zoom: 16, zoom: 16,
onTap: _onMapTap, onTap: _onMapTap,

View file

@ -309,6 +309,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_map:
dependency: "direct main"
description:
name: flutter_map
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -449,6 +456,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.1"
latlong2:
dependency: transitive
description:
name: latlong2
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -456,6 +470,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
lists:
dependency: transitive
description:
name: lists
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
logging: logging:
dependency: "direct main" dependency: "direct main"
description: description:
@ -477,6 +498,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.7.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -617,6 +645,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
positioned_tap_detector_2:
dependency: transitive
description:
name: positioned_tap_detector_2
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -624,6 +659,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
proj4dart:
dependency: transitive
description:
name: proj4dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -846,6 +888,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.0" version: "0.4.0"
transparent_image:
dependency: transitive
description:
name: transparent_image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
tuple: tuple:
dependency: "direct main" dependency: "direct main"
description: description:
@ -860,6 +909,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
unicode:
dependency: transitive
description:
name: unicode
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1013,6 +1069,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.10" version: "2.2.10"
wkt_parser:
dependency: transitive
description:
name: wkt_parser
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
woozy_search: woozy_search:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -43,6 +43,7 @@ dependencies:
url: https://gitlab.com/nkming2/exifdart.git url: https://gitlab.com/nkming2/exifdart.git
ref: 1.1.0 ref: 1.1.0
flutter_bloc: ^7.0.0 flutter_bloc: ^7.0.0
flutter_map: ^0.14.0
flutter_staggered_grid_view: flutter_staggered_grid_view:
git: git:
url: https://gitlab.com/nkming2/flutter_staggered_grid_view url: https://gitlab.com/nkming2/flutter_staggered_grid_view