diff --git a/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json b/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json index bff0c569a6..da66c7c34b 100644 --- a/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json +++ b/assets/layers/bicycle_tube_vending_machine/bicycle_tube_vending_machine.json @@ -32,7 +32,9 @@ "mappings": [ { "if": "name~*", - "then": "Bicycle tube vending machine {name}" + "then": { + "en": "Bicycle tube vending machine {name}" + } } ] }, @@ -70,8 +72,7 @@ }, "tags": [ "amenity=vending_machine", - "vending=bicycle_tube", - "vending:bicycle_tube=yes" + "vending=bicycle_tube" ] } ], @@ -176,65 +177,62 @@ "id": "Still in use?" }, { - "question": "How much does a bicycle tube cost?", - "render": "A bicycle tube costs {charge}", + "question": { + "en": "How much does a bicycle tube cost?" + }, + "render": { + "en": "A bicycle tube costs {charge}" + }, "freeform": { "key": "charge" }, "id": "bicycle_tube_vending_machine-charge" }, + "payment-options-split", { - "id": "vending-machine-payment-methods", - "question": "How can one pay at this tube vending machine?", - "mappings": [ - { - "if": "payment:coins=yes", - "ifnot": "payment:coins=no", - "then": "Payment with coins is possible" - }, - { - "if": "payment:notes=yes", - "ifnot": "payment:notes=no", - "then": "Payment with notes is possible" - }, - { - "if": "payment:cards=yes", - "ifnot": "payment:cards=no", - "then": "Payment with cards is possible" - } - ], - "multiAnswer": true - }, - { - "question": "Which brand of tubes are sold here?", + "question": { + "en": "Which brand of tubes are sold here?" + }, "freeform": { "key": "brand" }, - "render": "{brand} tubes are sold here", + "render": { + "en": "{brand} tubes are sold here" + }, "mappings": [ { "if": "brand=Continental", - "then": "Continental tubes are sold here" + "then": { + "en": "Continental tubes are sold here" + } }, { "if": "brand=Schwalbe", - "then": "Schwalbe tubes are sold here" + "then": { + "en": "Schwalbe tubes are sold here" + } } ], "multiAnswer": true, "id": "bicycle_tube_vending_machine-brand" }, { - "question": "Who maintains this vending machine?", + "question": { + "en": "Who maintains this vending machine?" + }, "render": "This vending machine is maintained by {operator}", "mappings": [ { "if": "operator=Schwalbe", - "then": "Maintained by Schwalbe" + "then": { + "en": "Maintained by Schwalbe" + } }, { "if": "operator=Continental", - "then": "Maintained by Continental" + "then": { + "en": "Maintained by Continental" + } } ], "freeform": { @@ -243,33 +241,52 @@ "id": "bicycle_tube_vending_machine-operator" }, { - "id": "bicycle_tube_vending_maching-other-items", - "question": "Are other bicycle bicycle accessories sold here?", + "id": "other-items-vending", + "question": { + "en": "Are other biycle accessories sold here?" + }, "mappings": [ { - "if": "vending:bicycle_light=yes", - "ifnot": "vending:bicycle_light=no", - "then": "Bicycle lights are sold here" + "if": "vending=bicycle_tube", + "then": { + "en": "Bicycle inner tubes are sold here", + "nl": "Fietsbinnenbanden worden hier verkocht" + } }, { - "if": "vending:gloves=yes", - "ifnot": "vending:gloves=no", - "then": "Gloves are sold here" + "if": "vending=bicycle_light", + "then": { + "en": "Bicycle lights are sold here", + "nl": "Fietslampjes worden hier verkocht" + } }, { - "if": "vending:bicycle_repair_kit=yes", - "ifnot": "vending:bicycle_repair_kit=no", - "then": "Bicycle repair kits are sold here" + "if": "vending=gloves", + "then": { + "en": "Gloves are sold here", + "nl": "Handschoenen worden hier verkocht" + } }, { - "if": "vending:bicycle_pump=yes", - "ifnot": "vending:bicycle_pump=no", - "then": "Bicycle pumps are sold here" + "if": "vending=bicycle_repair_kit", + "then": { + "en": "Bicycle repair kits are sold here", + "nl": "Fietsreparatiesets worden hier verkocht" + } }, { - "if": "vending:bicycle_lock=yes", - "ifnot": "vending:bicycle_lock=no", - "then": "Bicycle locks are sold here" + "if": "vending=bicycle_pump", + "then": { + "en": "Bicycle pumps are sold here", + "nl": "Fietspompen worden hier verkocht" + } + }, + { + "if": "vending=bicycle_lock", + "then": { + "en": "Bicycle locks are sold here", + "nl": "Fietssloten worden hier verkocht" + } } ], "multiAnswer": true @@ -322,4 +339,4 @@ "cs": "Vrstva zobrazující automaty na cyklistické duše (buď speciální automaty na cyklistické duše, nebo klasické automaty s cyklistickými dušemi a případně dalšími předměty souvisejícími s jízdními koly, jako jsou světla, rukavice, zámky, ...)", "ca": "Una capa que mostra màquines expenedores per a tubs de bicicleta (ja siguin màquines expenedores de tubs de bicicleta o màquines expenedores clàssiques amb tubs de bicicleta i opcionalment objectes addicionals relacionats amb la bicicleta com ara llums, guants, panys, ...)" } -} +} \ No newline at end of file diff --git a/assets/layers/vending_machine/vending_machine.json b/assets/layers/vending_machine/vending_machine.json index b9a92c1ed8..98ce6dbfd4 100644 --- a/assets/layers/vending_machine/vending_machine.json +++ b/assets/layers/vending_machine/vending_machine.json @@ -242,6 +242,14 @@ }, "icon": "./assets/layers/vending_machine/potato.svg" }, + { + "if": "vending=meat", + "then": { + "en": "Meat is sold", + "nl": "Vlees wordt verkocht" + }, + "icon": "./assets/layers/id_presets/temaki-meat.svg" + }, { "if": "vending=flowers", "then": { @@ -282,12 +290,39 @@ "icon": "./assets/themes/stations/public_transport_tickets.svg" }, { - "if": "vending=meat", + "if": "vending=bicycle_light", "then": { - "en": "Meat products are being sold", - "nl": "Vleesproducten worden hier verkocht" - }, - "icon": "./assets/layers/id_presets/temaki-meat.svg" + "en": "Bicycle lights are sold", + "nl": "Fietslampjes worden verkocht" + } + }, + { + "if": "vending=gloves", + "then": { + "en": "Gloves are sold", + "nl": "Handschoenen worden verkocht" + } + }, + { + "if": "vending=bicycle_repair_kit", + "then": { + "en": "Bicycle repair kits are sold", + "nl": "Fietsreparatiesets worden verkocht" + } + }, + { + "if": "vending=bicycle_pump", + "then": { + "en": "Bicycle pumps are sold", + "nl": "Fietspompen worden verkocht" + } + }, + { + "if": "vending=bicycle_lock", + "then": { + "en": "Bicycle locks are sold", + "nl": "Fietssloten worden verkocht" + } } ], "multiAnswer": true @@ -453,6 +488,10 @@ "if": "vending=potatoes", "then": "circle:white;./assets/layers/vending_machine/potato.svg" }, + { + "if": "vending=meat", + "then": "./assets/layers/id_presets/temaki-meat.svg" + }, { "if": "vending=flowers", "then": "circle:white;./assets/layers/id_presets/maki-florist.svg" @@ -792,6 +831,14 @@ }, "osmTags": "vending~i~.*potatoes.*" }, + { + "question": { + "en": "Sale of meat", + "nl": "Verkoop van vlees", + "de": "Verkauf von Fleisch" + }, + "osmTags": "vending~i~.*meat.*" + }, { "question": { "en": "Sale of flowers", @@ -805,7 +852,7 @@ { "osmTags": "vending~i~.*parking_tickets.*", "question": { - "en": "Sale of parking" + "en": "Sale of parking tickets" } }, { @@ -821,9 +868,38 @@ } }, { - "osmTags": "vending=meat", + "osmTags": "vending=bicycle_light", "question": { - "en": "Sale of meat products" + "en": "Sale of bicycle lights", + "nl": "Verkoop van fietslampjes" + } + }, + { + "osmTags": "vending=gloves", + "question": { + "en": "Sale of gloves", + "nl": "Verkoop van handschoenen" + } + }, + { + "osmTags": "vending=bicycle_repair_kit", + "question": { + "en": "Sale of bicycle repair kits", + "nl": "Verkoop van fietsreparatiesets" + } + }, + { + "osmTags": "vending=bicycle_pump", + "question": { + "en": "Sale of bicycle pumps", + "nl": "Verkoop van fietspompen" + } + }, + { + "osmTags": "vending=bicycle_lock", + "question": { + "en": "Sale of bicycle locks", + "nl": "Verkoop van fietssloten" } } ] @@ -834,4 +910,4 @@ "enableRelocation": true }, "deletion": true -} +} \ No newline at end of file diff --git a/langs/layers/ca.json b/langs/layers/ca.json index ab650ab69a..c0a5bd65ec 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -8073,7 +8073,7 @@ "15": { "question": "Venda de patates" }, - "16": { + "17": { "question": "Venda de flors" } } @@ -8154,16 +8154,16 @@ "14": { "then": "Es venen papes" }, - "15": { + "16": { "then": "Es venen flors" }, - "16": { + "17": { "then": "Es venen tiquets d'aparcament" }, - "17": { + "18": { "then": "Es venen cèntims premsats" }, - "18": { + "19": { "then": "Es venen bitllets de transport públic" } }, diff --git a/langs/layers/de.json b/langs/layers/de.json index 0db8b2538e..9a0b859e10 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -9762,6 +9762,9 @@ "question": "Verkauf von Kartoffeln" }, "16": { + "question": "Verkauf von Fleisch" + }, + "17": { "question": "Verkauf von Blumen" } } @@ -9842,13 +9845,13 @@ "14": { "then": "Kartoffeln werden verkauft" }, - "15": { + "16": { "then": "Blumen werden verkauft" }, - "16": { + "17": { "then": "Parkscheine werden verkauft" }, - "18": { + "19": { "then": "Fahrscheine werden verkauft" } }, diff --git a/langs/layers/en.json b/langs/layers/en.json index c0944b4372..a29f1be33f 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -1034,9 +1034,64 @@ }, "question": "Is this vending machine still operational?", "render": "The operational status is {operational_status}" + }, + "bicycle_tube_vending_machine-brand": { + "mappings": { + "0": { + "then": "Continental tubes are sold here" + }, + "1": { + "then": "Schwalbe tubes are sold here" + } + }, + "question": "Which brand of tubes are sold here?", + "render": "{brand} tubes are sold here" + }, + "bicycle_tube_vending_machine-charge": { + "question": "How much does a bicycle tube cost?", + "render": "A bicycle tube costs {charge}" + }, + "bicycle_tube_vending_machine-operator": { + "mappings": { + "0": { + "then": "Maintained by Schwalbe" + }, + "1": { + "then": "Maintained by Continental" + } + }, + "question": "Who maintains this vending machine?" + }, + "other-items-vending": { + "mappings": { + "0": { + "then": "Bicycle inner tubes are sold here" + }, + "1": { + "then": "Bicycle lights are sold here" + }, + "2": { + "then": "Gloves are sold here" + }, + "3": { + "then": "Bicycle repair kits are sold here" + }, + "4": { + "then": "Bicycle pumps are sold here" + }, + "5": { + "then": "Bicycle locks are sold here" + } + }, + "question": "Are other biycle accessories sold here?" } }, "title": { + "mappings": { + "0": { + "then": "Bicycle tube vending machine {name}" + } + }, "render": "Bicycle tube vending machine" } }, @@ -9900,19 +9955,34 @@ "question": "Sale of potatoes" }, "16": { - "question": "Sale of flowers" + "question": "Sale of meat" }, "17": { - "question": "Sale of parking" + "question": "Sale of flowers" }, "18": { - "question": "Sale of pressed pennies" + "question": "Sale of parking tickets" }, "19": { - "question": "Sale of public transport tickets" + "question": "Sale of pressed pennies" }, "20": { - "question": "Sale of meat products" + "question": "Sale of public transport tickets" + }, + "21": { + "question": "Sale of bicycle lights" + }, + "22": { + "question": "Sale of gloves" + }, + "23": { + "question": "Sale of bicycle repair kits" + }, + "24": { + "question": "Sale of bicycle pumps" + }, + "25": { + "question": "Sale of bicycle locks" } } } @@ -9999,19 +10069,34 @@ "then": "Potatoes are sold" }, "15": { - "then": "Flowers are sold" + "then": "Meat is sold" }, "16": { - "then": "Parking tickets are sold" + "then": "Flowers are sold" }, "17": { - "then": "Pressed pennies are sold" + "then": "Parking tickets are sold" }, "18": { - "then": "Public transport tickets are sold" + "then": "Pressed pennies are sold" }, "19": { - "then": "Meat products are being sold" + "then": "Public transport tickets are sold" + }, + "20": { + "then": "Bicycle lights are sold" + }, + "21": { + "then": "Gloves are sold" + }, + "22": { + "then": "Bicycle repair kits are sold" + }, + "23": { + "then": "Bicycle pumps are sold" + }, + "24": { + "then": "Bicycle locks are sold" } }, "question": "What does this vending machine sell?", diff --git a/langs/layers/fr.json b/langs/layers/fr.json index 3f4a929647..b5db38d708 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -6576,7 +6576,7 @@ "15": { "question": "Vente de pommes de terre" }, - "16": { + "17": { "question": "Vente de fleurs" } } @@ -6657,7 +6657,7 @@ "14": { "then": "Vent des pommes de terre" }, - "15": { + "16": { "then": "Vent des fleurs" } }, diff --git a/langs/layers/nl.json b/langs/layers/nl.json index a8ad5960ed..39a19e3e80 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -930,6 +930,28 @@ }, "question": "Is deze verkoopsautomaat nog steeds werkende?", "render": "Deze verkoopsautomaat is {operational_status}" + }, + "other-items-vending": { + "mappings": { + "0": { + "then": "Fietsbinnenbanden worden hier verkocht" + }, + "1": { + "then": "Fietslampjes worden hier verkocht" + }, + "2": { + "then": "Handschoenen worden hier verkocht" + }, + "3": { + "then": "Fietsreparatiesets worden hier verkocht" + }, + "4": { + "then": "Fietspompen worden hier verkocht" + }, + "5": { + "then": "Fietssloten worden hier verkocht" + } + } } }, "title": { @@ -9055,7 +9077,25 @@ "question": "Verkoop van aardappelen" }, "16": { + "question": "Verkoop van vlees" + }, + "17": { "question": "Verkoop van bloemen" + }, + "21": { + "question": "Verkoop van fietslampjes" + }, + "22": { + "question": "Verkoop van handschoenen" + }, + "23": { + "question": "Verkoop van fietsreparatiesets" + }, + "24": { + "question": "Verkoop van fietspompen" + }, + "25": { + "question": "Verkoop van fietssloten" } } } @@ -9136,16 +9176,31 @@ "then": "Aardappelen worden verkocht" }, "15": { - "then": "Bloemen worden verkocht" + "then": "Vlees wordt verkocht" }, "16": { + "then": "Bloemen worden verkocht" + }, + "17": { "then": "Parkeerkaarten worden verkocht" }, - "18": { + "19": { "then": "Openbaar vervoerkaartjes worden verkocht" }, - "19": { - "then": "Vleesproducten worden hier verkocht" + "20": { + "then": "Fietslampjes worden verkocht" + }, + "21": { + "then": "Handschoenen worden verkocht" + }, + "22": { + "then": "Fietsreparatiesets worden verkocht" + }, + "23": { + "then": "Fietspompen worden verkocht" + }, + "24": { + "then": "Fietssloten worden verkocht" } }, "question": "Wat verkoopt deze verkoopautomaat?", diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 07028c35a1..663dd15ebb 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -1,553 +1,550 @@ // @ts-ignore -import { osmAuth } from "osm-auth"; -import { Store, Stores, UIEventSource } from "../UIEventSource"; -import { OsmPreferences } from "./OsmPreferences"; -import { Utils } from "../../Utils"; -import { LocalStorageSource } from "../Web/LocalStorageSource"; -import * as config from "../../../package.json"; +import { osmAuth } from "osm-auth" +import { Store, Stores, UIEventSource } from "../UIEventSource" +import { OsmPreferences } from "./OsmPreferences" +import { Utils } from "../../Utils" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import * as config from "../../../package.json" export default class UserDetails { - public loggedIn = false; - public name = "Not logged in"; - public uid: number; - public csCount = 0; - public img?: string; - public unreadMessages = 0; - public totalMessages: number = 0; - public home: { lon: number; lat: number }; - public backend: string; - public account_created: string; - public tracesCount: number = 0; - public description: string; + public loggedIn = false + public name = "Not logged in" + public uid: number + public csCount = 0 + public img?: string + public unreadMessages = 0 + public totalMessages: number = 0 + public home: { lon: number; lat: number } + public backend: string + public account_created: string + public tracesCount: number = 0 + public description: string - constructor(backend: string) { - this.backend = backend; - } + constructor(backend: string) { + this.backend = backend + } } export interface AuthConfig { - "#"?: string; // optional comment - oauth_client_id: string; - oauth_secret: string; - url: string; + "#"?: string // optional comment + oauth_client_id: string + oauth_secret: string + url: string } export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" export class OsmConnection { - public static readonly oauth_configs: Record = - config.config.oauth_credentials; - public auth; - public userDetails: UIEventSource; - public isLoggedIn: Store; - public gpxServiceIsOnline: UIEventSource = new UIEventSource( - "unknown" - ); - public apiIsOnline: UIEventSource = new UIEventSource( - "unknown" - ); + public static readonly oauth_configs: Record = + config.config.oauth_credentials + public auth + public userDetails: UIEventSource + public isLoggedIn: Store + public gpxServiceIsOnline: UIEventSource = new UIEventSource( + "unknown" + ) + public apiIsOnline: UIEventSource = new UIEventSource( + "unknown" + ) - public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( - "not-attempted" - ); - public preferencesHandler: OsmPreferences; - public readonly _oauth_config: AuthConfig; - private readonly _dryRun: Store; - private fakeUser: boolean; - private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; - private readonly _iframeMode: Boolean | boolean; - private readonly _singlePage: boolean; - private isChecking = false; + public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( + "not-attempted" + ) + public preferencesHandler: OsmPreferences + public readonly _oauth_config: AuthConfig + private readonly _dryRun: Store + private fakeUser: boolean + private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] + private readonly _iframeMode: Boolean | boolean + private readonly _singlePage: boolean + private isChecking = false - constructor(options?: { - dryRun?: Store - fakeUser?: false | boolean - oauth_token?: UIEventSource - // Used to keep multiple changesets open and to write to the correct changeset - singlePage?: boolean - osmConfiguration?: "osm" | "osm-test" - attemptLogin?: true | boolean - }) { - options = options ?? {}; - this.fakeUser = options.fakeUser ?? false; - this._singlePage = options.singlePage ?? true; - this._oauth_config = - OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? - OsmConnection.oauth_configs.osm; - console.debug("Using backend", this._oauth_config.url); - this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; + constructor(options?: { + dryRun?: Store + fakeUser?: false | boolean + oauth_token?: UIEventSource + // Used to keep multiple changesets open and to write to the correct changeset + singlePage?: boolean + osmConfiguration?: "osm" | "osm-test" + attemptLogin?: true | boolean + }) { + options = options ?? {} + this.fakeUser = options.fakeUser ?? false + this._singlePage = options.singlePage ?? true + this._oauth_config = + OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? + OsmConnection.oauth_configs.osm + console.debug("Using backend", this._oauth_config.url) + this._iframeMode = Utils.runningFromConsole ? false : window !== window.top - // Check if there are settings available in environment variables, and if so, use those - if ( - import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined && - import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined - ) { - console.debug("Using environment variables for oauth config"); - this._oauth_config = { - oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID, - oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET, - url: "https://api.openstreetmap.org" - }; - } - - this.userDetails = new UIEventSource( - new UserDetails(this._oauth_config.url), - "userDetails" - ); - if (options.fakeUser) { - const ud = this.userDetails.data; - ud.csCount = 5678; - ud.loggedIn = true; - ud.unreadMessages = 0; - ud.name = "Fake user"; - ud.totalMessages = 42; - } - const self = this; - this.UpdateCapabilities(); - this.isLoggedIn = this.userDetails.map( - (user) => - user.loggedIn && - (self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), - [this.apiIsOnline] - ); - this.isLoggedIn.addCallback((isLoggedIn) => { - if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { - // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do - // This means someone attempted to toggle this; so we attempt to login! - self.AttemptLogin(); - } - }); - - this._dryRun = options.dryRun ?? new UIEventSource(false); - - this.updateAuthObject(); - - this.preferencesHandler = new OsmPreferences( - this.auth, - this - ); - - if (options.oauth_token?.data !== undefined) { - console.log(options.oauth_token.data); - const self = this; - this.auth.bootstrapToken( - options.oauth_token.data, - (x) => { - console.log("Called back: ", x); - self.AttemptLogin(); - }, - this.auth - ); - - options.oauth_token.setData(undefined); - } - if (this.auth.authenticated() && options.attemptLogin !== false) { - this.AttemptLogin(); // Also updates the user badge - } else { - console.log("Not authenticated"); - } - } - - public GetPreference( - key: string, - defaultValue: string = undefined, - options?: { - documentation?: string - prefix?: string - } - ): UIEventSource { - return this.preferencesHandler.GetPreference(key, defaultValue, options); - } - - public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - return this.preferencesHandler.GetLongPreference(key, prefix); - } - - public OnLoggedIn(action: (userDetails: UserDetails) => void) { - this._onLoggedIn.push(action); - } - - public LogOut() { - this.auth.logout(); - this.userDetails.data.loggedIn = false; - this.userDetails.data.csCount = 0; - this.userDetails.data.name = ""; - this.userDetails.ping(); - console.log("Logged out"); - this.loadingStatus.setData("not-attempted"); - } - - /** - * The backend host, without path or trailing '/' - * - * new OsmConnection().Backend() // => "https://www.openstreetmap.org" - */ - public Backend(): string { - return this._oauth_config.url; - } - - public AttemptLogin() { - this.UpdateCapabilities(); - this.loadingStatus.setData("loading"); - if (this.fakeUser) { - this.loadingStatus.setData("logged-in"); - console.log("AttemptLogin called, but ignored as fakeUser is set"); - return; - } - const self = this; - console.log("Trying to log in..."); - this.updateAuthObject(); - LocalStorageSource.Get("location_before_login").setData( - Utils.runningFromConsole ? undefined : window.location.href - ); - this.auth.xhr( - { - method: "GET", - path: "/api/0.6/user/details" - }, - function(err, details) { - if (err != null) { - console.log(err); - self.loadingStatus.setData("error"); - if (err.status == 401) { - console.log("Clearing tokens..."); - // Not authorized - our token probably got revoked - self.auth.logout(); - self.LogOut(); - } - return; + // Check if there are settings available in environment variables, and if so, use those + if ( + import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined && + import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined + ) { + console.debug("Using environment variables for oauth config") + this._oauth_config.oauth_client_id = import.meta.env.VITE_OSM_OAUTH_CLIENT_ID + this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET } - if (details == null) { - self.loadingStatus.setData("error"); - return; + this.userDetails = new UIEventSource( + new UserDetails(this._oauth_config.url), + "userDetails" + ) + if (options.fakeUser) { + const ud = this.userDetails.data + ud.csCount = 5678 + ud.loggedIn = true + ud.unreadMessages = 0 + ud.name = "Fake user" + ud.totalMessages = 42 + } + const self = this + this.UpdateCapabilities() + this.isLoggedIn = this.userDetails.map( + (user) => + user.loggedIn && + (self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), + [this.apiIsOnline] + ) + this.isLoggedIn.addCallback((isLoggedIn) => { + if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { + // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do + // This means someone attempted to toggle this; so we attempt to login! + self.AttemptLogin() + } + }) + + this._dryRun = options.dryRun ?? new UIEventSource(false) + + this.updateAuthObject() + + this.preferencesHandler = new OsmPreferences( + this.auth, + this + ) + + if (options.oauth_token?.data !== undefined) { + console.log(options.oauth_token.data) + const self = this + this.auth.bootstrapToken( + options.oauth_token.data, + (x) => { + console.log("Called back: ", x) + self.AttemptLogin() + }, + this.auth + ) + + options.oauth_token.setData(undefined) + } + if (this.auth.authenticated() && options.attemptLogin !== false) { + this.AttemptLogin() // Also updates the user badge + } else { + console.log("Not authenticated") + } + } + + public GetPreference( + key: string, + defaultValue: string = undefined, + options?: { + documentation?: string + prefix?: string + } + ): UIEventSource { + return this.preferencesHandler.GetPreference(key, defaultValue, options) + } + + public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { + return this.preferencesHandler.GetLongPreference(key, prefix) + } + + public OnLoggedIn(action: (userDetails: UserDetails) => void) { + this._onLoggedIn.push(action) + } + + public LogOut() { + this.auth.logout() + this.userDetails.data.loggedIn = false + this.userDetails.data.csCount = 0 + this.userDetails.data.name = "" + this.userDetails.ping() + console.log("Logged out") + this.loadingStatus.setData("not-attempted") + } + + /** + * The backend host, without path or trailing '/' + * + * new OsmConnection().Backend() // => "https://www.openstreetmap.org" + */ + public Backend(): string { + return this._oauth_config.url + } + + public AttemptLogin() { + this.UpdateCapabilities() + this.loadingStatus.setData("loading") + if (this.fakeUser) { + this.loadingStatus.setData("logged-in") + console.log("AttemptLogin called, but ignored as fakeUser is set") + return + } + const self = this + console.log("Trying to log in...") + this.updateAuthObject() + LocalStorageSource.Get("location_before_login").setData( + Utils.runningFromConsole ? undefined : window.location.href + ) + this.auth.xhr( + { + method: "GET", + path: "/api/0.6/user/details", + }, + function (err, details) { + if (err != null) { + console.log(err) + self.loadingStatus.setData("error") + if (err.status == 401) { + console.log("Clearing tokens...") + // Not authorized - our token probably got revoked + self.auth.logout() + self.LogOut() + } + return + } + + if (details == null) { + self.loadingStatus.setData("error") + return + } + + self.CheckForMessagesContinuously() + + // details is an XML DOM of user details + let userInfo = details.getElementsByTagName("user")[0] + + let data = self.userDetails.data + data.loggedIn = true + console.log("Login completed, userinfo is ", userInfo) + data.name = userInfo.getAttribute("display_name") + data.account_created = userInfo.getAttribute("account_created") + data.uid = Number(userInfo.getAttribute("id")) + data.csCount = Number.parseInt( + userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0 + ) + data.tracesCount = Number.parseInt( + userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0 + ) + + data.img = undefined + const imgEl = userInfo.getElementsByTagName("img") + if (imgEl !== undefined && imgEl[0] !== undefined) { + data.img = imgEl[0].getAttribute("href") + } + + const description = userInfo.getElementsByTagName("description") + if (description !== undefined && description[0] !== undefined) { + data.description = description[0]?.innerHTML + } + const homeEl = userInfo.getElementsByTagName("home") + if (homeEl !== undefined && homeEl[0] !== undefined) { + const lat = parseFloat(homeEl[0].getAttribute("lat")) + const lon = parseFloat(homeEl[0].getAttribute("lon")) + data.home = { lat: lat, lon: lon } + } + + self.loadingStatus.setData("logged-in") + const messages = userInfo + .getElementsByTagName("messages")[0] + .getElementsByTagName("received")[0] + data.unreadMessages = parseInt(messages.getAttribute("unread")) + data.totalMessages = parseInt(messages.getAttribute("count")) + + self.userDetails.ping() + for (const action of self._onLoggedIn) { + action(self.userDetails.data) + } + self._onLoggedIn = [] + } + ) + } + + /** + * Interact with the API. + * + * @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close' + */ + public async interact( + path: string, + method: "GET" | "POST" | "PUT" | "DELETE", + header?: Record, + content?: string + ): Promise { + return new Promise((ok, error) => { + this.auth.xhr( + { + method, + options: { + header, + }, + content, + path: `/api/0.6/${path}`, + }, + function (err, response) { + if (err !== null) { + error(err) + } else { + ok(response) + } + } + ) + }) + } + + public async post( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "POST", header, content) + } + + public async put( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "PUT", header, content) + } + + public async get(path: string, header?: Record): Promise { + return await this.interact(path, "GET", header) + } + + public closeNote(id: number | string, text?: string): Promise { + let textSuffix = "" + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text) + } + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) + return new Promise((ok) => { + ok() + }) + } + return this.post(`notes/${id}/close${textSuffix}`) + } + + public reopenNote(id: number | string, text?: string): Promise { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) + return new Promise((ok) => { + ok() + }) + } + let textSuffix = "" + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text) + } + return this.post(`notes/${id}/reopen${textSuffix}`) + } + + public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually opening note with text ", text) + return new Promise<{ id: number }>((ok) => { + window.setTimeout( + () => ok({ id: Math.floor(Math.random() * 1000) }), + Math.random() * 5000 + ) + }) + } + // Lat and lon must be strings for the API to accept it + const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}` + const response = await this.post("notes.json", content, { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }) + const parsed = JSON.parse(response) + const id = parsed.properties + console.log("OPENED NOTE", id) + return id + } + + public async uploadGpxTrack( + gpx: string, + options: { + description: string + visibility: "private" | "public" | "trackable" | "identifiable" + filename?: string + /** + * Some words to give some properties; + * + * Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words. + */ + labels: string[] + } + ): Promise<{ id: number }> { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually uploading GPX ", gpx) + return new Promise<{ id: number }>((ok, error) => { + window.setTimeout( + () => ok({ id: Math.floor(Math.random() * 1000) }), + Math.random() * 5000 + ) + }) } - self.CheckForMessagesContinuously(); - - // details is an XML DOM of user details - let userInfo = details.getElementsByTagName("user")[0]; - - let data = self.userDetails.data; - data.loggedIn = true; - console.log("Login completed, userinfo is ", userInfo); - data.name = userInfo.getAttribute("display_name"); - data.account_created = userInfo.getAttribute("account_created"); - data.uid = Number(userInfo.getAttribute("id")); - data.csCount = Number.parseInt( - userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0 - ); - data.tracesCount = Number.parseInt( - userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0 - ); - - data.img = undefined; - const imgEl = userInfo.getElementsByTagName("img"); - if (imgEl !== undefined && imgEl[0] !== undefined) { - data.img = imgEl[0].getAttribute("href"); + const contents = { + file: gpx, + description: options.description ?? "", + tags: options.labels?.join(",") ?? "", + visibility: options.visibility, } - const description = userInfo.getElementsByTagName("description"); - if (description !== undefined && description[0] !== undefined) { - data.description = description[0]?.innerHTML; - } - const homeEl = userInfo.getElementsByTagName("home"); - if (homeEl !== undefined && homeEl[0] !== undefined) { - const lat = parseFloat(homeEl[0].getAttribute("lat")); - const lon = parseFloat(homeEl[0].getAttribute("lon")); - data.home = { lat: lat, lon: lon }; + const extras = { + file: + '; filename="' + + (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + + '"\r\nContent-Type: application/gpx+xml', } - self.loadingStatus.setData("logged-in"); - const messages = userInfo - .getElementsByTagName("messages")[0] - .getElementsByTagName("received")[0]; - data.unreadMessages = parseInt(messages.getAttribute("unread")); - data.totalMessages = parseInt(messages.getAttribute("count")); + const boundary = "987654" - self.userDetails.ping(); - for (const action of self._onLoggedIn) { - action(self.userDetails.data); + let body = "" + for (const key in contents) { + body += "--" + boundary + "\r\n" + body += 'Content-Disposition: form-data; name="' + key + '"' + if (extras[key] !== undefined) { + body += extras[key] + } + body += "\r\n\r\n" + body += contents[key] + "\r\n" } - self._onLoggedIn = []; - } - ); - } + body += "--" + boundary + "--\r\n" - /** - * Interact with the API. - * - * @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close' - */ - public async interact( - path: string, - method: "GET" | "POST" | "PUT" | "DELETE", - header?: Record, - content?: string - ): Promise { - return new Promise((ok, error) => { - this.auth.xhr( - { - method, - options: { - header - }, - content, - path: `/api/0.6/${path}` - }, - function(err, response) { - if (err !== null) { - error(err); - } else { - ok(response); - } + const response = await this.post("gpx/create", body, { + "Content-Type": "multipart/form-data; boundary=" + boundary, + "Content-Length": body.length, + }) + const parsed = JSON.parse(response) + console.log("Uploaded GPX track", parsed) + return { id: parsed } + } + + public addCommentToNote(id: number | string, text: string): Promise { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id) + return new Promise((ok) => { + ok() + }) } - ); - }); - } - - public async post( - path: string, - content?: string, - header?: Record - ): Promise { - return await this.interact(path, "POST", header, content); - } - - public async put( - path: string, - content?: string, - header?: Record - ): Promise { - return await this.interact(path, "PUT", header, content); - } - - public async get(path: string, header?: Record): Promise { - return await this.interact(path, "GET", header); - } - - public closeNote(id: number | string, text?: string): Promise { - let textSuffix = ""; - if ((text ?? "") !== "") { - textSuffix = "?text=" + encodeURIComponent(text); - } - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text); - return new Promise((ok) => { - ok(); - }); - } - return this.post(`notes/${id}/close${textSuffix}`); - } - - public reopenNote(id: number | string, text?: string): Promise { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text); - return new Promise((ok) => { - ok(); - }); - } - let textSuffix = ""; - if ((text ?? "") !== "") { - textSuffix = "?text=" + encodeURIComponent(text); - } - return this.post(`notes/${id}/reopen${textSuffix}`); - } - - public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually opening note with text ", text); - return new Promise<{ id: number }>((ok) => { - window.setTimeout( - () => ok({ id: Math.floor(Math.random() * 1000) }), - Math.random() * 5000 - ); - }); - } - // Lat and lon must be strings for the API to accept it - const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}` - const response = await this.post("notes.json", content, { - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" - }); - const parsed = JSON.parse(response); - const id = parsed.properties; - console.log("OPENED NOTE", id); - return id; - } - - public async uploadGpxTrack( - gpx: string, - options: { - description: string - visibility: "private" | "public" | "trackable" | "identifiable" - filename?: string - /** - * Some words to give some properties; - * - * Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words. - */ - labels: string[] - } - ): Promise<{ id: number }> { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually uploading GPX ", gpx); - return new Promise<{ id: number }>((ok, error) => { - window.setTimeout( - () => ok({ id: Math.floor(Math.random() * 1000) }), - Math.random() * 5000 - ); - }); - } - - const contents = { - file: gpx, - description: options.description ?? "", - tags: options.labels?.join(",") ?? "", - visibility: options.visibility - }; - - const extras = { - file: - "; filename=\"" + - (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + - "\"\r\nContent-Type: application/gpx+xml" - }; - - const boundary = "987654"; - - let body = ""; - for (const key in contents) { - body += "--" + boundary + "\r\n"; - body += "Content-Disposition: form-data; name=\"" + key + "\""; - if (extras[key] !== undefined) { - body += extras[key]; - } - body += "\r\n\r\n"; - body += contents[key] + "\r\n"; - } - body += "--" + boundary + "--\r\n"; - - const response = await this.post("gpx/create", body, { - "Content-Type": "multipart/form-data; boundary=" + boundary, - "Content-Length": body.length - }); - const parsed = JSON.parse(response); - console.log("Uploaded GPX track", parsed); - return { id: parsed }; - } - - public addCommentToNote(id: number | string, text: string): Promise { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id); - return new Promise((ok) => { - ok(); - }); - } - if ((text ?? "") === "") { - throw "Invalid text!"; - } - - return new Promise((ok, error) => { - this.auth.xhr( - { - method: "POST", - - path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` - }, - function(err, _) { - if (err !== null) { - error(err); - } else { - ok(); - } + if ((text ?? "") === "") { + throw "Invalid text!" } - ); - }); - } - /** - * To be called by land.html - */ - public finishLogin(callback: (previousURL: string) => void) { - this.auth.authenticate(function() { - // Fully authed at this point - console.log("Authentication successful!"); - const previousLocation = LocalStorageSource.Get("location_before_login"); - callback(previousLocation.data); - }); - } + return new Promise((ok, error) => { + this.auth.xhr( + { + method: "POST", - private updateAuthObject() { - let pwaStandAloneMode = false; - try { - if (Utils.runningFromConsole) { - pwaStandAloneMode = true; - } else if ( - window.matchMedia("(display-mode: standalone)").matches || - window.matchMedia("(display-mode: fullscreen)").matches - ) { - pwaStandAloneMode = true; - } - } catch (e) { - console.warn( - "Detecting standalone mode failed", - e, - ". Assuming in browser and not worrying furhter" - ); + path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, + }, + function (err, _) { + if (err !== null) { + error(err) + } else { + ok() + } + } + ) + }) } - const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage; - // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... - // Same for an iframe... - - this.auth = new osmAuth({ - client_id: this._oauth_config.oauth_client_id, - url: this._oauth_config.url, - scope: "read_prefs write_prefs write_api write_gpx write_notes", - redirect_uri: Utils.runningFromConsole - ? "https://mapcomplete.org/land.html" - : window.location.protocol + "//" + window.location.host + "/land.html", - singlepage: !standalone, - auto: true - }); - } - - private CheckForMessagesContinuously() { - const self = this; - if (this.isChecking) { - return; + /** + * To be called by land.html + */ + public finishLogin(callback: (previousURL: string) => void) { + this.auth.authenticate(function () { + // Fully authed at this point + console.log("Authentication successful!") + const previousLocation = LocalStorageSource.Get("location_before_login") + callback(previousLocation.data) + }) } - this.isChecking = true; - Stores.Chronic(5 * 60 * 1000).addCallback((_) => { - if (self.isLoggedIn.data) { - console.log("Checking for messages"); - self.AttemptLogin(); - } - }); - } - private UpdateCapabilities(): void { - const self = this; - this.FetchCapabilities().then(({ api, gpx }) => { - self.apiIsOnline.setData(api); - self.gpxServiceIsOnline.setData(gpx); - }); - } + private updateAuthObject() { + let pwaStandAloneMode = false + try { + if (Utils.runningFromConsole) { + pwaStandAloneMode = true + } else if ( + window.matchMedia("(display-mode: standalone)").matches || + window.matchMedia("(display-mode: fullscreen)").matches + ) { + pwaStandAloneMode = true + } + } catch (e) { + console.warn( + "Detecting standalone mode failed", + e, + ". Assuming in browser and not worrying furhter" + ) + } + const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage - private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> { - if (Utils.runningFromConsole) { - return { api: "online", gpx: "online" }; + // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... + // Same for an iframe... + + this.auth = new osmAuth({ + client_id: this._oauth_config.oauth_client_id, + url: this._oauth_config.url, + scope: "read_prefs write_prefs write_api write_gpx write_notes", + redirect_uri: Utils.runningFromConsole + ? "https://mapcomplete.org/land.html" + : window.location.protocol + "//" + window.location.host + "/land.html", + singlepage: !standalone, + auto: true, + }) } - const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities"); - if (result["content"] === undefined) { - console.log("Something went wrong:", result); - return { api: "unreachable", gpx: "unreachable" }; + + private CheckForMessagesContinuously() { + const self = this + if (this.isChecking) { + return + } + this.isChecking = true + Stores.Chronic(5 * 60 * 1000).addCallback((_) => { + if (self.isLoggedIn.data) { + console.log("Checking for messages") + self.AttemptLogin() + } + }) + } + + private UpdateCapabilities(): void { + const self = this + this.FetchCapabilities().then(({ api, gpx }) => { + self.apiIsOnline.setData(api) + self.gpxServiceIsOnline.setData(gpx) + }) + } + + private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> { + if (Utils.runningFromConsole) { + return { api: "online", gpx: "online" } + } + const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities") + if (result["content"] === undefined) { + console.log("Something went wrong:", result) + return { api: "unreachable", gpx: "unreachable" } + } + const xmlRaw = result["content"] + const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml") + const statusEl = parsed.getElementsByTagName("status")[0] + const api = statusEl.getAttribute("api") + const gpx = statusEl.getAttribute("gpx") + return { api, gpx } } - const xmlRaw = result["content"]; - const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml"); - const statusEl = parsed.getElementsByTagName("status")[0]; - const api = statusEl.getAttribute("api"); - const gpx = statusEl.getAttribute("gpx"); - return { api, gpx }; - } } diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index 305f9e0d22..8003d5dc24 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -1,44 +1,45 @@ @@ -69,13 +70,13 @@ {:else if $geopermission === "requested"} - {:else if $geopermission === "denied"} - @@ -84,7 +85,6 @@ - {/if}