From 4852888b41b9c532a08fbf963d38cc810bc53787 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 27 Sep 2023 22:21:35 +0200 Subject: [PATCH 01/41] Refactoring: make needed URLs explicit --- assets/layers/questions/questions.json | 8 +- package.json | 15 +- scripts/build.sh | 7 +- scripts/generateLayouts.ts | 75 +- scripts/hetzner/config/Caddyfile | 1 - scripts/hetzner/deployHetzner.sh | 9 +- src/InstallServiceWorker.ts | 13 + src/Logic/ImageProviders/AllImageProviders.ts | 20 +- .../ImageProviders/GenericImageProvider.ts | 4 + src/Logic/ImageProviders/ImageProvider.ts | 2 + src/Logic/ImageProviders/Imgur.ts | 16 +- src/Logic/ImageProviders/Mapillary.ts | 4 + .../ImageProviders/WikidataImageProvider.ts | 3 + .../ImageProviders/WikimediaImageProvider.ts | 8 +- src/Logic/Maproulette.ts | 3 +- src/Logic/Osm/AuthConfig.ts | 6 + src/Logic/Osm/OsmConnection.ts | 1025 ++++++++--------- src/Logic/State/FeatureSwitchState.ts | 7 - src/Logic/Web/NearbyImagesSearch.ts | 43 +- src/Logic/Web/PlantNet.ts | 2 +- src/Logic/Web/Wikidata.ts | 9 +- src/Logic/Web/Wikipedia.ts | 2 + src/Models/Constants.ts | 4 +- src/Models/ThemeViewState.ts | 3 +- src/UI/AllThemesGui.ts | 3 +- src/UI/BigComponents/OpenJosm.ts | 1 + src/UI/Popup/AddNoteCommentViz.ts | 2 + src/UI/Popup/AutoApplyButton.ts | 12 +- src/UI/Popup/CloseNoteButton.ts | 2 + src/UI/Popup/ExportAsGpxViz.ts | 2 +- src/UI/Popup/HistogramViz.ts | 3 + .../ImportButtons/ConflateImportButtonViz.ts | 1 + src/UI/Popup/ImportButtons/ImportFlow.ts | 5 +- .../ImportButtons/PointImportButtonViz.ts | 1 + .../Popup/ImportButtons/WayImportButtonViz.ts | 1 + src/UI/Popup/LanguageElement.ts | 1 + src/UI/Popup/MapillaryLinkVis.ts | 2 + src/UI/Popup/MinimapViz.ts | 1 + src/UI/Popup/MultiApplyViz.ts | 1 + src/UI/Popup/PlantNetDetectionViz.ts | 3 +- src/UI/Popup/QuestionViz.ts | 2 + src/UI/Popup/ShareLinkViz.ts | 1 + src/UI/Popup/TagApplyButton.ts | 1 + src/UI/Popup/UploadToOsmViz.ts | 2 + src/UI/RemoveOtherLanguages.ts | 32 + src/UI/SpecialVisualization.ts | 192 +-- src/UI/SpecialVisualizations.ts | 223 ++-- src/UI/SubstitutedTranslation.ts | 1 + src/assets/editor-layer-index.json | 7 +- src/index_theme.ts.template | 1 + theme.html | 57 +- 51 files changed, 978 insertions(+), 871 deletions(-) create mode 100644 src/InstallServiceWorker.ts create mode 100644 src/Logic/Osm/AuthConfig.ts create mode 100644 src/UI/RemoveOtherLanguages.ts diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index a3cf36052..2aff3d584 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -1658,7 +1658,9 @@ }, { "id": "repeated", - "labels": ["level"], + "labels": [ + "level" + ], "condition": "repeat_on~*", "render": { "en": "Multiple, identical objects can be found on floors {repeat_on}.", @@ -1667,7 +1669,9 @@ }, { "id": "single_level", - "labels": ["level"], + "labels": [ + "level" + ], "condition": "repeat_on=", "question": { "nl": "Op welke verdieping bevindt dit punt zich?", diff --git a/package.json b/package.json index 66bf5bccb..98b48288c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.33.4", + "version": "0.33.5", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", @@ -18,23 +18,10 @@ "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`" ], "oauth_credentials": { - "osm_pietervdvn": { - "#": "This client_id is registered by 'Pieter Vander Vennet' on OSM.org", - "oauth_client_id": "sa1ngLJBJ8McmzHElN8NYtIDm5TZTYEYhq3-0snO4Qc", - "oauth_secret": "XU_cD5Mvw9VKk9T0t_gO8V7cbRC4Hmw2Tb4Rv0Zmz-U", - "url": "https://www.openstreetmap.org" - }, - "osm": { "#": "This client-id is registered by 'MapComplete' on osm.org", "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", "url": "https://www.openstreetmap.org" - }, - "osm-test": { - "oauth_client_id": "HwUn6GPxGm1m9WwMarxTglhy6dBTM4YkaV1I9h6pDGU", - "oauth_secret": "luFZtPJg7j96K6WM6RpcZ_3M-r6muuDq6fG1ygk0I_4", - "url": "https://master.apis.dev.openstreetmap.org" - } }, "api_keys": { "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", diff --git a/scripts/build.sh b/scripts/build.sh index 737f929d4..4a2fe310d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null export NODE_OPTIONS="--max-old-space-size=8192" # This script ends every line with '&&' to chain everything. A failure will thus stop the build -# npm run generate:editor-layer-index && -# npm run generate && +npm run generate:editor-layer-index && +npm run generate && npm run generate:layouts if [ $? -ne 0 ]; then @@ -38,7 +38,8 @@ then export ASSET_URL echo "$ASSET_URL" else - ASSET_URL="$BRANCH" + # ASSET_URL="$BRANCH" + ASSET_URL="./" export ASSET_URL echo "$ASSET_URL" fi diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index d62a736d2..4a8086728 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -8,6 +8,10 @@ import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig" import xml2js from "xml2js" import ScriptUtils from "./ScriptUtils" import { Utils } from "../src/Utils" +import SpecialVisualizations from "../src/UI/SpecialVisualizations" +import Constants from "../src/Models/Constants" +import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" +import { ImmutableStore } from "../src/Logic/UIEventSource" const sharp = require("sharp") const template = readFileSync("theme.html", "utf8") @@ -195,29 +199,72 @@ function asLangSpan(t: Translation, tag = "span"): string { if (lang === "_context") { continue } - values.push(`<${tag} lang='${lang}'>${t.translations[lang]}`) + values.push(`<${tag} lang="${lang}">${t.translations[lang]}`) } return values.join("\n") } -let cspCached: string = undefined -function generateCsp(): string { - if (cspCached !== undefined) { - return cspCached +let previousSrc: Set = new Set() +function generateCsp(layout: LayoutConfig): string { + const apiUrls: string[] = [ + "self", + ...Constants.defaultOverpassUrls, + Constants.countryCoderEndpoint, + "https://api.openstreetmap.org", + "https://pietervdvn.goatcounter.com", + ].concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) + + const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) + const hosts = new Set() + const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( + new ImmutableStore({ lon: 0, lat: 0 }) + ).data + const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") + const vectorSources = vectorLayers.map((l) => l.properties.url) + apiUrls.push(...vectorSources) + for (const connectSource of apiUrls.concat(geojsonSources)) { + if (!connectSource) { + continue + } + try { + const url = new URL(connectSource) + hosts.add("https://" + url.host) + } catch (e) { + hosts.add(connectSource) + } } + const connectSrc = Array.from(hosts).sort() + + const newSrcs = connectSrc.filter((newItem) => !previousSrc.has(newItem)) + + console.log( + "Got", + hosts.size, + "connect-src items for theme", + layout.id, + "(extra sources: ", + newSrcs.join(" ") + ")" + ) + previousSrc = hosts + const csp = { "default-src": "'self'", - "script-src": "'self'", - "img-src": "*", - "connect-src": "*", + "script-src": "'self' https://gc.zgo.at/count.js", + "img-src": "* data:", // maplibre depends on 'data:' to load + "connect-src": connectSrc.join(" "), + "report-to": "https://report.mapcomplete.org/csp", + "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob' + "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours } const content = Object.keys(csp) - .map((k) => k + ": " + csp[k]) + .map((k) => k + " " + csp[k]) .join("; ") - cspCached = `` - return cspCached + return [ + ``, + ``, + ].join("\n") } async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { @@ -290,7 +337,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr ...apple_icons, ].join("\n") - const loadingText = Translations.t.general.loadingTheme.Subs({ theme: ogTitle }) + const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) @@ -299,7 +346,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr Translations.t.general.poweredByOsm.textFor(targetLanguage) ) .replace(/.*/s, themeSpecific) - .replace(//, generateCsp()) + .replace(//, generateCsp(layout)) .replace( /.*/s, asLangSpan(layout.shortDescription) @@ -311,7 +358,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr .replace( '', - `` + `` ) return output diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index a417808d2..1638f7541 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -4,7 +4,6 @@ hosted.mapcomplete.org { header { +Permissions-Policy "interest-cohort=()" +Report-To `\{"group":"csp-endpoint", "max_age": 86400,"endpoints": [\{"url": "https://report.mapcomplete.org/csp"}], "include_subdomains": true}` - +Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://gc.zgo.at ; img-src * ; report-uri https://report.mapcomplete.org/csp ; report-to csp-endpoint ;" } } diff --git a/scripts/hetzner/deployHetzner.sh b/scripts/hetzner/deployHetzner.sh index 8d692a40c..587a5fddb 100755 --- a/scripts/hetzner/deployHetzner.sh +++ b/scripts/hetzner/deployHetzner.sh @@ -10,14 +10,15 @@ # unzip tiles.zip MAPCOMPLETE_CONFIGURATION="config_hetzner" +cp config.json config.json.bu && +cp ./scripts/hetzner/config.json . && # Copy the config _before_ building, as the config might contain some needed URLs npm run reset:layeroverview npm run test -cp config.json config.json.bu && -cp ./scripts/hetzner/config.json . && npm run prepare-deploy && mv config.json.bu config.json && zip dist.zip -r dist/* && scp -r dist.zip hetzner:/root/ && -scp ./scripts/hetzner/config/* hetzner:/root/ -ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" +echo "Upload completed, deploying config and booting" && +rsync -rzh --progress dist.zip hetzner:/root/ && +ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" && rm dist.zip diff --git a/src/InstallServiceWorker.ts b/src/InstallServiceWorker.ts new file mode 100644 index 000000000..49afbed20 --- /dev/null +++ b/src/InstallServiceWorker.ts @@ -0,0 +1,13 @@ +export {} +window.addEventListener("load", async () => { + if (!("serviceWorker" in navigator)) { + console.log("Service workers are not supported") + return + } + try { + await navigator.serviceWorker.register("/service-worker.js") + console.log("Service worker registration successful") + } catch (err) { + console.error("Service worker registration failed", err) + } +}) diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts index 89318aeb3..5fc9c1ec7 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -23,27 +23,27 @@ export default class AllImageProviders { ) ), ] - + public static apiUrls: string[] = [].concat( + ...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()) + ) + public static defaultKeys = [].concat( + AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) + ) private static providersByName = { imgur: Imgur.singleton, mapillary: Mapillary.singleton, wikidata: WikidataImageProvider.singleton, wikimedia: WikimediaImageProvider.singleton, } - - public static byName(name: string) { - return AllImageProviders.providersByName[name.toLowerCase()] - } - - public static defaultKeys = [].concat( - AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) - ) - private static _cache: Map> = new Map< string, UIEventSource >() + public static byName(name: string) { + return AllImageProviders.providersByName[name.toLowerCase()] + } + public static LoadImagesFor( tags: Store>, tagKey?: string[] diff --git a/src/Logic/ImageProviders/GenericImageProvider.ts b/src/Logic/ImageProviders/GenericImageProvider.ts index 4cb382b23..178137027 100644 --- a/src/Logic/ImageProviders/GenericImageProvider.ts +++ b/src/Logic/ImageProviders/GenericImageProvider.ts @@ -3,6 +3,10 @@ import ImageProvider, { ProvidedImage } from "./ImageProvider" export default class GenericImageProvider extends ImageProvider { public defaultKeyPrefixes: string[] = ["image"] + public apiUrls(): string[] { + return [] + } + private readonly _valuePrefixBlacklist: string[] public constructor(valuePrefixBlacklist: string[]) { diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 92d3bc941..6d42623ce 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -65,4 +65,6 @@ export default abstract class ImageProvider { public abstract ExtractUrls(key: string, value: string): Promise[]> public abstract DownloadAttribution(url: string): Promise + + public abstract apiUrls(): string[] } diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index 4e4a1c541..86787f89b 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -4,16 +4,23 @@ import { Utils } from "../../Utils"; import Constants from "../../Models/Constants"; import { LicenseInfo } from "./LicenseInfo"; import { ImageUploader } from "./ImageUploader"; +import Img from "../../UI/Base/Img"; -export class Imgur extends ImageProvider implements ImageUploader{ +export class Imgur extends ImageProvider implements ImageUploader { public static readonly defaultValuePrefix = ["https://i.imgur.com"] public static readonly singleton = new Imgur() public readonly defaultKeyPrefixes: string[] = ["image"] - public readonly maxFileSizeInMegabytes = 10 + public readonly maxFileSizeInMegabytes = 10 + public static readonly apiUrl = "https://api.imgur.com/3/image" + private constructor() { super() } + apiUrls(): string[] { + return [Imgur.apiUrl] + } + /** * Uploads an image, returns the URL where to find the image * @param title @@ -24,8 +31,8 @@ export class Imgur extends ImageProvider implements ImageUploader{ title: string, description: string, blob: File - ): Promise<{ key: string, value: string }> { - const apiUrl = "https://api.imgur.com/3/image" + ): Promise<{ key: string; value: string }> { + const apiUrl = Imgur.apiUrl const apiKey = Constants.ImgurApiKey const formData = new FormData() @@ -33,7 +40,6 @@ export class Imgur extends ImageProvider implements ImageUploader{ formData.append("title", title) formData.append("description", description) - const settings: RequestInit = { method: "POST", body: formData, diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index 102bb709b..96ec29ebc 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -17,6 +17,10 @@ export class Mapillary extends ImageProvider { ] defaultKeyPrefixes = ["mapillary", "image"] + apiUrls(): string[] { + return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"] + } + /** * Indicates that this is the same URL * Ignores 'stp' parameter diff --git a/src/Logic/ImageProviders/WikidataImageProvider.ts b/src/Logic/ImageProviders/WikidataImageProvider.ts index 043fee4b7..1c7cbea8a 100644 --- a/src/Logic/ImageProviders/WikidataImageProvider.ts +++ b/src/Logic/ImageProviders/WikidataImageProvider.ts @@ -5,6 +5,9 @@ import { WikimediaImageProvider } from "./WikimediaImageProvider" import Wikidata from "../Web/Wikidata" export class WikidataImageProvider extends ImageProvider { + public apiUrls(): string[] { + return Wikidata.neededUrls + } public static readonly singleton = new WikidataImageProvider() public readonly defaultKeyPrefixes = ["wikidata"] diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index f9f40261d..a9841b8fd 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -11,11 +11,11 @@ import Wikimedia from "../Web/Wikimedia" */ export class WikimediaImageProvider extends ImageProvider { public static readonly singleton = new WikimediaImageProvider() - public static readonly commonsPrefixes = [ + public static readonly apiUrls = [ "https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", - "File:", ] + public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"] private readonly commons_key = "wikimedia_commons" public readonly defaultKeyPrefixes = [this.commons_key, "image"] @@ -66,6 +66,10 @@ export class WikimediaImageProvider extends ImageProvider { return value } + apiUrls(): string[] { + return WikimediaImageProvider.apiUrls + } + SourceIcon(backlink: string): BaseUIElement { const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") if (backlink === undefined) { diff --git a/src/Logic/Maproulette.ts b/src/Logic/Maproulette.ts index 50f550c43..1eaf999b3 100644 --- a/src/Logic/Maproulette.ts +++ b/src/Logic/Maproulette.ts @@ -32,11 +32,12 @@ export default class Maproulette { private readonly apiKey: string public static singleton = new Maproulette() + public static readonly defaultEndpoint = "https://maproulette.org/api/v2" /** * Creates a new Maproulette instance * @param endpoint The API endpoint to use */ - constructor(endpoint: string = "https://maproulette.org/api/v2") { + constructor(endpoint: string = Maproulette.defaultEndpoint) { this.endpoint = endpoint this.apiKey = Constants.MaprouletteApiKey } diff --git a/src/Logic/Osm/AuthConfig.ts b/src/Logic/Osm/AuthConfig.ts new file mode 100644 index 000000000..b3c2330e2 --- /dev/null +++ b/src/Logic/Osm/AuthConfig.ts @@ -0,0 +1,6 @@ +export interface AuthConfig { + "#"?: string // optional comment + oauth_client_id: string + oauth_secret: string + url: string +} diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 07028c35a..13a029439 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -1,553 +1,542 @@ // @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 { AuthConfig } from "./AuthConfig" +import Constants from "../../Models/Constants" 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; - } -} - -export interface AuthConfig { - "#"?: string; // optional comment - oauth_client_id: string; - oauth_secret: string; - url: string; + constructor(backend: string) { + this.backend = backend + } } 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 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 readonly 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 + attemptLogin?: true | boolean + }) { + options ??= {} + this.fakeUser = options?.fakeUser ?? false + this._singlePage = options?.singlePage ?? true + this._oauth_config = Constants.osmAuthConfig + 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, + oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET, + url: "https://api.openstreetmap.org", + } } - 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/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index a776ae6af..d2e25b987 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -28,14 +28,8 @@ class FeatureSwitchUtils { export class OsmConnectionFeatureSwitches { public readonly featureSwitchFakeUser: UIEventSource - public readonly featureSwitchApiURL: UIEventSource constructor() { - this.featureSwitchApiURL = QueryParameters.GetQueryParameter( - "backend", - "osm", - "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" - ) this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter( "fake-user", @@ -143,7 +137,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { let testingDefaultValue = false if ( - this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole && (location.hostname === "localhost" || location.hostname === "127.0.0.1") ) { diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts index 665cc66e7..2c81f19e0 100644 --- a/src/Logic/Web/NearbyImagesSearch.ts +++ b/src/Logic/Web/NearbyImagesSearch.ts @@ -1,9 +1,9 @@ import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { GeoOperations } from "../GeoOperations" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" -import { Mapillary } from "../ImageProviders/Mapillary" import P4C from "pic4carto" import { Utils } from "../../Utils" + export interface NearbyImageOptions { lon: number lat: number @@ -35,17 +35,12 @@ export interface P4CPicture { } /** - * Uses Pic4wCarto to fetch nearby images from various providers + * Uses Pic4Carto to fetch nearby images from various providers */ export default class NearbyImagesSearch { - private static readonly services = [ - "mapillary", - "flickr", - "openstreetcam", - "wikicommons", - ] as const - - private individualStores + public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const + public static readonly apiUrls = ["https://api.flickr.com"] + private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[] private readonly _store: UIEventSource = new UIEventSource([]) public readonly store: Store = this._store private readonly _options: NearbyImageOptions @@ -71,16 +66,16 @@ export default class NearbyImagesSearch { this.update() } - private static buildPictureFetcher( + private static async fetchImages( options: NearbyImageOptions, - fetcher: "mapillary" | "flickr" | "openstreetcam" | "wikicommons" - ): Store<{ images: P4CPicture[]; beforeFilter: number }> { + fetcher: P4CService + ): Promise { const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] }) - const searchRadius = options.searchRadius ?? 100 const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000 + const searchRadius = options.searchRadius ?? 100 - const p4cStore = Stores.FromPromise( - picManager.startPicsRetrievalAround( + try { + const pics: P4CPicture[] = await picManager.startPicsRetrievalAround( new P4C.LatLng(options.lat, options.lon), searchRadius, { @@ -88,7 +83,21 @@ export default class NearbyImagesSearch { towardscenter: false, } ) + return pics + } catch (e) { + console.error("Could not fetch images from service", fetcher, e) + return [] + } + } + + private static buildPictureFetcher( + options: NearbyImageOptions, + fetcher: P4CService + ): Store<{ images: P4CPicture[]; beforeFilter: number }> { + const p4cStore = Stores.FromPromise( + NearbyImagesSearch.fetchImages(options, fetcher) ) + const searchRadius = options.searchRadius ?? 100 return p4cStore.map( (images) => { if (images === undefined) { @@ -220,3 +229,5 @@ class ImagesInLoadedDataFetcher { return foundImages } } + +type P4CService = (typeof NearbyImagesSearch.services)[number] diff --git a/src/Logic/Web/PlantNet.ts b/src/Logic/Web/PlantNet.ts index dab705ad0..4012040e0 100644 --- a/src/Logic/Web/PlantNet.ts +++ b/src/Logic/Web/PlantNet.ts @@ -1,7 +1,7 @@ import { Utils } from "../../Utils" export default class PlantNet { - private static baseUrl = + public static baseUrl = "https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe" public static query(imageUrls: string[]): Promise { diff --git a/src/Logic/Web/Wikidata.ts b/src/Logic/Web/Wikidata.ts index 31eaa33d8..1f60f32ec 100644 --- a/src/Logic/Web/Wikidata.ts +++ b/src/Logic/Web/Wikidata.ts @@ -123,6 +123,11 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { * Utility functions around wikidata */ export default class Wikidata { + public static readonly neededUrls = [ + "https://www.wikidata.org/", + "https://wikidata.org/", + "https://query.wikidata.org", + ] private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase()) private static readonly _prefixesToRemove = [ "https://www.wikidata.org/wiki/Lexeme:", @@ -130,11 +135,11 @@ export default class Wikidata { "http://www.wikidata.org/entity/", "Lexeme:", ].map((str) => str.toLowerCase()) - private static readonly _storeCache = new Map< string, Store<{ success: WikidataResponse } | { error: any }> >() + /** * Same as LoadWikidataEntry, but wrapped into a UIEventSource * @param value @@ -388,6 +393,7 @@ export default class Wikidata { } private static _cache = new Map>() + public static async LoadWikidataEntryAsync(value: string | number): Promise { const key = "" + value const cached = Wikidata._cache.get(key) @@ -398,6 +404,7 @@ export default class Wikidata { Wikidata._cache.set(key, uncached) return uncached } + /** * Loads a wikidata page * @returns the entity of the given value diff --git a/src/Logic/Web/Wikipedia.ts b/src/Logic/Web/Wikipedia.ts index 0cde301db..81f7bba90 100644 --- a/src/Logic/Web/Wikipedia.ts +++ b/src/Logic/Web/Wikipedia.ts @@ -34,6 +34,8 @@ export default class Wikipedia { private static readonly idsToRemove = ["sjabloon_zie"] + public static readonly neededUrls = ["*.wikipedia.org"] + private static readonly _cache = new Map>() private static _fullDetailsCache = new Map>() public readonly backend: string diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index 75ea6f73f..35959c4ca 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -1,6 +1,7 @@ import * as packagefile from "../../package.json" import * as extraconfig from "../../config.json" import { Utils } from "../Utils" +import { AuthConfig } from "../Logic/Osm/AuthConfig" export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number] @@ -104,7 +105,8 @@ export default class Constants { public static ImgurApiKey = Constants.config.api_keys.imgur public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4 public static defaultOverpassUrls = Constants.config.default_overpass_urls - static countryCoderEndpoint: string = Constants.config.country_coder_host + public static countryCoderEndpoint: string = Constants.config.country_coder_host + public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials /** * These are the values that are allowed to use as 'backdrop' icon for a map pin diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 1adc59b5a..eb7835991 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -140,8 +140,7 @@ export default class ThemeViewState implements SpecialVisualizationState { "oauth_token", undefined, "Used to complete the login" - ), - osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, + ) }) this.userRelatedState = new UserRelatedState( this.osmConnection, diff --git a/src/UI/AllThemesGui.ts b/src/UI/AllThemesGui.ts index 437fb3ac4..1cf890316 100644 --- a/src/UI/AllThemesGui.ts +++ b/src/UI/AllThemesGui.ts @@ -22,8 +22,7 @@ export default class AllThemesGui { "oauth_token", undefined, "Used to complete the login" - ), - osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data, + ) }) const state = new UserRelatedState(osmConnection) const intro = new Combine([ diff --git a/src/UI/BigComponents/OpenJosm.ts b/src/UI/BigComponents/OpenJosm.ts index 8f86f6d98..3eafbd2ac 100644 --- a/src/UI/BigComponents/OpenJosm.ts +++ b/src/UI/BigComponents/OpenJosm.ts @@ -11,6 +11,7 @@ import { Utils } from "../../Utils" import Constants from "../../Models/Constants" export class OpenJosm extends Combine { + public static readonly needsUrls = ["http://127.0.0.1:8111/load_and_zoom"] constructor(osmConnection: OsmConnection, bounds: Store, iconStyle?: string) { const t = Translations.t.general.attribution diff --git a/src/UI/Popup/AddNoteCommentViz.ts b/src/UI/Popup/AddNoteCommentViz.ts index 15bc8c1cd..e40a6d041 100644 --- a/src/UI/Popup/AddNoteCommentViz.ts +++ b/src/UI/Popup/AddNoteCommentViz.ts @@ -10,9 +10,11 @@ import Combine from "../Base/Combine" import Title from "../Base/Title" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { UIEventSource } from "../../Logic/UIEventSource" +import Constants from "../../Models/Constants" export class AddNoteCommentViz implements SpecialVisualization { funcName = "add_note_comment" + needsUrls = [Constants.osmAuthConfig.url] docs = "A textfield to add a comment to a node (with the option to close the note)." args = [ { diff --git a/src/UI/Popup/AutoApplyButton.ts b/src/UI/Popup/AutoApplyButton.ts index 2a38c6dd1..4f89f23c5 100644 --- a/src/UI/Popup/AutoApplyButton.ts +++ b/src/UI/Popup/AutoApplyButton.ts @@ -9,7 +9,6 @@ import { Utils } from "../../Utils" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import { VariableUiElement } from "../Base/VariableUIElement" import Loading from "../Base/Loading" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" import Translations from "../i18n/Translations" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { Changes } from "../../Logic/Osm/Changes" @@ -209,6 +208,8 @@ class ApplyButton extends UIElement { export default class AutoApplyButton implements SpecialVisualization { public readonly docs: BaseUIElement public readonly funcName: string = "auto_apply" + public readonly needsUrls = [] + public readonly args: { name: string defaultValue?: string @@ -271,14 +272,7 @@ export default class AutoApplyButton implements SpecialVisualization { argument: string[] ): BaseUIElement { try { - if ( - !state.layout.official && - !( - state.featureSwitchIsTesting.data || - state.osmConnection._oauth_config.url === - OsmConnection.oauth_configs["osm-test"].url - ) - ) { + if (!state.layout.official && !state.featureSwitchIsTesting.data) { const t = Translations.t.general.add.import return new Combine([ new FixedUiElement( diff --git a/src/UI/Popup/CloseNoteButton.ts b/src/UI/Popup/CloseNoteButton.ts index d1cf0ce29..21bce9253 100644 --- a/src/UI/Popup/CloseNoteButton.ts +++ b/src/UI/Popup/CloseNoteButton.ts @@ -8,9 +8,11 @@ import Toggle from "../Input/Toggle" import { LoginToggle } from "./LoginButton" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { UIEventSource } from "../../Logic/UIEventSource" +import Constants from "../../Models/Constants" export class CloseNoteButton implements SpecialVisualization { public readonly funcName = "close_note" + public readonly needsUrls = [Constants.osmAuthConfig.url] public readonly docs = "Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text." public readonly args = [ diff --git a/src/UI/Popup/ExportAsGpxViz.ts b/src/UI/Popup/ExportAsGpxViz.ts index 63ae33b5d..c7821da10 100644 --- a/src/UI/Popup/ExportAsGpxViz.ts +++ b/src/UI/Popup/ExportAsGpxViz.ts @@ -13,7 +13,7 @@ export class ExportAsGpxViz implements SpecialVisualization { funcName = "export_as_gpx" docs = "Exports the selected feature as GPX-file" args = [] - + needsUrls = [] constr( state: SpecialVisualizationState, tagSource: UIEventSource>, diff --git a/src/UI/Popup/HistogramViz.ts b/src/UI/Popup/HistogramViz.ts index fffc451d4..94b0f2b89 100644 --- a/src/UI/Popup/HistogramViz.ts +++ b/src/UI/Popup/HistogramViz.ts @@ -2,10 +2,13 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import Histogram from "../BigComponents/Histogram" import { Feature } from "geojson" +import Constants from "../../Models/Constants" export class HistogramViz implements SpecialVisualization { funcName = "histogram" docs = "Create a histogram for a list of given values, read from the properties." + needsUrls = [] + example = '`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram' args = [ diff --git a/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts b/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts index 8c924c37d..fee676bf7 100644 --- a/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts +++ b/src/UI/Popup/ImportButtons/ConflateImportButtonViz.ts @@ -24,6 +24,7 @@ export interface ConflateFlowArguments extends ImportFlowArguments { export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction { supportsAutoAction: boolean = true + needsUrls = [] public readonly funcName: string = "conflate_button" public readonly args: { name: string diff --git a/src/UI/Popup/ImportButtons/ImportFlow.ts b/src/UI/Popup/ImportButtons/ImportFlow.ts index caf9f3b9c..50943d8af 100644 --- a/src/UI/Popup/ImportButtons/ImportFlow.ts +++ b/src/UI/Popup/ImportButtons/ImportFlow.ts @@ -194,10 +194,7 @@ export default abstract class ImportFlow { return { error: t.hasBeenImported } } - const usesTestUrl = - this.state.osmConnection._oauth_config.url === - OsmConnection.oauth_configs["osm-test"].url - if (!state.layout.official && !(isTesting || usesTestUrl)) { + if (!state.layout.official && !isTesting) { // Unofficial theme - imports not allowed return { error: t.officialThemesOnly, diff --git a/src/UI/Popup/ImportButtons/PointImportButtonViz.ts b/src/UI/Popup/ImportButtons/PointImportButtonViz.ts index eeb875c08..583e5d3c3 100644 --- a/src/UI/Popup/ImportButtons/PointImportButtonViz.ts +++ b/src/UI/Popup/ImportButtons/PointImportButtonViz.ts @@ -18,6 +18,7 @@ export class PointImportButtonViz implements SpecialVisualization { public readonly docs: string | BaseUIElement public readonly example?: string public readonly args: { name: string; defaultValue?: string; doc: string }[] + public needsUrls = [] constructor() { this.funcName = "import_button" diff --git a/src/UI/Popup/ImportButtons/WayImportButtonViz.ts b/src/UI/Popup/ImportButtons/WayImportButtonViz.ts index 13e71beee..d3e8bb6d4 100644 --- a/src/UI/Popup/ImportButtons/WayImportButtonViz.ts +++ b/src/UI/Popup/ImportButtons/WayImportButtonViz.ts @@ -20,6 +20,7 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou */ export default class WayImportButtonViz implements AutoAction, SpecialVisualization { public readonly funcName: string = "import_way_button" + needsUrls = [] public readonly docs: string = "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + ImportFlowUtils.documentationGeneral diff --git a/src/UI/Popup/LanguageElement.ts b/src/UI/Popup/LanguageElement.ts index 754026228..93c03039d 100644 --- a/src/UI/Popup/LanguageElement.ts +++ b/src/UI/Popup/LanguageElement.ts @@ -20,6 +20,7 @@ import { Feature } from "geojson" export class LanguageElement implements SpecialVisualization { funcName: string = "language_chooser" + needsUrls = [] docs: string | BaseUIElement = "The language element allows to show and pick all known (modern) languages. The key can be set" diff --git a/src/UI/Popup/MapillaryLinkVis.ts b/src/UI/Popup/MapillaryLinkVis.ts index 9722680a9..b65d24b8b 100644 --- a/src/UI/Popup/MapillaryLinkVis.ts +++ b/src/UI/Popup/MapillaryLinkVis.ts @@ -9,6 +9,8 @@ import MapillaryLink from "../BigComponents/MapillaryLink.svelte" export class MapillaryLinkVis implements SpecialVisualization { funcName = "mapillary_link" docs = "Adds a button to open mapillary on the specified location" + needsUrls = [] + args = [ { name: "zoom", diff --git a/src/UI/Popup/MinimapViz.ts b/src/UI/Popup/MinimapViz.ts index 556157537..873770b4e 100644 --- a/src/UI/Popup/MinimapViz.ts +++ b/src/UI/Popup/MinimapViz.ts @@ -13,6 +13,7 @@ import { BBox } from "../../Logic/BBox" export class MinimapViz implements SpecialVisualization { funcName = "minimap" docs = "A small map showing the selected feature." + needsUrls = [] args = [ { doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close", diff --git a/src/UI/Popup/MultiApplyViz.ts b/src/UI/Popup/MultiApplyViz.ts index 876678cb8..cb2513aac 100644 --- a/src/UI/Popup/MultiApplyViz.ts +++ b/src/UI/Popup/MultiApplyViz.ts @@ -4,6 +4,7 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua export class MultiApplyViz implements SpecialVisualization { funcName = "multi_apply" + needsUrls = [] docs = "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags" args = [ diff --git a/src/UI/Popup/PlantNetDetectionViz.ts b/src/UI/Popup/PlantNetDetectionViz.ts index ed5a7acc4..101f37ec6 100644 --- a/src/UI/Popup/PlantNetDetectionViz.ts +++ b/src/UI/Popup/PlantNetDetectionViz.ts @@ -8,9 +8,10 @@ import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import SvelteUIElement from "../Base/SvelteUIElement" import PlantNet from "../PlantNet/PlantNet.svelte" - +import { default as PlantNetCode } from "../../Logic/Web/PlantNet" export class PlantNetDetectionViz implements SpecialVisualization { funcName = "plantnet_detection" + needsUrls = [PlantNetCode.baseUrl] docs = "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). " diff --git a/src/UI/Popup/QuestionViz.ts b/src/UI/Popup/QuestionViz.ts index 8d5f8f4c2..f4cc957a3 100644 --- a/src/UI/Popup/QuestionViz.ts +++ b/src/UI/Popup/QuestionViz.ts @@ -11,6 +11,8 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig" */ export default class QuestionViz implements SpecialVisualization { funcName = "questions" + needsUrls = [] + docs = "The special element which shows the questions which are unkown. Added by default if not yet there" args = [ diff --git a/src/UI/Popup/ShareLinkViz.ts b/src/UI/Popup/ShareLinkViz.ts index 40ef684fd..1233e22b4 100644 --- a/src/UI/Popup/ShareLinkViz.ts +++ b/src/UI/Popup/ShareLinkViz.ts @@ -15,6 +15,7 @@ export class ShareLinkViz implements SpecialVisualization { doc: "The url to share (default: current URL)", }, ] + needsUrls = [] public constr( state: SpecialVisualizationState, diff --git a/src/UI/Popup/TagApplyButton.ts b/src/UI/Popup/TagApplyButton.ts index 2491f217a..fa3405b98 100644 --- a/src/UI/Popup/TagApplyButton.ts +++ b/src/UI/Popup/TagApplyButton.ts @@ -21,6 +21,7 @@ import Maproulette from "../../Logic/Maproulette" export default class TagApplyButton implements AutoAction, SpecialVisualization { public readonly funcName = "tag_apply" + needsUrls = [] public readonly docs = "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + Utils.Special_visualizations_tagsToApplyHelpText diff --git a/src/UI/Popup/UploadToOsmViz.ts b/src/UI/Popup/UploadToOsmViz.ts index 427235aeb..0679885fe 100644 --- a/src/UI/Popup/UploadToOsmViz.ts +++ b/src/UI/Popup/UploadToOsmViz.ts @@ -2,6 +2,7 @@ import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { UIEventSource } from "../../Logic/UIEventSource" import { GeoOperations } from "../../Logic/GeoOperations" +import Constants from "../../Models/Constants" /** * Wrapper around 'UploadTraceToOsmUI' @@ -11,6 +12,7 @@ export class UploadToOsmViz implements SpecialVisualization { docs = "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored." args = [] + needsUrls = [Constants.osmAuthConfig.url] constr( state: SpecialVisualizationState, diff --git a/src/UI/RemoveOtherLanguages.ts b/src/UI/RemoveOtherLanguages.ts new file mode 100644 index 000000000..f321df3e5 --- /dev/null +++ b/src/UI/RemoveOtherLanguages.ts @@ -0,0 +1,32 @@ +export {} +let lang = ( + (navigator.languages && navigator.languages[0]) || + navigator.language || + navigator["userLanguage"] || + "en" +).substr(0, 2) + +function filterLangs(maindiv: HTMLElement) { + let foundLangs = 0 + for (const child of Array.from(maindiv.children)) { + if (child.attributes.getNamedItem("lang")?.value === lang) { + foundLangs++ + } + } + if (foundLangs === 0) { + lang = "en" + } + for (const child of Array.from(maindiv.children)) { + const childLang = child.attributes.getNamedItem("lang") + if (childLang === undefined) { + continue + } + if (childLang.value === lang) { + continue + } + child.parentElement.removeChild(child) + } +} + +filterLangs(document.getElementById("descriptions-while-loading")) +filterLangs(document.getElementById("default-title")) diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index a4e00100b..28125837c 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -1,118 +1,122 @@ -import { Store, UIEventSource } from "../Logic/UIEventSource"; -import BaseUIElement from "./BaseUIElement"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; -import { OsmConnection } from "../Logic/Osm/OsmConnection"; -import { Changes } from "../Logic/Osm/Changes"; -import { ExportableMap, MapProperties } from "../Models/MapProperties"; -import LayerState from "../Logic/State/LayerState"; -import { Feature, Geometry, Point } from "geojson"; -import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; -import { MangroveIdentity } from "../Logic/Web/MangroveReviews"; -import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; -import { MenuState } from "../Models/MenuState"; -import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; -import { RasterLayerPolygon } from "../Models/RasterLayers"; -import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; -import { OsmTags } from "../Models/OsmFeature"; +import { Store, UIEventSource } from "../Logic/UIEventSource" +import BaseUIElement from "./BaseUIElement" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { Changes } from "../Logic/Osm/Changes" +import { ExportableMap, MapProperties } from "../Models/MapProperties" +import LayerState from "../Logic/State/LayerState" +import { Feature, Geometry, Point } from "geojson" +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" +import { MangroveIdentity } from "../Logic/Web/MangroveReviews" +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import FeatureSwitchState from "../Logic/State/FeatureSwitchState" +import { MenuState } from "../Models/MenuState" +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" +import { RasterLayerPolygon } from "../Models/RasterLayers" +import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" +import { OsmTags } from "../Models/OsmFeature" /** * The state needed to render a special Visualisation. */ export interface SpecialVisualizationState { - readonly guistate: MenuState; - readonly layout: LayoutConfig; - readonly featureSwitches: FeatureSwitchState; + readonly guistate: MenuState + readonly layout: LayoutConfig + readonly featureSwitches: FeatureSwitchState - readonly layerState: LayerState; - readonly featureProperties: { getStore(id: string): UIEventSource>, trackFeature?(feature: { properties: OsmTags }) }; + readonly layerState: LayerState + readonly featureProperties: { + getStore(id: string): UIEventSource> + trackFeature?(feature: { properties: OsmTags }) + } - readonly indexedFeatures: IndexedFeatureSource; + readonly indexedFeatures: IndexedFeatureSource - /** - * Some features will create a new element that should be displayed. - * These can be injected by appending them to this featuresource (and pinging it) - */ - readonly newFeatures: WritableFeatureSource; + /** + * Some features will create a new element that should be displayed. + * These can be injected by appending them to this featuresource (and pinging it) + */ + readonly newFeatures: WritableFeatureSource - readonly historicalUserLocations: WritableFeatureSource>; + readonly historicalUserLocations: WritableFeatureSource> - readonly osmConnection: OsmConnection; - readonly featureSwitchUserbadge: Store; - readonly featureSwitchIsTesting: Store; - readonly changes: Changes; - readonly osmObjectDownloader: OsmObjectDownloader; - /** - * State of the main map - */ - readonly mapProperties: MapProperties & ExportableMap; + readonly osmConnection: OsmConnection + readonly featureSwitchUserbadge: Store + readonly featureSwitchIsTesting: Store + readonly changes: Changes + readonly osmObjectDownloader: OsmObjectDownloader + /** + * State of the main map + */ + readonly mapProperties: MapProperties & ExportableMap - readonly selectedElement: UIEventSource; - /** - * Works together with 'selectedElement' to indicate what properties should be displayed - */ - readonly selectedLayer: UIEventSource; - readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; + readonly selectedElement: UIEventSource + /** + * Works together with 'selectedElement' to indicate what properties should be displayed + */ + readonly selectedLayer: UIEventSource + readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> - /** - * If data is currently being fetched from external sources - */ - readonly dataIsLoading: Store; - /** - * Only needed for 'ReplaceGeometryAction' - */ - readonly fullNodeDatabase?: FullNodeDatabaseSource; + /** + * If data is currently being fetched from external sources + */ + readonly dataIsLoading: Store + /** + * Only needed for 'ReplaceGeometryAction' + */ + readonly fullNodeDatabase?: FullNodeDatabaseSource - readonly perLayer: ReadonlyMap; - readonly userRelatedState: { - readonly imageLicense: UIEventSource; - readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> - readonly mangroveIdentity: MangroveIdentity - readonly showAllQuestionsAtOnce: UIEventSource - readonly preferencesAsTags: Store> - readonly language: UIEventSource - }; - readonly lastClickObject: WritableFeatureSource; + readonly perLayer: ReadonlyMap + readonly userRelatedState: { + readonly imageLicense: UIEventSource + readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> + readonly mangroveIdentity: MangroveIdentity + readonly showAllQuestionsAtOnce: UIEventSource + readonly preferencesAsTags: Store> + readonly language: UIEventSource + } + readonly lastClickObject: WritableFeatureSource - readonly availableLayers: Store; + readonly availableLayers: Store - readonly imageUploadManager: ImageUploadManager; + readonly imageUploadManager: ImageUploadManager } export interface SpecialVisualization { - readonly funcName: string; - readonly docs: string | BaseUIElement; - readonly example?: string; + readonly funcName: string + readonly docs: string | BaseUIElement + readonly example?: string + readonly needsUrls: string[] - /** - * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included - */ - readonly needsNodeDatabase?: boolean; - readonly args: { - name: string - defaultValue?: string - doc: string - required?: false | boolean - }[]; - readonly getLayerDependencies?: (argument: string[]) => string[]; + /** + * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included + */ + readonly needsNodeDatabase?: boolean + readonly args: { + name: string + defaultValue?: string + doc: string + required?: false | boolean + }[] + readonly getLayerDependencies?: (argument: string[]) => string[] - structuredExamples?(): { feature: Feature>; args: string[] }[]; + structuredExamples?(): { feature: Feature>; args: string[] }[] - constr( - state: SpecialVisualizationState, - tagSource: UIEventSource>, - argument: string[], - feature: Feature, - layer: LayerConfig - ): BaseUIElement; + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement } export type RenderingSpecification = - | string - | { - func: SpecialVisualization - args: string[] - style: string -} + | string + | { + func: SpecialVisualization + args: string[] + style: string + } diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 23d1b57c8..216972b54 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -1,71 +1,79 @@ -import Combine from "./Base/Combine"; -import { FixedUiElement } from "./Base/FixedUiElement"; -import BaseUIElement from "./BaseUIElement"; -import Title from "./Base/Title"; -import Table from "./Base/Table"; -import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"; -import { HistogramViz } from "./Popup/HistogramViz"; -import { MinimapViz } from "./Popup/MinimapViz"; -import { ShareLinkViz } from "./Popup/ShareLinkViz"; -import { UploadToOsmViz } from "./Popup/UploadToOsmViz"; -import { MultiApplyViz } from "./Popup/MultiApplyViz"; -import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"; -import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"; -import TagApplyButton from "./Popup/TagApplyButton"; -import { CloseNoteButton } from "./Popup/CloseNoteButton"; -import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"; -import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"; -import AllTagsPanel from "./Popup/AllTagsPanel.svelte"; -import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; -import { ImageCarousel } from "./Image/ImageCarousel"; -import { VariableUiElement } from "./Base/VariableUIElement"; -import { Utils } from "../Utils"; -import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"; -import { Translation } from "./i18n/Translation"; -import Translations from "./i18n/Translations"; -import ReviewForm from "./Reviews/ReviewForm"; -import ReviewElement from "./Reviews/ReviewElement"; -import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; -import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; -import { SubtleButton } from "./Base/SubtleButton"; -import Svg from "../Svg"; -import NoteCommentElement from "./Popup/NoteCommentElement"; -import { SubstitutedTranslation } from "./SubstitutedTranslation"; -import List from "./Base/List"; -import StatisticsPanel from "./BigComponents/StatisticsPanel"; -import AutoApplyButton from "./Popup/AutoApplyButton"; -import { LanguageElement } from "./Popup/LanguageElement"; -import FeatureReviews from "../Logic/Web/MangroveReviews"; -import Maproulette from "../Logic/Maproulette"; -import SvelteUIElement from "./Base/SvelteUIElement"; -import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; -import QuestionViz from "./Popup/QuestionViz"; -import { Feature, Point } from "geojson"; -import { GeoOperations } from "../Logic/GeoOperations"; -import CreateNewNote from "./Popup/CreateNewNote.svelte"; -import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"; -import UserProfile from "./BigComponents/UserProfile.svelte"; -import LanguagePicker from "./LanguagePicker"; -import Link from "./Base/Link"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; -import { OsmTags, WayId } from "../Models/OsmFeature"; -import MoveWizard from "./Popup/MoveWizard"; -import SplitRoadWizard from "./Popup/SplitRoadWizard"; -import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"; -import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"; -import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"; -import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"; -import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"; -import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"; -import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"; -import { OpenJosm } from "./BigComponents/OpenJosm"; -import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; -import FediverseValidator from "./InputElement/Validators/FediverseValidator"; -import SendEmail from "./Popup/SendEmail.svelte"; -import NearbyImages from "./Popup/NearbyImages.svelte"; -import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"; -import UploadImage from "./Image/UploadImage.svelte"; +import Combine from "./Base/Combine" +import { FixedUiElement } from "./Base/FixedUiElement" +import BaseUIElement from "./BaseUIElement" +import Title from "./Base/Title" +import Table from "./Base/Table" +import { + RenderingSpecification, + SpecialVisualization, + SpecialVisualizationState, +} from "./SpecialVisualization" +import { HistogramViz } from "./Popup/HistogramViz" +import { MinimapViz } from "./Popup/MinimapViz" +import { ShareLinkViz } from "./Popup/ShareLinkViz" +import { UploadToOsmViz } from "./Popup/UploadToOsmViz" +import { MultiApplyViz } from "./Popup/MultiApplyViz" +import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz" +import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" +import TagApplyButton from "./Popup/TagApplyButton" +import { CloseNoteButton } from "./Popup/CloseNoteButton" +import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" +import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" +import AllTagsPanel from "./Popup/AllTagsPanel.svelte" +import AllImageProviders from "../Logic/ImageProviders/AllImageProviders" +import { ImageCarousel } from "./Image/ImageCarousel" +import { VariableUiElement } from "./Base/VariableUIElement" +import { Utils } from "../Utils" +import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" +import { Translation } from "./i18n/Translation" +import Translations from "./i18n/Translations" +import ReviewForm from "./Reviews/ReviewForm" +import ReviewElement from "./Reviews/ReviewElement" +import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" +import { SubtleButton } from "./Base/SubtleButton" +import Svg from "../Svg" +import NoteCommentElement from "./Popup/NoteCommentElement" +import { SubstitutedTranslation } from "./SubstitutedTranslation" +import List from "./Base/List" +import StatisticsPanel from "./BigComponents/StatisticsPanel" +import AutoApplyButton from "./Popup/AutoApplyButton" +import { LanguageElement } from "./Popup/LanguageElement" +import FeatureReviews from "../Logic/Web/MangroveReviews" +import Maproulette from "../Logic/Maproulette" +import SvelteUIElement from "./Base/SvelteUIElement" +import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" +import QuestionViz from "./Popup/QuestionViz" +import { Feature, Point } from "geojson" +import { GeoOperations } from "../Logic/GeoOperations" +import CreateNewNote from "./Popup/CreateNewNote.svelte" +import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" +import UserProfile from "./BigComponents/UserProfile.svelte" +import LanguagePicker from "./LanguagePicker" +import Link from "./Base/Link" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" +import { OsmTags, WayId } from "../Models/OsmFeature" +import MoveWizard from "./Popup/MoveWizard" +import SplitRoadWizard from "./Popup/SplitRoadWizard" +import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" +import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" +import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" +import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz" +import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz" +import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz" +import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte" +import { OpenJosm } from "./BigComponents/OpenJosm" +import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" +import FediverseValidator from "./InputElement/Validators/FediverseValidator" +import SendEmail from "./Popup/SendEmail.svelte" +import NearbyImages from "./Popup/NearbyImages.svelte" +import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" +import UploadImage from "./Image/UploadImage.svelte" +import { Imgur } from "../Logic/ImageProviders/Imgur" +import Constants from "../Models/Constants" +import { MangroveReviews } from "mangrove-reviews-typescript" +import Wikipedia from "../Logic/Web/Wikipedia" +import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -79,7 +87,7 @@ class NearbyImageVis implements SpecialVisualization { docs = "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" funcName = "nearby_images" - + needsUrls = NearbyImagesSearch.apiUrls constr( state: SpecialVisualizationState, tags: UIEventSource>, @@ -117,6 +125,7 @@ class StealViz implements SpecialVisualization { required: true, }, ] + needsUrls = [] constr(state: SpecialVisualizationState, featureTags, args) { const [featureIdKey, layerAndtagRenderingIds] = args @@ -264,7 +273,6 @@ export default class SpecialVisualizations { SpecialVisualizations.specialVisualizations .map((sp) => sp.funcName + "()") .join(", ") - } } @@ -378,6 +386,7 @@ export default class SpecialVisualizations { funcName: "add_new_point", docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`", args: [], + needsUrls: [], constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement { let [lon, lat] = GeoOperations.centerpointCoordinates(feature) return new SvelteUIElement(AddNewPoint, { @@ -389,6 +398,7 @@ export default class SpecialVisualizations { { funcName: "user_profile", args: [], + needsUrls: [], docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'", constr(state: SpecialVisualizationState): BaseUIElement { return new SvelteUIElement(UserProfile, { @@ -399,6 +409,7 @@ export default class SpecialVisualizations { { funcName: "language_picker", args: [], + needsUrls: [], docs: "A component to set the language of the user interface", constr(state: SpecialVisualizationState): BaseUIElement { return new LanguagePicker( @@ -410,6 +421,7 @@ export default class SpecialVisualizations { { funcName: "logout", args: [], + needsUrls: [Constants.osmAuthConfig.url], docs: "Shows a button where the user can log out", constr(state: SpecialVisualizationState): BaseUIElement { return new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, { @@ -426,6 +438,7 @@ export default class SpecialVisualizations { funcName: "split_button", docs: "Adds a button which allows to split a way", args: [], + needsUrls: [], constr( state: SpecialVisualizationState, tagSource: UIEventSource> @@ -446,6 +459,7 @@ export default class SpecialVisualizations { funcName: "move_button", docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config", args: [], + needsUrls: [], constr( state: SpecialVisualizationState, tagSource: UIEventSource>, @@ -469,6 +483,7 @@ export default class SpecialVisualizations { funcName: "delete_button", docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config", args: [], + needsUrls: [], constr( state: SpecialVisualizationState, tagSource: UIEventSource>, @@ -493,6 +508,7 @@ export default class SpecialVisualizations { { funcName: "open_note", args: [], + needsUrls: [Constants.osmAuthConfig.url], docs: "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled", constr( state: SpecialVisualizationState, @@ -525,6 +541,7 @@ export default class SpecialVisualizations { defaultValue: "wikidata;wikipedia", }, ], + needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls], example: "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", constr: (_, tagsSource, args) => { @@ -548,6 +565,7 @@ export default class SpecialVisualizations { defaultValue: "wikidata", }, ], + needsUrls: Wikidata.neededUrls, example: "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", constr: (_, tagsSource, args) => @@ -577,6 +595,7 @@ export default class SpecialVisualizations { funcName: "all_tags", docs: "Prints all key-value pairs of the object - used for debugging", args: [], + needsUrls: [], constr: (state, tags: UIEventSource) => new SvelteUIElement(AllTagsPanel, { tags, state }), }, @@ -590,6 +609,7 @@ export default class SpecialVisualizations { doc: "The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Multiple values are allowed if ';'-separated ", }, ], + needsUrls: AllImageProviders.apiUrls, constr: (state, tags, args) => { let imagePrefixes: string[] = undefined if (args.length > 0) { @@ -605,27 +625,32 @@ export default class SpecialVisualizations { { funcName: "image_upload", docs: "Creates a button where a user can upload an image to IMGUR", + needsUrls: [Imgur.apiUrl], args: [ { name: "image-key", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", - required: false + required: false, }, { name: "label", doc: "The text to show on the button", - required: false + required: false, }, ], constr: (state, tags, args) => { return new SvelteUIElement(UploadImage, { - state,tags, labelText: args[1], image: args[0] + state, + tags, + labelText: args[1], + image: args[0], }) }, }, { funcName: "reviews", docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", + needsUrls: [MangroveReviews.ORIGINAL_API], example: "`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used", args: [ @@ -676,6 +701,7 @@ export default class SpecialVisualizations { doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", }, ], + needsUrls: [], example: "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", constr: (state, tagSource: UIEventSource, args) => { @@ -688,38 +714,9 @@ export default class SpecialVisualizations { ) }, }, - { - funcName: "live", - docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}", - example: - "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}", - args: [ - { - name: "Url", - doc: "The URL to load", - required: true, - }, - { - name: "Shorthands", - doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;", - }, - { - name: "path", - doc: "The path (or shorthand) that should be returned", - }, - ], - constr: (_, tagSource: UIEventSource, args) => { - const url = args[0] - const shorthands = args[1] - const neededValue = args[2] - const source = LiveQueryHandler.FetchLiveData(url, shorthands.split(";")) - return new VariableUiElement( - source.map((data) => data[neededValue] ?? "Loading...") - ) - }, - }, { funcName: "canonical", + needsUrls: [], docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ", example: "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...", @@ -757,6 +754,7 @@ export default class SpecialVisualizations { funcName: "export_as_geojson", docs: "Exports the selected feature as GeoJson-file", args: [], + needsUrls: [], constr: (state, tagSource, tagsSource, feature, layer) => { const t = Translations.t.general.download @@ -786,6 +784,7 @@ export default class SpecialVisualizations { funcName: "open_in_iD", docs: "Opens the current view in the iD-editor", args: [], + needsUrls: [], constr: (state, feature) => { return new SvelteUIElement(OpenIdEditor, { mapProperties: state.mapProperties, @@ -797,6 +796,8 @@ export default class SpecialVisualizations { funcName: "open_in_josm", docs: "Opens the current view in the JOSM-editor", args: [], + needsUrls: OpenJosm.needsUrls, + constr: (state) => { return new OpenJosm(state.osmConnection, state.mapProperties.bounds) }, @@ -805,6 +806,7 @@ export default class SpecialVisualizations { funcName: "clear_location_history", docs: "A button to remove the travelled track information from the device", args: [], + needsUrls: [], constr: (state) => { return new SubtleButton( Svg.delete_icon_svg().SetStyle("height: 1.5rem"), @@ -830,6 +832,7 @@ export default class SpecialVisualizations { defaultValue: "0", }, ], + needsUrls: [Constants.osmAuthConfig.url], constr: (state, tags, args) => new VariableUiElement( tags @@ -858,16 +861,18 @@ export default class SpecialVisualizations { defaultValue: "id", }, ], + needsUrls: [Imgur.apiUrl], constr: (state, tags, args) => { const id = tags.data[args[0] ?? "id"] tags = state.featureProperties.getStore(id) console.log("Id is", id) - return new SvelteUIElement(UploadImage, {state, tags}) - } + return new SvelteUIElement(UploadImage, { state, tags }) + }, }, { funcName: "title", args: [], + needsUrls: [], docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", example: "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", @@ -888,6 +893,7 @@ export default class SpecialVisualizations { { funcName: "maproulette_task", args: [], + needsUrls: [Maproulette.defaultEndpoint], constr(state, tagSource) { let parentId = tagSource.data.mr_challengeId if (parentId === undefined) { @@ -931,6 +937,7 @@ export default class SpecialVisualizations { { funcName: "maproulette_set_status", docs: "Change the status of the given MapRoulette task", + needsUrls: [Maproulette.defaultEndpoint], example: " The following example sets the status to '2' (false positive)\n" + "\n" + @@ -1054,6 +1061,7 @@ export default class SpecialVisualizations { funcName: "statistics", docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", args: [], + needsUrls: [], constr: (state) => { return new Combine( state.layout.layers @@ -1096,6 +1104,8 @@ export default class SpecialVisualizations { required: true, }, ], + needsUrls: [], + constr(__, tags, args) { return new SvelteUIElement(SendEmail, { args, tags }) }, @@ -1123,6 +1133,7 @@ export default class SpecialVisualizations { doc: "If set, this link will act as a download-button. The contents of `href` will be offered for download; this parameter will act as the proposed filename", }, ], + needsUrls: [], constr( state: SpecialVisualizationState, tagSource: UIEventSource>, @@ -1144,6 +1155,7 @@ export default class SpecialVisualizations { { funcName: "multi", docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", + needsUrls: [], example: "```json\n" + JSON.stringify( @@ -1204,6 +1216,7 @@ export default class SpecialVisualizations { required: true, }, ], + needsUrls: [], constr( state: SpecialVisualizationState, tagSource: UIEventSource>, diff --git a/src/UI/SubstitutedTranslation.ts b/src/UI/SubstitutedTranslation.ts index a6721532a..3aacaecb4 100644 --- a/src/UI/SubstitutedTranslation.ts +++ b/src/UI/SubstitutedTranslation.ts @@ -37,6 +37,7 @@ export class SubstitutedTranslation extends VariableUiElement { constr: typeof value === "function" ? value : () => value, docs: "Dynamically injected input element", args: [], + needsUrls: [], example: "", }) }) diff --git a/src/assets/editor-layer-index.json b/src/assets/editor-layer-index.json index 6b1d00bf7..f5f8b4ca4 100644 --- a/src/assets/editor-layer-index.json +++ b/src/assets/editor-layer-index.json @@ -159,7 +159,7 @@ {"properties":{"name":"Frankfurt am Main Luftbild 2017","id":"Frankfurt-am-Main-2017","url":"https://geowebdienste.frankfurt.de/OD_Luftbilder_2017?REQUEST=GetMap&VERSION=1.3.0&SERVICE=WMS&CRS={proj}&FORMAT=image/jpeg&STYLES=&bbox={bbox}&WIDTH={width}&HEIGHT={height}&LAYERS=opendata_luftbilder_2017","attribution":{"required":true,"text":"Stadtvermessungsam Frankfurt am Main","url":"https://offenedaten.frankfurt.de/dataset/wms-luftbild-2017"},"type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[8.8046,50.0111],[8.8046,50.228],[8.46726,50.228],[8.46726,50.0111],[8.8046,50.0111]]],"type":"Polygon"}}, {"properties":{"name":"Frankfurt am Main Luftbild 2018","id":"Frankfurt-am-Main-2018","url":"https://geowebdienste.frankfurt.de/OD_Luftbilder_2018?REQUEST=GetMap&VERSION=1.3.0&SERVICE=WMS&CRS={proj}&FORMAT=image/jpeg&STYLES=&bbox={bbox}&WIDTH={width}&HEIGHT={height}&LAYERS=opendata_luftbilder_2018","attribution":{"required":true,"text":"Stadtvermessungsamt Frankfurt am Main","url":"https://offenedaten.frankfurt.de/dataset/wms-luftbild-2018"},"type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[8.8046,50.0111],[8.8046,50.228],[8.46726,50.228],[8.46726,50.0111],[8.8046,50.0111]]],"type":"Polygon"}}, {"properties":{"name":"Frankfurt am Main Luftbild 2019","id":"Frankfurt-am-Main-2019","url":"https://geowebdienste.frankfurt.de/OD_Luftbilder_2019?REQUEST=GetMap&VERSION=1.3.0&SERVICE=WMS&CRS={proj}&FORMAT=image/jpeg&STYLES=&bbox={bbox}&WIDTH={width}&HEIGHT={height}&LAYERS=opendata_luftbilder_2019","attribution":{"required":true,"text":"Stadtvermessungsamt Frankfurt am Main","url":"https://www.offenedaten.frankfurt.de/dataset/wms-luftbilder-2019-frankfurt-am-main"},"type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[8.84647,50.0111],[8.84647,50.22807],[8.46726,50.22807],[8.46726,50.0111],[8.84647,50.0111]]],"type":"Polygon"}}, -{"properties":{"name":"Hamburg 20cm (HH LGV DOP20 2021)","id":"hamburg-20cm","url":"https://geodienste.hamburg.de/HH_WMS_DOP?LAYERS=DOP&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Freie und Hansestadt Hamburg, Landesbetrieb Geoinformation und Vermessung","url":"https://www.hamburg.de/bsw/landesbetrieb-geoinformation-und-vermessung"},"type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[[9.76314,53.55521],[9.77434,53.55433],[9.77232,53.54352],[9.77352,53.52796],[9.78105,53.51838],[9.77107,53.52185],[9.76885,53.5053],[9.78203,53.49236],[9.8028,53.49383],[9.80021,53.47372],[9.80663,53.46648],[9.84872,53.44111],[9.86211,53.42942],[9.86885,53.44462],[9.89493,53.45583],[9.90436,53.45707],[9.91704,53.44664],[9.92305,53.43631],[9.90667,53.41596],[9.92552,53.41924],[9.92953,53.42007],[9.9581,53.42708],[9.97873,53.4142],[9.98243,53.41478],[9.99754,53.42546],[10.02294,53.43228],[10.01449,53.44203],[10.03517,53.4469],[10.05155,53.46394],[10.07581,53.45436],[10.1068,53.42658],[10.10949,53.42649],[10.14506,53.41614],[10.16555,53.39933],[10.24155,53.39797],[10.24578,53.40261],[10.25089,53.41024],[10.25598,53.41623],[10.30799,53.43332],[10.32514,53.44979],[10.31223,53.45229],[10.30962,53.44309],[10.29043,53.45512],[10.26592,53.47079],[10.25008,53.47898],[10.2367,53.49629],[10.21828,53.49923],[10.21043,53.51996],[10.18951,53.51148],[10.16919,53.51965],[10.16611,53.52013],[10.16327,53.52185],[10.16874,53.5374],[10.15465,53.53657],[10.15189,53.5417],[10.15942,53.56091],[10.15308,53.56242],[10.148,53.5639],[10.15067,53.56973],[10.15169,53.57619],[10.20117,53.58392],[10.19236,53.59474],[10.18887,53.61316],[10.22202,53.63349],[10.18973,53.63838],[10.19885,53.64675],[10.17153,53.66869],[10.14955,53.67545],[10.14643,53.67588],[10.14473,53.67613],[10.14176,53.67744],[10.14342,53.68057],[10.15829,53.68944],[10.15694,53.70451],[10.1779,53.70992],[10.19369,53.731],[10.16939,53.73896],[10.11908,53.71324],[10.08198,53.72044],[10.0707,53.70996],[10.071,53.69585],[10.0604,53.68833],[10.06925,53.67955],[10.05148,53.67759],[10.04338,53.68198],[10.02282,53.68157],[9.9996,53.68153],[9.98739,53.65072],[9.98492,53.6483],[9.97795,53.64887],[9.95155,53.65065],[9.95024,53.65085],[9.94552,53.65276],[9.93115,53.65262],[9.90678,53.65231],[9.89688,53.63492],[9.89637,53.63122],[9.89356,53.63026],[9.88697,53.6252],[9.88505,53.62199],[9.86931,53.61323],[9.86814,53.6093],[9.85416,53.59805],[9.84498,53.59498],[9.83773,53.59198],[9.81817,53.58591],[9.78993,53.60386],[9.79634,53.6103],[9.7707,53.61607],[9.77129,53.63131],[9.75793,53.61828],[9.73465,53.56536],[9.73047,53.55787],[9.76314,53.55521]]],[[[8.5275,53.90941],[8.52792,53.93577],[8.4826,53.9356],[8.48274,53.90924],[8.5275,53.90941]]]],"type":"MultiPolygon"}}, +{"properties":{"name":"Hamburg 20cm (HH LGV DOP20 2022)","id":"hamburg-20cm","url":"https://geodienste.hamburg.de/HH_WMS_DOP?LAYERS=DOP&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Freie und Hansestadt Hamburg, Landesbetrieb Geoinformation und Vermessung","url":"https://www.hamburg.de/bsw/landesbetrieb-geoinformation-und-vermessung"},"type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[[9.76314,53.55521],[9.77434,53.55433],[9.77232,53.54352],[9.77352,53.52796],[9.78105,53.51838],[9.77107,53.52185],[9.76885,53.5053],[9.78203,53.49236],[9.8028,53.49383],[9.80021,53.47372],[9.80663,53.46648],[9.84872,53.44111],[9.86211,53.42942],[9.86885,53.44462],[9.89493,53.45583],[9.90436,53.45707],[9.91704,53.44664],[9.92305,53.43631],[9.90667,53.41596],[9.92552,53.41924],[9.92953,53.42007],[9.9581,53.42708],[9.97873,53.4142],[9.98243,53.41478],[9.99754,53.42546],[10.02294,53.43228],[10.01449,53.44203],[10.03517,53.4469],[10.05155,53.46394],[10.07581,53.45436],[10.1068,53.42658],[10.10949,53.42649],[10.14506,53.41614],[10.16555,53.39933],[10.24155,53.39797],[10.24578,53.40261],[10.25089,53.41024],[10.25598,53.41623],[10.30799,53.43332],[10.32514,53.44979],[10.31223,53.45229],[10.30962,53.44309],[10.29043,53.45512],[10.26592,53.47079],[10.25008,53.47898],[10.2367,53.49629],[10.21828,53.49923],[10.21043,53.51996],[10.18951,53.51148],[10.16919,53.51965],[10.16611,53.52013],[10.16327,53.52185],[10.16874,53.5374],[10.15465,53.53657],[10.15189,53.5417],[10.15942,53.56091],[10.15308,53.56242],[10.148,53.5639],[10.15067,53.56973],[10.15169,53.57619],[10.20117,53.58392],[10.19236,53.59474],[10.18887,53.61316],[10.22202,53.63349],[10.18973,53.63838],[10.19885,53.64675],[10.17153,53.66869],[10.14955,53.67545],[10.14643,53.67588],[10.14473,53.67613],[10.14176,53.67744],[10.14342,53.68057],[10.15829,53.68944],[10.15694,53.70451],[10.1779,53.70992],[10.19369,53.731],[10.16939,53.73896],[10.11908,53.71324],[10.08198,53.72044],[10.0707,53.70996],[10.071,53.69585],[10.0604,53.68833],[10.06925,53.67955],[10.05148,53.67759],[10.04338,53.68198],[10.02282,53.68157],[9.9996,53.68153],[9.98739,53.65072],[9.98492,53.6483],[9.97795,53.64887],[9.95155,53.65065],[9.95024,53.65085],[9.94552,53.65276],[9.93115,53.65262],[9.90678,53.65231],[9.89688,53.63492],[9.89637,53.63122],[9.89356,53.63026],[9.88697,53.6252],[9.88505,53.62199],[9.86931,53.61323],[9.86814,53.6093],[9.85416,53.59805],[9.84498,53.59498],[9.83773,53.59198],[9.81817,53.58591],[9.78993,53.60386],[9.79634,53.6103],[9.7707,53.61607],[9.77129,53.63131],[9.75793,53.61828],[9.73465,53.56536],[9.73047,53.55787],[9.76314,53.55521]]],[[[8.5275,53.90941],[8.52792,53.93577],[8.4826,53.9356],[8.48274,53.90924],[8.5275,53.90941]]]],"type":"MultiPolygon"}}, {"properties":{"name":"Hamburg DK5 (HH LGV DK5 2021)","id":"Hamburg-DK5","url":"https://geodienste.hamburg.de/HH_WMS_DK5?LAYERS=DK5&STYLES=&CRS={proj}&BBOX={bbox}&FORMAT=image/png&WIDTH={width}&HEIGHT={height}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Freie und Hansestadt Hamburg, Landesbetrieb Geoinformation und Vermessung","url":"https://www.hamburg.de/bsw/landesbetrieb-geoinformation-und-vermessung"},"type":"wms","category":"map"},"type":"Feature","geometry":{"coordinates":[[[[9.76314,53.55521],[9.77434,53.55433],[9.77232,53.54352],[9.77352,53.52796],[9.78105,53.51838],[9.77107,53.52185],[9.76885,53.5053],[9.78203,53.49236],[9.8028,53.49383],[9.80021,53.47372],[9.80663,53.46648],[9.84872,53.44111],[9.86211,53.42942],[9.86885,53.44462],[9.89493,53.45583],[9.90436,53.45707],[9.91704,53.44664],[9.92305,53.43631],[9.90667,53.41596],[9.92552,53.41924],[9.92953,53.42007],[9.9581,53.42708],[9.97873,53.4142],[9.98243,53.41478],[9.99754,53.42546],[10.02294,53.43228],[10.01449,53.44203],[10.03517,53.4469],[10.05155,53.46394],[10.07581,53.45436],[10.1068,53.42658],[10.10949,53.42649],[10.14506,53.41614],[10.16555,53.39933],[10.24155,53.39797],[10.24578,53.40261],[10.25089,53.41024],[10.25598,53.41623],[10.30799,53.43332],[10.32514,53.44979],[10.31223,53.45229],[10.30962,53.44309],[10.29043,53.45512],[10.26592,53.47079],[10.25008,53.47898],[10.2367,53.49629],[10.21828,53.49923],[10.21043,53.51996],[10.18951,53.51148],[10.16919,53.51965],[10.16611,53.52013],[10.16327,53.52185],[10.16874,53.5374],[10.15465,53.53657],[10.15189,53.5417],[10.15942,53.56091],[10.15308,53.56242],[10.148,53.5639],[10.15067,53.56973],[10.15169,53.57619],[10.20117,53.58392],[10.19236,53.59474],[10.18887,53.61316],[10.22202,53.63349],[10.18973,53.63838],[10.19885,53.64675],[10.17153,53.66869],[10.14955,53.67545],[10.14643,53.67588],[10.14473,53.67613],[10.14176,53.67744],[10.14342,53.68057],[10.15829,53.68944],[10.15694,53.70451],[10.1779,53.70992],[10.19369,53.731],[10.16939,53.73896],[10.11908,53.71324],[10.08198,53.72044],[10.0707,53.70996],[10.071,53.69585],[10.0604,53.68833],[10.06925,53.67955],[10.05148,53.67759],[10.04338,53.68198],[10.02282,53.68157],[9.9996,53.68153],[9.98739,53.65072],[9.98492,53.6483],[9.97795,53.64887],[9.95155,53.65065],[9.95024,53.65085],[9.94552,53.65276],[9.93115,53.65262],[9.90678,53.65231],[9.89688,53.63492],[9.89637,53.63122],[9.89356,53.63026],[9.88697,53.6252],[9.88505,53.62199],[9.86931,53.61323],[9.86814,53.6093],[9.85416,53.59805],[9.84498,53.59498],[9.83773,53.59198],[9.81817,53.58591],[9.78993,53.60386],[9.79634,53.6103],[9.7707,53.61607],[9.77129,53.63131],[9.75793,53.61828],[9.73465,53.56536],[9.73047,53.55787],[9.76314,53.55521]]],[[[8.5275,53.90941],[8.52792,53.93577],[8.4826,53.9356],[8.48274,53.90924],[8.5275,53.90941]]]],"type":"MultiPolygon"}}, {"properties":{"name":"Hesse DOP20","id":"Hessen-DOP20","url":"https://www.gds-srv.hessen.de/cgi-bin/lika-services/ogc-free-images.ows?LAYERS=he_dop20_rgb&STYLES=default&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Geobasisdaten © Hessische Verwaltung für Bodenmanagement und Geoinformation: Digitale Orthophotos","url":"https://hvbg.hessen.de"},"type":"wms","category":"photo","min_zoom":16,"max_zoom":19,"best":true},"type":"Feature","geometry":{"coordinates":[[[9.04158,49.49511],[9.06561,49.52721],[9.10595,49.5076],[9.1341,49.51406],[9.10847,49.55019],[9.10767,49.58467],[9.0699,49.61838],[9.10526,49.64118],[9.11041,49.66074],[9.09359,49.69184],[9.13101,49.69695],[9.15642,49.74379],[9.13204,49.79711],[9.08973,49.80149],[9.09067,49.8485],[9.05617,49.85514],[9.03935,49.89309],[9.03265,49.92487],[9.02269,49.98269],[9.05428,49.98986],[9.03308,50.04016],[9.02484,50.04523],[9.00604,50.04722],[8.98261,50.04402],[8.98827,50.06067],[9.00132,50.06937],[9.00578,50.09856],[9.02312,50.11117],[9.0778,50.11463],[9.11522,50.12498],[9.14749,50.10913],[9.14011,50.0946],[9.15316,50.08568],[9.21598,50.14578],[9.37992,50.11552],[9.40361,50.07918],[9.51897,50.08975],[9.53476,50.15887],[9.50798,50.22788],[9.59063,50.21334],[9.65458,50.22481],[9.6738,50.23996],[9.64943,50.25928],[9.71432,50.28133],[9.75054,50.30842],[9.74796,50.36101],[9.76925,50.41989],[9.86506,50.39054],[9.96288,50.41967],[10.04494,50.48351],[10.0458,50.51648],[10.06554,50.55642],[10.04734,50.61375],[10.09369,50.61985],[10.06004,50.67688],[9.9955,50.67753],[9.9519,50.66927],[9.94125,50.66383],[9.96082,50.64097],[9.9416,50.6303],[9.91276,50.64075],[9.88873,50.64271],[9.88564,50.67057],[9.92615,50.69407],[9.91722,50.70907],[9.93851,50.72276],[9.94572,50.75058],[9.9531,50.77197],[9.95619,50.78239],[9.96714,50.82389],[10.0288,50.82589],[10.06536,50.88874],[10.02485,50.91829],[9.98863,50.92457],[10.00151,50.93355],[10.05266,50.93636],[10.06983,50.94383],[10.04373,50.96891],[10.02399,50.98091],[10.04442,51.01116],[10.1433,50.99237],[10.20081,50.99766],[10.2202,51.02714],[10.15755,51.06923],[10.14999,51.08649],[10.2111,51.11613],[10.21505,51.16363],[10.24595,51.18462],[10.18587,51.20817],[10.14107,51.22151],[10.07378,51.23032],[10.08236,51.24354],[10.06176,51.27921],[10.00374,51.28941],[9.95567,51.3083],[9.94898,51.32948],[9.93318,51.35045],[9.9337,51.37778],[9.93387,51.39235],[9.90624,51.42201],[9.79414,51.40981],[9.78744,51.39305],[9.692,51.36749],[9.73509,51.3186],[9.73148,51.29756],[9.67209,51.31946],[9.58214,51.34359],[9.56806,51.3438],[9.5545,51.35892],[9.56944,51.36085],[9.58059,51.37242],[9.56102,51.37741],[9.58162,51.39797],[9.59338,51.3969],[9.63269,51.40295],[9.64514,51.41725],[9.63183,51.45786],[9.64926,51.4678],[9.58969,51.51911],[9.61561,51.51985],[9.62917,51.54666],[9.65046,51.54922],[9.68702,51.56555],[9.69595,51.57792],[9.64102,51.6178],[9.63261,51.63848],[9.61326,51.63526],[9.56755,51.62697],[9.55484,51.64039],[9.54111,51.64157],[9.52429,51.62803],[9.50523,51.62899],[9.49932,51.65916],[9.4388,51.65169],[9.42404,51.63144],[9.36996,51.62803],[9.33134,51.61471],[9.363,51.58944],[9.31383,51.55242],[9.30406,51.51885],[9.27864,51.51526],[9.22045,51.49229],[9.20843,51.46417],[9.17959,51.46941],[9.14852,51.44331],[9.1408,51.45187],[9.09205,51.44737],[9.09428,51.49592],[9.07883,51.50554],[9.03471,51.50778],[9.02063,51.52092],[8.89441,51.48841],[8.88794,51.48149],[8.89333,51.4683],[8.90854,51.46128],[8.91695,51.42876],[8.94249,51.4274],[8.93532,51.39353],[8.88811,51.39492],[8.85498,51.37917],[8.83644,51.39096],[8.79009,51.3931],[8.73516,51.37489],[8.69997,51.3795],[8.67422,51.3721],[8.60899,51.33211],[8.552,51.27846],[8.58976,51.24655],[8.61191,51.24451],[8.6404,51.26063],[8.71765,51.27094],[8.74924,51.17891],[8.69053,51.1366],[8.69018,51.11161],[8.65101,51.09641],[8.61362,51.10363],[8.54238,51.1087],[8.49775,51.08067],[8.49964,51.07387],[8.52144,51.06395],[8.51269,51.05273],[8.50016,51.04075],[8.52728,51.01689],[8.51149,51.00997],[8.45604,50.96675],[8.4478,50.94069],[8.4514,50.9184],[8.43321,50.91959],[8.38652,50.89264],[8.35939,50.86729],[8.30738,50.86513],[8.29382,50.88441],[8.26996,50.88441],[8.1225,50.78814],[8.13563,50.76187],[8.16207,50.73656],[8.13864,50.6957],[8.13023,50.69787],[8.10843,50.67569],[8.10774,50.65316],[8.13452,50.63074],[8.1225,50.60721],[8.151,50.5973],[8.14739,50.58847],[8.10224,50.53591],[8.0377,50.56143],[7.98088,50.5119],[7.97539,50.48176],[8.00646,50.45477],[7.97419,50.43805],[7.95942,50.40928],[7.99084,50.3967],[8.01439,50.39177],[8.01092,50.3816],[8.06053,50.36704],[8.06929,50.33012],[8.10207,50.32135],[8.10053,50.30831],[8.11684,50.27947],[8.1031,50.26466],[8.06122,50.27793],[8.03272,50.27124],[8.01865,50.25741],[8.04989,50.23304],[8.0286,50.22019],[7.99187,50.2415],[7.90381,50.2024],[7.88168,50.18169],[7.88218,50.16656],[7.92355,50.14171],[7.88664,50.11827],[7.86106,50.13147],[7.84469,50.12599],[7.82021,50.08479],[7.80287,50.08645],[7.76802,50.06579],[7.77154,50.05113],[7.85231,50.00548],[7.86209,49.97794],[7.87805,49.97005],[7.89359,49.96856],[7.96363,49.96972],[7.99393,49.97872],[8.04963,50.00178],[8.08285,50.00493],[8.13151,50.0153],[8.15615,50.02528],[8.18722,50.0325],[8.23846,50.02324],[8.26798,50.00917],[8.28215,49.99422],[8.3236,49.96806],[8.34652,49.91509],[8.33605,49.887],[8.3418,49.87168],[8.35218,49.86322],[8.37853,49.85608],[8.37313,49.82392],[8.41364,49.76996],[8.42866,49.76364],[8.47447,49.75984],[8.45467,49.74623],[8.43424,49.7247],[8.37776,49.70961],[8.35304,49.69595],[8.35115,49.6799],[8.36274,49.66002],[8.37467,49.62967],[8.38568,49.61717],[8.40617,49.6037],[8.41776,49.58312],[8.47097,49.58223],[8.48162,49.57354],[8.52333,49.54816],[8.53071,49.53602],[8.54101,49.52599],[8.57534,49.51496],[8.60882,49.52777],[8.62341,49.54181],[8.60539,49.61093],[8.66856,49.62027],[8.68401,49.55985],[8.70152,49.53412],[8.72692,49.51674],[8.81275,49.5057],[8.83352,49.48853],[8.82511,49.47136],[8.79919,49.41075],[8.80297,49.40103],[8.81876,49.39388],[8.85395,49.39321],[8.95351,49.45462],[8.96021,49.49968],[9.04158,49.49511]]],"type":"Polygon"}}, {"properties":{"name":"Mainz all aerial imageries","id":"mainzallaerialimageries","url":"https://gint.mainz.de/gint1-cgi/mapserv?map=/data/mapbender-int/umn-www/client/a62/luftbild.map","attribution":{"required":true,"text":"Vermessung und Geoinformation Mainz","url":"https://www.mainz.de/vv/oe/100140100000035141.php#tab-infos"},"type":"wms_endpoint","category":"photo","min_zoom":17},"type":"Feature","geometry":{"coordinates":[[[8.10355,49.865],[8.38356,49.865],[8.38356,50.0466],[8.10355,50.0466],[8.10355,49.865]]],"type":"Polygon"}}, @@ -169,7 +169,9 @@ {"properties":{"name":"Munich latest aerial imagery 60cm","id":"MunichLatestAerialImagery","url":"https://ogc.muenchen.de/wms/opendata_luftbild?LAYERS=bgl0&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Datenquelle: dl-de/by-2-0: Landeshauptstadt München – Kommunalreferat – GeodatenService – www.geodatenservice-muenchen.de","url":"https://www.muenchen.de/rathaus/Stadtverwaltung/Kommunalreferat/geodatenservice.html"},"type":"wms","category":"historicphoto","min_zoom":11},"type":"Feature","geometry":{"coordinates":[[[11.48878,48.053],[11.55589,48.05323],[11.55583,48.06224],[11.56915,48.06229],[11.56908,48.07128],[11.64986,48.07155],[11.64993,48.06256],[11.70326,48.06274],[11.70313,48.08074],[11.71673,48.08079],[11.71667,48.08968],[11.7299,48.08972],[11.72963,48.12566],[11.74313,48.1257],[11.74292,48.15276],[11.72943,48.15271],[11.72936,48.16152],[11.71612,48.16147],[11.71592,48.18859],[11.7027,48.18855],[11.70263,48.19752],[11.67558,48.19743],[11.67537,48.22446],[11.66176,48.22441],[11.66169,48.23355],[11.64863,48.2335],[11.64857,48.24246],[11.54064,48.2421],[11.54058,48.25093],[11.52735,48.25088],[11.52728,48.26001],[11.47335,48.25983],[11.47356,48.23291],[11.46014,48.23287],[11.46021,48.22373],[11.43336,48.22364],[11.43343,48.21439],[11.3798,48.21421],[11.37987,48.20518],[11.36607,48.20514],[11.36621,48.18741],[11.35259,48.18737],[11.35266,48.17817],[11.33946,48.17813],[11.33973,48.14216],[11.36684,48.14225],[11.36697,48.12443],[11.38083,48.12448],[11.3809,48.11558],[11.44769,48.1158],[11.44804,48.07087],[11.46186,48.07091],[11.46193,48.06193],[11.48872,48.06202],[11.48878,48.053]]],"type":"Polygon"}}, {"properties":{"name":"NRW Liegenschaftskataster","id":"nrw_alkis_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_alkis?LAYERS=adv_alkis_tatsaechliche_nutzung,adv_alkis_gewaesser,adv_alkis_vegetation,adv_alkis_verkehr,adv_alkis_siedlung,adv_alkis_gesetzl_festlegungen,adv_alkis_bodensch,adv_alkis_oeff_rechtl_sonst_festl,adv_alkis_weiteres,adv_alkis_bauw_einricht,adv_alkis_gebaeude,adv_alkis_flurstuecke&FORMAT=image/png&STYLES=&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"map","best":true},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, {"properties":{"name":"NRW DTM Hillshade","id":"nrw_dtm_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_dgm-schummerung?LAYERS=nw_dgm-schummerung_pan&STYLES=default&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"elevation"},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, -{"properties":{"name":"NRW Orthophoto","id":"nrw_ortho_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_dop?LAYERS=nw_dop_rgb&STYLES=default&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, +{"properties":{"name":"NRW iDOP","id":"nrw_idop_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_idop?LAYERS=nw_idop_rgb&STYLES=default&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, +{"properties":{"name":"NRW Orthophoto","id":"nrw_ortho_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_dop?LAYERS=nw_dop_rgb&STYLES=default&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, +{"properties":{"name":"NRW vDOP","id":"nrw_vdop_wms","url":"https://www.wms.nrw.de/geobasis/wms_nw_vdop?LAYERS=nw_vdop_rgb&STYLES=default&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[6.48,50.32],[6.88,50.32],[6.88,50.4],[6.96,50.4],[6.96,50.48],[7.04,50.48],[7.04,50.56],[7.36,50.56],[7.36,50.64],[7.52,50.64],[7.52,50.72],[7.76,50.72],[7.76,50.8],[7.92,50.8],[7.92,50.72],[8,50.72],[8,50.64],[8.24,50.64],[8.24,50.8],[8.4,50.8],[8.4,50.88],[8.48,50.88],[8.48,50.96],[8.56,50.96],[8.56,51.04],[8.8,51.04],[8.8,51.28],[8.72,51.28],[8.72,51.36],[8.96,51.36],[8.96,51.44],[9.04,51.44],[9.04,51.36],[9.2,51.36],[9.2,51.44],[9.36,51.44],[9.36,51.52],[9.44,51.52],[9.44,51.6],[9.52,51.6],[9.52,51.68],[9.44,51.68],[9.44,51.76],[9.52,51.76],[9.52,51.92],[9.36,51.92],[9.36,52],[9.28,52],[9.28,52.08],[9.2,52.08],[9.2,52.16],[9.12,52.16],[9.12,52.32],[9.2,52.32],[9.2,52.56],[8.96,52.56],[8.96,52.48],[8.72,52.48],[8.72,52.56],[8.4,52.56],[8.4,52.48],[8.24,52.48],[8.24,52.32],[8.4,52.32],[8.4,52.16],[8.08,52.16],[8.08,52.24],[8,52.24],[8,52.4],[7.76,52.4],[7.76,52.48],[7.52,52.48],[7.52,52.4],[7.36,52.4],[7.36,52.32],[7.04,52.32],[7.04,52.24],[6.8,52.24],[6.8,52.16],[6.72,52.16],[6.72,52.08],[6.64,52.08],[6.64,51.92],[5.92,51.92],[5.92,51.68],[6,51.68],[6,51.6],[6.08,51.6],[6.08,51.52],[6.16,51.52],[6.16,51.36],[6.08,51.36],[6.08,51.28],[6,51.28],[6,51.12],[5.84,51.12],[5.84,50.96],[6,50.96],[6,50.88],[5.92,50.88],[5.92,50.72],[6,50.72],[6,50.64],[6.16,50.64],[6.16,50.48],[6.32,50.48],[6.32,50.32],[6.4,50.32],[6.4,50.24],[6.48,50.24],[6.48,50.32]]],"type":"Polygon"}}, {"properties":{"name":"Saxony latest aerial imagery","id":"GEOSN-DOP-RGB","url":"https://geodienste.sachsen.de/wms_geosn_dop-rgb/guest?LAYERS=sn_dop_020&STYLES=&FORMAT=image/jpeg&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Staatsbetrieb Geobasisinformation und Vermessung Sachsen","url":"https://geoportal.sachsen.de/cps/metadaten_portal.html?id=cd01c334-7e32-482f-bd43-af286707178a"},"type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[13.54901,50.69792],[13.84251,50.71126],[13.93239,50.74504],[14.04614,50.79389],[14.25257,50.85953],[14.40002,50.88966],[14.42671,50.9357],[14.35649,50.97197],[14.32559,50.99319],[14.27504,50.99054],[14.31577,51.04266],[14.41547,51.01263],[14.51939,51.0038],[14.5882,50.9817],[14.54467,50.91977],[14.57977,50.90649],[14.64718,50.92243],[14.64437,50.90915],[14.60505,50.85687],[14.7202,50.81785],[14.81008,50.81341],[14.85642,50.89055],[14.91259,50.94721],[14.99685,51.08679],[15.05303,51.24793],[15.05583,51.29274],[14.99264,51.34452],[14.98843,51.398],[14.97719,51.45754],[14.90979,51.49603],[14.73986,51.5371],[14.73986,51.59122],[14.70054,51.60605],[14.67948,51.5982],[14.68369,51.57813],[14.67386,51.55806],[14.61769,51.55718],[14.58399,51.59035],[14.51939,51.56941],[14.43513,51.5598],[14.327,51.52574],[14.13461,51.55544],[14.0672,51.49952],[14.02788,51.47854],[14.04333,51.45229],[13.99558,51.39274],[13.95767,51.40588],[13.88886,51.38836],[13.72455,51.37434],[13.55463,51.39274],[13.40437,51.45929],[13.35241,51.43916],[13.3159,51.44354],[13.28641,51.41815],[13.22602,51.40063],[13.21339,51.46104],[13.219,51.52661],[13.17406,51.5982],[13.00274,51.67751],[12.90584,51.65312],[12.90303,51.66619],[12.85388,51.69318],[12.76401,51.65922],[12.68817,51.67054],[12.64324,51.62959],[12.57723,51.63046],[12.42557,51.61041],[12.23037,51.57028],[12.17701,51.53011],[12.13909,51.46017],[12.16718,51.41727],[12.1742,51.33487],[12.13207,51.3182],[12.18684,51.21364],[12.15875,51.18812],[12.22054,51.09296],[12.49017,51.05414],[12.52106,50.99319],[12.60532,50.97286],[12.62639,50.91889],[12.50281,50.91092],[12.23739,50.81874],[12.21352,50.72993],[12.28654,50.665],[12.21773,50.6463],[12.13347,50.6276],[12.05343,50.56342],[12.01972,50.64719],[11.85963,50.54825],[11.87649,50.50808],[11.92704,50.5054],[11.93687,50.48664],[11.87087,50.44194],[11.93406,50.39989],[11.96917,50.33987],[12.12083,50.29773],[12.17279,50.3067],[12.18543,50.26094],[12.21212,50.25375],[12.25705,50.21603],[12.28233,50.15668],[12.35535,50.15848],[12.35535,50.22142],[12.41433,50.28158],[12.51123,50.34705],[12.53791,50.38735],[12.67835,50.40257],[12.71205,50.38646],[12.7289,50.39631],[12.75699,50.42584],[12.78648,50.43389],[12.81737,50.41779],[12.84686,50.43657],[12.94797,50.38735],[13.00976,50.41421],[13.04627,50.44999],[13.0561,50.48753],[13.21479,50.49289],[13.27517,50.56609],[13.34118,50.56877],[13.39173,50.61334],[13.47739,50.58571],[13.54761,50.63473],[13.56867,50.67212],[13.54901,50.69792]]],"type":"Polygon"}}, {"properties":{"name":"© GeoBasis-DE/LVermGeo LSA, DOP20","id":"LSA-DOP20","url":"https://www.geodatenportal.sachsen-anhalt.de/wss/service/ST_LVermGeo_DOP_WMS_OpenData/guest?FORMAT=image/png&TRANSPARENT=TRUE&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap&LAYERS=lsa_lvermgeo_dop20_2&STYLES=&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}","attribution":{"text":"© GeoBasis-DE/LVermGeo LSA","url":"https://www.lvermgeo.sachsen-anhalt.de/de/kostenfreie_geobasisdaten_lvermgeo.html"},"type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[10.56074,52.00392],[10.56588,52],[10.61351,51.92026],[10.56621,51.85598],[10.69013,51.64568],[10.88483,51.61439],[10.99158,51.41802],[11.34477,51.38654],[11.46667,51.30465],[11.36695,51.22636],[11.46956,51.10979],[12.2299,50.93098],[12.29319,51.02138],[12.15085,51.45859],[12.23452,51.55626],[13.02375,51.63743],[13.15432,51.68641],[13.15012,51.87062],[12.65345,52.0114],[12.44355,52.0167],[12.21787,52.17114],[12.29715,52.22891],[12.31883,52.49459],[12.18565,52.49612],[12.14428,52.53241],[12.23671,52.62868],[12.25584,52.79854],[12.23343,52.85993],[11.82449,52.91752],[11.84481,52.95218],[11.62298,53.0417],[11.51282,53.00726],[11.49252,52.95981],[11.3554,52.89056],[11.09873,52.9127],[10.99998,52.9112],[10.94128,52.8538],[10.76565,52.84212],[10.75152,52.78499],[10.79684,52.71404],[11.00297,52.49614],[10.9346,52.4741],[11.0153,52.38886],[11.04297,52.38843],[11.06922,52.35727],[10.98026,52.34328],[11.01205,52.29087],[11.08563,52.22908],[11.02578,52.20988],[11.01905,52.17829],[11.06046,52.1724],[10.97565,52.10553],[10.94254,52.10349],[10.94274,52.09386],[10.9704,52.08525],[10.96512,52.05748],[10.87552,52.05818],[10.71061,52.04976],[10.64827,52.04107],[10.65499,52.02454],[10.56074,52.00392]]],"type":"Polygon"}}, {"properties":{"name":"Saxony WebAtlasSN","id":"GEOSN-WebAtlas","url":"https://geodienste.sachsen.de/wms_geosn_webatlas-sn/guest?LAYERS=Vegetation,Siedlung,Gewaesser,Verkehr,Administrative_Einheiten,Beschriftung&STYLES=&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"Staatsbetrieb Geobasisinformation und Vermessung Sachsen","url":"https://geoportal.sachsen.de/cps/metadaten_portal.html?id=475a9197-620f-4dcb-b8aa-7f71b626443f"},"type":"wms","category":"map"},"type":"Feature","geometry":{"coordinates":[[[13.54901,50.69792],[13.84251,50.71126],[13.93239,50.74504],[14.04614,50.79389],[14.25257,50.85953],[14.40002,50.88966],[14.42671,50.9357],[14.35649,50.97197],[14.32559,50.99319],[14.27504,50.99054],[14.31577,51.04266],[14.41547,51.01263],[14.51939,51.0038],[14.5882,50.9817],[14.54467,50.91977],[14.57977,50.90649],[14.64718,50.92243],[14.64437,50.90915],[14.60505,50.85687],[14.7202,50.81785],[14.81008,50.81341],[14.85642,50.89055],[14.91259,50.94721],[14.99685,51.08679],[15.05303,51.24793],[15.05583,51.29274],[14.99264,51.34452],[14.98843,51.398],[14.97719,51.45754],[14.90979,51.49603],[14.73986,51.5371],[14.73986,51.59122],[14.70054,51.60605],[14.67948,51.5982],[14.68369,51.57813],[14.67386,51.55806],[14.61769,51.55718],[14.58399,51.59035],[14.51939,51.56941],[14.43513,51.5598],[14.327,51.52574],[14.13461,51.55544],[14.0672,51.49952],[14.02788,51.47854],[14.04333,51.45229],[13.99558,51.39274],[13.95767,51.40588],[13.88886,51.38836],[13.72455,51.37434],[13.55463,51.39274],[13.40437,51.45929],[13.35241,51.43916],[13.3159,51.44354],[13.28641,51.41815],[13.22602,51.40063],[13.21339,51.46104],[13.219,51.52661],[13.17406,51.5982],[13.00274,51.67751],[12.90584,51.65312],[12.90303,51.66619],[12.85388,51.69318],[12.76401,51.65922],[12.68817,51.67054],[12.64324,51.62959],[12.57723,51.63046],[12.42557,51.61041],[12.23037,51.57028],[12.17701,51.53011],[12.13909,51.46017],[12.16718,51.41727],[12.1742,51.33487],[12.13207,51.3182],[12.18684,51.21364],[12.15875,51.18812],[12.22054,51.09296],[12.49017,51.05414],[12.52106,50.99319],[12.60532,50.97286],[12.62639,50.91889],[12.50281,50.91092],[12.23739,50.81874],[12.21352,50.72993],[12.28654,50.665],[12.21773,50.6463],[12.13347,50.6276],[12.05343,50.56342],[12.01972,50.64719],[11.85963,50.54825],[11.87649,50.50808],[11.92704,50.5054],[11.93687,50.48664],[11.87087,50.44194],[11.93406,50.39989],[11.96917,50.33987],[12.12083,50.29773],[12.17279,50.3067],[12.18543,50.26094],[12.21212,50.25375],[12.25705,50.21603],[12.28233,50.15668],[12.35535,50.15848],[12.35535,50.22142],[12.41433,50.28158],[12.51123,50.34705],[12.53791,50.38735],[12.67835,50.40257],[12.71205,50.38646],[12.7289,50.39631],[12.75699,50.42584],[12.78648,50.43389],[12.81737,50.41779],[12.84686,50.43657],[12.94797,50.38735],[13.00976,50.41421],[13.04627,50.44999],[13.0561,50.48753],[13.21479,50.49289],[13.27517,50.56609],[13.34118,50.56877],[13.39173,50.61334],[13.47739,50.58571],[13.54761,50.63473],[13.56867,50.67212],[13.54901,50.69792]]],"type":"Polygon"}}, @@ -185,6 +187,7 @@ {"properties":{"name":"Worms 2016","id":"Worms-2016","url":"https://geoportal-worms.de/ogc/wms/luftbild2016?LAYERS=FFF9DFB4F6814391AB0B4BC96B3B70B2&STYLES=&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"© Nibelungenstadt Worms","url":"https://www.worms.de"},"type":"wms","category":"photo"},"type":"Feature","geometry":{"coordinates":[[[8.41625,49.59524],[8.41147,49.60428],[8.40616,49.60903],[8.39679,49.61394],[8.38843,49.62225],[8.3867,49.62296],[8.37888,49.63745],[8.37361,49.65316],[8.37042,49.66052],[8.3646,49.66594],[8.36269,49.66817],[8.35869,49.67585],[8.35633,49.68297],[8.35897,49.69256],[8.36624,49.69906],[8.38915,49.70835],[8.42429,49.7152],[8.43416,49.71893],[8.44252,49.72434],[8.44789,49.7311],[8.44216,49.73321],[8.42298,49.72751],[8.41079,49.73292],[8.4077,49.73603],[8.39806,49.73727],[8.39279,49.73686],[8.37724,49.73439],[8.37615,49.72587],[8.37297,49.72593],[8.36942,49.72704],[8.37115,49.7308],[8.34915,49.73169],[8.34751,49.71752],[8.34651,49.71258],[8.35006,49.69718],[8.33842,49.69712],[8.3376,49.696],[8.33985,49.6856],[8.34087,49.68373],[8.33778,49.6839],[8.33658,49.68343],[8.33494,49.68375],[8.32789,49.6845],[8.32508,49.68428],[8.32426,49.68503],[8.32039,49.68482],[8.31701,49.68647],[8.30998,49.68522],[8.30857,49.68934],[8.29623,49.68853],[8.29587,49.69035],[8.29441,49.69106],[8.29005,49.69071],[8.28946,49.69332],[8.28814,49.694],[8.25672,49.68954],[8.25607,49.68856],[8.25827,49.6782],[8.25481,49.67838],[8.25418,49.67718],[8.26309,49.67094],[8.26436,49.6707],[8.26691,49.66597],[8.26991,49.6667],[8.27723,49.65567],[8.24409,49.65107],[8.23977,49.64713],[8.23895,49.63547],[8.23754,49.62852],[8.23636,49.62758],[8.2379,49.62346],[8.24181,49.62399],[8.24318,49.61624],[8.24527,49.6161],[8.25113,49.61689],[8.25104,49.60045],[8.256,49.58755],[8.26213,49.58769],[8.26282,49.58581],[8.26759,49.58536],[8.27614,49.58663],[8.27855,49.58557],[8.29164,49.58772],[8.2915,49.5892],[8.30905,49.59138],[8.32469,49.59415],[8.32887,49.59633],[8.33083,49.59606],[8.33324,49.59848],[8.34365,49.59388],[8.34474,49.59438],[8.35092,49.60537],[8.35228,49.6069],[8.35706,49.60632],[8.36001,49.60484],[8.36219,49.60089],[8.3686,49.60331],[8.37329,49.60219],[8.37511,49.60334],[8.37847,49.60428],[8.38606,49.60185],[8.38743,49.60275],[8.39006,49.60287],[8.39393,49.6006],[8.39943,49.5963],[8.40243,49.59521],[8.40302,49.59235],[8.40602,49.59158],[8.4097,49.59317],[8.41498,49.594],[8.4162,49.59453],[8.41625,49.59524]]],"type":"Polygon"}}, {"properties":{"name":"Worms 2020","id":"Worms-2020","url":"https://geoportal-worms.de/ogc/wms/luftbild2020?LAYERS=E1C1EF1295564C3E8B3504D516F081E9&STYLES=&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","attribution":{"required":true,"text":"© Nibelungenstadt Worms","url":"https://www.worms.de"},"type":"wms","category":"photo","best":true},"type":"Feature","geometry":{"coordinates":[[[8.41904,49.59534],[8.41645,49.60086],[8.41243,49.60693],[8.40566,49.61182],[8.39547,49.6178],[8.38906,49.62422],[8.3797,49.64198],[8.37351,49.66058],[8.36388,49.67164],[8.35924,49.67959],[8.3626,49.694],[8.38597,49.7057],[8.42325,49.71311],[8.43598,49.71711],[8.45143,49.73086],[8.44216,49.73533],[8.42307,49.72957],[8.41434,49.73345],[8.40961,49.73791],[8.39234,49.73862],[8.38688,49.73674],[8.37579,49.73627],[8.37411,49.73433],[8.37413,49.7306],[8.37372,49.72766],[8.37304,49.72789],[8.37376,49.72989],[8.37354,49.73121],[8.37261,49.73208],[8.37042,49.7329],[8.34806,49.73304],[8.34651,49.73171],[8.34619,49.72954],[8.34396,49.71273],[8.34696,49.69888],[8.33815,49.69894],[8.33592,49.69773],[8.3351,49.69565],[8.33705,49.68541],[8.33378,49.68556],[8.3281,49.68632],[8.32655,49.68621],[8.32392,49.68694],[8.32237,49.68685],[8.31951,49.68762],[8.31646,49.68823],[8.31201,49.68741],[8.31,49.69073],[8.30819,49.69118],[8.29841,49.69062],[8.29628,49.69247],[8.29237,49.69285],[8.29059,49.69512],[8.28759,49.69579],[8.25663,49.69144],[8.25372,49.69023],[8.25345,49.68762],[8.25522,49.68021],[8.25191,49.67847],[8.25172,49.67626],[8.26091,49.66976],[8.26232,49.6685],[8.26427,49.66494],[8.26823,49.66408],[8.27368,49.65699],[8.24281,49.65263],[8.23731,49.64801],[8.23613,49.63677],[8.23649,49.63512],[8.23545,49.62255],[8.23863,49.62231],[8.23863,49.6216],[8.2399,49.62166],[8.24018,49.61621],[8.24136,49.61512],[8.24829,49.6152],[8.24829,49.61475],[8.24863,49.61484],[8.24865,49.61448],[8.24942,49.61448],[8.24968,49.59589],[8.25254,49.58993],[8.25377,49.58651],[8.25704,49.58545],[8.25995,49.58569],[8.26077,49.58492],[8.28964,49.58519],[8.29305,49.58589],[8.29409,49.58725],[8.29523,49.58805],[8.3,49.58855],[8.31487,49.59082],[8.3151,49.5912],[8.3196,49.59132],[8.32496,49.59217],[8.32951,49.59435],[8.33246,49.59474],[8.3336,49.59591],[8.34224,49.59223],[8.34546,49.59258],[8.34806,49.59488],[8.35319,49.60411],[8.35433,49.60496],[8.35578,49.60437],[8.35751,49.60349],[8.35919,49.60057],[8.36824,49.60078],[8.36906,49.60125],[8.36988,49.60072],[8.37561,49.60081],[8.37624,49.60157],[8.37811,49.60222],[8.38252,49.60084],[8.38879,49.60084],[8.38915,49.60113],[8.39415,49.59842],[8.39606,49.59674],[8.39706,49.59671],[8.39684,49.59568],[8.40025,49.59441],[8.40079,49.59129],[8.40293,49.59132],[8.40306,49.59085],[8.40506,49.59082],[8.40516,49.59126],[8.41043,49.59129],[8.41088,49.59149],[8.41688,49.59261],[8.41911,49.59459],[8.41904,49.59534]]],"type":"Polygon"}}, {"properties":{"name":"Aachen Liegenschaftskataster","id":"aachen_alkis_wms","url":"https://geodienste.staedteregion-aachen.de/cgi-bin/qgis_mapserv.fcgi?MAP=/home/geonet/inkasserver/QMAPS/ALKIS/ALKIS_LK_Inkas.qgs&LAYERS=alkis_lk_inkas&FORMAT=image/png&STYLES=&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap","type":"wms","category":"map","best":true},"type":"Feature","geometry":{"coordinates":[[[6.2,50.48],[6.36,50.49],[6.43,50.6],[6.31,50.95],[6.11,50.95],[5.96,50.79],[6.2,50.48]]],"type":"Polygon"}}, +{"properties":{"name":"ALKIS Kreis Viersen","id":"viersen_alkis_wms","url":"https://gdi-niederrhein-geodienste.de/flurkarte_verb_sammeldienst/service?VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap&LAYERS=FlurkarteAdV_Viersen&STYLES=&CRS={proj}&BBOX={bbox}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png","type":"wms","category":"map","min_zoom":16,"best":true},"type":"Feature","geometry":{"coordinates":[[[6.064,51.22],[6.07,51.17],[6.211,51.164],[6.321,51.179],[6.34,51.22],[6.35,51.22],[6.37,51.21],[6.43,51.22],[6.46,51.24],[6.523,51.216],[6.64,51.25],[6.59,51.29],[6.514,51.293],[6.52,51.33],[6.5,51.35],[6.49,51.4],[6.52,51.39],[6.54,51.4],[6.53,51.43],[6.47,51.42],[6.46,51.41],[6.4,51.42],[6.35,51.38],[6.222,51.368],[6.17,51.337],[6.07,51.24],[6.064,51.22]]],"type":"Polygon"}}, {"properties":{"name":"SDFI Aerial Imagery","id":"Geodatastyrelsen_Denmark","url":"https://osmtools.septima.dk/mapproxy/tiles/1.0.0/kortforsyningen_ortoforaar/EPSG3857/{zoom}/{x}/{y}.jpeg","attribution":{"required":true,"text":"Styrelsen for Dataforsyning og Infrastruktur","url":"https://dataforsyningen.dk/asset/PDF/rettigheder_vilkaar/Vilk%C3%A5r%20for%20brug%20af%20frie%20geografiske%20data.pdf"},"type":"tms","category":"photo","max_zoom":21,"best":true},"type":"Feature","geometry":{"coordinates":[[[[15.28158,55.15442],[15.12556,55.16238],[15.13934,55.25174],[14.82638,55.26713],[14.83952,55.35652],[14.68259,55.36394],[14.63175,55.00625],[15.25356,54.97576],[15.28158,55.15442]]],[[[15.29572,55.24374],[15.30992,55.33306],[15.1532,55.34108],[15.13934,55.25174],[15.29572,55.24374]]],[[[11.57829,56.18804],[11.73923,56.18458],[11.74564,56.27432],[11.58433,56.27779],[11.57829,56.18804]]],[[[8.01851,56.75014],[8.05027,55.49247],[8.20873,55.49373],[8.2104,55.40398],[8.36838,55.40421],[8.37439,54.95517],[8.53143,54.95516],[8.5322,54.86638],[9.15628,54.86754],[9.15558,54.77696],[10.08737,54.77239],[10.09023,54.86221],[10.24598,54.86047],[10.2424,54.77059],[10.55472,54.76702],[10.5511,54.67817],[10.70411,54.67567],[10.70745,54.7113],[10.73844,54.71085],[10.73891,54.71976],[10.7544,54.71957],[10.75514,54.73758],[10.77073,54.73728],[10.77136,54.76439],[10.86512,54.76347],[10.86172,54.6734],[11.17064,54.66865],[11.16585,54.57822],[11.78374,54.56548],[11.7795,54.47536],[12.08586,54.46817],[12.10707,54.73782],[12.26102,54.73316],[12.27666,54.9119],[12.5872,54.90363],[12.60486,55.08329],[12.28973,55.09236],[12.2987,55.18223],[12.45529,55.17782],[12.46273,55.26722],[12.62009,55.26326],[12.62697,55.35238],[12.47024,55.35705],[12.47782,55.44707],[12.32061,55.45137],[12.32687,55.54121],[12.96129,55.52173],[12.97923,55.7014],[12.66111,55.71143],[12.70235,56.15944],[12.06085,56.17626],[12.05403,56.08713],[11.732,56.09521],[11.7265,56.00506],[11.08581,56.01783],[11.08028,55.92792],[10.91971,55.93094],[10.92587,56.02012],[10.60521,56.02475],[10.60797,56.11503],[10.76948,56.11201],[10.77197,56.20202],[10.93412,56.19948],[10.94299,56.37953],[11.10526,56.37683],[11.10993,56.46647],[10.94792,56.46922],[10.95242,56.55898],[10.4649,56.56567],[10.47503,56.83509],[10.31123,56.83693],[10.3144,56.92676],[10.47862,56.92491],[10.48577,57.10451],[10.65078,57.10245],[10.67104,57.55141],[10.504,57.55351],[10.5077,57.64331],[10.67516,57.6412],[10.68349,57.82077],[10.51521,57.82289],[10.51183,57.73303],[10.17542,57.73678],[10.17257,57.64628],[9.83749,57.64933],[9.8352,57.55963],[9.66873,57.56056],[9.66497,57.38116],[9.49886,57.38206],[9.49789,57.29196],[9.33191,57.29248],[9.33163,57.20276],[8.50339,57.20205],[8.50544,57.11232],[8.33925,57.11196],[8.34133,57.02199],[8.17633,57.02089],[8.18192,56.75099],[8.01851,56.75014]],[[10.28659,56.11868],[10.44667,56.11672],[10.44393,56.02704],[10.28315,56.02819],[10.28659,56.11868]],[[10.4335,55.66935],[10.44177,55.75792],[10.75623,55.75792],[10.74381,55.66469],[10.4335,55.66935]],[[10.74381,55.57123],[10.74381,55.66469],[10.92587,55.66702],[10.8969,55.57123],[10.74381,55.57123]],[[10.90518,55.39539],[10.8969,55.57123],[11.07896,55.57123],[11.06137,55.38128],[10.90518,55.39539]],[[11.04586,55.03186],[11.0593,55.11241],[11.20308,55.11714],[11.20308,55.02475],[11.04586,55.03186]]],[[[11.44596,56.64011],[11.77167,56.63328],[11.78492,56.81274],[11.45777,56.81955],[11.44596,56.64011]]],[[[11.31618,57.1818],[11.32747,57.3613],[10.82906,57.36953],[10.81577,57.10017],[11.14566,57.09496],[11.15087,57.18473],[11.31618,57.1818]]]],"type":"MultiPolygon"}}, {"properties":{"name":"SDFI Cadastral Parcels INSPIRE View","id":"Geodatastyrelsen_Cadastral_Parcels_INSPIRE_View","url":"https://kortforsyningen.kms.dk/cp_inspire?LAYERS=CP.CadastralParcel&STYLES=&FORMAT=image/png&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}&VERSION=1.3.0&SERVICE=WMS&REQUEST=GetMap&LOGIN=OpenStreetMapDK2015&PASSWORD=Gall4Peters","attribution":{"required":true,"text":"Geodatastyrelsen og Styrelsen for Dataforsyning og Infrastruktur","url":"https://dataforsyningen.dk/asset/PDF/rettigheder_vilkaar/Vilk%C3%A5r%20for%20brug%20af%20data%20fra%20GST.pdf"},"type":"wms","category":"other","max_zoom":19},"type":"Feature","geometry":{"coordinates":[[[[15.28158,55.15442],[15.12556,55.16238],[15.13934,55.25174],[14.82638,55.26713],[14.83952,55.35652],[14.68259,55.36394],[14.63175,55.00625],[15.25356,54.97576],[15.28158,55.15442]]],[[[15.29572,55.24374],[15.30992,55.33306],[15.1532,55.34108],[15.13934,55.25174],[15.29572,55.24374]]],[[[11.57829,56.18804],[11.73923,56.18458],[11.74564,56.27432],[11.58433,56.27779],[11.57829,56.18804]]],[[[8.01851,56.75014],[8.05027,55.49247],[8.20873,55.49373],[8.2104,55.40398],[8.36838,55.40421],[8.37439,54.95517],[8.53143,54.95516],[8.5322,54.86638],[9.15628,54.86754],[9.15558,54.77696],[10.08737,54.77239],[10.09023,54.86221],[10.24598,54.86047],[10.2424,54.77059],[10.55472,54.76702],[10.5511,54.67817],[10.70411,54.67567],[10.70745,54.7113],[10.73844,54.71085],[10.73891,54.71976],[10.7544,54.71957],[10.75514,54.73758],[10.77073,54.73728],[10.77136,54.76439],[10.86512,54.76347],[10.86172,54.6734],[11.17064,54.66865],[11.16585,54.57822],[11.78374,54.56548],[11.7795,54.47536],[12.08586,54.46817],[12.10707,54.73782],[12.26102,54.73316],[12.27666,54.9119],[12.5872,54.90363],[12.60486,55.08329],[12.28973,55.09236],[12.2987,55.18223],[12.45529,55.17782],[12.46273,55.26722],[12.62009,55.26326],[12.62697,55.35238],[12.47024,55.35705],[12.47782,55.44707],[12.32061,55.45137],[12.32687,55.54121],[12.96129,55.52173],[12.97923,55.7014],[12.66111,55.71143],[12.70235,56.15944],[12.06085,56.17626],[12.05403,56.08713],[11.732,56.09521],[11.7265,56.00506],[11.08581,56.01783],[11.08028,55.92792],[10.91971,55.93094],[10.92587,56.02012],[10.60521,56.02475],[10.60797,56.11503],[10.76948,56.11201],[10.77197,56.20202],[10.93412,56.19948],[10.94299,56.37953],[11.10526,56.37683],[11.10993,56.46647],[10.94792,56.46922],[10.95242,56.55898],[10.4649,56.56567],[10.47503,56.83509],[10.31123,56.83693],[10.3144,56.92676],[10.47862,56.92491],[10.48577,57.10451],[10.65078,57.10245],[10.67104,57.55141],[10.504,57.55351],[10.5077,57.64331],[10.67516,57.6412],[10.68349,57.82077],[10.51521,57.82289],[10.51183,57.73303],[10.17542,57.73678],[10.17257,57.64628],[9.83749,57.64933],[9.8352,57.55963],[9.66873,57.56056],[9.66497,57.38116],[9.49886,57.38206],[9.49789,57.29196],[9.33191,57.29248],[9.33163,57.20276],[8.50339,57.20205],[8.50544,57.11232],[8.33925,57.11196],[8.34133,57.02199],[8.17633,57.02089],[8.18192,56.75099],[8.01851,56.75014]],[[10.28659,56.11868],[10.44667,56.11672],[10.44393,56.02704],[10.28315,56.02819],[10.28659,56.11868]],[[10.4335,55.66935],[10.44177,55.75792],[10.75623,55.75792],[10.74381,55.66469],[10.4335,55.66935]],[[10.74381,55.57123],[10.74381,55.66469],[10.92587,55.66702],[10.8969,55.57123],[10.74381,55.57123]],[[10.90518,55.39539],[10.8969,55.57123],[11.07896,55.57123],[11.06137,55.38128],[10.90518,55.39539]],[[11.04586,55.03186],[11.0593,55.11241],[11.20308,55.11714],[11.20308,55.02475],[11.04586,55.03186]]],[[[11.44596,56.64011],[11.77167,56.63328],[11.78492,56.81274],[11.45777,56.81955],[11.44596,56.64011]]],[[[11.31618,57.1818],[11.32747,57.3613],[10.82906,57.36953],[10.81577,57.10017],[11.14566,57.09496],[11.15087,57.18473],[11.31618,57.1818]]]],"type":"MultiPolygon"}}, {"properties":{"name":"SDFI DTK Map25","id":"Geodatastyrelsen_DTK_Kort25","url":"https://api.dataforsyningen.dk/dtk_25_DAF?service=WMS&request=GetMap&token=52065b2ec5fda5a46a50b451f3f24473&FORMAT=image/png&TRANSPARENT=TRUE&VERSION=1.3.0&Layers=dtk25&STYLES=&CRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}","attribution":{"required":true,"text":"Styrelsen for Dataforsyning og Infrastruktur","url":"https://dataforsyningen.dk/asset/PDF/rettigheder_vilkaar/Vilk%C3%A5r%20for%20brug%20af%20frie%20geografiske%20data.pdf"},"type":"wms","category":"map","max_zoom":19},"type":"Feature","geometry":{"coordinates":[[[[15.28158,55.15442],[15.12556,55.16238],[15.13934,55.25174],[14.82638,55.26713],[14.83952,55.35652],[14.68259,55.36394],[14.63175,55.00625],[15.25356,54.97576],[15.28158,55.15442]]],[[[15.29572,55.24374],[15.30992,55.33306],[15.1532,55.34108],[15.13934,55.25174],[15.29572,55.24374]]],[[[11.57829,56.18804],[11.73923,56.18458],[11.74564,56.27432],[11.58433,56.27779],[11.57829,56.18804]]],[[[8.01851,56.75014],[8.05027,55.49247],[8.20873,55.49373],[8.2104,55.40398],[8.36838,55.40421],[8.37439,54.95517],[8.53143,54.95516],[8.5322,54.86638],[9.15628,54.86754],[9.15558,54.77696],[10.08737,54.77239],[10.09023,54.86221],[10.24598,54.86047],[10.2424,54.77059],[10.55472,54.76702],[10.5511,54.67817],[10.70411,54.67567],[10.70745,54.7113],[10.73844,54.71085],[10.73891,54.71976],[10.7544,54.71957],[10.75514,54.73758],[10.77073,54.73728],[10.77136,54.76439],[10.86512,54.76347],[10.86172,54.6734],[11.17064,54.66865],[11.16585,54.57822],[11.78374,54.56548],[11.7795,54.47536],[12.08586,54.46817],[12.10707,54.73782],[12.26102,54.73316],[12.27666,54.9119],[12.5872,54.90363],[12.60486,55.08329],[12.28973,55.09236],[12.2987,55.18223],[12.45529,55.17782],[12.46273,55.26722],[12.62009,55.26326],[12.62697,55.35238],[12.47024,55.35705],[12.47782,55.44707],[12.32061,55.45137],[12.32687,55.54121],[12.96129,55.52173],[12.97923,55.7014],[12.66111,55.71143],[12.70235,56.15944],[12.06085,56.17626],[12.05403,56.08713],[11.732,56.09521],[11.7265,56.00506],[11.08581,56.01783],[11.08028,55.92792],[10.91971,55.93094],[10.92587,56.02012],[10.60521,56.02475],[10.60797,56.11503],[10.76948,56.11201],[10.77197,56.20202],[10.93412,56.19948],[10.94299,56.37953],[11.10526,56.37683],[11.10993,56.46647],[10.94792,56.46922],[10.95242,56.55898],[10.4649,56.56567],[10.47503,56.83509],[10.31123,56.83693],[10.3144,56.92676],[10.47862,56.92491],[10.48577,57.10451],[10.65078,57.10245],[10.67104,57.55141],[10.504,57.55351],[10.5077,57.64331],[10.67516,57.6412],[10.68349,57.82077],[10.51521,57.82289],[10.51183,57.73303],[10.17542,57.73678],[10.17257,57.64628],[9.83749,57.64933],[9.8352,57.55963],[9.66873,57.56056],[9.66497,57.38116],[9.49886,57.38206],[9.49789,57.29196],[9.33191,57.29248],[9.33163,57.20276],[8.50339,57.20205],[8.50544,57.11232],[8.33925,57.11196],[8.34133,57.02199],[8.17633,57.02089],[8.18192,56.75099],[8.01851,56.75014]],[[10.28659,56.11868],[10.44667,56.11672],[10.44393,56.02704],[10.28315,56.02819],[10.28659,56.11868]],[[10.4335,55.66935],[10.44177,55.75792],[10.75623,55.75792],[10.74381,55.66469],[10.4335,55.66935]],[[10.74381,55.57123],[10.74381,55.66469],[10.92587,55.66702],[10.8969,55.57123],[10.74381,55.57123]],[[10.90518,55.39539],[10.8969,55.57123],[11.07896,55.57123],[11.06137,55.38128],[10.90518,55.39539]],[[11.04586,55.03186],[11.0593,55.11241],[11.20308,55.11714],[11.20308,55.02475],[11.04586,55.03186]]],[[[11.44596,56.64011],[11.77167,56.63328],[11.78492,56.81274],[11.45777,56.81955],[11.44596,56.64011]]],[[[11.31618,57.1818],[11.32747,57.3613],[10.82906,57.36953],[10.81577,57.10017],[11.14566,57.09496],[11.15087,57.18473],[11.31618,57.1818]]]],"type":"MultiPolygon"}}, diff --git a/src/index_theme.ts.template b/src/index_theme.ts.template index c90fe9fba..218113851 100644 --- a/src/index_theme.ts.template +++ b/src/index_theme.ts.template @@ -3,6 +3,7 @@ import SvelteUIElement from "./src/UI/Base/SvelteUIElement" import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte" import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig"; import MetaTagging from "./src/Logic/MetaTagging"; +import { FixedUiElement } from "./src/UI/Base/FixedUiElement"; function webgl_support() { try { diff --git a/theme.html b/theme.html index 9c94eaf72..362218168 100644 --- a/theme.html +++ b/theme.html @@ -4,7 +4,7 @@ - + @@ -65,57 +65,12 @@
Below
+ + + + - - - - - - - + From 5a6f5f064b9f1e68dbf502fe7e25859262ea9f1e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 28 Sep 2023 03:00:22 +0200 Subject: [PATCH 02/41] Security: add inline script with automatic hash --- package-lock.json | 16 +++++++++++-- package.json | 10 ++++---- scripts/generateLayouts.ts | 39 ++++++++++++++++++++++++++------ scripts/hetzner/config/Caddyfile | 4 ++++ scripts/hetzner/deployHetzner.sh | 4 ++-- src/UI/RemoveOtherLanguages.js | 31 +++++++++++++++++++++++++ src/UI/RemoveOtherLanguages.ts | 32 -------------------------- theme.html | 2 +- 8 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 src/UI/RemoveOtherLanguages.js delete mode 100644 src/UI/RemoveOtherLanguages.ts diff --git a/package-lock.json b/package-lock.json index 5f6a1ce4f..696938687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.33.1", + "version": "0.33.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.33.1", + "version": "0.33.5", "license": "GPL-3.0-or-later", "dependencies": { "@rgossiaux/svelte-headlessui": "^1.0.2", @@ -23,6 +23,7 @@ "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", + "crypto": "^1.0.1", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", "dompurify": "^3.0.5", @@ -5392,6 +5393,12 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -17341,6 +17348,11 @@ "which": "^2.0.1" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", diff --git a/package.json b/package.json index 98b48288c..45ede4ec4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "main": "index.ts", "type": "module", "config": { - "#": "Various endpoints that are instance-specific. This is the default configuration, which is re-exported in 'Constants.ts'.", "#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`", "#oauth_credentials:comment": [ "`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.", @@ -18,10 +17,10 @@ "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`" ], "oauth_credentials": { - "#": "This client-id is registered by 'MapComplete' on osm.org", - "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", - "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", - "url": "https://www.openstreetmap.org" + "#": "This client-id is registered by 'MapComplete' on osm.org", + "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", + "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", + "url": "https://www.openstreetmap.org" }, "api_keys": { "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", @@ -108,6 +107,7 @@ "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", + "crypto": "^1.0.1", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", "dompurify": "^3.0.5", diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 4a8086728..83103f7c3 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -12,6 +12,7 @@ import SpecialVisualizations from "../src/UI/SpecialVisualizations" import Constants from "../src/Models/Constants" import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" import { ImmutableStore } from "../src/Logic/UIEventSource" +import * as crypto from "crypto" const sharp = require("sharp") const template = readFileSync("theme.html", "utf8") @@ -205,9 +206,14 @@ function asLangSpan(t: Translation, tag = "span"): string { } let previousSrc: Set = new Set() -function generateCsp(layout: LayoutConfig): string { +function generateCsp( + layout: LayoutConfig, + options: { + scriptSrcs: string[] + } +): string { const apiUrls: string[] = [ - "self", + "'self'", ...Constants.defaultOverpassUrls, Constants.countryCoderEndpoint, "https://api.openstreetmap.org", @@ -248,9 +254,11 @@ function generateCsp(layout: LayoutConfig): string { ) previousSrc = hosts - const csp = { + const csp: Record = { "default-src": "'self'", - "script-src": "'self' https://gc.zgo.at/count.js", + "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( + " " + ), "img-src": "* data:", // maplibre depends on 'data:' to load "connect-src": connectSrc.join(" "), "report-to": "https://report.mapcomplete.org/csp", @@ -267,6 +275,14 @@ function generateCsp(layout: LayoutConfig): string { ].join("\n") } +const removeOtherLanguages = readFileSync("./src/UI/RemoveOtherLanguages.js", "utf8") + .split("\n") + .map((s) => s.trim()) + .join("\n") +const removeOtherLanguagesHash = crypto + .createHash("sha256") + .update(removeOtherLanguages) + .digest("base64") async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] @@ -338,7 +354,10 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr ].join("\n") const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) - + const templateLines = template.split("\n") + const removeOtherLanguagesReference = templateLines.find( + (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 + ) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace( @@ -346,7 +365,13 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr Translations.t.general.poweredByOsm.textFor(targetLanguage) ) .replace(/.*/s, themeSpecific) - .replace(//, generateCsp(layout)) + .replace( + //, + generateCsp(layout, { + scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], + }) + ) + .replace(removeOtherLanguagesReference, "") .replace( /.*/s, asLangSpan(layout.shortDescription) @@ -357,7 +382,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr ) .replace( - '', + /.*\/src\/index\.ts.*/, `` ) diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index 1638f7541..27b328008 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -10,6 +10,10 @@ hosted.mapcomplete.org { countrycoder.mapcomplete.org { root * tiles/ file_server + header { + +Permissions-Policy "interest-cohort=()" + +Access-Control-Allow-Origin https://hosted.mapcomplete.org https://dev.mapcomplete.org https://mapcomplete.org + } } diff --git a/scripts/hetzner/deployHetzner.sh b/scripts/hetzner/deployHetzner.sh index 587a5fddb..95fab61c1 100755 --- a/scripts/hetzner/deployHetzner.sh +++ b/scripts/hetzner/deployHetzner.sh @@ -17,8 +17,8 @@ npm run test npm run prepare-deploy && mv config.json.bu config.json && zip dist.zip -r dist/* && -scp -r dist.zip hetzner:/root/ && -echo "Upload completed, deploying config and booting" && +scp ./scripts/hetzner/config/* hetzner:/root/ && rsync -rzh --progress dist.zip hetzner:/root/ && +echo "Upload completed, deploying config and booting" && ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" && rm dist.zip diff --git a/src/UI/RemoveOtherLanguages.js b/src/UI/RemoveOtherLanguages.js new file mode 100644 index 000000000..7486047ca --- /dev/null +++ b/src/UI/RemoveOtherLanguages.js @@ -0,0 +1,31 @@ +let lang = ( + (navigator.languages && navigator.languages[0]) || + navigator.language || + navigator["userLanguage"] || + "en" +).substr(0, 2) + +function filterLangs(maindiv) { + let foundLangs = 0 + for (const child of Array.from(maindiv.children)) { + if (child.attributes.getNamedItem("lang")?.value === lang) { + foundLangs++ + } + } + if (foundLangs === 0) { + lang = "en" + } + for (const child of Array.from(maindiv.children)) { + const childLang = child.attributes.getNamedItem("lang") + if (childLang === undefined) { + continue + } + if (childLang.value === lang) { + continue + } + child.parentElement.removeChild(child) + } +} + +filterLangs(document.getElementById("descriptions-while-loading")) +filterLangs(document.getElementById("default-title")) diff --git a/src/UI/RemoveOtherLanguages.ts b/src/UI/RemoveOtherLanguages.ts deleted file mode 100644 index f321df3e5..000000000 --- a/src/UI/RemoveOtherLanguages.ts +++ /dev/null @@ -1,32 +0,0 @@ -export {} -let lang = ( - (navigator.languages && navigator.languages[0]) || - navigator.language || - navigator["userLanguage"] || - "en" -).substr(0, 2) - -function filterLangs(maindiv: HTMLElement) { - let foundLangs = 0 - for (const child of Array.from(maindiv.children)) { - if (child.attributes.getNamedItem("lang")?.value === lang) { - foundLangs++ - } - } - if (foundLangs === 0) { - lang = "en" - } - for (const child of Array.from(maindiv.children)) { - const childLang = child.attributes.getNamedItem("lang") - if (childLang === undefined) { - continue - } - if (childLang.value === lang) { - continue - } - child.parentElement.removeChild(child) - } -} - -filterLangs(document.getElementById("descriptions-while-loading")) -filterLangs(document.getElementById("default-title")) diff --git a/theme.html b/theme.html index 362218168..3aab458b1 100644 --- a/theme.html +++ b/theme.html @@ -65,7 +65,7 @@
Below
- + From 451aa3bcd428159cc1f364d1eddfedd591d9e5c8 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 29 Sep 2023 11:13:30 +0200 Subject: [PATCH 03/41] Security: add nominatim endpoint to config and csp --- package.json | 3 ++- scripts/generateLayouts.ts | 3 +++ src/Logic/Osm/Geocoding.ts | 3 ++- src/Models/Constants.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 45ede4ec4..3e74f9209 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "https://overpass.kumi.systems/api/interpreter", "https://overpass.openstreetmap.ru/cgi/interpreter" ], - "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country" + "country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country", + "nominatimEndpoint": "https://nominatim.openstreetmap.org/search?" }, "scripts": { "start": "npm run generate:layeroverview && npm run strt", diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 83103f7c3..14793d226 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -206,6 +206,7 @@ function asLangSpan(t: Translation, tag = "span"): string { } let previousSrc: Set = new Set() + function generateCsp( layout: LayoutConfig, options: { @@ -216,6 +217,7 @@ function generateCsp( "'self'", ...Constants.defaultOverpassUrls, Constants.countryCoderEndpoint, + Constants.nominatimEndpoint, "https://api.openstreetmap.org", "https://pietervdvn.goatcounter.com", ].concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) @@ -283,6 +285,7 @@ const removeOtherLanguagesHash = crypto .createHash("sha256") .update(removeOtherLanguages) .digest("base64") + async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] diff --git a/src/Logic/Osm/Geocoding.ts b/src/Logic/Osm/Geocoding.ts index 09da7af6d..d3af5d6a5 100644 --- a/src/Logic/Osm/Geocoding.ts +++ b/src/Logic/Osm/Geocoding.ts @@ -1,5 +1,6 @@ import { Utils } from "../../Utils" import { BBox } from "../BBox" +import Constants from "../../Models/Constants" export interface GeoCodeResult { display_name: string @@ -15,7 +16,7 @@ export interface GeoCodeResult { } export class Geocoding { - private static readonly host = "https://nominatim.openstreetmap.org/search?" + public static readonly host = Constants.nominatimEndpoint static async Search(query: string, bbox: BBox): Promise { const b = bbox ?? BBox.global diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index 35959c4ca..ad68f6979 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -107,7 +107,7 @@ export default class Constants { public static defaultOverpassUrls = Constants.config.default_overpass_urls public static countryCoderEndpoint: string = Constants.config.country_coder_host public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials - + public static nominatimEndpoint: string = Constants.config.nominatimEndpoint /** * These are the values that are allowed to use as 'backdrop' icon for a map pin */ From d103911d143803c9e0373a673110009a785f94da Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 29 Sep 2023 16:17:39 +0200 Subject: [PATCH 04/41] Fix: load translations for PDF-export internally --- scripts/build.sh | 4 +++- src/Utils/svgToPdf.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 4a2fe310d..6649d61ca 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,6 +8,8 @@ rm -rf dist/* rm -rf .cache mkdir dist 2> /dev/null mkdir dist/assets 2> /dev/null +mkdir dist/assets/langs 2> /dev/null +mkdir dist/assets/langs/layers 2> /dev/null export NODE_OPTIONS="--max-old-space-size=8192" @@ -52,5 +54,5 @@ vite build $SRC_MAPS cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ - +cp -r langs/layers/ dist/assets/langs/layers/ export NODE_OPTIONS="" diff --git a/src/Utils/svgToPdf.ts b/src/Utils/svgToPdf.ts index 0ae239992..d591b9f14 100644 --- a/src/Utils/svgToPdf.ts +++ b/src/Utils/svgToPdf.ts @@ -566,6 +566,7 @@ class SvgToPdfPage { images: Record = {} rects: Record = {} readonly options: SvgToPdfOptions + public readonly status: UIEventSource private readonly importedTranslations: Record = {} private readonly layerTranslations: Record> = {} /** @@ -574,7 +575,6 @@ class SvgToPdfPage { */ private readonly _state: UIEventSource private _isPrepared = false - public readonly status: UIEventSource constructor( page: string, @@ -674,7 +674,10 @@ class SvgToPdfPage { public async PrepareLanguage(language: string) { // Always fetch the remote data - it's cached anyway this.layerTranslations[language] = await Utils.downloadJsonCached( - "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" + + window.location.protocol + + "//" + + window.location.host + + "/assets/langs/layers/" + language + ".json", 24 * 60 * 60 * 1000 @@ -995,6 +998,7 @@ export interface PdfTemplateInfo { orientation: "portrait" | "landscape" isPublic: boolean } + export class SvgToPdf { public static readonly templates: Record< "flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3", From 82bbc50f950bf207f10ca7a5892e20a497080bb7 Mon Sep 17 00:00:00 2001 From: paunofu Date: Fri, 29 Sep 2023 11:39:03 +0000 Subject: [PATCH 05/41] Translated using Weblate (Catalan) Currently translated at 87.4% (2778 of 3178 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/ca/ --- langs/layers/ca.json | 578 ++++++++++++++++++++++++------------------- 1 file changed, 324 insertions(+), 254 deletions(-) diff --git a/langs/layers/ca.json b/langs/layers/ca.json index c6888ea26..f59f0ff28 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -35,6 +35,16 @@ "1": { "title": "un mupi" }, + "10": { + "description": "S'utilitza per a cartells publicitaris, rètols de neó, logotips i cartells en entrades institucionals", + "title": "un lletrer" + }, + "11": { + "title": "una escupltura" + }, + "12": { + "title": "una paret pintada" + }, "2": { "title": "un mupi sobre la paret" }, @@ -61,16 +71,6 @@ }, "9": { "title": "un tòtem" - }, - "10": { - "description": "S'utilitza per a cartells publicitaris, rètols de neó, logotips i cartells en entrades institucionals", - "title": "un lletrer" - }, - "11": { - "title": "una escupltura" - }, - "12": { - "title": "una paret pintada" } }, "tagRenderings": { @@ -165,6 +165,9 @@ "1": { "then": "Açò és un tauló d'anunis" }, + "10": { + "then": "Açò és una paret pintada" + }, "2": { "then": "Açò és una columna" }, @@ -188,9 +191,6 @@ }, "9": { "then": "Açò és un tòtem" - }, - "10": { - "then": "Açò és una paret pintada" } }, "question": "Quin tipus d'element publicitari és aquest?", @@ -205,6 +205,9 @@ "1": { "then": "Tauló d'anuncis" }, + "10": { + "then": "Paret Pintada" + }, "2": { "then": "Mupi" }, @@ -228,9 +231,6 @@ }, "9": { "then": "Tòtem" - }, - "10": { - "then": "Paret Pintada" } } } @@ -312,6 +312,15 @@ "1": { "then": "Mural" }, + "10": { + "then": "Azulejo (Rajoles decoratives espanyoles i portugueses)" + }, + "11": { + "then": "Enrajolat" + }, + "12": { + "then": "Tallat a la fusta" + }, "2": { "then": "Pintura" }, @@ -335,15 +344,6 @@ }, "9": { "then": "Relleu" - }, - "10": { - "then": "Azulejo (Rajoles decoratives espanyoles i portugueses)" - }, - "11": { - "then": "Enrajolat" - }, - "12": { - "then": "Tallat a la fusta" } }, "question": "Quin tipus d'obra és aquesta peça?", @@ -1830,6 +1830,27 @@ "1": { "question": "Té un connector
Schuko sense pin de terra (CEE7/4 tipus F)
connector" }, + "10": { + "question": "Té un connector
Tipus 2 amb cable (mennekes)
" + }, + "11": { + "question": "Té un connector
CCS Tesla Supercharger (un tipus2_css de marca)
" + }, + "12": { + "question": "Té un connector
Tesla Supercharger (destination)
" + }, + "13": { + "question": "Té un connector
Tesla Supercharger (Destination) (Tipus 2 amb un cable de marca tesla)
" + }, + "14": { + "question": "Té un connector
USB per a carregar telèfons i dispositius electrònics petits
" + }, + "15": { + "question": "Té un connector
Bosch Active Connect amb 3 pins i cable
" + }, + "16": { + "question": "Té un connector
Bosch Active Connect amb 5 pins i cable
" + }, "2": { "question": "Té un connector
endoll de paret Europeu amb un pin de terra (CEE7/4 tipus F)
" }, @@ -1853,27 +1874,6 @@ }, "9": { "question": "Té un connector
CCS Tipus 2 (mennekes)
" - }, - "10": { - "question": "Té un connector
Tipus 2 amb cable (mennekes)
" - }, - "11": { - "question": "Té un connector
CCS Tesla Supercharger (un tipus2_css de marca)
" - }, - "12": { - "question": "Té un connector
Tesla Supercharger (destination)
" - }, - "13": { - "question": "Té un connector
Tesla Supercharger (Destination) (Tipus 2 amb un cable de marca tesla)
" - }, - "14": { - "question": "Té un connector
USB per a carregar telèfons i dispositius electrònics petits
" - }, - "15": { - "question": "Té un connector
Bosch Active Connect amb 3 pins i cable
" - }, - "16": { - "question": "Té un connector
Bosch Active Connect amb 5 pins i cable
" } } } @@ -1929,30 +1929,6 @@ "1": { "then": "Endoll de paret Schuko sense pin a terra (CEE7/4 tipus F)" }, - "2": { - "then": "Endoll de paret Europeu amb pin de terra (CEE7/4 tipus E)" - }, - "3": { - "then": "Endoll de paret Europeu amb pin a terra (CEE7/4 tipus E)" - }, - "4": { - "then": "Chademo" - }, - "5": { - "then": "Chademo" - }, - "6": { - "then": "Tipus 1 amb cable (J1772)" - }, - "7": { - "then": "Tipus 1 amb cable (J1772)" - }, - "8": { - "then": "Tipus 1 sense cable (J1772)" - }, - "9": { - "then": "Tipus 1 sense cable (J1772)" - }, "10": { "then": "CSS 1Tipus 1 (també conegut com Tipus 1 combo)" }, @@ -1983,6 +1959,9 @@ "19": { "then": "Tipus 2 amb cable (mennekes)" }, + "2": { + "then": "Endoll de paret Europeu amb pin de terra (CEE7/4 tipus E)" + }, "20": { "then": "CSS Supercarregador Tesla (un tipus2_css de la marca)" }, @@ -2013,11 +1992,32 @@ "29": { "then": "Bosch Active Connect amb 3 pins i cable" }, + "3": { + "then": "Endoll de paret Europeu amb pin a terra (CEE7/4 tipus E)" + }, "30": { "then": "Bosch Active Connect amb 5 pins i cable" }, "31": { "then": "Bosch Active Connect amb 5 pins i cable" + }, + "4": { + "then": "Chademo" + }, + "5": { + "then": "Chademo" + }, + "6": { + "then": "Tipus 1 amb cable (J1772)" + }, + "7": { + "then": "Tipus 1 amb cable (J1772)" + }, + "8": { + "then": "Tipus 1 sense cable (J1772)" + }, + "9": { + "then": "Tipus 1 sense cable (J1772)" } }, "question": "Quins tipus de connexions de càrrega estan disponibles aquí?" @@ -2396,10 +2396,25 @@ } }, "tagRenderings": { + "Contained_climbing_routes": { + "render": "

Conté {_contained_climbing_routes_count} rutes

    {_contained_climbing_routes
" + }, "Rock type (crag/rock/cliff only)": { + "mappings": { + "0": { + "then": "Calcària" + } + }, "question": "Quin és el tipus de roca aquí?", "render": "El tipus de roca és {rock}" }, + "Type": { + "mappings": { + "0": { + "then": "Una roca d'escalada: una única roca o penya-segat amb una o unes quantes vies d'escalada que es poden escalar amb seguretat sense corda" + } + } + }, "name": { "render": "{name}" } @@ -2855,6 +2870,9 @@ "1": { "then": "Aquesta via ciclista està pavimentada" }, + "10": { + "then": "Aquesta via ciclista està feta de grava fina" + }, "2": { "then": "Aquesta via ciclista està feta d'asfalt" }, @@ -2878,9 +2896,6 @@ }, "9": { "then": "Aquesta via ciclista està feta de grava" - }, - "10": { - "then": "Aquesta via ciclista està feta de grava fina" } }, "question": "De quina superfície està fet aquesta via ciclista?", @@ -2926,6 +2941,9 @@ "1": { "then": "Aquest carril bici està pavimentat" }, + "10": { + "then": "Aquesta via ciclista està feta de gravilla" + }, "2": { "then": "Aquest carril bici està fet d'asfalt" }, @@ -2937,9 +2955,6 @@ }, "9": { "then": "Aquesta via ciclista està feta de grava" - }, - "10": { - "then": "Aquesta via ciclista està feta de gravilla" } }, "question": "De què està feta la superfície d'aquest carrer?", @@ -3982,6 +3997,21 @@ "1": { "then": "Aquesta estació de fitness té un cartell amb instruccions per a un exercici concret." }, + "10": { + "then": "Aquesta estació de gimnàs té esglaons." + }, + "11": { + "then": "Aquesta estació de fitness disposa de cons per fer salts de granota." + }, + "12": { + "then": "Aquesta estació de fitness té bigues per saltar." + }, + "13": { + "then": "Aquesta estació de fitness té obstacles per a travesar." + }, + "14": { + "then": "Aquesta estació de fitness té una paret per enfilar-se." + }, "2": { "then": "Aquesta estació de fitness té una instal·lació per fer abdominals." }, @@ -4005,21 +4035,6 @@ }, "9": { "then": "Aquesta estació de fitness té llocs per fer exercicis d'eslàlom." - }, - "10": { - "then": "Aquesta estació de gimnàs té esglaons." - }, - "11": { - "then": "Aquesta estació de fitness disposa de cons per fer salts de granota." - }, - "12": { - "then": "Aquesta estació de fitness té bigues per saltar." - }, - "13": { - "then": "Aquesta estació de fitness té obstacles per a travesar." - }, - "14": { - "then": "Aquesta estació de fitness té una paret per enfilar-se." } } } @@ -4138,6 +4153,21 @@ "1": { "then": "Això és una fregiduria" }, + "10": { + "then": "Aquí es serveixen plats xinesos" + }, + "11": { + "then": "Aquí es serveixen plats grecs" + }, + "12": { + "then": "Aquí es serveixen plats indis" + }, + "13": { + "then": "Aquí es serveixen plats turcs" + }, + "14": { + "then": "Aquí es serveixen plats tailandesos" + }, "2": { "then": "Principalment serveix pasta" }, @@ -4161,21 +4191,6 @@ }, "9": { "then": "Aquí es serveixen plats francesos" - }, - "10": { - "then": "Aquí es serveixen plats xinesos" - }, - "11": { - "then": "Aquí es serveixen plats grecs" - }, - "12": { - "then": "Aquí es serveixen plats indis" - }, - "13": { - "then": "Aquí es serveixen plats turcs" - }, - "14": { - "then": "Aquí es serveixen plats tailandesos" } }, "question": "Quin menjar es serveix aquí?", @@ -4424,7 +4439,8 @@ } }, "ghost_bike-start_date": { - "question": "Quan es va instal·lar aquesta bicicleta Ghost?" + "question": "Quan es va instal·lar aquesta bicicleta Ghost?", + "render": "Col·locat el {start_date}" } }, "title": { @@ -5332,6 +5348,19 @@ } } }, + "10": { + "options": { + "0": { + "question": "Totes les notes" + }, + "1": { + "question": "Oculta les notes d'importació" + }, + "2": { + "question": "Mostrar només les notes d'importació" + } + } + }, "2": { "options": { "0": { @@ -5387,19 +5416,6 @@ "question": "Sols mostra les notes obertes" } } - }, - "10": { - "options": { - "0": { - "question": "Totes les notes" - }, - "1": { - "question": "Oculta les notes d'importació" - }, - "2": { - "question": "Mostrar només les notes d'importació" - } - } } }, "name": "Notes d'OpenStreetMap", @@ -5708,6 +5724,12 @@ "1": { "then": "Aquesta és una plaça d'aparcament normal." }, + "10": { + "then": "Es tracta d'una plaça d'aparcament reservada per a pares amb fills." + }, + "11": { + "then": "Es tracta d'una plaça d'aparcament reservada al personal." + }, "2": { "then": "Aquesta és una plaça d'aparcament per a minusvàlids." }, @@ -5725,12 +5747,6 @@ }, "9": { "then": "Es tracta d'una plaça d'aparcament reservada per a motos." - }, - "10": { - "then": "Es tracta d'una plaça d'aparcament reservada per a pares amb fills." - }, - "11": { - "then": "Es tracta d'una plaça d'aparcament reservada al personal." } } } @@ -6294,6 +6310,18 @@ "1": { "then": "S'accepten monedes de 2 cèntims" }, + "10": { + "then": "S'accepten monedes de 20 cèntims" + }, + "12": { + "then": "S'accepten monedes d'1 franc" + }, + "13": { + "then": "S'accepten monedes de 2 francs" + }, + "14": { + "then": "S'accepten monedes de 5 francs" + }, "2": { "then": "S'accepten monedes de 5 cèntims" }, @@ -6317,18 +6345,6 @@ }, "9": { "then": "S'accepten monedes de 10 cèntims" - }, - "10": { - "then": "S'accepten monedes de 20 cèntims" - }, - "12": { - "then": "S'accepten monedes d'1 franc" - }, - "13": { - "then": "S'accepten monedes de 2 francs" - }, - "14": { - "then": "S'accepten monedes de 5 francs" } }, "question": "Quines monedes es poden utilitzar per a pagar aquí?" @@ -6341,6 +6357,15 @@ "1": { "then": "S'accepten bitllets de 10 euros" }, + "10": { + "then": "S'accepten bitllets de 100 francs" + }, + "11": { + "then": "S'accepten bitllets de 200 francs" + }, + "12": { + "then": "S'accepten bitllets de 1000 francs" + }, "2": { "then": "S'accepten bitllets de 20 euros" }, @@ -6364,15 +6389,6 @@ }, "9": { "then": "S'accepten bitllets de 50 francs" - }, - "10": { - "then": "S'accepten bitllets de 100 francs" - }, - "11": { - "then": "S'accepten bitllets de 200 francs" - }, - "12": { - "then": "S'accepten bitllets de 1000 francs" } }, "question": "Amb quins bitllets pot pagar aquí?" @@ -6739,30 +6755,6 @@ "1": { "question": "Reciclatge de piles" }, - "2": { - "question": "Reciclatge de cartrons de begudes" - }, - "3": { - "question": "Reciclatge de llaunes" - }, - "4": { - "question": "Reciclatge de roba" - }, - "5": { - "question": "Reciclatge d'oli de cuina" - }, - "6": { - "question": "Reciclatge d'oli de motor" - }, - "7": { - "question": "Reciclatge de tubs fluorescents" - }, - "8": { - "question": "Reciclatge de residus verds" - }, - "9": { - "question": "Reciclatge d'ampolles de vidre" - }, "10": { "question": "Reciclatge de vidre" }, @@ -6793,11 +6785,35 @@ "19": { "question": "Reciclatge del rebuig" }, + "2": { + "question": "Reciclatge de cartrons de begudes" + }, "20": { "question": "Reciclatge de cartutxos d'impressora" }, "21": { "question": "Reciclatge de bicicletes" + }, + "3": { + "question": "Reciclatge de llaunes" + }, + "4": { + "question": "Reciclatge de roba" + }, + "5": { + "question": "Reciclatge d'oli de cuina" + }, + "6": { + "question": "Reciclatge d'oli de motor" + }, + "7": { + "question": "Reciclatge de tubs fluorescents" + }, + "8": { + "question": "Reciclatge de residus verds" + }, + "9": { + "question": "Reciclatge d'ampolles de vidre" } } }, @@ -6865,30 +6881,6 @@ "1": { "then": "Aquí es poden reciclar els cartons de begudes" }, - "2": { - "then": "Aquí es poden reciclar llaunes" - }, - "3": { - "then": "Aquí es pot reciclar roba" - }, - "4": { - "then": "Aquí es pot reciclar oli de cuina" - }, - "5": { - "then": "Aquí es pot reciclar oli de motor" - }, - "6": { - "then": "Aquí es poden reciclar tub fluroescents" - }, - "7": { - "then": "Aquí es poden reciclar residus verds" - }, - "8": { - "then": "Ací es poden reciclar residus orgànics" - }, - "9": { - "then": "Aquí es poden reciclar ampolles de vidre" - }, "10": { "then": "Aquí es pot reciclar vidre" }, @@ -6919,6 +6911,9 @@ "19": { "then": "Aquí es poden reciclar sabates" }, + "2": { + "then": "Aquí es poden reciclar llaunes" + }, "20": { "then": "Aquí es poden reciclar petits electrodomèstics" }, @@ -6933,6 +6928,27 @@ }, "24": { "then": "Aquí es poden reciclar bicicletes" + }, + "3": { + "then": "Aquí es pot reciclar roba" + }, + "4": { + "then": "Aquí es pot reciclar oli de cuina" + }, + "5": { + "then": "Aquí es pot reciclar oli de motor" + }, + "6": { + "then": "Aquí es poden reciclar tub fluroescents" + }, + "7": { + "then": "Aquí es poden reciclar residus verds" + }, + "8": { + "then": "Ací es poden reciclar residus orgànics" + }, + "9": { + "then": "Aquí es poden reciclar ampolles de vidre" } }, "question": "Què es pot reciclar aquí?" @@ -7451,8 +7467,13 @@ }, "4": { "then": "Aquí es juga al corfbol" + }, + "6": { + "then": "Açò és un skatepark" } - } + }, + "question": "Quin esport es pot practicar aquí?", + "render": "{sport} es juga aquí" }, "sport_pitch-surface": { "mappings": { @@ -7472,14 +7493,28 @@ "then": "La superfície és formigó" } }, - "question": "Quina és la superfície d'aquest camp esportiu?" + "question": "Quina és la superfície d'aquest camp esportiu?", + "render": "La superfícies és {surface}" } }, "title": { "render": "Camp d'esports" } }, + "sports_centre": { + "description": "En aquesta capa es poden trobar centres esportius interiors i exteriors", + "name": "Centres esportius", + "presets": { + "0": { + "title": "un centre esportiu" + } + }, + "title": { + "render": "Centre esportiu" + } + }, "stairs": { + "description": "Capa que mostra escales i escales mecàniques", "name": "Escales", "tagRenderings": { "conveying": { @@ -7638,6 +7673,12 @@ "1": { "then": "Aquest fanal utilitza LED" }, + "10": { + "then": "Aquest fanal utilitza làmpades de sodi d'alta pressió (taronja amb blanc)" + }, + "11": { + "then": "Aquest fanal s'il·lumina amb gas" + }, "2": { "then": "Aquest fanal utilitza il·luminació incandescent" }, @@ -7661,12 +7702,6 @@ }, "9": { "then": "Aquest fanal utilitza làmpades de sodi de baixa pressió (taronja monocroma)" - }, - "10": { - "then": "Aquest fanal utilitza làmpades de sodi d'alta pressió (taronja amb blanc)" - }, - "11": { - "then": "Aquest fanal s'il·lumina amb gas" } }, "question": "Quin tipus d'il·luminació utilitza aquest fanal?" @@ -7746,6 +7781,7 @@ "question": "Quin tipus de càmera és aquesta?" }, "Level": { + "question": "A quina planta es troba aquesta càmera?", "render": "Ubicat a la planta {level}" }, "Operator": { @@ -7852,14 +7888,19 @@ } }, "tertiary_education": { + "name": "Instituts superiors i universitats", "presets": { "0": { + "description": "Un institut on s'imparteix ensenyament terciari (al nivell equivalent de batxillerat o superior). N'hi ha prou amb un únic punt per campus: els edificis i les facultats no s'han de cartografiar amb diferents punts universitaris.", "title": "una universitat" } }, "tagRenderings": { "institution-kind": { "mappings": { + "0": { + "then": "Aquesta és una institució d'educació postsecundària i no terciària. S'ha d'haver completat l'educació secundària per matricular-se aquí, però aquí no s'obtenen títols de batxillerat (o superiors)" + }, "1": { "then": "Açò és una universitat, una institució d'educació terciaria on s'imparteixen carreres universitàries o superior." } @@ -7883,8 +7924,14 @@ }, "title": { "mappings": { + "1": { + "then": "Institut superior" + }, "2": { "then": "Universitat" + }, + "3": { + "then": "Escola que imparteix educació terciària" } } } @@ -8147,10 +8194,22 @@ "question": "Accessible amb cadira de rodes" } } + }, + "2": { + "options": { + "0": { + "question": "Ús gratuït" + } + } } }, "name": "Lavabos a altres instal·lacions", "tagRenderings": { + "opening_hours": { + "override": { + "question": "Quan està oberta la instal·lació on es troben aquests lavabos?" + } + }, "toilet-access": { "mappings": { "0": { @@ -8202,9 +8261,18 @@ "question": "Hi ha un lavabo específic per a usuaris amb cadira de rodes?" }, "wheelchair-door-width": { - "question": "Quina és l'amplada de la porta del lavabo accessible per a cadira de rodes?" + "question": "Quina és l'amplada de la porta del lavabo accessible per a cadira de rodes?", + "render": "La porta del vàter accessible amb cadira de rodes és {canònic(toilets:door:width)} d'ample" } }, + "title": { + "mappings": { + "0": { + "then": "Bany a {name}" + } + }, + "render": "Lavabo a la instal·lació" + }, "units": { "0": { "applicableUnits": { @@ -8290,6 +8358,7 @@ } }, "transit_stops": { + "description": "Capa que mostra diferents tipus de parades de transport públic", "filter": { "0": { "options": { @@ -8422,7 +8491,8 @@ "0": { "then": "Parada {name}" } - } + }, + "render": "Parada de transport públic" } }, "tree_node": { @@ -8767,30 +8837,6 @@ "1": { "question": "Venda de begudes" }, - "2": { - "question": "Venda de llaminadures" - }, - "3": { - "question": "Venda de menjar" - }, - "4": { - "question": "Venda de tabaco" - }, - "5": { - "question": "Venda de preservatius" - }, - "6": { - "question": "Venda de cafè" - }, - "7": { - "question": "Venda d'aigua" - }, - "8": { - "question": "Venda de diaris" - }, - "9": { - "question": "Venda de càmeres interiors de bicicletes" - }, "10": { "question": "Venda de llet" }, @@ -8821,8 +8867,32 @@ "19": { "question": "Venda de bitllets de transport públic" }, + "2": { + "question": "Venda de llaminadures" + }, "20": { "question": "Venda de productes carnis" + }, + "3": { + "question": "Venda de menjar" + }, + "4": { + "question": "Venda de tabaco" + }, + "5": { + "question": "Venda de preservatius" + }, + "6": { + "question": "Venda de cafè" + }, + "7": { + "question": "Venda d'aigua" + }, + "8": { + "question": "Venda de diaris" + }, + "9": { + "question": "Venda de càmeres interiors de bicicletes" } } } @@ -8869,30 +8939,6 @@ "1": { "then": "Es venen llaminadures" }, - "2": { - "then": "Es ven menjar" - }, - "3": { - "then": "Es ven tabaco" - }, - "4": { - "then": "Es venen preservatius" - }, - "5": { - "then": "Es ven cafè" - }, - "6": { - "then": "Es ven aigua" - }, - "7": { - "then": "Es venen diaris" - }, - "8": { - "then": "Es venen càmeres interiors de bicicletes" - }, - "9": { - "then": "Es ven llet" - }, "10": { "then": "Es ven pa" }, @@ -8922,6 +8968,30 @@ }, "19": { "then": "Es venen productes carnis" + }, + "2": { + "then": "Es ven menjar" + }, + "3": { + "then": "Es ven tabaco" + }, + "4": { + "then": "Es venen preservatius" + }, + "5": { + "then": "Es ven cafè" + }, + "6": { + "then": "Es ven aigua" + }, + "7": { + "then": "Es venen diaris" + }, + "8": { + "then": "Es venen càmeres interiors de bicicletes" + }, + "9": { + "then": "Es ven llet" } }, "question": "Que ven aquesta màquina expenedora?", @@ -9258,4 +9328,4 @@ } } } -} \ No newline at end of file +} From 245f94d3dcced3661e7d477efa8053ebca84c86d Mon Sep 17 00:00:00 2001 From: paunofu Date: Fri, 29 Sep 2023 11:33:48 +0000 Subject: [PATCH 06/41] Translated using Weblate (Spanish) Currently translated at 44.4% (1412 of 3178 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/es/ --- langs/layers/es.json | 295 +++++++++++++++++++++---------------------- 1 file changed, 145 insertions(+), 150 deletions(-) diff --git a/langs/layers/es.json b/langs/layers/es.json index d2c209405..a130bdee2 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -35,6 +35,16 @@ "1": { "title": "un mupi" }, + "10": { + "description": "Se utiliza para carteles publicitarios, letreros de neón, logotipos y carteles en entradas institucionales", + "title": "un lletrer" + }, + "11": { + "title": "una escultura" + }, + "12": { + "title": "una pared pintada" + }, "2": { "title": "un mupi sobre la pared" }, @@ -61,16 +71,6 @@ }, "9": { "title": "un tótem" - }, - "10": { - "description": "Se utiliza para carteles publicitarios, letreros de neón, logotipos y carteles en entradas institucionales", - "title": "un lletrer" - }, - "11": { - "title": "una escultura" - }, - "12": { - "title": "una pared pintada" } }, "tagRenderings": { @@ -165,6 +165,9 @@ "1": { "then": "Esto es un tablón de anuncios" }, + "10": { + "then": "Esto es una pared pintada" + }, "2": { "then": "Esto es una columna" }, @@ -188,9 +191,6 @@ }, "9": { "then": "Esto es un tótem" - }, - "10": { - "then": "Esto es una pared pintada" } }, "question": "¿Qué tipo de elemento publicitario es?", @@ -205,6 +205,9 @@ "1": { "then": "Tablon de anuncios" }, + "10": { + "then": "Pared Pintada" + }, "2": { "then": "Mupi" }, @@ -228,9 +231,6 @@ }, "9": { "then": "Tótem" - }, - "10": { - "then": "Pared Pintada" } } } @@ -312,6 +312,15 @@ "1": { "then": "Mural" }, + "10": { + "then": "Azulejo (Baldosas decorativas Españolas y Portuguesas)" + }, + "11": { + "then": "Cerámica" + }, + "12": { + "then": "Tallado en madera" + }, "2": { "then": "Pintura" }, @@ -335,15 +344,6 @@ }, "9": { "then": "Relieve" - }, - "10": { - "then": "Azulejo (Baldosas decorativas Españolas y Portuguesas)" - }, - "11": { - "then": "Cerámica" - }, - "12": { - "then": "Tallado en madera" } }, "question": "¿Qué tipo de obra es esta pieza?", @@ -1440,6 +1440,27 @@ "0": { "question": "Todos los conectores" }, + "10": { + "question": "Tiene un conector
Tipo 2 con cable (mennekes)
" + }, + "11": { + "question": "Tiene un conector
Tesla Supercharger CCS (un tipo2_css de marca)
" + }, + "12": { + "question": "Tiene un conector
Tesla Supercharger (destination)
" + }, + "13": { + "question": "Tiene un conector
Tesla Supercharger (Destination) (Tipo2 A con un cable de marca tesla)
" + }, + "14": { + "question": "Tiene un conector
USB para cargar teléfonos y dispositivos electrónicos pequeños
" + }, + "15": { + "question": "Tiene un conector
Bosch Active Connect con 3 pines y cable
" + }, + "16": { + "question": "Tiene un conector
Bosch Active Connect con 5 pines y cable
" + }, "2": { "question": "Tiene un conector
enchufe de pared Europeo con un pin de tierra (CEE7/4 tipo E)
" }, @@ -1463,27 +1484,6 @@ }, "9": { "question": "Tiene un conector
Tipo 2 CCS (mennekes)
" - }, - "10": { - "question": "Tiene un conector
Tipo 2 con cable (mennekes)
" - }, - "11": { - "question": "Tiene un conector
Tesla Supercharger CCS (un tipo2_css de marca)
" - }, - "12": { - "question": "Tiene un conector
Tesla Supercharger (destination)
" - }, - "13": { - "question": "Tiene un conector
Tesla Supercharger (Destination) (Tipo2 A con un cable de marca tesla)
" - }, - "14": { - "question": "Tiene un conector
USB para cargar teléfonos y dispositivos electrónicos pequeños
" - }, - "15": { - "question": "Tiene un conector
Bosch Active Connect con 3 pines y cable
" - }, - "16": { - "question": "Tiene un conector
Bosch Active Connect con 5 pines y cable
" } } } @@ -1538,30 +1538,6 @@ "1": { "then": "Enchufe de pared Schuko sin pin de tierra (CEE7/4 tipo F)" }, - "2": { - "then": "Enchufe de pared Europeo con pin de tierra (CEE7/4 tipo E)" - }, - "3": { - "then": "Enchufe de pared Europeo con pin de tierra (CEE7/4 tipo E)" - }, - "4": { - "then": "Chademo" - }, - "5": { - "then": "Chademo" - }, - "6": { - "then": "Tipo 1 con cable (J1772)" - }, - "7": { - "then": "Tipo 1 con cable (J1772)" - }, - "8": { - "then": "Tipo 1 sin cable (J1772)" - }, - "9": { - "then": "Tipo 1 sin cable (J1772)" - }, "10": { "then": "CSS Tipo 1 (también conocido como Tipo 1 Combo)" }, @@ -1592,6 +1568,9 @@ "19": { "then": "Tipo 2 con cable (mennekes)" }, + "2": { + "then": "Enchufe de pared Europeo con pin de tierra (CEE7/4 tipo E)" + }, "20": { "then": "CCS Supercargador Tesla (un tipo2_css con marca)" }, @@ -1622,11 +1601,32 @@ "29": { "then": "Bosch Active Connect con 3 pines y cable" }, + "3": { + "then": "Enchufe de pared Europeo con pin de tierra (CEE7/4 tipo E)" + }, "30": { "then": "Bosch Active Connect con 5 pines y cable" }, "31": { "then": "Bosch Active Connect con 5 pines y cable" + }, + "4": { + "then": "Chademo" + }, + "5": { + "then": "Chademo" + }, + "6": { + "then": "Tipo 1 con cable (J1772)" + }, + "7": { + "then": "Tipo 1 con cable (J1772)" + }, + "8": { + "then": "Tipo 1 sin cable (J1772)" + }, + "9": { + "then": "Tipo 1 sin cable (J1772)" } }, "question": "¿Qué tipo de conexiones de carga están disponibles aquí?" @@ -2021,6 +2021,12 @@ "1": { "then": "Este carril bici está pavimentado" }, + "10": { + "then": "Este carril bici está hecho de gravilla" + }, + "12": { + "then": "Este carril bici está hecho de tierra natural" + }, "2": { "then": "Este carril bici está hecho de asfalto" }, @@ -2035,12 +2041,6 @@ }, "9": { "then": "Este carril bici está hecho de grava" - }, - "10": { - "then": "Este carril bici está hecho de gravilla" - }, - "12": { - "then": "Este carril bici está hecho de tierra natural" } }, "question": "¿De qué superficie está hecho este carril bici?", @@ -2086,6 +2086,9 @@ "1": { "then": "Este carril bici está pavimentado" }, + "10": { + "then": "Este carril bici está hecho de gravilla" + }, "2": { "then": "Este carril bici está hecho de asfalto" }, @@ -2097,9 +2100,6 @@ }, "9": { "then": "Este carril bici está hecho de grava" - }, - "10": { - "then": "Este carril bici está hecho de gravilla" } }, "question": "¿De qué esta hecha la superficie de esta calle?", @@ -2710,6 +2710,18 @@ "0": { "then": "Esto es una pizzería" }, + "10": { + "then": "Aquí se sirven platos Chinos" + }, + "11": { + "then": "Aquí se sirven platos Griegos" + }, + "12": { + "then": "Aquí se sirven platos Indios" + }, + "13": { + "then": "Aquí se sirven platos Turcos" + }, "2": { "then": "Principalmente sirve pasta" }, @@ -2730,18 +2742,6 @@ }, "9": { "then": "Aquí se sirven platos Franceses" - }, - "10": { - "then": "Aquí se sirven platos Chinos" - }, - "11": { - "then": "Aquí se sirven platos Griegos" - }, - "12": { - "then": "Aquí se sirven platos Indios" - }, - "13": { - "then": "Aquí se sirven platos Turcos" } }, "question": "¿Qué comida se sirve aquí?", @@ -3139,6 +3139,19 @@ } } }, + "10": { + "options": { + "0": { + "question": "Todas las notas" + }, + "1": { + "question": "Ocultar las notas de importación" + }, + "2": { + "question": "Solo mostrar las notas de importación" + } + } + }, "2": { "options": { "0": { @@ -3194,19 +3207,6 @@ "question": "Solo mostrar las notas abiertas" } } - }, - "10": { - "options": { - "0": { - "question": "Todas las notas" - }, - "1": { - "question": "Ocultar las notas de importación" - }, - "2": { - "question": "Solo mostrar las notas de importación" - } - } } }, "name": "Notas de OpenStreetMap", @@ -3822,21 +3822,6 @@ "1": { "question": "Reciclaje de baterías" }, - "3": { - "question": "Reciclaje de latas" - }, - "4": { - "question": "Reciclaje de ropa" - }, - "5": { - "question": "Reciclaje de aceite de cocina" - }, - "6": { - "question": "Reciclaje de aceite de motor" - }, - "9": { - "question": "Reciclaje de botellas de cristal" - }, "10": { "question": "Reciclaje de cristal" }, @@ -3860,6 +3845,21 @@ }, "18": { "question": "Reciclaje de pequeños electrodomésticos" + }, + "3": { + "question": "Reciclaje de latas" + }, + "4": { + "question": "Reciclaje de ropa" + }, + "5": { + "question": "Reciclaje de aceite de cocina" + }, + "6": { + "question": "Reciclaje de aceite de motor" + }, + "9": { + "question": "Reciclaje de botellas de cristal" } } } @@ -3902,24 +3902,6 @@ "0": { "then": "Aquí se pueden reciclar baterías" }, - "2": { - "then": "Aquí se pueden reciclar latas" - }, - "3": { - "then": "Aquí se puede reciclar ropa" - }, - "4": { - "then": "Aquí se puede reciclar aceite de cocina" - }, - "5": { - "then": "Aquí se puede reciclar aceite de motor" - }, - "8": { - "then": "Aquí se pueden reciclar residuos orgánicos" - }, - "9": { - "then": "Aquí se pueden reciclar botellas de cristal" - }, "10": { "then": "Aquí se puede reciclar cristal" }, @@ -3943,6 +3925,24 @@ }, "19": { "then": "Aquí se pueden reciclar zapatos" + }, + "2": { + "then": "Aquí se pueden reciclar latas" + }, + "3": { + "then": "Aquí se puede reciclar ropa" + }, + "4": { + "then": "Aquí se puede reciclar aceite de cocina" + }, + "5": { + "then": "Aquí se puede reciclar aceite de motor" + }, + "8": { + "then": "Aquí se pueden reciclar residuos orgánicos" + }, + "9": { + "then": "Aquí se pueden reciclar botellas de cristal" } }, "question": "¿Qué se puede reciclar aquí?" @@ -4193,7 +4193,7 @@ "then": "Aquí se juega al baloncesto" } }, - "question": "¿Qué deporte se practica aquí?", + "question": "¿Qué deporte se puede practicar aquí?", "render": "Aquí se juega al {sport}" }, "sport_pitch-surface": { @@ -4246,11 +4246,6 @@ "question": "¿De qué color es la luz que emite esta lámpara?", "render": "Esta lámpara emite luz {light:colour}" }, - "count": { - "mappings": { - "0": {} - } - }, "direction": { "question": "¿Hacia donde apunta esta lámpara?", "render": "Esta lámpara apunta hacia {light:direction}" @@ -4291,6 +4286,12 @@ "1": { "then": "Esta lámpara utiliza LEDs" }, + "10": { + "then": "Esta lámpara utiliza lámparas de sodio de alta presión (naranja con blanco)" + }, + "11": { + "then": "Esta lampara se ilumina con gas" + }, "2": { "then": "Esta lámpara utiliza iluminación incandescente" }, @@ -4311,12 +4312,6 @@ }, "9": { "then": "Esta lámpara utiliza lámparas de sodio de baja presión (naranja monocromo)" - }, - "10": { - "then": "Esta lámpara utiliza lámparas de sodio de alta presión (naranja con blanco)" - }, - "11": { - "then": "Esta lampara se ilumina con gas" } }, "question": "¿Qué tipo de iluminación utiliza esta lámpara?" @@ -4891,4 +4886,4 @@ } } } -} \ No newline at end of file +} From 6d33e4f32518cd5ba63df5b30a66b0388c88d90f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 30 Sep 2023 15:30:58 +0200 Subject: [PATCH 07/41] Chore: add .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a9dcf16ad..c5a6e0be2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ service-worker.js *.vsix public/*.webmanifest public/assets/generated/ +public/assets/langs/* From 419bfd416c58ce7e3bd679da117ff55f46b43ee5 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 30 Sep 2023 15:44:43 +0200 Subject: [PATCH 08/41] Chore: housekeeping, new docs --- Docs/BuiltinIndex.md | 47 +- Docs/Layers/all_vending_machine.md | 24 +- Docs/Layers/bicycle_tube_vending_machine.md | 43 +- Docs/Layers/indoors.md | 281 ++++++ Docs/Layers/toilet.md | 24 + Docs/Layers/toilet_at_amenity.md | 10 + Docs/Layers/vending_machine.md | 24 +- Docs/Layers/waste_basket.md | 3 +- Docs/TagInfo/mapcomplete_cyclofix.json | 71 +- Docs/TagInfo/mapcomplete_indoors.json | 180 ++++ Docs/TagInfo/mapcomplete_personal.json | 285 +++++- Docs/TagInfo/mapcomplete_vending_machine.json | 29 +- Docs/TagInfo/mapcomplete_waste.json | 5 + Docs/TagInfo/mapcomplete_waste_basket.json | 5 + src/UI/Map/ShowDataLayer.ts | 97 +- src/Utils.ts | 896 +++++++++--------- src/assets/contributors.json | 18 +- src/assets/translators.json | 4 +- 18 files changed, 1443 insertions(+), 603 deletions(-) diff --git a/Docs/BuiltinIndex.md b/Docs/BuiltinIndex.md index 75a5ccafb..5e79db2e1 100644 --- a/Docs/BuiltinIndex.md +++ b/Docs/BuiltinIndex.md @@ -21,6 +21,7 @@ + [description](#description) + [payment-options](#payment-options) + [payment-options-advanced](#payment-options-advanced) + + [payment-options-split](#payment-options-split) + [opening_hours_24_7](#opening_hours_24_7) + [level](#level) + [bicycle_rental.*bicycle_rental](#bicycle_rental*bicycle_rental) @@ -46,7 +47,6 @@ + [opening_hours_by_appointment](#opening_hours_by_appointment) + [multilevels](#multilevels) + [induction-loop](#induction-loop) - + [payment-options-split](#payment-options-split) + [denominations-coins](#denominations-coins) + [check_date](#check_date) + [all_tags](#all_tags) @@ -56,6 +56,7 @@ + [mastodon](#mastodon) + [contact](#contact) + [etymology.wikipedia-etymology](#etymologywikipedia-etymology) + + [toilet.relevant-questions](#toiletrelevant-questions) + [denominations-notes](#denominations-notes) + [single_level](#single_level) + [survey_date](#survey_date) @@ -410,6 +411,23 @@ +### payment-options-split + + + + + + - bicycle_tube_vending_machine + - elongated_coin + - parking_ticket_machine + - shower + - ticket_machine + - toilet + - vending_machine + + + + ### opening_hours_24_7 @@ -753,22 +771,6 @@ -### payment-options-split - - - - - - - elongated_coin - - parking_ticket_machine - - shower - - ticket_machine - - toilet - - vending_machine - - - - ### denominations-coins @@ -866,6 +868,17 @@ + - indoors + + + + +### toilet.relevant-questions + + + + + - indoors diff --git a/Docs/Layers/all_vending_machine.md b/Docs/Layers/all_vending_machine.md index 469cc7e90..a09962892 100644 --- a/Docs/Layers/all_vending_machine.md +++ b/Docs/Layers/all_vending_machine.md @@ -47,7 +47,7 @@ this quick overview is incomplete attribute | type | values which are supported by this layer ----------- | ------ | ------------------------------------------ [](https://taginfo.openstreetmap.org/keys/id#values) [id](https://wiki.openstreetmap.org/wiki/Key:id) | Multiple choice | -[](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | [string](../SpecialInputElements.md#string) | [drinks](https://wiki.openstreetmap.org/wiki/Tag:vending%3Ddrinks) [sweets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dsweets) [food](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dfood) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcigarettes) [condoms](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcondoms) [coffee](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcoffee) [water](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dwater) [newspapers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dnewspapers) [bicycle_tube](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_tube) [milk](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmilk) [bread](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbread) [eggs](https://wiki.openstreetmap.org/wiki/Tag:vending%3Deggs) [cheese](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcheese) [honey](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dhoney) [potatoes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpotatoes) [flowers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dflowers) [parking_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dparking_tickets) [elongated_coin](https://wiki.openstreetmap.org/wiki/Tag:vending%3Delongated_coin) [public_transport_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpublic_transport_tickets) [meat](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmeat) +[](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | [string](../SpecialInputElements.md#string) | [drinks](https://wiki.openstreetmap.org/wiki/Tag:vending%3Ddrinks) [sweets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dsweets) [food](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dfood) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcigarettes) [condoms](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcondoms) [coffee](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcoffee) [water](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dwater) [newspapers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dnewspapers) [bicycle_tube](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_tube) [milk](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmilk) [bread](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbread) [eggs](https://wiki.openstreetmap.org/wiki/Tag:vending%3Deggs) [cheese](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcheese) [honey](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dhoney) [potatoes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpotatoes) [meat](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmeat) [flowers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dflowers) [parking_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dparking_tickets) [elongated_coin](https://wiki.openstreetmap.org/wiki/Tag:vending%3Delongated_coin) [public_transport_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpublic_transport_tickets) [bicycle_light](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_light) [gloves](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dgloves) [bicycle_repair_kit](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_repair_kit) [bicycle_pump](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_pump) [bicycle_lock](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_lock) [](https://taginfo.openstreetmap.org/keys/opening_hours#values) [opening_hours](https://wiki.openstreetmap.org/wiki/Key:opening_hours) | [opening_hours](../SpecialInputElements.md#opening_hours) | [24/7](https://wiki.openstreetmap.org/wiki/Tag:opening_hours%3D24/7) [](https://taginfo.openstreetmap.org/keys/payment:coins:denominations#values) [payment:coins:denominations](https://wiki.openstreetmap.org/wiki/Key:payment:coins:denominations) | Multiple choice | [0.01 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.01 EUR) [0.02 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.02 EUR) [0.05 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.05 EUR) [0.10 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.10 EUR) [0.20 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.20 EUR) [0.50 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.50 EUR) [1 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D1 EUR) [2 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D2 EUR) [0.05 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.05 CHF) [0.10 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.10 CHF) [0.20 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.20 CHF) [0.50 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.50 CHF) [1 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D1 CHF) [2 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D2 CHF) [5 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D5 CHF) [](https://taginfo.openstreetmap.org/keys/payment:notes:denominations#values) [payment:notes:denominations](https://wiki.openstreetmap.org/wiki/Key:payment:notes:denominations) | Multiple choice | [5 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D5 EUR) [10 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D10 EUR) [20 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D20 EUR) [50 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D50 EUR) [100 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D100 EUR) [200 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D200 EUR) [500 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D500 EUR) [10 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D10 CHF) [20 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D20 CHF) [50 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D50 CHF) [100 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D100 CHF) [200 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D200 CHF) [1000 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D1000 CHF) @@ -119,11 +119,16 @@ This is rendered with `This vending machine sells {vending}` - *Cheese is sold* corresponds with `vending=cheese` - *Honey is sold* corresponds with `vending=honey` - *Potatoes are sold* corresponds with `vending=potatoes` + - *Meat is sold* corresponds with `vending=meat` - *Flowers are sold* corresponds with `vending=flowers` - *Parking tickets are sold* corresponds with `vending=parking_tickets` - *Pressed pennies are sold* corresponds with `vending=elongated_coin` - *Public transport tickets are sold* corresponds with `vending=public_transport_tickets` - - *Meat products are being sold* corresponds with `vending=meat` + - *Bicycle lights are sold* corresponds with `vending=bicycle_light` + - *Gloves are sold* corresponds with `vending=gloves` + - *Bicycle repair kits are sold* corresponds with `vending=bicycle_repair_kit` + - *Bicycle pumps are sold* corresponds with `vending=bicycle_pump` + - *Bicycle locks are sold* corresponds with `vending=bicycle_lock` @@ -429,11 +434,16 @@ vending.12 | Sale of eggs | vending~^(.*eggs.*)$ vending.13 | Sale of cheese | vending~^(.*cheese.*)$ vending.14 | Sale of honey | vending~^(.*honey.*)$ vending.15 | Sale of potatoes | vending~^(.*potatoes.*)$ -vending.16 | Sale of flowers | vending~^(.*flowers.*)$ -vending.17 | Sale of parking | vending~^(.*parking_tickets.*)$ -vending.18 | Sale of pressed pennies | vending=elongated_coin -vending.19 | Sale of public transport tickets | vending~^(.*public_transport_tickets.*)$ -vending.20 | Sale of meat products | vending=meat +vending.16 | Sale of meat | vending~^(.*meat.*)$ +vending.17 | Sale of flowers | vending~^(.*flowers.*)$ +vending.18 | Sale of parking tickets | vending~^(.*parking_tickets.*)$ +vending.19 | Sale of pressed pennies | vending=elongated_coin +vending.20 | Sale of public transport tickets | vending~^(.*public_transport_tickets.*)$ +vending.21 | Sale of bicycle lights | vending=bicycle_light +vending.22 | Sale of gloves | vending=gloves +vending.23 | Sale of bicycle repair kits | vending=bicycle_repair_kit +vending.24 | Sale of bicycle pumps | vending=bicycle_pump +vending.25 | Sale of bicycle locks | vending=bicycle_lock This document is autogenerated from [assets/themes/vending_machine/vending_machine.json](https://github.com/pietervdvn/MapComplete/blob/develop/assets/themes/vending_machine/vending_machine.json) diff --git a/Docs/Layers/bicycle_tube_vending_machine.md b/Docs/Layers/bicycle_tube_vending_machine.md index 5a127bab0..2859ae344 100644 --- a/Docs/Layers/bicycle_tube_vending_machine.md +++ b/Docs/Layers/bicycle_tube_vending_machine.md @@ -51,6 +51,7 @@ attribute | type | values which are supported by this layer [](https://taginfo.openstreetmap.org/keys/charge#values) [charge](https://wiki.openstreetmap.org/wiki/Key:charge) | [string](../SpecialInputElements.md#string) | [](https://taginfo.openstreetmap.org/keys/brand#values) [brand](https://wiki.openstreetmap.org/wiki/Key:brand) | [string](../SpecialInputElements.md#string) | [Continental](https://wiki.openstreetmap.org/wiki/Tag:brand%3DContinental) [Schwalbe](https://wiki.openstreetmap.org/wiki/Tag:brand%3DSchwalbe) [](https://taginfo.openstreetmap.org/keys/operator#values) [operator](https://wiki.openstreetmap.org/wiki/Key:operator) | [string](../SpecialInputElements.md#string) | [Schwalbe](https://wiki.openstreetmap.org/wiki/Tag:operator%3DSchwalbe) [Continental](https://wiki.openstreetmap.org/wiki/Tag:operator%3DContinental) +[](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | Multiple choice | [bicycle_tube](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_tube) [bicycle_light](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_light) [gloves](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dgloves) [bicycle_repair_kit](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_repair_kit) [bicycle_pump](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_pump) [bicycle_lock](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_lock) @@ -121,22 +122,32 @@ This is rendered with `A bicycle tube costs {charge}` -### vending-machine-payment-methods +### payment-options-split -The question is *How can one pay at this tube vending machine?* +The question is *Which methods of payment are accepted here?* - - *Payment with coins is possible* corresponds with `payment:coins=yes` + - *Cash is accepted here* corresponds with `payment:cash=yes` + - This option cannot be chosen as answer + - Unselecting this answer will add + - *Payment cards are accepted here* corresponds with `payment:cards=yes` + - This option cannot be chosen as answer + - Unselecting this answer will add + - *Payment by QR-code is possible here* corresponds with `payment:qr_code=yes` + - Unselecting this answer will add payment:qr_code=no + - *Coins are accepted here* corresponds with `payment:coins=yes` - Unselecting this answer will add payment:coins=no - - *Payment with notes is possible* corresponds with `payment:notes=yes` + - *Bank notes are accepted here* corresponds with `payment:notes=yes` - Unselecting this answer will add payment:notes=no - - *Payment with cards is possible* corresponds with `payment:cards=yes` - - Unselecting this answer will add payment:cards=no + - *Debit cards are accepted here* corresponds with `payment:debit_cards=yes` + - Unselecting this answer will add payment:debit_cards=no + - *Credit cards are accepted here* corresponds with `payment:credit_cards=yes` + - Unselecting this answer will add payment:credit_cards=no @@ -181,26 +192,22 @@ This is rendered with `This vending machine is maintained by {operator}` -### bicycle_tube_vending_maching-other-items +### other-items-vending -The question is *Are other bicycle bicycle accessories sold here?* +The question is *Are other biycle accessories sold here?* - - *Bicycle lights are sold here* corresponds with `vending:bicycle_light=yes` - - Unselecting this answer will add vending:bicycle_light=no - - *Gloves are sold here* corresponds with `vending:gloves=yes` - - Unselecting this answer will add vending:gloves=no - - *Bicycle repair kits are sold here* corresponds with `vending:bicycle_repair_kit=yes` - - Unselecting this answer will add vending:bicycle_repair_kit=no - - *Bicycle pumps are sold here* corresponds with `vending:bicycle_pump=yes` - - Unselecting this answer will add vending:bicycle_pump=no - - *Bicycle locks are sold here* corresponds with `vending:bicycle_lock=yes` - - Unselecting this answer will add vending:bicycle_lock=no + - *Bicycle inner tubes are sold here* corresponds with `vending=bicycle_tube` + - *Bicycle lights are sold here* corresponds with `vending=bicycle_light` + - *Gloves are sold here* corresponds with `vending=gloves` + - *Bicycle repair kits are sold here* corresponds with `vending=bicycle_repair_kit` + - *Bicycle pumps are sold here* corresponds with `vending=bicycle_pump` + - *Bicycle locks are sold here* corresponds with `vending=bicycle_lock` diff --git a/Docs/Layers/indoors.md b/Docs/Layers/indoors.md index 25feafe57..76a0e44b8 100644 --- a/Docs/Layers/indoors.md +++ b/Docs/Layers/indoors.md @@ -55,6 +55,17 @@ attribute | type | values which are supported by this layer [](https://taginfo.openstreetmap.org/keys/room#values) [room](https://wiki.openstreetmap.org/wiki/Key:room) | Multiple choice | [administration](https://wiki.openstreetmap.org/wiki/Tag:room%3Dadministration) [auditorium](https://wiki.openstreetmap.org/wiki/Tag:room%3Dauditorium) [bedroom](https://wiki.openstreetmap.org/wiki/Tag:room%3Dbedroom) [chapel](https://wiki.openstreetmap.org/wiki/Tag:room%3Dchapel) [class](https://wiki.openstreetmap.org/wiki/Tag:room%3Dclass) [computer](https://wiki.openstreetmap.org/wiki/Tag:room%3Dcomputer) [conference](https://wiki.openstreetmap.org/wiki/Tag:room%3Dconference) [crypt](https://wiki.openstreetmap.org/wiki/Tag:room%3Dcrypt) [kitchen](https://wiki.openstreetmap.org/wiki/Tag:room%3Dkitchen) [laboratory](https://wiki.openstreetmap.org/wiki/Tag:room%3Dlaboratory) [library](https://wiki.openstreetmap.org/wiki/Tag:room%3Dlibrary) [locker](https://wiki.openstreetmap.org/wiki/Tag:room%3Dlocker) [nursery](https://wiki.openstreetmap.org/wiki/Tag:room%3Dnursery) [office](https://wiki.openstreetmap.org/wiki/Tag:room%3Doffice) [prison_cell](https://wiki.openstreetmap.org/wiki/Tag:room%3Dprison_cell) [restaurant](https://wiki.openstreetmap.org/wiki/Tag:room%3Drestaurant) [security_check](https://wiki.openstreetmap.org/wiki/Tag:room%3Dsecurity_check) [sport](https://wiki.openstreetmap.org/wiki/Tag:room%3Dsport) [storage](https://wiki.openstreetmap.org/wiki/Tag:room%3Dstorage) [technical](https://wiki.openstreetmap.org/wiki/Tag:room%3Dtechnical) [toilets](https://wiki.openstreetmap.org/wiki/Tag:room%3Dtoilets) [waiting](https://wiki.openstreetmap.org/wiki/Tag:room%3Dwaiting) [](https://taginfo.openstreetmap.org/keys/capacity#values) [capacity](https://wiki.openstreetmap.org/wiki/Key:capacity) | [pnat](../SpecialInputElements.md#pnat) | [](https://taginfo.openstreetmap.org/keys/name:etymology:wikidata#values) [name:etymology:wikidata](https://wiki.openstreetmap.org/wiki/Key:name:etymology:wikidata) | [wikidata](../SpecialInputElements.md#wikidata) | +[](https://taginfo.openstreetmap.org/keys/access#values) [access](https://wiki.openstreetmap.org/wiki/Key:access) | [string](../SpecialInputElements.md#string) | [yes](https://wiki.openstreetmap.org/wiki/Tag:access%3Dyes) [customers](https://wiki.openstreetmap.org/wiki/Tag:access%3Dcustomers) [no](https://wiki.openstreetmap.org/wiki/Tag:access%3Dno) [key](https://wiki.openstreetmap.org/wiki/Tag:access%3Dkey) +[](https://taginfo.openstreetmap.org/keys/fee#values) [fee](https://wiki.openstreetmap.org/wiki/Key:fee) | Multiple choice | [yes](https://wiki.openstreetmap.org/wiki/Tag:fee%3Dyes) [no](https://wiki.openstreetmap.org/wiki/Tag:fee%3Dno) +[](https://taginfo.openstreetmap.org/keys/charge#values) [charge](https://wiki.openstreetmap.org/wiki/Key:charge) | [string](../SpecialInputElements.md#string) | +[](https://taginfo.openstreetmap.org/keys/opening_hours#values) [opening_hours](https://wiki.openstreetmap.org/wiki/Key:opening_hours) | [opening_hours](../SpecialInputElements.md#opening_hours) | [24/7](https://wiki.openstreetmap.org/wiki/Tag:opening_hours%3D24/7) +[](https://taginfo.openstreetmap.org/keys/wheelchair#values) [wheelchair](https://wiki.openstreetmap.org/wiki/Key:wheelchair) | Multiple choice | [yes](https://wiki.openstreetmap.org/wiki/Tag:wheelchair%3Dyes) [no](https://wiki.openstreetmap.org/wiki/Tag:wheelchair%3Dno) [designated](https://wiki.openstreetmap.org/wiki/Tag:wheelchair%3Ddesignated) +[](https://taginfo.openstreetmap.org/keys/door:width#values) [door:width](https://wiki.openstreetmap.org/wiki/Key:door:width) | [pfloat](../SpecialInputElements.md#pfloat) | +[](https://taginfo.openstreetmap.org/keys/toilets:position#values) [toilets:position](https://wiki.openstreetmap.org/wiki/Key:toilets:position) | Multiple choice | [seated](https://wiki.openstreetmap.org/wiki/Tag:toilets:position%3Dseated) [urinal](https://wiki.openstreetmap.org/wiki/Tag:toilets:position%3Durinal) [squat](https://wiki.openstreetmap.org/wiki/Tag:toilets:position%3Dsquat) [seated;urinal](https://wiki.openstreetmap.org/wiki/Tag:toilets:position%3Dseated;urinal) +[](https://taginfo.openstreetmap.org/keys/changing_table#values) [changing_table](https://wiki.openstreetmap.org/wiki/Key:changing_table) | Multiple choice | [yes](https://wiki.openstreetmap.org/wiki/Tag:changing_table%3Dyes) [no](https://wiki.openstreetmap.org/wiki/Tag:changing_table%3Dno) +[](https://taginfo.openstreetmap.org/keys/changing_table:location#values) [changing_table:location](https://wiki.openstreetmap.org/wiki/Key:changing_table:location) | [string](../SpecialInputElements.md#string) | [female_toilet](https://wiki.openstreetmap.org/wiki/Tag:changing_table:location%3Dfemale_toilet) [male_toilet](https://wiki.openstreetmap.org/wiki/Tag:changing_table:location%3Dmale_toilet) [wheelchair_toilet](https://wiki.openstreetmap.org/wiki/Tag:changing_table:location%3Dwheelchair_toilet) [dedicated_room](https://wiki.openstreetmap.org/wiki/Tag:changing_table:location%3Ddedicated_room) +[](https://taginfo.openstreetmap.org/keys/toilets:handwashing#values) [toilets:handwashing](https://wiki.openstreetmap.org/wiki/Key:toilets:handwashing) | Multiple choice | [yes](https://wiki.openstreetmap.org/wiki/Tag:toilets:handwashing%3Dyes) [no](https://wiki.openstreetmap.org/wiki/Tag:toilets:handwashing%3Dno) +[](https://taginfo.openstreetmap.org/keys/toilets:paper_supplied#values) [toilets:paper_supplied](https://wiki.openstreetmap.org/wiki/Key:toilets:paper_supplied) | Multiple choice | [yes](https://wiki.openstreetmap.org/wiki/Tag:toilets:paper_supplied%3Dyes) [no](https://wiki.openstreetmap.org/wiki/Tag:toilets:paper_supplied%3Dno) @@ -231,6 +242,276 @@ This is rendered with `

Wikipedia article of the name giver

{wikipedia(n +### toilet-access + + + +The question is *Are these toilets publicly accessible?* + +This rendering asks information about the property [access](https://wiki.openstreetmap.org/wiki/Key:access) + +This is rendered with `Access is {access}` + + + + + + - *Public access* corresponds with `access=yes` + - *Only access to customers* corresponds with `access=customers` + - *Not accessible* corresponds with `access=no` + - *Accessible, but one has to ask a key to enter* corresponds with `access=key` + - *Public access* corresponds with `access=public` + - This option cannot be chosen as answer + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets` + +This tagrendering has labels `relevant-questions` + + + +### toilets-fee + + + +The question is *Are these toilets free to use?* + + + + + + - *These are paid toilets* corresponds with `fee=yes` + - *Free to use* corresponds with `fee=no` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&access!=no` + +This tagrendering has labels `relevant-questions` + + + +### toilet-charge + + + +The question is *How much does one have to pay for these toilets?* + +This rendering asks information about the property [charge](https://wiki.openstreetmap.org/wiki/Key:charge) + +This is rendered with `The fee is {charge}` + + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&fee=yes` + +This tagrendering has labels `relevant-questions` + + + +### payment-options-split + + + +The question is *Which methods of payment are accepted here?* + + + + + + - *Cash is accepted here* corresponds with `payment:cash=yes` + - This option cannot be chosen as answer + - Unselecting this answer will add + - *Payment cards are accepted here* corresponds with `payment:cards=yes` + - This option cannot be chosen as answer + - Unselecting this answer will add + - *Payment by QR-code is possible here* corresponds with `payment:qr_code=yes` + - Unselecting this answer will add payment:qr_code=no + - *Coins are accepted here* corresponds with `payment:coins=yes` + - Unselecting this answer will add payment:coins=no + - *Bank notes are accepted here* corresponds with `payment:notes=yes` + - Unselecting this answer will add payment:notes=no + - *Debit cards are accepted here* corresponds with `payment:debit_cards=yes` + - Unselecting this answer will add payment:debit_cards=no + - *Credit cards are accepted here* corresponds with `payment:credit_cards=yes` + - Unselecting this answer will add payment:credit_cards=no + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&fee=yes` + +This tagrendering has labels `relevant-questions` + + + +### opening_hours_24_7 + + + +The question is *When are these toilets opened?* + +This rendering asks information about the property [opening_hours](https://wiki.openstreetmap.org/wiki/Key:opening_hours) + +This is rendered with `

Opening hours

{opening_hours_table(opening_hours)}` + + + + + + - *24/7 opened (including holidays)* corresponds with `opening_hours=24/7` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&access!=no` + +This tagrendering has labels `relevant-questions` + + + +### toilets-wheelchair + + + +The question is *Is there a dedicated toilet for wheelchair users?* + + + + + + - *There is a dedicated toilet for wheelchair users* corresponds with `wheelchair=yes` + - *No wheelchair access* corresponds with `wheelchair=no` + - *There is only a dedicated toilet for wheelchair users* corresponds with `wheelchair=designated` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets` + +This tagrendering has labels `relevant-questions` + + + +### wheelchair-door-width + + + +The question is *What is the width of the door to the wheelchair accessible toilet?* + +This rendering asks information about the property [door:width](https://wiki.openstreetmap.org/wiki/Key:door:width) + +This is rendered with `The door to the wheelchair-accessible toilet is {canonical(door:width)} wide` + + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&wheelchair=yes|wheelchair=designated` + +This tagrendering has labels `relevant-questions` + + + +### toilets-type + + + +The question is *Which kind of toilets are this?* + + + + + + - *There are only seated toilets* corresponds with `toilets:position=seated` + - *There are only urinals here* corresponds with `toilets:position=urinal` + - *There are only squat toilets here* corresponds with `toilets:position=squat` + - *Both seated toilets and urinals are available here* corresponds with `toilets:position=seated;urinal` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets` + +This tagrendering has labels `relevant-questions` + + + +### toilets-changing-table + + + +The question is *Is a changing table (to change diapers) available?* + + + + + + - *A changing table is available* corresponds with `changing_table=yes` + - *No changing table is available* corresponds with `changing_table=no` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets` + +This tagrendering has labels `relevant-questions` + + + +### toilet-changing_table:location + + + +The question is *Where is the changing table located?* + +This rendering asks information about the property [changing_table:location](https://wiki.openstreetmap.org/wiki/Key:changing_table:location) + +This is rendered with `The changing table is located at {changing_table:location}` + + + + + + - *The changing table is in the toilet for women. * corresponds with `changing_table:location=female_toilet` + - *The changing table is in the toilet for men. * corresponds with `changing_table:location=male_toilet` + - *The changing table is in the toilet for wheelchair users. * corresponds with `changing_table:location=wheelchair_toilet` + - *The changing table is in a dedicated room. * corresponds with `changing_table:location=dedicated_room` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&changing_table=yes` + +This tagrendering has labels `relevant-questions` + + + +### toilet-handwashing + + + +The question is *Do these toilets have a sink to wash your hands?* + + + + + + - *This toilets have a sink to wash your hands* corresponds with `toilets:handwashing=yes` + - *This toilets don't have a sink to wash your hands* corresponds with `toilets:handwashing=no` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets` + +This tagrendering has labels `relevant-questions` + + + +### toilet-has-paper + + + +The question is *Does one have to bring their own toilet paper to this toilet?* + + + + + + - *This toilet is equipped with toilet paper* corresponds with `toilets:paper_supplied=yes` + - *You have to bring your own toilet paper to this toilet* corresponds with `toilets:paper_supplied=no` + + +This tagrendering is only visible in the popup if the following condition is met: `amenity=toilets&toilets:position!=urinal` + +This tagrendering has labels `relevant-questions` + + + ### leftover-questions diff --git a/Docs/Layers/toilet.md b/Docs/Layers/toilet.md index 19e914f89..a2a467436 100644 --- a/Docs/Layers/toilet.md +++ b/Docs/Layers/toilet.md @@ -161,6 +161,8 @@ This is rendered with `Access is {access}` - This option cannot be chosen as answer +This tagrendering has labels `relevant-questions` + ### toilets-fee @@ -177,6 +179,8 @@ The question is *Are these toilets free to use?* - *Free to use* corresponds with `fee=no` +This tagrendering has labels `relevant-questions` + ### toilet-charge @@ -193,6 +197,8 @@ This is rendered with `The fee is {charge}` This tagrendering is only visible in the popup if the following condition is met: `fee=yes` +This tagrendering has labels `relevant-questions` + ### payment-options-split @@ -225,6 +231,8 @@ The question is *Which methods of payment are accepted here?* This tagrendering is only visible in the popup if the following condition is met: `fee=yes` +This tagrendering has labels `relevant-questions` + ### opening_hours_24_7 @@ -244,6 +252,8 @@ This is rendered with `

Opening hours

{opening_hours_table(opening_hours - *24/7 opened (including holidays)* corresponds with `opening_hours=24/7` +This tagrendering has labels `relevant-questions` + ### toilets-wheelchair @@ -261,6 +271,8 @@ The question is *Is there a dedicated toilet for wheelchair users?* - *There is only a dedicated toilet for wheelchair users* corresponds with `wheelchair=designated` +This tagrendering has labels `relevant-questions` + ### wheelchair-door-width @@ -277,6 +289,8 @@ This is rendered with `The door to the wheelchair-accessible toilet is {canonic This tagrendering is only visible in the popup if the following condition is met: `wheelchair=yes|wheelchair=designated` +This tagrendering has labels `relevant-questions` + ### toilets-type @@ -295,6 +309,8 @@ The question is *Which kind of toilets are this?* - *Both seated toilets and urinals are available here* corresponds with `toilets:position=seated;urinal` +This tagrendering has labels `relevant-questions` + ### toilets-changing-table @@ -311,6 +327,8 @@ The question is *Is a changing table (to change diapers) available?* - *No changing table is available* corresponds with `changing_table=no` +This tagrendering has labels `relevant-questions` + ### toilet-changing_table:location @@ -335,6 +353,8 @@ This is rendered with `The changing table is located at {changing_table:locatio This tagrendering is only visible in the popup if the following condition is met: `changing_table=yes` +This tagrendering has labels `relevant-questions` + ### toilet-handwashing @@ -351,6 +371,8 @@ The question is *Do these toilets have a sink to wash your hands?* - *This toilets don't have a sink to wash your hands* corresponds with `toilets:handwashing=no` +This tagrendering has labels `relevant-questions` + ### toilet-has-paper @@ -367,6 +389,8 @@ The question is *Does one have to bring their own toilet paper to this toilet?* - *You have to bring your own toilet paper to this toilet* corresponds with `toilets:paper_supplied=no` +This tagrendering has labels `relevant-questions` + ### description diff --git a/Docs/Layers/toilet_at_amenity.md b/Docs/Layers/toilet_at_amenity.md index 0644230f1..2e4ea44c3 100644 --- a/Docs/Layers/toilet_at_amenity.md +++ b/Docs/Layers/toilet_at_amenity.md @@ -255,6 +255,8 @@ The question is *Which kind of toilets are this?* - *Both seated toilets and urinals are available here* corresponds with `toilets:position=seated;urinal` +This tagrendering has labels `relevant-questions` + ### toilets-changing-table @@ -271,6 +273,8 @@ The question is *Is a changing table (to change diapers) available?* - *No changing table is available* corresponds with `changing_table=no` +This tagrendering has labels `relevant-questions` + ### toilet-changing_table:location @@ -295,6 +299,8 @@ This is rendered with `The changing table is located at {changing_table:locatio This tagrendering is only visible in the popup if the following condition is met: `changing_table=yes` +This tagrendering has labels `relevant-questions` + ### toilet-handwashing @@ -311,6 +317,8 @@ The question is *Do these toilets have a sink to wash your hands?* - *This toilets don't have a sink to wash your hands* corresponds with `toilets:handwashing=no` +This tagrendering has labels `relevant-questions` + ### toilet-has-paper @@ -327,6 +335,8 @@ The question is *Does one have to bring their own toilet paper to this toilet?* - *You have to bring your own toilet paper to this toilet* corresponds with `toilets:paper_supplied=no` +This tagrendering has labels `relevant-questions` + ### description diff --git a/Docs/Layers/vending_machine.md b/Docs/Layers/vending_machine.md index 5e5300866..3b592ab05 100644 --- a/Docs/Layers/vending_machine.md +++ b/Docs/Layers/vending_machine.md @@ -47,7 +47,7 @@ this quick overview is incomplete attribute | type | values which are supported by this layer ----------- | ------ | ------------------------------------------ [](https://taginfo.openstreetmap.org/keys/id#values) [id](https://wiki.openstreetmap.org/wiki/Key:id) | Multiple choice | -[](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | [string](../SpecialInputElements.md#string) | [drinks](https://wiki.openstreetmap.org/wiki/Tag:vending%3Ddrinks) [sweets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dsweets) [food](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dfood) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcigarettes) [condoms](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcondoms) [coffee](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcoffee) [water](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dwater) [newspapers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dnewspapers) [bicycle_tube](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_tube) [milk](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmilk) [bread](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbread) [eggs](https://wiki.openstreetmap.org/wiki/Tag:vending%3Deggs) [cheese](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcheese) [honey](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dhoney) [potatoes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpotatoes) [flowers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dflowers) [parking_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dparking_tickets) [elongated_coin](https://wiki.openstreetmap.org/wiki/Tag:vending%3Delongated_coin) [public_transport_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpublic_transport_tickets) [meat](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmeat) +[](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | [string](../SpecialInputElements.md#string) | [drinks](https://wiki.openstreetmap.org/wiki/Tag:vending%3Ddrinks) [sweets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dsweets) [food](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dfood) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcigarettes) [condoms](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcondoms) [coffee](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcoffee) [water](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dwater) [newspapers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dnewspapers) [bicycle_tube](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_tube) [milk](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmilk) [bread](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbread) [eggs](https://wiki.openstreetmap.org/wiki/Tag:vending%3Deggs) [cheese](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dcheese) [honey](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dhoney) [potatoes](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpotatoes) [meat](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dmeat) [flowers](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dflowers) [parking_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dparking_tickets) [elongated_coin](https://wiki.openstreetmap.org/wiki/Tag:vending%3Delongated_coin) [public_transport_tickets](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dpublic_transport_tickets) [bicycle_light](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_light) [gloves](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dgloves) [bicycle_repair_kit](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_repair_kit) [bicycle_pump](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_pump) [bicycle_lock](https://wiki.openstreetmap.org/wiki/Tag:vending%3Dbicycle_lock) [](https://taginfo.openstreetmap.org/keys/opening_hours#values) [opening_hours](https://wiki.openstreetmap.org/wiki/Key:opening_hours) | [opening_hours](../SpecialInputElements.md#opening_hours) | [24/7](https://wiki.openstreetmap.org/wiki/Tag:opening_hours%3D24/7) [](https://taginfo.openstreetmap.org/keys/payment:coins:denominations#values) [payment:coins:denominations](https://wiki.openstreetmap.org/wiki/Key:payment:coins:denominations) | Multiple choice | [0.01 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.01 EUR) [0.02 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.02 EUR) [0.05 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.05 EUR) [0.10 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.10 EUR) [0.20 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.20 EUR) [0.50 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.50 EUR) [1 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D1 EUR) [2 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D2 EUR) [0.05 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.05 CHF) [0.10 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.10 CHF) [0.20 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.20 CHF) [0.50 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D0.50 CHF) [1 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D1 CHF) [2 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D2 CHF) [5 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:coins:denominations%3D5 CHF) [](https://taginfo.openstreetmap.org/keys/payment:notes:denominations#values) [payment:notes:denominations](https://wiki.openstreetmap.org/wiki/Key:payment:notes:denominations) | Multiple choice | [5 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D5 EUR) [10 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D10 EUR) [20 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D20 EUR) [50 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D50 EUR) [100 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D100 EUR) [200 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D200 EUR) [500 EUR](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D500 EUR) [10 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D10 CHF) [20 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D20 CHF) [50 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D50 CHF) [100 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D100 CHF) [200 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D200 CHF) [1000 CHF](https://wiki.openstreetmap.org/wiki/Tag:payment:notes:denominations%3D1000 CHF) @@ -119,11 +119,16 @@ This is rendered with `This vending machine sells {vending}` - *Cheese is sold* corresponds with `vending=cheese` - *Honey is sold* corresponds with `vending=honey` - *Potatoes are sold* corresponds with `vending=potatoes` + - *Meat is sold* corresponds with `vending=meat` - *Flowers are sold* corresponds with `vending=flowers` - *Parking tickets are sold* corresponds with `vending=parking_tickets` - *Pressed pennies are sold* corresponds with `vending=elongated_coin` - *Public transport tickets are sold* corresponds with `vending=public_transport_tickets` - - *Meat products are being sold* corresponds with `vending=meat` + - *Bicycle lights are sold* corresponds with `vending=bicycle_light` + - *Gloves are sold* corresponds with `vending=gloves` + - *Bicycle repair kits are sold* corresponds with `vending=bicycle_repair_kit` + - *Bicycle pumps are sold* corresponds with `vending=bicycle_pump` + - *Bicycle locks are sold* corresponds with `vending=bicycle_lock` @@ -429,11 +434,16 @@ vending.12 | Sale of eggs | vending~^(.*eggs.*)$ vending.13 | Sale of cheese | vending~^(.*cheese.*)$ vending.14 | Sale of honey | vending~^(.*honey.*)$ vending.15 | Sale of potatoes | vending~^(.*potatoes.*)$ -vending.16 | Sale of flowers | vending~^(.*flowers.*)$ -vending.17 | Sale of parking | vending~^(.*parking_tickets.*)$ -vending.18 | Sale of pressed pennies | vending=elongated_coin -vending.19 | Sale of public transport tickets | vending~^(.*public_transport_tickets.*)$ -vending.20 | Sale of meat products | vending=meat +vending.16 | Sale of meat | vending~^(.*meat.*)$ +vending.17 | Sale of flowers | vending~^(.*flowers.*)$ +vending.18 | Sale of parking tickets | vending~^(.*parking_tickets.*)$ +vending.19 | Sale of pressed pennies | vending=elongated_coin +vending.20 | Sale of public transport tickets | vending~^(.*public_transport_tickets.*)$ +vending.21 | Sale of bicycle lights | vending=bicycle_light +vending.22 | Sale of gloves | vending=gloves +vending.23 | Sale of bicycle repair kits | vending=bicycle_repair_kit +vending.24 | Sale of bicycle pumps | vending=bicycle_pump +vending.25 | Sale of bicycle locks | vending=bicycle_lock This document is autogenerated from [assets/layers/vending_machine/vending_machine.json](https://github.com/pietervdvn/MapComplete/blob/develop/assets/layers/vending_machine/vending_machine.json) diff --git a/Docs/Layers/waste_basket.md b/Docs/Layers/waste_basket.md index d916b8de3..479b1e21a 100644 --- a/Docs/Layers/waste_basket.md +++ b/Docs/Layers/waste_basket.md @@ -49,7 +49,7 @@ this quick overview is incomplete attribute | type | values which are supported by this layer ----------- | ------ | ------------------------------------------ [](https://taginfo.openstreetmap.org/keys/id#values) [id](https://wiki.openstreetmap.org/wiki/Key:id) | Multiple choice | -[](https://taginfo.openstreetmap.org/keys/waste#values) [waste](https://wiki.openstreetmap.org/wiki/Key:waste) | Multiple choice | [trash](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dtrash) [dog_excrement](https://wiki.openstreetmap.org/wiki/Tag:waste%3Ddog_excrement) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dcigarettes) [drugs](https://wiki.openstreetmap.org/wiki/Tag:waste%3Ddrugs) [sharps](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dsharps) [plastic](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dplastic) +[](https://taginfo.openstreetmap.org/keys/waste#values) [waste](https://wiki.openstreetmap.org/wiki/Key:waste) | Multiple choice | [trash](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dtrash) [dog_excrement](https://wiki.openstreetmap.org/wiki/Tag:waste%3Ddog_excrement) [cigarettes](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dcigarettes) [drugs](https://wiki.openstreetmap.org/wiki/Tag:waste%3Ddrugs) [sharps](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dsharps) [plastic](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dplastic) [paper](https://wiki.openstreetmap.org/wiki/Tag:waste%3Dpaper) [](https://taginfo.openstreetmap.org/keys/vending#values) [vending](https://wiki.openstreetmap.org/wiki/Key:vending) | Multiple choice | [dog_excrement_bag](https://wiki.openstreetmap.org/wiki/Tag:vending%3Ddog_excrement_bag) [](https://wiki.openstreetmap.org/wiki/Tag:vending%3D) @@ -104,6 +104,7 @@ The question is *What kind of waste basket is this?* - *A waste basket for drugs* corresponds with `waste=drugs` - *A waste basket for needles and other sharp objects* corresponds with `waste=sharps` - *A waste basket for plastic* corresponds with `waste=plastic` + - *A waste basket for paper* corresponds with `waste=paper` diff --git a/Docs/TagInfo/mapcomplete_cyclofix.json b/Docs/TagInfo/mapcomplete_cyclofix.json index e2c867d7f..4a050062c 100644 --- a/Docs/TagInfo/mapcomplete_cyclofix.json +++ b/Docs/TagInfo/mapcomplete_cyclofix.json @@ -988,18 +988,38 @@ "description": "Layer 'Bicycle tube vending machine' shows and asks freeform values for key 'charge' (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')" }, { - "key": "payment:coins", - "description": "Layer 'Bicycle tube vending machine' shows payment:coins=yes with a fixed text, namely 'Payment with coins is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" - }, - { - "key": "payment:notes", - "description": "Layer 'Bicycle tube vending machine' shows payment:notes=yes with a fixed text, namely 'Payment with notes is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "key": "payment:cash", + "description": "Layer 'Bicycle tube vending machine' shows payment:cash=yes with a fixed text, namely 'Cash is accepted here' (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", "value": "yes" }, { "key": "payment:cards", - "description": "Layer 'Bicycle tube vending machine' shows payment:cards=yes with a fixed text, namely 'Payment with cards is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "description": "Layer 'Bicycle tube vending machine' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "yes" + }, + { + "key": "payment:qr_code", + "description": "Layer 'Bicycle tube vending machine' shows payment:qr_code=yes with a fixed text, namely 'Payment by QR-code is possible here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "yes" + }, + { + "key": "payment:coins", + "description": "Layer 'Bicycle tube vending machine' shows payment:coins=yes with a fixed text, namely 'Coins are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "yes" + }, + { + "key": "payment:notes", + "description": "Layer 'Bicycle tube vending machine' shows payment:notes=yes with a fixed text, namely 'Bank notes are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "yes" + }, + { + "key": "payment:debit_cards", + "description": "Layer 'Bicycle tube vending machine' shows payment:debit_cards=yes with a fixed text, namely 'Debit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "yes" + }, + { + "key": "payment:credit_cards", + "description": "Layer 'Bicycle tube vending machine' shows payment:credit_cards=yes with a fixed text, namely 'Credit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", "value": "yes" }, { @@ -1031,29 +1051,34 @@ "value": "Continental" }, { - "key": "vending:bicycle_light", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_light=yes with a fixed text, namely 'Bicycle lights are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_tube with a fixed text, namely 'Bicycle inner tubes are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "bicycle_tube" }, { - "key": "vending:gloves", - "description": "Layer 'Bicycle tube vending machine' shows vending:gloves=yes with a fixed text, namely 'Gloves are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_light with a fixed text, namely 'Bicycle lights are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "bicycle_light" }, { - "key": "vending:bicycle_repair_kit", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_repair_kit=yes with a fixed text, namely 'Bicycle repair kits are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=gloves with a fixed text, namely 'Gloves are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "gloves" }, { - "key": "vending:bicycle_pump", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_pump=yes with a fixed text, namely 'Bicycle pumps are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_repair_kit with a fixed text, namely 'Bicycle repair kits are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "bicycle_repair_kit" }, { - "key": "vending:bicycle_lock", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_lock=yes with a fixed text, namely 'Bicycle locks are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_pump with a fixed text, namely 'Bicycle pumps are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "bicycle_pump" + }, + { + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_lock with a fixed text, namely 'Bicycle locks are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Cyclofix - a map for cyclists')", + "value": "bicycle_lock" }, { "key": "amenity", diff --git a/Docs/TagInfo/mapcomplete_indoors.json b/Docs/TagInfo/mapcomplete_indoors.json index b28e95e76..9b9327b26 100644 --- a/Docs/TagInfo/mapcomplete_indoors.json +++ b/Docs/TagInfo/mapcomplete_indoors.json @@ -215,6 +215,186 @@ "key": "name:etymology:wikidata", "description": "Layer 'Indoors' shows and asks freeform values for key 'name:etymology:wikidata' (in the mapcomplete.org theme 'Indoors') (This is only shown if name:etymology!=unknown)" }, + { + "key": "access", + "description": "Layer 'Indoors' shows and asks freeform values for key 'access' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=yes with a fixed text, namely 'Public access' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=customers with a fixed text, namely 'Only access to customers' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "customers" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=no with a fixed text, namely 'Not accessible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=key with a fixed text, namely 'Accessible, but one has to ask a key to enter' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "key" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=public with a fixed text, namely 'Public access' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "public" + }, + { + "key": "fee", + "description": "Layer 'Indoors' shows fee=yes with a fixed text, namely 'These are paid toilets' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&access!=no)", + "value": "yes" + }, + { + "key": "fee", + "description": "Layer 'Indoors' shows fee=no with a fixed text, namely 'Free to use' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&access!=no)", + "value": "no" + }, + { + "key": "charge", + "description": "Layer 'Indoors' shows and asks freeform values for key 'charge' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)" + }, + { + "key": "payment:cash", + "description": "Layer 'Indoors' shows payment:cash=yes with a fixed text, namely 'Cash is accepted here' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:cards", + "description": "Layer 'Indoors' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:qr_code", + "description": "Layer 'Indoors' shows payment:qr_code=yes with a fixed text, namely 'Payment by QR-code is possible here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:coins", + "description": "Layer 'Indoors' shows payment:coins=yes with a fixed text, namely 'Coins are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:notes", + "description": "Layer 'Indoors' shows payment:notes=yes with a fixed text, namely 'Bank notes are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:debit_cards", + "description": "Layer 'Indoors' shows payment:debit_cards=yes with a fixed text, namely 'Debit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:credit_cards", + "description": "Layer 'Indoors' shows payment:credit_cards=yes with a fixed text, namely 'Credit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "opening_hours", + "description": "Layer 'Indoors' shows and asks freeform values for key 'opening_hours' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&access!=no)" + }, + { + "key": "opening_hours", + "description": "Layer 'Indoors' shows opening_hours=24/7 with a fixed text, namely '24/7 opened (including holidays)' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&access!=no)", + "value": "24/7" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=yes with a fixed text, namely 'There is a dedicated toilet for wheelchair users' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=no with a fixed text, namely 'No wheelchair access' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=designated with a fixed text, namely 'There is only a dedicated toilet for wheelchair users' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "designated" + }, + { + "key": "door:width", + "description": "Layer 'Indoors' shows and asks freeform values for key 'door:width' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&wheelchair=yes|wheelchair=designated)" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=seated with a fixed text, namely 'There are only seated toilets' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "seated" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=urinal with a fixed text, namely 'There are only urinals here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "urinal" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=squat with a fixed text, namely 'There are only squat toilets here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "squat" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=seated;urinal with a fixed text, namely 'Both seated toilets and urinals are available here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "seated;urinal" + }, + { + "key": "changing_table", + "description": "Layer 'Indoors' shows changing_table=yes with a fixed text, namely 'A changing table is available' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "changing_table", + "description": "Layer 'Indoors' shows changing_table=no with a fixed text, namely 'No changing table is available' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows and asks freeform values for key 'changing_table:location' (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&changing_table=yes)" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=female_toilet with a fixed text, namely 'The changing table is in the toilet for women. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "female_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=male_toilet with a fixed text, namely 'The changing table is in the toilet for men. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "male_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=wheelchair_toilet with a fixed text, namely 'The changing table is in the toilet for wheelchair users. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "wheelchair_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=dedicated_room with a fixed text, namely 'The changing table is in a dedicated room. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "dedicated_room" + }, + { + "key": "toilets:handwashing", + "description": "Layer 'Indoors' shows toilets:handwashing=yes with a fixed text, namely 'This toilets have a sink to wash your hands' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "toilets:handwashing", + "description": "Layer 'Indoors' shows toilets:handwashing=no with a fixed text, namely 'This toilets don't have a sink to wash your hands' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "toilets:paper_supplied", + "description": "Layer 'Indoors' shows toilets:paper_supplied=yes with a fixed text, namely 'This toilet is equipped with toilet paper' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&toilets:position!=urinal)", + "value": "yes" + }, + { + "key": "toilets:paper_supplied", + "description": "Layer 'Indoors' shows toilets:paper_supplied=no with a fixed text, namely 'You have to bring your own toilet paper to this toilet' and allows to pick this as a default answer (in the mapcomplete.org theme 'Indoors') (This is only shown if amenity=toilets&toilets:position!=urinal)", + "value": "no" + }, { "key": "highway", "description": "The MapComplete theme Indoors has a layer Pedestrian paths showing features with this tag", diff --git a/Docs/TagInfo/mapcomplete_personal.json b/Docs/TagInfo/mapcomplete_personal.json index 09d70051e..168f35745 100644 --- a/Docs/TagInfo/mapcomplete_personal.json +++ b/Docs/TagInfo/mapcomplete_personal.json @@ -1514,18 +1514,38 @@ "description": "Layer 'Bicycle tube vending machine' shows and asks freeform values for key 'charge' (in the mapcomplete.org theme 'Personal theme')" }, { - "key": "payment:coins", - "description": "Layer 'Bicycle tube vending machine' shows payment:coins=yes with a fixed text, namely 'Payment with coins is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" - }, - { - "key": "payment:notes", - "description": "Layer 'Bicycle tube vending machine' shows payment:notes=yes with a fixed text, namely 'Payment with notes is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "key": "payment:cash", + "description": "Layer 'Bicycle tube vending machine' shows payment:cash=yes with a fixed text, namely 'Cash is accepted here' (in the mapcomplete.org theme 'Personal theme')", "value": "yes" }, { "key": "payment:cards", - "description": "Layer 'Bicycle tube vending machine' shows payment:cards=yes with a fixed text, namely 'Payment with cards is possible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "description": "Layer 'Bicycle tube vending machine' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' (in the mapcomplete.org theme 'Personal theme')", + "value": "yes" + }, + { + "key": "payment:qr_code", + "description": "Layer 'Bicycle tube vending machine' shows payment:qr_code=yes with a fixed text, namely 'Payment by QR-code is possible here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "yes" + }, + { + "key": "payment:coins", + "description": "Layer 'Bicycle tube vending machine' shows payment:coins=yes with a fixed text, namely 'Coins are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "yes" + }, + { + "key": "payment:notes", + "description": "Layer 'Bicycle tube vending machine' shows payment:notes=yes with a fixed text, namely 'Bank notes are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "yes" + }, + { + "key": "payment:debit_cards", + "description": "Layer 'Bicycle tube vending machine' shows payment:debit_cards=yes with a fixed text, namely 'Debit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "yes" + }, + { + "key": "payment:credit_cards", + "description": "Layer 'Bicycle tube vending machine' shows payment:credit_cards=yes with a fixed text, namely 'Credit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", "value": "yes" }, { @@ -1557,29 +1577,34 @@ "value": "Continental" }, { - "key": "vending:bicycle_light", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_light=yes with a fixed text, namely 'Bicycle lights are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_tube with a fixed text, namely 'Bicycle inner tubes are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_tube" }, { - "key": "vending:gloves", - "description": "Layer 'Bicycle tube vending machine' shows vending:gloves=yes with a fixed text, namely 'Gloves are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_light with a fixed text, namely 'Bicycle lights are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_light" }, { - "key": "vending:bicycle_repair_kit", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_repair_kit=yes with a fixed text, namely 'Bicycle repair kits are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=gloves with a fixed text, namely 'Gloves are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "gloves" }, { - "key": "vending:bicycle_pump", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_pump=yes with a fixed text, namely 'Bicycle pumps are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_repair_kit with a fixed text, namely 'Bicycle repair kits are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_repair_kit" }, { - "key": "vending:bicycle_lock", - "description": "Layer 'Bicycle tube vending machine' shows vending:bicycle_lock=yes with a fixed text, namely 'Bicycle locks are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "yes" + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_pump with a fixed text, namely 'Bicycle pumps are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_pump" + }, + { + "key": "vending", + "description": "Layer 'Bicycle tube vending machine' shows vending=bicycle_lock with a fixed text, namely 'Bicycle locks are sold here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_lock" }, { "key": "amenity", @@ -8548,6 +8573,186 @@ "key": "name:etymology:wikidata", "description": "Layer 'Indoors' shows and asks freeform values for key 'name:etymology:wikidata' (in the mapcomplete.org theme 'Personal theme') (This is only shown if name:etymology!=unknown)" }, + { + "key": "access", + "description": "Layer 'Indoors' shows and asks freeform values for key 'access' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=yes with a fixed text, namely 'Public access' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=customers with a fixed text, namely 'Only access to customers' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "customers" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=no with a fixed text, namely 'Not accessible' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=key with a fixed text, namely 'Accessible, but one has to ask a key to enter' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "key" + }, + { + "key": "access", + "description": "Layer 'Indoors' shows access=public with a fixed text, namely 'Public access' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "public" + }, + { + "key": "fee", + "description": "Layer 'Indoors' shows fee=yes with a fixed text, namely 'These are paid toilets' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&access!=no)", + "value": "yes" + }, + { + "key": "fee", + "description": "Layer 'Indoors' shows fee=no with a fixed text, namely 'Free to use' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&access!=no)", + "value": "no" + }, + { + "key": "charge", + "description": "Layer 'Indoors' shows and asks freeform values for key 'charge' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)" + }, + { + "key": "payment:cash", + "description": "Layer 'Indoors' shows payment:cash=yes with a fixed text, namely 'Cash is accepted here' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:cards", + "description": "Layer 'Indoors' shows payment:cards=yes with a fixed text, namely 'Payment cards are accepted here' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:qr_code", + "description": "Layer 'Indoors' shows payment:qr_code=yes with a fixed text, namely 'Payment by QR-code is possible here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:coins", + "description": "Layer 'Indoors' shows payment:coins=yes with a fixed text, namely 'Coins are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:notes", + "description": "Layer 'Indoors' shows payment:notes=yes with a fixed text, namely 'Bank notes are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:debit_cards", + "description": "Layer 'Indoors' shows payment:debit_cards=yes with a fixed text, namely 'Debit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "payment:credit_cards", + "description": "Layer 'Indoors' shows payment:credit_cards=yes with a fixed text, namely 'Credit cards are accepted here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&fee=yes)", + "value": "yes" + }, + { + "key": "opening_hours", + "description": "Layer 'Indoors' shows and asks freeform values for key 'opening_hours' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&access!=no)" + }, + { + "key": "opening_hours", + "description": "Layer 'Indoors' shows opening_hours=24/7 with a fixed text, namely '24/7 opened (including holidays)' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&access!=no)", + "value": "24/7" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=yes with a fixed text, namely 'There is a dedicated toilet for wheelchair users' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=no with a fixed text, namely 'No wheelchair access' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "wheelchair", + "description": "Layer 'Indoors' shows wheelchair=designated with a fixed text, namely 'There is only a dedicated toilet for wheelchair users' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "designated" + }, + { + "key": "door:width", + "description": "Layer 'Indoors' shows and asks freeform values for key 'door:width' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&wheelchair=yes|wheelchair=designated)" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=seated with a fixed text, namely 'There are only seated toilets' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "seated" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=urinal with a fixed text, namely 'There are only urinals here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "urinal" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=squat with a fixed text, namely 'There are only squat toilets here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "squat" + }, + { + "key": "toilets:position", + "description": "Layer 'Indoors' shows toilets:position=seated;urinal with a fixed text, namely 'Both seated toilets and urinals are available here' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "seated;urinal" + }, + { + "key": "changing_table", + "description": "Layer 'Indoors' shows changing_table=yes with a fixed text, namely 'A changing table is available' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "changing_table", + "description": "Layer 'Indoors' shows changing_table=no with a fixed text, namely 'No changing table is available' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows and asks freeform values for key 'changing_table:location' (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&changing_table=yes)" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=female_toilet with a fixed text, namely 'The changing table is in the toilet for women. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "female_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=male_toilet with a fixed text, namely 'The changing table is in the toilet for men. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "male_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=wheelchair_toilet with a fixed text, namely 'The changing table is in the toilet for wheelchair users. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "wheelchair_toilet" + }, + { + "key": "changing_table:location", + "description": "Layer 'Indoors' shows changing_table:location=dedicated_room with a fixed text, namely 'The changing table is in a dedicated room. ' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&changing_table=yes)", + "value": "dedicated_room" + }, + { + "key": "toilets:handwashing", + "description": "Layer 'Indoors' shows toilets:handwashing=yes with a fixed text, namely 'This toilets have a sink to wash your hands' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "yes" + }, + { + "key": "toilets:handwashing", + "description": "Layer 'Indoors' shows toilets:handwashing=no with a fixed text, namely 'This toilets don't have a sink to wash your hands' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets)", + "value": "no" + }, + { + "key": "toilets:paper_supplied", + "description": "Layer 'Indoors' shows toilets:paper_supplied=yes with a fixed text, namely 'This toilet is equipped with toilet paper' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&toilets:position!=urinal)", + "value": "yes" + }, + { + "key": "toilets:paper_supplied", + "description": "Layer 'Indoors' shows toilets:paper_supplied=no with a fixed text, namely 'You have to bring your own toilet paper to this toilet' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if amenity=toilets&toilets:position!=urinal)", + "value": "no" + }, { "key": "information", "description": "The MapComplete theme Personal theme has a layer Information boards showing features with this tag", @@ -14229,6 +14434,11 @@ "description": "Layer 'Vending Machines' shows vending=potatoes with a fixed text, namely 'Potatoes are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", "value": "potatoes" }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=meat with a fixed text, namely 'Meat is sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "meat" + }, { "key": "vending", "description": "Layer 'Vending Machines' shows vending=flowers with a fixed text, namely 'Flowers are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", @@ -14251,8 +14461,28 @@ }, { "key": "vending", - "description": "Layer 'Vending Machines' shows vending=meat with a fixed text, namely 'Meat products are being sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", - "value": "meat" + "description": "Layer 'Vending Machines' shows vending=bicycle_light with a fixed text, namely 'Bicycle lights are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_light" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=gloves with a fixed text, namely 'Gloves are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "gloves" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_repair_kit with a fixed text, namely 'Bicycle repair kits are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_repair_kit" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_pump with a fixed text, namely 'Bicycle pumps are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_pump" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_lock with a fixed text, namely 'Bicycle locks are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "bicycle_lock" }, { "key": "opening_hours", @@ -14587,6 +14817,11 @@ "description": "Layer 'Waste Basket' shows waste=plastic with a fixed text, namely 'A waste basket for plastic' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", "value": "plastic" }, + { + "key": "waste", + "description": "Layer 'Waste Basket' shows waste=paper with a fixed text, namely 'A waste basket for paper' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme')", + "value": "paper" + }, { "key": "vending", "description": "Layer 'Waste Basket' shows vending=dog_excrement_bag with a fixed text, namely 'This waste basket has a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the mapcomplete.org theme 'Personal theme') (This is only shown if waste=dog_excrement|waste=trash|)", diff --git a/Docs/TagInfo/mapcomplete_vending_machine.json b/Docs/TagInfo/mapcomplete_vending_machine.json index 98450f988..f71c8e6db 100644 --- a/Docs/TagInfo/mapcomplete_vending_machine.json +++ b/Docs/TagInfo/mapcomplete_vending_machine.json @@ -114,6 +114,11 @@ "description": "Layer 'Vending Machines' shows vending=potatoes with a fixed text, namely 'Potatoes are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", "value": "potatoes" }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=meat with a fixed text, namely 'Meat is sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "meat" + }, { "key": "vending", "description": "Layer 'Vending Machines' shows vending=flowers with a fixed text, namely 'Flowers are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", @@ -136,8 +141,28 @@ }, { "key": "vending", - "description": "Layer 'Vending Machines' shows vending=meat with a fixed text, namely 'Meat products are being sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", - "value": "meat" + "description": "Layer 'Vending Machines' shows vending=bicycle_light with a fixed text, namely 'Bicycle lights are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "bicycle_light" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=gloves with a fixed text, namely 'Gloves are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "gloves" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_repair_kit with a fixed text, namely 'Bicycle repair kits are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "bicycle_repair_kit" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_pump with a fixed text, namely 'Bicycle pumps are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "bicycle_pump" + }, + { + "key": "vending", + "description": "Layer 'Vending Machines' shows vending=bicycle_lock with a fixed text, namely 'Bicycle locks are sold' and allows to pick this as a default answer (in the mapcomplete.org theme 'Vending Machines')", + "value": "bicycle_lock" }, { "key": "opening_hours", diff --git a/Docs/TagInfo/mapcomplete_waste.json b/Docs/TagInfo/mapcomplete_waste.json index 7aaf9b8a7..4f8a8b2ef 100644 --- a/Docs/TagInfo/mapcomplete_waste.json +++ b/Docs/TagInfo/mapcomplete_waste.json @@ -70,6 +70,11 @@ "description": "Layer 'Waste Basket' shows waste=plastic with a fixed text, namely 'A waste basket for plastic' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste')", "value": "plastic" }, + { + "key": "waste", + "description": "Layer 'Waste Basket' shows waste=paper with a fixed text, namely 'A waste basket for paper' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste')", + "value": "paper" + }, { "key": "vending", "description": "Layer 'Waste Basket' shows vending=dog_excrement_bag with a fixed text, namely 'This waste basket has a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste') (This is only shown if waste=dog_excrement|waste=trash|)", diff --git a/Docs/TagInfo/mapcomplete_waste_basket.json b/Docs/TagInfo/mapcomplete_waste_basket.json index 260e1695b..6b95079ea 100644 --- a/Docs/TagInfo/mapcomplete_waste_basket.json +++ b/Docs/TagInfo/mapcomplete_waste_basket.json @@ -70,6 +70,11 @@ "description": "Layer 'Waste Basket' shows waste=plastic with a fixed text, namely 'A waste basket for plastic' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste Basket')", "value": "plastic" }, + { + "key": "waste", + "description": "Layer 'Waste Basket' shows waste=paper with a fixed text, namely 'A waste basket for paper' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste Basket')", + "value": "paper" + }, { "key": "vending", "description": "Layer 'Waste Basket' shows vending=dog_excrement_bag with a fixed text, namely 'This waste basket has a dispenser for (dog) excrement bags' and allows to pick this as a default answer (in the mapcomplete.org theme 'Waste Basket') (This is only shown if waste=dog_excrement|waste=trash|)", diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 80c1aa360..1f439acc5 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -1,21 +1,21 @@ -import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"; -import type { Map as MlMap } from "maplibre-gl"; -import { GeoJSONSource, Marker } from "maplibre-gl"; -import { ShowDataLayerOptions } from "./ShowDataLayerOptions"; -import { GeoOperations } from "../../Logic/GeoOperations"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"; -import { OsmTags } from "../../Models/OsmFeature"; -import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"; -import { BBox } from "../../Logic/BBox"; -import { Feature, Point } from "geojson"; -import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"; -import { Utils } from "../../Utils"; -import * as range_layer from "../../../assets/layers/range/range.json"; -import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; -import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; -import FilteredLayer from "../../Models/FilteredLayer"; -import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"; +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" +import type { Map as MlMap } from "maplibre-gl" +import { GeoJSONSource, Marker } from "maplibre-gl" +import { ShowDataLayerOptions } from "./ShowDataLayerOptions" +import { GeoOperations } from "../../Logic/GeoOperations" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig" +import { OsmTags } from "../../Models/OsmFeature" +import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource" +import { BBox } from "../../Logic/BBox" +import { Feature, Point } from "geojson" +import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig" +import { Utils } from "../../Utils" +import * as range_layer from "../../../assets/layers/range/range.json" +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" +import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteredLayer from "../../Models/FilteredLayer" +import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" class PointRenderingLayer { private readonly _config: PointRenderingConfig @@ -284,18 +284,19 @@ class LineRenderingLayer { // Already up to date return } - {// Add source to the map or update the features + { + // Add source to the map or update the features if (src === undefined) { - this.currentSourceData = features; + this.currentSourceData = features map.addSource(this._layername, { type: "geojson", data: { type: "FeatureCollection", - features + features, }, - promoteId: "id" - }); - const linelayer = this._layername + "_line"; + promoteId: "id", + }) + const linelayer = this._layername + "_line" map.addLayer({ source: this._layername, id: linelayer, @@ -304,12 +305,12 @@ class LineRenderingLayer { "line-color": ["feature-state", "color"], "line-opacity": ["feature-state", "color-opacity"], "line-width": ["feature-state", "width"], - "line-offset": ["feature-state", "offset"] + "line-offset": ["feature-state", "offset"], }, layout: { - "line-cap": "round" - } - }); + "line-cap": "round", + }, + }) for (const feature of features) { map.setFeatureState( @@ -320,10 +321,10 @@ class LineRenderingLayer { map.on("click", linelayer, (e) => { // line-layer-listener - e.originalEvent["consumed"] = true; - this._onClick(e.features[0]); - }); - const polylayer = this._layername + "_polygon"; + e.originalEvent["consumed"] = true + this._onClick(e.features[0]) + }) + const polylayer = this._layername + "_polygon" map.addLayer({ source: this._layername, @@ -333,41 +334,41 @@ class LineRenderingLayer { layout: {}, paint: { "fill-color": ["feature-state", "fillColor"], - "fill-opacity": ["feature-state", "fillColor-opacity"] - } - }); + "fill-opacity": ["feature-state", "fillColor-opacity"], + }, + }) if (this._onClick) { map.on("click", polylayer, (e) => { // polygon-layer-listener if (e.originalEvent["consumed"]) { // This is a polygon beneath a marker, we can ignore it - return; + return } - e.originalEvent["consumed"] = true; - console.log("Got features:", e.features, e); - this._onClick(e.features[0]); - }); + e.originalEvent["consumed"] = true + console.log("Got features:", e.features, e) + this._onClick(e.features[0]) + }) } this._visibility?.addCallbackAndRunD((visible) => { try { - map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none"); - map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none"); + map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") + map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") } catch (e) { console.warn( "Error while setting visibility of layers ", linelayer, polylayer, e - ); + ) } - }); + }) } else { - this.currentSourceData = features; + this.currentSourceData = features src.setData({ type: "FeatureCollection", - features: this.currentSourceData - }); + features: this.currentSourceData, + }) } } for (let i = 0; i < features.length; i++) { @@ -400,7 +401,7 @@ class LineRenderingLayer { const tags = this._fetchStore(id) this._listenerInstalledOn.add(id) tags.addCallbackAndRunD((properties) => { - if(!map.getLayer(this._layername)){ + if (!map.getLayer(this._layername)) { return } map.setFeatureState( diff --git a/src/Utils.ts b/src/Utils.ts index 7c214c5bb..1f196de6e 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,5 +1,5 @@ -import colors from "./assets/colors.json"; -import DOMPurify from "dompurify"; +import colors from "./assets/colors.json" +import DOMPurify from "dompurify" export class Utils { /** @@ -7,12 +7,12 @@ export class Utils { * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. * This is a workaround and yet another hack */ - public static runningFromConsole = typeof window === "undefined"; - public static readonly assets_path = "./assets/svg/"; + public static runningFromConsole = typeof window === "undefined" + public static readonly assets_path = "./assets/svg/" public static externalDownloadFunction: ( url: string, headers?: any - ) => Promise<{ content: string } | { redirect: string }>; + ) => Promise<{ content: string } | { redirect: string }> public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. @@ -23,8 +23,8 @@ This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering... Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) - `; - public static readonly imageExtensions = new Set(["jpg", "png", "svg", "jpeg", ".gif"]); + ` + public static readonly imageExtensions = new Set(["jpg", "png", "svg", "jpeg", ".gif"]) public static readonly special_visualizations_importRequirementDocs = `#### Importing a dataset into OpenStreetMap: requirements If you want to import a dataset, make sure that: @@ -47,7 +47,7 @@ There are also some technicalities in your theme to keep in mind: The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. -In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org`; +In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org` private static knownKeys = [ "addExtraTags", "and", @@ -116,8 +116,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "version", "wayHandling", "widenFactor", - "width" - ]; + "width", + ] private static extraKeys = [ "nl", "en", @@ -135,36 +135,36 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "yes", "no", "true", - "false" - ]; - private static injectedDownloads = {}; + "false", + ] + private static injectedDownloads = {} private static _download_cache = new Map< string, { promise: Promise timestamp: number } - >(); + >() public static initDomPurify() { if (Utils.runningFromConsole) { - return; + return } - DOMPurify.addHook("afterSanitizeAttributes", function(node) { + DOMPurify.addHook("afterSanitizeAttributes", function (node) { // set all elements owning target to target=_blank + add noopener noreferrer - const target = node.getAttribute("target"); + const target = node.getAttribute("target") if (target) { - node.setAttribute("target", "_blank"); - node.setAttribute("rel", "noopener noreferrer"); + node.setAttribute("target", "_blank") + node.setAttribute("rel", "noopener noreferrer") } - }); + }) } public static purify(src: string): string { return DOMPurify.sanitize(src, { USE_PROFILES: { html: true }, - ADD_ATTR: ["target"] // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener' - }); + ADD_ATTR: ["target"], // Don't remove target='_blank'. Note that Utils.initDomPurify does add a hook which automatically adds 'rel=noopener' + }) } /** @@ -174,7 +174,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be specs: { name: string; defaultValue?: string }[], args: string[] ): Record { - const parsed: Record = {}; + const parsed: Record = {} if (args.length > specs.length) { throw ( "To much arguments for special visualization: got " + @@ -182,23 +182,23 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be " but expected only " + args.length + " arguments" - ); + ) } for (let i = 0; i < specs.length; i++) { - const spec = specs[i]; - let arg = args[i]?.trim(); + const spec = specs[i] + let arg = args[i]?.trim() if (arg === undefined || arg === "") { - arg = spec.defaultValue; + arg = spec.defaultValue } - parsed[spec.name] = arg; + parsed[spec.name] = arg } - return parsed; + return parsed } static EncodeXmlValue(str) { if (typeof str !== "string") { - str = "" + str; + str = "" + str } return str @@ -206,7 +206,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be .replace(//g, ">") .replace(/"/g, """) - .replace(/'/g, "'"); + .replace(/'/g, "'") } /** @@ -215,24 +215,24 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ static asFloat(str): number { if (str) { - const i = parseFloat(str); + const i = parseFloat(str) if (isNaN(i)) { - return undefined; + return undefined } - return i; + return i } - return undefined; + return undefined } public static Upper(str: string) { - return str.substr(0, 1).toUpperCase() + str.substr(1); + return str.substr(0, 1).toUpperCase() + str.substr(1) } public static TwoDigits(i: number) { if (i < 10) { - return "0" + i; + return "0" + i } - return "" + i; + return "" + i } /** @@ -242,37 +242,37 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static Round7(i: number): number { if (i == undefined) { - return undefined; + return undefined } - return Math.round(i * 10000000) / 10000000; + return Math.round(i * 10000000) / 10000000 } public static Times(f: (i: number) => string, count: number): string { - let res = ""; + let res = "" for (let i = 0; i < count; i++) { - res += f(i); + res += f(i) } - return res; + return res } public static TimesT(count: number, f: (i: number) => T): T[] { - const res: T[] = []; + const res: T[] = [] for (let i = 0; i < count; i++) { - res.push(f(i)); + res.push(f(i)) } - return res; + return res } public static NoNull(array: T[]): NonNullable[] { - return array?.filter((o) => o !== undefined && o !== null); + return array?.filter((o) => o !== undefined && o !== null) } public static Hist(array: string[]): Map { - const hist = new Map(); + const hist = new Map() for (const s of array) { - hist.set(s, 1 + (hist.get(s) ?? 0)); + hist.set(s, 1 + (hist.get(s) ?? 0)) } - return hist; + return hist } /** @@ -284,27 +284,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * */ public static NoEmpty(array: string[]): string[] { - const ls: string[] = []; + const ls: string[] = [] if (!array) { - return ls; + return ls } for (const t of array) { if (t === "") { - continue; + continue } - ls.push(t); + ls.push(t) } - return ls; + return ls } public static EllipsesAfter(str: string, l: number = 100) { if (str === undefined || str === null) { - return undefined; + return undefined } if (str.length <= l) { - return str; + return str } - return str.substr(0, l - 3) + "..."; + return str.substr(0, l - 3) + "..." } /** @@ -326,14 +326,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be enumerable: false, configurable: true, get: () => { - delete object[name]; - object[name] = init(); + delete object[name] + object[name] = init() if (whenDone) { - whenDone(); + whenDone() } - return object[name]; - } - }); + return object[name] + }, + }) } /** @@ -350,22 +350,22 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be configurable: true, get: () => { init().then((r) => { - delete object[name]; - object[name] = r; + delete object[name] + object[name] = r if (whenDone) { - whenDone(); + whenDone() } - }); - } - }); + }) + }, + }) } public static FixedLength(str: string, l: number) { - str = Utils.EllipsesAfter(str, l); + str = Utils.EllipsesAfter(str, l) while (str.length < l) { - str = " " + str; + str = " " + str } - return str; + return str } /** @@ -376,30 +376,30 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static Dedup(arr: string[]): string[] { if (arr === undefined) { - return undefined; + return undefined } - const newArr = []; + const newArr = [] for (const string of arr) { if (newArr.indexOf(string) < 0) { - newArr.push(string); + newArr.push(string) } } - return newArr; + return newArr } public static Duplicates(arr: string[]): string[] { if (arr === undefined) { - return undefined; + return undefined } - const newArr = []; - const seen = new Set(); + const newArr = [] + const seen = new Set() for (const string of arr) { if (seen.has(string)) { - newArr.push(string); + newArr.push(string) } - seen.add(string); + seen.add(string) } - return newArr; + return newArr } /** @@ -408,15 +408,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * Utils.Flatten([ [1,2], 3, [4, [5 ,6]] ]) // => [1, 2, 3, 4, [5, 6]] */ public static Flatten(list: (T | T[])[]): T[] { - const result = []; + const result = [] for (const value of list) { if (Array.isArray(value)) { - result.push(...value); + result.push(...value) } else { - result.push(value); + result.push(value) } } - return result; + return result } /** @@ -426,37 +426,37 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static Identical(t1: T[], t2: T[], eq?: (t: T, t0: T) => boolean): boolean { if (t1.length !== t2.length) { - return false; + return false } - eq = (a, b) => a === b; + eq = (a, b) => a === b for (let i = 0; i < t1.length; i++) { if (!eq(t1[i], t2[i])) { - return false; + return false } } - return true; + return true } /** * Utils.MergeTags({k0:"v0","common":"0"},{k1:"v1", common: "1"}) // => {k0: "v0", k1:"v1", common: "1"} */ public static MergeTags(a: any, b: any) { - const t = {}; + const t = {} for (const k in a) { - t[k] = a[k]; + t[k] = a[k] } for (const k in b) { - t[k] = b[k]; + t[k] = b[k] } - return t; + return t } public static SplitFirst(a: string, sep: string): string[] { - const index = a.indexOf(sep); + const index = a.indexOf(sep) if (index < 0) { - return [a]; + return [a] } - return [a.substr(0, index), a.substr(index + sep.length)]; + return [a.substr(0, index), a.substr(index + sep.length)] } /** @@ -478,32 +478,32 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be useLang?: string ): string | undefined { if (txt === undefined) { - return undefined; + return undefined } - const regex = /(.*?){([^}]*)}(.*)/s; + const regex = /(.*?){([^}]*)}(.*)/s - let match = txt.match(regex); + let match = txt.match(regex) if (!match) { - return txt; + return txt } - let result = ""; + let result = "" while (match) { - const [_, normal, key, leftover] = match; - let v = tags === undefined ? undefined : tags[key]; + const [_, normal, key, leftover] = match + let v = tags === undefined ? undefined : tags[key] if (v !== undefined && v !== null) { if (v["toISOString"] != undefined) { // This is a date, probably the timestamp of the object // @ts-ignore - const date: Date = el; - v = date.toISOString(); + const date: Date = el + v = date.toISOString() } if (useLang !== undefined && v?.translations !== undefined) { v = v.translations[useLang] ?? v.translations["*"] ?? - (v.textFor !== undefined ? v.textFor(useLang) : v); + (v.textFor !== undefined ? v.textFor(useLang) : v) } if (v.InnerConstructElement !== undefined) { @@ -512,45 +512,45 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be key, "\nThe value is", v - ); - v = v.InnerConstructElement()?.textContent; + ) + v = v.InnerConstructElement()?.textContent } if (typeof v !== "string") { - v = "" + v; + v = "" + v } - v = v.replace(/\n/g, "
"); + v = v.replace(/\n/g, "
") } else { // v === undefined - v = ""; + v = "" } - result += normal + v; - match = leftover.match(regex); + result += normal + v + match = leftover.match(regex) if (!match) { - result += leftover; + result += leftover } } - return result; + return result } public static LoadCustomCss(location: string) { - const head = document.getElementsByTagName("head")[0]; - const link = document.createElement("link"); - link.id = "customCss"; - link.rel = "stylesheet"; - link.type = "text/css"; - link.href = location; - link.media = "all"; - head.appendChild(link); - console.log("Added custom css file ", location); + const head = document.getElementsByTagName("head")[0] + const link = document.createElement("link") + link.id = "customCss" + link.rel = "stylesheet" + link.type = "text/css" + link.href = location + link.media = "all" + head.appendChild(link) + console.log("Added custom css file ", location) } public static PushList(target: T[], source?: T[]) { if (source === undefined) { - return; + return } - target.push(...source); + target.push(...source) } /** @@ -561,8 +561,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * @constructor * @private */ - private static CleanMergeObject(obj: any){ - if(Array.isArray(obj)){ + private static CleanMergeObject(obj: any) { + if (Array.isArray(obj)) { const result = [] for (const el of obj) { result.push(Utils.CleanMergeObject(el)) @@ -570,14 +570,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return result } if (typeof obj !== "object") { - return obj; + return obj } const newObj = {} for (let objKey in obj) { let cleanKey = objKey - if(objKey.startsWith("+") || objKey.startsWith("=")){ + if (objKey.startsWith("+") || objKey.startsWith("=")) { cleanKey = objKey.substring(1) - }else if(objKey.endsWith("+") || objKey.endsWith("=")){ + } else if (objKey.endsWith("+") || objKey.endsWith("=")) { cleanKey = objKey.substring(0, objKey.length - 1) } newObj[cleanKey] = Utils.CleanMergeObject(obj[objKey]) @@ -633,62 +633,70 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ static Merge(source: Readonly, target: T): T & S { if (target === null) { - return Utils.CleanMergeObject(source); + return Utils.CleanMergeObject(source) } for (const key in source) { if (!source.hasOwnProperty(key)) { - continue; + continue } if (key.startsWith("=")) { - const trimmedKey = key.substr(1); - target[trimmedKey] = source[key]; - continue; + const trimmedKey = key.substr(1) + target[trimmedKey] = source[key] + continue } if (key.startsWith("+") || key.endsWith("+")) { - const trimmedKey = key.replace("+", ""); - const sourceV = source[key]; - const targetV = target[trimmedKey] ?? []; + const trimmedKey = key.replace("+", "") + const sourceV = source[key] + const targetV = target[trimmedKey] ?? [] - let newList: any[]; + let newList: any[] if (key.startsWith("+")) { - if(!Array.isArray(targetV)){ - throw new Error("Cannot concatenate: value to add is not an array: "+JSON.stringify(targetV)) + if (!Array.isArray(targetV)) { + throw new Error( + "Cannot concatenate: value to add is not an array: " + + JSON.stringify(targetV) + ) } if (Array.isArray(sourceV)) { - newList = sourceV.concat(targetV) ?? targetV; + newList = sourceV.concat(targetV) ?? targetV } else { - throw new Error("Could not merge concatenate " + JSON.stringify(sourceV) + " and " + JSON.stringify(targetV)); + throw new Error( + "Could not merge concatenate " + + JSON.stringify(sourceV) + + " and " + + JSON.stringify(targetV) + ) } } else { - newList = targetV.concat(sourceV ?? []); + newList = targetV.concat(sourceV ?? []) } - target[trimmedKey] = newList; - continue; + target[trimmedKey] = newList + continue } - const sourceV = source[key]; + const sourceV = source[key] // @ts-ignore - const targetV = target[key]; + const targetV = target[key] if (typeof sourceV === "object") { if (sourceV === null) { // @ts-ignore - target[key] = null; + target[key] = null } else if (targetV === undefined) { // @ts-ignore - target[key] = Utils.CleanMergeObject(sourceV); + target[key] = Utils.CleanMergeObject(sourceV) } else { - Utils.Merge(sourceV, targetV); + Utils.Merge(sourceV, targetV) } } else { // @ts-ignore - target[key] = Utils.CleanMergeObject(sourceV); + target[key] = Utils.CleanMergeObject(sourceV) } } // @ts-ignore - return target; + return target } /** @@ -706,39 +714,39 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be travelledPath: string[] = [] ): void { if (object == null) { - return; + return } - const head = path[0]; + const head = path[0] if (path.length === 1) { // We have reached the leaf - const leaf = object[head]; + const leaf = object[head] if (leaf !== undefined) { if (Array.isArray(leaf)) { - object[head] = leaf.map((o) => replaceLeaf(o, travelledPath)); + object[head] = leaf.map((o) => replaceLeaf(o, travelledPath)) } else { - object[head] = replaceLeaf(leaf, travelledPath); + object[head] = replaceLeaf(leaf, travelledPath) if (object[head] === undefined) { - delete object[head]; + delete object[head] } } } - return; + return } - const sub = object[head]; + const sub = object[head] if (sub === undefined) { - return; + return } if (typeof sub !== "object") { - return; + return } if (Array.isArray(sub)) { sub.forEach((el, i) => Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]) - ); - return; + ) + return } - Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, head]); + Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, head]) } /** @@ -754,41 +762,41 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be travelledPath: string[] = [] ): { leaf: any; path: string[] }[] { if (object === undefined || object === null) { - return collectedList; + return collectedList } - const head = path[0]; - travelledPath = [...travelledPath, head]; + const head = path[0] + travelledPath = [...travelledPath, head] if (path.length === 1) { // We have reached the leaf - const leaf = object[head]; + const leaf = object[head] if (leaf === undefined || leaf === null) { - return collectedList; + return collectedList } if (Array.isArray(leaf)) { for (let i = 0; i < (leaf).length; i++) { - const l = (leaf)[i]; - collectedList.push({ leaf: l, path: [...travelledPath, "" + i] }); + const l = (leaf)[i] + collectedList.push({ leaf: l, path: [...travelledPath, "" + i] }) } } else { - collectedList.push({ leaf, path: travelledPath }); + collectedList.push({ leaf, path: travelledPath }) } - return collectedList; + return collectedList } - const sub = object[head]; + const sub = object[head] if (sub === undefined || sub === null) { - return collectedList; + return collectedList } if (Array.isArray(sub)) { sub.forEach((el, i) => Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]) - ); - return collectedList; + ) + return collectedList } if (typeof sub !== "object") { - return collectedList; + return collectedList } - return Utils.CollectPath(path.slice(1), sub, collectedList, travelledPath); + return Utils.CollectPath(path.slice(1), sub, collectedList, travelledPath) } /** @@ -828,31 +836,31 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be path: string[] = [] ) { if (json === undefined || json === null) { - return f(json, path); + return f(json, path) } - const jtp = typeof json; + const jtp = typeof json if (isLeaf !== undefined) { if (jtp === "object") { if (isLeaf(json)) { - return f(json, path); + return f(json, path) } } else { - return json; + return json } } else if (jtp === "boolean" || jtp === "string" || jtp === "number") { - return f(json, path); + return f(json, path) } if (Array.isArray(json)) { return json.map((sub, i) => { - return Utils.WalkJson(sub, f, isLeaf, [...path, "" + i]); - }); + return Utils.WalkJson(sub, f, isLeaf, [...path, "" + i]) + }) } - const cp = { ...json }; + const cp = { ...json } for (const key in json) { - cp[key] = Utils.WalkJson(json[key], f, isLeaf, [...path, key]); + cp[key] = Utils.WalkJson(json[key], f, isLeaf, [...path, key]) } - return cp; + return cp } /** @@ -867,94 +875,94 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be path = [] ): void { if (json === undefined) { - return; + return } - const jtp = typeof json; + const jtp = typeof json if (isLeaf !== undefined) { if (jtp !== "object") { - return; + return } if (isLeaf(json)) { - return collect(json, path); + return collect(json, path) } } else if (jtp === "boolean" || jtp === "string" || jtp === "number") { - collect(json, path); - return; + collect(json, path) + return } if (Array.isArray(json)) { json.map((sub, i) => { - return Utils.WalkObject(sub, collect, isLeaf, [...path, i]); - }); - return; + return Utils.WalkObject(sub, collect, isLeaf, [...path, i]) + }) + return } for (const key in json) { - Utils.WalkObject(json[key], collect, isLeaf, [...path, key]); + Utils.WalkObject(json[key], collect, isLeaf, [...path, key]) } } static getOrSetDefault(dict: Map, k: K, v: () => V) { - const found = dict.get(k); + const found = dict.get(k) if (found !== undefined) { - return found; + return found } - dict.set(k, v()); - return dict.get(k); + dict.set(k, v()) + return dict.get(k) } /** * Tries to minify the given JSON by applying some compression */ public static MinifyJSON(stringified: string): string { - stringified = stringified.replace(/\|/g, "||"); + stringified = stringified.replace(/\|/g, "||") - const keys = Utils.knownKeys.concat(Utils.extraKeys); + const keys = Utils.knownKeys.concat(Utils.extraKeys) for (let i = 0; i < keys.length; i++) { - const knownKey = keys[i]; - let code = i; + const knownKey = keys[i] + let code = i if (i >= 124) { - code += 1; // Character 127 is our 'escape' character | + code += 1 // Character 127 is our 'escape' character | } - const replacement = "|" + String.fromCharCode(code); - stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement); + const replacement = "|" + String.fromCharCode(code) + stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement) } - return stringified; + return stringified } public static UnMinify(minified: string): string { if (minified === undefined || minified === null) { - return undefined; + return undefined } - const parts = minified.split("|"); - let result = parts.shift(); - const keys = Utils.knownKeys.concat(Utils.extraKeys); + const parts = minified.split("|") + let result = parts.shift() + const keys = Utils.knownKeys.concat(Utils.extraKeys) for (const part of parts) { if (part == "") { // Empty string => this was a || originally - result += "|"; - continue; + result += "|" + continue } - const i = part.charCodeAt(0); - result += "\"" + keys[i] + "\":" + part.substring(1); + const i = part.charCodeAt(0) + result += '"' + keys[i] + '":' + part.substring(1) } - return result; + return result } public static injectJsonDownloadForTests(url: string, data) { - Utils.injectedDownloads[url] = data; + Utils.injectedDownloads[url] = data } public static async download(url: string, headers?: any): Promise { - const result = await Utils.downloadAdvanced(url, headers); + const result = await Utils.downloadAdvanced(url, headers) if (result["error"] !== undefined) { - throw result["error"]; + throw result["error"] } - return result["content"]; + return result["content"] } /** @@ -971,60 +979,60 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | { error: string; url: string; statuscode?: number } > { if (this.externalDownloadFunction !== undefined) { - return this.externalDownloadFunction(url, headers); + return this.externalDownloadFunction(url, headers) } return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest() xhr.onload = () => { if (xhr.status == 200) { - resolve({ content: xhr.response }); + resolve({ content: xhr.response }) } else if (xhr.status === 302) { - resolve({ redirect: xhr.getResponseHeader("location") }); + resolve({ redirect: xhr.getResponseHeader("location") }) } else if (xhr.status === 509 || xhr.status === 429) { - resolve({ error: "rate limited", url, statuscode: xhr.status }); + resolve({ error: "rate limited", url, statuscode: xhr.status }) } else { resolve({ error: "other error: " + xhr.statusText, url, - statuscode: xhr.status - }); + statuscode: xhr.status, + }) } - }; - xhr.open("GET", url); + } + xhr.open("GET", url) if (headers !== undefined) { for (const key in headers) { - xhr.setRequestHeader(key, headers[key]); + xhr.setRequestHeader(key, headers[key]) } } - xhr.send(); - xhr.onerror = reject; - }); + xhr.send() + xhr.onerror = reject + }) } public static upload(url: string, data, headers?: any): Promise { return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest() xhr.onload = () => { if (xhr.status == 200) { - resolve(xhr.response); + resolve(xhr.response) } else if (xhr.status === 509 || xhr.status === 429) { - reject("rate limited"); + reject("rate limited") } else { - reject(xhr.statusText); + reject(xhr.statusText) } - }; - xhr.open("POST", url); + } + xhr.open("POST", url) if (headers !== undefined) { for (const key in headers) { - xhr.setRequestHeader(key, headers[key]); + xhr.setRequestHeader(key, headers[key]) } } - xhr.send(data); - xhr.onerror = reject; - }); + xhr.send(data) + xhr.onerror = reject + }) } public static async downloadJsonCached( @@ -1032,11 +1040,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be maxCacheTimeMs: number, headers?: any ): Promise { - const result = await Utils.downloadJsonAdvanced(url, headers); + const result = await Utils.downloadJsonAdvanced(url, headers) if (result["content"]) { - return result["content"]; + return result["content"] } - throw result["error"]; + throw result["error"] } public static async downloadJsonCachedAdvanced( @@ -1044,54 +1052,54 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be maxCacheTimeMs: number, headers?: any ): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> { - const cached = Utils._download_cache.get(url); + const cached = Utils._download_cache.get(url) if (cached !== undefined) { if (new Date().getTime() - cached.timestamp <= maxCacheTimeMs) { - return cached.promise; + return cached.promise } } const promise = /*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced( - url, - headers - ); - Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }); - return await promise; + url, + headers + ) + Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }) + return await promise } public static async downloadJson(url: string, headers?: any): Promise { - const result = await Utils.downloadJsonAdvanced(url, headers); + const result = await Utils.downloadJsonAdvanced(url, headers) if (result["content"]) { - return result["content"]; + return result["content"] } - throw result["error"]; + throw result["error"] } public static async downloadJsonAdvanced( url: string, headers?: any ): Promise<{ content: any } | { error: string; url: string; statuscode?: number }> { - const injected = Utils.injectedDownloads[url]; + const injected = Utils.injectedDownloads[url] if (injected !== undefined) { - console.log("Using injected resource for test for URL", url); - return new Promise((resolve, _) => resolve({ content: injected })); + console.log("Using injected resource for test for URL", url) + return new Promise((resolve, _) => resolve({ content: injected })) } const result = await Utils.downloadAdvanced( url, Utils.Merge({ accept: "application/json" }, headers ?? {}) - ); + ) if (result["error"] !== undefined) { - return <{ error: string; url: string; statuscode?: number }>result; + return <{ error: string; url: string; statuscode?: number }>result } - const data = result["content"]; + const data = result["content"] try { if (typeof data === "string") { - return { content: JSON.parse(data) }; + return { content: JSON.parse(data) } } - return { content: data }; + return { content: data } } catch (e) { - console.error("Could not parse ", data, "due to", e, "\n", e.stack); - return { error: "malformed", url }; + console.error("Could not parse ", data, "due to", e, "\n", e.stack) + return { error: "malformed", url } } } @@ -1112,53 +1120,53 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | "image/png" } ) { - const element = document.createElement("a"); - let file; + const element = document.createElement("a") + let file if (typeof contents === "string") { - file = new Blob([contents], { type: options?.mimetype ?? "text/plain" }); + file = new Blob([contents], { type: options?.mimetype ?? "text/plain" }) } else { - file = contents; + file = contents } - element.href = URL.createObjectURL(file); - element.download = fileName; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); + element.href = URL.createObjectURL(file) + element.download = fileName + document.body.appendChild(element) // Required for this to work in FireFox + element.click() } public static ColourNameToHex(color: string): string { - return colors[color.toLowerCase()] ?? color; + return colors[color.toLowerCase()] ?? color } public static HexToColourName(hex: string): string { - hex = hex.toLowerCase(); + hex = hex.toLowerCase() if (!hex.startsWith("#")) { - return hex; + return hex } - const c = Utils.color(hex); + const c = Utils.color(hex) - let smallestDiff = Number.MAX_VALUE; - let bestColor = undefined; + let smallestDiff = Number.MAX_VALUE + let bestColor = undefined for (const color in colors) { if (!colors.hasOwnProperty(color)) { - continue; + continue } - const foundhex = colors[color]; + const foundhex = colors[color] if (typeof foundhex !== "string") { - continue; + continue } if (foundhex === hex) { - return color; + return color } - const diff = this.colorDiff(Utils.color(foundhex), c); + const diff = this.colorDiff(Utils.color(foundhex), c) if (diff > 50) { - continue; + continue } if (diff < smallestDiff) { - smallestDiff = diff; - bestColor = color; + smallestDiff = diff + bestColor = color } } - return bestColor ?? hex; + return bestColor ?? hex } /** @@ -1168,37 +1176,37 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * JSON.stringify(sorted) // => '{"abc":{"a":"a","x":"x"},"def":"def","x":"x"}' */ static sortKeys(o: any) { - const copy = {}; - let keys = Object.keys(o); - keys = keys.sort(); + const copy = {} + let keys = Object.keys(o) + keys = keys.sort() for (const key of keys) { - let v = o[key]; + let v = o[key] if (typeof v === "object") { - v = Utils.sortKeys(v); + v = Utils.sortKeys(v) } - copy[key] = v; + copy[key] = v } - return copy; + return copy } public static async waitFor(timeMillis: number): Promise { return new Promise((resolve) => { - window.setTimeout(resolve, timeMillis); - }); + window.setTimeout(resolve, timeMillis) + }) } public static toHumanTime(seconds): string { - seconds = Math.floor(seconds); - let minutes = Math.floor(seconds / 60); - seconds = seconds % 60; - let hours = Math.floor(minutes / 60); - minutes = minutes % 60; - const days = Math.floor(hours / 24); - hours = hours % 24; + seconds = Math.floor(seconds) + let minutes = Math.floor(seconds / 60) + seconds = seconds % 60 + let hours = Math.floor(minutes / 60) + minutes = minutes % 60 + const days = Math.floor(hours / 24) + hours = hours % 24 if (days > 0) { - return days + "days" + " " + hours + "h"; + return days + "days" + " " + hours + "h" } - return hours + ":" + Utils.TwoDigits(minutes) + ":" + Utils.TwoDigits(seconds); + return hours + ":" + Utils.TwoDigits(minutes) + ":" + Utils.TwoDigits(seconds) } public static DisableLongPresses() { @@ -1209,55 +1217,55 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be // Not compatible with IE < 9 if (e.target["nodeName"] === "INPUT") { - return; + return } - e.preventDefault(); - return false; + e.preventDefault() + return false }, false - ); + ) } public static preventDefaultOnMouseEvent(event: any) { - event?.originalEvent?.preventDefault(); - event?.originalEvent?.stopPropagation(); - event?.originalEvent?.stopImmediatePropagation(); + event?.originalEvent?.preventDefault() + event?.originalEvent?.stopPropagation() + event?.originalEvent?.stopImmediatePropagation() if (event?.originalEvent) { // This is a total workaround, as 'preventDefault' and everything above seems to be not working - event.originalEvent["dismissed"] = true; + event.originalEvent["dismissed"] = true } } public static HomepageLink(): string { if (typeof window === "undefined") { - return "https://mapcomplete.org"; + return "https://mapcomplete.org" } const path = ( window.location.protocol + "//" + window.location.host + window.location.pathname - ).split("/"); - path.pop(); - path.push("index.html"); - return path.join("/"); + ).split("/") + path.pop() + path.push("index.html") + return path.join("/") } public static OsmChaLinkFor(daysInThePast, theme = undefined): string { - const now = new Date(); - const lastWeek = new Date(now.getTime() - daysInThePast * 24 * 60 * 60 * 1000); + const now = new Date() + const lastWeek = new Date(now.getTime() - daysInThePast * 24 * 60 * 60 * 1000) const date = lastWeek.getFullYear() + "-" + Utils.TwoDigits(lastWeek.getMonth() + 1) + "-" + - Utils.TwoDigits(lastWeek.getDate()); - let osmcha_link = `"date__gte":[{"label":"${date}","value":"${date}"}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}]`; + Utils.TwoDigits(lastWeek.getDate()) + let osmcha_link = `"date__gte":[{"label":"${date}","value":"${date}"}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}]` if (theme !== undefined) { osmcha_link = - osmcha_link + "," + `"comment":[{"label":"#${theme}","value":"#${theme}"}]`; + osmcha_link + "," + `"comment":[{"label":"#${theme}","value":"#${theme}"}]` } - return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}"); + return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}") } /** @@ -1267,31 +1275,31 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ static Clone(x: T): T { if (x === undefined) { - return undefined; + return undefined } - return JSON.parse(JSON.stringify(x)); + return JSON.parse(JSON.stringify(x)) } public static ParseDate(str: string): Date { if (str.endsWith(" UTC")) { - str = str.replace(" UTC", "+00"); + str = str.replace(" UTC", "+00") } - return new Date(str); + return new Date(str) } public static selectTextIn(node) { if (document.body["createTextRange"]) { - const range = document.body["createTextRange"](); - range.moveToElementText(node); - range.select(); + const range = document.body["createTextRange"]() + range.moveToElementText(node) + range.select() } else if (window.getSelection) { - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(node); - selection.removeAllRanges(); - selection.addRange(range); + const selection = window.getSelection() + const range = document.createRange() + range.selectNodeContents(node) + selection.removeAllRanges() + selection.addRange(range) } else { - console.warn("Could not select text in node: Unsupported browser."); + console.warn("Could not select text in node: Unsupported browser.") } } @@ -1302,46 +1310,46 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be ): T[] { const withDistance: [T, number][] = ts.map((t) => [ t, - Utils.levenshteinDistance(getName(t), reference) - ]); - withDistance.sort(([_, a], [__, b]) => a - b); - return withDistance.map((n) => n[0]); + Utils.levenshteinDistance(getName(t), reference), + ]) + withDistance.sort(([_, a], [__, b]) => a - b) + return withDistance.map((n) => n[0]) } public static levenshteinDistance(str1: string, str2: string) { const track = Array(str2.length + 1) .fill(null) - .map(() => Array(str1.length + 1).fill(null)); + .map(() => Array(str1.length + 1).fill(null)) for (let i = 0; i <= str1.length; i += 1) { - track[0][i] = i; + track[0][i] = i } for (let j = 0; j <= str2.length; j += 1) { - track[j][0] = j; + track[j][0] = j } for (let j = 1; j <= str2.length; j += 1) { for (let i = 1; i <= str1.length; i += 1) { - const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; + const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1 track[j][i] = Math.min( track[j][i - 1] + 1, // deletion track[j - 1][i] + 1, // insertion track[j - 1][i - 1] + indicator // substitution - ); + ) } } - return track[str2.length][str1.length]; + return track[str2.length][str1.length] } public static MapToObj( d: Map, onValue: (t: V, key: string) => T ): Record { - const o = {}; - const keys = Array.from(d.keys()); - keys.sort(); + const o = {} + const keys = Array.from(d.keys()) + keys.sort() for (const key of keys) { - o[key] = onValue(d.get(key), key); + o[key] = onValue(d.get(key), key) } - return o; + return o } /** @@ -1352,20 +1360,20 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static TransposeMap( d: Record ): Record { - const newD: Record = {}; + const newD: Record = {} for (const k in d) { - const vs = d[k]; + const vs = d[k] for (const v of vs) { - const list = newD[v]; + const list = newD[v] if (list === undefined) { - newD[v] = [k]; // Left: indexing; right: list with one element + newD[v] = [k] // Left: indexing; right: list with one element } else { - list.push(k); + list.push(k) } } } - return newD; + return newD } /** @@ -1374,15 +1382,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static colorAsHex(c: { r: number; g: number; b: number }) { if (c === undefined) { - return undefined; + return undefined } function componentToHex(n) { - const hex = n.toString(16); - return hex.length == 1 ? "0" + hex : hex; + const hex = n.toString(16) + return hex.length == 1 ? "0" + hex : hex } - return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b); + return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b) } /** @@ -1394,90 +1402,90 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static color(hex: string): { r: number; g: number; b: number } { if (hex === undefined) { - return undefined; + return undefined } - hex = hex.replace(/[ \t]/g, ""); + hex = hex.replace(/[ \t]/g, "") if (hex.startsWith("rgba(")) { - const match = hex.match(/rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/); + const match = hex.match(/rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/) if (match == undefined) { - return undefined; + return undefined } - return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) }; + return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) } } if (!hex.startsWith("#")) { - return undefined; + return undefined } if (hex.length === 4) { return { r: parseInt(hex.substr(1, 1), 16), g: parseInt(hex.substr(2, 1), 16), - b: parseInt(hex.substr(3, 1), 16) - }; + b: parseInt(hex.substr(3, 1), 16), + } } return { r: parseInt(hex.substr(1, 2), 16), g: parseInt(hex.substr(3, 2), 16), - b: parseInt(hex.substr(5, 2), 16) - }; + b: parseInt(hex.substr(5, 2), 16), + } } public static asDict( tags: { key: string; value: string | number }[] ): Map { - const d = new Map(); + const d = new Map() for (const tag of tags) { - d.set(tag.key, tag.value); + d.set(tag.key, tag.value) } - return d; + return d } static toIdRecord(ts: T[]): Record { - const result: Record = {}; + const result: Record = {} for (const t of ts) { - result[t.id] = t; + result[t.id] = t } - return result; + return result } public static SetMidnight(d: Date): void { - d.setUTCHours(0); - d.setUTCSeconds(0); - d.setUTCMilliseconds(0); - d.setUTCMinutes(0); + d.setUTCHours(0) + d.setUTCSeconds(0) + d.setUTCMilliseconds(0) + d.setUTCMinutes(0) } public static scrollIntoView(element: HTMLBaseElement) { // Is the element completely in the view? - const parentRect = Utils.findParentWithScrolling(element).getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); + const parentRect = Utils.findParentWithScrolling(element).getBoundingClientRect() + const elementRect = element.getBoundingClientRect() // Check if the element is within the vertical bounds of the parent element - const topIsVisible = elementRect.top >= parentRect.top; - const bottomIsVisible = elementRect.bottom <= parentRect.bottom; - const inView = topIsVisible && bottomIsVisible; + const topIsVisible = elementRect.top >= parentRect.top + const bottomIsVisible = elementRect.bottom <= parentRect.bottom + const inView = topIsVisible && bottomIsVisible if (inView) { - return; + return } - element.scrollIntoView({ behavior: "smooth", block: "nearest" }); + element.scrollIntoView({ behavior: "smooth", block: "nearest" }) } public static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement { // Check if the element itself has scrolling if (element.scrollHeight > element.clientHeight) { - return element; + return element } // If the element does not have scrolling, check if it has a parent element if (!element.parentElement) { - return null; + return null } // If the element has a parent, repeat the process for the parent element - return Utils.findParentWithScrolling(element.parentElement); + return Utils.findParentWithScrolling(element.parentElement) } /** @@ -1488,55 +1496,55 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static sameList(a: ReadonlyArray, b: ReadonlyArray) { if (a == b) { - return true; + return true } if (a === undefined || a === null || b === undefined || b === null) { - return false; + return false } if (a.length !== b.length) { - return false; + return false } for (let i = 0; i < a.length; i++) { - const ai = a[i]; - const bi = b[i]; + const ai = a[i] + const bi = b[i] if (ai == bi) { - continue; + continue } if (ai === bi) { - continue; + continue } - return false; + return false } - return true; + return true } public static SameObject(a: any, b: any) { if (a === b) { - return true; + return true } if (a === undefined || a === null || b === null || b === undefined) { - return false; + return false } if (typeof a === "object" && typeof b === "object") { for (const aKey in a) { if (!(aKey in b)) { - return false; + return false } } for (const bKey in b) { if (!(bKey in a)) { - return false; + return false } } for (const k in a) { if (!Utils.SameObject(a[k], b[k])) { - return false; + return false } } - return true; + return true } - return false; + return false } /** @@ -1548,22 +1556,22 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static splitIntoSubstitutionParts( template: string ): ({ message: string } | { subs: string })[] { - const preparts = template.split("{"); - const spec: ({ message: string } | { subs: string })[] = []; + const preparts = template.split("{") + const spec: ({ message: string } | { subs: string })[] = [] for (const prepart of preparts) { - const postParts = prepart.split("}"); + const postParts = prepart.split("}") if (postParts.length === 1) { // This was a normal part - spec.push({ message: postParts[0] }); + spec.push({ message: postParts[0] }) } else { - const [subs, message] = postParts; - spec.push({ subs }); + const [subs, message] = postParts + spec.push({ subs }) if (message !== "") { - spec.push({ message }); + spec.push({ message }) } } } - return spec; + return spec } /** @@ -1577,40 +1585,40 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be filename: string functionName: string } { - const error = new Error("No error"); - const stack = error.stack.split("\n"); - stack.shift(); // Remove "Error: No error" - const regex = /at (.*) \(([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)\)/; - const stackItem = stack[Math.abs(offset) + 1]; + const error = new Error("No error") + const stack = error.stack.split("\n") + stack.shift() // Remove "Error: No error" + const regex = /at (.*) \(([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)\)/ + const stackItem = stack[Math.abs(offset) + 1] - let functionName: string; - let path: string; - let line: string; - let column: string; - let _: string; - const matchWithFuncName = stackItem.match(regex); + let functionName: string + let path: string + let line: string + let column: string + let _: string + const matchWithFuncName = stackItem.match(regex) if (matchWithFuncName) { - ;[_, functionName, path, line, column] = matchWithFuncName; + ;[_, functionName, path, line, column] = matchWithFuncName } else { const regexNoFuncName: RegExp = new RegExp("at ([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)") - ;[_, path, line, column] = stackItem.match(regexNoFuncName); + ;[_, path, line, column] = stackItem.match(regexNoFuncName) } - const markdownLocation = path.substring(path.indexOf("MapComplete/src") + 11) + "#L" + line; + const markdownLocation = path.substring(path.indexOf("MapComplete/src") + 11) + "#L" + line return { path, functionName, line: Number(line), column: Number(column), markdownLocation, - filename: path.substring(path.lastIndexOf("/") + 1) - }; + filename: path.substring(path.lastIndexOf("/") + 1), + } } private static colorDiff( c0: { r: number; g: number; b: number }, c1: { r: number; g: number; b: number } ) { - return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); + return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b) } } diff --git a/src/assets/contributors.json b/src/assets/contributors.json index 7aee99716..933d38695 100644 --- a/src/assets/contributors.json +++ b/src/assets/contributors.json @@ -1,11 +1,11 @@ { "contributors": [ { - "commits": 6039, + "commits": 6055, "contributor": "Pieter Vander Vennet" }, { - "commits": 408, + "commits": 414, "contributor": "Robin van der Linde" }, { @@ -52,6 +52,10 @@ "commits": 24, "contributor": "Ward" }, + { + "commits": 21, + "contributor": "dependabot[bot]" + }, { "commits": 21, "contributor": "wjtje" @@ -60,10 +64,6 @@ "commits": 21, "contributor": "AlexanderRebai" }, - { - "commits": 20, - "contributor": "dependabot[bot]" - }, { "commits": 19, "contributor": "Niels Elgaard Larsen" @@ -106,11 +106,11 @@ }, { "commits": 11, - "contributor": "RobJN" + "contributor": "Thibault Molleman" }, { - "commits": 10, - "contributor": "Thibault Molleman" + "commits": 11, + "contributor": "RobJN" }, { "commits": 10, diff --git a/src/assets/translators.json b/src/assets/translators.json index 3637f0b3a..d0a89d435 100644 --- a/src/assets/translators.json +++ b/src/assets/translators.json @@ -1,7 +1,7 @@ { "contributors": [ { - "commits": 306, + "commits": 308, "contributor": "kjon" }, { @@ -9,7 +9,7 @@ "contributor": "Pieter Vander Vennet" }, { - "commits": 171, + "commits": 176, "contributor": "paunofu" }, { From 917729bdec9de4f17eb1399a6e3dcbe84194acca Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sat, 30 Sep 2023 13:41:40 +0000 Subject: [PATCH 09/41] Translated using Weblate (German) Currently translated at 98.5% (3131 of 3178 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/de/ --- langs/layers/de.json | 599 +++++++++++++++++++++++++------------------ 1 file changed, 344 insertions(+), 255 deletions(-) diff --git a/langs/layers/de.json b/langs/layers/de.json index 465881a40..d5e76899c 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -35,6 +35,16 @@ "1": { "title": "eine freistehende Posterbox" }, + "10": { + "description": "Verwendet für Werbeschilder, Leuchtreklamen, Logos und institutionelle Eingangsschilder", + "title": "ein Schild" + }, + "11": { + "title": "eine Skulptur" + }, + "12": { + "title": "eine Wandmalerei" + }, "2": { "title": "eine wandmontierte Posterbox" }, @@ -61,16 +71,6 @@ }, "9": { "title": "ein Totem" - }, - "10": { - "description": "Verwendet für Werbeschilder, Leuchtreklamen, Logos und institutionelle Eingangsschilder", - "title": "ein Schild" - }, - "11": { - "title": "eine Skulptur" - }, - "12": { - "title": "eine Wandmalerei" } }, "tagRenderings": { @@ -165,6 +165,9 @@ "1": { "then": "Dies ist ein Brett" }, + "10": { + "then": "Dies ist eine Wandmalerei" + }, "2": { "then": "Dies ist eine Litfaßsäule" }, @@ -188,9 +191,6 @@ }, "9": { "then": "Dies ist ein Totem" - }, - "10": { - "then": "Dies ist eine Wandmalerei" } }, "question": "Welche Art von Werbung ist das?", @@ -205,6 +205,9 @@ "1": { "then": "Brett" }, + "10": { + "then": "Wandmalerei" + }, "2": { "then": "Posterbox" }, @@ -228,9 +231,6 @@ }, "9": { "then": "Totem" - }, - "10": { - "then": "Wandmalerei" } } } @@ -312,6 +312,15 @@ "1": { "then": "Wandbild" }, + "10": { + "then": "Azulejo (spanische dekorative Fliesenarbeit)" + }, + "11": { + "then": "Fliesenarbeit" + }, + "12": { + "then": "Holzschnitzerei" + }, "2": { "then": "Malerei" }, @@ -335,15 +344,6 @@ }, "9": { "then": "Relief" - }, - "10": { - "then": "Azulejo (spanische dekorative Fliesenarbeit)" - }, - "11": { - "then": "Fliesenarbeit" - }, - "12": { - "then": "Holzschnitzerei" } }, "question": "Um welche Art Kunstwerk handelt es sich?", @@ -1837,6 +1837,27 @@ "1": { "question": "Verfügt über einen
Schuko-Stecker ohne Erdungsstift (CEE7/4 Typ F)
" }, + "10": { + "question": "Hat einen
Typ 2 (Mennekes)
Anschluss mit Kabel" + }, + "11": { + "question": "Hat einen
Tesla Supercharger CCS (Typ 2 CSS vonTesla)
Anschluss" + }, + "12": { + "question": "Hat einen
Tesla Supercharger (Destination)
Anschluss" + }, + "13": { + "question": "Hat einen
Tesla Supercharger (Destination) (Typ 2 von Tesla)
Anschluss mit Kabel" + }, + "14": { + "question": "Hat einen
USB-Anschluss zum Aufladen von Telefonen und kleinen Elektrogeräten
" + }, + "15": { + "question": "Hat einen
Bosch Active Connect Anschluss mit 3 Pins
und Kabel" + }, + "16": { + "question": "Hat einen
Bosch Active Connect Anschluss mit 5 Pins
und Kabel" + }, "2": { "question": "Verfügt über einen
europäischen Netzstecker mit Erdungsstift (CEE7/4 Typ E)
Anschluss" }, @@ -1860,27 +1881,6 @@ }, "9": { "question": "Hat einen
Typ 2 CCS (Mennekes)
Anschluss" - }, - "10": { - "question": "Hat einen
Typ 2 (Mennekes)
Anschluss mit Kabel" - }, - "11": { - "question": "Hat einen
Tesla Supercharger CCS (Typ 2 CSS vonTesla)
Anschluss" - }, - "12": { - "question": "Hat einen
Tesla Supercharger (Destination)
Anschluss" - }, - "13": { - "question": "Hat einen
Tesla Supercharger (Destination) (Typ 2 von Tesla)
Anschluss mit Kabel" - }, - "14": { - "question": "Hat einen
USB-Anschluss zum Aufladen von Telefonen und kleinen Elektrogeräten
" - }, - "15": { - "question": "Hat einen
Bosch Active Connect Anschluss mit 3 Pins
und Kabel" - }, - "16": { - "question": "Hat einen
Bosch Active Connect Anschluss mit 5 Pins
und Kabel" } } } @@ -1936,30 +1936,6 @@ "1": { "then": "Schuko-Stecker ohne Erdungsstift (CEE7/4 Typ F)" }, - "2": { - "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" - }, - "3": { - "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" - }, - "4": { - "then": "Chademo-Anschluss" - }, - "5": { - "then": "Chademo-Anschluss" - }, - "6": { - "then": "Typ 1 mit Kabel (J1772)" - }, - "7": { - "then": "Typ 1 mit Kabel (J1772)" - }, - "8": { - "then": "Typ 1 ohne Kabel (J1772)" - }, - "9": { - "then": " Typ 1 ohne Kabel (J1772)" - }, "10": { "then": "Typ 1 CCS (Typ 1 Combo)" }, @@ -1990,6 +1966,9 @@ "19": { "then": "Typ 2 mit Kabel (mennekes)" }, + "2": { + "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" + }, "20": { "then": "Tesla Supercharger CCS (Typ 2 CSS von Tesla)" }, @@ -2020,11 +1999,32 @@ "29": { "then": " Bosch Active Connect mit 3 Pins und Kabel" }, + "3": { + "then": "Europäischer Netzstecker mit Erdungsstift (CEE7/4 Typ E)" + }, "30": { "then": "Bosch Active Connect mit 5 Pins und Kabel" }, "31": { "then": " Bosch Active Connect mit 5 Pins und Kabel" + }, + "4": { + "then": "Chademo-Anschluss" + }, + "5": { + "then": "Chademo-Anschluss" + }, + "6": { + "then": "Typ 1 mit Kabel (J1772)" + }, + "7": { + "then": "Typ 1 mit Kabel (J1772)" + }, + "8": { + "then": "Typ 1 ohne Kabel (J1772)" + }, + "9": { + "then": " Typ 1 ohne Kabel (J1772)" } }, "question": "Welche Ladeanschlüsse gibt es hier?" @@ -3562,6 +3562,15 @@ "1": { "then": "Dieser Radweg hat einen festen Belag" }, + "10": { + "then": "Dieser Radweg besteht aus feinem Schotter" + }, + "11": { + "then": "Der Radweg ist aus Kies" + }, + "12": { + "then": "Dieser Radweg besteht aus Rohboden" + }, "2": { "then": "Der Radweg ist aus Asphalt" }, @@ -3585,15 +3594,6 @@ }, "9": { "then": "Der Radweg ist aus Schotter" - }, - "10": { - "then": "Dieser Radweg besteht aus feinem Schotter" - }, - "11": { - "then": "Der Radweg ist aus Kies" - }, - "12": { - "then": "Dieser Radweg besteht aus Rohboden" } }, "question": "Was ist der Belag dieses Radwegs?", @@ -3642,6 +3642,15 @@ "1": { "then": "Dieser Radweg hat einen festen Belag" }, + "10": { + "then": "Dieser Radweg besteht aus feinem Schotter" + }, + "11": { + "then": "Der Radweg ist aus Kies" + }, + "12": { + "then": "Dieser Radweg besteht aus Rohboden" + }, "2": { "then": "Der Radweg ist aus Asphalt" }, @@ -3665,15 +3674,6 @@ }, "9": { "then": "Der Radweg ist aus Schotter" - }, - "10": { - "then": "Dieser Radweg besteht aus feinem Schotter" - }, - "11": { - "then": "Der Radweg ist aus Kies" - }, - "12": { - "then": "Dieser Radweg besteht aus Rohboden" } }, "question": "Was ist der Belag dieser Straße?", @@ -4217,6 +4217,17 @@ "question": "Verfügt der Aufzug über eine Sprachausgabe?", "questionHint": "Z.B. werden Stockwerke angesagt" }, + "tactile_writing_available": { + "mappings": { + "0": { + "then": "Dieser Aufzug hat eine taktile Schrift in Brailleschrift" + }, + "1": { + "then": "Dieser Aufzug hat keine taktile Schrift" + } + }, + "question": "Hat dieser Aufzug taktile Schrift?" + }, "tactile_writing_language": { "render": { "special": { @@ -4561,6 +4572,19 @@ } } }, + "10": { + "options": { + "0": { + "question": "Keine Bevorzugung von Hunden" + }, + "1": { + "question": "Hunde erlaubt" + }, + "2": { + "question": "Keine Hunde erlaubt" + } + } + }, "2": { "options": { "0": { @@ -4750,30 +4774,6 @@ "1": { "then": "Die Fitness-Station hat ein Schild mit Anweisungen für eine bestimmte Übung." }, - "2": { - "then": "Die Fitness-Station hat eine Einrichtung für Sit-ups." - }, - "3": { - "then": "Die Fitness-Station hat eine Vorrichtung für Liegestütze. In der Regel eine oder mehrere niedrige Reckstangen." - }, - "4": { - "then": "Die Fitness-Station hat Stangen zum Dehnen." - }, - "5": { - "then": "Die Fitness-Station hat eine Vorrichtung für Rückenstrecker (Hyperextensions)." - }, - "6": { - "then": "Die Fitness-Station hat Ringe für Gymnastikübungen." - }, - "7": { - "then": "Die Fitness-Station hat eine horizontale Leiter (Monkey Bars)." - }, - "8": { - "then": "Die Fitness-Station hat eine Sprossenwand zum Klettern." - }, - "9": { - "then": "Die Fitness-Station hat Pfosten für Slalomübungen." - }, "10": { "then": "Die Fitness-Station hat Trittsteine." }, @@ -4804,6 +4804,9 @@ "19": { "then": "Die Fitness-Station hat Kampfseile (battle ropes)." }, + "2": { + "then": "Die Fitness-Station hat eine Einrichtung für Sit-ups." + }, "20": { "then": "Die Fitness-Station hat ein Fahrradergometer." }, @@ -4818,6 +4821,27 @@ }, "24": { "then": "Die Fitness-Station hat eine Slackline." + }, + "3": { + "then": "Die Fitness-Station hat eine Vorrichtung für Liegestütze. In der Regel eine oder mehrere niedrige Reckstangen." + }, + "4": { + "then": "Die Fitness-Station hat Stangen zum Dehnen." + }, + "5": { + "then": "Die Fitness-Station hat eine Vorrichtung für Rückenstrecker (Hyperextensions)." + }, + "6": { + "then": "Die Fitness-Station hat Ringe für Gymnastikübungen." + }, + "7": { + "then": "Die Fitness-Station hat eine horizontale Leiter (Monkey Bars)." + }, + "8": { + "then": "Die Fitness-Station hat eine Sprossenwand zum Klettern." + }, + "9": { + "then": "Die Fitness-Station hat Pfosten für Slalomübungen." } }, "question": "Welche Übungsgeräte gibt es an dieser Fitness-Station?" @@ -4880,6 +4904,9 @@ }, "2": { "options": { + "0": { + "question": "Restaurants und Schnellimbissbetriebe" + }, "1": { "question": "Nur Fastfood-Geschäfte" }, @@ -4888,6 +4915,13 @@ } } }, + "3": { + "options": { + "0": { + "question": "Hat ein vegetarisches Menü" + } + } + }, "4": { "options": { "0": { @@ -4927,6 +4961,21 @@ "1": { "then": "Dies ist eine Pommesbude" }, + "10": { + "then": "Hier werden chinesische Gerichte serviert" + }, + "11": { + "then": "Hier werden griechische Gerichte serviert" + }, + "12": { + "then": "Hier werden indische Gerichte serviert" + }, + "13": { + "then": "Hier werden türkische Gerichte serviert" + }, + "14": { + "then": "Hier werden thailändische Gerichte serviert" + }, "2": { "then": "Bietet vorwiegend Pastagerichte an" }, @@ -4950,21 +4999,6 @@ }, "9": { "then": "Hier werden französische Gerichte serviert" - }, - "10": { - "then": "Hier werden chinesische Gerichte serviert" - }, - "11": { - "then": "Hier werden griechische Gerichte serviert" - }, - "12": { - "then": "Hier werden indische Gerichte serviert" - }, - "13": { - "then": "Hier werden türkische Gerichte serviert" - }, - "14": { - "then": "Hier werden thailändische Gerichte serviert" } }, "question": "Was für Essen gibt es hier?", @@ -5532,6 +5566,61 @@ }, "question": "Wie lautet die Nummer dieses Raums?", "render": "Dieser Raum hat die Raumnummer {ref}" + }, + "room-capacity": { + "question": "Wie viele Personen passen höchstens in diesen Raum?" + }, + "room-type": { + "mappings": { + "0": { + "then": "Dies ist ein Verwaltungsraum" + }, + "1": { + "then": "Dies ist ein Auditorium" + }, + "10": { + "then": "Dies ist ein Labor" + }, + "11": { + "then": "Dies ist eine Bibliothek" + }, + "12": { + "then": "Dies ist ein Umkleideraum" + }, + "14": { + "then": "Dies ist ein Büro" + }, + "15": { + "then": "Dies ist eine Gefängniszelle" + }, + "16": { + "then": "Dies ist ein Restaurant" + }, + "2": { + "then": "Dies ist ein Schlafzimmer" + }, + "3": { + "then": "Dies ist eine Kapelle" + }, + "4": { + "then": "Dies ist ein Klassenzimmer" + }, + "5": { + "then": "Dies ist ein Klassenzimmer" + }, + "6": { + "then": "Dies ist ein Computerraum" + }, + "7": { + "then": "Dies ist ein Konferenzraum" + }, + "8": { + "then": "Dies ist eine Krypta" + }, + "9": { + "then": "Dies ist eine Küche" + } + } } }, "title": { @@ -6183,6 +6272,19 @@ } } }, + "10": { + "options": { + "0": { + "question": "Alle Notizen" + }, + "1": { + "question": "Importnotizen ausblenden" + }, + "2": { + "question": "Nur Importnotizen anzeigen" + } + } + }, "2": { "options": { "0": { @@ -6238,19 +6340,6 @@ "question": "Nur offene Notizen anzeigen" } } - }, - "10": { - "options": { - "0": { - "question": "Alle Notizen" - }, - "1": { - "question": "Importnotizen ausblenden" - }, - "2": { - "question": "Nur Importnotizen anzeigen" - } - } } }, "name": "OpenStreetMap-Hinweise", @@ -6579,6 +6668,21 @@ "1": { "then": "Dies ist ein normaler Stellplatz." }, + "10": { + "then": "Dies ist ein Stellplatz, der für Eltern mit Kindern reserviert ist." + }, + "11": { + "then": "Dies ist ein Stellplatz, der für das Personal reserviert ist." + }, + "12": { + "then": "Dies ist ein Stellplatz, der für Taxis reserviert ist." + }, + "13": { + "then": "Dies ist ein Stellplatz, der für Fahrzeuge mit Anhänger reserviert ist." + }, + "14": { + "then": "Dies ist ein Stellplatz, der für Carsharing reserviert ist." + }, "2": { "then": "Dies ist ein Behindertenstellplatz." }, @@ -6602,21 +6706,6 @@ }, "9": { "then": "Dies ist ein Stellplatz, der für Motorräder reserviert ist." - }, - "10": { - "then": "Dies ist ein Stellplatz, der für Eltern mit Kindern reserviert ist." - }, - "11": { - "then": "Dies ist ein Stellplatz, der für das Personal reserviert ist." - }, - "12": { - "then": "Dies ist ein Stellplatz, der für Taxis reserviert ist." - }, - "13": { - "then": "Dies ist ein Stellplatz, der für Fahrzeuge mit Anhänger reserviert ist." - }, - "14": { - "then": "Dies ist ein Stellplatz, der für Carsharing reserviert ist." } }, "question": "Welche Art von Stellplatz ist dies?" @@ -7629,30 +7718,6 @@ "1": { "question": "Recycling von Batterien" }, - "2": { - "question": "Recycling von Getränkekartons" - }, - "3": { - "question": "Recycling von Dosen" - }, - "4": { - "question": "Recycling von Kleidung" - }, - "5": { - "question": "Recycling von Speiseöl" - }, - "6": { - "question": "Recycling von Motoröl" - }, - "7": { - "question": "Recycling von Leuchtstoffröhren" - }, - "8": { - "question": "Recycling von Grünabfällen" - }, - "9": { - "question": "Recycling von Glasflaschen" - }, "10": { "question": "Recycling von Glas" }, @@ -7683,11 +7748,35 @@ "19": { "question": "Recycling von Restabfällen" }, + "2": { + "question": "Recycling von Getränkekartons" + }, "20": { "question": "Recycling von Druckerpatronen" }, "21": { "question": "Recycling von Fahrrädern" + }, + "3": { + "question": "Recycling von Dosen" + }, + "4": { + "question": "Recycling von Kleidung" + }, + "5": { + "question": "Recycling von Speiseöl" + }, + "6": { + "question": "Recycling von Motoröl" + }, + "7": { + "question": "Recycling von Leuchtstoffröhren" + }, + "8": { + "question": "Recycling von Grünabfällen" + }, + "9": { + "question": "Recycling von Glasflaschen" } } }, @@ -7755,30 +7844,6 @@ "1": { "then": "Getränkekartons können hier recycelt werden" }, - "2": { - "then": "Dosen können hier recycelt werden" - }, - "3": { - "then": "Kleidung kann hier recycelt werden" - }, - "4": { - "then": "Speiseöl kann hier recycelt werden" - }, - "5": { - "then": "Motoröl kann hier recycelt werden" - }, - "6": { - "then": "Hier können Leuchtstoffröhren recycelt werden" - }, - "7": { - "then": "Grünabfälle können hier recycelt werden" - }, - "8": { - "then": "Bio-Abfall kann hier recycelt werden" - }, - "9": { - "then": "Glasflaschen können hier recycelt werden" - }, "10": { "then": "Glas kann hier recycelt werden" }, @@ -7809,6 +7874,9 @@ "19": { "then": "Schuhe können hier recycelt werden" }, + "2": { + "then": "Dosen können hier recycelt werden" + }, "20": { "then": "Elektrokleingeräte können hier recycelt werden" }, @@ -7823,6 +7891,27 @@ }, "24": { "then": "Fahrräder können hier recycelt werden" + }, + "3": { + "then": "Kleidung kann hier recycelt werden" + }, + "4": { + "then": "Speiseöl kann hier recycelt werden" + }, + "5": { + "then": "Motoröl kann hier recycelt werden" + }, + "6": { + "then": "Hier können Leuchtstoffröhren recycelt werden" + }, + "7": { + "then": "Grünabfälle können hier recycelt werden" + }, + "8": { + "then": "Bio-Abfall kann hier recycelt werden" + }, + "9": { + "then": "Glasflaschen können hier recycelt werden" } }, "question": "Was kann hier recycelt werden?" @@ -8626,6 +8715,12 @@ "1": { "then": "Diese Straßenlaterne verwendet LEDs" }, + "10": { + "then": "Diese Straßenlaterne verwendet Hochdruck-Natriumdampflampen (orange mit weiß)" + }, + "11": { + "then": "Diese Straßenlaterne wird mit Gas beleuchtet" + }, "2": { "then": "Diese Straßenlaterne verwendet Glühlampenlicht" }, @@ -8649,12 +8744,6 @@ }, "9": { "then": "Diese Straßenlaterne verwendet Niederdruck-Natriumdampflampen (einfarbig orange)" - }, - "10": { - "then": "Diese Straßenlaterne verwendet Hochdruck-Natriumdampflampen (orange mit weiß)" - }, - "11": { - "then": "Diese Straßenlaterne wird mit Gas beleuchtet" } }, "question": "Mit welcher Art von Beleuchtung arbeitet diese Straßenlaterne?" @@ -9748,6 +9837,27 @@ "1": { "question": "Verkauf von Getränken" }, + "10": { + "question": "Verkauf von Milch" + }, + "11": { + "question": "Verkauf von Brot" + }, + "12": { + "question": "Verkauf von Eiern" + }, + "13": { + "question": "Verkauf von Käse" + }, + "14": { + "question": "Verkauf von Honig" + }, + "15": { + "question": "Verkauf von Kartoffeln" + }, + "16": { + "question": "Verkauf von Blumen" + }, "2": { "question": "Verkauf von Süßigkeiten" }, @@ -9771,27 +9881,6 @@ }, "9": { "question": "Verkauf von Fahrradschläuchen" - }, - "10": { - "question": "Verkauf von Milch" - }, - "11": { - "question": "Verkauf von Brot" - }, - "12": { - "question": "Verkauf von Eiern" - }, - "13": { - "question": "Verkauf von Käse" - }, - "14": { - "question": "Verkauf von Honig" - }, - "15": { - "question": "Verkauf von Kartoffeln" - }, - "16": { - "question": "Verkauf von Blumen" } } } @@ -9832,30 +9921,6 @@ "1": { "then": "Süßigkeiten werden verkauft" }, - "2": { - "then": "Lebensmittel werden verkauft" - }, - "3": { - "then": "Zigaretten werden verkauft" - }, - "4": { - "then": "Kondome werden verkauft" - }, - "5": { - "then": "Kaffee wird verkauft" - }, - "6": { - "then": "Trinkwasser wird verkauft" - }, - "7": { - "then": "Zeitungen werden verkauft" - }, - "8": { - "then": "Fahrradschläuche werden verkauft" - }, - "9": { - "then": "Milch wird verkauft" - }, "10": { "then": "Brot wird verkauft" }, @@ -9879,6 +9944,30 @@ }, "18": { "then": "Fahrscheine werden verkauft" + }, + "2": { + "then": "Lebensmittel werden verkauft" + }, + "3": { + "then": "Zigaretten werden verkauft" + }, + "4": { + "then": "Kondome werden verkauft" + }, + "5": { + "then": "Kaffee wird verkauft" + }, + "6": { + "then": "Trinkwasser wird verkauft" + }, + "7": { + "then": "Zeitungen werden verkauft" + }, + "8": { + "then": "Fahrradschläuche werden verkauft" + }, + "9": { + "then": "Milch wird verkauft" } }, "question": "Was wird in diesem Automaten verkauft?", @@ -10215,4 +10304,4 @@ } } } -} \ No newline at end of file +} From af2c6f47829dd91e5fa0ac4dad2f23e18ebf1d3f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 1 Oct 2023 11:58:11 +0200 Subject: [PATCH 10/41] Themes: add filters for 'wifi' and 'electricity' --- assets/layers/cafe_pub/cafe_pub.json | 4 +++- assets/layers/filters/filters.json | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/assets/layers/cafe_pub/cafe_pub.json b/assets/layers/cafe_pub/cafe_pub.json index c61d451e2..5ae5287d9 100644 --- a/assets/layers/cafe_pub/cafe_pub.json +++ b/assets/layers/cafe_pub/cafe_pub.json @@ -296,7 +296,9 @@ "filter": [ "open_now", "accepts_cash", - "accepts_cards" + "accepts_cards", + "has_internet", + "has_electricity" ], "deletion": { "softDeletionTags": { diff --git a/assets/layers/filters/filters.json b/assets/layers/filters/filters.json index 8d7dc27d1..55fe99196 100644 --- a/assets/layers/filters/filters.json +++ b/assets/layers/filters/filters.json @@ -272,6 +272,33 @@ "osmTags": "dog=no" } ] + }, + { + "id": "has_internet", + "options": [ + { + "question": { + "en": "Offers internet" + }, + "osmTags": { + "or": [ + "internet_access=wlan", + "internet_access=yes", + "internet_access=wired" + ] + } + } + ] + }, + {"id": "has_electricity", + "options": [ + { + "question": { + "en": "Offers electricity" + }, + "osmTags": "service:electricity=yes" + } + ] } ] } From e2d3a71c5b3a15b5a21db112baaf5a13dec55a3c Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 1 Oct 2023 12:13:09 +0200 Subject: [PATCH 11/41] Themes: add minzoom to cafe_pub --- assets/layers/cafe_pub/cafe_pub.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/layers/cafe_pub/cafe_pub.json b/assets/layers/cafe_pub/cafe_pub.json index 5ae5287d9..3684d2c0d 100644 --- a/assets/layers/cafe_pub/cafe_pub.json +++ b/assets/layers/cafe_pub/cafe_pub.json @@ -293,6 +293,7 @@ "internet-ssid", "reviews" ], + "minzoom": 12, "filter": [ "open_now", "accepts_cash", From 5ea08040d7ec40abaabf849ef20771c1a5112637 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 2 Oct 2023 00:15:49 +0200 Subject: [PATCH 12/41] Fix: enable external data sources from the ELI --- scripts/generateLayouts.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 14793d226..bbeaa533b 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -10,10 +10,11 @@ import ScriptUtils from "./ScriptUtils" import { Utils } from "../src/Utils" import SpecialVisualizations from "../src/UI/SpecialVisualizations" import Constants from "../src/Models/Constants" -import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" +import { AvailableRasterLayers, EditorLayerIndexProperties, RasterLayerPolygon } from "../src/Models/RasterLayers"; import { ImmutableStore } from "../src/Logic/UIEventSource" import * as crypto from "crypto" - +import * as eli from "../src/assets/editor-layer-index.json" +import dom from "svelte/types/compiler/compile/render_dom"; const sharp = require("sharp") const template = readFileSync("theme.html", "utf8") const codeTemplate = readFileSync("src/index_theme.ts.template", "utf8") @@ -207,6 +208,28 @@ function asLangSpan(t: Translation, tag = "span"): string { let previousSrc: Set = new Set() +let eliUrlsCached : string[] +function eliUrls(): string[]{ + if(eliUrlsCached){ + return eliUrlsCached + } + const urls: string[] = [] + const regex =/{switch:([^}]+)}/ + for (const feature of eli.features) { + const url = ( feature).properties.url + const match = url.match(regex) + if(match){ + const domains = match[1].split(",") + const subpart = match[0] + urls.push(...domains.map(d => url.replace(subpart, d))) + }else{ + urls.push(url) + } + } + eliUrlsCached = urls + return urls +} + function generateCsp( layout: LayoutConfig, options: { @@ -221,6 +244,7 @@ function generateCsp( "https://api.openstreetmap.org", "https://pietervdvn.goatcounter.com", ].concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) + .concat(...eliUrls()) const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) const hosts = new Set() From 701f03397e0339cba98fc776f06b38ccb13511b2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 3 Oct 2023 16:29:45 +0200 Subject: [PATCH 13/41] Themes: enable reviews on pharmacies-theme --- assets/layers/pharmacy/pharmacy.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/layers/pharmacy/pharmacy.json b/assets/layers/pharmacy/pharmacy.json index 95d8ffbb3..e0b646842 100644 --- a/assets/layers/pharmacy/pharmacy.json +++ b/assets/layers/pharmacy/pharmacy.json @@ -47,6 +47,7 @@ "minzoom": 13, "tagRenderings": [ "images", + "reviews", { "id": "name", "freeform": { From 79c543269927378f109e30caad0cf32d6c91b0cc Mon Sep 17 00:00:00 2001 From: paunofu Date: Fri, 29 Sep 2023 11:27:31 +0000 Subject: [PATCH 14/41] Translated using Weblate (Catalan) Currently translated at 100.0% (503 of 503 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/ca/ --- langs/ca.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/langs/ca.json b/langs/ca.json index 5ff6fcdd5..4178af93c 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -197,6 +197,7 @@ "example": "Exemple", "examples": "Exemples", "fewChangesBefore": "Contesta unes quantes preguntes sobre elements existents abans d'afegir-ne un de nou.", + "geopermissionDenied": "Es va denegar l'ús de la geolocalització", "getStartedLogin": "Entra a OpenStreetMap per començar", "getStartedNewAccount": " o crea un nou compte", "goToInbox": "Obrir missatges", @@ -556,11 +557,16 @@ "reviews": { "affiliated_reviewer_warning": "(Ressenya afiliada)", "attribution": "Les ressenyes funcionen gràcies a Mangrove Reviews i estan disponibles sota CC-BY 4.0.", - "i_am_affiliated": "Tinc alguna filiació amb aquest objecte
Marca-ho si n'ets cap, creador, treballador, …", + "i_am_affiliated": "Tinc alguna filiació amb aquest objecte", + "i_am_affiliated_explanation": "Marqueu si sou propietari, creador, empleat,…", "name_required": "És requerit un nom per mostrar i crear revisions", "no_reviews_yet": "No hi ha revisions encara. Sigues el primer a escriure'n una i ajuda al negoci i a les dades lliures!", + "question": "Com valoraries {title()}?", + "question_opinion": "Com va ser la vostra experiència?", + "reviewing_as": "Ressenyant com a {nickname}", + "reviewing_as_anonymous": "Ressenyant com a anònim", "save": "Desar", - "saved": "Revisió compartida. Gràcies per compartir!", + "saved": "Ressenya compartida. Gràcies per compartir!", "saving_review": "Desant…", "title": "{count} revisions", "title_singular": "Una revisió", From add3191b6d4b256e88938ee2705751a1c2dea8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Podhoreck=C3=BD?= Date: Sat, 30 Sep 2023 12:31:36 +0000 Subject: [PATCH 15/41] Translated using Weblate (Czech) Currently translated at 71.1% (358 of 503 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/cs/ --- langs/cs.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/langs/cs.json b/langs/cs.json index fe833def6..a2f0f24f5 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -1,4 +1,7 @@ { + "advanced": { + "title": "Pokročilé funkce" + }, "centerMessage": { "loadingData": "Načítání dat…", "ready": "Hotovo!", From c769f46bc4e462e981311096200d372b478f2824 Mon Sep 17 00:00:00 2001 From: kjon Date: Tue, 3 Oct 2023 08:14:29 +0000 Subject: [PATCH 16/41] Translated using Weblate (German) Currently translated at 100.0% (503 of 503 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/de/ --- langs/de.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/langs/de.json b/langs/de.json index 93c229561..8c37d23bb 100644 --- a/langs/de.json +++ b/langs/de.json @@ -197,6 +197,7 @@ "example": "Beispiel", "examples": "Beispiele", "fewChangesBefore": "Bitte beantworten Sie einige Fragen zu bestehenden Objekten, bevor Sie ein neues Objekt hinzufügen.", + "geopermissionDenied": "Die Verwendung der Standortbestimmung wurde verweigert", "getStartedLogin": "Bei OpenStreetMap anmelden, um loszulegen", "getStartedNewAccount": " oder ein neues Konto anlegen", "goToInbox": "Posteingang öffnen", @@ -417,7 +418,20 @@ "respectPrivacy": "Bitte respektieren Sie die Privatsphäre. Fotografieren Sie weder Personen noch Nummernschilder. Benutzen Sie keine urheberrechtlich geschützten Quellen wie z.B. Google Maps oder Google Streetview.", "toBig": "Ihr Bild ist mit {actual_size} zu groß. Die maximale Bildgröße ist {max_size}", "upload": { - "failReasons": "Keine Internetverbindung" + "failReasons": "Keine Internetverbindung", + "failReasonsAdvanced": "Alternativ dazu können Sie einstellen, dass Ihr Browser und Ihre Erweiterungen die APIs von Drittanbietern nicht blockieren.", + "multiple": { + "done": "{count} Bilder erfolgreich hochgeladen. Vielen Dank!", + "partiallyDone": "{count} Bilder werden hochgeladen, {done} Bilder erledigt…", + "someFailed": "Entschuldigung, {count} Bilder konnten nicht hochgeladen werden", + "uploading": "{count} Bilder werden hochgeladen…" + }, + "one": { + "done": "Bild erfolgreich hochgeladen. Vielen Dank!", + "failed": "Entschuldigung, das Bild konnte nicht hochgeladen werden", + "retrying": "Das Bild wird wiederholt hochgeladen…", + "uploading": "Das Bild wird hochgeladen…" + } }, "uploadDone": "Ihr Bild wurde hinzugefügt. Vielen Dank für Ihre Hilfe!", "uploadFailed": "Das Bild konnte nicht hochladen werden. Haben Sie eine aktive Internetverbindung und sind APIs von Dritten erlaubt? Der Brave Browser oder UMatrix blockieren diese eventuell.", @@ -504,7 +518,9 @@ }, "plantDetection": { "back": "Zurück zur Artenübersicht", + "button": "Automatische Erkennung der Pflanzenart durch die KI von Plantnet.org", "confirm": "Arten auswählen", + "done": "Die Art wurde übernommen", "error": "Bei der Erkennung der Baumart ist ein Fehler aufgetreten: {error}", "howTo": { "intro": "Für optimale Ergebnisse,", @@ -521,7 +537,8 @@ "poweredByPlantnet": "Bereitgestellt von plantnet.org", "querying": "Abfrage bei plantnet.org mit {length} Fotos", "seeInfo": "Weitere Informationen über diese Art", - "takeImages": "Machen Sie Fotos vom Baum, um die Baumart automatisch zu erkennen" + "takeImages": "Machen Sie Fotos vom Baum, um die Baumart automatisch zu erkennen", + "tryAgain": "Wähle eine andere Art" }, "privacy": { "editing": "Ihre Änderungen werden auf OpenStreetMap gespeichert und sind öffentlich zugänglich. Ein mit MapComplete erstellter Änderungssatz enthält folgende Daten:
  • Ihre Änderungen
  • Ihren Benutzernamen
  • Den Zeitpunkt Ihrer Änderungen
  • Das bei Ihren Änderungen verwendete MapComplete-Thema
  • Die Sprache Ihrer Benutzeroberfläche
  • Ihre Entfernung zu den geänderten Objekten. Dadurch kann festgestellt werden, ob die Änderungen vor Ort gespeichert wurden
Ausführliche Informationen finden Sie in den Datenschutzbestimmungen auf OpenStreetMap.org. Wir möchten Sie daran erinnern, dass Sie zur Anmeldung einen fiktiven Namen verwenden können.", @@ -540,11 +557,16 @@ "reviews": { "affiliated_reviewer_warning": "(Partner-Rezension)", "attribution": "Rezensionen von Mangrove Reviews sind unter CC-BY 4.0 verfügbar.", - "i_am_affiliated": "Ich bin an diesem Objekt beteiligt
Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind", + "i_am_affiliated": "Ich bin mit diesem Objekt vertraut", + "i_am_affiliated_explanation": "Prüfung, ob Sie der Eigentümer, Ersteller, Angestellter, … sind", "name_required": "Der Name des Objekts ist erforderlich, um Bewertungen zu erstellen und anzuzeigen", "no_reviews_yet": "Es gibt noch keine Bewertungen. Hilf mit der ersten Bewertung dem Geschäft und der Open Data Bewegung!", + "question": "Wie bewerten Sie {title()}?", + "question_opinion": "Wie war Ihre Erfahrung?", + "reviewing_as": "Als {nickname} bewerten", + "reviewing_as_anonymous": "Anonym bewerten", "save": "Speichern", - "saved": "Bewertung gespeichert. Danke fürs Teilen!", + "saved": "Bewertung gespeichert. Danke fürs Teilen!", "saving_review": "Speichern…", "title": "{count} Rezensionen", "title_singular": "Eine Rezension", From 83d936d98c05384c5a0e347ab230e969dab5b034 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 3 Oct 2023 20:09:19 +0200 Subject: [PATCH 17/41] Override keys --- src/Logic/Osm/OsmConnection.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 13a029439..ecf29dcf2 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -72,11 +72,8 @@ export class OsmConnection { 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._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( From a71b987506bb2edd0daa2ba6804169fa61b790a2 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 3 Oct 2023 20:09:39 +0200 Subject: [PATCH 18/41] Add Relatify icon (#1633) --- .../layers/transit_routes/license_info.json | 12 +++++++++++ assets/layers/transit_routes/relatify.png | Bin 0 -> 64248 bytes .../transit_routes/relatify.png.license | 2 ++ .../layers/transit_routes/transit_routes.json | 20 ++++++++++++++++-- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 assets/layers/transit_routes/license_info.json create mode 100644 assets/layers/transit_routes/relatify.png create mode 100644 assets/layers/transit_routes/relatify.png.license diff --git a/assets/layers/transit_routes/license_info.json b/assets/layers/transit_routes/license_info.json new file mode 100644 index 000000000..2af5aeef8 --- /dev/null +++ b/assets/layers/transit_routes/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "relatify.png", + "license": "AGPLv3", + "authors": [ + "Kamil Monicz" + ], + "sources": [ + "https://github.com/Zaczero/osm-relatify/blob/main/web/static/img/icon.png" + ] + } +] \ No newline at end of file diff --git a/assets/layers/transit_routes/relatify.png b/assets/layers/transit_routes/relatify.png new file mode 100644 index 0000000000000000000000000000000000000000..39b23abdec37c988afc1065ea40f65e6359081b5 GIT binary patch literal 64248 zcmeEsWm}tF&~32d?(XhV+-ZT}6nA%bcQ5X41q#L8B{%^J6nB^6?sn+&p7%$b>pEYO ze94`i*)wa_npsgw3R1`j1PA~C02wI#>7i0KbT-dM=-= zdHE7sxIMg2dYPZir83FZGoN=P^TzG)y65;&`iS2_Plr;Un9--;AlL=HA=o{Bv$Uee zdT2gLaDdYZJb4!1=2d{yA{urw9*#TpjMmVqPxGnSU8 z6vq+&0>SdhN8f2MU|6QJHI-S>Ckui?5n}jvJfn`jhHv-ILJWHv0M7(v*Uh_5$)(%m z2yRQF%JEHE>xdT7tqOgqQj{jJ9NLFXL%LLjOIbz|BYrQ&P~VU)tQ-Y5Pr^bZL5>Fi!_8M3y5KVHTx#I z|0A$y5}B9WdI!-0ex_c09;!b`CJ2cPVf#CJk4w&m4W!ab7Gv8}$0~$zIu(>xSZ9bh zl01wiyp9H=LwE7ZwYFxvy4m2h<@%fTkS3&I%o3^v90nrxH4$zOnXj(`Xz4bgBHC$A zl)FnjS26pL1^Ef3aePjP8{$GQpv)Jtz|~)&u*jnTO-vTa0pZU+(qjp_Vki1F=+mAQ zYk!=`*(1aM9Coj|%LD#;!Z}ATajS^aXb6c~2Uk>5z?jP_+Q4LrE_Mmxp%xcD(A{M& zod$3?X=%k8L=_e8v1tsqO+^?HCaV;{62&SCS0<3;sxJ9JJ_6!P#edfG@{gjTlD8VG`#lBPD>W{(K*f>u(D(iu4gTNdY_ybtmuZ%OeE$vV zcNV|3yZZy#ZW>^`*a_JuilAJSJj}G$X3&9t<<^;q9d*4(*T0N&(R`8)2V#b@%Bd@e zsAO=G51GM;FQ7+ubdhyr*Dn0V3s`~%x%s~-6hW`8LgbV_14H%l8XmHsmEa70ppo&f zbck8s%A-7d1&qUTVTOo&AwOyXDF%k^M0*&P1?lnf5dwD{b0`i1TrB(WT}+iufV{^= zlFSFV(Bxv{=ODr|-9Qs~WBO3g7(tfXg63WJz2H-@?m(z@tGBJ_@hY%VlO&OKkwV<0 z5*CzEcx2hzC1N^HwWo7UbP$hPfB?6{1&{@*=*rTZrP_VJmp~N-&#`c@2?2gsO&1Wu zLqIa)FD8V2RE6;Jkci;?2pY<+3qGY*Z-!dI;m}InbZ^knb z0t_L+=Z~kidXC-U?et;p#}^Qam1nR zN{}Duv8;Mm7IjaE;kmj7!4@aK-LT06WV}VFm}v^c>JyclLN3`VC!B7nh7lP$o9Dm7 z6PmyWvPX!Iz58yxxJdXrX@knLT&?nF;>i9MyNKoX(?HO9Nk3gseGP%krve-miW@=i z9)&UzY!1>Ih228q{rfs>)eh%KgtP8l98=4_Y9>9J*Hp$HooR{9 znX8VHc<0{s*^39b0_NT2C#cReP_85D5y2H)LlJbU@AGUB4FAl*r_BHGU40E&;yK z{CD)MlX1>}4FV-YkuR=?YPTAbSS2FW);~ni{8?pnRjNI6K*PMxz{9D@k4{X00oFR5 zLO}0e-EG;p;P{K+;X8CeM||TXe29;%`t*GkJh^IB98p8(PMk(cXKv6xP#-UN=5l_a zCcn=~H2OvjZp}`8=!0e%l30#FZlt2j5Csv6O5LReew;-B z>m*>BinfrVFiORVE!WQgz7HHq@gQhbNkNsbFnk(8GM%pSxo?g=3nG&+%F=bpAS0GR z532+zldIu|t-cuXE=@@PLw0i|BKnxj{5@7$-o87~IZC(HAwSmL&5h55&jm?21+O+9 z=inu4I$yoJI>AhUEgUF=qZ(UMFJs^TZ<@o0pd&Lkh z514zIo(*C6Nsk)@ELu_=TA1&Okjy1I5b5~(bobuR-im8}3Z|_6kY@U2Gd54VpVZJn zyu+$B4^~nz_W8^1J|$|V?)E8InaWM$1iWCp$iK6vq&66Gs3l+Bwv)l>w{%8j79_uN3`>8p z&qEo?^|UMVZubSU{>DD4FJ?AdlPb)9JAqKT)8yHh!Es&hM?vj?w1h8Uhw9(}(s!*v z>c9}d)GL4UG4RfK^Efh6x)ZA2SRe_k_8tDspx#F_@)?wST*)2C?q}=+ftSoLHIj4u z5bp({!U(*pmYR;Wc|x-;j_86LZ1b9Si_s0rxC4g_@FH?ak5{`LC|;q3ISnB@A-CIzvgI1poRUX|J9UWn$> zQ1ymKNQ>W3VW8oy*cF=MbZq{cTy{trUNg)lxpUuLgph~gE?hLpRKFlign zqqa_Ezar8|Y~-V5nB$gLUKGJM5eCp4f7u0b&<3^!W^osU{!`(dD69VlF0B@S86fUv zmWcQgdvkJ%ij;lmL~3UMC5YWoU33o_|Ezu5>C7{~`3-y9I5xmL4RG@qESDTcCk<&( zRuHz^LkQ)`{KKu{YWh>_p3|@~@#G9@H?Z-SThB%;;EFrgHg-TGKYb?$A7VisS8^VU zEGBt0LiiM$PT`1^h9TFuJ%#kl(GTfQ_n2Ne@pjB)Erld;V*l$NJvm5Kqc*n&&*T z6^Q4nU%+GfpTAz3-yJ==L6h_Q)F94^%@Oqw)9KHTgT{XKHS>Ol=3ibNZRvpgkU>WY zWa?KsDbMrTdVv2p$E%YY&i!RzO%5Qx>b4c+p#X42^9H2sHhq6RV^MhX2?344r`QVIw<3Pba7p!D^1r=+^cX&ePTG>k1d6lEp3M`nVcQo*_Azl!a_vZgYpz# z^o}SZ^Q)xOfuAzT{rL!smHWN_RZ#XYG->xje(Gjyhb6y*&+K?8j0AFNNA|zO8J6j6e(}qI$wo7{u*NZkbj}Q9cc&rzNAzw+=kVilN;@qmpx5Sx(jopr6qq*m0J4oAyv7v+PTKZ8 zM#DpHJ*%Lqf}1e6uKvXR(UPaZ;5#)pF8=1f1oFQDNP~aMW!+_12DUN@c+jC%*1baf z$4I@mP04vBJ>Oe1Dj!@qRt$mJlj7gX@4YndF?_v%9(z706dnxOBteY%mz9=@jT{B; zGus;nS-r1^pZ+fXu3g?KH(uw~1~22=$(=7U09+PF;WK?fUWF;H@5gz}Q}s@E4$qJ7 zTVpp3UPEVn?I^sjo(<$s#=P#LPe z5HzgaXhL%mNCTmlhJ^*a9{7^WPU_nZ>WVlL^OdvGKiXrRFJJkrDMWW_20I-U!MAB_ zv@O8t{_Q`?Dg;j@qpr)ZXcw4PC3e+0DDVH>OEg;?Vx9@v^MPKd(W0g&P`Dij$DS4J zv}^-~q*U4|OH1q>2#{1~6GkX&Y*^07YXAoB*J783dFlci>UA0CflC?s{%!{8`M1mT zTcXniO~jhTg8m(~CV=Y0Ob}$!nv3jb$N&KM?E#`N+@H(s^L!jfzbbJ={PBGIbiYeZEf*$+=7ac$HiWO9r6yw78oBS)tmauKb`2iTd_fIl=Xy+YUuP zf)xFBWy~xHF(@9Oak6)?4s8#027OkVpGIswg31l|?g%|=QBo9!0yT@cDc}t$9%c=< z;u;@1jMsq64Q+@b+wYN>Ni8>l&C{Hy5Ig!N3z()71&%I!kDx(i^*%FzQM{iSBZ7DV zJP0{z0VA32@t}OsTCqJpd3pqJL6byU`suYb0WX}a>QxLk!_}vcViRe`AmS%AjX3`! zR|d3i_WLKcX69d0Vl<@0IH;!1r?npk8Rdg`h^p=x4PIi;)2sYgq4Hqt8$Il6qhsObs^1S&UQ~ObCUU zX9$0uTLR8G#IK~r0jcWCtN&#eIO@FKRU1lkb=^Jw)XHr#!u9%ZyB!+|m4XI7FCFiQ z)NZzK{_wiQ<~!6;hi{i|OMqhcyVvI5?#{vzEpFQw&tmp7Udh3owqYFb+ShB2c<13B zUQ?A5-6$?CEu;S`z>kL*sJ)9p9e;=aS1&}V4YV%bdGzG+1N^0ltSV(;pR9-}HcaFB z;p`pa0pLe!#EJqt3-SJyRQnGrPd_I*vR69RVWR6ocRg%$9xcT3@BD%cM=NkX_&+`6 zQ^Ia3oS6}Eyr^qVc8z=qt_QU<$$P{dt8AhKjveO9d-#F7HlYn|4Nxljyio_fT~&rC z6}0&Ah@1RVHzz`5vMKR{xU`jnC=~b^M4(g@WSk_c?4mFTnPnzCpf}qm zA0VU=xpY#@&kV?p=bK8*X+t5jh4bLbW|YZu8C#b<)q{nck};Xz~}b&LLtlK6W`${j_y|bT%qA4 zzqTlwZ{2PcYref!QcA1Hc`vJvY^_L@A0#i~6UyxC)2iUW-G0aM}#lqbcmpq89bOR>#O31p1+q%XjK;VDki&ACy zBDS3E*MtVsGACJPxGNRDR**^ISxUEC(*cm-{_882ar7;;KWgRi7^Yfqg_5q)$zvq! zeg+OAvKIt=fx-i(I^xLxfVE9i=9;XKj*hI%*qvaxN^@ED%o%CIiJl1U?)YKffN=Ng zSCGTs{HRVAZZ{+kwW64TGq47DnY?ZWi>rvIV!x=!(z+qmpzI%Ppk+&s}- zP+guPv!%&Pq4&uVSq_D-DS;{Dr^CBut>sznSe{|;TW}T zj8W7yH+%;E)qLV+j_hzXwceP%Z?Tv{yVr^ArNsbse5GjD&R=lcL5rvUigP4U4Lw-^ zzGE*odCNFFo_;0?KpwMfT59r6etk^r1{{y;p|W~vNG*6THLQ{LWx)N8;NSesF9|a( z7NONzW@NFP3$4vR4{NHMRf&Ko1WuBVfnn=AGrMe_tob47taZ|6!0)g4{{EFpI;Y8m zQI*X#Et%0>YYtLF@o@a| z@r%*d?7}Jc1mbM(1d$Lz1%!t;Eqv?p?;WpgNt*`MFsx?bvW?>l#V?cXN%;ZDSJ5eo z0i5QUxmExDM2{BiAEd?4@YVR|aF9zi)W<&ZfY9QE*a5H$Y+!f-rYQ!IMpfsIw5JO( z(~4Jut&;%kY6KCFF7ZWIvbeQm`_2BADCeQYj6I4BsW%|UJ>}c_nIT{2gUzIdI>T@G z=Ko^sun z6>GoC9|#4=xUtQ3a^I6xrVTzCiMdGQ$w(T~#)tLjVVL1=3&}e+2s(f8a4ntr^J(6- z{iSVf!|Z=p>|sMrKwFu`perH?H^!32jJqm>5xB03fW;4KMWzIS6e2g|GLb00BJD<) zUL=4zP?vG$NIHH|h$zv`t}~+cY$|KQYY7ABk%8 zU_NpdKG|;7up&QPp+hW<(&5K1APrz!5knCdd@MbDHxO;rJlXfxsq-9|8|HidbwbW5 z8UzP9q(z}`t;Gs=ku$^%4`VRR>r?>h%urPReJm^=ECwt|v$6~{$3C{nxncVB@jx|7 z!(;0w3;I>2FAC!2B1k)v7;n$vfdP7Gj=!;_e)a{8m0sg;9O~Sr7!WR{LzCHE$^v^! zh#}*Lnc5pb$UTiw0Yf(q$g{I(_+wI6Wgun zaA{mPxCz3Dm@OvhwQr}_k9Sro)c{y62j`gDgSdED| zZW33QoR=Z`~NwzO&;@V;NPk?**$EIn4w-Lkv=uNUA13qTwS4Jzq}rt-nY;@P}R z>SQ&z5?+qjo4M*&AX8nIy-!17ZG^qdYn<-@0%QzYc;Ex46IH!XYj8$3cH7=IQa4tX zEXF}_r)YQ$4)5UsUj=)Fg}0M;8ggmNQ>!C1G=$;*YFIf@C?^D?MF6k$rfXobCZ<{8 z{|-j^oSqBhbGQ95H3&|)qxk9!*k9_nG4XblDP>-=J{9f>?N_YW*39BsV#b&nY^o~brOzJ#itU0xvn{!zfZFycXWsaxgl>P zlZCDy&_WCj#U1t7xo=Ey@DR~q*(|NulU>h_FZ?Z`0>^6cupOTb`}6pn!e~Ivabbxd zr(BdgatRGB7psB5E#Bewh z4|WI*FWpSWq_EE-{ide8N?Ty6AZnoVuKlw^cl!!H81*lma`)x7 zx4-msE+XVC|x{J{nl_1JjlMEuXJOW2m zu~%i}i?djg9%67sZ7YI0V&^o)YU4mdg?J=R#Vy8vqz`Qu3y&L$@6dIZGId)qcYd^1 znVb@QJ+8C$;$!F$(DRp6%s|~gKS*GRenHXs5T+P-Jg4$)uI4^=`&AI*^`b+YW)aoU z(g82FEJ)|NhfwRi@$VRkkb7kUAx#1d!?r2*02{f00vQSy zE*}g3IyENK!obVUassjNZd#_@Y6Eg}rILE*a>v)i|N4(&E_^jS1C1yzs+TOpx);?r z|I%UIj%tJ0r~0IEV+RCYb0#@ZgRx;S?AvW%k3YTnTWe%yrtNaY*$n%mL~Wybn_z)R z@Qbhu7A*8(=qTPi{|jjh6Ib(pseRK6jbD(wl$Y&UY)XpTxgH`(bd4w*(0h zwbU3Glh)Jflz1jp77zjg#78WeeX>YnwHkFST`w%5MFM23Fh@kJ;Dh)=tNlz+%CE1) zsVXD*$mRI^4`EW{Dy)qbk<3g}zgKv$F)1n0K_51^BOpVfdcD;G`Oaq^gRF-04iX4A zb4%$(G7sT6GaIQ1gHV0k)!bSTKL#nd@7rn*{$;4Rc7i?Qsde`}YSsundg zu6=xNtqgsb03O0zFCrq&Z{S$)c6h`gaw}ln=39UJVwjvWlzkL=JX@dDt&RU?(n^F- zc^@mGq5fxIZ;V#a>7zhrxX-Uo(@tv(;JD?wFz4jHZ%<`o2Ui%^eaKM&FXcY zQfZ1Ur`wD386L4%ReY#%C8;LIp|*aa?ir#@R4zD>u>C>&sD%+l6i1zZU+l;?mf-v? zKxtO&lYD*e+A=S4i0#K7&3&MM`<9U}jt70F$b8r3L>3xNj%HmZ4-ZIcsiF$WpO0Do zTDpFEEFN5>0dv=d1y8|oZT?7oh+T2?=!|)vXqhsDdWB#KKe~q&42ar^TsCfSCry61 zE73*VNQG})&i+zV%KV_eiq?o57Fe9U;UUU-k#q37gQEITdpw!Q^l_#{oLfE9AFHls}S3)rJptAbY+VNm0s~om3xmke*JLe zm{$um=mRj2?9Sm6kn_-S8H%zKYUDzH4fiUiLd;=`yJ-a-P2(6Zx8GYiWn_HJ!~+co zXHq`as$sb2u}e9K%u{1%<&heHJ{c&)?DlJA+)04J|}Glj6ohEcRn zJyo6OuzA_2ch?>q(6r)n(z~Nhg?XOsJG_F|$K*WN&Z%|0QuWa$9I6GtcqN)&cD00y zBs+xAJ%sZ@*bQxf76(lK_Qi&4#pb2Nf9EBy#!0mKDrK=b%9(0%M6?j^7E_K0j#I+J zkw7cmDJTCzZYro07~H22bW83ZUB5>?xzDXBr0`HfYpl)#+F#RJK64OoWG)-yY^^X6 z=HuM7p^Y$&t~mC_uthy40T?!fNSTm4*ax*Re2xBSvnli(Ndl#UCz@S$<5kD?Y-TkhZ(ZN>Ysj>ws|Lx|{AbZyUKJ zjPW+vz|Lwf_x+X#6&7G?3lOyPjGq4wW7?$oyGU}z9ystsX zG|1Q2VySBPSfm9ET8A7aOxOqQ_1&E}IW40K)u7BB?`Tsk#>~$NFic!I7lL4a3gr6A zjm%aK$3ipsabRJX7bFA)Lm5bOdst(<@GIJ=kZjO(b_?zY&5e>0ggxh9HdfWahkdYN z&J2Yy3N`eiICYvyjP>(QU0uY~Yl&Pz3c0%kTD@e}!!N9A34)E1;+%2F+u1x(&D`N! z+=wDRRFjOLnd20-c$bT&3!fwYvH$~65~uhO>I~IpTfxgT?;J$d<6OVtd3)QHk2btx z4KQ})x+l!Ej24v%-m<$QHQtYYp9ZA{-ti1ccg?fa)BP{*cmyFW!<-6{n%F0XK2tL> zJnASzVd-=i$%F;dR>O`>gG<|0j5^1*i&I=7n@)@V9d2F2kCb^mrE-i#o4_s~?u%{M z9Ka{HK#I7;q;OV)Tmm>Jt#*E|(uBD>RB*Gw>Y=3Z?j5Ul8>#>Fu+a42hW;Ule;Pa& zqC+d-?%EFDYQVmDRws?&v@9|wLn9qR7mS@-6O3SK((eZ39qcGdzWgN*cxX0M#MS+b zlur}$OIetC>eAW_d>}169b=BE`$3 zGaB6y(NAtJXm$;hxq12=NTNNl7T3#O4oNJDx2uHj`L^co`K$B9>Vv~~S9@AMFsp>U8Ig8A!Y0$Ih+yvq>fd3=($+t*oI3-0uz}3w`%0G(S5JP=L^xR};{4Zz3ai@tefQiRllR9I?gE=!VDHc8lgl z7hS}0W~E|s&){8vm9%$Y?Sq(4k)J$?>;|}6M;zd0d0=;2q$+(`_J5IWzOSBpafUXB zMQF*MbKvnTTz1^^Ddgeo^hr=bOeH~!J~2W_=eIz3jx_9|dmg{zEE;O8t>;og&oMMf z^%*GO=+%nu?dld#N`7ihjAJ7d4WkLjrc1^uMDPgNPhJnM!he}h_*)XGHo2!&HqxRm zsu4AHSAa50Gxm|>E+a3eH8_%RIiB@9FNY8dlL&Y^$0KT%_4G2LSB}E)6i)5Wx|LH{ z5-OnqU(I&hA8sXnwAh8~k?8wNNBA-Z8S+)DP<6#f$b(!focJKAb3%y zmaT0}OODLg2$Q@X#_jK3p)57$d|2>mnw>C`@cnnm6PYtrFe(t5vv)bznk*` zy>qC-M`$q-D8t05Y%%QgShyOY1uw#3i;BR-cXO~9njIvL6aGA6pmRfGq1}7PVaL*+$!bM>>UPoiJ~dbYj+FL$=8z7 z9DNZKc^$HCyR>;crumJH;LqaC z{NXYM40>q?oF7{OH0tAI56k+$t@6Ybf;t-){x)bA$t8}HX-F0J@k?vpYSa3=J7rr> zSc!UsQ0h~sl_^0TR~j3IS%Ah3rD>T+Cph$4L7Jz@$d(U1W2fY;A38b~c5fT}7rCTR zzV+B#kx-&(w_1Wl!&<`Z#j@}WZ%gHKAJ`=fOxYVdDIUhi*XCQ#FU1M|(s#;UdB^qH z(_ISY%v;cS3q|uOvTgHfDD zwU6y^E*3y9N`%N9R4))NN$oA6d0*go&r$-B%8Yyw9rkw~AyQ)nxlrRL+*T=3z6fbI zTR&|!*fPOBlrz*D@V!T6m9k80^D}jT!$6PU#L2f zT4`z%b1lvJ;}b^e9EtmC5DHl9Y6nD&m{}BxNlT}ZgX1iT#^zC+M2MSI1<4^cEfIK( zSAR)@U|0J&yh;4}N2CUYCJBu1ZChlleC;*410&+Uwkv;@5A#T;>VUnDgGlMDxO{-7CB7v#>TCbs|p=w{;knikAZWu8?F1tYgk(86el1LyZ;m*buH#;OfXyx6ba~`-odStBPsdvz4Ioa4e=f}Q@)1Gt4dNj}L+Ud2eTj>= z_noNEDB$d6TbAJ^r=9n3iNStghx!rDDeHwh8Eeiex)9;2hCbUM(2k{_HJzcQnyL1# zFr&F}(Gm~USzdJQ)1FY-y+C4Yp+niuU z%fN{>j(>;r@fs0O#1`rx8?V*?&lN3=5~(tT zx~}f#(4a#2=8P|H6x7Zo8em^d^G}Mf#%bngY%`s-%SX(|+S9OfLT45{5Dp$*z{C~( z49nrsDkEf3+DjIPzW>71x#qH0BPj5R9pA~p$@f8Kn8$)~Nhbf(^vQY$;KOxM%ihjg+bSJjg?Va-n*i^X`%<+es{jtO3ah&3&;~quZtv$x~bEL3K;!F+6fulmQ$-k7mPOBf#58Zbzchvywz!^Yiv6VTR~sRw zClZhdnao8fowe9L0`lo@CizHR z{U220U&G$mK^T6}HoAe2yB6PrQuvi~geL@ReS|HZzPkD)NLNahX}Z{K<|Ij7QwN$5 zfve_@$xC4mHui71FR0I8NaDCp5O-D6=v%6ikJd;EG0bDSNvMf+&c38{{i9OJ&j{dj z`wfUDD2zSe!6eFWG6uFMDN%0 z>b``Tw8;Kc$91aXMA@UimoA4!Gmpc+Pks{5{oyh+u^ivbkJZ{xHS8}7WL?K>C{!~$ zc6Oa1Dh;z&;GH=|jXWy*{Cm3hk-C3qk!RtUS5{reP zbrICLHXR&~3CbE5{^rx~_}n}P^N~LF`(o}KNcnUxuR0u^J0l6o$tfp}y@}O($kOiU z*HmlS14;>|xOJzS9-@u>=5sQd3y=i#lQ0+g@xxp!-?YQ0FSAg$9LZfe9gl4HFq6e6 zNU)WJsHp@5XG$g+KuiYP`{6I7Dw*|Y-d-uI`*5l2!sY#4gFUOtKxOYAp zS(f9Z`$*Ov@I>z-)aLglq-jXy6_`Ax+U@R0&UX!1hqeuHa1QzELm`n!&aY6T&iBM6 z*yX=`18dU+)+#!wfDbq#cv zUe7{vv9zWB&<~SowWbf5k|i>zt=L?;8`)j*%L8x7cI>;k7QIhA~OmVdM5+MBK$e zDe&aOW~uGo@r3lDshN$QQgwq_pV+zD!1lqG75z0)OINOW9R?8rs|6>{Xd=B+*|n}= z{c52|+4P-Z86K!A&nc?UM{ zhmWg<8==ApX5(|CP_z~q0vyQr-?G}lVo3nF+=Yh{0~Zqa!3)oUdY`ST4dr>1{YwEx zi96@LGeXWBk#UAND6ED-5M}&0o6~YK6+1yLOy}-w@bB^sF}cm0nF>qrW#uco{o^p0NL*F>ji}gV3z25R3>_&B8XZhR3=0 z91eP&ic)`>kP+qg?oBfr>9v2YhWgPc8{pwBu|bS!_kDz?f2btyYlTU(xZ@ESSX1)#_1D^9dd_P>UuYtBhU}PT+ut^8q&t6cg2or_-ii zsDJ=>K>WVf*?Qzyz{sYAgQCVdURGx3|9FHZ5@(69xse-c{{1zEPdw?G4>Zv7WN zxtdes;A(u!SVKRE9_6JrUUMrkjCk=9Z6@FEk3K{LNW4Ok6Tna)pw`F;%dej;-0VR#&I|@MPkp4~EM= zb37p<@UD766zAA6R>))>&!y<(gv7YvoUursxadKMElYDU7l+A!;weSwtH!E*ywF)d`=~ht#sR z)VEEeSj11_-&#Ict^N?r!26g& zl+0*kiw&k~)b&OkO@XG5O=d6^R(R8+6UBVckjQGLmnDhv9Dr|OQP*EDA8=s1NgK_X zs5QjvkQ#TF!$Zx_OxsJbVyoh&5ztP8kH;rVb}z-L082YLc{N|l>pm|+DGBxouNAV& z`4|uc;ZkPx?6vJ`IdOosub%+wZbTdgz+6rCpdMndWIsMyHx@B5D3GB#> zvfNkv+#!GYnOr{FMBjk=#kj(8=1QF)Pt8i(fV+gxRbCbsCFdOPGw^W}K28?WYywW| z4vst>(TRX6-7jO}Fu_$pwIO7U-1hPd>w#7mw|R8PbIwkhREl2Wl&gbaq^vhH1l|X+ zB`XV@5g!JoN67uU&{cxKbdFoqBu~dI8>c#M>5{cL@qwd+V+^EQO83!$K9+b1&-l%H zur&SiBm?gKrmWVvrS{gtf5goKY(|cqZ0u3ZFdWw#ksd8zWHXd6AX7$y+GfPE)ZR$J zY&a{t^t*(%h$d)A3hd;jFjZ^v}KUZ{C8$&~{+w);+D4SD3d;9(Du2CjeDt0HyQM z3dhsbAFcB6XVYtjrQwZNQEI^SV*<@P%iX7+J6kM%8+Mt+Df&ebvFN|+uAV$SZw|cX zt5xPLY#x5!6ViJUYwvl~U^_D_8K7b#rpP*V2Ka|lIzxP>47_All>{C$Oibwy&>Kka z>u^D>5dTrP{4TDqgXwheBa%kheb)5xca4sfH)e&HAv{RGb1jjOB}RbS@J@ZNhNfRx zYDliyEz|@OkA)bs;7WMy4>Ob#cU$C+xUqQ}bs1MQ-Kj z1q@fsmGvH7?lXeYSI^{J2& z;F*dBinjFX$_Bd~ay0r;1N>$`F-2QFbsdbdGR6}{n)nKiJcps?qhFkwNc71$2`+gs z0bRbmZQlxY{^&SERa(tk#2tR|KBC95 zF{Su^YPoN0*L5mcFW%aVI}}E3I39$(UZW+@cCJ2udtdT7XX7hpJb!m_P#XpViE~9PD@*PZ_Bzr<;LD)VPltV3aTjVgN^H`^+Hy&>34+Xx3W?@q@f#oG&7b3q z$P7nc)U)I&*S>2;_gV9J-L~;6HiKBX>F=89DyxPak$dYP093}mik-WS_cg)D)SqZ4 zQI#+`%8+d6)D)z(+qMjAV8*TkxfK57m5}`)DxdP;ip@6JK0ltCTnetT%hDimJNCmf zhy1+$CC65|i-4WDPy`oiJf9W+bSA?;2QEsnRnCy=>2e191gu&klFV@j2=sFL1^Qnv zKqA#IC;)&dM~t1-`c4Q5_hrhw-w=4(0+X+8cTogRGMp-?xL>d3Xo0xH1_a6;+O&pM zu+Y_VozzFJ*a!yuxabh86rpKqG zXHK=3Pb8#Iw!f@QLUSpC$%W+4!Lr-s8x%MV&ac81{6nxA+aVBD7|+bc#smSf!f=w_ z$+y_9n9{XHNXymld)cx3IV=6*-%{FX^FwQtx`oMOqQo)gO^k33u1+$9NB?_W+t6;Q z54MACTvT%tO2A63b_H?_SSf0spvQFotG{FzbwkNliP#~e9V{V&OhKHFZMvdz^S4s= zzcA2%M%N#L5zr@Wi*MI-r7v^aQ-(SHhh&qfx;@jw0gCC+CCKKwWWMbhLkv2A0w;vG z;%8FvK7GXbmCDD8?d=uVUogiNkt%6~PiHE=F>uk)yrlMkP3eOanU~@w!c~Gs#g|5h^!wG3|5rJq^`RC*Q}5} zNrIGd)Y{qR)@B7Ic5Njc;=nAn6z#a^(k7L}3Z#Q8tF1;JtAZj-ZKj8aVzc<4*q#q< zcpf}fcWS9uFTYP0%Z$P?kO!|V@EjM#@1fG{{fQc}}$iEk?8`i$m>VdjB6+Ch+4+V`BAtjGSMP|g3%B8|dU8*=5G zDlxWrs_ri{K*_4RW0=SA{{VkLfWJED)MCRk7P}sVbAG1y$6$Q_zue2{1I4CA*fj9<5UIL%@owv*vJY4O1!go>wdK1{!=)$1MNVYfcI_o?U!^a&BmbM z_iJJtjtX`j^s(phNalb|5daL}nkNt@l$;;r>=XR_pj^>VX<#-;q~Aj7xMM$50A$b^ zY1xJ4+-h?R=n&9oV6`X?z1qg}Z(8i!C;0IeXbcKs&#K{v08fyJiJI_#n0^g@j|biLIBeJJGTdCWM^vMX#IT)i!gZtwK(dk?Yycp;D^ckd+{HA! zD^S0-_{kJxb*p521(aB#7(m(nb^NWHfnzyYrmjri{z#4aSn~LjUPXcy=}Lk);3ue1{AT1#(p~HrbIJ~3S_qJy1n{dgYdjnI|^|?A}-#|SVJ>wiplZwG+&x*(@-UmWj?3c+jK)@q8CDQw2cya za0M-s0#8Sqn~E{ofVGP4Pbya2qz1n4-CNT^TxRgW51Rb$_&9!_a16Eg!iGvBQaReg z?8NsM?w!!??^zn!YlEuAQ*1oI@M&lSseeyY=Ktw>>0?W`uLhf|PDC%X&o+mXwd>!t1 z+~U2VxOYPe0Gu!JPZKPgEcn;ilDSm@!ZiXJbkVK=jJV6bFRpMpjG8E|aW1OR}&AXZjkh*C*{n8cSqx>7~oFppAr= ztOYbH*lKfNtKjU9Tf8`EAYTypu*t(`OP17HX&n9{0O)-L!mWAI%3&joXlIo89zkTI zVpE5rT}u910rx*)$9e^7w% zJ5gh)-1BzZleK$RQ46B^vj@-V(%rKd;sEjhdEP#O~h?0^RFCov9Cu%LVgB!JLH#A28t5 zX$I9bK&b(KcfgENj(~gQBmy^_pJS;Q$^ZbI$kE#p*2KBta~A)PLF96{PBQu|$$iTV zPONi17Ug}HUTm`LBCU66eub0!d{puNeVSEoz}6@A;CxRXh_6eoS?%ZM6C{2aOLJC( z3;>cpTcr7X-3JAmcL0!7Kp@{fUm&9Ky& zXCp-rJwgawQz@vN4gWRE$Bar%c_~mXAuQb}K@`viS|}*d7(m>H_Y^!4;cxeA@zJ{( z91^i%2>>oKsGM!`tEDDq&eT+g4G6fx9k-nvO^0&w_)P|}JvNPX2G4Bn3GU!<(FbSJ(y?yF+}~@hQ4dD$tg81558~-WOTL$!wVBDr=_ml zgclXDM=iepZTRM3g)?Uh#QB2DE(~zX36ij3xgcRVN=SwQD z^zrISA7g8j6LLjoY2b+Jk5O7fVX6HTmONk_*Pjq^=#A{@wcn0A+6;kmmD~ zYh^?qfNr*FwoLb+PiZ>Y^WJ##HxOwG2~+;tb~}M$(ruSh&~Br)X?E=sJh#Q>hdbe5 z%Lv}z*f@N_03c=yg0ltJTrBzAJcAh(8hn<+%(PD^9+PU_J&KF((SySwlI=1J@uemT z#00o-j-S)3w9~E;4>_U!CnCK4n#G?ilb>y}IQW1~bT~@b@e=d2QbyE7xn{9qL78T8 zwLxXQiC^Hf=!J18FooenL_JjRvBYIGps5K;k)#L)!Xx)Ui0GO%(?z`0wd^#nBQiKP zvb$pUA{=<>jXuI$?HYcMb~mWuL`|tJxmRi~{E_B`K@<`FlHz|%D7{$n9}}fh(3!Oc za|=Ejf%`1(e-FMon8qLA1i|vTe$E+{-0tRaO9+mD8jGvXLX-*hrlICGa5(pU!pVo% zJ6#2GJWOg9Oq!>-xKj|RZercv;LSF{n-TcSI!(I)*0$l}py}5%ov&$rB^38HEAC%z za$=pS!M3PTf~9bJOz`JcxZrVJxU&ZeG6HbEpz0bQubf%J*fQIxSZy)k$p|n1CCa(C zYZ`}}w|}7zQ^%QHD)29mOg9DpG9kd^ae~U4IMllR&%$&Ehc@@eDF~4oCBR zY&|eir<3S{Ws*1Q4GhYNS&}J)GYEkaPFPd8149haw+KMjJ9=QsmBnTgo`Uz+X=eX4 z%HhG3o8!RuRlz;yO3ohDi~e{a;IIL&Dfax|23yAS~`Z2{{E0))Jnna%YB?uZ6_Etw@M;^kequ{`{2wJ8L|<8xdtm1_!(U$ zor|tC>}^Lq5*NR{9ldv`3(R7H)EfU(m=zXWF9iOx4WAzaG2-)3+f0D%>!}W^LGDe7WwKVoCR% z+BLiwXU`hNsyj5V4eEUd<21q7PlYKV%T43>5)&Ga#!Hb`*C?B*lS9hg@BcHr87W5nK(wY${{H|F+Up1OVAAiByu01x=)K1+CqHjhZbl!>@;^ zjELC{7^C+xeX6A9OoOvqEUuUgbz>D_Ey1s#)J%A*(sqqIj@RF9#Ap)hoA)!Gf$9-T zFHl9~DUtT$rV<@H4u3-fYSbv~#ixVc-=(k8MSk@Wbed<0BRh5w`TO4}+4w9n($SUU zl)Y3l9;otqe}xM&Mk_eU;8QCt?)oDy4dEJooy;cbMmTJhNY7+rk5vW$nkqb9HF(7# z>6x!?vf;NAlRmX8YX&Yag?8jlNvG}DuaQ+gf>FWb=`cBB@u96Y-)f)|=# z_x-TLrCJ|SfZ%n+{#Ot`KSAk#D>oc+!JrMbnh83}QsC}_G)#r|LZ6nDe86Pud_QBu zt|HiK6Zu&?tAC?;Y^VstRRVEH6I>)kjW+bRFDYeitdog`B3Lgm13*ZF&owZ%H{`@f zQFwwQRstu_7#BIXH98N~)tQuXTQETEJO4%92S11r4m(whfk18)RXmQhcP|Hj_A@Ge ze+&NR189|)s@A3Sv-w`K=N@PS4l7t^^YR@wtNx_fKiD#N1026v^6-2g6Dma#Sabn^ zG96BXMumDX#_hK#KKwj`GvLQtACuIcFIlrxFt-es*T7A?bP=x@iK}3nf{v;Ys89(A z;Q+I)$d~R?YF)s={Tq=cs^f*Sh5K27c%0y&MuZy@ zGAJ6^p5)EcsNX5rW#7W#;} zwYfhGzaQVYanyYN^BguL)i?hf+FH9t6^q`08kJr#R~Rr)bJB}8{{?)6K~_{Z!J84y z4;$d;0pS!a%A|I`xDERSthM;#6PiyBw(-A4P`65O|009ArKv*f?xait!9qjR@iQs= z+Sd_NrlQ?khxSOpw05Vt_B8rJq_)F2Y*@m(YFg9Y+|0h){z}b;XOP=oMc6ufPXFS{ z3Y2*DJq8eV3z;i5a+F3!Z7Q2!`FMln`xRF-LF4=I+Zpg$2edUCJoTVOyiir0Lhor8 z>^E%2gA=I=Y6vKCW`{FCwFm&RunRtyj^3&0wiCtNg|-$8WF?fAfPWnP(;R~frc0(8 zHootQHkL1_iFBaZjg_1J^Xf-#mR9W9~rMPD}__HpnyXf*4y0sXIp&Y zarB@X|5Xxsf#BM?g1Kd0SUC+7eR7+cib8bdIaIDb4_R88^LHxOn$i#Sg*>1BoB5Ap=fCjZa z0*xzeo;*=-O}R)6P_xiRv_KO}1z7~xMeXYi2>Jy8KK)*xlk~3q~L?q zl8OnE@>+p3twg1|aA&J4?GS4y+&r@AWuN;nTo#~xJ}~nyf`Q6Pnu0+}J+d6n;8^MP zQMQUpY6sSzY*4D<_wU(!;5p5@!Q9s^n&`tepJ~->m@WCY#RlWcfi?--plKU?<#Bs( zjXyrbDVj@132tmKkiwnY;f=ga4liqwFu)oRp&%khX`-HrM<6RIkYmQuS=&J1-5t*R zq#KcAQe3&&a)d`oj63xZC+#DFYK^P{EP=EPCe1aNv`@2gOPu|!ntN*nk15TA|Ep+$ z!LJ>7587L`L&nw>!7AWYm;gX_hf7MJ5Dd!D$_o&NhT6qK&xi}Y)?iY%RC0VQNNL!H zat&eInW6_y!Cvx6R*z+?i=0L$2|K+bZW(UCUkNcV_tj@&2ZK&aAc;_=(bwmB`C)e& zpWlGVP(DxbZx?Ih`8%*X26J!kgXjy2-=3{`wp{b<2~hruMblc%N4}@oHkj|W(2qDr zbLm+=ely#kRN7$J&O8DSm~{)gyR3ya3`_u32!ab1(0clEtXp>={XHj97gruUD|ZaF z7X|Vttz}YiVbD#Nh6J*~puWxK`jaIejlzMsf_E)=WMUaNuj^$0PoslF#Tx@U4cA*0 zNnUJ$I(r2N03-yIND$U}5rF~PKLN;#k^Xx0suH26FZSu#K3H*_rn*8R116MMPAWO!cIT|J@~cRI~q z3q{sk0e$H-I_m5Vl?3{d`+sHkpD}S}n}h|_X=Ge5;y528+9h*i5x!Jmv2Bjvu8o2N zrs8gieD@ZVY8{Ai$HJ}a@=|XSr0G!D7a|BKya<56z*VY7+{v3G^NPU52cTh`pmc$c z4nU54f?gfr5$K;qvRaf700IK4oz0fx1T&@>%$RRL%;pFCv}!sI z_ErVCe{z^V|D~CO8!h&1%)_1VxVVH%+}6dHP@s{Q!pvlOY;T6^8RRgGGEpEGmf3>S@o(g_A(3`dwu-M#1D&X*oKR%45Xfd~c%=CiaWtn5TJc@8dQx_)t= zhWQ5mu*p}q$5{6WY#8iq{s-u<*n@BUR~VEmGP(IAKNHGgY4p1|%6%BXYFQnPpbzXX z=+JF(rT1N=0G|OP1T;v9Xw0clyAWz)7PH#h`O>)thsN8iohR7Q2*22@*{8vNBNy(E z8wFO%sb?tkIW&y$2sG5OBPnk%M;XwVTSuT+6k%wvL!5z(tb8lsEDRv75 z(x0UGw^J1tl*bYTMTPse-VlhSh|w^{?S=APl8Zlk&2!-LH7M8XmTq)5U^sTf2ZR+= z+Q^ZD+Ia?7wi)mbn@{dm9JU3|Jfr!>t(y08v6qs>c;zAh=*uSRq&Y5%m~iSV`n=8( zgw8d%Wu@SfL zVY_$U)$=|HK{sVEQcxR})V2#IkFb~@jdRYQHShGtc8OH)s({gALE@9-S?unv+M=K4 zcckE>OZ|LsJjikj3^xp!91ix1e*{U~`ns2-F-MY$RO#@%3-6D|I+pI|b(%lv6r=9_ zACcyn!%#=`*!rx_N@Tf@$-gd~Nom&w7=NzGjVlEHV^uZ(v%#YCeB3+F&xo*e>t&KC zm1P=HPaVjlq&wVcfKWimtXcRLEJVgKD@d=6x0{YgClAoKm@K$CXLeC{Ki6Uepf?vm zbUQO8`niXf`j}AWKFo_SbPdsQt2Vvxk@vZFE^~mrDdJ0434N4DiX%KT(}fagKtw(S zRScpx=Pk=MBr>^}BikZMmHii$fHIs%*Go86a>)+O+X0(@y-yb&6IedirGiOk8T@^L zj}fI_bySLBO6M^p|46q2L4fvlbR>fC`SOQa3(3kx-QNq?-vZ(c ze=Yi<&vX`gC?{v8($Ev9MU?674a$iOn1b?L8U8MrC}78`py@1<&MzkW*#%r8@XIWa zX|dmTc&*?fDmJ~k3bmxDY_bJ;B0Vi90JHclqm<@(>hnKWl!=^M=L-s zuHkXB2jenI4P#8qnKu7)s?BdU!@I}AmVfpB`)fl?QPg=A4YbxK{$-K_9GbM#^v|rwMOe>yZrx{)P*@Xay#X5g>GBS zV@3d703b;pmMV7p4A33R8KUdGAwH_G>HNrXSiu5=T3c}+miWNU=(mUU8EYNTO5G+_ ztQgN_m!dj4k;$B>6_rp?k(JA(V=?qK*Me<>lvIrxg$#xA`u?47zm5Ion@;Jtq^KZ@ zpr@D0CecEk+!as+0KHs>a`LIUc^HXYuILH;bfG}B-sB;&%)=e??aV@;JlblLP1anG;t$fMUhXuzQ#qe*^ffka8(f!ss#mQhvfT z5fdj4=y!G$dY#X$b?Yt+4?x<*2tcn7C^=l8E6-CG z+9{;dj4HDc@9~||fFogNw1HVE0-fo1v)wzD=2?Ti@1YR>4_(8dx89<3({uQ|7H^kj z66I6{Bit(Dbmt^`CuhPNX{o?Ao#&T_0?c2ra$5DNr+xpkQ>J`)&czoGt9k&}`2PmD zYRFs2qoe>-26o7_S6b&+cXXrjO?RPnpLQTg8K=3%D)IkpeT1PmnRG`#B|mLMPiFc3 zMdE!$9)N*2@XautdE3G+TuC>F!d=W607zqyyo;>Gu|1t{pq5HHj{yUerpbsED>xJi z5q)|+@rNEFaPR=W9dC3g0xkMU+947xYo5~a_`rn}M8{7ivUn+#pZlCVebVul3%~#O zz{A7H_zU1`z=wzWcL|Jap!0;2;fY6`D1hv{UMi}Tp%)$K?*p_OZ5Bfy^jdHLK&3Ko z=wBt|%@&{nK4K@Hg7xLEQn_G0MoldU;Ro*2S}zhpyfRFT|NjAQ91b!X zoG=07_{p&L(eA0k99jyG;autdkBm)M!jcoJ>mz`f5<$XluVzy?@oiq0EiRpbHa z!?Ho!AdK8u3y-`b+51fH2oSme(*(nH4aSWS0$El@u&j*Wm@(K37IJ9L355UpSAv^1 zBBHUr%bG_BV>@Iw#Gzxt1Y)1~G{I9BQ89l$bnjgc0}pAfPY^;h4)N2x3i!tGaRUWH z=C^E1<kRe`*W)yUwg#1ONjWgDI}!%3>J?MokSh*Ih^S%rj}d=_bnl z@+WkA3tBr=&_@D;J#>u#74Tz+D~SaCbe_7H*zz+NbHx>i8JDot001BWNklUSfLj;}>BL@Gl6|S1sESAia?^Hb<_oHoLI(EWB zi?GO&5<=6a!56izdoZonpOET9(!yy-5A6d6@O z+UwWjktz?q(^2kzU!H$78uf0Y(NjJ)>)}AD@2HdXs7O#q3)}gQaTj&TsPk(!5N)X7 z&}nBLh-`lSzuESW+@QZCYWG3~dB+R@CqxLYG;tz<8*k+BrcKyC_#WYP4E{^>kz`I(g8!ZH~vXbh>i)jmgi~axUr)=$gglz3x zk`#NclvCu_F?&8O%gMi=%t% zxpX#$i{p|hIHEJ?iQYsDyV`G)qPsc{GVVOTqqd6J{8MQ4n}ioHq4dl%DVsJ8;qy@s z-VFNal!gYozu&P7gxD_l7--hBA^=Dqj?r#Nw<|Q0d}_t;z<>j?Lfmp;nv~67K=|AL z&U-#T#VB=p8YywJ$ zt8Auq6p9`~PK>`-S|LP^Rj+G#BYr>DnB!h=0zBU>H=_)lsry1vQ}%1cQh` zAcK|Y|9jXTcu}D2!j_$5DFtxU!&QY+8*og70t)%v1jg(+jQqh5>3HfX>>vGr(AL)x z2E@wBh%Pw`@xd#oS-lz~9M0tw5kK&t*7{T-#H$5p{HuYdhqLj|D669Hzgol&Y4$~R z2Le63{*;2g)FM6&*b0;d@t{F`$r;2etMHE;N%)E@Fv`j>s;ddq)gh(KrWPkRFfXM@ zO1Sj~#S}n+AkczygHtbto%?5IENDIg!&koTw(RIc?|vVqOv%p}t{i}Xz=R3-8yYxR zQGxZ;ljwKeL7jaLmFJv;Z^Vdfl}c|{7`RVseXKR-dnWJ~OrWm6XrY8TLdZ?@1C65(2P6S|AW;Q)3%WQDj%apzXek zOVzDxx4l+?HF+|2NeN15Vkex0HGct-V1U}ir&HO`fb{tw5Fn5zC$YES&+iAHa=MBN z(Fh$g06@8O#Z{8jD$%{t)SEkATmv9@JGOB6Pk*BRxDvySOFOS5Khj2^0DWtlJ(4uqUV$)c=9}BG)g8a{JTJdOnCqC zk)w$&UV`lp(Cqh9deMahkDGvL7=+5pF>7j^)}`F=Hsn5A@1mdLEPy`BoysGJf6>?v z-ZC@joqtFFcXKtR*( z-2t@zE3h~hQ56?a^!$Itm3JN18z)rc;hD+5q-6qJ37mpGZG%;2(lK=o?WT!w@?4Cw zR#3fU2}USHm6VQ4oxi)Dmu|gc7eH@M!87_f)D6<*W|le;6-y0miD6I-*rz`IL3+`W z-R}{)=_hpV-c4uS2s(x6Ruq)(IK1!YwC8Coz^Jch^m*qygge8=^U}1x{q{LqfANbo zH-!ARybz21@fmyI+KJB)gwCHj^&9WseDirtBkSj+a|V;f9;8&xiLR+=qTA0o=0c^M zts#>LqEi5YTTA6K@@k9`_0*qn2ELM#fxA!xq`gI+ezu!_BeO##Qn8xq-MeI~Z?phQ zf-()PK!`{nMEvw6h!OROsw%3kxROvsIc8b8;}}RefLf{l59n6lEV|JY6ahdVPodvM z<3&HCaF*1rMi*f0+KKU>-^Ij9ey7?1P5RuV2iKJW@iC)0(AY@nnP(B2K0RN#`2kyY zOABqUy@vYT@8rze{<`#99a}h2^5!!(&kn@rpC$-hFZlKBcXwS?f8#eDVwIG;DbgtZ zWZ5{$bVmn4Wg}E(0AP#W>9B0SXX+1& z3@o5W(ZG|G zG))3^wbWgF34!Wrq~DJS2AvleMyVigYpf(SDGT&nhe{Ek)I}$Lc&*+T3)x*EdXnJQ z5g~v;^C9YqMmph0_^LB%+a(AJKIZKBn29>%K*w=dK&v%H`3M_A+%%(W_5MpDjcoR z9UVk=?IQA*zfiIEVc5DAVr^(MDTFRylz`=evxVReG#CF-nhtaD ze}7NaFMf_c7V8@6ERWR-C7jwRgd1)1#$9VCe;*w+nvUvf>=i4Cmy}TM_fxrS85L8e zKp=n&g@&4UpOaMmP~wm(%o+gU7;IDJPnl#2GpM5RAJRburNP*=k>J<9!ogD)QTl~1 zP%>lYKo$~Zr)fGmIQ;zch^L+;@W2CQ6W&6d|On+`j?@uqVgJ}XTaeh8MC z9IXhj+t0d1fHn}-AmR{JpiWtURbNk_vKrr&SK=Enf~qM~FaiPRmkeVl`%k&E)k%de zb^#;E!i)fj#1I9MyGP2S74#ID0EfK8;*gPOFn4aJ^6j^1iA2y}_#(l{lQ9c`Z$R5N z@on2^y7g9!M<1qi*H%m`?&MHYN{EGUzK@g+5D+mzwK6yhd%0ieH76NdeH*;7-tM2` zS|Kpcfp4yo{Od@8G*lv2EOmlIqo*|;xO3G>Ml96PfqME0lnaoOX=Swl{rJNeW+3fz z1R52O5{O5;Y70!^?0rzXc3nHfGd{OaRfr5EJ44aJ9pxN7(1`$DT1s@}7&`p{;)|Ei zF=-NYixyEeWeWIwP7TKJYU9icYroLNHo&55nn|-|^RpT5nATbTsOZIe^`^>K56t(^ zsuJ71WR_c?W?0~l#;N%GUukS?#P~noq+!Vt#9&1N;&EaJ4$!`C9r4@$M(O=`5ReWj zir4lkllp~h5<^6^`<;Afw6F;XSS*>lMf3QpHm9$L9esVR3J|9n{Og$pU!A0phUhB1 z1(_%S>~SV4O9PWzzp6`Y3_;hBCY>Zp8Xy`$L?cJ`g3~;&OCC)ZSg0iQA+mh2BglU@ z);y7ZPj$9?nxQ||18o=yWqUdJhm>1Y^5R7iZ&r%`g=d6buy z;jgWA2)hh}VZ3r0bJp%fHgw?{+G7a-h}27!$^ir!tfZqG_1UtO>7@wbbFq3j`u+1^v#i|eS<|9Wo#fG01?HSXSb(k}Uq zupJqTn$S^NN@U#eh~pv&rU}15WTDt96}V)GEO}rwAHC z?mOt@Auaj{U~K?Loa&=_!}(ZAshG4W5WB!GMAFb4jYG6!xZbA#Vp0$_Ok$^=PHfyb z%Kd&yX3U`EoO6-o75Hnao#>c6W=kGXO3B^3cZ*z~2^3QRy;GR(to1JBq*0=u;Te(S zL8N*x!u5Ox;*fg7)K^?-F6j}xAr%K}{CL_=o{w?KWsE-aOhiphfhcH+QCdpbMHg|{ zFwnpF1%XX36KZM3HXS1)a20k^@m)qx>KfoG4JVq6x0Jr;E0RCHr1_6OYZ{NFbEcVy zRhr{Y)7*24U{$4|pHeduM&iOil& z&8%7YCr$*v52SHyQZc(ITWd|^zxtt}E)LJNmM@l=z_<29d}2OpRb<@Qa; zpKL+xNoL)f>%*uJoHI{z(F~I_j)T#)s{0f+4}286VFp>>Woyd>7(R&|4AC)eBAp8t z5iw0{AqY&E$cPIrKn8>O%F7`fJ~lmk+eUYGf^B1Wc4DnvOXISeQr|NYIu7oZoV`?62s$PZP@ zsfHI`9@9twc}qdOvYO^ke3lW{UyrP-EBrzwzn{>gNd#~B9IbQb5&O%ZDY^5n7=SjM zFuX_sJ0jiYdm~g8gn+RiN5I%|lFy&0^+)y!4%^-Z640OtkC)V!0V4$jbXSB8EVtNC z_xwDar2vQwKisQY)4`m)m(n7vdr|+;^_n`%`;xBKMY{d=I?>JI;b7&Sv&V?2{17>5 zJ}toz{z;QCSD#14$dOdcnd9&vNa@^zW7OcI6!Df8I(F^Ce)nAt{_QqG9i4=%IR59K zVZ^>BXJoJsl*>)j9D^V-C+uNfDn?6}HL^Gk_*e-qWw`n26uHXm_4asXABy9;dCa2k zzkehl=P2HJR(o|60H9sAF$1TkRtQi+&{!L+eopC00S6+oIWYV8>`&U~_`T6G) znKFgWNt4iP*Wlayk|PG%F$g-zD-_B5mRcz2{;wb~-h{E}N)44g9256B!cH+-s{6SJ zeUvlJV5rk|2c)gL$#75fAOHZJ#b>1*9M;=c%|K`BG3|97t8@UFSCFVBM3g{R)zDf| zN%V{}3689%_TozjjTnIp29e&-oMR=V@1ZlxLdWB%SPX27=8YTa*t7|0S(F|)fba1~ z3AQv-dSEv~B@RG@dx$*32STZSyNW`m49Y4-0Q#_rB0ra37cLkpu1vCEy7R+r5EewM ztLQlObmY}nQ#p4op$QX)uBRg^j>!srZCR-1 zW?HswLq{U0=QiMf?m0SRF-)xxZ@)$Pw(VVWMSS62cs%0RD~T`*@giV5p->M(i5w}Y zuDQ%Q!Dn4ug=+N^NKI(h@cTp97oSJ-^5xWCaw)#5s$ru9WjIXPs#U~KK8cP${DIP6 z{v2cP+sp#~;NPX~!3B#cXS*+HwK0J6x=X$H$RbV7q{D4s3!KaW%C2 zNR>IAR0IHNZkxoZWVIC-vb!V_L=m^5ivdOyN6 z@i&Y@e(ZXhfBv~;>|+6Cuw?r^L7hzaqt;mzArB$^1Gh}fq2}!vlI!!l`e1C8&|5Ge&*Iw zmtuVBD>R(87*SR>u&-&`bnM%Qv~5BoMmXds`M#9Z)zp6EI-0AhXnyQ5%J2R=q1HCX z4#@5@n|C5Bk-y^~(q=8qrP96G%ak9s6dXQv36TpgWaI@G;ydoR;cxi6c!xnNMcbAw z?EUkf@U^rO-uWismo^es$^ir&oft}^d|;VQU4*n;gD+i6X7}80F&J_o=vDwAiba5Z z&^JjltF^|bhf2X>x`MmZBCBj5Wa=|z=IBMYAGOmzGanZrEkC--a!=ej;$G2{mJW(n z%E|Y4`ocs`Uq)wrJ>?($D5VP)g3mXw$|)M9;~)PZ_S2uhp53%udO0PhFQ#|9j#bb9_6oP;@=|{G%s0B$&KHlZsEcK0hxoclzYMBaXNSHqV!;~Dj>Z_}~yISzmCdz3x;5T@x;TG-u%S{CKEu$N2%A1*IRWWrQBuf7)FvSpMl zSTMX&Tds$%V=*G{y$5R^#NM)n!;uKQ^%lVmPZP3j{CI|4IRH0P_u|pIQBskbBJ0VO zIl4L;XiGNuLV{VG2AHF*?Z-;z#E5j`e}{Z*atH)TwCnAYU}scu&^}4avK9Eh^c5;+ z&cdjz9azJ!HM*sR18dfx{_6)+y#5lth!g3M29$P03EcfRT9tw?eVMWuGaYi3eD7B% zM9JdQ5&vGw;c??BdHhj=@9w~G=`m%0WVTpe(CbJ*U)7NZb`1#;1+8Ny!oPfx+RHzP zEG->AA^%tmd)rppHoZb~EJm=Y3H$E5@V)vH+kQiiPUb>93OE05r z>eT#)Z4HC)+_?m%OyS_Vb+kS4IAxDNMrhk>V5!VC0Y?G{MOhzs-KL;37^3xK*JG?$ zLEV{WK`1mdzmFHv*Ba~4Avzly(XlAed+wp*@ka?g|12Xd@Vn!b1t9#+K4{rBbIECc zbpL(OWYW%!fi?t(q8mZ7Q)hHlwWl<=IGJL zfe!b_Vsth(()z1kQgYXw1b6-eBW|M%H_vXmUD_a{HsSS85#0O=@dwwi_p_g+ z?)>vH@?Ehl1V(u|BUZ1*TC|ARrI*qA!|zl2=pzv8K%;x+8;cRG0hjT#=|mwQ6r%aW zh1g4%QhWW!F-MLZTKpC9I64+X+cvG+w$bsz^TZ=jDzwIUb34YfPh(mZ{@w3V(bkTU zRLt9z&J~II7T(BZtrCvcP!ECxB|A+!i2<;{ms!Xcsf!0-m?(yx2s(WXDQ##<4G>6D zegD}W#b?PamPt&Av2Ypjc?*eNdO7BT1(Z*pju_y=CmoN|@#dST=Qg13xSh%kPvUE7 zb>@V)Q~89OWA8R2!X5TuNRVwU_+EM*Zu~#&Tl)}Y*L{@0tl9YL>hc`6l9)9$7z-BA z^38A2an5SYzukuU(neHUivw7UB(F1xaZzWIF{>f7Rn4!tQ6VTGSLIxRm+59;Nml86aW7{g|T1()w5;|xV-ntvj=R;qJ7s+ICzNI z{rAziZ5w{AF!$`CWaA6?aOzQ{fjh3+txrjsPS`crN+-^#+el}#d(<~o&UA)7@2w6@ zm7`=?%M}a@Dh^_z@FQ*E4gxCRC_urs0d`XaI(N@4t~}}u_X5l0>U6iVT-#tv6RTk~ zhpxPu>W_b%l71}o;@7;``__BvKHT~n<#7EkF>QD&6GWjiVzuqoU6P- zZ@-1H=UuD~8`yWjg^c|4KOskrLIeW&y(fl2#hf|lY14?USV7Zoe?!?Hevi5TeZWdg zv@H?|4UFu&{pbj{OF%l-uw&{hI==X2>MpntBOER~H&<(PJdSQ{#oD%ww)IcZ7L8#5 zhGk)Fcm`wV4uXgG5sq|V0Hy`fe{`JHoA&DApr50PTBVU_@Vk>53;BB$Y#X3wJUm3% zUnZp^4HGI-1Lsheq&j56QHt$09KP%#%0BjS>Q6oyInZNzl|r?((s;+6n1B8wRnM-& z*BN)b57m1O)Qw3}Kqzjb-`YXow?Ak9E3crhyo$O@F2O7>&+~;5h(LhAamQiY@Htve zTMW0}LdE^JfvpRcfo%98fJ83aD`-7UHW6!q@))_0Yl7Ij$8b)tm!$_m;iO+&9fpHY`zjxlM{z~()*Mq3t#Hf$h%*Bz8T za38_<-gNSlrhD|R=dWb%F00pT>~-S_0Y;>gnx`K@?c70gYb)iKUFM_&2B5vmtgWSL z#R@vcjA6geN7dc8;kRN#V=BooK)zmYy6uBl{TNz5a1rvX6^vZD(rINK1kc|4V>dUW zEvsuvUh5rq(0S+()$hHBZ~ar0x3!|%nw=l_xwAKLL-1a0gFAm>AOjd;0M&CYKBKyt z7kqJIO5@kv%uf*j^!?BUe$h`J0HG2>?R`kMDS4te6Q$Q^*;RNX74T?RVZ;MITIVl9 zu3SaQ*=JKeea3(*K^={9aQ%Axd-vkM^|u6`c#J@>%VFz^Fe&M_A>s@k%I3Bz;pEw+ zN<2@vf-42^?k>VV{4O1jJ%at28>m{k)EU#7@4F)*) zitZOIwOFmlh0!-pQMheoT1lX8g?u zy1s%+0xgDD_?U@0Rb?l-n)C&G*(yLSdciK)8?@e=dzOY5BrOx7`}>x3{ummfK|dXr zUPb5&U!>&t;|B)tdLDi>igoxfG8!fR&_hIDeHAvnL~#4-m}ml>9nLsh67ng6&Uq;k z&QXsokZEv_$KBB%jhwEqG}|+w7bB+A6;NkM4%C-bY9LUK%=?v-X(`qK^wQWQd+N$9 zs8KnZ)YMHjWKVLBq6d`y-g)OGT_6zG&^&Vvu`|wO(gr1{BpG~e{! zRPWzM)y{1g(T*-!PLC?N2W_p)T!ntLNI*tW)(++5nMgzsHgL6qYopaySp;SO|W()U-&a*sF8K3G83I zgi$x#Kw#p;0rC2EOAF2GpQ5v+mH69l6MyId$~!v=zw;KQd-g*x$*}9hB-Ae4ko@17 z3PtUn##BI&Kq|v(?L=Fkk_#6Kd8-PSBPQAc001BWNklU3 z^5@s1UVNH(O+D`}UqR@KtEgGNJnx+X_GV;pBY_@W%#j{-tv`sK?5A;VfbkeE+pJZ%W~JZ&fFx(yi`SR!Z&1Agn}tq{ck3m-@f`p6EaR!QqZ}D$iYw9Do#wEH9_>(o2ag zTZXlHGtJ-oE~V=pAxIJ>^DuYpjwaW=2(F+I#OlWqJL_yJKXM(xdGiLO(9>#d<22qWNJB8i5r#SqHQ7krpFXaAaflhH zp6U*wH9 z=)CJrf|g-v%8w9PbvAWpoB{c6^~4!V z9o@d2gE##*rB6Ra$@bT~D796RK^HP{Z#`lnABCe}7Hyq6R~1dF^^SH_!5> z02cb)DNFz$lF|S!zj+QeQX(>sX6%cHo|g4YXHcZs79zaVzBaE6#DMRdy+S|7L{dG9@h-`I-T+~mBz=&syS z^ZGi?S5EcT)4K;L|4U`bTIY73bTRO$L~two%1gw*@?}~oD>;1OMU4F9r!ed52LR+i z8^7Szn^Z1dj6HQK2iB}1xb`9Zk32-^(BZ^)P;MGq9QnTF+(S<@)4eZ|=Wch?PJbb-hYwnk!@ixsY*^{Q(r`N9{d9W!P?W>)CKhk56=+bDVDVX7ax8=qw-WKSk}t};pf zQdm@VE(~`rziL0nql^ALb09%u5dicNYbdQXVq17Xe1Q2K;zr<4t0Cp z!Fp#WO_oK;Cq7B2p&|c<#V{~Nj->vYYlxq52F+u~;rr#!kZm0(+cEyi>uom%QI{10 zM~4LB&>A%9{OHH2{LE(wj2kx~K$lpI#@lYA`Ws)Oq@~qqHzcX9~lB8pnx-nej)JvSj70d5GqF?jdl)4V28Ci}2^UQ$PsJh6YA_ z;fu5^KZDj^{esea?!;$0<6DJI_W>?h>`5V#pNOQwwXE&1611Q50fJxq8kHxVlz%cO zt+CtNX}R}a>>vCmb(=Qh4>+|f$-{*TJ-R%Uik@?M(Y;o3>5e-u^u~#TtpV^z=5obO z)c!I2|j09fq0y-IsKEqC5Y{E@YU z*FJ!+`2bRZ2qg^CPz`@38z7H8(33qC0{?sO5ZLqovv=O%aaCvj|D1F06!q?I;{v9b zYHBD3V=$qa&{81D?uI0r5C|aw0wkO4Z#M}LlCaCBkpd|Y0)$RXx4~dc$6(WpJ1(*$ zt81j0x%ZskANSswtMSN^JjEk<&hzN8EzjI?%KLub_I*Dl*3`tNxpSFt$(J$4j?K9@ zz^JdM=9E*2PM$=|A%{}3@Sjwz`VhK1h9oBw`4*`lS{2^B4{F6B(Dmc!JZ=sZ*I!TB zfd}Ot{8|>h%ip5wo_h#B{!hv`e1K8v$|RSkLD(STF7PeZ;uo9;z^_|`*Xb};xbIt# z&46-LmaKMJ!w=BVLCef`Ryui-&c&w(GP#B@NkS)o`hh90TbWlHr9oFz(|gn>Xdgd; z$}fJA+F7$Oc0pSo zX(`d)Viv+F84SI!nCe%T(A?BS@T{|_J@&ZVmsv_lX#WG4c=a`OpLr(RA9;k5U)@S+ zTRYMX%PEgYNF^BrcUGpGmbGmdYckZcH&7P0cmGrmfHsm9A_|Ae(g1V}qK6%d^_{C3 zbIj4msktY#if`Ub*Rq$e@3@2NCm(kz3WcwP%vC5}@@YOXRa$F)kOdrcwuZU^#L(rb z9zxsZ>}mO(>#x*b7OE$tV3Pm~X~qWZQ$C!HodEzCaL*2fz{6Gg-gQOF1+@Yz7@%j` zzC_NuklHVOiODrJhu4j;xDkzVu%q^B;@N4ku4ZU^f1v#@hic6aQsLt-<Vh!s@B+LSq* zD^Z~C<-!C2Ddj6NOLMm;()?-xwoF<&!V3dv&=&M*N#v{x2wrwM4g2j!AlJ3%X(Ybt z>}31m#e|-IiqMkh5g)zhSev+g)zy&k&InowSSrwo zEL;x&z6K>(ApyV;BP_@-brx95bW-pM6>{FofloilrLR^sH)2OGuWM-wEXq2l>YQaN}D$IBZC+%E9zJ05ub-icy#RJe z=!qO`#64m&r|D_nWuCpKWM{(@Y0AP2^jbY@R4DW+GNlx=8v!y2546E$VjWubN z!h5b9i&~Wg={VRSzKJh6X1M#=qmA)rpRz(rvm`0zm*OJOkSYn3jZGT~{QT#{7d}SQCyt}+i(jPd;6sqr)wz8L!yr6$FDkFQ4tvuV>AmBR zlsx+shUw&QXm&3JfM-M~3B+7xOQ3tsDah-tqw4q*khxBzREo$4AJG0kw@|ihDW$95 zcN#+#Yxwt!QQ&ds!t(%#6p*MKWf9L*VCtQN4lN3E?+mN5MkGB2Rwzv0w7uxN?3*;4 ze?GFV4q@hTxh)IT)I`($_hT+zM8#7J3E45EOW&)qcs_E;j+j#gmBAc{lrIMisQlA(lgJZIuGr8wQbY=?n)vz-ALUN_Y;W4lZ~N@$hY8zk%fIX z((=p-x(Xm6DeUYkiFB=QH6uGwP%97@5_u9pHhFy4FytT>m`#}K<_^LVSV#TY8EV@vb+MBPck2^QIQC-)vM{a`)xgH+vTi1LXiV=eBhU10Ol74w1N+!35mJsggBJ`_U=>6woG+lKywWpniSy!L) zB*8#sB@Gu|$o5c(w&$Ls>X8RAwl}3M4fNI|Ti!PbJLJmr)iu!bg@2>rn^$0tAD`Rv zpfx%kr+4#aI{yB5%9lJx^-IqpU6+?u$)wJr@-KADGkc;(7w}>iU=@IXIA}B?;Kn+2 z7KwmF8iuVjARzU7ayU$YBF!#H^I<7=+)k+$F6hVkg5R4;$iA*z#ZH#wghSb7PK!#Egn0D4!Iu54-`DnOlq z49GA!Dcv*qt=?hnZQ1`jsgL0$9vJ|AeU!bwg2)fQPs^i^PJ1*yO%~YWmZ)zgm)<*Xow^RDZKM?M2N16$Ps5o+cR)!$|$~JGLr14|AdwS^k z-uEdx;z%bSB$w8(lmsSBp!%9?=v;6%J;-KX?z(+wN3B*zP-IY0G+IBPs-u zvlkFO<4ne#cOEj=S^l<76v*-~fO^ zV>!A7!*;9%f30jLfK&Th{-*H^WJ~Dg48^+hpls;g_aNd|f0weOkEUkIl)QKA)4jcP zzVQaV|N9F{H*TbQ?T5&&?j0K46vysS{=&cS0Yhsl7e7waw%LBcg_vWFi8Ne( zId%K(m;0UxmPOmLW%S;71LGDwP7tSz)l>XR?pWG4kX=*62%|vZlO*gi+-6x`Uxy+9 z7~%vfshx?FZg;M%98@<>>LwL>HMm}QAse*2IhzxY)CcIZp#9L<*jIg*NedR_zFA4C zAdW=nUAva{zuiUklIN*d{xTBncyzP2SjCaG^%BYa05k+%c#7Z)PZ1fv7meS#nyS-I zBRq9#ZYxBDK$MhFGHVv*<(Jc35~l3_e^C1V3Pdd4ulL75TF}eg8EAhA4Q+=V4Od^o z#8Xd2R8-{HQc-BjBJ%31@Z%p*z2qs(L{H~rtUvMW19kZ%zg4-#k$e5s51?l=sc=sK z0QoSv6YX(F!iI9`RpY@R9mmZ@PoF{UdFK&4{sagH^XQsUSnch!+;=Z!f4ZG9AFf39 z^g2ydMD`PCiz9mgz#^GTgLc4g$;J&--1sAUAA5whE5Ad{@y9zIAagtd_4SOo^c%$I zpGNEd+(OmE4-kk%5`6@;@VPDgKLPs3h}|YcS_mTbQZo%|FjlQ}QNF+xoDJ!}-72f$S({rc8tLlDZOGEH4*eGp)y;K-p!N5uP?Z=WcVevJ&&K!{9%DKxFR87B~zowRQ+{ zyqp&su-wp{Nuu0Q7*0mL-3QTg&%wI*^Hk5Bi;)Lv>v(%R+wZ&+8S=$vz?JMt)ue6%R_DSjP`5#6w!w*S4A(kGv!{Nr^< zsgi=(q!_pyAzq}^zk4llg@&@X-y*o?ecD@EXrFsBV;3wy)YXE_ArKIPlD+pPyw_f| zOq@vU$3LQe*$Ym*)OKR}2oSC@uT5vqC0az6meGFtsTd2+p?<-F+~>ep7Jb{d(|Olj zRQ~Fhm`$6Jp)^QSCyf|%GW&nzx{&rDZ&{XJ*Sn6=&QBpvVL<`_e;)uXoTn7=EUb%8 z9?(Ks72&h~g@8UO==sd&5ug4HV~;t;p>)lo#jhJ1>D;)Ho@Fl)e(W(CUU~^L8g*wa__^ywXh1?QZt;QGM6nh%I`OO;0>d_1C^e#qlRVD3ntWAc8>}=FX)% zVAA-b|E6y7a~P(hvMJ%J#*JQNRB^)yrPQ zXlioZD#bDC5I~kM;Uxr8OadGRdh$5DvrG+|xPhOHra<0V0Jb6NX&6W3jI(L@?zNax zrsTa_Uv&L?Hr#Xrb&H>+uB#nmdz;fm&2U_V!Y1LW+#ShFgOyM3jwKZ%5Ty8H)5;~L zF@g|k-_E#4?dC8D~>-_0@!@PR;3a>ApU!&Q7|QE+u@^O@!Wf6(O~A4vOls2ml7VjK}{{ z1OUUEJU0Z6vdO&u&nJ%+Bxq=bOZwogOTH@r68J-aih(s@96iSz3t#vm4M!Y-IblLx zHLU9Fr0wOGvG4jT;~%<<65I7%8OeE4?&3&UKy6-&W0!_4eI#2VnW-*qr!RfU8?O<3 zU!^DFQqVt?j(S6U|l;8C`0+!{J0|x~?$Ie4tdKtB| zXA_z@F{jUCx3<#qr#mp8e1g(Vn=n880BQO>ld#i<7HYI0gd$ycL4b-q1_1Q4QoJ9p zL@$HM^pbSa8UFsu$~0)LUn!Muq$Q%$>CcexYMwo1>ShS5$|}?+&ml5r4pnC_z^JXw zeeH6;m(!@$R<_*sTa4fTj+*UT2wFOsJfn9^;?ur3vd(mYcbLEdgwRgdymm>}4l#)? zYa5w?pMRI;WH{d+XzAT-dm(lN;22<-z+?B(Zd(}Ff1mKISvmJeiLx@Pjyn!(>QtIT zVJh$bGorJN&J*SnzW#fZAAL;DUHK;AuJV zrpfC2=wz%_$fVUTq$GCW;j|oo0`-?(M$P2O`DO6~h(>9D=_O)|7g71g+X%Kc6A&pb ztTcy2>nzY)iX45mIEE#d2HgMd`3T12)GoZ2*0@FaHQ%ND;DZsl=AIA&bJ8U0zx^H7 zl$2m>`k4B!UrOoWN8}tF#~T}Ix$QP8ese43-L1Qv#O2LvaqEsOXGbnZA(uxNW)ffy zTu;J}E>c`~eivx2UdZ3r$f}=6*SPU;^>>+g>U>0bd42=xN@4f*vi+a`r1XX#Q}NNe zpl!6=d7p%l;L<9N><`0&n#UeSwYSqeXATvgyNK|VDTrJq2m*l#6WI6q?}1VdM?tR6 zh}P)#cB0KqME>+A>i=*XMps+nJY*`%iXdRm0s&$K*)N5c1K=i_E9tXNROWg|_$=~7 zO|%-Efh^i(7%ninsoY|9oZ{_h_7YpOhW0!ELimo~5bEf5^82Jq zWTHXENwTmNM{#%_0Czvw0Z_d?RJ{BmG&a%^i&1*)v6y4V5U8!qZ^cSBhHcaR&O3De z;D=N^vyhNd4uOw{unfl#F;sC66am2K90i>OC|u-SrwJZ!Q~s;WK!r}Y;?pej&MYgD z!iJUjx=emYyMO@UBobRI&htR+O9TltJ zBiPgJD1BTq80AJqTsl*kGAaF%{bY(;C3V8o5Ute=+vyJ5{gcvqSLh6{JDfJ7!?}+d z{I)y2zZ$?K8Or<*f{>-pnkiqm8uefQNu<7>woe>K>D-g3KI051EgKPnPsihQyu6IQ z8*ilH`Ns(v?$lXMevo!Ug#%If4$F|xxsi>T+(+aK)Z|gA4w&$l=A7s>dw)-~dkPx> zctBjhb|v;X0{!yqySPp|#S^g07r>Xur*{;P-sXy1&~w;f=xe@9^(Q|`T}f&FJNCt5 zbiVOA>bJk8a_JJ-vSEkG(&0dw1C zsv0*__RiaMKE05pwsz`1^BIiF$^zE>8f)uTdRD9;aLX^KSn{+(c<3hSre?>F+H%)D z*~?c9aTYiL@Hzq7Nud+fx}h&idSGWs8behK!dKFl_>S8MBr;|kZKt0>$%1pJJMFZb zCrhOha`xuUwB2_v=E8+kEqfLz-1vUcomm`NI7A8W5e`jn<;UycXFsL0x0mwIeU?C7 zec^=!v?j8CJ>9?f1?5jXj`{I=5E|j;ITUrmo;|YRtPnnM<@I$LtdnIC0Hln7Ta2uf zBSYdgvJa-mGQe&^`O5H8T>`E$PY3}qG~}=FH7*lig-p5*I1+X4xs3Uje<4&~pLchE zysL}oJMR#``)`!o{#%0DvDj=Q|3-TuItYr%T-h0tBsH%voH)5&znapUZ=mbNC755k zl!_BhKm-bdGgozW(Y<^*_MLyB>drd|#%*kGLnybwP!RzX!EF{UA^etPfeW0%^#G`( zk~JM970?+FRY;j;Ju+wutX(bvp+HMXw0;a-C(ozk+gDLOV@6(DtM+@ISd5;fONlM~ zC&4Elr)tYubihgADz@n@j;x(a_W;@Mo-!1e$~_MTn9Yq;|MPyLjg7Q@_gbosJw3y9odB_f&3KkD+ZaoN$-6opWq?`7gzTvgdAZnYpFDLu!GWOp0BY z?_7`oz%cr|>xE-wlq-AJ@sut4vk7XibJtK8*4-Q$5SEkZrv${J(0cr_1g`uxHHRKb zAjkaVI}FPrzHuY%ufLA*+h0@h!t(?|u6IHV-{Y^v;G25z|Hv{5*LnQ@uAcO4J=1VL zt26k%*oAjZWw#T_WVysg43-fuBLDy(07*naRA>8#UR{d5<{BE0J&w}PewOMZjzCmZ z<=5)BZJJ+v5&M5`p?=v*1hzLjJuN&UFxQ2r25WgMcH`b3=w1V?=?)5LlmZ%^#<4n_ z#KwV`1T!UuOEj?adyxy+3s7(XKuRzIfCbWW!T=%&m`qf0#I7DZC6nu+-FUb$)S!<@ z;Q*JQE$Es&oyfTtP=Dr`1dly7zf69Xh1JwV(*yStSn?cY%U&SV+2YKV`(7%I<(5so zHevukuJRsq2G5kK0w3t!nb0c_<30V^@wV9UKt0Bs&QKhyvXEL!`phVibaaOkL{(%ttmtK%Vnj zD<_9d6+-|S%2=^e(Y8yAKOB@NTtR2p?gj}$tSn6TQF92McL8HAyb!ahD!*2LZ!ay2 z7i0hGRw`G%j%?dbK=d!uDvsjFpQ-Iry2@!NM8N49-8Eq%;Y+^4gi}tq%6CwA6Zbl-9drO!N#5$NygFP@p=04EP58~qUa;RwUcKPv)&A)@wlE*cHeq9YR| zYj|yJ-MIfyujOa*Gfj!L*90LwJ!oxV4HC(Xq?X)bkVpAETY^xFvt z2v3|ycms ze^G=S-o8LU+e!0~S6xl*fd>*8J1(zQzuvx`)+e7tJ@ODWkKISWEnSk3AocOr^cR(9 zaqJHHX-h%pywfnRx|*5;4#2E0ka9rNr2Lp;C^_&znx{@B{@{aDEO{2u-C10?;ux-- zG-^iS^#X9+CfeSOL@kNhJ}23v*MhcFPp9U(>nT6xnEYA&T0=BS@4B`0+!{li`xVO zJJ$n#mQ$^r*nKWULlFQBbK*p*^{z9&t{BR}@M7s2VhTE^?M=_T`7~VeWl9e@B!AXS zG)njT?-PIaS;7xLNbQF!kv$#6q@b&A9D2^l*eRuk{*CP6n1PhOH>HNw$o1<8zP6O0 ztq8>87+NO;UA{e|Vx$0obV5jwe!>b60O$c?;9`j16)yM`!V#L?GxjAq?-Z)P@)be{ z9#q(U0Zfz7qo$S@r%S&H^qbJ+6PU1JS zy_N8D&k*kFNRI(r`Ibo3v&nh~+CtC2EJzPPuCi8z9(;AA?O4~b%2Ij{KMLzBUuE*y zXCuqY^EL-oVl`uM|$sEx2(1TB3)tT^`gA!IW26pKUaA%`Nay`C|rpMfYV8zC#t&NT$kwLCZ$uttxVSgU^vP`3FkaYKkl>GBb&+W>gYS@ zP^`X4!}!+F#Rl;_m|i~egQ0StqVxhK;%cP@3GyNIgs;~^N# z5BQ14(Ob9D`kP-<_Sf4fX>U$=Bb0`o1COTcy6<7`yKlx8mfwIanKg@&0}endMP%l_ z#5R6R=#AH$cFCSKlLzHr?46!@$$nmBl5jHot-5ivpLPbJ^Dm%g-aJH}&Pg_htgEB$ z-1CSZdmLRaETQDan+U(X92s(zzq=@gN^VfU^7mW&UXO$#P~Q27l~=-vbE!S=d_)!v ztJFPhOqhT^a>5*i$}(Ay z9sn&}`dX0$077Jz1eh995Q2W){FJ*f+Rr|h(rd3}+`$Luw|P%b56urfgu44qs$YG9 zP*+d#x!sn%c1a1Mp&|S3RUtqKgp`!boJq@>XHoIaJD7G1*^f{lzlyIocGCeAJNK07 zI7G(O5&gzDssGZK2-G)>yjH;eS2PTQ6DCl5&ZlS&hKSy9168lRMyxtW+aX62EiFX{ z!`M;~QVP4L2SX{smQ85Io0PP5JH|?I06|}DK9igS3wqDJkh04z%c}BEkM1P$=%Z=B z{m(ndyAsoMtEV(lNXpB~2px1#@A*Qk`oeYB?Jg}cb_S>-^xS#5^s|an~ zoQOASGvzVzNr{h81r z&1VPYQ;<8}BSB$mJCZ@kWTKS{$~kabv32??wg-#n0TD-4I7BJwNZ2M!I0z5 znj#dU@{^yW@|08DB!D!P3_i=%{QB3~%eJvPJFyQqfY$&1A?4dz2?4h0;D&%68qn2} zw3Q|4C~ngw{0&5KHsmC}T=3NkXCDb1_SGH%HKk?wYZ5g)S-!^wZwfL8z~f~0CuL?z zsc2}Fl%R~H%U?~}i>^NtHrRX2=)>c~XxSi@>{tp$+ zTfr?W(lV*|`^|y(Pwli^72VxLnwluDug~$Rl^|u31f@7e$Jm)La{o``oHwb#8Z(xT zgAYM|=W41?JTdofTcwCMH`DXVD}-LC;+g01VAC(@rRD zm#1-w%}3M!>nEKRC!SOpnl=>~%%dAB8YR}zLC;_QOx16GNw|5lQ(kAdmVoZl+d7L-?JOZ5zzjaTYO6^DLer<`KHjes?JHLj z|HaQJdvPh@^=qA9>!c?F3DQ=CTN(-7`V0CVe1Oh#&!_$ymtj^_7Uo0;0$6pmDB)`- zF4$wD&9?8Ir7rmHbHyBhLNHPwk*Aj+<+5RFCQX%o0>AyymoB+fnC5|b)BD}sMAxmO z`<}lew{4?z)rU|&hQ9iSelIdm{kyjtCC&+oPnk;X{Q3D&5WXLVW3&v^dN$3463N}W z`3Mer|KTJyX4t1ZYwt9e)#1zV*V=K_VmYjSv56DtKK(4}FS-ab4{mmC+eB8bBzD73 zsC?o<0IbpnV73R}1eY&y1uIDk~#2Qm9c=mqDjkAjS5AqeLS6 zT>sJqcs4|;D(Sl9->LihrGzF<%smf=*2EecY5CpnD1Y{8D&Bkb`9ZAe}=OC_JeS^a5Rf>JS=IwVSC25OR^^Ff;A`hI5Ovg^Z+OYN(qGF z5aNgt3IImM$f2nsa`d<(#M;RJJBg&?R6Z_@p7J`nzx-9kUGXi9hKAf%#KdAmwr-{C zH@~L#&vy{q){HjXGT5Qaho?lf7aoIvgg|c}_HDn$diH6WzWxnr7o6jSFr+M=X#Rlv52OhE=I91;U1n`jMj1?}af1g^M} zabNv+j9gcj0J^7#_JCx^u=VGu3(cmv_h8;NY&#^!nR81wlr5X?ttBXw97banUNb_03U z((Q@i?6YAV-l1P}sI7m~IgYeX@h5A6;A5+n=dRL=-klW3RZvp}x+gQjGS|{_-nyKJpOZ z<;$s9^AUk9n|JgqiQ%;ZPxK5CDShl+C1$vart0}cl)Si@$fnIi&OMjPqmDvWROFBN ztGyljsVDk@Wsh2U2l+M{9nfj%%=fGifGFT*5Ct0ouw@ErERMXNTj@WvMG#O}i=+E{ zF@#DUX60J^mV&4POG7LGsA|fB7rH+Nvrh8!=+> zerMZz2eB*rY>4h5CBamhsz3aZ-e;d?)BI0T`I*m9ee}`!>1Ci4^!D!fTB#nWJ?MND za0Nux0f3aPt%4#cqMC4ea5wI4C&S0B;y?h0D3y`I{WPRf1D zZvk3LFY?mg2-bU!Eb)7GETyx*;R+1g`1i83iynZC1Yj`_tvE)dq}WwcNGCh2p&%|{ zizK#H5No&C_JU$zmqnMdS=JEbnT;{*Hx=}3+@v>r<*OE7`6|F$!0lRV0n`GAx`1GR zU?MOT7(NS-QdsTnH2>)i!vFJg$~SLCD1jC_8KZY|7<8V6ik4smpkmo`wBID?oG^jP zS+k0JF%R=EJirR*Q5*tD1$kN(w(cs}B)}cKW5tw!!doYqX1(+vW_hly4L!u$>I}UO z8NR2qKq!ea6v}X1g=jZkpX$jFPJ3wOR>DLK*amci+Mw9H#b$XM{B(t8QxrP?Y0_rd z?j>S_%$E=v*bY1kJnI4i6DVlw$mEycW!at2nlE{#HJzQE zSwRt+krBQwSPwvPjDQ2#;N2ze%=FN1! z{yM>*-b85mD;RFvPP^bGp-k+QImkznsYn70F9A;rNysqr2TTi1DCVL?WiGCn>9B1& zHmoNQiz9rApB|{uzK8FM^fdhbkAFNIcsU!>I}&tR!S(|1cfR!!(uy8{-Ln?OkpuZ_ z0!kr->k@Z8DwY9JAf_Q^!#06>1Kw$ma_5^C_qHh_6^hn-JPSWh{1`4UXa$x6OI<)< zXr%&gKk>wQ(Yx-tdGaeSj}LBIhYElpK*U|^+a-a~^jE}rOYD(5T4*stP+}UzjX2t6 zO0m9j&+sJ+Mq zl*fbWl`Yv$(pfG}ZmwB|3%P*7tBgNmn2Wv*O@*zj9B{@|OA zU7uH_KLkKv0oKXz@Jy&U+T{AZ3@T=7s*O05xw_YkJ0T8%!_%NWZUT06FY7$by|ugd zh1zrEh)6k)d1YeIP~O{1>53I-Gr0#n@Tq-!B5aGGv?nr|IoFm8fm|1wFi#u9Rj(6Us52#_S2$kHD5bIS}y8YUD3CQL+z zLd6Z4FA(4+0XmK!k@qu)NH)F_I=Nr8k049gno0;v48i8;E^ozTXv-JcD*}Kaj)$Xk z&Mbv0k)-#`)_&PjI&fvxd>)DkKY-OOhIorzE{7IFd!|gkFGzMp6vC-vPgrp}X~}v( zNxLtvJSIK~y*6~((4}FupuNqedAZ_-4nb2~v0ap~{4a5$SR)f_y#x9k=yJUi&j8P! zBaoFg2WiE`$>_QR;9L7^W(73Cu!0&JN^!ykrcAZ~$99#4d;SwS5iT6kX3~sIG&b_B z-JlZ~hv1x(2uz+ba?j#$;_bs=oZNwka5(5+r2n0AiL1P%I4SZdg@thTh@I3YTDzrr zDWF^k7%wTCS%ui#l;Lvep^MQX0Pu|t?SM0XJ!Lj5(1W0?XUmq7QXlG9fEf(BLOT@_ zrY35M^z32&-wpQv1C2mn=zYvZ0c|Or9_{Z?#W7Hy@S$cR4|K{h0RTh%z5t{f#}=s@ z(ueScbcBp@^b1j-9n>~~W!X?I*t%Hp)_PmMyhf-$G-)DBA;}Qa$!Ht;;W#$1-2LHn zxaSiPP;qAT+I;NnT<#M`An}Tj=$m>S5U01weIpST& z6tV4_laDMq7B}s>y3wgtP!RwO--V*gq}xcDWR%tHjp>4a-Ehg58YaZ!i5QfHARI91 zMLXU|)nEBb+$JRmG@1lOxey4EB=riF*j>6VPVEDrnlOX$`|q9*eT3TcI{d z-hbVu6O`Ul<0+48qK{idfwzH6Z!#eyke`e*rB91-^I>u&`pRm-Ui(YNjuFVP4W@F~ z4Q0TYI4`kKy4%y^6i^;G8zi(KgTNCDiJW&H6^9<0+m|jcM;?6)Jpc5b=}2_rT_OhY z>C@;vWj>X2<`k6C;@3So8zRgrq_xrj)gk~G0tm1`$DQ_|B1^IVFq8zBJXKvObs`2U zEhnF*TgUQ-kMXIGAq7~$tfBZwb#Ah9dIL;aa&V${~|_Gc0UTA!I++helzJcC*kwoz1& z9)NT}Ez?6#3Bs^+!W-uyQxiv!@9bWFH=~eg+-E7Z%n7g{hWPBg5r{+v-mr$VQexdY zr@L5jy9K>L)J93mYK!)1g1?xOryhyZ-6GlepeBM{%$eNP@v6%~utf8J z4+r?=VNu3Rg8K7K#%}84tGza}r$OChLG2g=Dl`O*BuEoO5a2?Cv1vW6f4hsS-4Tzw5y=bndATDtqZ5gsqaPDsGALWMAbDm)632uP-(Pt0^- z&JHoY%t|H_8KS>&t9Ei9L4P(vf6OTP(C&u!A8J? z1ok@5slSJmcI9hqiJ-SQq=(yX>*n1l@El;w6O_*u{Of#^BRT}Nn>EKykd%iNvP?r5 zrw@Yg(IulvWEpA=I;IG1*#b@5AWr}wL5!5E1EF_jy<}n<57kxX$iYO;f)I2cbU0m~ zy@>J09lzU@zw0C#JaQ`bJV$520f1WRIOMXu5G7z3I;kW`QR>of&5NBYJAdtH66&Ud zzrMewxu*2R2=q?81=1~A4#zF*NCcyb)+ z4x=4xYOHu8tflG2 z9#n!SZ@2=sI5HX=-QvMJG;Md-+=30~RAQ9$F!6Ys6MEqCX@Z)gB$GNJY}&|LAgCdv z+>n9cRB_k}!b_i}chMqBr_ac_?ni+zom>Wu6WSDMJWT^C)*&B?G)ir#$+DTy*Qw+m zwuHEb-a`*ZUiTkNnm->S9Nw*aN2HV4snhSTM9K7iDY~XmfknXqfKx-%g$yJql%X>7 z9U#&n+>wCzgmU7Z%G$546l$%q`d)a(r#kayfpc;o6M z!HUS0oTfwhEQded;_yr=L#s zr_M!=8I#)t%bkHP+d?aic>Q(E58i{IlUTkp0AOYF%zI`-fg4a}0X~ops^qTKK5~yt z=2E1q?-0Y)#Jdk2c)10k#*Osd_!G9gw3O=e&L>n?hdFU#X0^t)O?1NsdRv;2ciu_a zTdz9ISgzs}BS;x0NPBRs`?esoD)`W1+nb7CHfc7*6)kaC{8xxM#PBG#k3NNeasXLr zFC0KW##i+_M{fkpH*aC2$W*hL_7c{|u z=46SgnehAH8^_O>{`x7`eAf^{%){Y_ud=KJg63>Kq<6R(ALJgLkQZ=S_$I z-{+RsIKqO)@LD;18MYdJ-i_Y|7qRM(~3b z=m-8m*NlDWJN8)OiOdN-Y%ZK|+=3;~5#G3gP|FsG_Mt@51CtMnI5np49aiOwq4fhI zDW_LShXxwFng9SG07*naRPe*XX{57Fpdyma7cAarkMoz;Z60lc_SZ&n75<3`(Sr_7 zmyD}2_|`s>Df@*vu0~Q-si8p;E(Jn>3OIYpinC9v;;HcgBF&1|Y7i@g(6_&>`6vdu z9AZmGmp?@P75dODO#J5B>zp50p|$>BoIJv#fd2pvN9*! zvM^-s8TL{(61pV|rGEWog5Nj0hY6;47c5I02pszjuTWy4dxvk)bko`zP1{+cWe4e;f_g4 zKHr7<@KM*2W)!DBx=oIHf#-qewbtR*EnANH@TME@8M}FNW$@LP!-1X-T8}!8($W%y zVbI;%=ghBrb_~m+a`|hR?d^%!c%u9-p&17dIU5o@pgfv~mx(WkaLH+Ge486@&t`w<|Wh5@kcDW@Rn>-y%iNVuy`) zRE>iLqFfW&4^dku^w&xS2OVTGvq3Vo+|Xvdq0KTA8Ptw~H{#BZt(?VH68eIuhy@`V z!f{7tnmFK30wosIL3lrtiE#tsn*W+*D637=e0-EQn-%NI1V4RQ(ez}p7Gl&6Aw=KV z73E8&pl_>@#{MTpx(_awYD(SGXUwqFW=O#poitXww#Qny{2e-3dOQ*}jqOvK+O1%4 z&Z8i9%!#zkJB9II{1?nglX9mMqEQ+bE=2$ACMvdU#*D=&ZEiwn?QUKlSyxKcgZ2Th zgjJqKMtcTGw!#33XVFd%0#TrWwL)Y9)VAoa$ZSdGOlrD+{jJWIOJF3AA%dA*Zsz0j za(GKDa6pZgE(4;34guRGthQn?oQaayX(i_IAff?=*u2eeX*)))tt|4m*#@J8`AvzNw=;uZuMV~2b~bp z0yGu3hkdUeT+gzV&EF~Jk_u)rO0qpxFy5SPx-rk{3ieE%VL42OQ9}d#QVh^Rva?r< zA?TFdnIau%CNpH6VUQNUi#cWkk4TFy4A7)U7-Gjnos+dupFz~mw!Gd@8O-71Kxfow zl&dkT#h+;+n%$kPvV(a;GhhqZ4T3VK_3iQKZA{|WeZW##PM`9D(E%8=L=7#4dN+jc zs$Sjwb?N)zG9eJ#nUlC##J`h?DUXF-dS(3G#1l zA4007f*SIm3yvThp{Q#IIR2|J>NeJthPC*RVS38t>cF~l&Kx={La$5%v)Jt`uM7jM z5SczKK+tr5YGrh!KFseoCE{aw5my%kQzE-Z8#I!{qgU#2*`=+t)g6zarG)rsQ99+j(5iN~x($^;+G?i)aE8`{+8m zGl}O1#YX2BENwCfqW7)POx@W{&fX%K#n*ESnJB86qGcPN%t?a}d|PbX`rI~ZS-@yj z;zl*PhMm^+t%GLwtb>mkjYmkL=6qhKa}aOl)q$eu+JzAi-4gw`a`8X%`SguSnv_>s89vcXl{^5Or zj6!>>U`nNw;cbgOInP_bF9Olk!$GNhxqk@YTr(I~?Q|=g>hHawPsHTh<0o$p8M5OM z{hC!~R|)qPFCfB%^lI#2nXGpTUyvb!tC39yq z9!qmCGRz=v(!{X-&&GG~;J4nS@?~FQBjm7C(XRdh zEO+G(hbKLevGEQ&A3M)KrFlv{xWPARu=|pO3#dYM&qMqg%jJ>DQg*SSp6|zcUW^w0 zYO&viA5+9KS8r6f7?Yq=!`X*u&>O=MukSmN8%fJRziz*kw6amBLP zlGUcRldm8OqSD3`G3h`lD27~;?imFR#ssT)qvEp3CZg4lAf@I%Gs$S>a~O-%NAngVc2q~ zPSJ9|dS5HAqET(}120l@ZQK9&QW)3M%abVDK-jII9pW@5M#BBX7H1H&{wDpv1yZRl zshpcJ5cV8Fz)^wD)BEKgkt=8=ma{!@p1$HI!prpydphD;3%b<~=mjQsN&dCK(MY6{Aw8W&7#HuB z$|S{7ty`*pxr_m9MLY})WA<4_ef`x2+6Y_ttupqqf&0Smr4eN)d7t{Y4?>E}t{n@# zyat8*6_%_p@o#$bQN-9{K1)La>9m~I$NT<) z+7OPVlEqUTXQ~yP>pVP9`Ni2|LhH+WCExzE&Vhh7sLCyiA^1@O?tP&s5Q!Rw?xy& zO2?Qt=S26zKE<)k#XTVKmN5|J~<)3Sx^RjnYh0$ui=?S zU7_){$p^*qc0;ux6cYfX<^`ZHUovm1udsA|2j_C4$4Gn)nHltKkZ zQ1~RF8S>#71Po@UxNPMxi;^;3yn-WzY3XY<4gmpVWCo}!EiNO7^2KA4`;hZ>h(`MD zkn-7*{k6m7PKy(g)<&nx^Ny#VN^N%%qUBo>-_C2Y@xOBo!YdHnm(e0(bzMjSgpFP+ zwhSW@Tq@0bm#3EF&F%r%Y+wCp{FnE6BCE8i_PnIk{)j?rmf3FYHl(ocm)$j4T2AZl z5$9h9eEGwUmXJz}#nKurTh49_^JCt@<|Rul{h6a#P_>X4DJloNLU@uFdrsLx@?@y; z39=Odv{}&{0=Qz6!54BWQ|kiXwn4EHTdllz$rSzCkBh(K$Eo*!cLI5m+GP|cjxM{q zM|{&uZ=I;p{RV_aj!PmbZztl)lf>4TgvFtF02M`JeEHKYC$3%J3Xy$=q%lG5=@q0~ z6!nUy5Tj7WqmnA{!4#=&H9Lm4RBSpT)O!miwm0&mHGqWnVg(5KiZ@}`+~VXv2$D+I z#H1?1gl64F1RQ(uFfR9pK(0GXH+nw=JJKA_SXW!UvKBN#69rgU(h-FdO0A}uzkfbl z@Q+pR@LoMNpLG74>)z6?Tv}Uu#L9!I4Q3uP$V3yf8Ag$(cB2-oWs6Lj7?`4Tl&4M_ zlwnP(B^q)>V&Q}}wy}W*=`ui)kufw-G{{u`sI)76{J6jDEaUU=E4 zAsQo6olXOg{(K!kHJKx>eqo<*hk=FTeYp4Mx;jvRL&9^>qk{M^a0(FDM)}&Id1T|< z!OFA!;B8c2E>Xyby|f9@*qqwpiog^HViVN{jFi=G(jzczE18 zL&W)@r?p{M{GmfQb9AnDPjmyBi`i`uaU!$T-0tQv`yy5U@7-dxqWiGFL6(po5S*&U zZvNSK`!p5B3FS2%?jql5v#Hry^;7-9Y$mrSw_@2v7%fRwOBbR+dk2|{)7I@QhQAi` z*28A6Dpr(kSMGQ=FsaherTz8WNO;)9b6>t&|79#|Ob0afvRO$1mVW)=c9 zP(3Bahmy(UByO!jV8zjQ98xSLYFpv4jXLP*{>+oD@suKN z4mA4Zx7~J`JGb-86YHGhotq=)lYbQflll2A?G?t$`*^i8`p86`Y;$SaQ3R!QQ}EABajbb@6J;`V2?uuIe3-eJ5NCKgdzTMsTH0 zPZa+FXp6sG&8mu-AiG)0L~#4jb7$9oN5D`p{GA*K^CE z##ecB6+O+i@VN8Sq!KST$~Q?0{lndY)+a$$Uq!nc6-O*6fJq&LX%#_cpENfs0tjVJ zvz8&|75d0f@%xA$l}mH_!S^gtS=q4VMIZXL*zOcHu6#T_HFe8WbFG0N1Rbk4H5UQf zo!-QLmh$}Tde&-NAD(kPQT_(Anixl@dF$I5Aua2(OM4zCX%S5fgjFcdFW)4Wo&BD& z_NoKw^2xY{6j>;?QGaNC*F?N^;`vsFyfC=pgR~0BM9&OlPU_Ggmr8upGLn`{X(=AK z;D|e1GjvTFiAVi$XO|F|swrjupd9mCUpfxCiQGn)iJw`Mmf*0}QoJV=N2P}H5Tpjz zG=1deKJe-h^5sNBu83|nygCaQjto1n zf>^??*`T`knGAk(=FJ!+u0a{L%}r{TFt7XwzGt~ls@Iu=Aq+-kbS2c7pXS0ga61dh zsg@uQlQLi7c!x*pJc>wc2{=TX2sgTcvGB<$Fw*fp7ma@>a zX=XiG>}Pc_|Ff9$2xc=^&7pNC#C)Kpbi+qiqS1;;uHq3IWcehc8 z{-X85J*!A#rqP0;9QKu%YW9sG&E#o^{M3A(k%L0)EP}h#&>qQTXOG2&nr?i}JQ2$; zK9oY#zImpD^C0j5^w+At`D;zUj_>gDVUsd{dlYA?!|)0uI{s(Fhni9qQF0uhFv-5a zgw#5R_g5Xa&r$Pc(bL8=y+#uzSsRzg$Xr}j=j_QTslPh#oP?ba8R?(@th4?o+MjPc z4-Ui3h>SN=4=jP3cnb6tXG?D>R$Oh$vzgA$Yqly~# zVo+uH+fNxzOv-ZaYnm!)9@zb zxGxY1WYuwF?88tpr(IGxs#{jg$*XGI5ir zlewj)3cVpCzO2E#(b=}BDNpg7;0Jw;7d z|A|TVs2?DDr-NTli5_Dd&bobZ{~a<9QL1wpGS;Yx`=Nr6bbvZ2IqKng)H1r=F^%KXmr+(Dxn8oLZ_$z7mG}Xuj5Iq?Jz~0=-^Q${Ky@Sc7fUuj3 zL1A)zq^zwOqd^kpWWAVGG`-==3O{ZW~D zZiIW1+{WVrX!tpM;tu&T`R=2qB^(1J@CI@dalvs*6*aXOU3Dofo1(+HrCSqurs2aB8;$q%F-i{~(qPbhck0f+-c)vb*1F8IV14I8j*_RD z>uQdc?WrE2xpHQ(@cJ~n*BLN(v2O}=VLauglJp$^s%yDk{^Q{vRRwVz{gMJcW<#{B zbwGOSkWU4Ff_?_(jD~q_qp)cL@$mp!gh7WeXv6lx*#YUOVP?Acc86~iLxv}g)72F6EmdZ;wv44iVBx^sBGg_efU-ScSIkA@CZv$k>^gTxcZ)rI zl!Sa@Zlt}#zzxN19tSw<;c4?U|Ghd&zL=ypT3`Jx2-3H+V}FVnDWX?XsFPzv;f48U zbhVQwSZG^qcuJJ5a4D%}RV(1$eIWm_YTHUiE26;+8YELDAHLMRF!c*&iKdU;qX|hA z*urN2`T29MgOYQGfEo+<+E+Mn0Wp0cm)$A24HwS%c9kg}kqev*0+zKEiUydHSY z2cCLXWP!A6VByMQlEu$3RGPw219IBeh-B`eCgb7aiFO7K0ono7k3&8mJD}+%ky1LN zTaDo`zo!qTkbB74bi*XOSFK%Y`Q*W?Bp?HmANO#jGUVT8@t6w7l{SDigUP(8Jh-Qa zi*oWeCl2*%lCL#i33NLwz}~leDlAxS9-`P+MtjP@ zg_ApIAGs;m=Y`~ZXa2HzqJHh@Z2MwuCFt`Y7WabrY8NjrF%nYr^0Al0sX%*o z+HTb8>nINYI88*U4#t?=YJfEkg6lqd(?4NcVOKy}AV%x4^yfXQ_|bG1A(eJnjmxfZZT;_gB` zp)^rBeYc?I1dljZi6R5s;_c%iwT;)Bde(evN@uSs%>^pYv!7NCSA%|brdt1QPlT15 zhTKpKibnc}XQ3d}uc(fGyUG*hS&uwE#I4Q8dRf29?D?(+t%%nLui&N>#v(B;C%GMl zDssIafsEs=WegT>-1isUK1RuF@Z$dOpOvH$SK%*Fm&v}AKZ*9 zG7_0%>_8FQd^>4sa5^SR2W1PH^?kK&i<~_gEa}ZJw1GWu4%M_}M@wEy+HV#a|SY9u|$@5&dUEoj~MorNmwuh^@nm91xS3t z8d95QPBXP__S<+66AS>cg!AzKxq|SO5JR9dG8G0?SB=gPY@VNrE zPKHM#MX`pbZ{Vovki_%9?>%wE2a>C8QKzxI6GstiAs3;n+-L|f5jrMQ4lAUKsa;+a zG>FV`Ju;`=R);y+kH~W^EaHB65b7{%gLI*uPY4%aR+xR+o@BLL=^-PNzJ=;yYatDC zA6%&o&l4a^4%YLUdu*dieq!r_%gHP-N2uTO_^!hbywrxX4UqP*xj(L~#_RYQ$^%bW z^fRG*0RVh!DRB{%Fq=?=+g8fjnuNeG06x+4E#tFB%Z2JwkHmIXQGHR)yNt7swevtc zZ-`ORjPLXn_xFU@p!JA;bB?Ia%dj$kUO5z+KU1zAN=P2V zNGgZd)NkCXmy>h)EzgIWx48N=7xd@^rs4N3UWO|yYosyrk_rtejXYCc(=bcjV(WO3 z-4>w zsK##Y7_SG0t`sCyT>p6?t1gSmQ6Bg&z|UhF{9UBLx>uw9nRvJjckynPNW&uCJoo~| zeL`o;So?}TJVqfpZqf1>VR)HDyquXlxt)2LS{@eqni%z8I2uZ25aWHee_Km9Pa3gZ zOGTzN!NRr7AgNtmKM%h1p?Y#*pm=W`j zlSnYef0LyO{{c*; zL1(7#Jkc9T=3ienFg{5Z?oU0?Z!^_Q(qlGE;D7wz_+WjgH6)0QlTzQFQkzT>n}&}@ zGl&0I(YAPCupgWxLj%v-uUg3_-p}Zp04Dz3m1YcHFW9MT*Bp6dXE{>2>?5janI4&T z{=nE0wuEwwGBb}4JMoD#Ae*eU%gPPA-*;dVN;n4|ZrWYthywG(=_s3hJb3p!>>!q^ z)t#)OIkEkdOj-aAjmiAE{Y3TZk(9~+tFNpxsPv*396S`q%W%_d`s1scAzO}$APHEd zU4^-_EWhn-2L7A6zD#3PF;2*9g-Xb7M9plE&Rm0fI;3E(ig+#pJ_mEn!hYD!U#4hFX z{z^p2g+{jeC=!btNJVo8Dx~T4qjmH0l7UbV!mK|j;o`Fz<^N#1%kk()<_)hr8-S36 zRfT@NN(7zBWb1#5_r&;tqqaaiswob#VNtW#88odyis&Mq@D#`}B&@D!S2U6V##Pn)u9B`A9J!=o?GxVt|7Nehm0e~Lg`D&I)$NHwyxa0 zdPw1){)gN^;KP*c);pCNDw(l;%J<5%1am$;c9k~pS8N5)%Rg}=zqf{$B9*F9MYKC2 zKazl2XCOUF!iP5YH!vwWkBIx*(|55q#Iub<&o-EkX9VhsM`-yJS z|MVR;KKzMvj7J|m#^}kM^#;swGzD4MOm=7!Hv`d_9p^U6riA4Hj>3!2=)e~vHhY+Z zX*akkZit?dLSbreDi_C!7a*q(eRG9&Di2K7S%zTI)q+~c6 z2VWD)jtdVPtNY;U9L-g&3zz*MB?lGIJcLs50(YK~X(h1#T|sbU1~)me9M6mt;9$Ar zw}#kcVxS&5#p&UvKa!QfpV8D8)+jX_2d?{NTg4GF#JpdZz_W<{SxGp=h-4yLNj5Q% zaGO9>Vy-9)NnwsO1&>0D=;F1L6*u)BmP#DF9#u2ld;17{A0{a`bj8nKXg|y=^fE=? z76DgJIlo;-Z=c9ORaM`2_>27J1#()GS(CX)F(MP1#5C_igd(K$VT_v_F*SH}u@7I} z{2KrCTB+~UvGByxBSPr}5|i70sN`ckUG0;B|C`=9j=TMfYiEa~$xJ0CF+|mhg(npR zAtl3~;lgzrv#~65NFV^|!3UwNbwBx6o4Z&`n&n&KZQNQ9HD)&^?O=@%`Nb5PBt$ru z#F=eeE8oML0i%o1oRALZy&*aX&FRq4OpQE;ce=(vWh8$nKom-fEIUR@4by0z2z2L& zAq81*5Hm0wV4Qs+5O)u^O+WyS6wJGn(%qtW{SiKZJk!>D9sHi%ewtge$krtB6kOW( ztZbl{l|VwXA{v1#s}$b7LCpal6I?4g$=;H?==5qTx?y~@f?NBNJ41a{aDR2j=BKft zoJeDwPh*H&bMOLM$A6v0P>YKzhn%XLN!)bGh0gkpT>?W5&`bEa8=&)=Snnfr-=;X7Q0NakQ^Ew z7cn9QL-J}NCl@#uJBzwhWrc`<*JuT}8J=L=nn1#T>UmS6cvDloS#$cU*!)?#b6pLQ zW#(B|lIE)Qv26UK}O8Z7Ske@jyNE8SHSy}x%mvM-VkSNQS;?OAlloe;_>OehG6Br(yirh44*o# zReZ|RrDp$#m(0E4io-XufP{t6-+|>bdBhMMhkoB&m?p1Ug`E4#U@CeI1T9M?TCVy6 zLId~RB@sb!k+61DXgJx8U=aCM+(;$cA@%U1988l zNa(l-=LM z>kL>&jd4=1UXhB-WDfhY!?j<}w-AOtt8>JPadwb-!SoyzZt%pBk%2@)p+vXf(hdVA zca?ijLH|~+FIhIsyl5%yxS?(G8H$OVlK-Ig6}gT@i8j0|(bXdm>H633Y*EdA33#fp zPl24pcOR7fnj~0kzP}6YkimCx68P zw~%_ii1_G~p4sm&28onkfnombnDQ?o_mDT)_ZLAfal?7@Z4$}{{uGh^2B201MB zKrX2zaba_1Hq7Gjq*u6~8P^6|Z4%x>h6g~(Ov@~Ho<|%}72aeRkGdw)2Wbygu%y&% z6=l`syF?EJ_fph~puHe9@QBejt%UJy*zB86dq21vT=)@MA_`on>h9AQ1!yNFon-yP zbXyvDb1}qw7zC}>?ux;bbg=JGC?m%fi1yvwS=F#e6?Gz_suJ^!Dq)Jr>G$aC;YIt8 z*7=4U8c(uRbBe6X99mo?bl826l%e*`@I(Ftvj2Rc)$+8BKLS$%-S}5faeoj6>$4bH zVfTmLR_X|{!;kqD{|(%`*hW-tlb$k*GKS~ewryMA@^6-2dLnnn)eN(pk@r z5_sk*me?QX-xGFUmA~Y(I-!P>@*1z1Xs^yCq9ek-5XgY7AA5Tcqex|Qd=qQDdNIgi zxXz|S@l|n?I94H^&W1K*35dapWMR^%csVNTKnQ6XeDwRUn)zAZk@@SG!zJpsUL>>0 z&b}1Ef#OunnMi%QdlWb$Ph2gMhm9`o@kbmFYMf9koD^KPxtqw}5EQ4R>+qqh8HDI2 z3@U@n>x!U1MKvQg&w>y}N3oGy2(NYxgx|-n4(YvDF^#nSqL@&c|9qa}G-x4Q*+%f+ z4b7y6%dR1fKsY9|XYmkBvVr{InNUamcnS(a`Jcxm$ozm-;ebx8#GnPv6%wG5Lkd1` zvL*g^69MxOY>%D3l-y#UacIq$T@W4`r=MApRg$b!>?*IVKTsK-)iXr`xO9JW-X4|Z zjOpUtUP31Wmh*#G{7T|#S7mP^DdvXwsnaa`O8XY{`WNB?Bxw$gFTOp;&Ug5Z8FqIj zw(vlf26@ag<4(kp7!C^;E(87zL769LBxV2-Q_keZe`T6V0uB&mKlnji3uB8}DGKrR z3kqYugr+Dbzd4XX5y9&!CDs9tWG?pW$DJFdck4xE_ErPI!|}r#&e1c)@XSxO_`W9Qbu#W0(aO;~4Ve~&IvF-uMmI}KKK-tcq21-X%hY4`Iz%|=dv{-f% zhRqhF{sn8YXoNGG&`G4r(=TactY)mpKpKb?#(dqf!o_Y^V86^@{nU#{BhEX z=9M5BY9bA1W`q0!1Je6n1!PY&bFPeh2PHZ?A4CyKkU8Ee0 zc={>ab)?BV$YPhW%a)cFfKL{B6XSS6OeT|k{W_{%g^T48ylWmPUJOu zl|+veUwv+99?#XDE!CA1m(WiKC>g)j&<;GvpTTW0@#e?rv%$Uh3+lUViF@2Rd(3~_p#ST;Jy$NOx4Yf?a-(MoC@*XD(4 zp+<{+GYe6G=`9oX=K|;BHQh;nosK~~&eS-ymJ|f45Tq$XIUJ-H0 z+fVTcdw#ri+%DLq2H+v16gY4Yxp@>^0&G_vOGfn#5s&YyI#f8@$-5k;3m?&B{z zk8Pj58rGnq+4W&EVHo0(CI2K%=Ncs0b$Co;+1Kx9qT3CAunwV*1xFk9?68o9vc^xg z7e@Y?68=t~Oi;qXjBr9R?TP~=;Wn|zVmLd(PA_BeuU~U?;Bz_O-aLk?CpTEInZ}jz z@)Zc79nxfDk9Q6?q7v@CR$xTW!AFK6pmNmd6%BSBkM~@Ts5x}0x~n&+u4Tv~aOyYe zAO10{67T%CY%K!$ryE%i?dlp%YUaOGqz2EGZJrH$=d+T-{X+r&O_)wohr-dHuhD(E za&%d*f0RxpJ3xB^f0_pP8))_%uVvflB>QKL-{iL(M{UNBSjbdWvB&1B{sxA!LdC+^ zK(S8*hyBdm9F4`4IrC;XAQ)#+5Dd22$Vyh`F^&xJ&7O{jABhU|0}e|wE4A>MAAMce zGp?QSwBe?kL(5Jkoei1?{O0*a{YQg!xz>H0Za^6a*M@4rE6)Mq?eJI1X=O)Kua#{JU!>d1cxuzs@JupKDX>v==fstH zH;n(fMXg#Q-;D(D6zTk5>~rOM=Y80G-wt}>`E}EatCN*3tMoZSY8CZOWuJZMbFRb2 zX%2BaM`pOchXqO9!0IH#>{3ggl;1(mfFtx{WwrH5g<6Hox`7(L6+QJrCwoLBjQS#= zZUp~vju9GaEd)6L9C?3tFQb3_+lQhq2Fj{-6c;}AXBcSsJJ7_b2*Odywx|kl;ZC?o z>Ej0X_U3owQ4jZM7JSad{(ai{594)fYu`jGS?CKX-An*n48^qIXv>$kjggGvk=z)_?d3}+Pch^z#&hx~R!nZVFbDC5L|KTH6wRA-m zQlu`jJ3>frwpUWhV0g;{eav)0NhhGgK4W+-0Q*8%X&suySkpdnQ$!Qel$AZz**2&C zHTp|084uZ#bl|R;`SW-vUKR1Le5;P$H9!!+$uV(!AF{^MJzrk3WBS*|-s(fg?&!)@ zfqoB@bP#Uv`uA$xr^z6Lvb-Iy{^n}bCkNjEZ;cq z#Qk2drO29U&oYn3Rqa2=dK2_p%zw`lerBBH4UT zyC8{v-pG1de?+<7+^Ns+_!wqT{!8$&zns}=#hW;-(F+gae@fVe;~F-%Np+b6=8Gt| z1k}$EZ$FA$rh_IYw<^y1-(J;xBy;jwwA7!a={Oj4VOn9u+^{FEjk-PP9^SM}WW}~^ zTwyb$k{)5rAabP2ny_JBVA^B`Bk1|0d!0?VvA* z1r*y5LU&s}k8q@Dn?b9@QXV6d_D1q-{iSmAT?-tOuIp63abOOT3p)VVc@ACIGuG_) z?u{-;>C+WW(FL~RDdX)<&G%3b{;)KD4{F0m<{?em3mpt=!0~2XCCE6V&bSjb zQ~9 zeP||sXO4yFD|GXA7B5xhZt8U!jwjUTJviQS#GYh2u<_|j5mf}nIzwU0H@!J-&Ek59 zML?X=L2k}ahA|24^2|`2c|)(cHhz_klgWdD_ybxOi3t#0CfDYTr0bMQJWYjw_`)yt zfy4{e$KH?1?CJ|Po0U}X%+SuNJKsd+`SemnAz?Q%+`H)URE$5Kk*@%KQLon relatify" + }, + "icons.defaults" + ], + "calculatedTags": [ + "_numeric_id=feat.properties.id.split('/')[1]" ] -} +} \ No newline at end of file From f3d36efd412dc37dcf829cb0869fc061485fd0a0 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 3 Oct 2023 20:36:31 +0200 Subject: [PATCH 19/41] Add options, website, missing badge (#1634 #1622) --- assets/layers/vending_machine/fruits.svg | 1034 +++++++++++++++++ .../layers/vending_machine/fruits.svg.license | 2 + .../layers/vending_machine/license_info.json | 22 + assets/layers/vending_machine/strawberry.svg | 524 +++++++++ .../vending_machine/strawberry.svg.license | 2 + .../vending_machine/vending_machine.json | 70 +- langs/layers/ca.json | 40 +- langs/layers/de.json | 13 +- langs/layers/en.json | 47 +- langs/layers/fr.json | 7 +- langs/layers/nl.json | 45 +- 11 files changed, 1755 insertions(+), 51 deletions(-) create mode 100644 assets/layers/vending_machine/fruits.svg create mode 100644 assets/layers/vending_machine/fruits.svg.license create mode 100644 assets/layers/vending_machine/strawberry.svg create mode 100644 assets/layers/vending_machine/strawberry.svg.license diff --git a/assets/layers/vending_machine/fruits.svg b/assets/layers/vending_machine/fruits.svg new file mode 100644 index 000000000..48478b6c4 --- /dev/null +++ b/assets/layers/vending_machine/fruits.svg @@ -0,0 +1,1034 @@ + + + + fruits - coloured + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + fruits - coloured + + 2016-03-28 + + + Frank Tremmel + + + + + früchte + apfel + banane + pfirsich + birne + fruits + apple + banana + peach + pear + fruit + fucht + line art + education + school + unterricht + schule + + + coloured line art fruits + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +