Merge pull request #1592 from pietervdvn/RobinLinde-patch-1

Add some vending options
This commit is contained in:
Pieter Vander Vennet 2023-09-28 14:48:02 +00:00 committed by GitHub
commit 7d9192dd26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 864 additions and 631 deletions

View file

@ -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"
"question": {
"en": "Which brand of tubes are sold here?"
},
{
"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?",
"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

View file

@ -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"
"en": "Bicycle lights are sold",
"nl": "Fietslampjes worden verkocht"
}
},
"icon": "./assets/layers/id_presets/temaki-meat.svg"
{
"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"
}
}
]

View file

@ -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"
}
},

View file

@ -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"
}
},

View file

@ -1034,9 +1034,64 @@
},
"question": "Is this vending machine still operational?",
"render": "The operational status is <i>{operational_status}</i>"
},
"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?",

View file

@ -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"
}
},

View file

@ -930,6 +930,28 @@
},
"question": "Is deze verkoopsautomaat nog steeds werkende?",
"render": "Deze verkoopsautomaat is <i>{operational_status}</i>"
},
"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?",

View file

@ -1,63 +1,63 @@
// @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;
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<string, AuthConfig> =
config.config.oauth_credentials;
public auth;
public userDetails: UIEventSource<UserDetails>;
public isLoggedIn: Store<boolean>;
config.config.oauth_credentials
public auth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown"
);
)
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"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<boolean>;
private fakeUser: boolean;
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
private readonly _iframeMode: Boolean | boolean;
private readonly _singlePage: boolean;
private isChecking = false;
)
public preferencesHandler: OsmPreferences
public readonly _oauth_config: AuthConfig
private readonly _dryRun: Store<boolean>
private fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
private readonly _iframeMode: Boolean | boolean
private readonly _singlePage: boolean
private isChecking = false
constructor(options?: {
dryRun?: Store<boolean>
@ -68,83 +68,80 @@ export class OsmConnection {
osmConfiguration?: "osm" | "osm-test"
attemptLogin?: true | boolean
}) {
options = options ?? {};
this.fakeUser = options.fakeUser ?? false;
this._singlePage = options.singlePage ?? true;
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;
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"
};
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
}
this.userDetails = new UIEventSource<UserDetails>(
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 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();
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();
self.AttemptLogin()
}
});
})
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false);
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
this.updateAuthObject();
this.updateAuthObject()
this.preferencesHandler = new OsmPreferences(
this.auth,
<any /*This is needed to make the tests work*/>this
);
)
if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data);
const self = this;
console.log(options.oauth_token.data)
const self = this
this.auth.bootstrapToken(
options.oauth_token.data,
(x) => {
console.log("Called back: ", x);
self.AttemptLogin();
console.log("Called back: ", x)
self.AttemptLogin()
},
this.auth
);
)
options.oauth_token.setData(undefined);
options.oauth_token.setData(undefined)
}
if (this.auth.authenticated() && options.attemptLogin !== false) {
this.AttemptLogin(); // Also updates the user badge
this.AttemptLogin() // Also updates the user badge
} else {
console.log("Not authenticated");
console.log("Not authenticated")
}
}
@ -156,25 +153,25 @@ export class OsmConnection {
prefix?: string
}
): UIEventSource<string> {
return this.preferencesHandler.GetPreference(key, defaultValue, options);
return this.preferencesHandler.GetPreference(key, defaultValue, options)
}
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
return this.preferencesHandler.GetLongPreference(key, prefix);
return this.preferencesHandler.GetLongPreference(key, prefix)
}
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
this._onLoggedIn.push(action);
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");
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")
}
/**
@ -183,95 +180,95 @@ export class OsmConnection {
* new OsmConnection().Backend() // => "https://www.openstreetmap.org"
*/
public Backend(): string {
return this._oauth_config.url;
return this._oauth_config.url
}
public AttemptLogin() {
this.UpdateCapabilities();
this.loadingStatus.setData("loading");
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;
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();
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"
path: "/api/0.6/user/details",
},
function(err, details) {
function (err, details) {
if (err != null) {
console.log(err);
self.loadingStatus.setData("error");
console.log(err)
self.loadingStatus.setData("error")
if (err.status == 401) {
console.log("Clearing tokens...");
console.log("Clearing tokens...")
// Not authorized - our token probably got revoked
self.auth.logout();
self.LogOut();
self.auth.logout()
self.LogOut()
}
return;
return
}
if (details == null) {
self.loadingStatus.setData("error");
return;
self.loadingStatus.setData("error")
return
}
self.CheckForMessagesContinuously();
self.CheckForMessagesContinuously()
// details is an XML DOM of user details
let userInfo = details.getElementsByTagName("user")[0];
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"));
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");
data.img = undefined
const imgEl = userInfo.getElementsByTagName("img")
if (imgEl !== undefined && imgEl[0] !== undefined) {
data.img = imgEl[0].getAttribute("href");
data.img = imgEl[0].getAttribute("href")
}
const description = userInfo.getElementsByTagName("description");
const description = userInfo.getElementsByTagName("description")
if (description !== undefined && description[0] !== undefined) {
data.description = description[0]?.innerHTML;
data.description = description[0]?.innerHTML
}
const homeEl = userInfo.getElementsByTagName("home");
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 lat = parseFloat(homeEl[0].getAttribute("lat"))
const lon = parseFloat(homeEl[0].getAttribute("lon"))
data.home = { lat: lat, lon: lon }
}
self.loadingStatus.setData("logged-in");
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"));
.getElementsByTagName("received")[0]
data.unreadMessages = parseInt(messages.getAttribute("unread"))
data.totalMessages = parseInt(messages.getAttribute("count"))
self.userDetails.ping();
self.userDetails.ping()
for (const action of self._onLoggedIn) {
action(self.userDetails.data);
action(self.userDetails.data)
}
self._onLoggedIn = [];
self._onLoggedIn = []
}
);
)
}
/**
@ -290,20 +287,20 @@ export class OsmConnection {
{
method,
options: {
header
header,
},
content,
path: `/api/0.6/${path}`
path: `/api/0.6/${path}`,
},
function(err, response) {
function (err, response) {
if (err !== null) {
error(err);
error(err)
} else {
ok(response);
ok(response)
}
}
);
});
)
})
}
public async post(
@ -311,7 +308,7 @@ export class OsmConnection {
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "POST", header, content);
return await this.interact(path, "POST", header, content)
}
public async put(
@ -319,60 +316,60 @@ export class OsmConnection {
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "PUT", header, content);
return await this.interact(path, "PUT", header, content)
}
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
return await this.interact(path, "GET", header);
return await this.interact(path, "GET", header)
}
public closeNote(id: number | string, text?: string): Promise<void> {
let textSuffix = "";
let textSuffix = ""
if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text);
textSuffix = "?text=" + encodeURIComponent(text)
}
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text);
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok) => {
ok();
});
ok()
})
}
return this.post(`notes/${id}/close${textSuffix}`);
return this.post(`notes/${id}/close${textSuffix}`)
}
public reopenNote(id: number | string, text?: string): Promise<void> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text);
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok) => {
ok();
});
ok()
})
}
let textSuffix = "";
let textSuffix = ""
if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text);
textSuffix = "?text=" + encodeURIComponent(text)
}
return this.post(`notes/${id}/reopen${textSuffix}`);
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);
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;
"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(
@ -390,61 +387,61 @@ export class OsmConnection {
}
): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually uploading GPX ", gpx);
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
};
visibility: options.visibility,
}
const extras = {
file:
"; filename=\"" +
'; filename="' +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
"\"\r\nContent-Type: application/gpx+xml"
};
'"\r\nContent-Type: application/gpx+xml',
}
const boundary = "987654";
const boundary = "987654"
let body = "";
let body = ""
for (const key in contents) {
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"" + key + "\"";
body += "--" + boundary + "\r\n"
body += 'Content-Disposition: form-data; name="' + key + '"'
if (extras[key] !== undefined) {
body += extras[key];
body += extras[key]
}
body += "\r\n\r\n";
body += contents[key] + "\r\n";
body += "\r\n\r\n"
body += contents[key] + "\r\n"
}
body += "--" + boundary + "--\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 };
"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<void> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id);
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)
return new Promise((ok) => {
ok();
});
ok()
})
}
if ((text ?? "") === "") {
throw "Invalid text!";
throw "Invalid text!"
}
return new Promise((ok, error) => {
@ -452,50 +449,50 @@ export class OsmConnection {
{
method: "POST",
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
},
function(err, _) {
function (err, _) {
if (err !== null) {
error(err);
error(err)
} else {
ok();
ok()
}
}
);
});
)
})
}
/**
* To be called by land.html
*/
public finishLogin(callback: (previousURL: string) => void) {
this.auth.authenticate(function() {
this.auth.authenticate(function () {
// Fully authed at this point
console.log("Authentication successful!");
const previousLocation = LocalStorageSource.Get("location_before_login");
callback(previousLocation.data);
});
console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login")
callback(previousLocation.data)
})
}
private updateAuthObject() {
let pwaStandAloneMode = false;
let pwaStandAloneMode = false
try {
if (Utils.runningFromConsole) {
pwaStandAloneMode = true;
pwaStandAloneMode = true
} else if (
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches
) {
pwaStandAloneMode = true;
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;
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...
@ -508,46 +505,46 @@ export class OsmConnection {
? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html",
singlepage: !standalone,
auto: true
});
auto: true,
})
}
private CheckForMessagesContinuously() {
const self = this;
const self = this
if (this.isChecking) {
return;
return
}
this.isChecking = true;
this.isChecking = true
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
if (self.isLoggedIn.data) {
console.log("Checking for messages");
self.AttemptLogin();
console.log("Checking for messages")
self.AttemptLogin()
}
});
})
}
private UpdateCapabilities(): void {
const self = this;
const self = this
this.FetchCapabilities().then(({ api, gpx }) => {
self.apiIsOnline.setData(api);
self.gpxServiceIsOnline.setData(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" };
return { api: "online", gpx: "online" }
}
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities");
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" };
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 = <OsmServiceState>statusEl.getAttribute("api");
const gpx = <OsmServiceState>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 = <OsmServiceState>statusEl.getAttribute("api")
const gpx = <OsmServiceState>statusEl.getAttribute("gpx")
return { api, gpx }
}
}

View file

@ -1,44 +1,45 @@
<script lang="ts">
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import Tr from "../Base/Tr.svelte";
import NextButton from "../Base/NextButton.svelte";
import Geosearch from "./Geosearch.svelte";
import ToSvelte from "../Base/ToSvelte.svelte";
import ThemeViewState from "../../Models/ThemeViewState";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
import { twJoin } from "tailwind-merge";
import { Utils } from "../../Utils";
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
import Translations from "../i18n/Translations"
import Svg from "../../Svg"
import Tr from "../Base/Tr.svelte"
import NextButton from "../Base/NextButton.svelte"
import Geosearch from "./Geosearch.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twJoin } from "tailwind-merge"
import { Utils } from "../../Utils"
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
/**
* The theme introduction panel
*/
export let state: ThemeViewState;
let layout = state.layout;
let selectedElement = state.selectedElement;
let selectedLayer = state.selectedLayer;
export let state: ThemeViewState
let layout = state.layout
let selectedElement = state.selectedElement
let selectedLayer = state.selectedLayer
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
let searchEnabled = false;
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
let geopermission: Store<GeolocationPermissionState> = state.geolocation.geolocationState.permission;
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
let geopermission: Store<GeolocationPermissionState> =
state.geolocation.geolocationState.permission
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
geopermission.addCallback(perm => console.log(">>>> Permission", perm));
geopermission.addCallback((perm) => console.log(">>>> Permission", perm))
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState;
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
state.guistate.themeIsOpened.setData(false);
const coor = { lon: c.longitude, lat: c.latitude };
state.mapProperties.location.setData(coor);
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
state.guistate.themeIsOpened.setData(false)
const coor = { lon: c.longitude, lat: c.latitude }
state.mapProperties.location.setData(coor)
}
if (glstate.permission.data !== "granted") {
glstate.requestPermission();
return;
glstate.requestPermission()
return
}
}
</script>
@ -69,13 +70,13 @@
</button>
<!-- No geolocation granted - we don't show the button -->
{:else if $geopermission === "requested"}
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
<button class="disabled flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetStyle("animation: 3s linear 0s infinite normal none running spin;")} />
<Tr t={Translations.t.general.waitingForGeopermission} />
</button>
{:else if $geopermission === "denied"}
<button class="flex w-full items-center gap-x-2 disabled">
<button class="disabled flex w-full items-center gap-x-2">
<ToSvelte construct={Svg.location_refused_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.geopermissionDenied} />
</button>
@ -84,7 +85,6 @@
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetStyle("animation: 3s linear 0s infinite normal none running spin;")} />
<Tr t={Translations.t.general.waitingForLocation} />
</button>
{/if}
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">