From a63d3e43a16a2740af6ab4e855b7fbd15db231ee Mon Sep 17 00:00:00 2001
From: Ming Ming <nkming2@gmail.com>
Date: Wed, 17 Nov 2021 22:41:11 +0800
Subject: [PATCH] Add OSM as alternative map provider

---
 assets/2.0x/gps_map_pin.png                   | Bin 0 -> 3442 bytes
 assets/3.0x/gps_map_pin.png                   | Bin 0 -> 5113 bytes
 assets/gps_map_pin.png                        | Bin 0 -> 1674 bytes
 lib/l10n/app_en.arb                           |   1 +
 lib/l10n/untranslated-messages.txt            |   9 ++
 .../{gps_map.dart => google_gps_map.dart}     |   5 +-
 lib/mobile/platform.dart                      |   2 +-
 lib/pref.dart                                 |   8 ++
 lib/web/{gps_map.dart => google_gps_map.dart} |  11 +-
 lib/web/platform.dart                         |   2 +-
 lib/widget/gps_map.dart                       | 124 ++++++++++++++++++
 lib/widget/settings.dart                      |  97 ++++++++++----
 lib/widget/viewer_detail_pane.dart            |   5 +-
 pubspec.lock                                  |  63 +++++++++
 pubspec.yaml                                  |   1 +
 15 files changed, 290 insertions(+), 38 deletions(-)
 create mode 100644 assets/2.0x/gps_map_pin.png
 create mode 100644 assets/3.0x/gps_map_pin.png
 create mode 100644 assets/gps_map_pin.png
 rename lib/mobile/{gps_map.dart => google_gps_map.dart} (90%)
 rename lib/web/{gps_map.dart => google_gps_map.dart} (78%)
 create mode 100644 lib/widget/gps_map.dart

