From c167094b6536f5ec9be700d73c0987489f742b11 Mon Sep 17 00:00:00 2001
From: Pieter Vander Vennet <pietervdvn@posteo.net>
Date: Wed, 22 Jan 2025 01:26:12 +0100
Subject: [PATCH] Merge develop

---
 assets/layers/usersettings/usersettings.json  |  42 +++-
 src/Logic/State/GeoLocationState.ts           |  21 +-
 src/Logic/State/UserRelatedState.ts           |   4 +
 src/Logic/Web/AndroidPolyfill.ts              |  41 ++-
 src/Models/ThemeConfig/TagRenderingConfig.ts  |   3 +
 src/UI/Base/Searchbar.svelte                  |  22 +-
 .../BigComponents/GeolocationIndicator.svelte |   5 +-
 .../TagRendering/TagRenderingQuestion.svelte  |   2 +-
 src/UI/SpecialVisualizations.ts               | 237 +++++++++---------
 src/index_theme.ts.template                   |   1 +
 tsconfig.json                                 |   2 +-
 vite.config.js                                |  10 +
 12 files changed, 251 insertions(+), 139 deletions(-)

diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json
index ed8d50057..fd6ef4d79 100644
--- a/assets/layers/usersettings/usersettings.json
+++ b/assets/layers/usersettings/usersettings.json
@@ -1465,13 +1465,51 @@
       "#force-save-button": "yes"
     },
     {
-      "id": "debug-gps",
+      "id": "debug-gps-group",
       "condition": "mapcomplete-show_debug=yes",
+      "render": {
+        "special": {
+          "type": "group",
+          "header": "debug-gps-title",
+          "labels": "debug-gps"
+        }
+      }
+    },
+    {
+      "id": "debug-gps-title",
+      "labels": ["hidden"],
+      "render": {
+        "en": "GPS info"
+      }
+    },
+    {
+      "id": "debug-gps",
+      "labels": ["hidden"],
       "render": "{gps_all_tags()}"
     },
+
+    {
+      "id": "debug-info-group",
+      "condition": "mapcomplete-show_debug=yes",
+
+      "render": {
+        "special": {
+          "type": "group",
+          "header": "debug-tags-title",
+          "labels": "debug"
+        }
+      }
+    },
+    {
+      "id": "debug-tags-title",
+      "labels": ["hidden"],
+      "render": {
+        "en": "Debug info"
+      }
+    },
     {
       "id": "debug",
-      "condition": "mapcomplete-show_debug=yes",
+      "labels": ["hidden"],
       "render": "{all_tags()}"
     }
   ],
diff --git a/src/Logic/State/GeoLocationState.ts b/src/Logic/State/GeoLocationState.ts
index 4806762b0..34618905c 100644
--- a/src/Logic/State/GeoLocationState.ts
+++ b/src/Logic/State/GeoLocationState.ts
@@ -14,7 +14,7 @@ export interface GeoLocationPointProperties extends GeolocationCoordinates {
 }
 
 /**
- * An abstract representation of the current state of the geolocation.
+ * An abstract representation of the current state of the geolocation, keeping track of permissions and if a location is known.
  */
 export class GeoLocationState {
     /**
@@ -167,8 +167,16 @@ export class GeoLocationState {
 
         if(AndroidPolyfill.inAndroid.data){
             this.permission.setData("requested")
-            AndroidPolyfill.geolocationPermission.addCallbackAndRunD(state => this.permission.set(state))
-            this.startWatching()
+            this.permission.addCallbackAndRunD(p => {
+                if(p === "granted"){
+                    this.startWatching()
+                    return true
+                }
+            })
+            AndroidPolyfill.requestGeoPermission().then(state => {
+                const granted = state.value === "true"
+                this.permission.set(granted ? "granted" : "denied")
+            })
             return
         }
 
@@ -210,6 +218,13 @@ export class GeoLocationState {
      * @private
      */
     private async startWatching() {
+
+        if(AndroidPolyfill.inAndroid.data){
+            AndroidPolyfill.watchLocation( this.currentGPSLocation, location => {
+                console.log(JSON.stringify(location))
+            })
+        }
+
         navigator.geolocation.watchPosition(
              (position: GeolocationPosition) => {
                 this._gpsAvailable.set(true)
diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts
index 27f03e99e..0b109e5ac 100644
--- a/src/Logic/State/UserRelatedState.ts
+++ b/src/Logic/State/UserRelatedState.ts
@@ -416,6 +416,10 @@ export default class UserRelatedState {
                 typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
             _iframe: Utils.isIframe ? "yes" : "no",
         })
+        if(!Utils.runningFromConsole){
+            amendedPrefs.data["_host"] = window.location.host
+            amendedPrefs.data["_path"] = window.location.pathname
+        }
 
         for (const key in Constants.userJourney) {
             amendedPrefs.data["__userjourney_" + key] = Constants.userJourney[key]
diff --git a/src/Logic/Web/AndroidPolyfill.ts b/src/Logic/Web/AndroidPolyfill.ts
index 8b85ef411..e9944acd5 100644
--- a/src/Logic/Web/AndroidPolyfill.ts
+++ b/src/Logic/Web/AndroidPolyfill.ts
@@ -19,9 +19,9 @@ const DatabridgePluginSingleton = registerPlugin<DatabridgePlugin>("Databridge",
                     return { value: "web" }
                 }
                 return null
-            }
+            },
         }
-    }
+    },
 })
 
 export class AndroidPolyfill {
@@ -36,12 +36,18 @@ export class AndroidPolyfill {
      * @private
      */
     private backfillGeolocation(databridgePlugin: DatabridgePlugin) {
-        const src = UIEventSource.FromPromise(databridgePlugin.request({ key: "location:request-permission" }))
+        const src = UIEventSource.FromPromise(databridgePlugin.request({ key: "location:has-permission" }))
         src.addCallbackAndRunD(permission => {
-            AndroidPolyfill._geolocationPermission.set(<"granted" | "denied">permission.value)
+            console.log("> Checking geopermission gave: ", JSON.stringify(permission), permission.value)
+            const granted = permission.value === "true"
+            AndroidPolyfill._geolocationPermission.set(granted ? "granted" : "denied")
         })
     }
 
+    public static async requestGeoPermission(): Promise<{ value: string | object }> {
+        return DatabridgePluginSingleton.request({ key: "location:request-permission" })
+    }
+
     public async init() {
         console.log("Sniffing shell version")
         const shell = await this.databridgePlugin.request({ key: "meta" })
@@ -55,9 +61,9 @@ export class AndroidPolyfill {
     }
 
     public static async requestLoginCodes() {
-        const result = await DatabridgePluginSingleton.request<{oauth_token: string}>({ key: "request:login" })
+        const result = await DatabridgePluginSingleton.request<{ oauth_token: string }>({ key: "request:login" })
         const token: string = result.value.oauth_token
-        console.log("AndroidPolyfill: received oauth_token; trying to pass them to the oauth lib",token)
+        console.log("AndroidPolyfill: received oauth_token; trying to pass them to the oauth lib", token)
         return token
     }
 
@@ -67,7 +73,7 @@ export class AndroidPolyfill {
         console.log("Registering back button callback", callback)
         DatabridgePluginSingleton.request({ key: "backbutton" }).then(ev => {
             console.log("AndroidPolyfill: received backbutton: ", ev)
-            if(ev === null){
+            if (ev === null) {
                 // Probably in web environment
                 return
             }
@@ -84,5 +90,26 @@ export class AndroidPolyfill {
         })
 
     }
+
+    public static watchLocation(writeInto: UIEventSource<GeolocationCoordinates>, callback: (location) => void) {
+        DatabridgePluginSingleton.request({
+            key: "location:watch",
+        }).then((l: {
+            value: { latitude: number, longitude: number, accuraccy: number, altidude: number, heading: number, speed:number }
+        }) => {
+            // example l: {"value":{"latitude":51.0618627,"longitude":3.730468566666667,"accuracy":2.0393495559692383,"altitude":46.408,"heading":168.2969970703125}}
+            console.log("Received location from Android:", JSON.stringify(l))
+            const loc = l.value
+            writeInto.set({
+                latitude: loc.latitude,
+                longitude: loc.longitude,
+                heading: loc.heading,
+                accuracy: loc.accuraccy,
+                altitude: loc.altidude,
+                altitudeAccuracy: undefined,
+                speed: loc.speed,
+            })
+        })
+    }
 }
 
diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts
index 7a763e346..84acfac09 100644
--- a/src/Models/ThemeConfig/TagRenderingConfig.ts
+++ b/src/Models/ThemeConfig/TagRenderingConfig.ts
@@ -126,6 +126,9 @@ export default class TagRenderingConfig {
                 this.id
             )
         }
+        if(json.labels && !Array.isArray( json.labels)){
+            throw (`Invalid labels at ${context}: labels should be a list of strings, but got a ${typeof json.labels}`)
+        }
 
         this.labels = json.labels ?? []
         if (typeof json.classes === "string") {
diff --git a/src/UI/Base/Searchbar.svelte b/src/UI/Base/Searchbar.svelte
index 35d2b4879..6f4d401ea 100644
--- a/src/UI/Base/Searchbar.svelte
+++ b/src/UI/Base/Searchbar.svelte
@@ -7,6 +7,7 @@
   import { ariaLabel } from "../../Utils/ariaLabel"
   import { Translation } from "../i18n/Translation"
   import Backspace from "@babeard/svelte-heroicons/outline/Backspace"
+  import { AndroidPolyfill } from "../../Logic/Web/AndroidPolyfill"
 
   export let value: UIEventSource<string>
   let _value = value.data ?? ""
@@ -36,6 +37,7 @@
   if (autofocus) {
     isFocused.set(true)
   }
+  let isAndroid = AndroidPolyfill.inAndroid
 </script>
 
 <form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
@@ -62,18 +64,20 @@
       use:set_placeholder={placeholder}
       use:ariaLabel={placeholder}
     />
-
-    {#if $value.length > 0}
-      <Backspace
-        on:click={(e) => {
+    {#if !isAndroid}
+      <!-- Show a 'clear field' icon in the searchbar. The android-build already provides this for us, hence the outer if -->
+      {#if $value.length > 0}
+        <Backspace
+          on:click={(e) => {
           value.set("")
           e.preventDefault()
         }}
-        color="var(--button-background)"
-        class="mr-3 h-6 w-6 cursor-pointer"
-      />
-    {:else}
-      <div class="mr-3 w-6" />
+          color="var(--button-background)"
+          class="mr-3 h-6 w-6 cursor-pointer"
+        />
+      {:else}
+        <div class="mr-3 w-6" />
+      {/if}
     {/if}
   </label>
 </form>
diff --git a/src/UI/BigComponents/GeolocationIndicator.svelte b/src/UI/BigComponents/GeolocationIndicator.svelte
index ef66911a7..69cacf5f7 100644
--- a/src/UI/BigComponents/GeolocationIndicator.svelte
+++ b/src/UI/BigComponents/GeolocationIndicator.svelte
@@ -6,6 +6,7 @@
   import Location_locked from "../../assets/svg/Location_locked.svelte"
   import Location_unlocked from "../../assets/svg/Location_unlocked.svelte"
   import Location from "../../assets/svg/Location.svelte"
+  import Location_empty from "../../assets/svg/Location_empty.svelte"
 
   export let state: ThemeViewState
   let geolocationstate = state.geolocation.geolocationState
@@ -31,10 +32,10 @@
 {:else if $geopermission === "denied" || !$isAvailable}
   <Location_refused class={clss} />
 {:else if $geopermission === "prompt"}
-  <Location class={clss} />
+  <Location_empty class={clss} />
 {:else if $geopermission === "requested"}
   <!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
-  <Location class={clss} style="animation: 3s linear 0s infinite normal none running spin;" />
+  <Location_empty class={clss} style="animation: 3s linear 0s infinite normal none running spin;" />
 {:else}
   <Location class={clss} style="animation: 3s linear 0s infinite normal none running spin;" />
 {/if}
diff --git a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte
index ea20fb8a7..fe72bdb85 100644
--- a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte
+++ b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte
@@ -319,7 +319,7 @@
   if (state?.osmConnection) {
     onDestroy(
       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
-        numberOfCs = ud.csCount
+        numberOfCs = ud?.csCount
       })
     )
   }
diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts
index 8844bedbf..8dbb6aab8 100644
--- a/src/UI/SpecialVisualizations.ts
+++ b/src/UI/SpecialVisualizations.ts
@@ -122,7 +122,7 @@ class NearbyImageVis implements SpecialVisualization {
         tags: UIEventSource<Record<string, string>>,
         args: string[],
         feature: Feature,
-        layer: LayerConfig
+        layer: LayerConfig,
     ): SvelteUIElement {
         const isOpen = args[0] === "open"
         const readonly = args[1] === "readonly" || args[1] === "yes"
@@ -189,7 +189,7 @@ class StealViz implements SpecialVisualization {
                                 selectedElement: otherFeature,
                                 state,
                                 layer,
-                            })
+                            }),
                         )
                     }
                     if (elements.length === 1) {
@@ -197,8 +197,8 @@ class StealViz implements SpecialVisualization {
                     }
                     return new Combine(elements).SetClass("flex flex-col")
                 },
-                [state.indexedFeatures.featuresById]
-            )
+                [state.indexedFeatures.featuresById],
+            ),
         )
     }
 
@@ -250,11 +250,11 @@ class CloseNoteViz implements SpecialVisualization {
     public constr(
         state: SpecialVisualizationState,
         tags: UIEventSource<Record<string, string>>,
-        args: string[]
+        args: string[],
     ): SvelteUIElement {
         const { text, icon, idkey, comment, minZoom, zoomButton } = Utils.ParseVisArgs(
             this.args,
-            args
+            args,
         )
 
         return new SvelteUIElement(CloseNoteButton, {
@@ -295,7 +295,7 @@ export class QuestionViz implements SpecialVisualization {
         tags: UIEventSource<Record<string, string>>,
         args: string[],
         feature: Feature,
-        layer: LayerConfig
+        layer: LayerConfig,
     ): SvelteUIElement {
         const labels = args[0]
             ?.split(";")
@@ -327,7 +327,7 @@ export default class SpecialVisualizations {
         for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
             SpecialVisualizations.specialVisualisationsDict.set(
                 specialVisualization.funcName,
-                specialVisualization
+                specialVisualization,
             )
         }
     }
@@ -347,15 +347,15 @@ export default class SpecialVisualizations {
             viz.docs,
             viz.args.length > 0
                 ? MarkdownUtils.table(
-                      ["name", "default", "description"],
-                      viz.args.map((arg) => {
-                          let defaultArg = arg.defaultValue ?? "_undefined_"
-                          if (defaultArg == "") {
-                              defaultArg = "_empty string_"
-                          }
-                          return [arg.name, defaultArg, arg.doc]
-                      })
-                  )
+                    ["name", "default", "description"],
+                    viz.args.map((arg) => {
+                        let defaultArg = arg.defaultValue ?? "_undefined_"
+                        if (defaultArg == "") {
+                            defaultArg = "_empty string_"
+                        }
+                        return [arg.name, defaultArg, arg.doc]
+                    }),
+                )
                 : undefined,
             "#### Example usage of " + viz.funcName,
             "<code>" + example + "</code>",
@@ -364,18 +364,18 @@ export default class SpecialVisualizations {
 
     public static constructSpecification(
         template: string,
-        extraMappings: SpecialVisualization[] = []
+        extraMappings: SpecialVisualization[] = [],
     ): RenderingSpecification[] {
         return SpecialVisualisationUtils.constructSpecification(
             template,
             SpecialVisualizations.specialVisualisationsDict,
-            extraMappings
+            extraMappings,
         )
     }
 
     public static HelpMessage(): string {
         const helpTexts: string[] = SpecialVisualizations.specialVisualizations.map((viz) =>
-            SpecialVisualizations.DocumentationFor(viz)
+            SpecialVisualizations.DocumentationFor(viz),
         )
 
         const firstPart = new Combine([
@@ -408,10 +408,10 @@ export default class SpecialVisualizations {
                         },
                     },
                     null,
-                    "  "
-                )
+                    "  ",
+                ),
             ).SetClass("code"),
-            'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
+            "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
         ])
             .SetClass("flex flex-col")
             .AsMarkdown()
@@ -452,7 +452,7 @@ export default class SpecialVisualizations {
                                     (ud) => ud?.languages ?? []
                                 ),
                             })
-                        })
+                        }),
                     )
                 },
             },
@@ -491,7 +491,7 @@ export default class SpecialVisualizations {
                     state: SpecialVisualizationState,
                     tagSource: UIEventSource<Record<string, string>>,
                     args: string[],
-                    feature: Feature
+                    feature: Feature,
                 ): SvelteUIElement {
                     return new SvelteUIElement(MinimapViz, { state, args, feature, tagSource })
                 },
@@ -503,7 +503,7 @@ export default class SpecialVisualizations {
 
                 constr(
                     state: SpecialVisualizationState,
-                    tagSource: UIEventSource<Record<string, string>>
+                    tagSource: UIEventSource<Record<string, string>>,
                 ): BaseUIElement {
                     return new VariableUiElement(
                         tagSource
@@ -513,7 +513,7 @@ export default class SpecialVisualizations {
                                     return new SvelteUIElement(SplitRoadWizard, { id, state })
                                 }
                                 return undefined
-                            })
+                            }),
                     )
                 },
             },
@@ -527,7 +527,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     if (feature.geometry.type !== "Point") {
                         return undefined
@@ -550,7 +550,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     if (!layer.deletion) {
                         return undefined
@@ -576,7 +576,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ) {
                     if (feature.geometry.type !== "LineString") {
                         return undefined
@@ -608,7 +608,7 @@ export default class SpecialVisualizations {
                     state: SpecialVisualizationState,
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
-                    feature: Feature
+                    feature: Feature,
                 ): BaseUIElement {
                     const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
                     return new SvelteUIElement(CreateNewNote, {
@@ -671,7 +671,7 @@ export default class SpecialVisualizations {
                             .map((tags) => tags[args[0]])
                             .map((wikidata) => {
                                 wikidata = Utils.NoEmpty(
-                                    wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
+                                    wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
                                 )[0]
                                 const entry = Wikidata.LoadWikidataEntry(wikidata)
                                 return new VariableUiElement(
@@ -681,9 +681,9 @@ export default class SpecialVisualizations {
                                         }
                                         const response = <WikidataResponse>e["success"]
                                         return Translation.fromMap(response.labels)
-                                    })
+                                    }),
                                 )
-                            })
+                            }),
                     ),
             },
             new MapillaryLinkVis(),
