From 4852888b41b9c532a08fbf963d38cc810bc53787 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 27 Sep 2023 22:21:35 +0200 Subject: [PATCH] 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
+ + + + - - - - - - - +