diff --git a/assets/2.0x/gps_map_pin.png b/assets/2.0x/gps_map_pin.png
new file mode 100644
index 0000000000000000000000000000000000000000..a76280a7b3ee9e9a1b3146d46a14c41835f4006b
GIT binary patch
literal 3442
zcmV-&4UO`NP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h00009a7bBm001Tw
z001Tw0n8z{hyVZp8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H14F5?)
zK~#90)tzf>9Mu)a|7T`*{aQN@NJuawc2I&HLqe0-G!<2;)Pky}YNaYwKeb;FY6}gj
zs<xtnv_hy*Aw)sYh<>0V6oQmS6xxa^p%5M+_Iek$Nvse^qBH^J;rP9~v$HdEPe0hR
zo>_bC^}P4@Y3_LD-m~+cxsP+ty(UB)-nmmgdHzf{3Hc6yH!{#gz$$>60d54K0~iJ{
z48j=*3`oF$B+EzE_w`MLZ2p2!*apl61JcJmTfPn<{Sbt^09@lBn<9XBnejTu<NLaf
z9v$)6OizfE02j7zZ^#<OAA{*W0P8%=S|P?Oki|0{eIFh3Fkjb1LV$Pfyfd~sKY0%`
zJq+Obxvq&z66#|yBqSLLNrEI1BqA^ah7i!1p&B|=!+>fSm5LGoy&4l@_xeocjBh_^
z5ri(l$*o&&g+wm^*jE0xI$6e&L;}e~0trd3I!}WcS*?g{QA1WMf@fc%5$JIvpMSJ#
zaIomcez$}s00VTgXX`JC=@9@4>)(>`IF{GfqcIkDU5+jU#)>LN3k4WFON!$H^n<Sc
z{{Ofx$RVK!@ZU|Fni6$&e+GD`^?XAthLsJCXo$r;FH<mMxKO~TT7a+%p@8_F&Q#_V
z&kL=Bzy&zj({l}290ah%JSP!hMMEQ&B@zJnYmz1eF6FYw>3Z2;__5CZ%&&bdd=3H?
z;PjU^t<;K%Lm+H2&n09TH#8-YkgKoby<k)=;7UGkZyyNHcBIm~2y-<X{t%b|gI!%o
zxv}AW25vLYHO1p-ZAwD&`K_-+Uf1w>HV1>n%t^wtovHLsgR(&iq7dG>Q<fU*_gNo)
zb3B0?LVWlD&=`wjZA%MclI1Zl?*F*E`+=Zr2t<I7&z=1RKs(HIGM>Qo$s{Pmhi{UQ
zB&=<=i@@L~Pj+{IC5ZBUe*&j^dNvCdDNJ`Y>tq>gn_EJC^XH;q7&t#UVd)|9xtJ8&
z?6*J3*Drtp5-j#$`r%7NXiYXp?%@MKy)0u@W0U0!wHZQx%<po272srd_xAv7H`6QX
z8<3C!sr4Mul1QK>F*7D)(7l7V-@eh$GJSg*3`hii#7x)8F)XXE_p3P1xUQkm`h~_M
zVfgwL_$I)qZQFJL*kGntHa7TGl&8p&gligXwtnlAFK+4dt32<={L?Vue)G6qmeJ&a
zU&rKfK992#6Btuf5D}In61Zu}60B_SM&xCQI*cfa={q7rNDlz`ktdm#;Y({c*xkKK
z0)GZT27uK~Ni0bu7PxDEm>C~jyoi6CJBJI|?3~x!m`q|vTO00Jz1npbcSun%Tu=Z2
z1dPd}W2@GmKCQaSbPjx}f{6LMn11*KpgHdD?H^H9>`tYy=fsJM4<7(7WV6_F;skz^
zN?}BG_pFxM8e=RqmM*>9RfZMtMF0u-nt7az$6a+hFXi)isJ9nq#>cC^=k)kE9`5bM
zrF`CTxp7%Wy~Qibgl{^|Y#uxpfB|B{m(6rz+<DcL*L6HGFn}=!Z}X_C;>m#l<Y#$3
zt0!ssa+zm-lb3idz^C2a?XXOuH^!WMIS-sUgNwPG<NH6)<?!0svySg?uyh=V&@s4u
z`*O#bRlsur3^LYR(~Fr(v@)uyc<aIi*Y+Gdf8Nm-1OWB2?eP!^bwj0Vo#MFw1Zp>r
z<C5gKH<G%18JZ9aU$_985a_>h#nHVoNsz7f8`Ap9S3AS|Mp#g@c_@?f?2iu*dtwd%
z9CMLZ%<ejI5}!0Lo(mv|Oa2BoUGb}hf*0lh0EQLC@x78|s6;@c7xvim_ZR{+^VrS2
zo}1oFIkV?~7lGv^B4W><mk;9rsr0=YF6!_WmxDK3UM@!AWxv6@dT|;*24XV;%#OpU
zn(G?9``X?3h-|-saMc5IZ1^z{D;X)69alrQU3;xZ76DsUtZ;m<J;;%x{{_fG*!p}<
zyOLX1u0)du)lf1XcPxM|1Yr3B2(jo3uwmW0PeCYo%nA-0z{Xe%-)?VT_`(JF&boDu
zBkc<IAYY?UD4cd}P#NC$g7)t>h-tt~E4nt{<@555wQF(9a_6J}Tb3`!-D}o3zTf)t
z5O93+v188tA{Fq(8_Gb+JTB-JL&X<FCc>`Go3XB?WzqMpTe1YZHgC3%WEY8ISifFM
zJI-hx{F)EwHIEfTpQ=k8l8nc(yQc?twzgK~)*vE$wY3$yySr`P%*7&@p;*2E>r3S&
zzD!A~#p0WZx<n2@697yUi&*YrLOvl$xVNhdcdc22f1W#sfuW&U1GUCj3}0BW0z2B;
zu(r9mLQd6_)ii4qHA9vUxz2PBd<)Py*}ZKafbRo9eJqBy=EYYdl`xobDVM|Ow3l;P
zT^&|6HMy#8%K0oiiL6#?zuxQYPx}%n=i8(_G4?S{2~g0dmJZc<6ag(0p*5LAYclCc
ze&rZK*xVOHyw8_{;A2>dp(y_Xs5B-pC%9{Y5mg1xR6j+Ayx*^~d=sE+aIgsGr_J<4
zv52Av{(Zj@%otN=Jh8-hsWX-Gd;n(RYn)($`8fck+4K<?ZsDL9Evi=kj{xw5pXK=~
zz)hLVIKY22(_=+-z9pogBbYI2dr9a(M>>7V&oX_Fl#S`)DFCGq?;*u`sEFSTD++YW
z+b@9M`CX>(0<0ex7y{^VGo95m<TU5*3BS;Uz=*Z(C%hcQ^3_0M#gnnv9{`*;(?bQt
z=4FfmL-qxGied0WL6jSa05={zsxYJUg9^Hiv0|tT)NLrbjtR|T58<h<Oy+#$JG>*X
z)e@cQ^vhO%%1}YEuTBh+%XyoV^Ky*j-v?E8U;+>zND@E6RM!rM5V)#%7y00nF;%ts
z{R#A7koyb;x?ZTW@1qQW7tQpTS_rjUYOt{N${0x9+~5D=JhymG;0v~NNq!L1-BQNX
zVvDf2s@OL05G;5XO@E<55x4-i_VrzcW$jpA*Dx7EgRB~c&E){_-mXlhFK9bLS+bZD
zcm_aegn32D2e&zcS*E?)ImrTnZ`1*VB*3;zMq@^4KL$+*jCdG|xDb<C5jo3TH4`4)
zn8{oW%m53aE!y3fP9I|6O*1`QP+U!2Rtqy@$m;JQoQ{u-Jze<@e+hNfw8Z!srejG3
zGp+{UEge-0C|bLn0zda-@m;lqDnLg%eTsnR%(UO$Qe%48EDMJ^(&;yRE3gVe-#4IG
zr#=Fpw5O+^-crljUjPt-@hjg7tCG+K*!02iQGiR69$8I8-bFB1C3I$t7A-Xm<1bz5
z^g!i1f+2JP0HD69+2$?1;zL_7l+RhiObTomyFD+&0g(`({qW%efqrYI3%ZU;&AY*w
zY8V)|4fg1{jROP!^Q-_DL_z=nxVf!uFM#7_df3Y)O16j$8Wpja?=xN15*Y!ef_hPM
zQ51CDab&d>U5Ij5#}iT7$U8Cu0Kmm$@^1h>Gt;9k_j>@Qy0+GtE)f06b*3H?DFMEE
z_^?jk=gstlR&+IK!+>p(HZflDGsedWkrDs^WL-QDKn14U24kx8#(1)*+T0r~i#@Il
z^q9yAFdap4z)ag+#*3q@3j1(+LMT#1PJn4j?g>o@M`0|gVc6pP!Fbja%RDBM0(AEG
ze+b}&nI0=HzA+xR-9uM|q8zArafn1x003ad3+8cF(-yVpo0-c7UU(_&f7DQk$O=G`
zv=^4fczhAlmgRzxWbF0aK#z#50G+ASFbID))8mV%hHTdo`l9xad#prO005B0Of{q$
zMpYwYo!Ppy2w-}l^3~xJX#qC0w;zOMWL(WxFmCU9=3|ods`r+8N2CRy{re4oUoz8^
znzmq2@1#W}A{>bNf76S!001DEO6#+9A&@sJS3^Z1Y+;5%AkuF?Py+&VrPBi-l=f(t
zELI+rS-qYFjIpA;<*#+_iM#*+Ks@7`m@KLlt0C(o4uf8gYTw^VZ3qCA1kjnG%n>=E
z2~pllisZ<*oSG2e=KlVE0HqyR=bEmZu%DZ(Z));oYXPsR2?3^{*UY22f_d%d_>K0%
zhr?fdPzALk0L`=mnh;Pey&=7<`eDLfgEh`MH6uVrD%A(eesoqBb<Q3~RLC1R7@TG9
zs2KqW5Db3XOj|e8u!=#zk!_hwbQdF+Q8NMn00^aqHLq!<$#(1BL4vp@8ebFNwyW_2
zHgM3~j+!yGADv8+J*?e8O$ks_U%>R*-A~MPPSeY$E3bBDGNJu*W+&8?0MnG-@SHwt
z+k>})US#K%niGI{riW873|k~M6C&^XDW~QHKwaHCFn4G#+Zvgq*uO)u$ptkhK*y0I
zlT4UvIq9j+-rjRT+vAp+6953fba>V&(A#rd8F^|_0FuP}Wv2`P?}lNETWV4Oku7`(
zz`Df=L5TOlvd0ZIDL~iYU={>xn0aql3$5K!lL7#MB*&iwa2DXt1?ax8?QzQg0mWZn
U%oodl-2eap07*qoM6N<$g5FYGp#T5?

literal 0
HcmV?d00001

diff --git a/assets/3.0x/gps_map_pin.png b/assets/3.0x/gps_map_pin.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a1eceabe9dad650a26e5dbb0bd932777236ad1f
GIT binary patch
literal 5113
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
XdvTjX3CH)a-*$kyvNobZ(K6(JNTh5>

literal 0
HcmV?d00001

diff --git a/assets/gps_map_pin.png b/assets/gps_map_pin.png
new file mode 100644
index 0000000000000000000000000000000000000000..0920af14e4cb3ab3934d65c7cd512bfed1aed8a7
GIT binary patch
literal 1674
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
UeOYH+tpET307*qoM6N<$f|x8a(*OVf

literal 0
HcmV?d00001

diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index d8d754a2..b63ef9bc 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -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",
diff --git a/lib/l10n/untranslated-messages.txt b/lib/l10n/untranslated-messages.txt
index 8e1232fa..a873507b 100644
--- a/lib/l10n/untranslated-messages.txt
+++ b/lib/l10n/untranslated-messages.txt
@@ -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",
diff --git a/lib/mobile/gps_map.dart b/lib/mobile/google_gps_map.dart
similarity index 90%
rename from lib/mobile/gps_map.dart
rename to lib/mobile/google_gps_map.dart
index 5415f832..9809111b 100644
--- a/lib/mobile/gps_map.dart
+++ b/lib/mobile/google_gps_map.dart
@@ -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;
diff --git a/lib/mobile/platform.dart b/lib/mobile/platform.dart
index 394ce6a2..29cf634f 100644
--- a/lib/mobile/platform.dart
+++ b/lib/mobile/platform.dart
@@ -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';
diff --git a/lib/pref.dart b/lib/pref.dart
index 2a2b8780..abe63b6e 100644
--- a/lib/pref.dart
+++ b/lib/pref.dart
@@ -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";
     }
   }
 }
diff --git a/lib/web/gps_map.dart b/lib/web/google_gps_map.dart
similarity index 78%
rename from lib/web/gps_map.dart
rename to lib/web/google_gps_map.dart
index 4b4fb0bb..5248dc6e 100644
--- a/lib/web/gps_map.dart
+++ b/lib/web/google_gps_map.dart
@@ -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})";
 }
diff --git a/lib/web/platform.dart b/lib/web/platform.dart
index 394ce6a2..29cf634f 100644
--- a/lib/web/platform.dart
+++ b/lib/web/platform.dart
@@ -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';
diff --git a/lib/widget/gps_map.dart b/lib/widget/gps_map.dart
new file mode 100644
index 00000000..1bfc570d
--- /dev/null
+++ b/lib/widget/gps_map.dart
@@ -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;
+}
diff --git a/lib/widget/settings.dart b/lib/widget/settings.dart
index 46dd5b69..3bdc01f7 100644
--- a/lib/widget/settings.dart
+++ b/lib/widget/settings.dart
@@ -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");
 }
diff --git a/lib/widget/viewer_detail_pane.dart b/lib/widget/viewer_detail_pane.dart
index 1b2681cf..2c71d859 100644
--- a/lib/widget/viewer_detail_pane.dart
+++ b/lib/widget/viewer_detail_pane.dart
@@ -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,
diff --git a/pubspec.lock b/pubspec.lock
index ff36f0e5..b219cd09 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 148e1ec3..1c3cb1f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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