@@ -697,7 +697,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     _,
                     __,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
             },
             {
@@ -782,7 +782,7 @@ export default class SpecialVisualizations {
                             nameKey: nameKey,
                             fallbackName,
                         },
-                        state.featureSwitchIsTesting
+                        state.featureSwitchIsTesting,
                     )
                     return new SvelteUIElement(StarsBarIcon, {
                         score: reviews.average,
@@ -821,7 +821,7 @@ export default class SpecialVisualizations {
                             nameKey: nameKey,
                             fallbackName,
                         },
-                        state.featureSwitchIsTesting
+                        state.featureSwitchIsTesting,
                     )
                     return new SvelteUIElement(ReviewForm, {
                         reviews,
@@ -859,7 +859,7 @@ export default class SpecialVisualizations {
                             nameKey: nameKey,
                             fallbackName,
                         },
-                        state.featureSwitchIsTesting
+                        state.featureSwitchIsTesting,
                     )
                     return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
                 },
@@ -889,7 +889,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     args: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new Combine([
                         SpecialVisualizations.specialVisualisationsDict
@@ -914,7 +914,7 @@ export default class SpecialVisualizations {
                 constr(
                     state: SpecialVisualizationState,
                     _: UIEventSource<Record<string, string>>,
-                    argument: string[]
+                    argument: string[],
                 ): BaseUIElement {
                     const [text] = argument
                     return new SvelteUIElement(ImportReviewIdentity, { state, text })
@@ -971,7 +971,7 @@ export default class SpecialVisualizations {
                 constr(
                     state: SpecialVisualizationState,
                     tags: UIEventSource<Record<string, string>>,
-                    args: string[]
+                    args: string[],
                 ): SvelteUIElement {
                     const keyToUse = args[0]
                     const prefix = args[1]
@@ -1008,17 +1008,17 @@ export default class SpecialVisualizations {
                                     return undefined
                                 }
                                 const allUnits: Unit[] = [].concat(
-                                    ...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
+                                    ...(state?.theme?.layers?.map((lyr) => lyr.units) ?? []),
                                 )
                                 const unit = allUnits.filter((unit) =>
-                                    unit.isApplicableToKey(key)
+                                    unit.isApplicableToKey(key),
                                 )[0]
                                 if (unit === undefined) {
                                     return value
                                 }
                                 const getCountry = () => tagSource.data._country
                                 return unit.asHumanLongValue(value, getCountry)
-                            })
+                            }),
                     )
                 },
             },
@@ -1072,7 +1072,7 @@ export default class SpecialVisualizations {
                 constr: (state) => {
                     return new SubtleButton(
                         new SvelteUIElement(Trash),
-                        Translations.t.general.removeLocationHistory
+                        Translations.t.general.removeLocationHistory,
                     ).onClick(() => {
                         state.historicalUserLocations.features.setData([])
                         state.selectedElement.setData(undefined)
@@ -1113,10 +1113,10 @@ export default class SpecialVisualizations {
                                                 new SvelteUIElement(NoteCommentElement, {
                                                     comment,
                                                     state,
-                                                })
-                                        )
+                                                }),
+                                        ),
                                 ).SetClass("flex flex-col")
-                            })
+                            }),
                     ),
             },
             {
@@ -1149,7 +1149,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     _: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ) => {
                     return new SvelteUIElement(FeatureTitle, { state, tags, feature, layer })
                 },
