Android: setup data bridge, polyfill geolocation

This commit is contained in:
Pieter Vander Vennet 2024-12-10 04:28:50 +01:00
parent 17450deb82
commit 76e9381650
23 changed files with 187 additions and 106 deletions

View file

@ -0,0 +1,12 @@
# Creating an APK from the code
We are using capacitor. This is a tool which packages some files into an Android shell.
## Developing
1. Build all the necessary files.
a. If no layer/theme changes were made, `npm run build` is sufficient
b. Otherwise, run `npm run prepare-deploy`.
2. All the web assets will now be in `dist/`
3. Run `scripts/prepareAndroid.sh`
4. Switch to Android Studio, open the subproject "Android" in it

View file

@ -3238,7 +3238,7 @@
}, },
{ {
"id": "name", "id": "name",
"question":{ "question": {
"en": "What is the name of this place?" "en": "What is the name of this place?"
}, },
"render": { "render": {

View file

@ -460,7 +460,8 @@
"activateButton": "Hjælp med at oversætte MapComplete", "activateButton": "Hjælp med at oversætte MapComplete",
"missing": "{count} uoversatte strenge" "missing": "{count} uoversatte strenge"
}, },
"userinfo": {}, "userinfo": {
},
"validation": { "validation": {
"color": { "color": {
"description": "En farve eller hex-kode" "description": "En farve eller hex-kode"

View file

@ -1 +1,2 @@
{} {
}

View file

@ -94,7 +94,8 @@
"question_opinion": "Kamusta ang iyong karanasan?", "question_opinion": "Kamusta ang iyong karanasan?",
"reviewPlaceholder": "Ilarawan ang iyong karanasan…" "reviewPlaceholder": "Ilarawan ang iyong karanasan…"
}, },
"translations": {}, "translations": {
},
"unknown": { "unknown": {
"clear": "Tanggalin ang sagot" "clear": "Tanggalin ang sagot"
}, },

View file

@ -1 +1,2 @@
{} {
}

View file

@ -150,7 +150,8 @@
"split": { "split": {
"cancel": "Batal" "cancel": "Batal"
}, },
"translations": {}, "translations": {
},
"validation": { "validation": {
"date": { "date": {
"description": "Tanggal, dimulai dari tahun" "description": "Tanggal, dimulai dari tahun"

View file

@ -959,6 +959,30 @@
"render": "BBQ" "render": "BBQ"
} }
}, },
"beehive": {
"description": "Layer showing beehives",
"name": "Beehives",
"presets": {
"0": {
"title": "a beehive"
}
},
"tagRenderings": {
"capacity": {
"freeform": {
"placeholder": "Number of beehives"
},
"mappings": {
"0": {
"then": "There is 1 beehive"
}
},
"question": "How many beehives are there?",
"render": "There are {capacity} beehives"
}
},
"title": "Beehive"
},
"bench": { "bench": {
"description": "A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them.", "description": "A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them.",
"filter": { "filter": {
@ -6358,6 +6382,16 @@
"render": "Information board" "render": "Information board"
} }
}, },
"insect_hotel": {
"description": "Layer showing insect hotels",
"name": "Insect Hotels",
"presets": {
"0": {
"title": "an insect hotel"
}
},
"title": "Insect Hotel"
},
"item_with_image": { "item_with_image": {
"name": "Items with at least one image", "name": "Items with at least one image",
"title": { "title": {
@ -8682,6 +8716,9 @@
"render": "This elevator goes to floors {level}" "render": "This elevator goes to floors {level}"
} }
}, },
"name": {
"question": "What is the name of this place?"
},
"nothing_known": { "nothing_known": {
"render": { "render": {
"special": { "special": {

View file

@ -5322,6 +5322,16 @@
"render": "Informatiebord" "render": "Informatiebord"
} }
}, },
"insect_hotel": {
"description": "Laag met insectenhotels",
"name": "Insectenhotels",
"presets": {
"0": {
"title": "een insectenhotel"
}
},
"title": "Insectenhotel"
},
"kerbs": { "kerbs": {
"description": "Een laag met stoepranden.", "description": "Een laag met stoepranden.",
"filter": { "filter": {

View file

@ -274,7 +274,8 @@
"importInspector": { "importInspector": {
"title": "Inspiser og håndter importnotater" "title": "Inspiser og håndter importnotater"
}, },
"importLayer": {}, "importLayer": {
},
"index": { "index": {
"intro": "MapComplete er en OpenStreetMap-viser og redigerer, som viser deg info om funksjoner for et gitt tema og tillater oppdatering av det.", "intro": "MapComplete er en OpenStreetMap-viser og redigerer, som viser deg info om funksjoner for et gitt tema og tillater oppdatering av det.",
"logIn": "Logg inn for å vise tema du har besøkt tidligere", "logIn": "Logg inn for å vise tema du har besøkt tidligere",
@ -369,7 +370,8 @@
"activateButton": "Bistå oversettelsen av MapComplete", "activateButton": "Bistå oversettelsen av MapComplete",
"missing": "{count} uoversatte strenger" "missing": "{count} uoversatte strenger"
}, },
"userinfo": {}, "userinfo": {
},
"validation": { "validation": {
"color": { "color": {
"description": "En farge eller heksadesimal kode" "description": "En farge eller heksadesimal kode"

View file

@ -53,7 +53,8 @@
"search": "ستھتیاں وچ کھوجو", "search": "ستھتیاں وچ کھوجو",
"searching": "کھوجیا جا رہا اے۔ ۔ ۔" "searching": "کھوجیا جا رہا اے۔ ۔ ۔"
}, },
"sharescreen": {}, "sharescreen": {
},
"weekdays": { "weekdays": {
"abbreviations": { "abbreviations": {
"friday": "جـ", "friday": "جـ",

View file

@ -1 +1,2 @@
{} {
}

View file

@ -488,6 +488,11 @@
"override": { "override": {
"=name": "Sport places without etymology information" "=name": "Sport places without etymology information"
} }
},
"8": {
"override": {
"=name": "Parks without etymology information"
}
} }
}, },
"shortDescription": "What is the origin of a toponym?", "shortDescription": "What is the origin of a toponym?",
@ -721,6 +726,10 @@
"description": "On this map, publicly accessible indoor places are shown", "description": "On this map, publicly accessible indoor places are shown",
"title": "Indoors" "title": "Indoors"
}, },
"insects": {
"description": "Insect hotels provide shelter for insects.",
"title": "Insect Hotels"
},
"items_with_image": { "items_with_image": {
"description": "A map showing all items on OSM which have an image. This theme is a very bad fit for MapComplete as someone is not able to directly add a picture. However, this theme is mostly here to include this all into the database, which'll allow this to quickly fetch images nearby for other features", "description": "A map showing all items on OSM which have an image. This theme is a very bad fit for MapComplete as someone is not able to directly add a picture. However, this theme is mostly here to include this all into the database, which'll allow this to quickly fetch images nearby for other features",
"title": "All items with images" "title": "All items with images"

View file

@ -777,6 +777,10 @@
"description": "Op deze kaart worden publiek toegankelijke binnenruimtes getoond", "description": "Op deze kaart worden publiek toegankelijke binnenruimtes getoond",
"title": "Binnenruimtes" "title": "Binnenruimtes"
}, },
"insects": {
"description": "Insectenhotels bieden onderdak aan insecten.",
"title": "Insectenhotels"
},
"items_with_image": { "items_with_image": {
"description": "Een kaart die alle items op OSM toont die een afbeelding hebben. Dit thema past heel slecht bij MapComplete omdat het niet mogelijk is een afbeelding toe te voegen. Dit thema is er vooral om alles in de database op te nemen, waardoor het snel afbeeldingen in de buurt kan ophalen voor andere functies", "description": "Een kaart die alle items op OSM toont die een afbeelding hebben. Dit thema past heel slecht bij MapComplete omdat het niet mogelijk is een afbeelding toe te voegen. Dit thema is er vooral om alles in de database op te nemen, waardoor het snel afbeeldingen in de buurt kan ophalen voor andere functies",
"title": "Alle items met afbeeldingen" "title": "Alle items met afbeeldingen"

View file

@ -532,7 +532,8 @@
} }
} }
}, },
"importLayer": {}, "importLayer": {
},
"index": { "index": {
"about": "Про MapComplete", "about": "Про MapComplete",
"intro": "Тематичні мапи, до створення яких ви можете долучитися", "intro": "Тематичні мапи, до створення яких ви можете долучитися",
@ -591,7 +592,8 @@
"removedKeys": "Наступні ключі будуть видалені:", "removedKeys": "Наступні ключі будуть видалені:",
"title": "Позначити як невідомий?" "title": "Позначити як невідомий?"
}, },
"userinfo": {}, "userinfo": {
},
"validation": { "validation": {
"opening_hours": { "opening_hours": {
"description": "Години роботи" "description": "Години роботи"

15
package-lock.json generated
View file

@ -12,7 +12,6 @@
"@capacitor/android": "^6.1.2", "@capacitor/android": "^6.1.2",
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@capacitor/core": "^6.1.2", "@capacitor/core": "^6.1.2",
"@capacitor/geolocation": "^6.0.1",
"@comunica/core": "^3.0.1", "@comunica/core": "^3.0.1",
"@comunica/query-sparql": "^3.0.1", "@comunica/query-sparql": "^3.0.1",
"@comunica/query-sparql-link-traversal": "^0.3.0", "@comunica/query-sparql-link-traversal": "^0.3.0",
@ -2041,14 +2040,6 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@capacitor/geolocation": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-6.0.1.tgz",
"integrity": "sha512-QOkIrSzG6E0vD2MF3gZmtuILQiuVro4LGPjqrUjCzhX10zl/4lx6bq4T+hj2YLUmMUnCiV1hWTOJHcpdVRMz7w==",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@colors/colors": { "node_modules/@colors/colors": {
"version": "1.6.0", "version": "1.6.0",
"license": "MIT", "license": "MIT",
@ -24098,12 +24089,6 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"@capacitor/geolocation": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-6.0.1.tgz",
"integrity": "sha512-QOkIrSzG6E0vD2MF3gZmtuILQiuVro4LGPjqrUjCzhX10zl/4lx6bq4T+hj2YLUmMUnCiV1hWTOJHcpdVRMz7w==",
"requires": {}
},
"@colors/colors": { "@colors/colors": {
"version": "1.6.0" "version": "1.6.0"
}, },

View file

@ -145,7 +145,10 @@
"generate:summaryCache": "vite-node scripts/generateSummaryTileCache.ts", "generate:summaryCache": "vite-node scripts/generateSummaryTileCache.ts",
"create:database": "vite-node scripts/osm2pgsql/createNewDatabase.ts", "create:database": "vite-node scripts/osm2pgsql/createNewDatabase.ts",
"delete:database:old": "vite-node scripts/osm2pgsql/deleteOldDbs.ts", "delete:database:old": "vite-node scripts/osm2pgsql/deleteOldDbs.ts",
"upload:panoramax": "vite-node scripts/ImgurToPanoramax.ts # && josm imgur_to_panoramax.osc" "upload:panoramax": "vite-node scripts/ImgurToPanoramax.ts # && josm imgur_to_panoramax.osc",
"#": "Android development"
}, },
"keywords": [ "keywords": [
"OpenStreetMap", "OpenStreetMap",
@ -163,7 +166,6 @@
"@capacitor/android": "^6.1.2", "@capacitor/android": "^6.1.2",
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@capacitor/core": "^6.1.2", "@capacitor/core": "^6.1.2",
"@capacitor/geolocation": "^6.0.1",
"@comunica/core": "^3.0.1", "@comunica/core": "^3.0.1",
"@comunica/query-sparql": "^3.0.1", "@comunica/query-sparql": "^3.0.1",
"@comunica/query-sparql-link-traversal": "^0.3.0", "@comunica/query-sparql-link-traversal": "^0.3.0",

View file

@ -3,7 +3,6 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import { Translation } from "../../UI/i18n/Translation" import { Translation } from "../../UI/i18n/Translation"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import { Geolocation } from "@capacitor/geolocation"
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied" export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
@ -179,20 +178,19 @@ export class GeoLocationState {
this.permission.setData("requested") this.permission.setData("requested")
try { try {
const status = await navigator?.permissions?.query({ name: "geolocation" }) const status = await navigator?.permissions?.query({ name: "geolocation" })
const self = this
console.log("Got geolocation state", status.state) console.log("Got geolocation state", status.state)
if (status.state === "granted" || status.state === "denied") { if (status.state === "granted" || status.state === "denied") {
self.permission.setData(status.state) this.permission.setData(status.state)
self.startWatching() this.startWatching()
return return
} }
status.addEventListener("change", () => { status.addEventListener("change", () => {
self.permission.setData(status.state) this.permission.setData(status.state)
}) })
// The code above might have reset it to 'prompt', but we _did_ request permission! // The code above might have reset it to 'prompt', but we _did_ request permission!
this.permission.setData("requested") this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup... // We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching() this.startWatching()
} catch (e) { } catch (e) {
console.error("Could not get permission:", e) console.error("Could not get permission:", e)
} }
@ -203,46 +201,29 @@ export class GeoLocationState {
* @private * @private
*/ */
private async startWatching() { private async startWatching() {
console.log("Starts watching", navigator.geolocation, Geolocation) navigator.geolocation.watchPosition(
const self = this (position: GeolocationPosition) => {
try { this._gpsAvailable.set(true)
await Geolocation.requestPermissions({ permissions: ["location"] }) this.currentGPSLocation.setData(position.coords)
console.log("Requested permission") this._previousLocationGrant.setData(true)
} catch (e) { },
// pass (e: GeolocationPositionError) => {
} if (e.code === 2 || e.code === 3) {
try { this._gpsAvailable.set(false)
await Geolocation.watchPosition( return
{ }
enableHighAccuracy: true, this._gpsAvailable.set(true) // We go back to the default assumption that the location is physically available
maximumAge: 120000, if (e.code === 1) {
}, this.permission.set("denied")
(position: GeolocationPosition, error: GeolocationPositionError) => { this._grantedThisSession.setData(false)
if (error) { return
if (error.code === 2 || error.code === 3) { }
self._gpsAvailable.set(false) console.warn("Could not get location with navigator.geolocation due to", e)
return },
} {
self._gpsAvailable.set(true) // We go back to the default assumption that the location is physically available enableHighAccuracy: true,
if (error.code === 1) { }
self.permission.set("denied") )
self._grantedThisSession.setData(false)
return
}
console.warn("Could not get location with navigator.geolocation due to", error)
}
console.log("Got position:", position, JSON.stringify(position))
if (!position) {
return
}
this._gpsAvailable.set(true)
this.currentGPSLocation.setData(position.coords)
this._previousLocationGrant.setData(true)
})
} catch (e) {
console.error("Could not get geolocation due to", e)
}
} }
} }

View file

@ -3,36 +3,62 @@
* If this is successful, it will patch some webAPIs * If this is successful, it will patch some webAPIs
*/ */
import { registerPlugin } from "@capacitor/core" import { registerPlugin } from "@capacitor/core"
import { UIEventSource } from "../UIEventSource"
export class AndroidPolyfill {
private readonly databridgePlugin: DatabridgePlugin
constructor() {
this.databridgePlugin = registerPlugin<DatabridgePlugin>("Databridge", {
web: () => {
return <DatabridgePlugin>{
async request(options: { key: string }): Promise<{ value: string }> {
return { value: "web" }
},
}
},
})
}
public async init(){
const shell = await this.databridgePlugin.request({ key: "meta" })
if(shell.value === "web"){
console.log("Not initing Android polyfill; web detected")
return
}
console.log("Detected shell:", shell.value)
}
}
export interface DatabridgePlugin { export interface DatabridgePlugin {
request(options: { key: string }): Promise<{ value: string }>; request(options: { key: string }): Promise<{ value: string }>;
} }
new AndroidPolyfill().init() const DatabridgePluginSingleton = registerPlugin<DatabridgePlugin>("Databridge", {
web: () => {
return <DatabridgePlugin>{
async request(options: { key: string }): Promise<{ value: string }> {
return { value: "web" }
},
}
},
})
export class AndroidPolyfill {
private readonly databridgePlugin: DatabridgePlugin = DatabridgePluginSingleton
/**
* Registers 'navigator.'
* @private
*/
private backfillGeolocation(databridgePlugin: DatabridgePlugin) {
const origQueryFunc = navigator?.permissions?.query
navigator.permissions.query = async (descr: PermissionDescriptor) => {
if (descr.name === "geolocation") {
console.log("Got a geolocation permission request")
const src = UIEventSource.FromPromise(databridgePlugin.request({ key: "location:request-permission" }))
return <PermissionStatus>{
state: undefined,
addEventListener(key: "change", f: (value: "granted" | "denied") => void) {
src.addCallbackAndRunD(v => {
const content = <"granted" | "denied">v.value
f(content)
return true
})
},
}
}
if (origQueryFunc) {
return await origQueryFunc(descr)
}
}
}
public async init() {
console.log("Sniffing shell version")
const shell = await this.databridgePlugin.request({ key: "meta" })
if (shell.value === "web") {
console.log("Not initing Android polyfill as not in a shell; web detected")
return
}
console.log("Detected shell:", shell.value)
this.backfillGeolocation(this.databridgePlugin)
}
}

View file

@ -25,7 +25,8 @@
import ThemeSearch from "../Logic/Search/ThemeSearch" import ThemeSearch from "../Logic/Search/ThemeSearch"
import SearchUtils from "../Logic/Search/SearchUtils" import SearchUtils from "../Logic/Search/SearchUtils"
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight" import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
new AndroidPolyfill().init().then(() => console.log("Android polyfill setup completed"))
const featureSwitches = new OsmConnectionFeatureSwitches() const featureSwitches = new OsmConnectionFeatureSwitches()
const osmConnection = new OsmConnection({ const osmConnection = new OsmConnection({
fakeUser: featureSwitches.featureSwitchFakeUser.data, fakeUser: featureSwitches.featureSwitchFakeUser.data,

View file

@ -1 +1 @@
{"properties":{"name":"Bing Maps Aerial","id":"Bing","url":"https://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14738&pr=odbl&n=f","type":"bing","category":"photo","min_zoom":1,"max_zoom":22},"type":"Feature","geometry":null} {"properties":{"name":"Bing Maps Aerial","id":"Bing","url":"https://ecn.t1.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14860&pr=odbl&n=f","type":"bing","category":"photo","min_zoom":1,"max_zoom":22},"type":"Feature","geometry":null}

View file

@ -8,6 +8,7 @@ import { SubtleButton } from "./UI/Base/SubtleButton"
import { Utils } from "./Utils" import { Utils } from "./Utils"
import Constants from "./Models/Constants" import Constants from "./Models/Constants"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray" import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import { AndroidPolyfill } from "./Logic/Web/AndroidPolyfill"
function webgl_support() { function webgl_support() {
try { try {
@ -48,6 +49,7 @@ async function main() {
if (!webgl_support()) { if (!webgl_support()) {
throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this." throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this."
} }
new AndroidPolyfill().init().then(() => console.log("Android polyfill setup completed"))
const [theme, availableLayers] = await Promise.all([ const [theme, availableLayers] = await Promise.all([
DetermineTheme.getTheme(), DetermineTheme.getTheme(),
await getAvailableLayers(), await getAvailableLayers(),

View file

@ -45,6 +45,7 @@ async function main() {
if (!webgl_support()) { if (!webgl_support()) {
new FixedUiElement("WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this.").SetClass("block alert").AttachTo("maindiv") new FixedUiElement("WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this.").SetClass("block alert").AttachTo("maindiv")
}else{ }else{
new AndroidPolyfill().init().then(() => console.log("Android polyfill setup completed"))
const availableLayers = await getAvailableLayers() const availableLayers = await getAvailableLayers()
MetaTagging.setThemeMetatagging(new ThemeMetaTagging()) MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
// LAYOUT.ADD_LAYERS // LAYOUT.ADD_LAYERS