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",
"settingsForceRotationTitle": "Ignore rotation lock",
"settingsForceRotationDescription": "Rotate the screen even when auto rotation is disabled",
"settingsMapProviderTitle": "Map provider",
"settingsAlbumTitle": "Album",
"settingsAlbumDescription": "Customize albums",
"settingsAlbumPageTitle": "Album settings",

View file

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

View file

@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:tuple/tuple.dart';
class GpsMap extends StatelessWidget {
const GpsMap({
class GoogleGpsMap extends StatelessWidget {
const GoogleGpsMap({
Key? key,
required this.center,
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 double zoom;
final VoidCallback? onTap;

View file

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

View file

@ -133,6 +133,11 @@ class Pref {
Future<bool> setNewSharedAlbum(bool 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() =>
provider.getBool(PrefKey.labEnableSharedAlbum);
bool isLabEnableSharedAlbumOr(bool def) => isLabEnableSharedAlbum() ?? def;
@ -308,6 +313,7 @@ enum PrefKey {
isSlideshowShuffle,
isSlideshowRepeat,
isAlbumBrowserShowDate,
gpsMapProvider,
}
extension on PrefKey {
@ -353,6 +359,8 @@ extension on PrefKey {
return "isSlideshowRepeat";
case PrefKey.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:tuple/tuple.dart';
class GpsMap extends StatefulWidget {
const GpsMap({
class GoogleGpsMap extends StatefulWidget {
const GoogleGpsMap({
Key? key,
required this.center,
required this.zoom,
@ -15,15 +15,14 @@ class GpsMap extends StatefulWidget {
}) : super(key: key);
@override
createState() => _GpsMapState();
createState() => _GoogleGpsMapState();
/// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center;
final double zoom;
final void Function()? onTap;
}
class _GpsMapState extends State<GpsMap> {
class _GoogleGpsMapState extends State<GoogleGpsMap> {
@override
initState() {
super.initState();
@ -45,5 +44,5 @@ class _GpsMapState extends State<GpsMap> {
static const _apiKey = "";
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 'download.dart';
export 'file_saver.dart';
export 'gps_map.dart';
export 'google_gps_map.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/theme.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/root_picker.dart';
import 'package:nc_photos/widget/share_folder_picker.dart';
@ -105,17 +106,16 @@ class _SettingsState extends State<Settings> {
label: L10n.global().settingsAccountTitle,
builder: () => AccountSettingsWidget(account: widget.account),
),
if (platform_k.isMobile)
_buildSubSettings(
context,
leading: Icon(
Icons.view_carousel_outlined,
color: AppTheme.getUnfocusedIconColor(context),
),
label: L10n.global().settingsViewerTitle,
description: L10n.global().settingsViewerDescription,
builder: () => _ViewerSettings(),
_buildSubSettings(
context,
leading: Icon(
Icons.view_carousel_outlined,
color: AppTheme.getUnfocusedIconColor(context),
),
label: L10n.global().settingsViewerTitle,
description: L10n.global().settingsViewerDescription,
builder: () => _ViewerSettings(),
),
_buildSubSettings(
context,
leading: Icon(
@ -715,6 +715,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
super.initState();
_screenBrightness = Pref().getViewerScreenBrightnessOr(-1);
_isForceRotation = Pref().isViewerForceRotationOr(false);
_gpsMapProvider = GpsMapProvider.values[Pref().getGpsMapProviderOr(0)];
}
@override
@ -738,19 +739,27 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
SliverList(
delegate: SliverChildListDelegate(
[
SwitchListTile(
title: Text(L10n.global().settingsScreenBrightnessTitle),
subtitle:
Text(L10n.global().settingsScreenBrightnessDescription),
value: _screenBrightness >= 0,
onChanged: (value) =>
_onScreenBrightnessChanged(context, value),
),
SwitchListTile(
title: Text(L10n.global().settingsForceRotationTitle),
subtitle: Text(L10n.global().settingsForceRotationDescription),
value: _isForceRotation,
onChanged: (value) => _onForceRotationChanged(value),
if (platform_k.isMobile)
SwitchListTile(
title: Text(L10n.global().settingsScreenBrightnessTitle),
subtitle:
Text(L10n.global().settingsScreenBrightnessDescription),
value: _screenBrightness >= 0,
onChanged: (value) =>
_onScreenBrightnessChanged(context, value),
),
if (platform_k.isMobile)
SwitchListTile(
title: Text(L10n.global().settingsForceRotationTitle),
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 {
brightness = value;
try {
await ScreenBrightness().setScreenBrightness(value);
await ScreenBrightness()
.setScreenBrightness(value);
} catch (e, stackTrace) {
_log.severe("Failed while setScreenBrightness", e,
stackTrace);
@ -830,6 +840,44 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
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 {
final oldValue = _screenBrightness;
setState(() {
@ -866,6 +914,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> {
late int _screenBrightness;
late bool _isForceRotation;
late GpsMapProvider _gpsMapProvider;
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/iterable_extension.dart';
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/platform/features.dart' as features;
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_property.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:path/path.dart';
import 'package:tuple/tuple.dart';
@ -248,7 +247,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
if (features.isSupportMapView && _gps != null)
SizedBox(
height: 256,
child: platform.GpsMap(
child: GpsMap(
center: _gps!,
zoom: 16,
onTap: _onMapTap,

View file

@ -309,6 +309,13 @@ packages:
description: flutter
source: sdk
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:
dependency: transitive
description:
@ -449,6 +456,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
latlong2:
dependency: transitive
description:
name: latlong2
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
lints:
dependency: transitive
description:
@ -456,6 +470,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
lists:
dependency: transitive
description:
name: lists
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
logging:
dependency: "direct main"
description:
@ -477,6 +498,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mgrs_dart:
dependency: transitive
description:
name: mgrs_dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
mime:
dependency: transitive
description:
@ -617,6 +645,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@ -624,6 +659,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
proj4dart:
dependency: transitive
description:
name: proj4dart
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
provider:
dependency: transitive
description:
@ -846,6 +888,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
transparent_image:
dependency: transitive
description:
name: transparent_image
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
tuple:
dependency: "direct main"
description:
@ -860,6 +909,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
unicode:
dependency: transitive
description:
name: unicode
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
url_launcher:
dependency: "direct main"
description:
@ -1013,6 +1069,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:

View file

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