@@ -1167,8 +1167,8 @@ export default class SpecialVisualizations {
                     const challenge = Stores.FromPromise(
                         Utils.downloadJsonCached<MaprouletteTask>(
                             `${Maproulette.defaultEndpoint}/challenge/${parentId}`,
-                            24 * 60 * 60 * 1000
-                        )
+                            24 * 60 * 60 * 1000,
+                        ),
                     )
 
                     return new VariableUiElement(
@@ -1193,7 +1193,7 @@ export default class SpecialVisualizations {
                             } else {
                                 return [title, new List(listItems)]
                             }
-                        })
+                        }),
                     )
                 },
                 docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
@@ -1207,15 +1207,15 @@ export default class SpecialVisualizations {
                     "\n" +
                     "```json\n" +
                     "{\n" +
-                    '   "id": "mark_duplicate",\n' +
-                    '   "render": {\n' +
-                    '      "special": {\n' +
-                    '         "type": "maproulette_set_status",\n' +
-                    '         "message": {\n' +
-                    '            "en": "Mark as not found or false positive"\n' +
+                    "   \"id\": \"mark_duplicate\",\n" +
+                    "   \"render\": {\n" +
+                    "      \"special\": {\n" +
+                    "         \"type\": \"maproulette_set_status\",\n" +
+                    "         \"message\": {\n" +
+                    "            \"en\": \"Mark as not found or false positive\"\n" +
                     "         },\n" +
-                    '         "status": "2",\n' +
-                    '         "image": "close"\n' +
+                    "         \"status\": \"2\",\n" +
+                    "         \"image\": \"close\"\n" +
                     "      }\n" +
                     "   }\n" +
                     "}\n" +
@@ -1291,7 +1291,7 @@ export default class SpecialVisualizations {
                                 (l) =>
                                     l.name !== null &&
                                     l.title &&
-                                    state.perLayer.get(l.id) !== undefined
+                                    state.perLayer.get(l.id) !== undefined,
                             )
                             .map(
                                 (l) => {
@@ -1301,8 +1301,8 @@ export default class SpecialVisualizations {
                                     const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
                                     return new StatisticsPanel(fsBboxed)
                                 },
-                                [state.mapProperties.bounds]
-                            )
+                                [state.mapProperties.bounds],
+                            ),
                     )
                 },
             },
@@ -1372,7 +1372,7 @@ export default class SpecialVisualizations {
                 constr(
                     state: SpecialVisualizationState,
                     tagSource: UIEventSource<Record<string, string>>,
-                    args: string[]
+                    args: string[],
                 ): SvelteUIElement {
                     let [text, href, classnames, download, ariaLabel, icon] = args
                     if (download === "") {
@@ -1410,7 +1410,7 @@ export default class SpecialVisualizations {
                             },
                         },
                         null,
-                        "  "
+                        "  ",
                     ) +
                     "\n```",
                 args: [
@@ -1434,7 +1434,7 @@ export default class SpecialVisualizations {
                     featureTags: UIEventSource<Record<string, string>>,
                     args: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ) {
                     const [key, tr, classesRaw] = args
                     let classes = classesRaw ?? ""
@@ -1452,7 +1452,7 @@ export default class SpecialVisualizations {
                                     "Could not create a special visualization for multi(",
                                     args.join(", ") + ")",
                                     "no properties found for object",
-                                    feature.properties.id
+                                    feature.properties.id,
                                 )
                                 return undefined
                             }
@@ -1469,7 +1469,7 @@ export default class SpecialVisualizations {
                                 elements.push(subsTr)
                             }
                             return elements
-                        })
+                        }),
                     )
                 },
             },
@@ -1489,7 +1489,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new VariableUiElement(
                         tagSource.map((tags) => {
@@ -1501,7 +1501,7 @@ export default class SpecialVisualizations {
                                 console.error("Cannot create a translation for", v, "due to", e)
                                 return JSON.stringify(v)
                             }
-                        })
+                        }),
                     )
                 },
             },
@@ -1521,7 +1521,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const key = argument[0]
                     return new SvelteUIElement(FediverseLink, { key, tags, state })
@@ -1543,7 +1543,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     args: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new FixedUiElement("{" + args[0] + "}")
                 },
@@ -1564,7 +1564,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const key = argument[0] ?? "value"
                     return new VariableUiElement(
@@ -1582,12 +1582,12 @@ export default class SpecialVisualizations {
                             } catch (e) {
                                 return new FixedUiElement(
                                     "Could not parse this tag: " +
-                                        JSON.stringify(value) +
-                                        " due to " +
-                                        e
+                                    JSON.stringify(value) +
+                                    " due to " +
+                                    e,
                                 ).SetClass("alert")
                             }
-                        })
+                        }),
                     )
                 },
             },
@@ -1608,7 +1608,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const giggityUrl = argument[0]
                     return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@@ -1624,15 +1624,24 @@ export default class SpecialVisualizations {
                     _: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
-                    const tags = (<ThemeViewState>(
-                        state
-                    )).geolocation.currentUserLocation.features.map(
-                        (features) => features[0]?.properties
+                    const geostate = (<ThemeViewState>(state)).geolocation.geolocationState
+                    const tags = geostate.currentGPSLocation.map(
+                        (geoloc) => {
+                            const tags = { }
+                            for (const k in geoloc ?? {}) {
+                                tags[k] = geoloc[k]
+                            }
+                            tags["_permission"] = geostate.permission.data,
+                            tags["_request_moment"] = geostate.requestMoment.data
+                            return tags
+                        },
+                        [geostate.permission, geostate.requestMoment]
                     )
+
                     return new Combine([
-                        new SvelteUIElement(OrientationDebugPanel, {}),
+                        new SvelteUIElement(OrientationDebugPanel, {}), // compass and gyroscope info
                         new SvelteUIElement(AllTagsPanel, {
                             state,
                             tags,
@@ -1651,7 +1660,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new SvelteUIElement(MarkAsFavourite, {
                         tags: tagSource,
@@ -1671,7 +1680,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new SvelteUIElement(MarkAsFavouriteMini, {
                         tags: tagSource,
@@ -1691,7 +1700,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new SvelteUIElement(DirectionIndicator, { state, feature })
                 },
@@ -1704,7 +1713,7 @@ export default class SpecialVisualizations {
                     state: SpecialVisualizationState,
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
-                    feature: Feature
+                    feature: Feature,
                 ): SvelteUIElement {
                     return new SvelteUIElement(QrCode, { state, tags, feature })
                 },
@@ -1723,7 +1732,7 @@ export default class SpecialVisualizations {
                 constr(
                     state: SpecialVisualizationState,
                     tagSource: UIEventSource<Record<string, string>>,
-                    args: string[]
+                    args: string[],
                 ): BaseUIElement {
                     const key = args[0] === "" ? "_direction:centerpoint" : args[0]
                     return new VariableUiElement(
@@ -1734,11 +1743,11 @@ export default class SpecialVisualizations {
                             })
                             .mapD((value) => {
                                 const dir = GeoOperations.bearingToHuman(
-                                    GeoOperations.parseBearing(value)
+                                    GeoOperations.parseBearing(value),
                                 )
                                 console.log("Human dir", dir)
                                 return Translations.t.general.visualFeedback.directionsAbsolute[dir]
-                            })
+                            }),
                     )
                 },
             },
@@ -1768,7 +1777,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     args: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const url = args[0]
                     const readonly = args[3] === "yes"
@@ -1794,12 +1803,12 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     args: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     return new Toggle(
                         undefined,
                         new SvelteUIElement(LoginButton, { osmConnection: state.osmConnection }),
-                        state.osmConnection.isLoggedIn
+                        state.osmConnection.isLoggedIn,
                     )
                 },
             },
@@ -1837,7 +1846,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const key = argument[0] ?? "website"
                     const useProxy = argument[1] !== "no"
@@ -1845,7 +1854,7 @@ export default class SpecialVisualizations {
                     const isClosed = (argument[4] ?? "yes") === "yes"
 
                     const countryStore: Store<string | undefined> = tags.mapD(
-                        (tags) => tags._country
+                        (tags) => tags._country,
                     )
                     const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
                         if (!tags[key] || tags[key] === "undefined") {
@@ -1867,24 +1876,24 @@ export default class SpecialVisualizations {
                                                 const features =
                                                     await LinkedDataLoader.fetchVeloparkEntry(
                                                         url,
-                                                        loadAll
+                                                        loadAll,
                                                     )
                                                 const feature =
                                                     features.find(
-                                                        (f) => f.properties["ref:velopark"] === url
+                                                        (f) => f.properties["ref:velopark"] === url,
                                                     ) ?? features[0]
                                                 const properties = feature.properties
                                                 properties["ref:velopark"] = url
                                                 console.log(
                                                     "Got properties from velopark:",
-                                                    properties
+                                                    properties,
                                                 )
                                                 return properties
                                             } catch (e) {
                                                 console.error(e)
                                                 throw e
                                             }
-                                        })()
+                                        })(),
                                     )
                                 }
                                 if (country === undefined) {
@@ -1896,29 +1905,29 @@ export default class SpecialVisualizations {
                                             return await LinkedDataLoader.fetchJsonLd(
                                                 url,
                                                 { country },
-                                                useProxy ? "proxy" : "fetch-lod"
+                                                useProxy ? "proxy" : "fetch-lod",
                                             )
                                         } catch (e) {
                                             console.log(
                                                 "Could not get with proxy/download LOD, attempting to download directly. Error for ",
                                                 url,
                                                 "is",
-                                                e
+                                                e,
                                             )
                                             return await LinkedDataLoader.fetchJsonLd(
                                                 url,
                                                 { country },
-                                                "fetch-raw"
+                                                "fetch-raw",
                                             )
                                         }
-                                    })()
+                                    })(),
                                 )
                             },
-                            [countryStore]
+                            [countryStore],
                         )
 
                     externalData.addCallbackAndRunD((lod) =>
-                        console.log("linked_data_from_website received the following data:", lod)
+                        console.log("linked_data_from_website received the following data:", lod),
                     )
 
                     return new Toggle(
@@ -1933,7 +1942,7 @@ export default class SpecialVisualizations {
                             collapsed: isClosed,
                         }),
                         undefined,
-                        sourceUrl.map((url) => !!url)
+                        sourceUrl.map((url) => !!url),
                     )
                 },
             },
@@ -1953,7 +1962,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const text = argument[0]
                     const cssClasses = argument[1]
@@ -1975,7 +1984,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const translation = tagSource.map((tags) => {
                         const layer = state.theme.getMatchingLayer(tags)
@@ -2007,7 +2016,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): SvelteUIElement {
                     return new SvelteUIElement<any, any, any>(ClearCaches, {
                         msg: argument[0] ?? "Clear local caches",
@@ -2032,7 +2041,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
                     selectedElement: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): SvelteUIElement {
                     const [header, labelsStr] = argument
                     const labels = labelsStr.split(";").map((x) => x.trim())
@@ -2055,7 +2064,7 @@ export default class SpecialVisualizations {
                     tags: UIEventSource<Record<string, string>>,
                     argument: string[],
                     selectedElement: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): SvelteUIElement {
                     const t = Translations.t.preset_type
                     const question: QuestionableTagRenderingConfigJson = {
@@ -2095,7 +2104,7 @@ export default class SpecialVisualizations {
                     tagSource: UIEventSource<Record<string, string>>,
                     argument: string[],
                     feature: Feature,
-                    layer: LayerConfig
+                    layer: LayerConfig,
                 ): BaseUIElement {
                     const text = argument[0]
                     return new SubtleButton(undefined, text).onClick(() => {
@@ -2126,7 +2135,7 @@ export default class SpecialVisualizations {
                 "Invalid special visualisation found: funcName is undefined or doesn't match " +
                 regex +
                 invalid.map((sp) => sp.i).join(", ") +
-                '. Did you perhaps type \n  funcName: "funcname" // type declaration uses COLON\ninstead of:\n  funcName = "funcName" // value definition uses EQUAL'
+                ". Did you perhaps type \n  funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n  funcName = \"funcName\" // value definition uses EQUAL"
             )
         }
 
diff --git a/src/index_theme.ts.template b/src/index_theme.ts.template
index 3c947bdd2..fbb362dca 100644
--- a/src/index_theme.ts.template
+++ b/src/index_theme.ts.template
@@ -5,6 +5,7 @@ import MetaTagging from "./src/Logic/MetaTagging";
 import { FixedUiElement } from "./src/UI/Base/FixedUiElement";
 import { Utils } from "./src/Utils"
 import Constants from "./src/Models/Constants"
+import { AndroidPolyfill } from "./src/Logic/Web/AndroidPolyfill"
 
 function webgl_support() {
     try {
diff --git a/tsconfig.json b/tsconfig.json
index e491c3572..80274e478 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,5 +19,5 @@
       "esModuleInterop": true
     }
   },
-  "exclude": ["node_modules", "test", "scripts"]
+  "exclude": ["node_modules", "test", "scripts","android","dist","dist-full"]
 }
diff --git a/vite.config.js b/vite.config.js
index f85973a7e..986daad5b 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -21,11 +21,21 @@ export default defineConfig({
   build: {
     rollupOptions: {
       input,
+      external:[
+        "android"
+      ]
     },
   },
   base: `${ASSET_URL}`,
   plugins ,
   server: {
     port: 1234,
+    watch:{
+      ignored: [
+        "**/android/**",
+        '**/.git/**',
+        '**/dist/**'
+      ]
+    }
   },
 })