diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index f979a0751..571aa45d8 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -1,26 +1,28 @@ import * as known_themes from "../assets/generated/known_layers_and_themes.json" -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import BaseUIElement from "../UI/BaseUIElement"; -import Combine from "../UI/Base/Combine"; -import Title from "../UI/Base/Title"; -import List from "../UI/Base/List"; -import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator"; -import Constants from "../Models/Constants"; -import {Utils} from "../Utils"; -import Link from "../UI/Base/Link"; -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import BaseUIElement from "../UI/BaseUIElement" +import Combine from "../UI/Base/Combine" +import Title from "../UI/Base/Title" +import List from "../UI/Base/List" +import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator" +import Constants from "../Models/Constants" +import { Utils } from "../Utils" +import Link from "../UI/Base/Link" +import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" export class AllKnownLayouts { - public static allKnownLayouts: Map = AllKnownLayouts.AllLayouts(); - public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(AllKnownLayouts.allKnownLayouts); + public static allKnownLayouts: Map = AllKnownLayouts.AllLayouts() + public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList( + AllKnownLayouts.allKnownLayouts + ) // Must be below the list... - private static sharedLayers: Map = AllKnownLayouts.getSharedLayers(); + private static sharedLayers: Map = AllKnownLayouts.getSharedLayers() public static AllPublicLayers(options?: { - includeInlineLayers:true | boolean - }) : LayerConfig[] { + includeInlineLayers: true | boolean + }): LayerConfig[] { const allLayers: LayerConfig[] = [] const seendIds = new Set() AllKnownLayouts.sharedLayers.forEach((layer, key) => { @@ -28,7 +30,7 @@ export class AllKnownLayouts { allLayers.push(layer) }) if (options?.includeInlineLayers ?? true) { - const publicLayouts = AllKnownLayouts.layoutsList.filter(l => !l.hideFromOverview) + const publicLayouts = AllKnownLayouts.layoutsList.filter((l) => !l.hideFromOverview) for (const layout of publicLayouts) { if (layout.hideFromOverview) { continue @@ -40,7 +42,6 @@ export class AllKnownLayouts { seendIds.add(layer.id) allLayers.push(layer) } - } } @@ -52,11 +53,14 @@ export class AllKnownLayouts { */ public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] { const themes = AllKnownLayouts.layoutsList - .filter(l => !(publicOnly && l.hideFromOverview) && l.id !== "personal") - .map(theme => ({theme, minzoom: theme.layers.find(layer => layer.id === id)?.minzoom})) - .filter(obj => obj.minzoom !== undefined) + .filter((l) => !(publicOnly && l.hideFromOverview) && l.id !== "personal") + .map((theme) => ({ + theme, + minzoom: theme.layers.find((layer) => layer.id === id)?.minzoom, + })) + .filter((obj) => obj.minzoom !== undefined) themes.sort((th0, th1) => th1.minzoom - th0.minzoom) - return themes.map(th => th.theme); + return themes.map((th) => th.theme) } /** @@ -65,12 +69,15 @@ export class AllKnownLayouts { * @param callback * @constructor */ - public static GenOverviewsForSingleLayer(callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void): void { - const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) - .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) + public static GenOverviewsForSingleLayer( + callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void + ): void { + const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter( + (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0 + ) const builtinLayerIds: Set = new Set() - allLayers.forEach(l => builtinLayerIds.add(l.id)) - const inlineLayers = new Map(); + allLayers.forEach((l) => builtinLayerIds.add(l.id)) + const inlineLayers = new Map() for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) { if (layout.hideFromOverview) { @@ -78,7 +85,6 @@ export class AllKnownLayouts { } for (const layer of layout.layers) { - if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { continue } @@ -113,7 +119,6 @@ export class AllKnownLayouts { } } - // Determine the cross-dependencies const layerIsNeededBy: Map = new Map() @@ -125,12 +130,14 @@ export class AllKnownLayouts { } layerIsNeededBy.get(dependency).push(layer.id) } - - } allLayers.forEach((layer) => { - const element = layer.GenerateDocumentation(themesPerLayer.get(layer.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(layer)) + const element = layer.GenerateDocumentation( + themesPerLayer.get(layer.id), + layerIsNeededBy, + DependencyCalculator.getLayerDependencies(layer) + ) callback(layer, element, inlineLayers.get(layer.id)) }) } @@ -146,11 +153,12 @@ export class AllKnownLayouts { } } - const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()) - .filter(layer => Constants.priviliged_layers.indexOf(layer.id) < 0) + const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter( + (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0 + ) const builtinLayerIds: Set = new Set() - allLayers.forEach(l => builtinLayerIds.add(l.id)) + allLayers.forEach((l) => builtinLayerIds.add(l.id)) const themesPerLayer = new Map() @@ -166,7 +174,6 @@ export class AllKnownLayouts { } } - // Determine the cross-dependencies const layerIsNeededBy: Map = new Map() @@ -178,25 +185,32 @@ export class AllKnownLayouts { } layerIsNeededBy.get(dependency).push(layer.id) } - - } - return new Combine([ new Title("Special and other useful layers", 1), "MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.", new Title("Priviliged layers", 1), - new List(Constants.priviliged_layers.map(id => "[" + id + "](#" + id + ")")), + new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")), ...Constants.priviliged_layers - .map(id => AllKnownLayouts.sharedLayers.get(id)) - .map((l) => l.GenerateDocumentation(themesPerLayer.get(l.id), layerIsNeededBy, DependencyCalculator.getLayerDependencies(l), Constants.added_by_default.indexOf(l.id) >= 0, Constants.no_include.indexOf(l.id) < 0)), + .map((id) => AllKnownLayouts.sharedLayers.get(id)) + .map((l) => + l.GenerateDocumentation( + themesPerLayer.get(l.id), + layerIsNeededBy, + DependencyCalculator.getLayerDependencies(l), + Constants.added_by_default.indexOf(l.id) >= 0, + Constants.no_include.indexOf(l.id) < 0 + ) + ), new Title("Normal layers", 1), "The following layers are included in MapComplete:", - new List(Array.from(AllKnownLayouts.sharedLayers.keys()).map(id => new Link(id, "./Layers/" + id + ".md"))) + new List( + Array.from(AllKnownLayouts.sharedLayers.keys()).map( + (id) => new Link(id, "./Layers/" + id + ".md") + ) + ), ]) - - } public static GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement { @@ -204,37 +218,42 @@ export class AllKnownLayouts { new Title(new Combine([theme.title, "(", theme.id + ")"]), 2), theme.description, "This theme contains the following layers:", - new List(theme.layers.map(l => l.id)), + new List(theme.layers.map((l) => l.id)), "Available languages:", - new List(theme.language) + new List(theme.language), ]) } public static getSharedLayers(): Map { - const sharedLayers = new Map(); + const sharedLayers = new Map() for (const layer of known_themes["layers"]) { try { // @ts-ignore const parsed = new LayerConfig(layer, "shared_layers") - sharedLayers.set(layer.id, parsed); + sharedLayers.set(layer.id, parsed) } catch (e) { if (!Utils.runningFromConsole) { - console.error("CRITICAL: Could not parse a layer configuration!", layer.id, " due to", e) + console.error( + "CRITICAL: Could not parse a layer configuration!", + layer.id, + " due to", + e + ) } } } - return sharedLayers; + return sharedLayers } public static getSharedLayersConfigs(): Map { - const sharedLayers = new Map(); + const sharedLayers = new Map() for (const layer of known_themes["layers"]) { - // @ts-ignore - sharedLayers.set(layer.id, layer); + // @ts-ignore + sharedLayers.set(layer.id, layer) } - return sharedLayers; + return sharedLayers } private static GenerateOrderedList(allKnownLayouts: Map): LayoutConfig[] { @@ -242,28 +261,26 @@ export class AllKnownLayouts { allKnownLayouts.forEach((layout) => { list.push(layout) }) - return list; + return list } private static AllLayouts(): Map { - const dict: Map = new Map(); + const dict: Map = new Map() for (const layoutConfigJson of known_themes["themes"]) { const layout = new LayoutConfig(layoutConfigJson, true) dict.set(layout.id, layout) for (let i = 0; i < layout.layers.length; i++) { - let layer = layout.layers[i]; - if (typeof (layer) === "string") { - layer = AllKnownLayouts.sharedLayers.get(layer); + let layer = layout.layers[i] + if (typeof layer === "string") { + layer = AllKnownLayouts.sharedLayers.get(layer) layout.layers[i] = layer if (layer === undefined) { console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys()) throw `Layer ${layer} was not found or defined - probably a type was made` } } - } } - return dict; + return dict } - } diff --git a/Customizations/SharedTagRenderings.ts b/Customizations/SharedTagRenderings.ts index 3f0288748..9938c7813 100644 --- a/Customizations/SharedTagRenderings.ts +++ b/Customizations/SharedTagRenderings.ts @@ -1,38 +1,49 @@ -import * as questions from "../assets/tagRenderings/questions.json"; -import * as icons from "../assets/tagRenderings/icons.json"; -import {Utils} from "../Utils"; -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; -import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import BaseUIElement from "../UI/BaseUIElement"; -import Combine from "../UI/Base/Combine"; -import Title from "../UI/Base/Title"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import List from "../UI/Base/List"; +import * as questions from "../assets/tagRenderings/questions.json" +import * as icons from "../assets/tagRenderings/icons.json" +import { Utils } from "../Utils" +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" +import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" +import BaseUIElement from "../UI/BaseUIElement" +import Combine from "../UI/Base/Combine" +import Title from "../UI/Base/Title" +import { FixedUiElement } from "../UI/Base/FixedUiElement" +import List from "../UI/Base/List" export default class SharedTagRenderings { - - public static SharedTagRendering: Map = SharedTagRenderings.generatedSharedFields(); - public static SharedTagRenderingJson: Map = SharedTagRenderings.generatedSharedFieldsJsons(); - public static SharedIcons: Map = SharedTagRenderings.generatedSharedFields(true); + public static SharedTagRendering: Map = + SharedTagRenderings.generatedSharedFields() + public static SharedTagRenderingJson: Map = + SharedTagRenderings.generatedSharedFieldsJsons() + public static SharedIcons: Map = + SharedTagRenderings.generatedSharedFields(true) private static generatedSharedFields(iconsOnly = false): Map { const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly) const d = new Map() for (const key of Array.from(configJsons.keys())) { try { - d.set(key, new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`)) + d.set( + key, + new TagRenderingConfig(configJsons.get(key), `SharedTagRenderings.${key}`) + ) } catch (e) { if (!Utils.runningFromConsole) { - console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e) - + console.error( + "BUG: could not parse", + key, + " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", + e + ) } } } return d } - private static generatedSharedFieldsJsons(iconsOnly = false): Map { - const dict = new Map(); + private static generatedSharedFieldsJsons( + iconsOnly = false + ): Map { + const dict = new Map() if (!iconsOnly) { for (const key in questions) { @@ -53,13 +64,16 @@ export default class SharedTagRenderings { if (key === "id") { return } - value.id = value.id ?? key; - if(value["builtin"] !== undefined){ - if(value["override"] == undefined){ - throw "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/"+key + value.id = value.id ?? key + if (value["builtin"] !== undefined) { + if (value["override"] == undefined) { + throw ( + "HUH? Why whould you want to reuse a builtin if one doesn't override? In questions.json/" + + key + ) } - if(typeof value["builtin"] !== "string"){ - return; + if (typeof value["builtin"] !== "string") { + return } // This is a really funny situation: we extend another tagRendering! const parent = Utils.Clone(dict.get(value["builtin"])) @@ -73,36 +87,31 @@ export default class SharedTagRenderings { } }) - - return dict; + return dict } - public static HelpText(): BaseUIElement { return new Combine([ new Combine([ + new Title("Builtin questions", 1), - new Title("Builtin questions",1), - - "The following items can be easily reused in your layers" + "The following items can be easily reused in your layers", ]).SetClass("flex flex-col"), - ... Array.from( SharedTagRenderings.SharedTagRendering.keys()).map(key => { + ...Array.from(SharedTagRenderings.SharedTagRendering.keys()).map((key) => { const tr = SharedTagRenderings.SharedTagRendering.get(key) let mappings: BaseUIElement = undefined - if(tr.mappings?.length > 0){ - mappings = new List(tr.mappings.map(m => m.then.textFor("en"))) + if (tr.mappings?.length > 0) { + mappings = new List(tr.mappings.map((m) => m.then.textFor("en"))) } return new Combine([ new Title(key), tr.render?.textFor("en"), - tr.question?.textFor("en") ?? new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), - mappings + tr.question?.textFor("en") ?? + new FixedUiElement("Read-only tagrendering").SetClass("font-bold"), + mappings, ]).SetClass("flex flex-col") - - }) - + }), ]).SetClass("flex flex-col") } - } diff --git a/Docs/Tools/GenerateSeries.ts b/Docs/Tools/GenerateSeries.ts index a85af367b..9d281b0b4 100644 --- a/Docs/Tools/GenerateSeries.ts +++ b/Docs/Tools/GenerateSeries.ts @@ -1,29 +1,27 @@ -import {existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync} from "fs"; -import ScriptUtils from "../../scripts/ScriptUtils"; -import {Utils} from "../../Utils"; +import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs" +import ScriptUtils from "../../scripts/ScriptUtils" +import { Utils } from "../../Utils" ScriptUtils.fixUtils() class StatsDownloader { + private readonly urlTemplate = + "https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100" - private readonly urlTemplate = "https://osmcha.org/api/v1/changesets/?date__gte={start_date}&date__lte={end_date}&page={page}&comment=%23mapcomplete&page_size=100" - - private readonly _targetDirectory: string; + private readonly _targetDirectory: string constructor(targetDirectory = ".") { - this._targetDirectory = targetDirectory; + this._targetDirectory = targetDirectory } public async DownloadStats(startYear = 2020, startMonth = 5, startDay = 1) { - - const today = new Date(); + const today = new Date() const currentYear = today.getFullYear() const currentMonth = today.getMonth() + 1 for (let year = startYear; year <= currentYear; year++) { for (let month = 1; month <= 12; month++) { - if (year === startYear && month < startMonth) { - continue; + continue } if (year === currentYear && month > currentMonth) { @@ -32,33 +30,40 @@ class StatsDownloader { const pathM = `${this._targetDirectory}/stats.${year}-${month}.json` if (existsSync(pathM)) { - continue; + continue } const features = [] let monthIsFinished = true const writtenFiles = [] for (let day = startDay; day <= 31; day++) { - if (year === currentYear && month === currentMonth && day === today.getDate()) { monthIsFinished = false - break; + break } { const date = new Date(year, month - 1, day) - if(date.getMonth() != month -1){ + if (date.getMonth() != month - 1) { // We did roll over continue } } - const path = `${this._targetDirectory}/stats.${year}-${month}-${(day < 10 ? "0" : "") + day}.day.json` + const path = `${this._targetDirectory}/stats.${year}-${month}-${ + (day < 10 ? "0" : "") + day + }.day.json` writtenFiles.push(path) if (existsSync(path)) { let features = JSON.parse(readFileSync(path, "UTF-8")) features = features?.features ?? features console.log(features) - features.push(...features.features ) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too - console.log("Loaded ", path, "from disk, got", features.length, "features now") + features.push(...features.features) // day-stats are generally a list already, but in some ad-hoc cases might be a geojson-collection too + console.log( + "Loaded ", + path, + "from disk, got", + features.length, + "features now" + ) continue } let dayFeatures: any[] = undefined @@ -66,15 +71,22 @@ class StatsDownloader { dayFeatures = await this.DownloadStatsForDay(year, month, day, path) } catch (e) { console.error(e) - console.error("Could not download " + year + "-" + month + "-" + day + "... Trying again") + console.error( + "Could not download " + + year + + "-" + + month + + "-" + + day + + "... Trying again" + ) dayFeatures = await this.DownloadStatsForDay(year, month, day, path) } writeFileSync(path, JSON.stringify(dayFeatures)) features.push(...dayFeatures) - } - if(monthIsFinished){ - writeFileSync(pathM, JSON.stringify({features})) + if (monthIsFinished) { + writeFileSync(pathM, JSON.stringify({ features })) for (const writtenFile of writtenFiles) { unlinkSync(writtenFile) } @@ -82,37 +94,49 @@ class StatsDownloader { } startDay = 1 } - } - public async DownloadStatsForDay(year: number, month: number, day: number, path: string): Promise { - - let page = 1; + public async DownloadStatsForDay( + year: number, + month: number, + day: number, + path: string + ): Promise { + let page = 1 let allFeatures = [] - let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1); - let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(endDay.getMonth() + 1)}-${Utils.TwoDigits(endDay.getDate())}` - let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)) + let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1) + let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits( + endDay.getMonth() + 1 + )}-${Utils.TwoDigits(endDay.getDate())}` + let url = this.urlTemplate + .replace( + "{start_date}", + year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day) + ) .replace("{end_date}", endDate) .replace("{page}", "" + page) - let headers = { - 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0', - 'Accept-Language': 'en-US,en;q=0.5', - 'Referer': 'https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D', - 'Content-Type': 'application/json', - 'Authorization': 'Token 6e422e2afedb79ef66573982012000281f03dc91', - 'DNT': '1', - 'Connection': 'keep-alive', - 'TE': 'Trailers', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache' + "User-Agent": + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0", + "Accept-Language": "en-US,en;q=0.5", + Referer: + "https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D", + "Content-Type": "application/json", + Authorization: "Token 6e422e2afedb79ef66573982012000281f03dc91", + DNT: "1", + Connection: "keep-alive", + TE: "Trailers", + Pragma: "no-cache", + "Cache-Control": "no-cache", } while (url) { - ScriptUtils.erasableLog(`Downloading stats for ${year}-${month}-${day}, page ${page} ${url}`) + ScriptUtils.erasableLog( + `Downloading stats for ${year}-${month}-${day}, page ${page} ${url}` + ) const result = await Utils.downloadJson(url, headers) - page++; + page++ allFeatures.push(...result.features) if (result.features === undefined) { console.log("ERROR", result) @@ -120,58 +144,59 @@ class StatsDownloader { } url = result.next } - console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80)) + console.log( + `Writing ${allFeatures.length} features to `, + path, + Utils.Times((_) => " ", 80) + ) allFeatures = Utils.NoNull(allFeatures) - allFeatures.forEach(f => { - f.properties = {...f.properties, ...f.properties.metadata} + allFeatures.forEach((f) => { + f.properties = { ...f.properties, ...f.properties.metadata } delete f.properties.metadata f.properties.id = f.id }) return allFeatures } - } - interface ChangeSetData { - "id": number, - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [number, number][][] - }, - "properties": { - "check_user": null, - "reasons": [], - "tags": [], - "features": [], - "user": string, - "uid": string, - "editor": string, - "comment": string, - "comments_count": number, - "source": string, - "imagery_used": string, - "date": string, - "reviewed_features": [], - "create": number, - "modify": number, - "delete": number, - "area": number, - "is_suspect": boolean, - "harmful": any, - "checked": boolean, - "check_date": any, - "metadata": { - "host": string, - "theme": string, - "imagery": string, - "language": string + id: number + type: "Feature" + geometry: { + type: "Polygon" + coordinates: [number, number][][] + } + properties: { + check_user: null + reasons: [] + tags: [] + features: [] + user: string + uid: string + editor: string + comment: string + comments_count: number + source: string + imagery_used: string + date: string + reviewed_features: [] + create: number + modify: number + delete: number + area: number + is_suspect: boolean + harmful: any + checked: boolean + check_date: any + metadata: { + host: string + theme: string + imagery: string + language: string } } } - async function main(): Promise { if (!existsSync("graphs")) { mkdirSync("graphs") @@ -181,43 +206,47 @@ async function main(): Promise { let year = 2020 let month = 5 let day = 1 - if(!isNaN(Number(process.argv[2]))){ + if (!isNaN(Number(process.argv[2]))) { year = Number(process.argv[2]) } - if(!isNaN(Number(process.argv[3]))){ + if (!isNaN(Number(process.argv[3]))) { month = Number(process.argv[3]) } - if(!isNaN(Number(process.argv[4]))){ + if (!isNaN(Number(process.argv[4]))) { day = Number(process.argv[4]) } - do { try { - await new StatsDownloader(targetDir).DownloadStats(year, month, day) break } catch (e) { console.log(e) } - } while (true) - const allPaths = readdirSync(targetDir) - .filter(p => p.startsWith("stats.") && p.endsWith(".json")); - let allFeatures: ChangeSetData[] = [].concat(...allPaths - .map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features)); - allFeatures = allFeatures.filter(f => f?.properties !== undefined && (f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete"))) + const allPaths = readdirSync(targetDir).filter( + (p) => p.startsWith("stats.") && p.endsWith(".json") + ) + let allFeatures: ChangeSetData[] = [].concat( + ...allPaths.map( + (path) => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features + ) + ) + allFeatures = allFeatures.filter( + (f) => + f?.properties !== undefined && + (f.properties.editor === null || + f.properties.editor.toLowerCase().startsWith("mapcomplete")) + ) - allFeatures = allFeatures.filter(f => f.properties.metadata?.theme !== "EMPTY CS") + allFeatures = allFeatures.filter((f) => f.properties.metadata?.theme !== "EMPTY CS") if (process.argv.indexOf("--no-graphs") >= 0) { return } - const allFiles = readdirSync("Docs/Tools/stats").filter(p => p.endsWith(".json")) + const allFiles = readdirSync("Docs/Tools/stats").filter((p) => p.endsWith(".json")) writeFileSync("Docs/Tools/stats/file-overview.json", JSON.stringify(allFiles)) - } -main().then(_ => console.log("All done!")) - +main().then((_) => console.log("All done!")) diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index db0d6ebe6..f6de0451a 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -1,15 +1,17 @@ -import BaseLayer from "../../Models/BaseLayer"; -import {ImmutableStore, Store, UIEventSource} from "../UIEventSource"; -import Loc from "../../Models/Loc"; +import BaseLayer from "../../Models/BaseLayer" +import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" +import Loc from "../../Models/Loc" export interface AvailableBaseLayersObj { - readonly osmCarto: BaseLayer; - layerOverview: BaseLayer[]; + readonly osmCarto: BaseLayer + layerOverview: BaseLayer[] AvailableLayersAt(location: Store): Store - SelectBestLayerAccordingTo(location: Store, preferedCategory: Store): Store; - + SelectBestLayerAccordingTo( + location: Store, + preferedCategory: Store + ): Store } /** @@ -17,20 +19,28 @@ export interface AvailableBaseLayersObj { * Changes the basemap */ export default class AvailableBaseLayers { - - - public static layerOverview: BaseLayer[]; - public static osmCarto: BaseLayer; + public static layerOverview: BaseLayer[] + public static osmCarto: BaseLayer private static implementation: AvailableBaseLayersObj static AvailableLayersAt(location: Store): Store { - return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore([]); + return ( + AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? + new ImmutableStore([]) + ) } - static SelectBestLayerAccordingTo(location: Store, preferedCategory: UIEventSource): Store { - return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore(undefined); - + static SelectBestLayerAccordingTo( + location: Store, + preferedCategory: UIEventSource + ): Store { + return ( + AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo( + location, + preferedCategory + ) ?? new ImmutableStore(undefined) + ) } public static implement(backend: AvailableBaseLayersObj) { @@ -38,5 +48,4 @@ export default class AvailableBaseLayers { AvailableBaseLayers.osmCarto = backend.osmCarto AvailableBaseLayers.implementation = backend } - -} \ No newline at end of file +} diff --git a/Logic/Actors/AvailableBaseLayersImplementation.ts b/Logic/Actors/AvailableBaseLayersImplementation.ts index 67e2bb2c3..5a107d551 100644 --- a/Logic/Actors/AvailableBaseLayersImplementation.ts +++ b/Logic/Actors/AvailableBaseLayersImplementation.ts @@ -1,66 +1,77 @@ -import BaseLayer from "../../Models/BaseLayer"; -import {Store, Stores} from "../UIEventSource"; -import Loc from "../../Models/Loc"; -import {GeoOperations} from "../GeoOperations"; -import * as editorlayerindex from "../../assets/editor-layer-index.json"; -import * as L from "leaflet"; -import {TileLayer} from "leaflet"; -import * as X from "leaflet-providers"; -import {Utils} from "../../Utils"; -import {AvailableBaseLayersObj} from "./AvailableBaseLayers"; -import {BBox} from "../BBox"; +import BaseLayer from "../../Models/BaseLayer" +import { Store, Stores } from "../UIEventSource" +import Loc from "../../Models/Loc" +import { GeoOperations } from "../GeoOperations" +import * as editorlayerindex from "../../assets/editor-layer-index.json" +import * as L from "leaflet" +import { TileLayer } from "leaflet" +import * as X from "leaflet-providers" +import { Utils } from "../../Utils" +import { AvailableBaseLayersObj } from "./AvailableBaseLayers" +import { BBox } from "../BBox" export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj { - - public readonly osmCarto: BaseLayer = - { - id: "osm", - name: "OpenStreetMap", - layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap", - "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", + public readonly osmCarto: BaseLayer = { + id: "osm", + name: "OpenStreetMap", + layer: () => + AvailableBaseLayersImplementation.CreateBackgroundLayer( + "osm", + "OpenStreetMap", + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "OpenStreetMap", + "https://openStreetMap.org/copyright", 19, - false, false), - feature: null, - max_zoom: 19, - min_zoom: 0, - isBest: true, // Of course, OpenStreetMap is the best map! - category: "osmbasedmap" - } + false, + false + ), + feature: null, + max_zoom: 19, + min_zoom: 0, + isBest: true, // Of course, OpenStreetMap is the best map! + category: "osmbasedmap", + } - public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex()); - public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null) - public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null) + public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat( + AvailableBaseLayersImplementation.LoadProviderIndex() + ) + public readonly globalLayers = this.layerOverview.filter( + (layer) => layer.feature?.geometry === undefined || layer.feature?.geometry === null + ) + public readonly localLayers = this.layerOverview.filter( + (layer) => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null + ) private static LoadRasterIndex(): BaseLayer[] { const layers: BaseLayer[] = [] // @ts-ignore - const features = editorlayerindex.features; + const features = editorlayerindex.features for (const i in features) { - const layer = features[i]; - const props = layer.properties; + const layer = features[i] + const props = layer.properties if (props.type === "bing") { // A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648 - continue; + continue } if (props.id === "MAPNIK") { // Already added by default - continue; + continue } if (props.overlay) { - continue; + continue } if (props.url.toLowerCase().indexOf("apikey") > 0) { - continue; + continue } if (props.max_zoom < 19) { // We want users to zoom to level 19 when adding a point // If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer - continue; + continue } if (props.name === undefined) { @@ -68,17 +79,17 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL continue } - - const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer( - props.id, - props.name, - props.url, - props.name, - props.license_url, - props.max_zoom, - props.type.toLowerCase() === "wms", - props.type.toLowerCase() === "wmts" - ) + const leafletLayer: () => TileLayer = () => + AvailableBaseLayersImplementation.CreateBackgroundLayer( + props.id, + props.name, + props.url, + props.name, + props.license_url, + props.max_zoom, + props.type.toLowerCase() === "wms", + props.type.toLowerCase() === "wmts" + ) // Note: if layer.geometry is null, there is global coverage for this layer layers.push({ @@ -89,34 +100,35 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL layer: leafletLayer, feature: layer.geometry !== null ? layer : null, isBest: props.best ?? false, - category: props.category - }); + category: props.category, + }) } - return layers; + return layers } private static LoadProviderIndex(): BaseLayer[] { // @ts-ignore - X; // Import X to make sure the namespace is not optimized away + X // Import X to make sure the namespace is not optimized away function l(id: string, name: string): BaseLayer { try { - const layer: any = L.tileLayer.provider(id, undefined); + const layer: any = L.tileLayer.provider(id, undefined) return { feature: null, id: id, name: name, - layer: () => L.tileLayer.provider(id, { - maxNativeZoom: layer.options?.maxZoom, - maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21) - }), + layer: () => + L.tileLayer.provider(id, { + maxNativeZoom: layer.options?.maxZoom, + maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21), + }), min_zoom: 1, max_zoom: layer.options.maxZoom, category: "osmbasedmap", - isBest: false + isBest: false, } } catch (e) { - console.error("Could not find provided layer", name, e); - return null; + console.error("Could not find provided layer", name, e) + return null } } @@ -129,38 +141,50 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"), l("CartoDB.Voyager", "Voyager (by CartoDB)"), l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"), - ]; - return Utils.NoNull(layers); - + ] + return Utils.NoNull(layers) } /** * Converts a layer from the editor-layer-index into a tilelayer usable by leaflet */ - private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string, - maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer { - - url = url.replace("{zoom}", "{z}") - .replace("&BBOX={bbox}", "") - .replace("&bbox={bbox}", ""); + private static CreateBackgroundLayer( + id: string, + name: string, + url: string, + attribution: string, + attributionUrl: string, + maxZoom: number, + isWms: boolean, + isWMTS?: boolean + ): TileLayer { + url = url.replace("{zoom}", "{z}").replace("&BBOX={bbox}", "").replace("&bbox={bbox}", "") const subdomainsMatch = url.match(/{switch:[^}]*}/) - let domains: string[] = []; + let domains: string[] = [] if (subdomainsMatch !== null) { - let domainsStr = subdomainsMatch[0].substr("{switch:".length); - domainsStr = domainsStr.substr(0, domainsStr.length - 1); - domains = domainsStr.split(","); + let domainsStr = subdomainsMatch[0].substr("{switch:".length) + domainsStr = domainsStr.substr(0, domainsStr.length - 1) + domains = domainsStr.split(",") url = url.replace(/{switch:[^}]*}/, "{s}") } - if (isWms) { - url = url.replace("&SRS={proj}", ""); - url = url.replace("&srs={proj}", ""); - const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"]; - const urlObj = new URL(url); + url = url.replace("&SRS={proj}", "") + url = url.replace("&srs={proj}", "") + const paramaters = [ + "format", + "layers", + "version", + "service", + "request", + "styles", + "transparent", + "version", + ] + const urlObj = new URL(url) - const isUpper = urlObj.searchParams["LAYERS"] !== null; + const isUpper = urlObj.searchParams["LAYERS"] !== null const options = { maxZoom: Math.max(maxZoom ?? 19, 21), maxNativeZoom: maxZoom ?? 19, @@ -168,116 +192,117 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL subdomains: domains, uppercase: isUpper, transparent: false, - }; + } for (const paramater of paramaters) { - let p = paramater; + let p = paramater if (isUpper) { - p = paramater.toUpperCase(); + p = paramater.toUpperCase() } - options[paramater] = urlObj.searchParams.get(p); + options[paramater] = urlObj.searchParams.get(p) } if (options.transparent === null) { - options.transparent = false; + options.transparent = false } - - return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options); + return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options) } if (attributionUrl) { - attribution = `${attribution}`; + attribution = `${attribution}` } - return L.tileLayer(url, - { - attribution: attribution, - maxZoom: Math.max(21, maxZoom ?? 19), - maxNativeZoom: maxZoom ?? 19, - minZoom: 1, - // @ts-ignore - wmts: isWMTS ?? false, - subdomains: domains - }); + return L.tileLayer(url, { + attribution: attribution, + maxZoom: Math.max(21, maxZoom ?? 19), + maxNativeZoom: maxZoom ?? 19, + minZoom: 1, + // @ts-ignore + wmts: isWMTS ?? false, + subdomains: domains, + }) } public AvailableLayersAt(location: Store): Store { - return Stores.ListStabilized(location.map( - (currentLocation) => { + return Stores.ListStabilized( + location.map((currentLocation) => { if (currentLocation === undefined) { - return this.layerOverview; + return this.layerOverview } - return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); - })); + return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat) + }) + ) } - public SelectBestLayerAccordingTo(location: Store, preferedCategory: Store): Store { - return this.AvailableLayersAt(location) - .map(available => { + public SelectBestLayerAccordingTo( + location: Store, + preferedCategory: Store + ): Store { + return this.AvailableLayersAt(location).map( + (available) => { // First float all 'best layers' to the top available.sort((a, b) => { - if (a.isBest && b.isBest) { - return 0; - } - if (!a.isBest) { - return 1 - } - - return -1; + if (a.isBest && b.isBest) { + return 0 } - ) + if (!a.isBest) { + return 1 + } + + return -1 + }) if (preferedCategory.data === undefined) { return available[0] } - let prefered: string [] + let prefered: string[] if (typeof preferedCategory.data === "string") { prefered = [preferedCategory.data] } else { - prefered = preferedCategory.data; + prefered = preferedCategory.data } - prefered.reverse(/*New list, inplace reverse is fine*/); + prefered.reverse(/*New list, inplace reverse is fine*/) for (const category of prefered) { //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top available.sort((a, b) => { - if (a.category === category && b.category === category) { - return 0; - } - if (a.category !== category) { - return 1 - } - - return -1; + if (a.category === category && b.category === category) { + return 0 } - ) + if (a.category !== category) { + return 1 + } + + return -1 + }) } return available[0] - }, [preferedCategory]) + }, + [preferedCategory] + ) } - private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { const availableLayers = [this.osmCarto] if (lon === undefined || lat === undefined) { - return availableLayers.concat(this.globalLayers); + return availableLayers.concat(this.globalLayers) } - const lonlat : [number, number] = [lon, lat]; + const lonlat: [number, number] = [lon, lat] for (const layerOverviewItem of this.localLayers) { - const layer = layerOverviewItem; + const layer = layerOverviewItem const bbox = BBox.get(layer.feature) - - if(!bbox.contains(lonlat)){ + + if (!bbox.contains(lonlat)) { continue } if (GeoOperations.inside(lonlat, layer.feature)) { - availableLayers.push(layer); + availableLayers.push(layer) } } - return availableLayers.concat(this.globalLayers); + return availableLayers.concat(this.globalLayers) } -} \ No newline at end of file +} diff --git a/Logic/Actors/BackgroundLayerResetter.ts b/Logic/Actors/BackgroundLayerResetter.ts index f8c73892e..ee559b4fb 100644 --- a/Logic/Actors/BackgroundLayerResetter.ts +++ b/Logic/Actors/BackgroundLayerResetter.ts @@ -1,50 +1,49 @@ -import {UIEventSource} from "../UIEventSource"; -import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "./AvailableBaseLayers"; -import Loc from "../../Models/Loc"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource" +import BaseLayer from "../../Models/BaseLayer" +import AvailableBaseLayers from "./AvailableBaseLayers" +import Loc from "../../Models/Loc" +import { Utils } from "../../Utils" /** * Sets the current background layer to a layer that is actually available */ export default class BackgroundLayerResetter { - - constructor(currentBackgroundLayer: UIEventSource, - location: UIEventSource, - availableLayers: UIEventSource, - defaultLayerId: string = undefined) { - + constructor( + currentBackgroundLayer: UIEventSource, + location: UIEventSource, + availableLayers: UIEventSource, + defaultLayerId: string = undefined + ) { if (Utils.runningFromConsole) { return } - defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id; + defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id // Change the baselayer back to OSM if we go out of the current range of the layer - availableLayers.addCallbackAndRun(availableLayers => { - let defaultLayer = undefined; - const currentLayer = currentBackgroundLayer.data.id; + availableLayers.addCallbackAndRun((availableLayers) => { + let defaultLayer = undefined + const currentLayer = currentBackgroundLayer.data.id for (const availableLayer of availableLayers) { if (availableLayer.id === currentLayer) { - if (availableLayer.max_zoom < location.data.zoom) { - break; + break } if (availableLayer.min_zoom > location.data.zoom) { - break; + break } if (availableLayer.id === defaultLayerId) { - defaultLayer = availableLayer; + defaultLayer = availableLayer } - return; // All good - the current layer still works! + return // All good - the current layer still works! } } // Oops, we panned out of range for this layer! - console.log("AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard") - currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto); - }); - + console.log( + "AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard" + ) + currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto) + }) } - -} \ No newline at end of file +} diff --git a/Logic/Actors/ChangeToElementsActor.ts b/Logic/Actors/ChangeToElementsActor.ts index 9ae5f87ef..1eac44bfd 100644 --- a/Logic/Actors/ChangeToElementsActor.ts +++ b/Logic/Actors/ChangeToElementsActor.ts @@ -1,36 +1,34 @@ -import {ElementStorage} from "../ElementStorage"; -import {Changes} from "../Osm/Changes"; +import { ElementStorage } from "../ElementStorage" +import { Changes } from "../Osm/Changes" export default class ChangeToElementsActor { constructor(changes: Changes, allElements: ElementStorage) { - changes.pendingChanges.addCallbackAndRun(changes => { + changes.pendingChanges.addCallbackAndRun((changes) => { for (const change of changes) { - const id = change.type + "/" + change.id; + const id = change.type + "/" + change.id if (!allElements.has(id)) { - continue; // Ignored as the geometryFixer will introduce this + continue // Ignored as the geometryFixer will introduce this } const src = allElements.getEventSourceById(id) - let changed = false; + let changed = false for (const kv of change.tags ?? []) { // Apply tag changes and ping the consumers const k = kv.k let v = kv.v if (v === "") { - v = undefined; + v = undefined } if (src.data[k] === v) { continue } - changed = true; - src.data[k] = v; + changed = true + src.data[k] = v } if (changed) { src.ping() } - - } }) } -} \ No newline at end of file +} diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index c283f2785..64437c831 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -1,60 +1,59 @@ -import {Store, UIEventSource} from "../UIEventSource"; -import Svg from "../../Svg"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; -import {VariableUiElement} from "../../UI/Base/VariableUIElement"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {QueryParameters} from "../Web/QueryParameters"; -import {BBox} from "../BBox"; -import Constants from "../../Models/Constants"; -import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; +import { Store, UIEventSource } from "../UIEventSource" +import Svg from "../../Svg" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { VariableUiElement } from "../../UI/Base/VariableUIElement" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { QueryParameters } from "../Web/QueryParameters" +import { BBox } from "../BBox" +import Constants from "../../Models/Constants" +import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" -export interface GeoLocationPointProperties { - id: "gps", - "user:location": "yes", - "date": string, - "latitude": number - "longitude": number, - "speed": number, - "accuracy": number - "heading": number - "altitude": number +export interface GeoLocationPointProperties { + id: "gps" + "user:location": "yes" + date: string + latitude: number + longitude: number + speed: number + accuracy: number + heading: number + altitude: number } export default class GeoLocationHandler extends VariableUiElement { - private readonly currentLocation?: SimpleFeatureSource /** * Wether or not the geolocation is active, aka the user requested the current location */ - private readonly _isActive: UIEventSource; + private readonly _isActive: UIEventSource /** * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user */ - private readonly _isLocked: UIEventSource; + private readonly _isLocked: UIEventSource /** * The callback over the permission API * @private */ - private readonly _permission: UIEventSource; + private readonly _permission: UIEventSource /** * Literally: _currentGPSLocation.data != undefined * @private */ - private readonly _hasLocation: Store; - private readonly _currentGPSLocation: UIEventSource; + private readonly _hasLocation: Store + private readonly _currentGPSLocation: UIEventSource /** * Kept in order to update the marker * @private */ - private readonly _leafletMap: UIEventSource; + private readonly _leafletMap: UIEventSource /** * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs */ - private _lastUserRequest: UIEventSource; + private _lastUserRequest: UIEventSource /** * A small flag on localstorage. If the user previously granted the geolocation, it will be set. @@ -64,54 +63,52 @@ export default class GeoLocationHandler extends VariableUiElement { * If the user denies the geolocation this time, we unset this flag * @private */ - private readonly _previousLocationGrant: UIEventSource; - private readonly _layoutToUse: LayoutConfig; + private readonly _previousLocationGrant: UIEventSource + private readonly _layoutToUse: LayoutConfig - constructor( - state: { - selectedElement: UIEventSource; - currentUserLocation?: SimpleFeatureSource, - leafletMap: UIEventSource, - layoutToUse: LayoutConfig, - featureSwitchGeolocation: UIEventSource - } - ) { - const currentGPSLocation = new UIEventSource(undefined, "GPS-coordinate") + constructor(state: { + selectedElement: UIEventSource + currentUserLocation?: SimpleFeatureSource + leafletMap: UIEventSource + layoutToUse: LayoutConfig + featureSwitchGeolocation: UIEventSource + }) { + const currentGPSLocation = new UIEventSource( + undefined, + "GPS-coordinate" + ) const leafletMap = state.leafletMap const initedAt = new Date() - let autozoomDone = false; - const hasLocation = currentGPSLocation.map( - (location) => location !== undefined - ); - const previousLocationGrant = LocalStorageSource.Get( - "geolocation-permissions" - ); - const isActive = new UIEventSource(false); - const isLocked = new UIEventSource(false); - const permission = new UIEventSource(""); - const lastClick = new UIEventSource(undefined); - const lastClickWithinThreeSecs = lastClick.map(lastClick => { + let autozoomDone = false + const hasLocation = currentGPSLocation.map((location) => location !== undefined) + const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions") + const isActive = new UIEventSource(false) + const isLocked = new UIEventSource(false) + const permission = new UIEventSource("") + const lastClick = new UIEventSource(undefined) + const lastClickWithinThreeSecs = lastClick.map((lastClick) => { if (lastClick === undefined) { - return false; + return false } const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 return timeDiff <= 3 }) - const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") - const willFocus = lastClick.map(lastUserRequest => { + const latLonGiven = + QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") + const willFocus = lastClick.map((lastUserRequest) => { const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000 if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) { return true } if (lastUserRequest === undefined) { - return false; + return false } const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000 return timeDiff <= Constants.zoomToLocationTimeout }) - lastClick.addCallbackAndRunD(_ => { + lastClick.addCallbackAndRunD((_) => { window.setTimeout(() => { if (lastClickWithinThreeSecs.data || willFocus.data) { lastClick.ping() @@ -123,7 +120,7 @@ export default class GeoLocationHandler extends VariableUiElement { hasLocation.map( (hasLocationData) => { if (permission.data === "denied") { - return Svg.location_refused_svg(); + return Svg.location_refused_svg() } if (!isActive.data) { @@ -134,7 +131,7 @@ export default class GeoLocationHandler extends VariableUiElement { // If will focus is active too, we indicate this differently const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg() icon.SetStyle("animation: spin 4s linear infinite;") - return icon; + return icon } if (isLocked.data) { return Svg.location_locked_svg() @@ -144,42 +141,41 @@ export default class GeoLocationHandler extends VariableUiElement { } // We have a location, so we show a dot in the center - return Svg.location_svg(); + return Svg.location_svg() }, [isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus] ) - ); + ) this.SetClass("mapcontrol") - this._isActive = isActive; - this._isLocked = isLocked; + this._isActive = isActive + this._isLocked = isLocked this._permission = permission - this._previousLocationGrant = previousLocationGrant; - this._currentGPSLocation = currentGPSLocation; - this._leafletMap = leafletMap; - this._layoutToUse = state.layoutToUse; - this._hasLocation = hasLocation; + this._previousLocationGrant = previousLocationGrant + this._currentGPSLocation = currentGPSLocation + this._leafletMap = leafletMap + this._layoutToUse = state.layoutToUse + this._hasLocation = hasLocation this._lastUserRequest = lastClick - const self = this; + const self = this const currentPointer = this._isActive.map( (isActive) => { if (isActive && !self._hasLocation.data) { - return "cursor-wait"; + return "cursor-wait" } - return "cursor-pointer"; + return "cursor-pointer" }, [this._hasLocation] - ); + ) currentPointer.addCallbackAndRun((pointerClass) => { self.RemoveClass("cursor-wait") self.RemoveClass("cursor-pointer") - self.SetClass(pointerClass); - }); - + self.SetClass(pointerClass) + }) this.onClick(() => { /* - * If the previous click was within 3 seconds (and we have an active location), then we lock to the location + * If the previous click was within 3 seconds (and we have an active location), then we lock to the location */ if (self._hasLocation.data) { if (isLocked.data) { @@ -197,14 +193,16 @@ export default class GeoLocationHandler extends VariableUiElement { } } - self.init(true, true); - }); + self.init(true, true) + }) + const doAutoZoomToLocation = + !latLonGiven && + state.featureSwitchGeolocation.data && + state.selectedElement.data !== undefined + this.init(false, doAutoZoomToLocation) - const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined - this.init(false, doAutoZoomToLocation); - - isLocked.addCallbackAndRunD(isLocked => { + isLocked.addCallbackAndRunD((isLocked) => { if (isLocked) { leafletMap.data?.dragging?.disable() } else { @@ -214,47 +212,45 @@ export default class GeoLocationHandler extends VariableUiElement { this.currentLocation = state.currentUserLocation this._currentGPSLocation.addCallback((location) => { - self._previousLocationGrant.setData("granted"); + self._previousLocationGrant.setData("granted") const feature = { - "type": "Feature", + type: "Feature", properties: { id: "gps", "user:location": "yes", - "date": new Date().toISOString(), - "latitude": location.latitude, - "longitude": location.longitude, - "speed": location.speed, - "accuracy": location.accuracy, - "heading": location.heading, - "altitude": location.altitude + date: new Date().toISOString(), + latitude: location.latitude, + longitude: location.longitude, + speed: location.speed, + accuracy: location.accuracy, + heading: location.heading, + altitude: location.altitude, }, geometry: { type: "Point", coordinates: [location.longitude, location.latitude], - } + }, } - self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) + self.currentLocation?.features?.setData([{ feature, freshness: new Date() }]) if (willFocus.data) { console.log("Zooming to user location: willFocus is set") - lastClick.setData(undefined); - autozoomDone = true; - self.MoveToCurrentLocation(16); + lastClick.setData(undefined) + autozoomDone = true + self.MoveToCurrentLocation(16) } else if (self._isLocked.data) { - self.MoveToCurrentLocation(); + self.MoveToCurrentLocation() } - - }); - + }) } private init(askPermission: boolean, zoomToLocation: boolean) { - const self = this; + const self = this if (self._isActive.data) { - self.MoveToCurrentLocation(16); - return; + self.MoveToCurrentLocation(16) + return } if (typeof navigator === "undefined") { @@ -262,27 +258,25 @@ export default class GeoLocationHandler extends VariableUiElement { } try { - navigator?.permissions - ?.query({name: "geolocation"}) - ?.then(function (status) { - console.log("Geolocation permission is ", status.state); - if (status.state === "granted") { - self.StartGeolocating(zoomToLocation); - } - self._permission.setData(status.state); - status.onchange = function () { - self._permission.setData(status.state); - }; - }); + navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) { + console.log("Geolocation permission is ", status.state) + if (status.state === "granted") { + self.StartGeolocating(zoomToLocation) + } + self._permission.setData(status.state) + status.onchange = function () { + self._permission.setData(status.state) + } + }) } catch (e) { - console.error(e); + console.error(e) } if (askPermission) { - self.StartGeolocating(zoomToLocation); + self.StartGeolocating(zoomToLocation) } else if (this._previousLocationGrant.data === "granted") { - this._previousLocationGrant.setData(""); - self.StartGeolocating(zoomToLocation); + this._previousLocationGrant.setData("") + self.StartGeolocating(zoomToLocation) } } @@ -311,7 +305,7 @@ export default class GeoLocationHandler extends VariableUiElement { * handler._currentGPSLocation.setData( {latitude : 60, longitude: 60) // out of bounds * handler.MoveToCurrentLocation() * resultingLocation // => [60, 60] - * + * * // should refuse to move if out of bounds * let resultingLocation = undefined * let resultingzoom = 1 @@ -322,7 +316,7 @@ export default class GeoLocationHandler extends VariableUiElement { * layoutToUse: new LayoutConfig({ * id: 'test', * title: {"en":"test"} - * "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]], + * "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]], * description: "A testing theme", * layers: [] * }), @@ -337,20 +331,20 @@ export default class GeoLocationHandler extends VariableUiElement { * resultingLocation // => [51.3, 4.1] */ private MoveToCurrentLocation(targetZoom?: number) { - const location = this._currentGPSLocation.data; - this._lastUserRequest.setData(undefined); + const location = this._currentGPSLocation.data + this._lastUserRequest.setData(undefined) if ( this._currentGPSLocation.data.latitude === 0 && this._currentGPSLocation.data.longitude === 0 ) { - console.debug("Not moving to GPS-location: it is null island"); - return; + console.debug("Not moving to GPS-location: it is null island") + return } // We check that the GPS location is not out of bounds - const b = this._layoutToUse.lockLocation; - let inRange = true; + const b = this._layoutToUse.lockLocation + let inRange = true if (b) { if (b !== true) { // B is an array with our locklocation @@ -358,41 +352,44 @@ export default class GeoLocationHandler extends VariableUiElement { } } if (!inRange) { - console.log("Not zooming to GPS location: out of bounds", b, location); + console.log("Not zooming to GPS location: out of bounds", b, location) } else { const currentZoom = this._leafletMap.data.getZoom() - this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom)); + this._leafletMap.data.setView( + [location.latitude, location.longitude], + Math.max(targetZoom ?? 0, currentZoom) + ) } } private StartGeolocating(zoomToGPS = true) { - const self = this; + const self = this this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0)) if (self._permission.data === "denied") { - self._previousLocationGrant.setData(""); + self._previousLocationGrant.setData("") self._isActive.setData(false) - return ""; + return "" } if (this._currentGPSLocation.data !== undefined) { - this.MoveToCurrentLocation(16); + this.MoveToCurrentLocation(16) } if (self._isActive.data) { - return; + return } - self._isActive.setData(true); + self._isActive.setData(true) navigator.geolocation.watchPosition( function (position) { - self._currentGPSLocation.setData(position.coords); + self._currentGPSLocation.setData(position.coords) }, function () { - console.warn("Could not get location with navigator.geolocation"); + console.warn("Could not get location with navigator.geolocation") }, { - enableHighAccuracy: true + enableHighAccuracy: true, } - ); + ) } } diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index d6db8be4a..f523ce2df 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -1,112 +1,124 @@ -import {Store, UIEventSource} from "../UIEventSource"; -import {Or} from "../Tags/Or"; -import {Overpass} from "../Osm/Overpass"; -import FeatureSource from "../FeatureSource/FeatureSource"; -import {Utils} from "../../Utils"; -import {TagsFilter} from "../Tags/TagsFilter"; -import SimpleMetaTagger from "../SimpleMetaTagger"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import RelationsTracker from "../Osm/RelationsTracker"; -import {BBox} from "../BBox"; -import Loc from "../../Models/Loc"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import Constants from "../../Models/Constants"; -import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator"; -import {Tiles} from "../../Models/TileRange"; - +import { Store, UIEventSource } from "../UIEventSource" +import { Or } from "../Tags/Or" +import { Overpass } from "../Osm/Overpass" +import FeatureSource from "../FeatureSource/FeatureSource" +import { Utils } from "../../Utils" +import { TagsFilter } from "../Tags/TagsFilter" +import SimpleMetaTagger from "../SimpleMetaTagger" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import RelationsTracker from "../Osm/RelationsTracker" +import { BBox } from "../BBox" +import Loc from "../../Models/Loc" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import Constants from "../../Models/Constants" +import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator" +import { Tiles } from "../../Models/TileRange" export default class OverpassFeatureSource implements FeatureSource { - public readonly name = "OverpassFeatureSource" /** * The last loaded features of the geojson */ - public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource(undefined); + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource(undefined) + public readonly runningQuery: UIEventSource = new UIEventSource(false) + public readonly timeout: UIEventSource = new UIEventSource(0) - public readonly runningQuery: UIEventSource = new UIEventSource(false); - public readonly timeout: UIEventSource = new UIEventSource(0); + public readonly relationsTracker: RelationsTracker - public readonly relationsTracker: RelationsTracker; - - - private readonly retries: UIEventSource = new UIEventSource(0); + private readonly retries: UIEventSource = new UIEventSource(0) private readonly state: { - readonly locationControl: Store, - readonly layoutToUse: LayoutConfig, - readonly overpassUrl: Store; - readonly overpassTimeout: Store; + readonly locationControl: Store + readonly layoutToUse: LayoutConfig + readonly overpassUrl: Store + readonly overpassTimeout: Store readonly currentBounds: Store } private readonly _isActive: Store /** * Callback to handle all the data */ - private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void; + private readonly onBboxLoaded: ( + bbox: BBox, + date: Date, + layers: LayerConfig[], + zoomlevel: number + ) => void /** * Keeps track of how fresh the data is * @private */ - private readonly freshnesses: Map; + private readonly freshnesses: Map constructor( state: { - readonly locationControl: Store, - readonly layoutToUse: LayoutConfig, - readonly overpassUrl: Store; - readonly overpassTimeout: Store; - readonly overpassMaxZoom: Store, + readonly locationControl: Store + readonly layoutToUse: LayoutConfig + readonly overpassUrl: Store + readonly overpassTimeout: Store + readonly overpassMaxZoom: Store readonly currentBounds: Store }, options: { - padToTiles: Store, - isActive?: Store, - relationTracker: RelationsTracker, - onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void, + padToTiles: Store + isActive?: Store + relationTracker: RelationsTracker + onBboxLoaded?: ( + bbox: BBox, + date: Date, + layers: LayerConfig[], + zoomlevel: number + ) => void freshnesses?: Map - }) { - + } + ) { this.state = state - this._isActive = options.isActive; + this._isActive = options.isActive this.onBboxLoaded = options.onBboxLoaded this.relationsTracker = options.relationTracker this.freshnesses = options.freshnesses - const self = this; - state.currentBounds.addCallback(_ => { + const self = this + state.currentBounds.addCallback((_) => { self.update(options.padToTiles.data) }) - } private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { - let filters: TagsFilter[] = []; - let extraScripts: string[] = []; + let filters: TagsFilter[] = [] + let extraScripts: string[] = [] for (const layer of layersToDownload) { if (layer.source.overpassScript !== undefined) { extraScripts.push(layer.source.overpassScript) } else { - filters.push(layer.source.osmTags); + filters.push(layer.source.osmTags) } } filters = Utils.NoNull(filters) extraScripts = Utils.NoNull(extraScripts) if (filters.length + extraScripts.length === 0) { - return undefined; + return undefined } - return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker); + return new Overpass( + new Or(filters), + extraScripts, + interpreterUrl, + this.state.overpassTimeout, + this.relationsTracker + ) } private update(paddedZoomLevel: number) { if (!this._isActive.data) { - return; + return } - const self = this; - this.updateAsync(paddedZoomLevel).then(bboxDate => { + const self = this + this.updateAsync(paddedZoomLevel).then((bboxDate) => { if (bboxDate === undefined || self.onBboxLoaded === undefined) { - return; + return } const [bbox, date, layers] = bboxDate self.onBboxLoaded(bbox, date, layers, paddedZoomLevel) @@ -115,56 +127,58 @@ export default class OverpassFeatureSource implements FeatureSource { private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> { if (this.runningQuery.data) { - console.log("Still running a query, not updating"); - return undefined; + console.log("Still running a query, not updating") + return undefined } if (this.timeout.data > 0) { console.log("Still in timeout - not updating") - return undefined; + return undefined } let data: any = undefined let date: Date = undefined - let lastUsed = 0; - + let lastUsed = 0 const layersToDownload = [] - const neededTiles = this.state.currentBounds.data.expandToTileBounds(padToZoomLevel).containingTileRange(padToZoomLevel) + const neededTiles = this.state.currentBounds.data + .expandToTileBounds(padToZoomLevel) + .containingTileRange(padToZoomLevel) for (const layer of this.state.layoutToUse.layers) { - - if (typeof (layer) === "string") { + if (typeof layer === "string") { throw "A layer was not expanded!" } if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { continue } if (this.state.locationControl.data.zoom < layer.minzoom) { - continue; + continue } if (layer.doNotDownload) { - continue; + continue } if (layer.source.geojsonSource !== undefined) { // Not our responsibility to download this layer! - continue; + continue } const freshness = this.freshnesses?.get(layer.id) if (freshness !== undefined) { - const oldestDataDate = Math.min(...Tiles.MapRange(neededTiles, (x, y) => { - const date = freshness.freshnessFor(padToZoomLevel, x, y); - if (date === undefined) { - return 0 - } - return date.getTime() - })) / 1000; + const oldestDataDate = + Math.min( + ...Tiles.MapRange(neededTiles, (x, y) => { + const date = freshness.freshnessFor(padToZoomLevel, x, y) + if (date === undefined) { + return 0 + } + return date.getTime() + }) + ) / 1000 const now = new Date().getTime() - const minRequiredAge = (now / 1000) - layer.maxAgeOfCache + const minRequiredAge = now / 1000 - layer.maxAgeOfCache if (oldestDataDate >= minRequiredAge) { // still fresh enough - not updating continue } - } layersToDownload.push(layer) @@ -172,34 +186,35 @@ export default class OverpassFeatureSource implements FeatureSource { if (layersToDownload.length == 0) { console.debug("Not updating - no layers needed") - return; + return } - const self = this; + const self = this const overpassUrls = self.state.overpassUrl.data let bounds: BBox do { try { - - bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel); + bounds = this.state.currentBounds.data + ?.pad(this.state.layoutToUse.widenFactor) + ?.expandToTileBounds(padToZoomLevel) if (bounds === undefined) { - return undefined; + return undefined } - const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload); + const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload) if (overpass === undefined) { - return undefined; + return undefined } - this.runningQuery.setData(true); + this.runningQuery.setData(true) - [data, date] = await overpass.queryGeoJson(bounds) + ;[data, date] = await overpass.queryGeoJson(bounds) console.log("Querying overpass is done", data) } catch (e) { - self.retries.data++; - self.retries.ping(); - console.error(`QUERY FAILED due to`, e); + self.retries.data++ + self.retries.ping() + console.error(`QUERY FAILED due to`, e) await Utils.waitFor(1000) @@ -208,34 +223,38 @@ export default class OverpassFeatureSource implements FeatureSource { console.log("Trying next time with", overpassUrls[lastUsed]) } else { lastUsed = 0 - self.timeout.setData(self.retries.data * 5); + self.timeout.setData(self.retries.data * 5) while (self.timeout.data > 0) { await Utils.waitFor(1000) console.log(self.timeout.data) self.timeout.data-- - self.timeout.ping(); + self.timeout.ping() } } } - } while (data === undefined && this._isActive.data); - + } while (data === undefined && this._isActive.data) try { if (data === undefined) { return undefined } - data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined, this.state)); - self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); - return [bounds, date, layersToDownload]; + data.features.forEach((feature) => + SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature( + feature, + date, + undefined, + this.state + ) + ) + self.features.setData(data.features.map((f) => ({ feature: f, freshness: date }))) + return [bounds, date, layersToDownload] } catch (e) { console.error("Got the overpass response, but could not process it: ", e, e.stack) return undefined } finally { - self.retries.setData(0); - self.runningQuery.setData(false); + self.retries.setData(0) + self.runningQuery.setData(false) } - - } -} \ No newline at end of file +} diff --git a/Logic/Actors/PendingChangesUploader.ts b/Logic/Actors/PendingChangesUploader.ts index f123123d1..cc7ebd436 100644 --- a/Logic/Actors/PendingChangesUploader.ts +++ b/Logic/Actors/PendingChangesUploader.ts @@ -1,46 +1,42 @@ -import {Changes} from "../Osm/Changes"; -import Constants from "../../Models/Constants"; -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; +import { Changes } from "../Osm/Changes" +import Constants from "../../Models/Constants" +import { UIEventSource } from "../UIEventSource" +import { Utils } from "../../Utils" export default class PendingChangesUploader { - - private lastChange: Date; + private lastChange: Date constructor(changes: Changes, selectedFeature: UIEventSource) { - const self = this; - this.lastChange = new Date(); + const self = this + this.lastChange = new Date() changes.pendingChanges.addCallback(() => { - self.lastChange = new Date(); + self.lastChange = new Date() window.setTimeout(() => { - const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000; + const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000 if (Constants.updateTimeoutSec >= diff - 1) { - changes.flushChanges("Flushing changes due to timeout"); + changes.flushChanges("Flushing changes due to timeout") } - }, Constants.updateTimeoutSec * 1000); - }); + }, Constants.updateTimeoutSec * 1000) + }) - - selectedFeature - .stabilized(10000) - .addCallback(feature => { - if (feature === undefined) { - // The popup got closed - we flush - changes.flushChanges("Flushing changes due to popup closed"); - } - }); + selectedFeature.stabilized(10000).addCallback((feature) => { + if (feature === undefined) { + // The popup got closed - we flush + changes.flushChanges("Flushing changes due to popup closed") + } + }) if (Utils.runningFromConsole) { - return; + return } - document.addEventListener('mouseout', e => { + document.addEventListener("mouseout", (e) => { // @ts-ignore if (!e.toElement && !e.relatedTarget) { - changes.flushChanges("Flushing changes due to focus lost"); + changes.flushChanges("Flushing changes due to focus lost") } - }); + }) document.onfocus = () => { changes.flushChanges("OnFocus") @@ -50,28 +46,28 @@ export default class PendingChangesUploader { changes.flushChanges("OnFocus") } try { - document.addEventListener("visibilitychange", () => { - changes.flushChanges("Visibility change") - }, false); + document.addEventListener( + "visibilitychange", + () => { + changes.flushChanges("Visibility change") + }, + false + ) } catch (e) { console.warn("Could not register visibility change listener", e) } - function onunload(e) { if (changes.pendingChanges.data.length == 0) { - return; + return } - changes.flushChanges("onbeforeunload - probably closing or something similar"); - e.preventDefault(); + changes.flushChanges("onbeforeunload - probably closing or something similar") + e.preventDefault() return "Saving your last changes..." } window.onbeforeunload = onunload // https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156 window.addEventListener("pagehide", onunload) - } - - -} \ No newline at end of file +} diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index beff7c21f..071efd3a8 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -1,51 +1,47 @@ /** * This actor will download the latest version of the selected element from OSM and update the tags if necessary. */ -import {UIEventSource} from "../UIEventSource"; -import {ElementStorage} from "../ElementStorage"; -import {Changes} from "../Osm/Changes"; -import {OsmObject} from "../Osm/OsmObject"; -import {OsmConnection} from "../Osm/OsmConnection"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import SimpleMetaTagger from "../SimpleMetaTagger"; +import { UIEventSource } from "../UIEventSource" +import { ElementStorage } from "../ElementStorage" +import { Changes } from "../Osm/Changes" +import { OsmObject } from "../Osm/OsmObject" +import { OsmConnection } from "../Osm/OsmConnection" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import SimpleMetaTagger from "../SimpleMetaTagger" export default class SelectedElementTagsUpdater { - - private static readonly metatags = new Set(["timestamp", + private static readonly metatags = new Set([ + "timestamp", "version", "changeset", "user", "uid", - "id"]) + "id", + ]) constructor(state: { - selectedElement: UIEventSource, - allElements: ElementStorage, - changes: Changes, - osmConnection: OsmConnection, + selectedElement: UIEventSource + allElements: ElementStorage + changes: Changes + osmConnection: OsmConnection layoutToUse: LayoutConfig }) { - - - state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => { + state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { if (isLoggedIn) { SelectedElementTagsUpdater.installCallback(state) - return true; + return true } }) - } public static installCallback(state: { - selectedElement: UIEventSource, - allElements: ElementStorage, - changes: Changes, - osmConnection: OsmConnection, + selectedElement: UIEventSource + allElements: ElementStorage + changes: Changes + osmConnection: OsmConnection layoutToUse: LayoutConfig }) { - - - state.selectedElement.addCallbackAndRunD(s => { + state.selectedElement.addCallbackAndRunD((s) => { let id = s.properties?.id const backendUrl = state.osmConnection._oauth_config.url @@ -55,31 +51,31 @@ export default class SelectedElementTagsUpdater { if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) { // This object is _not_ from OSM, so we skip it! - return; + return } if (id.indexOf("-") >= 0) { // This is a new object - return; + return } - OsmObject.DownloadPropertiesOf(id).then(latestTags => { + OsmObject.DownloadPropertiesOf(id).then((latestTags) => { SelectedElementTagsUpdater.applyUpdate(state, latestTags, id) }) - - }); - + }) } - public static applyUpdate(state: { - selectedElement: UIEventSource, - allElements: ElementStorage, - changes: Changes, - osmConnection: OsmConnection, - layoutToUse: LayoutConfig - }, latestTags: any, id: string + public static applyUpdate( + state: { + selectedElement: UIEventSource + allElements: ElementStorage + changes: Changes + osmConnection: OsmConnection + layoutToUse: LayoutConfig + }, + latestTags: any, + id: string ) { try { - const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() if (leftRightSensitive) { @@ -87,11 +83,11 @@ export default class SelectedElementTagsUpdater { } const pendingChanges = state.changes.pendingChanges.data - .filter(change => change.type + "/" + change.id === id) - .filter(change => change.tags !== undefined); + .filter((change) => change.type + "/" + change.id === id) + .filter((change) => change.tags !== undefined) for (const pendingChange of pendingChanges) { - const tagChanges = pendingChange.tags; + const tagChanges = pendingChange.tags for (const tagChange of tagChanges) { const key = tagChange.k const v = tagChange.v @@ -103,10 +99,9 @@ export default class SelectedElementTagsUpdater { } } - // With the changes applied, we merge them onto the upstream object - let somethingChanged = false; - const currentTagsSource = state.allElements.getEventSourceById(id); + let somethingChanged = false + const currentTagsSource = state.allElements.getEventSourceById(id) const currentTags = currentTagsSource.data for (const key in latestTags) { let osmValue = latestTags[key] @@ -117,7 +112,7 @@ export default class SelectedElementTagsUpdater { const localValue = currentTags[key] if (localValue !== osmValue) { - somethingChanged = true; + somethingChanged = true currentTags[key] = osmValue } } @@ -137,7 +132,6 @@ export default class SelectedElementTagsUpdater { somethingChanged = true } - if (somethingChanged) { console.log("Detected upstream changes to the object when opening it, updating...") currentTagsSource.ping() @@ -148,6 +142,4 @@ export default class SelectedElementTagsUpdater { console.error("Updating the tags of selected element ", id, "failed due to", e) } } - - -} \ No newline at end of file +} diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index c5598f346..69816866b 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -1,63 +1,67 @@ -import {UIEventSource} from "../UIEventSource"; -import {OsmObject} from "../Osm/OsmObject"; -import Loc from "../../Models/Loc"; -import {ElementStorage} from "../ElementStorage"; -import FeaturePipeline from "../FeatureSource/FeaturePipeline"; -import {GeoOperations} from "../GeoOperations"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import { UIEventSource } from "../UIEventSource" +import { OsmObject } from "../Osm/OsmObject" +import Loc from "../../Models/Loc" +import { ElementStorage } from "../ElementStorage" +import FeaturePipeline from "../FeatureSource/FeaturePipeline" +import { GeoOperations } from "../GeoOperations" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" /** * Makes sure the hash shows the selected element and vice-versa. */ export default class SelectedFeatureHandler { - private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "location_track", "", undefined]) - private readonly hash: UIEventSource; + private static readonly _no_trigger_on = new Set([ + "welcome", + "copyright", + "layers", + "new", + "filters", + "location_track", + "", + undefined, + ]) + private readonly hash: UIEventSource private readonly state: { - selectedElement: UIEventSource, - allElements: ElementStorage, - locationControl: UIEventSource, + selectedElement: UIEventSource + allElements: ElementStorage + locationControl: UIEventSource layoutToUse: LayoutConfig } constructor( hash: UIEventSource, state: { - selectedElement: UIEventSource, - allElements: ElementStorage, - featurePipeline: FeaturePipeline, - locationControl: UIEventSource, + selectedElement: UIEventSource + allElements: ElementStorage + featurePipeline: FeaturePipeline + locationControl: UIEventSource layoutToUse: LayoutConfig } ) { - this.hash = hash; + this.hash = hash this.state = state - // If the hash changes, set the selected element correctly - const self = this; + const self = this hash.addCallback(() => self.setSelectedElementFromHash()) - - state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => { + state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD((_) => { // New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) { // This is an invalid hash anyway - return; + return } if (state.selectedElement.data !== undefined) { // We already have something selected - return; + return } self.setSelectedElementFromHash() }) - this.initialLoad() - } - /** * On startup: check if the hash is loaded and eventually zoom to it * @private @@ -65,21 +69,18 @@ export default class SelectedFeatureHandler { private initialLoad() { const hash = this.hash.data if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) { - return; + return } if (SelectedFeatureHandler._no_trigger_on.has(hash)) { - return; + return } if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) { - return; + return } - - OsmObject.DownloadObjectAsync(hash).then(obj => { - + OsmObject.DownloadObjectAsync(hash).then((obj) => { try { - console.log("Downloaded selected object from OSM-API for initial load: ", hash) const geojson = obj.asGeoJson() this.state.allElements.addOrGetElement(geojson) @@ -88,9 +89,7 @@ export default class SelectedFeatureHandler { } catch (e) { console.error(e) } - }) - } private setSelectedElementFromHash() { @@ -98,22 +97,21 @@ export default class SelectedFeatureHandler { const h = this.hash.data if (h === undefined || h === "") { // Hash has been cleared - we clear the selected element - state.selectedElement.setData(undefined); + state.selectedElement.setData(undefined) } else { - // we search the element to select const feature = state.allElements.ContainingFeatures.get(h) if (feature === undefined) { - return; + return } const currentlySeleced = state.selectedElement.data if (currentlySeleced === undefined) { state.selectedElement.setData(feature) - return; + return } if (currentlySeleced.properties?.id === feature.properties.id) { // We already have the right feature - return; + return } state.selectedElement.setData(feature) } @@ -121,25 +119,24 @@ export default class SelectedFeatureHandler { // If a feature is selected via the hash, zoom there private zoomToSelectedFeature() { - const selected = this.state.selectedElement.data if (selected === undefined) { return } const centerpoint = GeoOperations.centerpointCoordinates(selected) - const location = this.state.locationControl; + const location = this.state.locationControl location.data.lon = centerpoint[0] location.data.lat = centerpoint[1] - const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? [])) + const minZoom = Math.max( + 14, + ...(this.state.layoutToUse?.layers?.map((l) => l.minzoomVisible) ?? []) + ) if (location.data.zoom < minZoom) { location.data.zoom = minZoom } - location.ping(); - - + location.ping() } - -} \ No newline at end of file +} diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index 7c9a51fa0..4e9f90a62 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -1,88 +1,87 @@ -import * as L from "leaflet"; -import {UIEventSource} from "../UIEventSource"; -import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; -import FilteredLayer from "../../Models/FilteredLayer"; -import Constants from "../../Models/Constants"; -import BaseUIElement from "../../UI/BaseUIElement"; +import * as L from "leaflet" +import { UIEventSource } from "../UIEventSource" +import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" +import FilteredLayer from "../../Models/FilteredLayer" +import Constants from "../../Models/Constants" +import BaseUIElement from "../../UI/BaseUIElement" /** * The stray-click-hanlders adds a marker to the map if no feature was clicked. * Shows the given uiToShow-element in the messagebox */ export default class StrayClickHandler { - private _lastMarker; + private _lastMarker constructor( state: { - LastClickLocation: UIEventSource<{ lat: number, lon: number }>, - selectedElement: UIEventSource, - filteredLayers: UIEventSource, + LastClickLocation: UIEventSource<{ lat: number; lon: number }> + selectedElement: UIEventSource + filteredLayers: UIEventSource leafletMap: UIEventSource }, uiToShow: ScrollableFullScreen, - iconToShow: BaseUIElement) { - const self = this; + iconToShow: BaseUIElement + ) { + const self = this const leafletMap = state.leafletMap state.filteredLayers.data.forEach((filteredLayer) => { - filteredLayer.isDisplayed.addCallback(isEnabled => { + filteredLayer.isDisplayed.addCallback((isEnabled) => { if (isEnabled && self._lastMarker && leafletMap.data !== undefined) { // When a layer is activated, we remove the 'last click location' in order to force the user to reclick // This reclick might be at a location where a feature now appeared... - state.leafletMap.data.removeLayer(self._lastMarker); + state.leafletMap.data.removeLayer(self._lastMarker) } }) }) state.LastClickLocation.addCallback(function (lastClick) { - if (self._lastMarker !== undefined) { - state.leafletMap.data?.removeLayer(self._lastMarker); + state.leafletMap.data?.removeLayer(self._lastMarker) } if (lastClick === undefined) { - return; + return } - state.selectedElement.setData(undefined); + state.selectedElement.setData(undefined) const clickCoor: [number, number] = [lastClick.lat, lastClick.lon] self._lastMarker = L.marker(clickCoor, { icon: L.divIcon({ html: iconToShow.ConstructElement(), iconSize: [50, 50], iconAnchor: [25, 50], - popupAnchor: [0, -45] - }) - }); + popupAnchor: [0, -45], + }), + }) const popup = L.popup({ autoPan: true, autoPanPaddingTopLeft: [15, 15], closeOnEscapeKey: true, - autoClose: true - }).setContent("
"); - self._lastMarker.addTo(leafletMap.data); - self._lastMarker.bindPopup(popup); + autoClose: true, + }).setContent("
") + self._lastMarker.addTo(leafletMap.data) + self._lastMarker.bindPopup(popup) self._lastMarker.on("click", () => { if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) { self._lastMarker.closePopup() - leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints) - return; + leafletMap.data.flyTo( + clickCoor, + Constants.userJourney.minZoomLevelToAddNewPoints + ) + return } - uiToShow.AttachTo("strayclick") - uiToShow.Activate(); - }); - }); + uiToShow.Activate() + }) + }) state.selectedElement.addCallback(() => { if (self._lastMarker !== undefined) { - leafletMap.data.removeLayer(self._lastMarker); - this._lastMarker = undefined; + leafletMap.data.removeLayer(self._lastMarker) + this._lastMarker = undefined } }) - } - - -} \ No newline at end of file +} diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index 5bfd92c5b..c807dfca4 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -1,19 +1,19 @@ -import {Store, UIEventSource} from "../UIEventSource"; -import Locale from "../../UI/i18n/Locale"; -import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"; -import Combine from "../../UI/Base/Combine"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {ElementStorage} from "../ElementStorage"; -import {Utils} from "../../Utils"; +import { Store, UIEventSource } from "../UIEventSource" +import Locale from "../../UI/i18n/Locale" +import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" +import Combine from "../../UI/Base/Combine" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { ElementStorage } from "../ElementStorage" +import { Utils } from "../../Utils" export default class TitleHandler { constructor(state: { - selectedElement: Store, - layoutToUse: LayoutConfig, + selectedElement: Store + layoutToUse: LayoutConfig allElements: ElementStorage }) { const currentTitle: Store = state.selectedElement.map( - selected => { + (selected) => { const layout = state.layoutToUse const defaultTitle = layout?.title?.txt ?? "MapComplete" @@ -21,27 +21,32 @@ export default class TitleHandler { return defaultTitle } - const tags = selected.properties; + const tags = selected.properties for (const layer of layout.layers) { if (layer.title === undefined) { - continue; + continue } if (layer.source.osmTags.matchesProperties(tags)) { - const tagsSource = state.allElements.getEventSourceById(tags.id) ?? new UIEventSource(tags) + const tagsSource = + state.allElements.getEventSourceById(tags.id) ?? + new UIEventSource(tags) const title = new TagRenderingAnswer(tagsSource, layer.title, {}) - return new Combine([defaultTitle, " | ", title]).ConstructElement()?.textContent ?? defaultTitle; + return ( + new Combine([defaultTitle, " | ", title]).ConstructElement() + ?.textContent ?? defaultTitle + ) } } return defaultTitle - }, [Locale.language] + }, + [Locale.language] ) - - currentTitle.addCallbackAndRunD(title => { + currentTitle.addCallbackAndRunD((title) => { if (Utils.runningFromConsole) { return } document.title = title }) } -} \ No newline at end of file +} diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 19e0bb876..a48497adc 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -1,31 +1,32 @@ -import * as turf from "@turf/turf"; -import {TileRange, Tiles} from "../Models/TileRange"; -import {GeoOperations} from "./GeoOperations"; +import * as turf from "@turf/turf" +import { TileRange, Tiles } from "../Models/TileRange" +import { GeoOperations } from "./GeoOperations" export class BBox { - - static global: BBox = new BBox([[-180, -90], [180, 90]]); - readonly maxLat: number; - readonly maxLon: number; - readonly minLat: number; - readonly minLon: number; + static global: BBox = new BBox([ + [-180, -90], + [180, 90], + ]) + readonly maxLat: number + readonly maxLon: number + readonly minLat: number + readonly minLon: number /*** * Coordinates should be [[lon, lat],[lon, lat]] * @param coordinates */ constructor(coordinates) { - this.maxLat = -90; - this.maxLon = -180; - this.minLat = 90; - this.minLon = 180; - + this.maxLat = -90 + this.maxLon = -180 + this.minLat = 90 + this.minLon = 180 for (const coordinate of coordinates) { - this.maxLon = Math.max(this.maxLon, coordinate[0]); - this.maxLat = Math.max(this.maxLat, coordinate[1]); - this.minLon = Math.min(this.minLon, coordinate[0]); - this.minLat = Math.min(this.minLat, coordinate[1]); + this.maxLon = Math.max(this.maxLon, coordinate[0]) + this.maxLat = Math.max(this.maxLat, coordinate[1]) + this.minLon = Math.min(this.minLon, coordinate[0]) + this.minLat = Math.min(this.minLat, coordinate[1]) } this.maxLon = Math.min(this.maxLon, 180) @@ -33,27 +34,32 @@ export class BBox { this.minLon = Math.max(this.minLon, -180) this.minLat = Math.max(this.minLat, -90) - - this.check(); + this.check() } static fromLeafletBounds(bounds) { - return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) + return new BBox([ + [bounds.getWest(), bounds.getNorth()], + [bounds.getEast(), bounds.getSouth()], + ]) } static get(feature): BBox { if (feature.bbox?.overlapsWith === undefined) { const turfBbox: number[] = turf.bbox(feature) - feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); + feature.bbox = new BBox([ + [turfBbox[0], turfBbox[1]], + [turfBbox[2], turfBbox[3]], + ]) } - return feature.bbox; + return feature.bbox } static bboxAroundAll(bboxes: BBox[]): BBox { - let maxLat: number = -90; - let maxLon: number = -180; - let minLat: number = 80; - let minLon: number = 180; + let maxLat: number = -90 + let maxLon: number = -180 + let minLat: number = 80 + let minLon: number = 180 for (const bbox of bboxes) { maxLat = Math.max(maxLat, bbox.maxLat) @@ -61,17 +67,20 @@ export class BBox { minLat = Math.min(minLat, bbox.minLat) minLon = Math.min(minLon, bbox.minLon) } - return new BBox([[maxLon, maxLat], [minLon, minLat]]) + return new BBox([ + [maxLon, maxLat], + [minLon, minLat], + ]) } /** * Calculates the BBox based on a slippy map tile number - * + * * const bbox = BBox.fromTile(16, 32754, 21785) - * bbox.minLon // => -0.076904296875 - * bbox.maxLon // => -0.0714111328125 - * bbox.minLat // => 51.5292513551899 - * bbox.maxLat // => 51.53266860674158 + * bbox.minLon // => -0.076904296875 + * bbox.maxLon // => -0.0714111328125 + * bbox.minLat // => 51.5292513551899 + * bbox.maxLat // => 51.53266860674158 */ static fromTile(z: number, x: number, y: number): BBox { return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) @@ -85,11 +94,10 @@ export class BBox { } public unionWith(other: BBox) { - return new BBox([[ - Math.max(this.maxLon, other.maxLon), - Math.max(this.maxLat, other.maxLat)], - [Math.min(this.minLon, other.minLon), - Math.min(this.minLat, other.minLat)]]) + return new BBox([ + [Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)], + [Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)], + ]) } /** @@ -102,32 +110,31 @@ export class BBox { public overlapsWith(other: BBox) { if (this.maxLon < other.minLon) { - return false; + return false } if (this.maxLat < other.minLat) { - return false; + return false } if (this.minLon > other.maxLon) { - return false; + return false } - return this.minLat <= other.maxLat; - + return this.minLat <= other.maxLat } public isContainedIn(other: BBox) { if (this.maxLon > other.maxLon) { - return false; + return false } if (this.maxLat > other.maxLat) { - return false; + return false } if (this.minLon < other.minLon) { - return false; + return false } if (this.minLat < other.minLat) { return false } - return true; + return true } getEast() { @@ -147,32 +154,35 @@ export class BBox { } contains(lonLat: [number, number]) { - return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat - && this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon + return ( + this.minLat <= lonLat[1] && + lonLat[1] <= this.maxLat && + this.minLon <= lonLat[0] && + lonLat[0] <= this.maxLon + ) } pad(factor: number, maxIncrease = 2): BBox { - const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor) const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor) - return new BBox([[ - this.minLon - lonDiff, - this.minLat - latDiff - ], [this.maxLon + lonDiff, - this.maxLat + latDiff]]) + return new BBox([ + [this.minLon - lonDiff, this.minLat - latDiff], + [this.maxLon + lonDiff, this.maxLat + latDiff], + ]) } padAbsolute(degrees: number): BBox { - - return new BBox([[ - this.minLon - degrees, - this.minLat - degrees - ], [this.maxLon + degrees, - this.maxLat + degrees]]) + return new BBox([ + [this.minLon - degrees, this.minLat - degrees], + [this.maxLon + degrees, this.maxLat + degrees], + ]) } toLeaflet() { - return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] + return [ + [this.minLat, this.minLon], + [this.maxLat, this.maxLon], + ] } asGeoJson(properties: any): any { @@ -181,16 +191,16 @@ export class BBox { properties: properties, geometry: { type: "Polygon", - coordinates: [[ - - [this.minLon, this.minLat], - [this.maxLon, this.minLat], - [this.maxLon, this.maxLat], - [this.minLon, this.maxLat], - [this.minLon, this.minLat], - - ]] - } + coordinates: [ + [ + [this.minLon, this.minLat], + [this.maxLon, this.minLat], + [this.maxLon, this.maxLat], + [this.minLon, this.maxLat], + [this.minLon, this.minLat], + ], + ], + }, } } @@ -206,22 +216,22 @@ export class BBox { return new BBox([].concat(boundsul, boundslr)) } - toMercator(): { minLat: number, maxLat: number, minLon: number, maxLon: number } { + toMercator(): { minLat: number; maxLat: number; minLon: number; maxLon: number } { const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat]) const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat]) return { - minLon, maxLon, - minLat, maxLat + minLon, + maxLon, + minLat, + maxLat, } - - } - private check() { + private check() { if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { - console.trace("BBox with NaN detected:", this); - throw "BBOX has NAN"; + console.trace("BBox with NaN detected:", this) + throw "BBOX has NAN" } } -} \ No newline at end of file +} diff --git a/Logic/ContributorCount.ts b/Logic/ContributorCount.ts index 99b9c503c..ef827585d 100644 --- a/Logic/ContributorCount.ts +++ b/Logic/ContributorCount.ts @@ -1,46 +1,56 @@ /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions -import {Store, UIEventSource} from "./UIEventSource"; -import FeaturePipeline from "./FeatureSource/FeaturePipeline"; -import Loc from "../Models/Loc"; -import {BBox} from "./BBox"; +import { Store, UIEventSource } from "./UIEventSource" +import FeaturePipeline from "./FeatureSource/FeaturePipeline" +import Loc from "../Models/Loc" +import { BBox } from "./BBox" export default class ContributorCount { + public readonly Contributors: UIEventSource> = new UIEventSource< + Map + >(new Map()) + private readonly state: { + featurePipeline: FeaturePipeline + currentBounds: Store + locationControl: Store + } + private lastUpdate: Date = undefined - public readonly Contributors: UIEventSource> = new UIEventSource>(new Map()); - private readonly state: { featurePipeline: FeaturePipeline, currentBounds: Store, locationControl: Store }; - private lastUpdate: Date = undefined; - - constructor(state: { featurePipeline: FeaturePipeline, currentBounds: Store, locationControl: Store }) { - this.state = state; - const self = this; - state.currentBounds.map(bbox => { + constructor(state: { + featurePipeline: FeaturePipeline + currentBounds: Store + locationControl: Store + }) { + this.state = state + const self = this + state.currentBounds.map((bbox) => { self.update(bbox) }) - state.featurePipeline.runningQuery.addCallbackAndRun( - _ => self.update(state.currentBounds.data) + state.featurePipeline.runningQuery.addCallbackAndRun((_) => + self.update(state.currentBounds.data) ) - } private update(bbox: BBox) { if (bbox === undefined) { - return; + return } - const now = new Date(); - if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { - return; + const now = new Date() + if ( + this.lastUpdate !== undefined && + now.getTime() - this.lastUpdate.getTime() < 1000 * 60 + ) { + return } - this.lastUpdate = now; + this.lastUpdate = now const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) - const hist = new Map(); + const hist = new Map() for (const list of featuresList) { for (const feature of list) { const contributor = feature.properties["_last_edit:contributor"] - const count = hist.get(contributor) ?? 0; + const count = hist.get(contributor) ?? 0 hist.set(contributor, count + 1) } } this.Contributors.setData(hist) } - -} \ No newline at end of file +} diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts index b85f6e40c..85da45e2b 100644 --- a/Logic/DetermineLayout.ts +++ b/Logic/DetermineLayout.ts @@ -1,35 +1,37 @@ -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import {QueryParameters} from "./Web/QueryParameters"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import {Utils} from "../Utils"; -import Combine from "../UI/Base/Combine"; -import {SubtleButton} from "../UI/Base/SubtleButton"; -import BaseUIElement from "../UI/BaseUIElement"; -import {UIEventSource} from "./UIEventSource"; -import {LocalStorageSource} from "./Web/LocalStorageSource"; -import LZString from "lz-string"; -import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import SharedTagRenderings from "../Customizations/SharedTagRenderings"; +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import { QueryParameters } from "./Web/QueryParameters" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import { FixedUiElement } from "../UI/Base/FixedUiElement" +import { Utils } from "../Utils" +import Combine from "../UI/Base/Combine" +import { SubtleButton } from "../UI/Base/SubtleButton" +import BaseUIElement from "../UI/BaseUIElement" +import { UIEventSource } from "./UIEventSource" +import { LocalStorageSource } from "./Web/LocalStorageSource" +import LZString from "lz-string" +import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import SharedTagRenderings from "../Customizations/SharedTagRenderings" import * as known_layers from "../assets/generated/known_layers.json" -import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; +import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" import * as licenses from "../assets/generated/license_info.json" -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; -import {FixImages} from "../Models/ThemeConfig/Conversion/FixImages"; -import Svg from "../Svg"; +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" +import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" +import Svg from "../Svg" export default class DetermineLayout { + private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) - private static readonly _knownImages =new Set( Array.from(licenses).map(l => l.path)) - /** * Gets the correct layout for this website */ public static async GetLayout(): Promise { - - const loadCustomThemeParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme") - const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data); + const loadCustomThemeParam = QueryParameters.GetQueryParameter( + "userlayout", + "false", + "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme" + ) + const layoutFromBase64 = decodeURIComponent(loadCustomThemeParam.data) if (layoutFromBase64.startsWith("http")) { return await DetermineLayout.LoadRemoteTheme(layoutFromBase64) @@ -42,150 +44,164 @@ export default class DetermineLayout { let layoutId: string = undefined - const path = window.location.pathname.split("/").slice(-1)[0]; + const path = window.location.pathname.split("/").slice(-1)[0] if (path !== "theme.html" && path !== "") { - layoutId = path; + layoutId = path if (path.endsWith(".html")) { - layoutId = path.substr(0, path.length - 5); + layoutId = path.substr(0, path.length - 5) } - console.log("Using layout", layoutId); + console.log("Using layout", layoutId) } - layoutId = QueryParameters.GetQueryParameter("layout", layoutId, "The layout to load into MapComplete").data; + layoutId = QueryParameters.GetQueryParameter( + "layout", + layoutId, + "The layout to load into MapComplete" + ).data return AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()) } - public static LoadLayoutFromHash( - userLayoutParam: UIEventSource - ): LayoutConfig | null { - let hash = location.hash.substr(1); - let json: any; + public static LoadLayoutFromHash(userLayoutParam: UIEventSource): LayoutConfig | null { + let hash = location.hash.substr(1) + let json: any try { // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter const dedicatedHashFromLocalStorage = LocalStorageSource.Get( "user-layout-" + userLayoutParam.data?.replace(" ", "_") - ); + ) if (dedicatedHashFromLocalStorage.data?.length < 10) { - dedicatedHashFromLocalStorage.setData(undefined); + dedicatedHashFromLocalStorage.setData(undefined) } - const hashFromLocalStorage = LocalStorageSource.Get( - "last-loaded-user-layout" - ); + const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout") if (hash.length < 10) { - hash = - dedicatedHashFromLocalStorage.data ?? - hashFromLocalStorage.data; + hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data } else { - console.log("Saving hash to local storage"); - hashFromLocalStorage.setData(hash); - dedicatedHashFromLocalStorage.setData(hash); + console.log("Saving hash to local storage") + hashFromLocalStorage.setData(hash) + dedicatedHashFromLocalStorage.setData(hash) } try { - json = JSON.parse(atob(hash)); + json = JSON.parse(atob(hash)) } catch (e) { // We try to decode with lz-string try { json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) } catch (e) { console.error(e) - DetermineLayout.ShowErrorOnCustomTheme("Could not decode the hash", new FixedUiElement("Not a valid (LZ-compressed) JSON")) - return null; + DetermineLayout.ShowErrorOnCustomTheme( + "Could not decode the hash", + new FixedUiElement("Not a valid (LZ-compressed) JSON") + ) + return null } } const layoutToUse = DetermineLayout.prepCustomTheme(json) - userLayoutParam.setData(layoutToUse.id); + userLayoutParam.setData(layoutToUse.id) return layoutToUse } catch (e) { console.error(e) if (hash === undefined || hash.length < 10) { - DetermineLayout.ShowErrorOnCustomTheme("Could not load a theme from the hash", new FixedUiElement("Hash does not contain data"), json) + DetermineLayout.ShowErrorOnCustomTheme( + "Could not load a theme from the hash", + new FixedUiElement("Hash does not contain data"), + json + ) } this.ShowErrorOnCustomTheme("Could not parse the hash", new FixedUiElement(e), json) - return null; + return null } } public static ShowErrorOnCustomTheme( intro: string = "Error: could not parse the custom layout:", error: BaseUIElement, - json?: any) { + json?: any + ) { new Combine([ intro, error.SetClass("alert"), - new SubtleButton(Svg.back_svg(), - "Go back to the theme overview", - {url: window.location.protocol + "//" + window.location.host + "/index.html", newTab: false}), - json !== undefined ? new SubtleButton(Svg.download_svg(),"Download the JSON file").onClick(() => { - Utils.offerContentsAsDownloadableFile(JSON.stringify(json, null, " "), "theme_definition.json") - }) : undefined + new SubtleButton(Svg.back_svg(), "Go back to the theme overview", { + url: window.location.protocol + "//" + window.location.host + "/index.html", + newTab: false, + }), + json !== undefined + ? new SubtleButton(Svg.download_svg(), "Download the JSON file").onClick(() => { + Utils.offerContentsAsDownloadableFile( + JSON.stringify(json, null, " "), + "theme_definition.json" + ) + }) + : undefined, ]) .SetClass("flex flex-col clickable") - .AttachTo("centermessage"); + .AttachTo("centermessage") } private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { - - if(json.layers === undefined && json.tagRenderings !== undefined){ - const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined) + if (json.layers === undefined && json.tagRenderings !== undefined) { + const iconTr = json.mapRendering.map((mr) => mr.icon).find((icon) => icon !== undefined) const icon = new TagRenderingConfig(iconTr).render.txt json = { id: json.id, description: json.description, descriptionTail: { - en: "
Layer only mode.
The loaded custom theme actually isn't a custom theme, but only contains a layer." + en: "
Layer only mode.
The loaded custom theme actually isn't a custom theme, but only contains a layer.", }, icon, title: json.name, layers: [json], } } - + const knownLayersDict = new Map() for (const key in known_layers.layers) { const layer = known_layers.layers[key] - knownLayersDict.set(layer.id, layer) + knownLayersDict.set(layer.id, layer) } const converState = { tagRenderings: SharedTagRenderings.SharedTagRenderingJson, sharedLayers: knownLayersDict, - publicLayers: new Set() + publicLayers: new Set(), } json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") - const raw = json; + const raw = json - json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images") - json.enableNoteImports = json.enableNoteImports ?? false; + json = new FixImages(DetermineLayout._knownImages).convertStrict( + json, + "While fixing the images" + ) + json.enableNoteImports = json.enableNoteImports ?? false json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") console.log("The layoutconfig is ", json) - + json.id = forceId ?? json.id - + return new LayoutConfig(json, false, { definitionRaw: JSON.stringify(raw, null, " "), - definedAtUrl: sourceUrl + definedAtUrl: sourceUrl, }) } private static async LoadRemoteTheme(link: string): Promise { - console.log("Downloading map theme from ", link); + console.log("Downloading map theme from ", link) - new FixedUiElement(`Downloading the theme from the link...`) - .AttachTo("centermessage"); + new FixedUiElement(`Downloading the theme from the link...`).AttachTo( + "centermessage" + ) try { - let parsed = await Utils.downloadJson(link) try { let forcedId = parsed.id const url = new URL(link) - if(!(url.hostname === "localhost" || url.hostname === "127.0.0.1")){ - forcedId = link; + if (!(url.hostname === "localhost" || url.hostname === "127.0.0.1")) { + forcedId = link } console.log("Loaded remote link:", link) - return DetermineLayout.prepCustomTheme(parsed, link, forcedId); + return DetermineLayout.prepCustomTheme(parsed, link, forcedId) } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( @@ -193,17 +209,15 @@ export default class DetermineLayout { new FixedUiElement(e), parsed ) - return null; + return null } - } catch (e) { console.error(e) DetermineLayout.ShowErrorOnCustomTheme( `${link} is invalid - probably not found or invalid JSON:`, new FixedUiElement(e) ) - return null; + return null } } - -} \ No newline at end of file +} diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index 2a23a9054..a142ef71b 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -1,20 +1,17 @@ /** * Keeps track of a dictionary 'elementID' -> UIEventSource */ -import {UIEventSource} from "./UIEventSource"; -import {GeoJSONObject} from "@turf/turf"; +import { UIEventSource } from "./UIEventSource" +import { GeoJSONObject } from "@turf/turf" export class ElementStorage { + public ContainingFeatures = new Map() + private _elements = new Map>() - public ContainingFeatures = new Map(); - private _elements = new Map>(); - - constructor() { - - } + constructor() {} addElementById(id: string, eventSource: UIEventSource) { - this._elements.set(id, eventSource); + this._elements.set(id, eventSource) } /** @@ -24,8 +21,8 @@ export class ElementStorage { * Note: it will cleverly merge the tags, if needed */ addOrGetElement(feature: any): UIEventSource { - const elementId = feature.properties.id; - const newProperties = feature.properties; + const elementId = feature.properties.id + const newProperties = feature.properties const es = this.addOrGetById(elementId, newProperties) @@ -33,91 +30,89 @@ export class ElementStorage { feature.properties = es.data if (!this.ContainingFeatures.has(elementId)) { - this.ContainingFeatures.set(elementId, feature); + this.ContainingFeatures.set(elementId, feature) } - return es; + return es } getEventSourceById(elementId): UIEventSource { if (elementId === undefined) { - return undefined; + return undefined } - return this._elements.get(elementId); + return this._elements.get(elementId) } has(id) { - return this._elements.has(id); + return this._elements.has(id) } - addAlias(oldId: string, newId: string){ + addAlias(oldId: string, newId: string) { if (newId === undefined) { // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap! - const element = this.getEventSourceById(oldId); + const element = this.getEventSourceById(oldId) element.data._deleted = "yes" - element.ping(); - return; + element.ping() + return } - + if (oldId == newId) { - return undefined; + return undefined } - const element = this.getEventSourceById( oldId); + const element = this.getEventSourceById(oldId) if (element === undefined) { // Element to rewrite not found, probably a node or relation that is not rendered return undefined } - element.data.id = newId; - this.addElementById(newId, element); - this.ContainingFeatures.set(newId, this.ContainingFeatures.get( oldId)) - element.ping(); + element.data.id = newId + this.addElementById(newId, element) + this.ContainingFeatures.set(newId, this.ContainingFeatures.get(oldId)) + element.ping() } - + private addOrGetById(elementId: string, newProperties: any): UIEventSource { if (!this._elements.has(elementId)) { - const eventSource = new UIEventSource(newProperties, "tags of " + elementId); - this._elements.set(elementId, eventSource); - return eventSource; + const eventSource = new UIEventSource(newProperties, "tags of " + elementId) + this._elements.set(elementId, eventSource) + return eventSource } - - const es = this._elements.get(elementId); + const es = this._elements.get(elementId) if (es.data == newProperties) { // Reference comparison gives the same object! we can just return the event source - return es; + return es } - const keptKeys = es.data; + const keptKeys = es.data // The element already exists // We use the new feature to overwrite all the properties in the already existing eventsource const debug_msg = [] - let somethingChanged = false; + let somethingChanged = false for (const k in newProperties) { if (!newProperties.hasOwnProperty(k)) { - continue; + continue } - const v = newProperties[k]; + const v = newProperties[k] if (keptKeys[k] !== v) { - if (v === undefined) { // The new value is undefined; the tag might have been removed // It might be a metatag as well // In the latter case, we do keep the tag! if (!k.startsWith("_")) { delete keptKeys[k] - debug_msg.push(("Erased " + k)) + debug_msg.push("Erased " + k) } } else { - keptKeys[k] = v; + keptKeys[k] = v debug_msg.push(k + " --> " + v) } - somethingChanged = true; + somethingChanged = true } } if (somethingChanged) { - es.ping(); + es.ping() } - return es; + return es } -} \ No newline at end of file +} diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index 71549df33..dd568cab4 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -1,11 +1,11 @@ -import {GeoOperations} from "./GeoOperations"; -import Combine from "../UI/Base/Combine"; -import RelationsTracker from "./Osm/RelationsTracker"; -import BaseUIElement from "../UI/BaseUIElement"; -import List from "../UI/Base/List"; -import Title from "../UI/Base/Title"; -import {BBox} from "./BBox"; -import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf"; +import { GeoOperations } from "./GeoOperations" +import Combine from "../UI/Base/Combine" +import RelationsTracker from "./Osm/RelationsTracker" +import BaseUIElement from "../UI/BaseUIElement" +import List from "../UI/Base/List" +import Title from "../UI/Base/Title" +import { BBox } from "./BBox" +import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf" export interface ExtraFuncParams { /** @@ -13,7 +13,7 @@ export interface ExtraFuncParams { * Note that more features then requested can be given back. * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] */ - getFeaturesWithin: (layerId: string, bbox: BBox) => Feature[][], + getFeaturesWithin: (layerId: string, bbox: BBox) => Feature[][] memberships: RelationsTracker getFeatureById: (id: string) => Feature } @@ -22,19 +22,23 @@ export interface ExtraFuncParams { * Describes a function that is added to a geojson object in order to calculate calculated tags */ interface ExtraFunction { - readonly _name: string; - readonly _args: string[]; - readonly _doc: string; - readonly _f: (params: ExtraFuncParams, feat: Feature) => any; - + readonly _name: string + readonly _args: string[] + readonly _doc: string + readonly _f: (params: ExtraFuncParams, feat: Feature) => any } class EnclosingFunc implements ExtraFunction { _name = "enclosingFeatures" - _doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", "", + _doc = [ + "Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)", + "", "The result is a list of features: `{feat: Polygon}[]`", - "This function will never return the feature itself."].join("\n") - _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] + "This function will never return the feature itself.", + ].join("\n") + _args = [ + "...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)", + ] _f(params: ExtraFuncParams, feat: Feature) { return (...layerIds: string[]) => { @@ -45,10 +49,10 @@ class EnclosingFunc implements ExtraFunction { for (const layerId of layerIds) { const otherFeaturess = params.getFeaturesWithin(layerId, bbox) if (otherFeaturess === undefined) { - continue; + continue } if (otherFeaturess.length === 0) { - continue; + continue } for (const otherFeatures of otherFeaturess) { for (const otherFeature of otherFeatures) { @@ -56,26 +60,33 @@ class EnclosingFunc implements ExtraFunction { continue } seenIds.add(otherFeature.properties.id) - if (otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon") { - continue; + if ( + otherFeature.geometry.type !== "Polygon" && + otherFeature.geometry.type !== "MultiPolygon" + ) { + continue } - if (GeoOperations.completelyWithin(feat, >otherFeature)) { - result.push({feat: otherFeature}) + if ( + GeoOperations.completelyWithin( + feat, + >otherFeature + ) + ) { + result.push({ feat: otherFeature }) } } } } - return result; + return result } } } class OverlapFunc implements ExtraFunction { - - - _name = "overlapWith"; - _doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.", + _name = "overlapWith" + _doc = [ + "Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.", "If the current feature is a point, all features that this point is embeded in are given.", "", "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.", @@ -83,27 +94,29 @@ class OverlapFunc implements ExtraFunction { "", "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", "", - "Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature" + "Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature", ].join("\n") - _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] + _args = [ + "...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)", + ] _f(params, feat) { return (...layerIds: string[]) => { - const result: { feat: any, overlap: number }[] = [] + const result: { feat: any; overlap: number }[] = [] const seenIds = new Set() const bbox = BBox.get(feat) for (const layerId of layerIds) { const otherFeaturess = params.getFeaturesWithin(layerId, bbox) if (otherFeaturess === undefined) { - continue; + continue } if (otherFeaturess.length === 0) { - continue; + continue } for (const otherFeatures of otherFeaturess) { const overlap = GeoOperations.calculateOverlap(feat, otherFeatures) for (const overlappingFeature of overlap) { - if(seenIds.has(overlappingFeature.feat.properties.id)){ + if (seenIds.has(overlappingFeature.feat.properties.id)) { continue } seenIds.add(overlappingFeature.feat.properties.id) @@ -113,105 +126,113 @@ class OverlapFunc implements ExtraFunction { } result.sort((a, b) => b.overlap - a.overlap) - return result; + return result } } } - class IntersectionFunc implements ExtraFunction { - - - _name = "intersectionsWith"; - _doc = "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" + + _name = "intersectionsWith" + _doc = + "Gives the intersection points with selected features. Only works with (Multi)Polygons and LineStrings.\n\n" + "Returns a `{feat: GeoJson, intersections: [number,number][]}` where `feat` is the full, original feature. This list is in random order.\n\n" + "If the current feature is a point, this function will return an empty list.\n" + "Points from other layers are ignored - even if the points are parts of the current linestring." - _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)"] + _args = [ + "...layerIds - one or more layer ids of the layer from which every feature is checked for intersection)", + ] _f(params: ExtraFuncParams, feat) { return (...layerIds: string[]) => { - const result: { feat: any, intersections: [number, number][] }[] = [] + const result: { feat: any; intersections: [number, number][] }[] = [] const bbox = BBox.get(feat) for (const layerId of layerIds) { const otherLayers = params.getFeaturesWithin(layerId, bbox) if (otherLayers === undefined) { - continue; + continue } if (otherLayers.length === 0) { - continue; + continue } for (const tile of otherLayers) { for (const otherFeature of tile) { - const intersections = GeoOperations.LineIntersections(feat, otherFeature) if (intersections.length === 0) { continue } - result.push({feat: otherFeature, intersections}) + result.push({ feat: otherFeature, intersections }) } } } - return result; + return result } } } - class DistanceToFunc implements ExtraFunction { - - _name = "distanceTo"; - _doc = "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object"; + _name = "distanceTo" + _doc = + "Calculates the distance between the feature and a specified point in meter. The input should either be a pair of coordinates, a geojson feature or the ID of an object" _args = ["feature OR featureID OR longitude", "undefined OR latitude"] _f(featuresPerLayer, feature) { return (arg0, lat) => { if (arg0 === undefined) { - return undefined; + return undefined } if (typeof arg0 === "number") { // Feature._lon and ._lat is conveniently place by one of the other metatags - return GeoOperations.distanceBetween([arg0, lat], GeoOperations.centerpointCoordinates(feature)); + return GeoOperations.distanceBetween( + [arg0, lat], + GeoOperations.centerpointCoordinates(feature) + ) } if (typeof arg0 === "string") { // This is an identifier const feature = featuresPerLayer.getFeatureById(arg0) if (feature === undefined) { - return undefined; + return undefined } - arg0 = feature; + arg0 = feature } // arg0 is probably a geojsonfeature - return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), GeoOperations.centerpointCoordinates(feature)) - + return GeoOperations.distanceBetween( + GeoOperations.centerpointCoordinates(arg0), + GeoOperations.centerpointCoordinates(feature) + ) } } } - class ClosestObjectFunc implements ExtraFunction { _name = "closest" - _doc = "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)" + _doc = + "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet loaded)" _args = ["list of features or a layer name or '*' to get all features"] _f(params, feature) { - return (features) => ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat + return (features) => + ClosestNObjectFunc.GetClosestNFeatures(params, feature, features)?.[0]?.feat } - } - class ClosestNObjectFunc implements ExtraFunction { _name = "closestn" - _doc = "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " + + _doc = + "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " + "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" + "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)" - _args = ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] + _args = [ + "list of features or layer name or '*' to get all features", + "amount of features", + "unique tag key (optional)", + "maxDistanceInMeters (optional)", + ] /** * Gets the closes N features, sorted by ascending distance. @@ -223,45 +244,61 @@ class ClosestNObjectFunc implements ExtraFunction { * @constructor * @private */ - static GetClosestNFeatures(params: ExtraFuncParams, - feature: any, - features: string | any[], - options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] { + static GetClosestNFeatures( + params: ExtraFuncParams, + feature: any, + features: string | any[], + options?: { maxFeatures?: number; uniqueTag?: string | undefined; maxDistance?: number } + ): { feat: any; distance: number }[] { const maxFeatures = options?.maxFeatures ?? 1 const maxDistance = options?.maxDistance ?? 500 const uniqueTag: string | undefined = options?.uniqueTag if (typeof features === "string") { const name = features - const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)) + const bbox = GeoOperations.bbox( + GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance) + ) features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) } else { features = [features] } if (features === undefined) { - return; + return } const selfCenter = GeoOperations.centerpointCoordinates(feature) - let closestFeatures: { feat: any, distance: number }[] = []; + let closestFeatures: { feat: any; distance: number }[] = [] for (const featureList of features) { // Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here for (const otherFeature of featureList) { - - if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { - continue; // We ignore self + if ( + otherFeature === feature || + otherFeature.properties.id === feature.properties.id + ) { + continue // We ignore self } const distance = GeoOperations.distanceBetween( GeoOperations.centerpointCoordinates(otherFeature), selfCenter ) if (distance === undefined || distance === null || isNaN(distance)) { - console.error("Could not calculate the distance between", feature, "and", otherFeature) + console.error( + "Could not calculate the distance between", + feature, + "and", + otherFeature + ) throw "Undefined distance!" } if (distance === 0) { - console.trace("Got a suspiciously zero distance between", otherFeature, "and self-feature", feature) + console.trace( + "Got a suspiciously zero distance between", + otherFeature, + "and self-feature", + feature + ) } if (distance > maxDistance) { @@ -272,13 +309,15 @@ class ClosestNObjectFunc implements ExtraFunction { // This is the first matching feature we find - always add it closestFeatures.push({ feat: otherFeature, - distance: distance + distance: distance, }) - continue; + continue } - - if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { + if ( + closestFeatures.length >= maxFeatures && + closestFeatures[maxFeatures - 1].distance < distance + ) { // The last feature of the list (and thus the furthest away is still closer // No use for checking, as we already have plenty of features! continue @@ -286,11 +325,13 @@ class ClosestNObjectFunc implements ExtraFunction { let targetIndex = closestFeatures.length for (let i = 0; i < closestFeatures.length; i++) { - const closestFeature = closestFeatures[i]; + const closestFeature = closestFeatures[i] if (uniqueTag !== undefined) { - const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && - closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] + const uniqueTagsMatch = + otherFeature.properties[uniqueTag] !== undefined && + closestFeature.feat.properties[uniqueTag] === + otherFeature.properties[uniqueTag] if (uniqueTagsMatch) { targetIndex = -1 if (closestFeature.distance > distance) { @@ -298,9 +339,9 @@ class ClosestNObjectFunc implements ExtraFunction { // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads') // AT this point, we have found a closer segment with the same, identical tag // so we replace directly - closestFeatures[i] = {feat: otherFeature, distance: distance} + closestFeatures[i] = { feat: otherFeature, distance: distance } } - break; + break } } @@ -316,19 +357,19 @@ class ClosestNObjectFunc implements ExtraFunction { } } } - break; + break } } if (targetIndex == -1) { - continue; // value is already swapped by the unique tag + continue // value is already swapped by the unique tag } if (targetIndex < maxFeatures) { // insert and drop one closestFeatures.splice(targetIndex, 0, { feat: otherFeature, - distance: distance + distance: distance, }) if (closestFeatures.length >= maxFeatures) { closestFeatures.splice(maxFeatures, 1) @@ -337,19 +378,15 @@ class ClosestNObjectFunc implements ExtraFunction { // Overwrite the last element closestFeatures[targetIndex] = { feat: otherFeature, - distance: distance + distance: distance, } - } - - } } - return closestFeatures; + return closestFeatures } _f(params, feature) { - return (features, amount, uniqueTag, maxDistanceInMeters) => { let distance: number = Number(maxDistanceInMeters) if (isNaN(distance)) { @@ -358,60 +395,54 @@ class ClosestNObjectFunc implements ExtraFunction { return ClosestNObjectFunc.GetClosestNFeatures(params, feature, features, { maxFeatures: Number(amount), uniqueTag: uniqueTag, - maxDistance: distance - }); + maxDistance: distance, + }) } } - } - class Memberships implements ExtraFunction { _name = "memberships" - _doc = "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + + _doc = + "Gives a list of `{role: string, relation: Relation}`-objects, containing all the relations that this feature is part of. " + "\n\n" + "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`" _args = [] _f(params, feat) { - return () => - params.memberships.knownRelations.data.get(feat.properties.id) ?? [] - + return () => params.memberships.knownRelations.data.get(feat.properties.id) ?? [] } } - class GetParsed implements ExtraFunction { _name = "get" - _doc = "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..." + _doc = + "Gets the property of the feature, parses it (as JSON) and returns it. Might return 'undefined' if not defined, null, ..." _args = ["key"] _f(params, feat) { - return key => { + return (key) => { const value = feat.properties[key] if (value === undefined) { - return undefined; + return undefined } try { const parsed = JSON.parse(value) if (parsed === null) { - return undefined; + return undefined } - return parsed; + return parsed } catch (e) { - console.warn("Could not parse property " + key + " due to: " + e + ", the value is " + value) - return undefined; + console.warn( + "Could not parse property " + key + " due to: " + e + ", the value is " + value + ) + return undefined } - } - } } - export class ExtraFunctions { - - static readonly intro = new Combine([ new Title("Calculating tags with Javascript", 2), "In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. `lat`, `lon`, `_country`), as detailed above.", @@ -421,13 +452,13 @@ export class ExtraFunctions { new List([ "DO NOT DO THIS AS BEGINNER", "**Only do this if all other techniques fail** This should _not_ be done to create a rendering effect, only to calculate a specific value", - "**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs." + "**THIS MIGHT BE DISABLED WITHOUT ANY NOTICE ON UNOFFICIAL THEMES** As unofficial themes might be loaded from the internet, this is the equivalent of injecting arbitrary code into the client. It'll be disabled if abuse occurs.", ]), "To enable this feature, add a field `calculatedTags` in the layer object, e.g.:", "````", - "\"calculatedTags\": [", - " \"_someKey=javascript-expression\",", - " \"name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator\",", + '"calculatedTags": [', + ' "_someKey=javascript-expression",', + ' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",', " \"_distanceCloserThen3Km=feat.distanceTo( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ", " ]", "````", @@ -436,11 +467,12 @@ export class ExtraFunctions { new List([ "`area` contains the surface area (in square meters) of the object", - "`lat` and `lon` contain the latitude and longitude" + "`lat` and `lon` contain the latitude and longitude", ]), - "Some advanced functions are available on **feat** as well:" - ]).SetClass("flex-col").AsMarkdown(); - + "Some advanced functions are available on **feat** as well:", + ]) + .SetClass("flex-col") + .AsMarkdown() private static readonly allFuncs: ExtraFunction[] = [ new DistanceToFunc(), @@ -450,8 +482,8 @@ export class ExtraFunctions { new ClosestObjectFunc(), new ClosestNObjectFunc(), new Memberships(), - new GetParsed() - ]; + new GetParsed(), + ] public static FullPatchFeature(params: ExtraFuncParams, feature) { if (feature._is_patched) { @@ -464,20 +496,15 @@ export class ExtraFunctions { } public static HelpText(): BaseUIElement { - const elems = [] for (const func of ExtraFunctions.allFuncs) { - elems.push(new Title(func._name, 3), - func._doc, - new List(func._args ?? [], true)) + elems.push(new Title(func._name, 3), func._doc, new List(func._args ?? [], true)) } return new Combine([ ExtraFunctions.intro, - new List(ExtraFunctions.allFuncs.map(func => `[${func._name}](#${func._name})`)), - ...elems - ]); + new List(ExtraFunctions.allFuncs.map((func) => `[${func._name}](#${func._name})`)), + ...elems, + ]) } - - } diff --git a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts index f17c27afe..933c1c116 100644 --- a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts +++ b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts @@ -1,26 +1,30 @@ -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import MetaTagging from "../../MetaTagging"; -import {ElementStorage} from "../../ElementStorage"; -import {ExtraFuncParams} from "../../ExtraFunctions"; -import FeaturePipeline from "../FeaturePipeline"; -import {BBox} from "../../BBox"; -import {UIEventSource} from "../../UIEventSource"; +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import MetaTagging from "../../MetaTagging" +import { ElementStorage } from "../../ElementStorage" +import { ExtraFuncParams } from "../../ExtraFunctions" +import FeaturePipeline from "../FeaturePipeline" +import { BBox } from "../../BBox" +import { UIEventSource } from "../../UIEventSource" /**** * Concerned with the logic of updating the right layer at the right time */ class MetatagUpdater { public readonly neededLayerBboxes = new Map() - private source: FeatureSourceForLayer & Tiled; + private source: FeatureSourceForLayer & Tiled private readonly params: ExtraFuncParams - private state: { allElements?: ElementStorage }; + private state: { allElements?: ElementStorage } private readonly isDirty = new UIEventSource(false) - constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) { - this.state = state; - this.source = source; - const self = this; + constructor( + source: FeatureSourceForLayer & Tiled, + state: { allElements?: ElementStorage }, + featurePipeline: FeaturePipeline + ) { + this.state = state + this.source = source + const self = this this.params = { getFeatureById(id) { return state.allElements.ContainingFeatures.get(id) @@ -29,21 +33,20 @@ class MetatagUpdater { // We keep track of the BBOX that this source needs let oldBbox: BBox = self.neededLayerBboxes.get(layerId) if (oldBbox === undefined) { - self.neededLayerBboxes.set(layerId, bbox); + self.neededLayerBboxes.set(layerId, bbox) } else if (!bbox.isContainedIn(oldBbox)) { self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox)) } return featurePipeline.GetFeaturesWithin(layerId, bbox) }, - memberships: featurePipeline.relationTracker + memberships: featurePipeline.relationTracker, } - this.isDirty.stabilized(100).addCallback(dirty => { + this.isDirty.stabilized(100).addCallback((dirty) => { if (dirty) { self.updateMetaTags() } }) - this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true)) - + this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true)) } public requestUpdate() { @@ -57,56 +60,58 @@ class MetatagUpdater { this.isDirty.setData(false) return } - MetaTagging.addMetatags( - features, - this.params, - this.source.layer.layerDef, - this.state) + MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state) this.isDirty.setData(false) - } - } export default class MetaTagRecalculator { private _state: { allElements?: ElementStorage - }; - private _featurePipeline: FeaturePipeline; - private readonly _alreadyRegistered: Set = new Set() + } + private _featurePipeline: FeaturePipeline + private readonly _alreadyRegistered: Set = new Set< + FeatureSourceForLayer & Tiled + >() private readonly _notifiers: MetatagUpdater[] = [] /** * The meta tag recalculator receives tiles of layers via the 'registerSource'-function. * It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded */ - constructor(state: { allElements?: ElementStorage, currentView: FeatureSourceForLayer & Tiled }, featurePipeline: FeaturePipeline) { - this._featurePipeline = featurePipeline; - this._state = state; - - if(state.currentView !== undefined){ - const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline) - this._alreadyRegistered.add(state.currentView) - this._notifiers.push(currentViewUpdater) - state.currentView.features.addCallback(_ => { - console.debug("Requesting an update for currentView") - currentViewUpdater.updateMetaTags(); - }) - } + constructor( + state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled }, + featurePipeline: FeaturePipeline + ) { + this._featurePipeline = featurePipeline + this._state = state + if (state.currentView !== undefined) { + const currentViewUpdater = new MetatagUpdater( + state.currentView, + this._state, + this._featurePipeline + ) + this._alreadyRegistered.add(state.currentView) + this._notifiers.push(currentViewUpdater) + state.currentView.features.addCallback((_) => { + console.debug("Requesting an update for currentView") + currentViewUpdater.updateMetaTags() + }) + } } public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) { if (source === undefined) { - return; + return } if (this._alreadyRegistered.has(source)) { - return; + return } this._alreadyRegistered.add(source) this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline)) - const self = this; - source.features.addCallbackAndRunD(_ => { + const self = this + source.features.addCallbackAndRunD((_) => { const layerName = source.layer.layerDef.id for (const updater of self._notifiers) { const neededBbox = updater.neededLayerBboxes.get(layerName) @@ -118,7 +123,5 @@ export default class MetaTagRecalculator { } } }) - } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts index d7b3768d1..01ad3917b 100644 --- a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts +++ b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts @@ -1,22 +1,21 @@ -import FeatureSource from "../FeatureSource"; -import {Store} from "../../UIEventSource"; -import {ElementStorage} from "../../ElementStorage"; +import FeatureSource from "../FeatureSource" +import { Store } from "../../UIEventSource" +import { ElementStorage } from "../../ElementStorage" /** * Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved */ export default class RegisteringAllFromFeatureSourceActor { - public readonly features: Store<{ feature: any; freshness: Date }[]>; - public readonly name; + public readonly features: Store<{ feature: any; freshness: Date }[]> + public readonly name constructor(source: FeatureSource, allElements: ElementStorage) { - this.features = source.features; - this.name = "RegisteringSource of " + source.name; - this.features.addCallbackAndRunD(features => { + this.features = source.features + this.name = "RegisteringSource of " + source.name + this.features.addCallbackAndRunD((features) => { for (const feature of features) { allElements.addOrGetElement(feature.feature) } }) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts index 360df9d42..4494bc4bb 100644 --- a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts +++ b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts @@ -1,12 +1,12 @@ -import FeatureSource, {Tiled} from "../FeatureSource"; -import {Tiles} from "../../../Models/TileRange"; -import {IdbLocalStorage} from "../../Web/IdbLocalStorage"; -import {UIEventSource} from "../../UIEventSource"; -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; -import {BBox} from "../../BBox"; -import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import Loc from "../../../Models/Loc"; +import FeatureSource, { Tiled } from "../FeatureSource" +import { Tiles } from "../../../Models/TileRange" +import { IdbLocalStorage } from "../../Web/IdbLocalStorage" +import { UIEventSource } from "../../UIEventSource" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { BBox } from "../../BBox" +import SimpleFeatureSource from "../Sources/SimpleFeatureSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import Loc from "../../../Models/Loc" /*** * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run @@ -15,20 +15,23 @@ import Loc from "../../../Models/Loc"; */ export default class SaveTileToLocalStorageActor { private readonly visitedTiles: UIEventSource> - private readonly _layer: LayerConfig; + private readonly _layer: LayerConfig private readonly _flayer: FilteredLayer private readonly initializeTime = new Date() constructor(layer: FilteredLayer) { this._flayer = layer this._layer = layer.layerDef - this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, - {defaultValue: new Map(),}) - this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => { + this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, { + defaultValue: new Map(), + }) + this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => { for (const key of Array.from(tiles.keys())) { const tileFreshness = tiles.get(key) - const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache + const toOld = + this.initializeTime.getTime() - tileFreshness.getTime() > + 1000 * this._layer.maxAgeOfCache if (toOld) { // Purge this tile this.SetIdb(key, undefined) @@ -37,27 +40,28 @@ export default class SaveTileToLocalStorageActor { } } this.visitedTiles.ping() - return true; + return true }) } - - public LoadTilesFromDisk(currentBounds: UIEventSource, location: UIEventSource, - registerFreshness: (tileId: number, freshness: Date) => void, - registerTile: ((src: FeatureSource & Tiled) => void)) { - const self = this; + public LoadTilesFromDisk( + currentBounds: UIEventSource, + location: UIEventSource, + registerFreshness: (tileId: number, freshness: Date) => void, + registerTile: (src: FeatureSource & Tiled) => void + ) { + const self = this const loadedTiles = new Set() - this.visitedTiles.addCallbackD(tiles => { + this.visitedTiles.addCallbackD((tiles) => { if (tiles.size === 0) { // We don't do anything yet as probably not yet loaded from disk // We'll unregister later on - return; + return } - currentBounds.addCallbackAndRunD(bbox => { - + currentBounds.addCallbackAndRunD((bbox) => { if (self._layer.minzoomVisible > location.data.zoom) { // Not enough zoom - return; + return } // Iterate over all available keys in the local storage, check which are needed and fresh enough @@ -71,32 +75,35 @@ export default class SaveTileToLocalStorageActor { registerFreshness(key, tileFreshness) const tileBbox = BBox.fromTileIndex(key) if (!bbox.overlapsWith(tileBbox)) { - continue; + continue } if (loadedTiles.has(key)) { // Already loaded earlier continue } loadedTiles.add(key) - this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => { - if(features === undefined){ - return; + this.GetIdb(key).then((features: { feature: any; freshness: Date }[]) => { + if (features === undefined) { + return } console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk") - const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features)) + const src = new SimpleFeatureSource( + self._flayer, + key, + new UIEventSource<{ feature: any; freshness: Date }[]>(features) + ) registerTile(src) }) } }) - return true; // Remove the callback - + return true // Remove the callback }) } public addTile(tile: FeatureSource & Tiled) { const self = this - tile.features.addCallbackAndRunD(features => { + tile.features.addCallbackAndRunD((features) => { const now = new Date() if (features.length > 0) { @@ -109,11 +116,10 @@ export default class SaveTileToLocalStorageActor { public poison(lon: number, lat: number) { for (let z = 0; z < 25; z++) { - const {x, y} = Tiles.embedded_tile(lat, lon, z) + const { x, y } = Tiles.embedded_tile(lat, lon, z) const tileId = Tiles.tile_index(z, x, y) this.visitedTiles.data.delete(tileId) } - } public MarkVisited(tileId: number, freshness: Date) { @@ -125,11 +131,18 @@ export default class SaveTileToLocalStorageActor { try { IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) } catch (e) { - console.error("Could not save tile to indexed-db: ", e, "tileIndex is:", tileIndex, "for layer", this._layer.id) + console.error( + "Could not save tile to indexed-db: ", + e, + "tileIndex is:", + tileIndex, + "for layer", + this._layer.id + ) } } private GetIdb(tileIndex) { return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex) } -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 09f88853e..e6f4a5c05 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -1,34 +1,33 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import FilteringFeatureSource from "./Sources/FilteringFeatureSource"; -import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"; -import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource"; -import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; -import {Store, UIEventSource} from "../UIEventSource"; -import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; -import RememberingSource from "./Sources/RememberingSource"; -import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; -import GeoJsonSource from "./Sources/GeoJsonSource"; -import Loc from "../../Models/Loc"; -import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"; -import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"; -import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"; -import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger"; -import RelationsTracker from "../Osm/RelationsTracker"; -import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource"; -import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"; -import {BBox} from "../BBox"; -import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"; -import {Tiles} from "../../Models/TileRange"; -import TileFreshnessCalculator from "./TileFreshnessCalculator"; -import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"; -import MapState from "../State/MapState"; -import {ElementStorage} from "../ElementStorage"; -import {OsmFeature} from "../../Models/OsmFeature"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {FilterState} from "../../Models/FilteredLayer"; -import {GeoOperations} from "../GeoOperations"; -import {Utils} from "../../Utils"; - +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import FilteringFeatureSource from "./Sources/FilteringFeatureSource" +import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter" +import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource" +import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource" +import { Store, UIEventSource } from "../UIEventSource" +import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy" +import RememberingSource from "./Sources/RememberingSource" +import OverpassFeatureSource from "../Actors/OverpassFeatureSource" +import GeoJsonSource from "./Sources/GeoJsonSource" +import Loc from "../../Models/Loc" +import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor" +import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor" +import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" +import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger" +import RelationsTracker from "../Osm/RelationsTracker" +import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource" +import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator" +import { BBox } from "../BBox" +import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" +import { Tiles } from "../../Models/TileRange" +import TileFreshnessCalculator from "./TileFreshnessCalculator" +import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource" +import MapState from "../State/MapState" +import { ElementStorage } from "../ElementStorage" +import { OsmFeature } from "../../Models/OsmFeature" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { FilterState } from "../../Models/FilteredLayer" +import { GeoOperations } from "../GeoOperations" +import { Utils } from "../../Utils" /** * The features pipeline ties together a myriad of various datasources: @@ -42,12 +41,12 @@ import {Utils} from "../../Utils"; * */ export default class FeaturePipeline { - - public readonly sufficientlyZoomed: Store; - public readonly runningQuery: Store; - public readonly timeout: UIEventSource; + public readonly sufficientlyZoomed: Store + public readonly runningQuery: Store + public readonly timeout: UIEventSource public readonly somethingLoaded: UIEventSource = new UIEventSource(false) - public readonly newDataLoadedSignal: UIEventSource = new UIEventSource(undefined) + public readonly newDataLoadedSignal: UIEventSource = + new UIEventSource(undefined) public readonly relationTracker: RelationsTracker /** * Keeps track of all raw OSM-nodes. @@ -55,19 +54,19 @@ export default class FeaturePipeline { */ public readonly fullNodeDatabase?: FullNodeDatabaseSource private readonly overpassUpdater: OverpassFeatureSource - private state: MapState; - private readonly perLayerHierarchy: Map; + private state: MapState + private readonly perLayerHierarchy: Map /** * Keeps track of the age of the loaded data. * Has one freshness-Calculator for every layer * @private */ - private readonly freshnesses = new Map(); - private readonly oldestAllowedDate: Date; + private readonly freshnesses = new Map() + private readonly oldestAllowedDate: Date private readonly osmSourceZoomLevel private readonly localStorageSavers = new Map() - - private readonly newGeometryHandler : NewGeometryFromChangesFeatureSource; + + private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource constructor( handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void, @@ -77,33 +76,40 @@ export default class FeaturePipeline { handleRawFeatureSource: (source: FeatureSourceForLayer) => void } ) { - this.state = state; + this.state = state const self = this - const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? []) - this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds); - this.osmSourceZoomLevel = state.osmApiTileSize.data; - const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12)) + const expiryInSeconds = Math.min( + ...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? []) + ) + this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds) + this.osmSourceZoomLevel = state.osmApiTileSize.data + const useOsmApi = state.locationControl.map( + (l) => l.zoom > (state.overpassMaxZoom.data ?? 12) + ) this.relationTracker = new RelationsTracker() - state.changes.allChanges.addCallbackAndRun(allChanges => { - allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined) - .map(ch => ch.changes) - .filter(coor => coor["lat"] !== undefined && coor["lon"] !== undefined) - .forEach(coor => { - state.layoutToUse.layers.forEach(l => self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])) + state.changes.allChanges.addCallbackAndRun((allChanges) => { + allChanges + .filter((ch) => ch.id < 0 && ch.changes !== undefined) + .map((ch) => ch.changes) + .filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined) + .forEach((coor) => { + state.layoutToUse.layers.forEach((l) => + self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]) + ) }) }) - - this.sufficientlyZoomed = state.locationControl.map(location => { - if (location?.zoom === undefined) { - return false; - } - let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18)); - return location.zoom >= minzoom; + this.sufficientlyZoomed = state.locationControl.map((location) => { + if (location?.zoom === undefined) { + return false } - ); + let minzoom = Math.min( + ...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18) + ) + return location.zoom >= minzoom + }) const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) @@ -111,9 +117,11 @@ export default class FeaturePipeline { this.perLayerHierarchy = perLayerHierarchy // Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource' - function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) { + function patchedHandleFeatureSource( + src: FeatureSourceForLayer & IndexedFeatureSource & Tiled + ) { // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile - const withChanges = new ChangeGeometryApplicator(src, state.changes); + const withChanges = new ChangeGeometryApplicator(src, state.changes) const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges) handleFeatureSource(srcFiltered) @@ -127,31 +135,29 @@ export default class FeaturePipeline { function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) { // Passthrough to passed function, except that it registers as well handleFeatureSource(src) - src.features.addCallbackAndRunD(fs => { - fs.forEach(ff => state.allElements.addOrGetElement(ff.feature)) + src.features.addCallbackAndRunD((fs) => { + fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature)) }) } - for (const filteredLayer of state.filteredLayers.data) { const id = filteredLayer.layerDef.id const source = filteredLayer.layerDef.source - const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile)) + const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => + patchedHandleFeatureSource(tile) + ) perLayerHierarchy.set(id, hierarchy) this.freshnesses.set(id, new TileFreshnessCalculator()) if (id === "type_node") { - - this.fullNodeDatabase = new FullNodeDatabaseSource( - filteredLayer, - tile => { - new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) - tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) - }); - continue; + this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => { + new RegisteringAllFromFeatureSourceActor(tile, state.allElements) + perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) + tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) + }) + continue } if (id === "gps_location") { @@ -187,13 +193,15 @@ export default class FeaturePipeline { // We load the cached values and register them // Getting data from upstream happens a bit lower localTileSaver.LoadTilesFromDisk( - state.currentBounds, state.locationControl, - (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), + state.currentBounds, + state.locationControl, + (tileIndex, freshness) => + self.freshnesses.get(id).addTileLoad(tileIndex, freshness), (tile) => { console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") new RegisteringAllFromFeatureSourceActor(tile, state.allElements) - hierarchy.registerTile(tile); - tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) + hierarchy.registerTile(tile) + tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) } ) @@ -213,47 +221,48 @@ export default class FeaturePipeline { registerTile: (tile) => { new RegisteringAllFromFeatureSourceActor(tile, state.allElements) perLayerHierarchy.get(id).registerTile(tile) - tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) - } + tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) + }, }) } else { new RegisteringAllFromFeatureSourceActor(src, state.allElements) perLayerHierarchy.get(id).registerTile(src) - src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src)) + src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src)) } } else { new DynamicGeoJsonTileSource( filteredLayer, - tile => { + (tile) => { new RegisteringAllFromFeatureSourceActor(tile, state.allElements) perLayerHierarchy.get(id).registerTile(tile) - tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) + tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) }, state ) } } - const osmFeatureSource = new OsmFeatureSource({ isActive: useOsmApi, neededTiles: neededTilesFromOsm, - handleTile: tile => { + handleTile: (tile) => { new RegisteringAllFromFeatureSourceActor(tile, state.allElements) if (tile.layer.layerDef.maxAgeOfCache > 0) { const saver = self.localStorageSavers.get(tile.layer.layerDef.id) if (saver === undefined) { - console.error("No localStorageSaver found for layer ", tile.layer.layerDef.id) + console.error( + "No localStorageSaver found for layer ", + tile.layer.layerDef.id + ) } saver?.addTile(tile) } perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) - tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) - + tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile)) }, state: state, markTileVisited: (tileId) => - state.filteredLayers.data.forEach(flayer => { + state.filteredLayers.data.forEach((flayer) => { const layer = flayer.layerDef if (layer.maxAgeOfCache > 0) { const saver = self.localStorageSavers.get(layer.id) @@ -264,110 +273,128 @@ export default class FeaturePipeline { } } self.freshnesses.get(layer.id).addTileLoad(tileId, new Date()) - }) + }), }) if (this.fullNodeDatabase !== undefined) { - osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId)) + osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => + this.fullNodeDatabase.handleOsmJson(osmJson, tileId) + ) } - const updater = this.initOverpassUpdater(state, useOsmApi) - this.overpassUpdater = updater; + this.overpassUpdater = updater this.timeout = updater.timeout // Actually load data from the overpass source - new PerLayerFeatureSourceSplitter(state.filteredLayers, - (source) => TiledFeatureSource.createHierarchy(source, { - layer: source.layer, - minZoomLevel: source.layer.layerDef.minzoom, - noDuplicates: true, - maxFeatureCount: state.layoutToUse.clustering.minNeededElements, - maxZoomLevel: state.layoutToUse.clustering.maxZoom, - registerTile: (tile) => { - // We save the tile data for the given layer to local storage - data sourced from overpass - self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) - perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile)) - tile.features.addCallbackAndRunD(f => { - if (f.length === 0) { - return - } - self.onNewDataLoaded(tile) - }) - - } - }), + new PerLayerFeatureSourceSplitter( + state.filteredLayers, + (source) => + TiledFeatureSource.createHierarchy(source, { + layer: source.layer, + minZoomLevel: source.layer.layerDef.minzoom, + noDuplicates: true, + maxFeatureCount: state.layoutToUse.clustering.minNeededElements, + maxZoomLevel: state.layoutToUse.clustering.maxZoom, + registerTile: (tile) => { + // We save the tile data for the given layer to local storage - data sourced from overpass + self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) + perLayerHierarchy + .get(source.layer.layerDef.id) + .registerTile(new RememberingSource(tile)) + tile.features.addCallbackAndRunD((f) => { + if (f.length === 0) { + return + } + self.onNewDataLoaded(tile) + }) + }, + }), updater, { handleLeftovers: (leftOvers) => { console.warn("Overpass returned a few non-matched features:", leftOvers) - } - }) + }, + } + ) - - // Also load points/lines that are newly added. - const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes, state.allElements, state.osmConnection._oauth_config.url) - this.newGeometryHandler = newGeometry; - newGeometry.features.addCallbackAndRun(geometries => { + // Also load points/lines that are newly added. + const newGeometry = new NewGeometryFromChangesFeatureSource( + state.changes, + state.allElements, + state.osmConnection._oauth_config.url + ) + this.newGeometryHandler = newGeometry + newGeometry.features.addCallbackAndRun((geometries) => { console.debug("New geometries are:", geometries) }) new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements) // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next - new PerLayerFeatureSourceSplitter(state.filteredLayers, + new PerLayerFeatureSourceSplitter( + state.filteredLayers, (perLayer) => { // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) // AT last, we always apply the metatags whenever possible - perLayer.features.addCallbackAndRunD(_ => { - self.onNewDataLoaded(perLayer); + perLayer.features.addCallbackAndRunD((_) => { + self.onNewDataLoaded(perLayer) }) - }, newGeometry, { handleLeftovers: (leftOvers) => { console.warn("Got some leftovers from the filteredLayers: ", leftOvers) - } + }, } ) this.runningQuery = updater.runningQuery.map( - overpass => { - console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,", - "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle") - return overpass || osmFeatureSource.isRunning.data; - }, [osmFeatureSource.isRunning] + (overpass) => { + console.log( + "FeaturePipeline: runningQuery state changed: Overpass", + overpass ? "is querying," : "is idle,", + "osmFeatureSource is", + osmFeatureSource.isRunning + ? "is running and needs " + + neededTilesFromOsm.data?.length + + " tiles (already got " + + osmFeatureSource.downloadedTiles.size + + " tiles )" + : "is idle" + ) + return overpass || osmFeatureSource.isRunning.data + }, + [osmFeatureSource.isRunning] ) - } public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] { const self = this const tiles: OsmFeature[][] = [] - Array.from(this.perLayerHierarchy.keys()) - .forEach(key => { - const fetched : OsmFeature[][] = self.GetFeaturesWithin(key, bbox) - tiles.push(...fetched); - }) - return tiles; + Array.from(this.perLayerHierarchy.keys()).forEach((key) => { + const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox) + tiles.push(...fetched) + }) + return tiles } - public GetAllFeaturesAndMetaWithin(bbox: BBox, layerIdWhitelist?: Set): - {features: OsmFeature[], layer: string}[] { + public GetAllFeaturesAndMetaWithin( + bbox: BBox, + layerIdWhitelist?: Set + ): { features: OsmFeature[]; layer: string }[] { const self = this - const tiles :{features: any[], layer: string}[]= [] - Array.from(this.perLayerHierarchy.keys()) - .forEach(key => { - if(layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)){ - return; - } - return tiles.push({ - layer: key, - features: [].concat(...self.GetFeaturesWithin(key, bbox)) - }); + const tiles: { features: any[]; layer: string }[] = [] + Array.from(this.perLayerHierarchy.keys()).forEach((key) => { + if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) { + return + } + return tiles.push({ + layer: key, + features: [].concat(...self.GetFeaturesWithin(key, bbox)), }) - return tiles; + }) + return tiles } /** @@ -380,16 +407,24 @@ export default class FeaturePipeline { } const requestedHierarchy = this.perLayerHierarchy.get(layerId) if (requestedHierarchy === undefined) { - console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys())) - return undefined; + console.warn( + "Layer ", + layerId, + "is not defined. Try one of ", + Array.from(this.perLayerHierarchy.keys()) + ) + return undefined } return TileHierarchyTools.getTiles(requestedHierarchy, bbox) - .filter(featureSource => featureSource.features?.data !== undefined) - .map(featureSource => featureSource.features.data.map(fs => fs.feature)) + .filter((featureSource) => featureSource.features?.data !== undefined) + .map((featureSource) => featureSource.features.data.map((fs) => fs.feature)) } - public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) { - Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => { + public GetTilesPerLayerWithin( + bbox: BBox, + handleTile: (tile: FeatureSourceForLayer & Tiled) => void + ) { + Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => { TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) }) } @@ -399,16 +434,16 @@ export default class FeaturePipeline { } private freshnessForVisibleLayers(z: number, x: number, y: number): Date { - let oldestDate = undefined; + let oldestDate = undefined for (const flayer of this.state.filteredLayers.data) { if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) { continue } if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { - continue; + continue } if (flayer.layerDef.maxAgeOfCache === 0) { - return undefined; + return undefined } const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) if (freshnessCalc === undefined) { @@ -428,117 +463,136 @@ export default class FeaturePipeline { } /* - * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM - * */ + * Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM + * */ private getNeededTilesFromOsm(isSufficientlyZoomed: Store): Store { const self = this - return this.state.currentBounds.map(bbox => { - if (bbox === undefined) { - return [] - } - if (!isSufficientlyZoomed.data) { - return []; - } - const osmSourceZoomLevel = self.osmSourceZoomLevel - const range = bbox.containingTileRange(osmSourceZoomLevel) - const tileIndexes = [] - if (range.total >= 100) { - // Too much tiles! - return undefined - } - Tiles.MapRange(range, (x, y) => { - const i = Tiles.tile_index(osmSourceZoomLevel, x, y); - const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y) - if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { - console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available") - // The cached tiles contain decently fresh data - return undefined; + return this.state.currentBounds.map( + (bbox) => { + if (bbox === undefined) { + return [] } - tileIndexes.push(i) - }) - return tileIndexes - }, [isSufficientlyZoomed]) + if (!isSufficientlyZoomed.data) { + return [] + } + const osmSourceZoomLevel = self.osmSourceZoomLevel + const range = bbox.containingTileRange(osmSourceZoomLevel) + const tileIndexes = [] + if (range.total >= 100) { + // Too much tiles! + return undefined + } + Tiles.MapRange(range, (x, y) => { + const i = Tiles.tile_index(osmSourceZoomLevel, x, y) + const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y) + if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) { + console.debug( + "Skipping tile", + osmSourceZoomLevel, + x, + y, + "as a decently fresh one is available" + ) + // The cached tiles contain decently fresh data + return undefined + } + tileIndexes.push(i) + }) + return tileIndexes + }, + [isSufficientlyZoomed] + ) } - private initOverpassUpdater(state: { - allElements: ElementStorage; - layoutToUse: LayoutConfig, - currentBounds: Store, - locationControl: Store, - readonly overpassUrl: Store; - readonly overpassTimeout: Store; - readonly overpassMaxZoom: Store, - }, useOsmApi: Store): OverpassFeatureSource { - const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) - const overpassIsActive = state.currentBounds.map(bbox => { - if (bbox === undefined) { - console.debug("Disabling overpass source: no bbox") - return false - } - let zoom = state.locationControl.data.zoom - if (zoom < minzoom) { - // We are zoomed out over the zoomlevel of any layer - console.debug("Disabling overpass source: zoom < minzoom") - return false; - } - - const range = bbox.containingTileRange(zoom) - if (range.total >= 5000) { - // Let's assume we don't have so much data cached - return true - } - const self = this; - const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y)) - return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate) - }, [state.locationControl]) - - const self = this; - const updater = new OverpassFeatureSource(state, - { - padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)), - relationTracker: this.relationTracker, - isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]), - freshnesses: this.freshnesses, - onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => { - Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => { - const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y) - downloadedLayers.forEach(layer => { - self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) - self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) - }) - }) - + private initOverpassUpdater( + state: { + allElements: ElementStorage + layoutToUse: LayoutConfig + currentBounds: Store + locationControl: Store + readonly overpassUrl: Store + readonly overpassTimeout: Store + readonly overpassMaxZoom: Store + }, + useOsmApi: Store + ): OverpassFeatureSource { + const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom)) + const overpassIsActive = state.currentBounds.map( + (bbox) => { + if (bbox === undefined) { + console.debug("Disabling overpass source: no bbox") + return false + } + let zoom = state.locationControl.data.zoom + if (zoom < minzoom) { + // We are zoomed out over the zoomlevel of any layer + console.debug("Disabling overpass source: zoom < minzoom") + return false } - }); + const range = bbox.containingTileRange(zoom) + if (range.total >= 5000) { + // Let's assume we don't have so much data cached + return true + } + const self = this + const allFreshnesses = Tiles.MapRange(range, (x, y) => + self.freshnessForVisibleLayers(zoom, x, y) + ) + return allFreshnesses.some( + (freshness) => freshness === undefined || freshness < this.oldestAllowedDate + ) + }, + [state.locationControl] + ) + + const self = this + const updater = new OverpassFeatureSource(state, { + padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)), + relationTracker: this.relationTracker, + isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]), + freshnesses: this.freshnesses, + onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => { + Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => { + const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y) + downloadedLayers.forEach((layer) => { + self.freshnesses.get(layer.id).addTileLoad(tileIndex, date) + self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date) + }) + }) + }, + }) // Register everything in the state' 'AllElements' new RegisteringAllFromFeatureSourceActor(updater, state.allElements) - return updater; + return updater } /** * Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters */ - public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] { + public getAllVisibleElementsWithmeta( + bbox: BBox + ): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] { if (bbox === undefined) { console.warn("No bbox") return [] } const layers = Utils.toIdRecord(this.state.layoutToUse.layers) - const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox) + const elementsWithMeta: { features: OsmFeature[]; layer: string }[] = + this.GetAllFeaturesAndMetaWithin(bbox) - let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = [] + let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = [] let seenElements = new Set() for (const elementsWithMetaElement of elementsWithMeta) { const layer = layers[elementsWithMetaElement.layer] - if(layer.title === undefined){ + if (layer.title === undefined) { continue } - const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer); + const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer) for (let i = 0; i < elementsWithMetaElement.features.length; i++) { - const element = elementsWithMetaElement.features[i]; + const element = elementsWithMetaElement.features[i] if (!filtered.isDisplayed.data) { continue } @@ -552,35 +606,38 @@ export default class FeaturePipeline { if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) { continue } - const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values()); - if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) { + const activeFilters: FilterState[] = Array.from( + filtered.appliedFilters.data.values() + ) + if ( + !activeFilters.every( + (filter) => + filter?.currentFilter === undefined || + filter?.currentFilter?.matchesProperties(element.properties) + ) + ) { continue } - const center = GeoOperations.centerpointCoordinates(element); + const center = GeoOperations.centerpointCoordinates(element) elements.push({ element, center, layer: layers[elementsWithMetaElement.layer], }) - } } - - - - return elements; + return elements } - /** - * Inject a new point + * Inject a new point */ InjectNewPoint(geojson) { this.newGeometryHandler.features.data.push({ feature: geojson, - freshness: new Date() + freshness: new Date(), }) - this.newGeometryHandler.features.ping(); + this.newGeometryHandler.features.ping() } -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index f28d2cde9..c5460ff68 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -1,19 +1,19 @@ -import {Store, UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {BBox} from "../BBox"; -import {Feature, Geometry} from "@turf/turf"; -import {OsmFeature} from "../../Models/OsmFeature"; +import { Store, UIEventSource } from "../UIEventSource" +import FilteredLayer from "../../Models/FilteredLayer" +import { BBox } from "../BBox" +import { Feature, Geometry } from "@turf/turf" +import { OsmFeature } from "../../Models/OsmFeature" export default interface FeatureSource { - features: Store<{ feature: OsmFeature, freshness: Date }[]>; + features: Store<{ feature: OsmFeature; freshness: Date }[]> /** * Mainly used for debuging */ - name: string; + name: string } export interface Tiled { - tileIndex: number, + tileIndex: number bbox: BBox } diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index f19e917c6..6b6bfd330 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -1,8 +1,7 @@ -import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; -import {Store} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; -import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; - +import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource" +import { Store } from "../UIEventSource" +import FilteredLayer from "../../Models/FilteredLayer" +import SimpleFeatureSource from "./Sources/SimpleFeatureSource" /** * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) @@ -10,30 +9,30 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; * In any case, this featureSource marks the objects with _matching_layer_id */ export default class PerLayerFeatureSourceSplitter { - - constructor(layers: Store, - handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, - upstream: FeatureSource, - options?: { - tileIndex?: number, - handleLeftovers?: (featuresWithoutLayer: any[]) => void - }) { - + constructor( + layers: Store, + handleLayerData: (source: FeatureSourceForLayer & Tiled) => void, + upstream: FeatureSource, + options?: { + tileIndex?: number + handleLeftovers?: (featuresWithoutLayer: any[]) => void + } + ) { const knownLayers = new Map() function update() { - const features = upstream.features?.data; + const features = upstream.features?.data if (features === undefined) { - return; + return } if (layers.data === undefined || layers.data.length === 0) { - return; + return } // We try to figure out (for each feature) in which feature store it should be saved. // Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go - const featuresPerLayer = new Map(); + const featuresPerLayer = new Map() const noLayerFound = [] for (const layer of layers.data) { @@ -41,19 +40,19 @@ export default class PerLayerFeatureSourceSplitter { } for (const f of features) { - let foundALayer = false; + let foundALayer = false for (const layer of layers.data) { if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { // We have found our matching layer! featuresPerLayer.get(layer.layerDef.id).push(f) - foundALayer = true; + foundALayer = true if (!layer.layerDef.passAllFeatures) { // If not 'passAllFeatures', we are done for this feature - break + break } } } - if(!foundALayer){ + if (!foundALayer) { noLayerFound.push(f) } } @@ -61,11 +60,11 @@ export default class PerLayerFeatureSourceSplitter { // At this point, we have our features per layer as a list // We assign them to the correct featureSources for (const layer of layers.data) { - const id = layer.layerDef.id; + const id = layer.layerDef.id const features = featuresPerLayer.get(id) if (features === undefined) { // No such features for this layer - continue; + continue } let featureSource = knownLayers.get(id) @@ -86,7 +85,7 @@ export default class PerLayerFeatureSourceSplitter { } } - layers.addCallback(_ => update()) - upstream.features.addCallbackAndRunD(_ => update()) + layers.addCallback((_) => update()) + upstream.features.addCallbackAndRunD((_) => update()) } -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts b/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts index d7d6c8b7a..0bac2ab6d 100644 --- a/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts +++ b/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts @@ -1,52 +1,52 @@ /** * Applies geometry changes from 'Changes' onto every feature of a featureSource */ -import {Changes} from "../../Osm/Changes"; -import {UIEventSource} from "../../UIEventSource"; -import {FeatureSourceForLayer, IndexedFeatureSource} from "../FeatureSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/ChangeDescription"; - +import { Changes } from "../../Osm/Changes" +import { UIEventSource } from "../../UIEventSource" +import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" export default class ChangeGeometryApplicator implements FeatureSourceForLayer { - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name: string; + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource<{ feature: any; freshness: Date }[]>([]) + public readonly name: string public readonly layer: FilteredLayer - private readonly source: IndexedFeatureSource; - private readonly changes: Changes; + private readonly source: IndexedFeatureSource + private readonly changes: Changes - constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) { - this.source = source; - this.changes = changes; + constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { + this.source = source + this.changes = changes this.layer = source.layer this.name = "ChangesApplied(" + source.name + ")" this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined) - const self = this; - source.features.addCallbackAndRunD(_ => self.update()) - - changes.allChanges.addCallbackAndRunD(_ => self.update()) + const self = this + source.features.addCallbackAndRunD((_) => self.update()) + changes.allChanges.addCallbackAndRunD((_) => self.update()) } private update() { const upstreamFeatures = this.source.features.data const upstreamIds = this.source.containedIds.data - const changesToApply = this.changes.allChanges.data - ?.filter(ch => + const changesToApply = this.changes.allChanges.data?.filter( + (ch) => // Does upsteram have this element? If not, we skip upstreamIds.has(ch.type + "/" + ch.id) && // Are any (geometry) changes defined? ch.changes !== undefined && // Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource - ch.id > 0) + ch.id > 0 + ) if (changesToApply === undefined || changesToApply.length === 0) { // No changes to apply! // Pass the original feature and lets continue our day - this.features.setData(upstreamFeatures); - return; + this.features.setData(upstreamFeatures) + return } const changesPerId = new Map() @@ -58,27 +58,32 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { changesPerId.set(key, [ch]) } } - const newFeatures: { feature: any, freshness: Date }[] = [] + const newFeatures: { feature: any; freshness: Date }[] = [] for (const feature of upstreamFeatures) { const changesForFeature = changesPerId.get(feature.feature.properties.id) if (changesForFeature === undefined) { // No changes for this element newFeatures.push(feature) - continue; + continue } // Allright! We have a feature to rewrite! const copy = { - ...feature + ...feature, } // We only apply the last change as that one'll have the latest geometry const change = changesForFeature[changesForFeature.length - 1] copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) - console.log("Applying a geometry change onto:", feature,"The change is:", change,"which becomes:", copy) + console.log( + "Applying a geometry change onto:", + feature, + "The change is:", + change, + "which becomes:", + copy + ) newFeatures.push(copy) } this.features.setData(newFeatures) - } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index cf621aca3..248a9d9d9 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,99 +1,112 @@ -import {UIEventSource} from "../../UIEventSource"; -import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; +import { UIEventSource } from "../../UIEventSource" +import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" - -export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource { - - public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name; +export default class FeatureSourceMerger + implements FeatureSourceForLayer, Tiled, IndexedFeatureSource +{ + public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< + { feature: any; freshness: Date }[] + >([]) + public readonly name public readonly layer: FilteredLayer - public readonly tileIndex: number; - public readonly bbox: BBox; - public readonly containedIds: UIEventSource> = new UIEventSource>(new Set()) - private readonly _sources: UIEventSource; + public readonly tileIndex: number + public readonly bbox: BBox + public readonly containedIds: UIEventSource> = new UIEventSource>( + new Set() + ) + private readonly _sources: UIEventSource /** * Merges features from different featureSources for a single layer * Uses the freshest feature available in the case multiple sources offer data with the same identifier */ - constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource) { - this.tileIndex = tileIndex; - this.bbox = bbox; - this._sources = sources; - this.layer = layer; - this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")" - const self = this; + constructor( + layer: FilteredLayer, + tileIndex: number, + bbox: BBox, + sources: UIEventSource + ) { + this.tileIndex = tileIndex + this.bbox = bbox + this._sources = sources + this.layer = layer + this.name = + "FeatureSourceMerger(" + + layer.layerDef.id + + ", " + + Tiles.tile_from_index(tileIndex).join(",") + + ")" + const self = this - const handledSources = new Set(); + const handledSources = new Set() - sources.addCallbackAndRunD(sources => { - let newSourceRegistered = false; + sources.addCallbackAndRunD((sources) => { + let newSourceRegistered = false for (let i = 0; i < sources.length; i++) { - let source = sources[i]; + let source = sources[i] if (handledSources.has(source)) { continue } handledSources.add(source) newSourceRegistered = true source.features.addCallback(() => { - self.Update(); - }); + self.Update() + }) if (newSourceRegistered) { - self.Update(); + self.Update() } } }) - } private Update() { - - let somethingChanged = false; - const all: Map = new Map(); + let somethingChanged = false + const all: Map = new Map< + string, + { feature: any; freshness: Date } + >() // We seed the dictionary with the previously loaded features - const oldValues = this.features.data ?? []; + const oldValues = this.features.data ?? [] for (const oldValue of oldValues) { all.set(oldValue.feature.id, oldValue) } for (const source of this._sources.data) { if (source?.features?.data === undefined) { - continue; + continue } for (const f of source.features.data) { - const id = f.feature.properties.id; + const id = f.feature.properties.id if (!all.has(id)) { // This is a new feature - somethingChanged = true; - all.set(id, f); - continue; + somethingChanged = true + all.set(id, f) + continue } // This value has been seen already, either in a previous run or by a previous datasource // Let's figure out if something changed - const oldV = all.get(id); + const oldV = all.get(id) if (oldV.freshness < f.freshness) { // Jup, this feature is fresher - all.set(id, f); - somethingChanged = true; + all.set(id, f) + somethingChanged = true } } } if (!somethingChanged) { // We don't bother triggering an update - return; + return } - const newList = []; + const newList = [] all.forEach((value, _) => { newList.push(value) }) this.containedIds.setData(new Set(all.keys())) - this.features.setData(newList); + this.features.setData(newList) } - - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index dc97e1d3a..533da5656 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -1,34 +1,35 @@ -import {Store, UIEventSource} from "../../UIEventSource"; -import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {BBox} from "../../BBox"; -import {ElementStorage} from "../../ElementStorage"; -import {TagsFilter} from "../../Tags/TagsFilter"; -import {OsmFeature} from "../../../Models/OsmFeature"; +import { Store, UIEventSource } from "../../UIEventSource" +import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { BBox } from "../../BBox" +import { ElementStorage } from "../../ElementStorage" +import { TagsFilter } from "../../Tags/TagsFilter" +import { OsmFeature } from "../../../Models/OsmFeature" export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { - public features: UIEventSource<{ feature: any; freshness: Date }[]> = - new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name; - public readonly layer: FilteredLayer; + public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource< + { feature: any; freshness: Date }[] + >([]) + public readonly name + public readonly layer: FilteredLayer public readonly tileIndex: number public readonly bbox: BBox - private readonly upstream: FeatureSourceForLayer; + private readonly upstream: FeatureSourceForLayer private readonly state: { - locationControl: Store<{ zoom: number }>; - selectedElement: Store, - globalFilters: Store<{ filter: FilterState }[]>, + locationControl: Store<{ zoom: number }> + selectedElement: Store + globalFilters: Store<{ filter: FilterState }[]> allElements: ElementStorage - }; - private readonly _alreadyRegistered = new Set>(); + } + private readonly _alreadyRegistered = new Set>() private readonly _is_dirty = new UIEventSource(false) - private previousFeatureSet: Set = undefined; + private previousFeatureSet: Set = undefined constructor( state: { - locationControl: Store<{ zoom: number }>, - selectedElement: Store, - allElements: ElementStorage, + locationControl: Store<{ zoom: number }> + selectedElement: Store + allElements: ElementStorage globalFilters: Store<{ filter: FilterState }[]> }, tileIndex, @@ -41,92 +42,95 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti this.upstream = upstream this.state = state - this.layer = upstream.layer; - const layer = upstream.layer; - const self = this; + this.layer = upstream.layer + const layer = upstream.layer + const self = this upstream.features.addCallback(() => { - self.update(); - }); - - - layer.appliedFilters.addCallback(_ => { self.update() }) - this._is_dirty.stabilized(1000).addCallbackAndRunD(dirty => { + layer.appliedFilters.addCallback((_) => { + self.update() + }) + + this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { if (dirty) { self.update() } }) - metataggingUpdated?.addCallback(_ => { + metataggingUpdated?.addCallback((_) => { self._is_dirty.setData(true) }) - - state.globalFilters.addCallback(_ => { + + state.globalFilters.addCallback((_) => { self.update() }) - this.update(); + this.update() } private update() { - const self = this; - const layer = this.upstream.layer; - const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []); - const includedFeatureIds = new Set(); - const globalFilters = self.state.globalFilters.data.map(f => f.filter); + const self = this + const layer = this.upstream.layer + const features: { feature: OsmFeature; freshness: Date }[] = + this.upstream.features.data ?? [] + const includedFeatureIds = new Set() + const globalFilters = self.state.globalFilters.data.map((f) => f.filter) const newFeatures = (features ?? []).filter((f) => { - self.registerCallback(f.feature) - const isShown: TagsFilter = layer.layerDef.isShown; - const tags = f.feature.properties; - if (isShown !== undefined && !isShown.matchesProperties(tags) ) { - return false; + const isShown: TagsFilter = layer.layerDef.isShown + const tags = f.feature.properties + if (isShown !== undefined && !isShown.matchesProperties(tags)) { + return false } const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) for (const filter of tagsFilter) { const neededTags: TagsFilter = filter?.currentFilter - if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { + if ( + neededTags !== undefined && + !neededTags.matchesProperties(f.feature.properties) + ) { // Hidden by the filter on the layer itself - we want to hide it no matter what - return false; + return false } } for (const filter of globalFilters) { const neededTags: TagsFilter = filter?.currentFilter - if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { + if ( + neededTags !== undefined && + !neededTags.matchesProperties(f.feature.properties) + ) { // Hidden by the filter on the layer itself - we want to hide it no matter what - return false; + return false } } includedFeatureIds.add(f.feature.properties.id) - return true; - }); + return true + }) - const previousSet = this.previousFeatureSet; + const previousSet = this.previousFeatureSet this._is_dirty.setData(false) // Is there any difference between the two sets? if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) { // The size of the sets is the same - they _might_ be identical - const newItemFound = Array.from(includedFeatureIds).some(id => !previousSet.has(id)) + const newItemFound = Array.from(includedFeatureIds).some((id) => !previousSet.has(id)) if (!newItemFound) { - // We know that: + // We know that: // - The sets have the same size // - Every item from the new set has been found in the old set // which means they are identical! - return; + return } - } // Something new has been found! - this.features.setData(newFeatures); - + this.features.setData(newFeatures) } private registerCallback(feature: any) { @@ -139,11 +143,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti } this._alreadyRegistered.add(src) - const self = this; + const self = this // Add a callback as a changed tag migh change the filter - src.addCallbackAndRunD(_ => { + src.addCallbackAndRunD((_) => { self._is_dirty.setData(true) }) } - } diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index 3c4e9142d..5c0ecb9e9 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -1,168 +1,163 @@ /** * Fetches a geojson file somewhere and passes it along */ -import {UIEventSource} from "../../UIEventSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {Utils} from "../../../Utils"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; -import {GeoOperations} from "../../GeoOperations"; - +import { UIEventSource } from "../../UIEventSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import { Utils } from "../../../Utils" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" +import { GeoOperations } from "../../GeoOperations" export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { - - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; - public readonly state = new UIEventSource(undefined) - public readonly name; + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> + public readonly state = new UIEventSource(undefined) + public readonly name public readonly isOsmCache: boolean - public readonly layer: FilteredLayer; + public readonly layer: FilteredLayer public readonly tileIndex - public readonly bbox; - private readonly seenids: Set; - private readonly idKey ?: string; - - public constructor(flayer: FilteredLayer, - zxy?: [number, number, number] | BBox, - options?: { - featureIdBlacklist?: Set - }) { + public readonly bbox + private readonly seenids: Set + private readonly idKey?: string + public constructor( + flayer: FilteredLayer, + zxy?: [number, number, number] | BBox, + options?: { + featureIdBlacklist?: Set + } + ) { if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) { throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead" } - this.layer = flayer; + this.layer = flayer this.idKey = flayer.layerDef.source.idKey this.seenids = options?.featureIdBlacklist ?? new Set() - let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); + let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id) if (zxy !== undefined) { - let tile_bbox: BBox; + let tile_bbox: BBox if (zxy instanceof BBox) { - tile_bbox = zxy; + tile_bbox = zxy } else { - const [z, x, y] = zxy; - tile_bbox = BBox.fromTile(z, x, y); + const [z, x, y] = zxy + tile_bbox = BBox.fromTile(z, x, y) this.tileIndex = Tiles.tile_index(z, x, y) this.bbox = BBox.fromTile(z, x, y) url = url - .replace('{z}', "" + z) - .replace('{x}', "" + x) - .replace('{y}', "" + y) + .replace("{z}", "" + z) + .replace("{x}", "" + x) + .replace("{y}", "" + y) } - let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox + let bounds: { minLat: number; maxLat: number; minLon: number; maxLon: number } = + tile_bbox if (this.layer.layerDef.source.mercatorCrs) { bounds = tile_bbox.toMercator() } url = url - .replace('{y_min}', "" + bounds.minLat) - .replace('{y_max}', "" + bounds.maxLat) - .replace('{x_min}', "" + bounds.minLon) - .replace('{x_max}', "" + bounds.maxLon) - - + .replace("{y_min}", "" + bounds.minLat) + .replace("{y_max}", "" + bounds.maxLat) + .replace("{x_min}", "" + bounds.minLon) + .replace("{x_max}", "" + bounds.maxLon) } else { this.tileIndex = Tiles.tile_index(0, 0, 0) - this.bbox = BBox.global; + this.bbox = BBox.global } - this.name = "GeoJsonSource of " + url; + this.name = "GeoJsonSource of " + url - this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; + this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) this.LoadJSONFrom(url) } - private LoadJSONFrom(url: string) { - const eventSource = this.features; - const self = this; + const eventSource = this.features + const self = this Utils.downloadJsonCached(url, 60 * 60) - .then(json => { + .then((json) => { self.state.setData("loaded") // TODO: move somewhere else, just for testing // Check for maproulette data if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) { console.log("MapRoulette data detected") - const data = json; - let maprouletteFeatures: any[] = []; - data.forEach(element => { + const data = json + let maprouletteFeatures: any[] = [] + data.forEach((element) => { maprouletteFeatures.push({ type: "Feature", geometry: { type: "Point", - coordinates: [element.point.lng, element.point.lat] + coordinates: [element.point.lng, element.point.lat], }, properties: { // Map all properties to the feature ...element, - } - }); - }); - json.features = maprouletteFeatures; + }, + }) + }) + json.features = maprouletteFeatures } if (json.features === undefined || json.features === null) { - return; + return } if (self.layer.layerDef.source.mercatorCrs) { json = GeoOperations.GeoJsonToWGS84(json) } - const time = new Date(); - const newFeatures: { feature: any, freshness: Date } [] = [] - let i = 0; - let skipped = 0; + const time = new Date() + const newFeatures: { feature: any; freshness: Date }[] = [] + let i = 0 + let skipped = 0 for (const feature of json.features) { const props = feature.properties for (const key in props) { - - if(props[key] === null){ + if (props[key] === null) { delete props[key] } - + if (typeof props[key] !== "string") { // Make sure all the values are string, it crashes stuff otherwise props[key] = JSON.stringify(props[key]) } } - if(self.idKey !== undefined){ + if (self.idKey !== undefined) { props.id = props[self.idKey] } - + if (props.id === undefined) { - props.id = url + "/" + i; - feature.id = url + "/" + i; - i++; + props.id = url + "/" + i + feature.id = url + "/" + i + i++ } if (self.seenids.has(props.id)) { - skipped++; - continue; + skipped++ + continue } self.seenids.add(props.id) - let freshness: Date = time; + let freshness: Date = time if (feature.properties["_last_edit:timestamp"] !== undefined) { freshness = new Date(props["_last_edit:timestamp"]) } - newFeatures.push({feature: feature, freshness: freshness}) + newFeatures.push({ feature: feature, freshness: freshness }) } if (newFeatures.length == 0) { - return; + return } eventSource.setData(eventSource.data.concat(newFeatures)) - - }).catch(msg => { - console.debug("Could not load geojson layer", url, "due to", msg); - self.state.setData({error: msg}) - }) + }) + .catch((msg) => { + console.debug("Could not load geojson layer", url, "due to", msg) + self.state.setData({ error: msg }) + }) } - } diff --git a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts index 777e639b3..1e44d9a06 100644 --- a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts +++ b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts @@ -1,50 +1,50 @@ -import {Changes} from "../../Osm/Changes"; -import {OsmNode, OsmObject, OsmRelation, OsmWay} from "../../Osm/OsmObject"; -import FeatureSource from "../FeatureSource"; -import {UIEventSource} from "../../UIEventSource"; -import {ChangeDescription} from "../../Osm/Actions/ChangeDescription"; -import {ElementStorage} from "../../ElementStorage"; -import {OsmId, OsmTags} from "../../../Models/OsmFeature"; +import { Changes } from "../../Osm/Changes" +import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" +import FeatureSource from "../FeatureSource" +import { UIEventSource } from "../../UIEventSource" +import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" +import { ElementStorage } from "../../ElementStorage" +import { OsmId, OsmTags } from "../../../Models/OsmFeature" export class NewGeometryFromChangesFeatureSource implements FeatureSource { // This class name truly puts the 'Java' into 'Javascript' /** * A feature source containing exclusively new elements. - * + * * These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too. * Other sources of new points are e.g. imports from nodes */ - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name: string = "newFeatures"; + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource<{ feature: any; freshness: Date }[]>([]) + public readonly name: string = "newFeatures" constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { + const seenChanges = new Set() + const features = this.features.data + const self = this - const seenChanges = new Set(); - const features = this.features.data; - const self = this; - - changes.pendingChanges.stabilized(100).addCallbackAndRunD(changes => { + changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { if (changes.length === 0) { - return; + return } - const now = new Date(); - let somethingChanged = false; + const now = new Date() + let somethingChanged = false function add(feature) { feature.id = feature.properties.id features.push({ feature: feature, - freshness: now + freshness: now, }) - somethingChanged = true; + somethingChanged = true } for (const change of changes) { if (seenChanges.has(change)) { // Already handled - continue; + continue } seenChanges.add(change) @@ -60,35 +60,32 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { // For this, we introspect the change if (allElementStorage.has(change.type + "/" + change.id)) { // The current point already exists, we don't have to do anything here - continue; + continue } console.debug("Detected a reused point") // The 'allElementsStore' does _not_ have this point yet, so we have to create it - OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then(feat => { + OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => { console.log("Got the reused point:", feat) for (const kv of change.tags) { feat.tags[kv.k] = kv.v } - const geojson = feat.asGeoJson(); + const geojson = feat.asGeoJson() allElementStorage.addOrGetElement(geojson) - self.features.data.push({feature: geojson, freshness: new Date()}) + self.features.data.push({ feature: geojson, freshness: new Date() }) self.features.ping() }) continue - - } else if (change.id < 0 && change.changes === undefined) { // The geometry is not described - not a new point if (change.id < 0) { console.error("WARNING: got a new point without geometry!") } - continue; + continue } - try { const tags: OsmTags = { - id: (change.type + "/" + change.id) + id: (change.type + "/" + change.id), } for (const kv of change.tags) { tags[kv.k] = kv.v @@ -104,30 +101,31 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { n.lon = change.changes["lon"] const geojson = n.asGeoJson() add(geojson) - break; + break case "way": const w = new OsmWay(change.id) w.tags = tags w.nodes = change.changes["nodes"] - w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon]) + w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [ + lat, + lon, + ]) add(w.asGeoJson()) - break; + break case "relation": const r = new OsmRelation(change.id) r.tags = tags r.members = change.changes["members"] add(r.asGeoJson()) - break; + break } } catch (e) { console.error("Could not generate a new geometry to render on screen for:", e) } - } if (somethingChanged) { self.features.ping() } }) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts index 344107f07..5d71541c4 100644 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -2,34 +2,36 @@ * Every previously added point is remembered, but new points are added. * Data coming from upstream will always overwrite a previous value */ -import FeatureSource, {Tiled} from "../FeatureSource"; -import {Store, UIEventSource} from "../../UIEventSource"; -import {BBox} from "../../BBox"; +import FeatureSource, { Tiled } from "../FeatureSource" +import { Store, UIEventSource } from "../../UIEventSource" +import { BBox } from "../../BBox" export default class RememberingSource implements FeatureSource, Tiled { - - public readonly features: Store<{ feature: any, freshness: Date }[]>; - public readonly name; + public readonly features: Store<{ feature: any; freshness: Date }[]> + public readonly name public readonly tileIndex: number public readonly bbox: BBox constructor(source: FeatureSource & Tiled) { - const self = this; - this.name = "RememberingSource of " + source.name; + const self = this + this.name = "RememberingSource of " + source.name this.tileIndex = source.tileIndex - this.bbox = source.bbox; + this.bbox = source.bbox - const empty = []; - const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty) + const empty = [] + const featureSource = new UIEventSource<{ feature: any; freshness: Date }[]>(empty) this.features = featureSource - source.features.addCallbackAndRunD(features => { - const oldFeatures = self.features?.data ?? empty; + source.features.addCallbackAndRunD((features) => { + const oldFeatures = self.features?.data ?? empty // Then new ids - const ids = new Set(features.map(f => f.feature.properties.id + f.feature.geometry.type)); + const ids = new Set( + features.map((f) => f.feature.properties.id + f.feature.geometry.type) + ) // the old data - const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type)) + const oldData = oldFeatures.filter( + (old) => !ids.has(old.feature.properties.id + old.feature.geometry.type) + ) featureSource.setData([...features, ...oldData]) }) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts index 8a8ba609c..2ee07bff9 100644 --- a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts +++ b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts @@ -1,50 +1,57 @@ /** * This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered. */ -import {Store} from "../../UIEventSource"; -import {GeoOperations} from "../../GeoOperations"; -import FeatureSource from "../FeatureSource"; -import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"; -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; -import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"; +import { Store } from "../../UIEventSource" +import { GeoOperations } from "../../GeoOperations" +import FeatureSource from "../FeatureSource" +import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig" export default class RenderingMultiPlexerFeatureSource { - public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>; - private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]; - private centroidRenderings: { rendering: PointRenderingConfig; index: number }[]; - private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[]; - private startRenderings: { rendering: PointRenderingConfig; index: number }[]; - private endRenderings: { rendering: PointRenderingConfig; index: number }[]; - private hasCentroid: boolean; - private lineRenderObjects: LineRenderingConfig[]; + public readonly features: Store< + (any & { + pointRenderingIndex: number | undefined + lineRenderingIndex: number | undefined + })[] + > + private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[] + private centroidRenderings: { rendering: PointRenderingConfig; index: number }[] + private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[] + private startRenderings: { rendering: PointRenderingConfig; index: number }[] + private endRenderings: { rendering: PointRenderingConfig; index: number }[] + private hasCentroid: boolean + private lineRenderObjects: LineRenderingConfig[] - - private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){ + private inspectFeature( + feat, + addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, + withIndex: any[] + ) { if (feat.geometry.type === "Point") { - for (const rendering of this.pointRenderings) { withIndex.push({ ...feat, - pointRenderingIndex: rendering.index + pointRenderingIndex: rendering.index, }) } } else { // This is a a line: add the centroids - let centerpoint: [number, number] = undefined; - let projectedCenterPoint : [number, number] = undefined - if(this.hasCentroid){ - centerpoint = GeoOperations.centerpointCoordinates(feat) - if(this.projectedCentroidRenderings.length > 0){ - projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates + let centerpoint: [number, number] = undefined + let projectedCenterPoint: [number, number] = undefined + if (this.hasCentroid) { + centerpoint = GeoOperations.centerpointCoordinates(feat) + if (this.projectedCentroidRenderings.length > 0) { + projectedCenterPoint = <[number, number]>( + GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates + ) } } for (const rendering of this.centroidRenderings) { addAsPoint(feat, rendering, centerpoint) } - if (feat.geometry.type === "LineString") { - for (const rendering of this.projectedCentroidRenderings) { addAsPoint(feat, rendering, projectedCenterPoint) } @@ -58,73 +65,69 @@ export default class RenderingMultiPlexerFeatureSource { const coordinate = coordinates[coordinates.length - 1] addAsPoint(feat, rendering, coordinate) } - - }else{ + } else { for (const rendering of this.projectedCentroidRenderings) { addAsPoint(feat, rendering, centerpoint) } } - // AT last, add it 'as is' to what we should render + // AT last, add it 'as is' to what we should render for (let i = 0; i < this.lineRenderObjects.length; i++) { withIndex.push({ ...feat, - lineRenderingIndex: i + lineRenderingIndex: i, }) } - } } - + constructor(upstream: FeatureSource, layer: LayerConfig) { - - const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({ - rendering: r, - index: i - })) - this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point")) - this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid")) - this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint")) - this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start")) - this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end")) - this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 + const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] = + layer.mapRendering.map((r, i) => ({ + rendering: r, + index: i, + })) + this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point")) + this.centroidRenderings = pointRenderObjects.filter((r) => + r.rendering.location.has("centroid") + ) + this.projectedCentroidRenderings = pointRenderObjects.filter((r) => + r.rendering.location.has("projected_centerpoint") + ) + this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start")) + this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end")) + this.hasCentroid = + this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0 this.lineRenderObjects = layer.lineRendering - - this.features = upstream.features.map( - features => { - if (features === undefined) { - return undefined; - } - - const withIndex: any[] = []; - - function addAsPoint(feat, rendering, coordinate) { - const patched = { - ...feat, - pointRenderingIndex: rendering.index - } - patched.geometry = { - type: "Point", - coordinates: coordinate - } - withIndex.push(patched) - } - - - for (const f of features) { - const feat = f.feature; - if(feat === undefined){ - continue - } - this.inspectFeature(feat, addAsPoint, withIndex) - } - - - return withIndex; + this.features = upstream.features.map((features) => { + if (features === undefined) { + return undefined } - ); + const withIndex: any[] = [] + + function addAsPoint(feat, rendering, coordinate) { + const patched = { + ...feat, + pointRenderingIndex: rendering.index, + } + patched.geometry = { + type: "Point", + coordinates: coordinate, + } + withIndex.push(patched) + } + + for (const f of features) { + const feat = f.feature + if (feat === undefined) { + continue + } + this.inspectFeature(feat, addAsPoint, withIndex) + } + + return withIndex + }) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts index 63937763e..f2dfec678 100644 --- a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -1,21 +1,24 @@ -import {UIEventSource} from "../../UIEventSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {BBox} from "../../BBox"; +import { UIEventSource } from "../../UIEventSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { BBox } from "../../BBox" export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled { - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; - public readonly name: string = "SimpleFeatureSource"; - public readonly layer: FilteredLayer; - public readonly bbox: BBox = BBox.global; - public readonly tileIndex: number; + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> + public readonly name: string = "SimpleFeatureSource" + public readonly layer: FilteredLayer + public readonly bbox: BBox = BBox.global + public readonly tileIndex: number - constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> ) { + constructor( + layer: FilteredLayer, + tileIndex: number, + featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> + ) { this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")" this.layer = layer - this.tileIndex = tileIndex ?? 0; + this.tileIndex = tileIndex ?? 0 this.bbox = BBox.fromTileIndex(this.tileIndex) - this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]); + this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/Logic/FeatureSource/Sources/StaticFeatureSource.ts index 3a1b3baca..29f52cc59 100644 --- a/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -1,62 +1,90 @@ -import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource"; -import {stat} from "fs"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {BBox} from "../../BBox"; -import {Feature} from "@turf/turf"; +import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" +import { stat } from "fs" +import FilteredLayer from "../../../Models/FilteredLayer" +import { BBox } from "../../BBox" +import { Feature } from "@turf/turf" /** * A simple, read only feature store. */ export default class StaticFeatureSource implements FeatureSource { - public readonly features: Store<{ feature: any; freshness: Date }[]>; + public readonly features: Store<{ feature: any; freshness: Date }[]> public readonly name: string - constructor(features: Store<{ feature: Feature, freshness: Date }[]>, name = "StaticFeatureSource") { + constructor( + features: Store<{ feature: Feature; freshness: Date }[]>, + name = "StaticFeatureSource" + ) { if (features === undefined) { throw "Static feature source received undefined as source" } - this.name = name; - this.features = features; + this.name = name + this.features = features } - public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { - return new StaticFeatureSource(new ImmutableStore(features), name); + public static fromGeojsonAndDate( + features: { feature: Feature; freshness: Date }[], + name = "StaticFeatureSourceFromGeojsonAndDate" + ): StaticFeatureSource { + return new StaticFeatureSource(new ImmutableStore(features), name) } - - public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { - const now = new Date(); - return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name); + public static fromGeojson( + geojson: Feature[], + name = "StaticFeatureSourceFromGeojson" + ): StaticFeatureSource { + const now = new Date() + return StaticFeatureSource.fromGeojsonAndDate( + geojson.map((feature) => ({ feature, freshness: now })), + name + ) } - public static fromGeojsonStore(geojson: Store, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { - const now = new Date(); - const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now}))) - return new StaticFeatureSource(mapped, name); + public static fromGeojsonStore( + geojson: Store, + name = "StaticFeatureSourceFromGeojson" + ): StaticFeatureSource { + const now = new Date() + const mapped: Store<{ feature: Feature; freshness: Date }[]> = geojson.map((features) => + features.map((feature) => ({ feature, freshness: now })) + ) + return new StaticFeatureSource(mapped, name) } - static fromDateless(featureSource: Store<{ feature: Feature }[]>, name = "StaticFeatureSourceFromDateless") { - const now = new Date(); - return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({ - feature: feature.feature, - freshness: now - }))), name); + static fromDateless( + featureSource: Store<{ feature: Feature }[]>, + name = "StaticFeatureSourceFromDateless" + ) { + const now = new Date() + return new StaticFeatureSource( + featureSource.map((features) => + features.map((feature) => ({ + feature: feature.feature, + freshness: now, + })) + ), + name + ) } } -export class TiledStaticFeatureSource extends StaticFeatureSource implements Tiled, FeatureSourceForLayer{ +export class TiledStaticFeatureSource + extends StaticFeatureSource + implements Tiled, FeatureSourceForLayer +{ + public readonly bbox: BBox = BBox.global + public readonly tileIndex: number + public readonly layer: FilteredLayer - public readonly bbox: BBox = BBox.global; - public readonly tileIndex: number; - public readonly layer: FilteredLayer; - - constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) { - super(features); - this.tileIndex = tileIndex ; - this.layer= layer; + constructor( + features: Store<{ feature: any; freshness: Date }[]>, + layer: FilteredLayer, + tileIndex: number = 0 + ) { + super(features) + this.tileIndex = tileIndex + this.layer = layer this.bbox = BBox.fromTileIndex(this.tileIndex) } - - } diff --git a/Logic/FeatureSource/TileFreshnessCalculator.ts b/Logic/FeatureSource/TileFreshnessCalculator.ts index 58a655151..3d1adde97 100644 --- a/Logic/FeatureSource/TileFreshnessCalculator.ts +++ b/Logic/FeatureSource/TileFreshnessCalculator.ts @@ -1,12 +1,11 @@ -import {Tiles} from "../../Models/TileRange"; +import { Tiles } from "../../Models/TileRange" export default class TileFreshnessCalculator { - /** * All the freshnesses per tile index * @private */ - private readonly freshnesses = new Map(); + private readonly freshnesses = new Map() /** * Marks that some data got loaded for this layer @@ -16,14 +15,14 @@ export default class TileFreshnessCalculator { public addTileLoad(tileId: number, freshness: Date) { const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId)) if (existingFreshness >= freshness) { - return; + return } this.freshnesses.set(tileId, freshness) // Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too! let [z, x, y] = Tiles.tile_from_index(tileId) if (z === 0) { - return; + return } x = x - (x % 2) // Make the tiles always even y = y - (y % 2) @@ -48,11 +47,7 @@ export default class TileFreshnessCalculator { const leastFresh = Math.min(ul, ur, ll, lr) const date = new Date() date.setTime(leastFresh) - this.addTileLoad( - Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), - date - ) - + this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date) } public freshnessFor(z: number, x: number, y: number): Date { @@ -65,7 +60,5 @@ export default class TileFreshnessCalculator { } // recurse up return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2)) - } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 65117fe3f..dd2b69ffe 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -1,21 +1,22 @@ -import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {UIEventSource} from "../../UIEventSource"; -import DynamicTileSource from "./DynamicTileSource"; -import {Utils} from "../../../Utils"; -import GeoJsonSource from "../Sources/GeoJsonSource"; -import {BBox} from "../../BBox"; +import FilteredLayer from "../../../Models/FilteredLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { UIEventSource } from "../../UIEventSource" +import DynamicTileSource from "./DynamicTileSource" +import { Utils } from "../../../Utils" +import GeoJsonSource from "../Sources/GeoJsonSource" +import { BBox } from "../../BBox" export default class DynamicGeoJsonTileSource extends DynamicTileSource { - private static whitelistCache = new Map() - constructor(layer: FilteredLayer, - registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, - state: { - locationControl?: UIEventSource<{zoom?: number}> - currentBounds: UIEventSource - }) { + constructor( + layer: FilteredLayer, + registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, + state: { + locationControl?: UIEventSource<{ zoom?: number }> + currentBounds: UIEventSource + } + ) { const source = layer.layerDef.source if (source.geojsonZoomLevel === undefined) { throw "Invalid layer: geojsonZoomLevel expected" @@ -26,7 +27,6 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { let whitelist = undefined if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) { - const whitelistUrl = source.geojsonSource .replace("{z}", "" + source.geojsonZoomLevel) .replace("{x}_{y}.geojson", "overview.json") @@ -35,26 +35,33 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) { whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl) } else { - Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60).then( - json => { - const data = new Map>(); + Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60) + .then((json) => { + const data = new Map>() for (const x in json) { if (x === "zoom") { continue } data.set(Number(x), new Set(json[x])) } - console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl) + console.log( + "The whitelist is", + data, + "based on ", + json, + "from", + whitelistUrl + ) whitelist = data DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist) - } - ).catch(err => { - console.warn("No whitelist found for ", layer.layerDef.id, err) - }) + }) + .catch((err) => { + console.warn("No whitelist found for ", layer.layerDef.id, err) + }) } } - const blackList = (new Set()) + const blackList = new Set() super( layer, source.geojsonZoomLevel, @@ -62,29 +69,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { if (whitelist !== undefined) { const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) if (!isWhiteListed) { - console.debug("Not downloading tile", ...zxy, "as it is not on the whitelist") - return undefined; + console.debug( + "Not downloading tile", + ...zxy, + "as it is not on the whitelist" + ) + return undefined } } - const src = new GeoJsonSource( - layer, - zxy, - { - featureIdBlacklist: blackList - } - ) - + const src = new GeoJsonSource(layer, zxy, { + featureIdBlacklist: blackList, + }) + registerLayer(src) return src }, state - ); - + ) } public static RegisterWhitelist(url: string, json: any) { - const data = new Map>(); + const data = new Map>() for (const x in json) { if (x === "zoom") { continue @@ -93,5 +99,4 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { } DynamicGeoJsonTileSource.whitelistCache.set(url, data) } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 99209b309..78f759818 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -1,64 +1,80 @@ -import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {UIEventSource} from "../../UIEventSource"; -import TileHierarchy from "./TileHierarchy"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; +import FilteredLayer from "../../../Models/FilteredLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { UIEventSource } from "../../UIEventSource" +import TileHierarchy from "./TileHierarchy" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" /*** * A tiled source which dynamically loads the required tiles at a fixed zoom level */ export default class DynamicTileSource implements TileHierarchy { - public readonly loadedTiles: Map; - private readonly _loadedTiles = new Set(); + public readonly loadedTiles: Map + private readonly _loadedTiles = new Set() constructor( layer: FilteredLayer, zoomlevel: number, - constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), + constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled, state: { - currentBounds: UIEventSource; - locationControl?: UIEventSource<{zoom?: number}> + currentBounds: UIEventSource + locationControl?: UIEventSource<{ zoom?: number }> } ) { - const self = this; + const self = this this.loadedTiles = new Map() - const neededTiles = state.currentBounds.map( - bounds => { - if (bounds === undefined) { - // We'll retry later - return undefined - } - - if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) { - // No need to download! - the layer is disabled - return undefined; - } + const neededTiles = state.currentBounds + .map( + (bounds) => { + if (bounds === undefined) { + // We'll retry later + return undefined + } - if (state.locationControl?.data?.zoom !== undefined && state.locationControl.data.zoom < layer.layerDef.minzoom) { - // No need to download! - the layer is disabled - return undefined; - } + if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) { + // No need to download! - the layer is disabled + return undefined + } - const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) - if (tileRange.total > 10000) { - console.error("Got a really big tilerange, bounds and location might be out of sync") - return undefined - } + if ( + state.locationControl?.data?.zoom !== undefined && + state.locationControl.data.zoom < layer.layerDef.minzoom + ) { + // No need to download! - the layer is disabled + return undefined + } - const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) - if (needed.length === 0) { - return undefined - } - return needed - } - , [layer.isDisplayed, state.locationControl]).stabilized(250); + const tileRange = Tiles.TileRangeBetween( + zoomlevel, + bounds.getNorth(), + bounds.getEast(), + bounds.getSouth(), + bounds.getWest() + ) + if (tileRange.total > 10000) { + console.error( + "Got a really big tilerange, bounds and location might be out of sync" + ) + return undefined + } - neededTiles.addCallbackAndRunD(neededIndexes => { + const needed = Tiles.MapRange(tileRange, (x, y) => + Tiles.tile_index(zoomlevel, x, y) + ).filter((i) => !self._loadedTiles.has(i)) + if (needed.length === 0) { + return undefined + } + return needed + }, + [layer.isDisplayed, state.locationControl] + ) + .stabilized(250) + + neededTiles.addCallbackAndRunD((neededIndexes) => { console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) if (neededIndexes === undefined) { - return; + return } for (const neededIndex of neededIndexes) { self._loadedTiles.add(neededIndex) @@ -68,10 +84,5 @@ export default class DynamicTileSource implements TileHierarchy { public readonly loadedTiles = new Map() - private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void; + private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void private readonly layer: FilteredLayer - private readonly nodeByIds = new Map(); + private readonly nodeByIds = new Map() private readonly parentWays = new Map>() - constructor( - layer: FilteredLayer, - onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) { + constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) { this.onTileLoaded = onTileLoaded - this.layer = layer; + this.layer = layer if (this.layer === undefined) { throw "Layer is undefined" } } public handleOsmJson(osmJson: any, tileId: number) { - const allObjects = OsmObject.ParseObjects(osmJson.elements) const nodesById = new Map() @@ -32,7 +28,7 @@ export default class FullNodeDatabaseSource implements TileHierarchyosmObj; + const osmNode = osmObj nodesById.set(osmNode.id, osmNode) this.nodeByIds.set(osmNode.id, osmNode) } @@ -41,33 +37,32 @@ export default class FullNodeDatabaseSource implements TileHierarchyosmObj; + const osmWay = osmObj for (const nodeId of osmWay.nodes) { - if (!this.parentWays.has(nodeId)) { const src = new UIEventSource([]) this.parentWays.set(nodeId, src) - src.addCallback(parentWays => { + src.addCallback((parentWays) => { const tgs = nodesById.get(nodeId).tags - tgs ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags)) - tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id)) + tgs["parent_ways"] = JSON.stringify(parentWays.map((w) => w.tags)) + tgs["parent_way_ids"] = JSON.stringify(parentWays.map((w) => w.id)) }) } const src = this.parentWays.get(nodeId) src.data.push(osmWay) - src.ping(); + src.ping() } } const now = new Date() - const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({ - feature: osmNode.asGeoJson(), freshness: now + const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) => ({ + feature: osmNode.asGeoJson(), + freshness: now, })) const featureSource = new SimpleFeatureSource(this.layer, tileId) featureSource.features.setData(asGeojsonFeatures) this.loadedTiles.set(tileId, featureSource) this.onTileLoaded(featureSource) - } /** @@ -88,6 +83,4 @@ export default class FullNodeDatabaseSource implements TileHierarchy { return this.parentWays.get(nodeId) } - } - diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index 330c0386f..949bb80f9 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -1,17 +1,17 @@ -import {Utils} from "../../../Utils"; -import * as OsmToGeoJson from "osmtogeojson"; -import StaticFeatureSource from "../Sources/StaticFeatureSource"; -import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; -import {Store, UIEventSource} from "../../UIEventSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; -import {Or} from "../../Tags/Or"; -import {TagsFilter} from "../../Tags/TagsFilter"; -import {OsmObject} from "../../Osm/OsmObject"; -import {FeatureCollection} from "@turf/turf"; +import { Utils } from "../../../Utils" +import * as OsmToGeoJson from "osmtogeojson" +import StaticFeatureSource from "../Sources/StaticFeatureSource" +import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter" +import { Store, UIEventSource } from "../../UIEventSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" +import { Or } from "../../Tags/Or" +import { TagsFilter } from "../../Tags/TagsFilter" +import { OsmObject } from "../../Osm/OsmObject" +import { FeatureCollection } from "@turf/turf" /** * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' @@ -20,67 +20,70 @@ export default class OsmFeatureSource { public readonly isRunning: UIEventSource = new UIEventSource(false) public readonly downloadedTiles = new Set() public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] - private readonly _backend: string; - private readonly filteredLayers: Store; - private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; - private isActive: Store; + private readonly _backend: string + private readonly filteredLayers: Store + private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void + private isActive: Store private options: { - handleTile: (tile: FeatureSourceForLayer & Tiled) => void; - isActive: Store, - neededTiles: Store, + handleTile: (tile: FeatureSourceForLayer & Tiled) => void + isActive: Store + neededTiles: Store markTileVisited?: (tileId: number) => void - }; - private readonly allowedTags: TagsFilter; + } + private readonly allowedTags: TagsFilter /** * * @param options: allowedFeatures is normally calculated from the layoutToUse */ constructor(options: { - handleTile: (tile: FeatureSourceForLayer & Tiled) => void; - isActive: Store, - neededTiles: Store, + handleTile: (tile: FeatureSourceForLayer & Tiled) => void + isActive: Store + neededTiles: Store state: { - readonly filteredLayers: UIEventSource; + readonly filteredLayers: UIEventSource readonly osmConnection: { Backend(): string - }; + } readonly layoutToUse?: LayoutConfig - }, - readonly allowedFeatures?: TagsFilter, + } + readonly allowedFeatures?: TagsFilter markTileVisited?: (tileId: number) => void }) { - this.options = options; - this._backend = options.state.osmConnection.Backend(); - this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined)) + this.options = options + this._backend = options.state.osmConnection.Backend() + this.filteredLayers = options.state.filteredLayers.map((layers) => + layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined) + ) this.handleTile = options.handleTile this.isActive = options.isActive const self = this - options.neededTiles.addCallbackAndRunD(neededTiles => { + options.neededTiles.addCallbackAndRunD((neededTiles) => { self.Update(neededTiles) }) - const neededLayers = (options.state.layoutToUse?.layers ?? []) - .filter(layer => !layer.doNotDownload) - .filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer) - this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags)) + .filter((layer) => !layer.doNotDownload) + .filter( + (layer) => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer + ) + this.allowedTags = + options.allowedFeatures ?? new Or(neededLayers.map((l) => l.source.osmTags)) } private async Update(neededTiles: number[]) { if (this.options.isActive?.data === false) { - return; + return } - neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile)) + neededTiles = neededTiles.filter((tile) => !this.downloadedTiles.has(tile)) if (neededTiles.length == 0) { - return; + return } this.isRunning.setData(true) try { - for (const neededTile of neededTiles) { this.downloadedTiles.add(neededTile) await this.LoadTile(...Tiles.tile_from_index(neededTile)) @@ -98,24 +101,30 @@ export default class OsmFeatureSource { * This method will download the full relation and return it as geojson if it was incomplete. * If the feature is already complete (or is not a relation), the feature will be returned */ - private async patchIncompleteRelations(feature: {properties: {id: string}}, - originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise { - if(!feature.properties.id.startsWith("relation")){ + private async patchIncompleteRelations( + feature: { properties: { id: string } }, + originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] } + ): Promise { + if (!feature.properties.id.startsWith("relation")) { return feature } - const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id) - const members : {type: string, ref: number}[] = relationSpec["members"] + const relationSpec = originalJson.elements.find( + (f) => "relation/" + f.id === feature.properties.id + ) + const members: { type: string; ref: number }[] = relationSpec["members"] for (const member of members) { - const isFound = originalJson.elements.some(f => f.id === member.ref && f.type === member.type) + const isFound = originalJson.elements.some( + (f) => f.id === member.ref && f.type === member.type + ) if (isFound) { continue } - + // This member is missing. We redownload the entire relation instead - console.debug("Fetching incomplete relation "+feature.properties.id) + console.debug("Fetching incomplete relation " + feature.properties.id) return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson() } - return feature; + return feature } private async LoadTile(z, x, y): Promise { @@ -130,52 +139,69 @@ export default class OsmFeatureSource { const bbox = BBox.fromTile(z, x, y) const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` - let error = undefined; + let error = undefined try { const osmJson = await Utils.downloadJson(url) try { - console.log("Got tile", z, x, y, "from the osm api") - this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y))) - const geojson = > OsmToGeoJson.default(osmJson, + this.rawDataHandlers.forEach((handler) => + handler(osmJson, Tiles.tile_index(z, x, y)) + ) + const geojson = >OsmToGeoJson.default( + osmJson, // @ts-ignore { - flatProperties: true - }); - + flatProperties: true, + } + ) // The geojson contains _all_ features at the given location // We only keep what is needed - geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties)) + geojson.features = geojson.features.filter((feature) => + this.allowedTags.matchesProperties(feature.properties) + ) for (let i = 0; i < geojson.features.length; i++) { - geojson.features[i] = await this.patchIncompleteRelations(geojson.features[i], osmJson) + geojson.features[i] = await this.patchIncompleteRelations( + geojson.features[i], + osmJson + ) } - geojson.features.forEach(f => { + geojson.features.forEach((f) => { f.properties["_backend"] = this._backend }) - const index = Tiles.tile_index(z, x, y); - new PerLayerFeatureSourceSplitter(this.filteredLayers, + const index = Tiles.tile_index(z, x, y) + new PerLayerFeatureSourceSplitter( + this.filteredLayers, this.handleTile, StaticFeatureSource.fromGeojson(geojson.features), { - tileIndex: index + tileIndex: index, } - ); + ) if (this.options.markTileVisited) { this.options.markTileVisited(index) } - }catch(e){ - console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile") - error = e; + } catch (e) { + console.error( + "PANIC: got the tile from the OSM-api, but something crashed handling this tile" + ) + error = e } - } catch (e) { - console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds") + console.error( + "Could not download tile", + z, + x, + y, + "due to", + e, + "; retrying with smaller bounds" + ) if (e === "rate limited") { - return; + return } await this.LoadTile(z + 1, x * 2, y * 2) await this.LoadTile(z + 1, 1 + x * 2, y * 2) @@ -183,10 +209,8 @@ export default class OsmFeatureSource { await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) } - if(error !== undefined){ - throw error; + if (error !== undefined) { + throw error } - } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts index cd6b3dda7..93d883c7e 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts @@ -1,25 +1,24 @@ -import FeatureSource, {Tiled} from "../FeatureSource"; -import {BBox} from "../../BBox"; +import FeatureSource, { Tiled } from "../FeatureSource" +import { BBox } from "../../BBox" export default interface TileHierarchy { - /** * A mapping from 'tile_index' to the actual tile featrues */ loadedTiles: Map - } export class TileHierarchyTools { - - public static getTiles(hierarchy: TileHierarchy, bbox: BBox): T[] { + public static getTiles( + hierarchy: TileHierarchy, + bbox: BBox + ): T[] { const result: T[] = [] hierarchy.loadedTiles.forEach((tile) => { if (tile.bbox.overlapsWith(bbox)) { result.push(tile) } }) - return result; + return result } - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts index c65decf9f..b4de5333d 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts @@ -1,20 +1,32 @@ -import TileHierarchy from "./TileHierarchy"; -import {UIEventSource} from "../../UIEventSource"; -import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; +import TileHierarchy from "./TileHierarchy" +import { UIEventSource } from "../../UIEventSource" +import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import FeatureSourceMerger from "../Sources/FeatureSourceMerger" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" export class TileHierarchyMerger implements TileHierarchy { - public readonly loadedTiles: Map = new Map(); - public readonly layer: FilteredLayer; - private readonly sources: Map> = new Map>(); - private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void; + public readonly loadedTiles: Map = new Map< + number, + FeatureSourceForLayer & Tiled + >() + public readonly layer: FilteredLayer + private readonly sources: Map> = new Map< + number, + UIEventSource + >() + private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void - constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) { - this.layer = layer; - this._handleTile = handleTile; + constructor( + layer: FilteredLayer, + handleTile: ( + src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, + index: number + ) => void + ) { + this.layer = layer + this._handleTile = handleTile } /** @@ -23,22 +35,24 @@ export class TileHierarchyMerger implements TileHierarchy([src]) this.sources.set(index, sources) - const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources) + const merger = new FeatureSourceMerger( + this.layer, + index, + BBox.fromTile(...Tiles.tile_from_index(index)), + sources + ) this.loadedTiles.set(index, merger) this._handleTile(merger, index) } - - -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts index be0f78e7b..9327ec706 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource.ts @@ -1,53 +1,65 @@ -import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource"; -import {Store, UIEventSource} from "../../UIEventSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import TileHierarchy from "./TileHierarchy"; -import {Tiles} from "../../../Models/TileRange"; -import {BBox} from "../../BBox"; +import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource" +import { Store, UIEventSource } from "../../UIEventSource" +import FilteredLayer from "../../../Models/FilteredLayer" +import TileHierarchy from "./TileHierarchy" +import { Tiles } from "../../../Models/TileRange" +import { BBox } from "../../BBox" /** * Contains all features in a tiled fashion. * The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high */ -export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy { - public readonly z: number; - public readonly x: number; - public readonly y: number; - public readonly parent: TiledFeatureSource; +export default class TiledFeatureSource + implements + Tiled, + IndexedFeatureSource, + FeatureSourceForLayer, + TileHierarchy +{ + public readonly z: number + public readonly x: number + public readonly y: number + public readonly parent: TiledFeatureSource public readonly root: TiledFeatureSource - public readonly layer: FilteredLayer; + public readonly layer: FilteredLayer /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. - * Only defined on the root element! + * Only defined on the root element! */ - public readonly loadedTiles: Map = undefined; + public readonly loadedTiles: Map = undefined - public readonly maxFeatureCount: number; - public readonly name; - public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> + public readonly maxFeatureCount: number + public readonly name + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> public readonly containedIds: Store> - public readonly bbox: BBox; - public readonly tileIndex: number; + public readonly bbox: BBox + public readonly tileIndex: number private upper_left: TiledFeatureSource private upper_right: TiledFeatureSource private lower_left: TiledFeatureSource private lower_right: TiledFeatureSource - private readonly maxzoom: number; + private readonly maxzoom: number private readonly options: TiledFeatureSourceOptions - private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { - this.z = z; - this.x = x; - this.y = y; + private constructor( + z: number, + x: number, + y: number, + parent: TiledFeatureSource, + options?: TiledFeatureSourceOptions + ) { + this.z = z + this.x = x + this.y = y this.bbox = BBox.fromTile(z, x, y) this.tileIndex = Tiles.tile_index(z, x, y) this.name = `TiledFeatureSource(${z},${x},${y})` - this.parent = parent; + this.parent = parent this.layer = options.layer options = options ?? {} - this.maxFeatureCount = options?.maxFeatureCount ?? 250; + this.maxFeatureCount = options?.maxFeatureCount ?? 250 this.maxzoom = options.maxZoomLevel ?? 18 - this.options = options; + this.options = options if (parent === undefined) { throw "Parent is not allowed to be undefined. Use null instead" } @@ -55,50 +67,51 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, throw "Invalid root tile: z, x and y should all be null" } if (parent === null) { - this.root = this; + this.root = this this.loadedTiles = new Map() } else { - this.root = this.parent.root; - this.loadedTiles = this.root.loadedTiles; + this.root = this.parent.root + this.loadedTiles = this.root.loadedTiles const i = Tiles.tile_index(z, x, y) this.root.loadedTiles.set(i, this) } this.features = new UIEventSource([]) - this.containedIds = this.features.map(features => { + this.containedIds = this.features.map((features) => { if (features === undefined) { - return undefined; + return undefined } - return new Set(features.map(f => f.feature.properties.id)) + return new Set(features.map((f) => f.feature.properties.id)) }) // We register this tile, but only when there is some data in it if (this.options.registerTile !== undefined) { - this.features.addCallbackAndRunD(features => { + this.features.addCallbackAndRunD((features) => { if (features.length === 0) { - return; + return } this.options.registerTile(this) - return true; + return true }) } - - } - public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource { + public static createHierarchy( + features: FeatureSource, + options?: TiledFeatureSourceOptions + ): TiledFeatureSource { options = { ...options, - layer: features["layer"] ?? options.layer + layer: features["layer"] ?? options.layer, } const root = new TiledFeatureSource(0, 0, 0, null, options) - features.features?.addCallbackAndRunD(feats => root.addFeatures(feats)) - return root; + features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats)) + return root } private isSplitNeeded(featureCount: number) { if (this.upper_left !== undefined) { // This tile has been split previously, so we keep on splitting - return true; + return true } if (this.z >= this.maxzoom) { // We are not allowed to split any further @@ -111,7 +124,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, // To much features - we split return featureCount > this.maxFeatureCount - } /*** @@ -120,21 +132,45 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, * @param features * @private */ - private addFeatures(features: { feature: any, freshness: Date }[]) { + private addFeatures(features: { feature: any; freshness: Date }[]) { if (features === undefined || features.length === 0) { - return; + return } if (!this.isSplitNeeded(features.length)) { this.features.setData(features) - return; + return } if (this.upper_left === undefined) { - this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options) - this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options) - this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options) - this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options) + this.upper_left = new TiledFeatureSource( + this.z + 1, + this.x * 2, + this.y * 2, + this, + this.options + ) + this.upper_right = new TiledFeatureSource( + this.z + 1, + this.x * 2 + 1, + this.y * 2, + this, + this.options + ) + this.lower_left = new TiledFeatureSource( + this.z + 1, + this.x * 2, + this.y * 2 + 1, + this, + this.options + ) + this.lower_right = new TiledFeatureSource( + this.z + 1, + this.x * 2 + 1, + this.y * 2 + 1, + this, + this.options + ) } const ulf = [] @@ -147,7 +183,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, const bbox = BBox.get(feature.feature) // There are a few strategies to deal with features that cross tile boundaries - + if (this.options.noDuplicates) { // Strategy 1: We put the feature into a somewhat matching tile if (bbox.overlapsWith(this.upper_left.bbox)) { @@ -195,19 +231,18 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, this.lower_left.addFeatures(llf) this.lower_right.addFeatures(lrf) this.features.setData(overlapsboundary) - } } export interface TiledFeatureSourceOptions { - readonly maxFeatureCount?: number, - readonly maxZoomLevel?: number, - readonly minZoomLevel?: number, + readonly maxFeatureCount?: number + readonly maxZoomLevel?: number + readonly minZoomLevel?: number /** * IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated. * Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile. */ - readonly noDuplicates?: boolean, - readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void, + readonly noDuplicates?: boolean + readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void readonly layer?: FilteredLayer -} \ No newline at end of file +} diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index e758d5546..a033d499f 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -1,17 +1,25 @@ -import * as turf from '@turf/turf' -import {BBox} from "./BBox"; +import * as turf from "@turf/turf" +import { BBox } from "./BBox" import togpx from "togpx" -import Constants from "../Models/Constants"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf"; +import Constants from "../Models/Constants" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import { + AllGeoJSON, + booleanWithin, + Coord, + Feature, + Geometry, + MultiPolygon, + Polygon, + Properties, +} from "@turf/turf" export class GeoOperations { - - private static readonly _earthRadius = 6378137; - private static readonly _originShift = 2 * Math.PI * GeoOperations._earthRadius / 2; + private static readonly _earthRadius = 6378137 + private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2 static surfaceAreaInSqMeters(feature: any) { - return turf.area(feature); + return turf.area(feature) } /** @@ -19,10 +27,10 @@ export class GeoOperations { * @param feature */ static centerpoint(feature: any) { - const newFeature = turf.center(feature); - newFeature.properties = feature.properties; - newFeature.id = feature.id; - return newFeature; + const newFeature = turf.center(feature) + newFeature.properties = feature.properties + newFeature.id = feature.id + return newFeature } /** @@ -30,7 +38,7 @@ export class GeoOperations { * @param feature */ static centerpointCoordinates(feature: AllGeoJSON): [number, number] { - return <[number, number]>turf.center(feature).geometry.coordinates; + return <[number, number]>turf.center(feature).geometry.coordinates } /** @@ -39,7 +47,7 @@ export class GeoOperations { * @param lonlat1 */ static distanceBetween(lonlat0: [number, number], lonlat1: [number, number]) { - return turf.distance(lonlat0, lonlat1, {units: "meters"}) + return turf.distance(lonlat0, lonlat1, { units: "meters" }) } static convexHull(featureCollection, options: { concavity?: number }) { @@ -69,16 +77,17 @@ export class GeoOperations { * const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]); * overlap.length // => 1 */ - static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any, overlap: number }[] { - - const featureBBox = BBox.get(feature); - const result: { feat: any, overlap: number }[] = []; + static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any; overlap: number }[] { + const featureBBox = BBox.get(feature) + const result: { feat: any; overlap: number }[] = [] if (feature.geometry.type === "Point") { - const coor = feature.geometry.coordinates; + const coor = feature.geometry.coordinates for (const otherFeature of otherFeatures) { - - if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { - continue; + if ( + feature.properties.id !== undefined && + feature.properties.id === otherFeature.properties.id + ) { + continue } if (otherFeature.geometry === undefined) { @@ -87,86 +96,105 @@ export class GeoOperations { } if (GeoOperations.inside(coor, otherFeature)) { - result.push({feat: otherFeature, overlap: undefined}) + result.push({ feat: otherFeature, overlap: undefined }) } } - return result; + return result } if (feature.geometry.type === "LineString") { - for (const otherFeature of otherFeatures) { - - if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { - continue; + if ( + feature.properties.id !== undefined && + feature.properties.id === otherFeature.properties.id + ) { + continue } - const intersection = GeoOperations.calculateInstersection(feature, otherFeature, featureBBox) + const intersection = GeoOperations.calculateInstersection( + feature, + otherFeature, + featureBBox + ) if (intersection === null) { continue } - result.push({feat: otherFeature, overlap: intersection}) - + result.push({ feat: otherFeature, overlap: intersection }) } - return result; + return result } if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { - for (const otherFeature of otherFeatures) { - - if (feature.properties.id !== undefined && feature.properties.id === otherFeature.properties.id) { - continue; + if ( + feature.properties.id !== undefined && + feature.properties.id === otherFeature.properties.id + ) { + continue } if (otherFeature.geometry.type === "Point") { if (this.inside(otherFeature, feature)) { - result.push({feat: otherFeature, overlap: undefined}) + result.push({ feat: otherFeature, overlap: undefined }) } - continue; + continue } - // Calculate the surface area of the intersection const intersection = this.calculateInstersection(feature, otherFeature, featureBBox) if (intersection === null) { - continue; + continue } - result.push({feat: otherFeature, overlap: intersection}) - + result.push({ feat: otherFeature, overlap: intersection }) } - return result; + return result } - console.error("Could not correctly calculate the overlap of ", feature, ": unsupported type") - return result; + console.error( + "Could not correctly calculate the overlap of ", + feature, + ": unsupported type" + ) + return result } /** * Helper function which does the heavy lifting for 'inside' */ - private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) { - const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0]) + private static pointInPolygonCoordinates( + x: number, + y: number, + coordinates: [number, number][][] + ) { + const inside = GeoOperations.pointWithinRing( + x, + y, + /*This is the outer ring of the polygon */ coordinates[0] + ) if (!inside) { - return false; + return false } for (let i = 1; i < coordinates.length; i++) { - const inHole = GeoOperations.pointWithinRing(x, y, coordinates[i] /* These are inner rings, aka holes*/) + const inHole = GeoOperations.pointWithinRing( + x, + y, + coordinates[i] /* These are inner rings, aka holes*/ + ) if (inHole) { - return false; + return false } } - return true; + return true } /** * Detect wether or not the given point is located in the feature - * + * * // Should work with a normal polygon * const polygon = {"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[1.8017578124999998,50.401515322782366],[-3.1640625,46.255846818480315],[5.185546875,44.74673324024678],[1.8017578124999998,50.401515322782366]]]}}; * GeoOperations.inside([3.779296875, 48.777912755501845], polygon) // => false * GeoOperations.inside([1.23046875, 47.60616304386874], polygon) // => true - * + * * // should work with a multipolygon and detect holes * const multiPolygon = {"type": "Feature", "properties": {}, * "geometry": { @@ -186,37 +214,32 @@ export class GeoOperations { // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html if (feature.geometry.type === "Point") { - return false; + return false } if (pointCoordinate.geometry !== undefined) { pointCoordinate = pointCoordinate.geometry.coordinates } - const x: number = pointCoordinate[0]; - const y: number = pointCoordinate[1]; - + const x: number = pointCoordinate[0] + const y: number = pointCoordinate[1] if (feature.geometry.type === "MultiPolygon") { - const coordinatess = feature.geometry.coordinates; + const coordinatess = feature.geometry.coordinates for (const coordinates of coordinatess) { const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates) if (inThisPolygon) { - return true; + return true } - } - return false; + return false } - if (feature.geometry.type === "Polygon") { return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates) } - throw "GeoOperations.inside: unsupported geometry type "+feature.geometry.type - - + throw "GeoOperations.inside: unsupported geometry type " + feature.geometry.type } static lengthInMeters(feature: any) { @@ -225,39 +248,24 @@ export class GeoOperations { static buffer(feature: any, bufferSizeInMeter: number) { return turf.buffer(feature, bufferSizeInMeter / 1000, { - units: 'kilometers' + units: "kilometers", }) } static bbox(feature: any) { const [lon, lat, lon0, lat0] = turf.bbox(feature) return { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [ - lon, - lat - ], - [ - lon0, - lat - ], - [ - lon0, - lat0 - ], - [ - lon, - lat0 - ], - [ - lon, - lat - ], - ] - } + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [lon, lat], + [lon0, lat], + [lon0, lat0], + [lon, lat0], + [lon, lat], + ], + }, } } @@ -273,18 +281,17 @@ export class GeoOperations { */ public static nearestPoint(way, point: [number, number]) { if (way.geometry.type === "Polygon") { - way = {...way} - way.geometry = {...way.geometry} + way = { ...way } + way.geometry = { ...way.geometry } way.geometry.type = "LineString" way.geometry.coordinates = way.geometry.coordinates[0] } - return turf.nearestPointOnLine(way, point, {units: "kilometers"}); + return turf.nearestPointOnLine(way, point, { units: "kilometers" }) } public static toCSV(features: any[]): string { - - const headerValuesSeen = new Set(); + const headerValuesSeen = new Set() const headerValuesOrdered: string[] = [] function addH(key) { @@ -300,18 +307,17 @@ export class GeoOperations { const lines: string[] = [] for (const feature of features) { - const properties = feature.properties; + const properties = feature.properties for (const key in properties) { if (!properties.hasOwnProperty(key)) { - continue; + continue } addH(key) - } } headerValuesOrdered.sort() for (const feature of features) { - const properties = feature.properties; + const properties = feature.properties let line = "" for (const key of headerValuesOrdered) { const value = properties[key] @@ -324,27 +330,27 @@ export class GeoOperations { lines.push(line) } - return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") + return headerValuesOrdered.map((v) => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") } //Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913 public static ConvertWgs84To900913(lonLat: [number, number]): [number, number] { - const lon = lonLat[0]; - const lat = lonLat[1]; - const x = lon * GeoOperations._originShift / 180; - let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); - y = y * GeoOperations._originShift / 180; - return [x, y]; + const lon = lonLat[0] + const lat = lonLat[1] + const x = (lon * GeoOperations._originShift) / 180 + let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180) + y = (y * GeoOperations._originShift) / 180 + return [x, y] } //Converts XY point from (Spherical) Web Mercator EPSG:3785 (unofficially EPSG:900913) to lat/lon in WGS84 Datum public static Convert900913ToWgs84(lonLat: [number, number]): [number, number] { const lon = lonLat[0] const lat = lonLat[1] - const x = 180 * lon / GeoOperations._originShift; - let y = 180 * lat / GeoOperations._originShift; - y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2); - return [x, y]; + const x = (180 * lon) / GeoOperations._originShift + let y = (180 * lat) / GeoOperations._originShift + y = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2) + return [x, y] } public static GeoJsonToWGS84(geojson) { @@ -360,10 +366,10 @@ export class GeoOperations { public static SimplifyCoordinates(coordinates: [number, number][]) { const newCoordinates = [] for (let i = 1; i < coordinates.length - 1; i++) { - const coordinate = coordinates[i]; + const coordinate = coordinates[i] const prev = coordinates[i - 1] const next = coordinates[i + 1] - const b0 = turf.bearing(prev, coordinate, {final: true}) + const b0 = turf.bearing(prev, coordinate, { final: true }) const b1 = turf.bearing(coordinate, next) const diff = Math.abs(b1 - b0) @@ -373,27 +379,27 @@ export class GeoOperations { newCoordinates.push(coordinate) } return newCoordinates - } /** * Calculates line intersection between two features. */ public static LineIntersections(feature, otherFeature): [number, number][] { - return turf.lineIntersect(feature, otherFeature).features.map(p => <[number, number]>p.geometry.coordinates) + return turf + .lineIntersect(feature, otherFeature) + .features.map((p) => <[number, number]>p.geometry.coordinates) } public static AsGpx(feature, generatedWithLayer?: LayerConfig) { - const metadata = {} const tags = feature.properties if (generatedWithLayer !== undefined) { - metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id if (tags._backend?.contains("openstreetmap")) { - metadata["copyright"] = "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" + metadata["copyright"] = + "Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright" metadata["author"] = tags["_last_edit:contributor"] metadata["link"] = "https://www.openstreetmap.org/" + tags.id metadata["time"] = tags["_last_edit:timestamp"] @@ -404,18 +410,22 @@ export class GeoOperations { return togpx(feature, { creator: "MapComplete " + Constants.vNumber, - metadata + metadata, }) } public static IdentifieCommonSegments(coordinatess: [number, number][][]): { - originalIndex: number, - segmentShardWith: number[], + originalIndex: number + segmentShardWith: number[] coordinates: [] }[] { - // An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1]) - type edge = { start: [number, number], end: [number, number], intermediate: [number, number][], members: { index: number, isReversed: boolean }[] } + type edge = { + start: [number, number] + end: [number, number] + intermediate: [number, number][] + members: { index: number; isReversed: boolean }[] + } // The strategy: // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them @@ -425,12 +435,11 @@ export class GeoOperations { const allEdgesByKey = new Map() for (let index = 0; index < coordinatess.length; index++) { - const coordinates = coordinatess[index]; + const coordinates = coordinatess[index] for (let i = 0; i < coordinates.length - 1; i++) { - - const c0 = coordinates[i]; + const c0 = coordinates[i] const c1 = coordinates[i + 1] - const isReversed = (c0[0] > c1[0]) || (c0[0] == c1[0] && c0[1] > c1[1]) + const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1]) let key: string if (isReversed) { @@ -438,40 +447,38 @@ export class GeoOperations { } else { key = "" + c0 + ";" + c1 } - const member = {index, isReversed} + const member = { index, isReversed } if (allEdgesByKey.has(key)) { allEdgesByKey.get(key).members.push(member) continue } - let edge: edge; + let edge: edge if (!isReversed) { edge = { start: c0, end: c1, members: [member], - intermediate: [] + intermediate: [], } } else { edge = { start: c1, end: c0, members: [member], - intermediate: [] + intermediate: [], } } allEdgesByKey.set(key, edge) - } } // Lets merge them back together! - let didMergeSomething = false; + let didMergeSomething = false let allMergedEdges = Array.from(allEdgesByKey.values()) const allEdgesByStartPoint = new Map() for (const edge of allMergedEdges) { - edge.members.sort((m0, m1) => m0.index - m1.index) const kstart = edge.start + "" @@ -481,7 +488,6 @@ export class GeoOperations { allEdgesByStartPoint.get(kstart).push(edge) } - function membersAreCompatible(first: edge, second: edge): boolean { // There must be an exact match between the members if (first.members === second.members) { @@ -504,7 +510,6 @@ export class GeoOperations { // Allrigth, they are the same, lets mark this permanently second.members = first.members return true - } do { @@ -524,9 +529,8 @@ export class GeoOperations { continue } - for (let i = 0; i < matchingEndEdges.length; i++) { - const endEdge = matchingEndEdges[i]; + const endEdge = matchingEndEdges[i] if (consumed.has(endEdge)) { continue @@ -543,12 +547,11 @@ export class GeoOperations { edge.end = endEdge.end consumed.add(endEdge) matchingEndEdges.splice(i, 1) - break; + break } } - allMergedEdges = allMergedEdges.filter(edge => !consumed.has(edge)); - + allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge)) } while (didMergeSomething) return [] @@ -557,7 +560,7 @@ export class GeoOperations { /** * Removes points that do not contribute to the geometry from linestrings and the outer ring of polygons. * Returs a new copy of the feature - * + * * const feature = {"geometry": {"type": "Polygon","coordinates": [[[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477964799999972,51.02785709999982],[4.477964699999964,51.02785690000006],[4.477944199999975,51.02783550000022]]]}} * const copy = GeoOperations.removeOvernoding(feature) * expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]]) @@ -569,7 +572,7 @@ export class GeoOperations { const copy = { ...feature, - geometry: {...feature.geometry} + geometry: { ...feature.geometry }, } let coordinates: [number, number][] if (feature.geometry.type === "LineString") { @@ -582,7 +585,7 @@ export class GeoOperations { // inline replacement in the coordinates list for (let i = coordinates.length - 2; i >= 1; i--) { - const coordinate = coordinates[i]; + const coordinate = coordinates[i] const nextCoordinate = coordinates[i + 1] const prevCoordinate = coordinates[i - 1] @@ -610,30 +613,27 @@ export class GeoOperations { // In case that the line is going south, e.g. bearingN = 179, bearingP = -179 coordinates.splice(i, 1) } - } - return copy; - + return copy } private static pointWithinRing(x: number, y: number, ring: [number, number][]) { - let inside = false; + let inside = false for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { - const coori = ring[i]; - const coorj = ring[j]; + const coori = ring[i] + const coorj = ring[j] - const xi = coori[0]; - const yi = coori[1]; - const xj = coorj[0]; - const yj = coorj[1]; + const xi = coori[0] + const yi = coori[1] + const xj = coorj[0] + const yj = coorj[1] - const intersect = ((yi > y) != (yj > y)) - && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi if (intersect) { - inside = !inside; + inside = !inside } } - return inside; + return inside } /** @@ -642,46 +642,47 @@ export class GeoOperations { * Returns 0 if both are linestrings * Returns null if the features are not intersecting */ - private static calculateInstersection(feature, otherFeature, featureBBox: BBox, otherFeatureBBox?: BBox): number { + private static calculateInstersection( + feature, + otherFeature, + featureBBox: BBox, + otherFeatureBBox?: BBox + ): number { if (feature.geometry.type === "LineString") { - - - otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature); + otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature) const overlaps = featureBBox.overlapsWith(otherFeatureBBox) if (!overlaps) { - return null; + return null } // Calculate the length of the intersection - - let intersectionPoints = turf.lineIntersect(feature, otherFeature); + let intersectionPoints = turf.lineIntersect(feature, otherFeature) if (intersectionPoints.features.length == 0) { // No intersections. // If one point is inside of the polygon, all points are - - const coors = feature.geometry.coordinates; + const coors = feature.geometry.coordinates const startCoor = coors[0] if (this.inside(startCoor, otherFeature)) { return this.lengthInMeters(feature) } - return null; + return null } - let intersectionPointsArray = intersectionPoints.features.map(d => { + let intersectionPointsArray = intersectionPoints.features.map((d) => { return d.geometry.coordinates - }); + }) if (otherFeature.geometry.type === "LineString") { if (intersectionPointsArray.length > 0) { return 0 } - return null; + return null } if (intersectionPointsArray.length == 1) { // We need to add the start- or endpoint of the current feature, depending on which one is embedded - const coors = feature.geometry.coordinates; + const coors = feature.geometry.coordinates const startCoor = coors[0] if (this.inside(startCoor, otherFeature)) { // The startpoint is embedded @@ -691,46 +692,50 @@ export class GeoOperations { } } - let intersection = turf.lineSlice(turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), feature); + let intersection = turf.lineSlice( + turf.point(intersectionPointsArray[0]), + turf.point(intersectionPointsArray[1]), + feature + ) if (intersection == null) { - return null; + return null } - const intersectionSize = turf.length(intersection); // in km + const intersectionSize = turf.length(intersection) // in km return intersectionSize * 1000 - - } if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") { - const otherFeatureBBox = BBox.get(otherFeature); + const otherFeatureBBox = BBox.get(otherFeature) const overlaps = featureBBox.overlapsWith(otherFeatureBBox) if (!overlaps) { - return null; + return null } if (otherFeature.geometry.type === "LineString") { - return this.calculateInstersection(otherFeature, feature, otherFeatureBBox, featureBBox) + return this.calculateInstersection( + otherFeature, + feature, + otherFeatureBBox, + featureBBox + ) } try { - - const intersection = turf.intersect(feature, otherFeature); + const intersection = turf.intersect(feature, otherFeature) if (intersection == null) { - return null; + return null } - return turf.area(intersection); // in m² + return turf.area(intersection) // in m² } catch (e) { if (e.message === "Each LinearRing of a Polygon must have 4 or more Positions.") { // WORKAROUND TIME! // See https://github.com/Turfjs/turf/pull/2238 - return null; + return null } - throw e; + throw e } - } throw "CalculateIntersection fallthrough: can not calculate an intersection between features" - } /** @@ -742,7 +747,7 @@ export class GeoOperations { /** * Returns 'true' if one feature contains the other feature - * + * * const pond: Feature = { * "type": "Feature", * "properties": {"natural":"water","water":"pond"}, @@ -769,9 +774,10 @@ export class GeoOperations { * GeoOperations.completelyWithin(pond, park) // => true * GeoOperations.completelyWithin(park, pond) // => false */ - static completelyWithin(feature: Feature, possiblyEncloingFeature: Feature) : boolean { - return booleanWithin(feature, possiblyEncloingFeature); + static completelyWithin( + feature: Feature, + possiblyEncloingFeature: Feature + ): boolean { + return booleanWithin(feature, possiblyEncloingFeature) } } - - diff --git a/Logic/ImageProviders/AllImageProviders.ts b/Logic/ImageProviders/AllImageProviders.ts index 413e51375..770da2fbf 100644 --- a/Logic/ImageProviders/AllImageProviders.ts +++ b/Logic/ImageProviders/AllImageProviders.ts @@ -1,45 +1,52 @@ -import {Mapillary} from "./Mapillary"; -import {WikimediaImageProvider} from "./WikimediaImageProvider"; -import {Imgur} from "./Imgur"; -import GenericImageProvider from "./GenericImageProvider"; -import {Store, UIEventSource} from "../UIEventSource"; -import ImageProvider, {ProvidedImage} from "./ImageProvider"; -import {WikidataImageProvider} from "./WikidataImageProvider"; +import { Mapillary } from "./Mapillary" +import { WikimediaImageProvider } from "./WikimediaImageProvider" +import { Imgur } from "./Imgur" +import GenericImageProvider from "./GenericImageProvider" +import { Store, UIEventSource } from "../UIEventSource" +import ImageProvider, { ProvidedImage } from "./ImageProvider" +import { WikidataImageProvider } from "./WikidataImageProvider" /** * A generic 'from the interwebz' image picker, without attribution */ export default class AllImageProviders { - public static ImageAttributionSource: ImageProvider[] = [ Imgur.singleton, Mapillary.singleton, WikidataImageProvider.singleton, WikimediaImageProvider.singleton, new GenericImageProvider( - [].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes) - ) + [].concat( + ...Imgur.defaultValuePrefix, + ...WikimediaImageProvider.commonsPrefixes, + ...Mapillary.valuePrefixes + ) + ), ] - private static providersByName= { - "imgur": Imgur.singleton, -"mapillary": Mapillary.singleton, - "wikidata": WikidataImageProvider.singleton, - "wikimedia": WikimediaImageProvider.singleton + private static providersByName = { + imgur: Imgur.singleton, + mapillary: Mapillary.singleton, + wikidata: WikidataImageProvider.singleton, + wikimedia: WikimediaImageProvider.singleton, } - - public static byName(name: string){ + + public static byName(name: string) { return AllImageProviders.providersByName[name.toLowerCase()] } - public static defaultKeys = [].concat(AllImageProviders.ImageAttributionSource.map(provider => provider.defaultKeyPrefixes)) + public static defaultKeys = [].concat( + AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes) + ) - - private static _cache: Map> = new Map>() + private static _cache: Map> = new Map< + string, + UIEventSource + >() public static LoadImagesFor(tags: Store, tagKey?: string[]): Store { if (tags.data.id === undefined) { - return undefined; + return undefined } const cacheKey = tags.data.id + tagKey @@ -48,23 +55,21 @@ export default class AllImageProviders { return cached } - const source = new UIEventSource([]) this._cache.set(cacheKey, source) const allSources = [] for (const imageProvider of AllImageProviders.ImageAttributionSource) { - let prefixes = imageProvider.defaultKeyPrefixes if (tagKey !== undefined) { prefixes = tagKey } const singleSource = imageProvider.GetRelevantUrls(tags, { - prefixes: prefixes + prefixes: prefixes, }) allSources.push(singleSource) - singleSource.addCallbackAndRunD(_ => { - const all: ProvidedImage[] = [].concat(...allSources.map(source => source.data)) + singleSource.addCallbackAndRunD((_) => { + const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) const uniq = [] const seen = new Set() for (const img of all) { @@ -77,7 +82,6 @@ export default class AllImageProviders { source.setData(uniq) }) } - return source; + return source } - -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/GenericImageProvider.ts b/Logic/ImageProviders/GenericImageProvider.ts index ab35a6501..4cb382b23 100644 --- a/Logic/ImageProviders/GenericImageProvider.ts +++ b/Logic/ImageProviders/GenericImageProvider.ts @@ -1,18 +1,17 @@ -import ImageProvider, {ProvidedImage} from "./ImageProvider"; +import ImageProvider, { ProvidedImage } from "./ImageProvider" export default class GenericImageProvider extends ImageProvider { - public defaultKeyPrefixes: string[] = ["image"]; + public defaultKeyPrefixes: string[] = ["image"] - private readonly _valuePrefixBlacklist: string[]; + private readonly _valuePrefixBlacklist: string[] public constructor(valuePrefixBlacklist: string[]) { - super(); - this._valuePrefixBlacklist = valuePrefixBlacklist; + super() + this._valuePrefixBlacklist = valuePrefixBlacklist } async ExtractUrls(key: string, value: string): Promise[]> { - - if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) { + if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) { return [] } @@ -23,20 +22,20 @@ export default class GenericImageProvider extends ImageProvider { return [] } - return [Promise.resolve({ - key: key, - url: value, - provider: this - })] + return [ + Promise.resolve({ + key: key, + url: value, + provider: this, + }), + ] } SourceIcon(backlinkSource?: string) { - return undefined; + return undefined } public DownloadAttribution(url: string) { return undefined } - - -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/ImageProvider.ts b/Logic/ImageProviders/ImageProvider.ts index d73e75fd8..92d3bc941 100644 --- a/Logic/ImageProviders/ImageProvider.ts +++ b/Logic/ImageProviders/ImageProvider.ts @@ -1,50 +1,53 @@ -import {Store, UIEventSource} from "../UIEventSource"; -import BaseUIElement from "../../UI/BaseUIElement"; -import {LicenseInfo} from "./LicenseInfo"; -import {Utils} from "../../Utils"; +import { Store, UIEventSource } from "../UIEventSource" +import BaseUIElement from "../../UI/BaseUIElement" +import { LicenseInfo } from "./LicenseInfo" +import { Utils } from "../../Utils" export interface ProvidedImage { - url: string, - key: string, + url: string + key: string provider: ImageProvider } export default abstract class ImageProvider { - public abstract readonly defaultKeyPrefixes: string[] - - public abstract SourceIcon(backlinkSource?: string): BaseUIElement; + + public abstract SourceIcon(backlinkSource?: string): BaseUIElement /** * Given a properies object, maps it onto _all_ the available pictures for this imageProvider */ - public GetRelevantUrls(allTags: Store, options?: { - prefixes?: string[] - }): UIEventSource { + public GetRelevantUrls( + allTags: Store, + options?: { + prefixes?: string[] + } + ): UIEventSource { const prefixes = options?.prefixes ?? this.defaultKeyPrefixes if (prefixes === undefined) { throw "No `defaultKeyPrefixes` defined by this image provider" } - const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([]) + const relevantUrls = new UIEventSource< + { url: string; key: string; provider: ImageProvider }[] + >([]) const seenValues = new Set() - allTags.addCallbackAndRunD(tags => { + allTags.addCallbackAndRunD((tags) => { for (const key in tags) { - if (!prefixes.some(prefix => key.startsWith(prefix))) { + if (!prefixes.some((prefix) => key.startsWith(prefix))) { continue } - const values = Utils.NoEmpty(tags[key]?.split(";")?.map(v => v.trim()) ?? []) + const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) for (const value of values) { - if (seenValues.has(value)) { continue } seenValues.add(value) - this.ExtractUrls(key, value).then(promises => { + this.ExtractUrls(key, value).then((promises) => { for (const promise of promises ?? []) { if (promise === undefined) { continue } - promise.then(providedImage => { + promise.then((providedImage) => { if (providedImage === undefined) { return } @@ -54,15 +57,12 @@ export default abstract class ImageProvider { } }) } - - } }) return relevantUrls } - public abstract ExtractUrls(key: string, value: string): Promise[]>; + public abstract ExtractUrls(key: string, value: string): Promise[]> - public abstract DownloadAttribution(url: string): Promise; - -} \ No newline at end of file + public abstract DownloadAttribution(url: string): Promise +} diff --git a/Logic/ImageProviders/Imgur.ts b/Logic/ImageProviders/Imgur.ts index 0e5841c98..f4a491d68 100644 --- a/Logic/ImageProviders/Imgur.ts +++ b/Logic/ImageProviders/Imgur.ts @@ -1,93 +1,105 @@ -import ImageProvider, { ProvidedImage } from "./ImageProvider"; -import BaseUIElement from "../../UI/BaseUIElement"; -import {Utils} from "../../Utils"; -import Constants from "../../Models/Constants"; -import {LicenseInfo} from "./LicenseInfo"; +import ImageProvider, { ProvidedImage } from "./ImageProvider" +import BaseUIElement from "../../UI/BaseUIElement" +import { Utils } from "../../Utils" +import Constants from "../../Models/Constants" +import { LicenseInfo } from "./LicenseInfo" export class Imgur extends ImageProvider { - public static readonly defaultValuePrefix = ["https://i.imgur.com"] - public static readonly singleton = new Imgur(); - public readonly defaultKeyPrefixes: string[] = ["image"]; + public static readonly singleton = new Imgur() + public readonly defaultKeyPrefixes: string[] = ["image"] private constructor() { - super(); + super() } static uploadMultiple( - title: string, description: string, blobs: FileList, - handleSuccessfullUpload: ((imageURL: string) => Promise), - allDone: (() => void), - onFail: ((reason: string) => void), - offset: number = 0) { - + title: string, + description: string, + blobs: FileList, + handleSuccessfullUpload: (imageURL: string) => Promise, + allDone: () => void, + onFail: (reason: string) => void, + offset: number = 0 + ) { if (blobs.length == offset) { - allDone(); - return; + allDone() + return } - const blob = blobs.item(offset); - const self = this; - this.uploadImage(title, description, blob, + const blob = blobs.item(offset) + const self = this + this.uploadImage( + title, + description, + blob, async (imageUrl) => { - await handleSuccessfullUpload(imageUrl); + await handleSuccessfullUpload(imageUrl) self.uploadMultiple( - title, description, blobs, + title, + description, + blobs, handleSuccessfullUpload, allDone, onFail, - offset + 1); + offset + 1 + ) }, onFail - ); - - + ) } - static uploadImage(title: string, description: string, blob: File, - handleSuccessfullUpload: ((imageURL: string) => Promise), - onFail: (reason: string) => void) { + static uploadImage( + title: string, + description: string, + blob: File, + handleSuccessfullUpload: (imageURL: string) => Promise, + onFail: (reason: string) => void + ) { + const apiUrl = "https://api.imgur.com/3/image" + const apiKey = Constants.ImgurApiKey - const apiUrl = 'https://api.imgur.com/3/image'; - const apiKey = Constants.ImgurApiKey; - - const formData = new FormData(); - formData.append('image', blob); - formData.append("title", title); + const formData = new FormData() + formData.append("image", blob) + formData.append("title", title) formData.append("description", description) const settings: RequestInit = { - method: 'POST', + method: "POST", body: formData, - redirect: 'follow', + redirect: "follow", headers: new Headers({ Authorization: `Client-ID ${apiKey}`, - Accept: 'application/json', + Accept: "application/json", }), - }; + } // Response contains stringified JSON // Image URL available at response.data.link - fetch(apiUrl, settings).then(async function (response) { - const content = await response.json() - await handleSuccessfullUpload(content.data.link); - }).catch((reason) => { - console.log("Uploading to IMGUR failed", reason); - // @ts-ignore - onFail(reason); - }); + fetch(apiUrl, settings) + .then(async function (response) { + const content = await response.json() + await handleSuccessfullUpload(content.data.link) + }) + .catch((reason) => { + console.log("Uploading to IMGUR failed", reason) + // @ts-ignore + onFail(reason) + }) } SourceIcon(): BaseUIElement { - return undefined; + return undefined } public async ExtractUrls(key: string, value: string): Promise[]> { - if (Imgur.defaultValuePrefix.some(prefix => value.startsWith(prefix))) { - return [Promise.resolve({ - url: value, - key: key, - provider: this - })] + if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) { + return [ + Promise.resolve({ + url: value, + key: key, + provider: this, + }), + ] } return [] } @@ -103,29 +115,27 @@ export class Imgur extends ImageProvider { * expected.artist = "Pieter Vander Vennet" * licenseInfo // => expected */ - public async DownloadAttribution (url: string) : Promise { - const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]; + public async DownloadAttribution(url: string): Promise { + const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0] - const apiUrl = 'https://api.imgur.com/3/image/' + hash; - const response = await Utils.downloadJsonCached(apiUrl, 365*24*60*60, - {Authorization: 'Client-ID ' + Constants.ImgurApiKey}) + const apiUrl = "https://api.imgur.com/3/image/" + hash + const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, { + Authorization: "Client-ID " + Constants.ImgurApiKey, + }) - const descr: string = response.data.description ?? ""; - const data: any = {}; + const descr: string = response.data.description ?? "" + const data: any = {} for (const tag of descr.split("\n")) { - const kv = tag.split(":"); - const k = kv[0]; - data[k] = kv[1]?.replace(/\r/g, ""); + const kv = tag.split(":") + const k = kv[0] + data[k] = kv[1]?.replace(/\r/g, "") } + const licenseInfo = new LicenseInfo() - const licenseInfo = new LicenseInfo(); - - licenseInfo.licenseShortName = data.license; - licenseInfo.artist = data.author; + licenseInfo.licenseShortName = data.license + licenseInfo.artist = data.author return licenseInfo } - - -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/ImgurUploader.ts b/Logic/ImageProviders/ImgurUploader.ts index 0e98364fc..bb4fc6a9f 100644 --- a/Logic/ImageProviders/ImgurUploader.ts +++ b/Logic/ImageProviders/ImgurUploader.ts @@ -1,16 +1,15 @@ -import {UIEventSource} from "../UIEventSource"; -import {Imgur} from "./Imgur"; +import { UIEventSource } from "../UIEventSource" +import { Imgur } from "./Imgur" export default class ImgurUploader { - - public readonly queue: UIEventSource = new UIEventSource([]); - public readonly failed: UIEventSource = new UIEventSource([]); - public readonly success: UIEventSource = new UIEventSource([]); - public maxFileSizeInMegabytes = 10; - private readonly _handleSuccessUrl: (string) => Promise; + public readonly queue: UIEventSource = new UIEventSource([]) + public readonly failed: UIEventSource = new UIEventSource([]) + public readonly success: UIEventSource = new UIEventSource([]) + public maxFileSizeInMegabytes = 10 + private readonly _handleSuccessUrl: (string) => Promise constructor(handleSuccessUrl: (string) => Promise) { - this._handleSuccessUrl = handleSuccessUrl; + this._handleSuccessUrl = handleSuccessUrl } public uploadMany(title: string, description: string, files: FileList): void { @@ -19,25 +18,26 @@ export default class ImgurUploader { } this.queue.ping() - const self = this; + const self = this this.queue.setData([...self.queue.data]) - Imgur.uploadMultiple(title, + Imgur.uploadMultiple( + title, description, files, async function (url) { - console.log("File saved at", url); + console.log("File saved at", url) self.success.data.push(url) - self.success.ping(); - await self._handleSuccessUrl(url); + self.success.ping() + await self._handleSuccessUrl(url) }, function () { - console.log("All uploads completed"); + console.log("All uploads completed") }, function (failReason) { console.log("Upload failed due to ", failReason) self.failed.setData([...self.failed.data, failReason]) } - ); + ) } -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/LicenseInfo.ts b/Logic/ImageProviders/LicenseInfo.ts index 0a9f837de..a75a1ad98 100644 --- a/Logic/ImageProviders/LicenseInfo.ts +++ b/Logic/ImageProviders/LicenseInfo.ts @@ -1,12 +1,12 @@ export class LicenseInfo { title: string = "" - artist: string = ""; - license: string = undefined; - licenseShortName: string = ""; - usageTerms: string = ""; - attributionRequired: boolean = false; - copyrighted: boolean = false; - credit: string = ""; - description: string = ""; + artist: string = "" + license: string = undefined + licenseShortName: string = "" + usageTerms: string = "" + attributionRequired: boolean = false + copyrighted: boolean = false + credit: string = "" + description: string = "" informationLocation: URL = undefined -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts index ffbf14945..1fbbbc145 100644 --- a/Logic/ImageProviders/Mapillary.ts +++ b/Logic/ImageProviders/Mapillary.ts @@ -1,21 +1,26 @@ -import ImageProvider, {ProvidedImage} from "./ImageProvider"; -import BaseUIElement from "../../UI/BaseUIElement"; -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; -import {LicenseInfo} from "./LicenseInfo"; -import Constants from "../../Models/Constants"; +import ImageProvider, { ProvidedImage } from "./ImageProvider" +import BaseUIElement from "../../UI/BaseUIElement" +import Svg from "../../Svg" +import { Utils } from "../../Utils" +import { LicenseInfo } from "./LicenseInfo" +import Constants from "../../Models/Constants" export class Mapillary extends ImageProvider { - - public static readonly singleton = new Mapillary(); + public static readonly singleton = new Mapillary() private static readonly valuePrefix = "https://a.mapillary.com" - public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com", "https://mapillary.com", "http://www.mapillary.com", "https://www.mapillary.com"] + public static readonly valuePrefixes = [ + Mapillary.valuePrefix, + "http://mapillary.com", + "https://mapillary.com", + "http://www.mapillary.com", + "https://www.mapillary.com", + ] defaultKeyPrefixes = ["mapillary", "image"] /** * Indicates that this is the same URL * Ignores 'stp' parameter - * + * * const a = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s1024x768&ccb=10-5&oh=00_AT-ZGTXHzihoaQYBILmEiAEKR64z_IWiTlcAYq_D7Ka0-Q&oe=6278C456&_nc_sid=122ab1" * const b = "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8xm5SGLt20ETziNqzhhBd8b8S5GHLiIu8N6BbyqHFohFAQoaJJPG8i5yQiSwjYmEqXSfVeoCmpiyBJICEkQK98JOB21kkJoBS8VdhYa-Ty93lBnznQyesJBtKcb32foGut2Hgt10hEMWJbE3dDgA?stp=s256x192&ccb=10-5&oh=00_AT9BZ1Rpc9zbY_uNu92A_4gj1joiy1b6VtgtLIu_7wh9Bg&oe=6278C456&_nc_sid=122ab1" * Mapillary.sameUrl(a, b) => true @@ -28,9 +33,9 @@ export class Mapillary extends ImageProvider { const aUrl = new URL(a) const bUrl = new URL(b) if (aUrl.host !== bUrl.host || aUrl.pathname !== bUrl.pathname) { - return false; + return false } - let allSame = true; + let allSame = true aUrl.searchParams.forEach((value, key) => { if (key === "stp") { // This is the key indicating the image size on mapillary; we ignore it @@ -41,20 +46,18 @@ export class Mapillary extends ImageProvider { return } }) - return allSame; - + return allSame } catch (e) { console.debug("Could not compare ", a, "and", b, "due to", e) } - return false; - + return false } /** * Returns the correct key for API v4.0 */ private static ExtractKeyFromURL(value: string): number { - let key: string; + let key: string const newApiFormat = value.match(/https?:\/\/www.mapillary.com\/app\/\?pKey=([0-9]*)/) if (newApiFormat !== null) { @@ -62,7 +65,7 @@ export class Mapillary extends ImageProvider { } else if (value.startsWith(Mapillary.valuePrefix)) { key = value.substring(0, value.lastIndexOf("?")).substring(value.lastIndexOf("/") + 1) } else if (value.match("[0-9]*")) { - key = value; + key = value } const keyAsNumber = Number(key) @@ -74,7 +77,7 @@ export class Mapillary extends ImageProvider { } SourceIcon(backlinkSource?: string): BaseUIElement { - return Svg.mapillary_svg(); + return Svg.mapillary_svg() } async ExtractUrls(key: string, value: string): Promise[]> { @@ -83,26 +86,30 @@ export class Mapillary extends ImageProvider { public async DownloadAttribution(url: string): Promise { const license = new LicenseInfo() - license.artist = "Contributor name unavailable"; - license.license = "CC BY-SA 4.0"; + license.artist = "Contributor name unavailable" + license.license = "CC BY-SA 4.0" // license.license = "Creative Commons Attribution-ShareAlike 4.0 International License"; - license.attributionRequired = true; + license.attributionRequired = true return license } private async PrepareUrlAsync(key: string, value: string): Promise { const mapillaryId = Mapillary.ExtractKeyFromURL(value) if (mapillaryId === undefined) { - return undefined; + return undefined } - const metadataUrl = 'https://graph.mapillary.com/' + mapillaryId + '?fields=thumb_1024_url&&access_token=' + Constants.mapillary_client_token_v4; - const response = await Utils.downloadJsonCached(metadataUrl,60*60) - const url = response["thumb_1024_url"]; + const metadataUrl = + "https://graph.mapillary.com/" + + mapillaryId + + "?fields=thumb_1024_url&&access_token=" + + Constants.mapillary_client_token_v4 + const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60) + const url = response["thumb_1024_url"] return { url: url, provider: this, - key: key + key: key, } } -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/WikidataImageProvider.ts b/Logic/ImageProviders/WikidataImageProvider.ts index 4bcb7c2fe..093153ab5 100644 --- a/Logic/ImageProviders/WikidataImageProvider.ts +++ b/Logic/ImageProviders/WikidataImageProvider.ts @@ -1,11 +1,10 @@ -import ImageProvider, {ProvidedImage} from "./ImageProvider"; -import BaseUIElement from "../../UI/BaseUIElement"; -import Svg from "../../Svg"; -import {WikimediaImageProvider} from "./WikimediaImageProvider"; -import Wikidata from "../Web/Wikidata"; +import ImageProvider, { ProvidedImage } from "./ImageProvider" +import BaseUIElement from "../../UI/BaseUIElement" +import Svg from "../../Svg" +import { WikimediaImageProvider } from "./WikimediaImageProvider" +import Wikidata from "../Web/Wikidata" export class WikidataImageProvider extends ImageProvider { - public static readonly singleton = new WikidataImageProvider() public readonly defaultKeyPrefixes = ["wikidata"] @@ -14,7 +13,7 @@ export class WikidataImageProvider extends ImageProvider { } public SourceIcon(backlinkSource?: string): BaseUIElement { - throw Svg.wikidata_svg(); + throw Svg.wikidata_svg() } public async ExtractUrls(key: string, value: string): Promise[]> { @@ -39,7 +38,10 @@ export class WikidataImageProvider extends ImageProvider { } const commons = entity.commons - if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) { + if ( + commons !== undefined && + (commons.startsWith("Category:") || commons.startsWith("File:")) + ) { const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) allImages.push(...promises) } @@ -47,7 +49,6 @@ export class WikidataImageProvider extends ImageProvider { } public DownloadAttribution(url: string): Promise { - throw new Error("Method not implemented; shouldn't be needed!"); + throw new Error("Method not implemented; shouldn't be needed!") } - -} \ No newline at end of file +} diff --git a/Logic/ImageProviders/WikimediaImageProvider.ts b/Logic/ImageProviders/WikimediaImageProvider.ts index 305070ffe..5d6dbbdbd 100644 --- a/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/Logic/ImageProviders/WikimediaImageProvider.ts @@ -1,45 +1,47 @@ -import ImageProvider, {ProvidedImage} from "./ImageProvider"; -import BaseUIElement from "../../UI/BaseUIElement"; -import Svg from "../../Svg"; -import Link from "../../UI/Base/Link"; -import {Utils} from "../../Utils"; -import {LicenseInfo} from "./LicenseInfo"; -import Wikimedia from "../Web/Wikimedia"; +import ImageProvider, { ProvidedImage } from "./ImageProvider" +import BaseUIElement from "../../UI/BaseUIElement" +import Svg from "../../Svg" +import Link from "../../UI/Base/Link" +import { Utils } from "../../Utils" +import { LicenseInfo } from "./LicenseInfo" +import Wikimedia from "../Web/Wikimedia" /** * This module provides endpoints for wikimedia and others */ export class WikimediaImageProvider extends ImageProvider { - - - public static readonly singleton = new WikimediaImageProvider(); - public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"] + public static readonly singleton = new WikimediaImageProvider() + public static readonly commonsPrefixes = [ + "https://commons.wikimedia.org/wiki/", + "https://upload.wikimedia.org", + "File:", + ] private readonly commons_key = "wikimedia_commons" public readonly defaultKeyPrefixes = [this.commons_key, "image"] private constructor() { - super(); + super() } private static ExtractFileName(url: string) { if (!url.startsWith("http")) { - return url; + return url } const path = new URL(url).pathname - return path.substring(path.lastIndexOf("/") + 1); - + return path.substring(path.lastIndexOf("/") + 1) } private static PrepareUrl(value: string): string { - if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) { - return value; + return value } - return (`https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(value)}?width=500&height=400`) + return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent( + value + )}?width=500&height=400` } private static startsWithCommonsPrefix(value: string): boolean { - return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix)) + return WikimediaImageProvider.commonsPrefixes.some((prefix) => value.startsWith(prefix)) } private static removeCommonsPrefix(value: string): string { @@ -49,7 +51,7 @@ export class WikimediaImageProvider extends ImageProvider { if (!value.startsWith("File:")) { value = "File:" + value } - return value; + return value } for (const prefix of WikimediaImageProvider.commonsPrefixes) { @@ -61,21 +63,20 @@ export class WikimediaImageProvider extends ImageProvider { return part } } - return value; + return value } SourceIcon(backlink: string): BaseUIElement { - const img = Svg.wikimedia_commons_white_svg() - .SetStyle("width:2em;height: 2em"); + const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") if (backlink === undefined) { return img } - - return new Link(Svg.wikimedia_commons_white_img, - `https://commons.wikimedia.org/wiki/${backlink}`, true) - - + return new Link( + Svg.wikimedia_commons_white_img, + `https://commons.wikimedia.org/wiki/${backlink}`, + true + ) } public PrepUrl(value: string): ProvidedImage { @@ -99,7 +100,9 @@ export class WikimediaImageProvider extends ImageProvider { value = WikimediaImageProvider.removeCommonsPrefix(value) if (value.startsWith("Category:")) { const urls = await Wikimedia.GetCategoryContents(value) - return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image))) + return urls + .filter((url) => url.startsWith("File:")) + .map((image) => Promise.resolve(this.UrlForImage(image))) } if (value.startsWith("File:")) { return [Promise.resolve(this.UrlForImage(value))] @@ -116,24 +119,30 @@ export class WikimediaImageProvider extends ImageProvider { filename = WikimediaImageProvider.ExtractFileName(filename) if (filename === "") { - return undefined; + return undefined } - const url = "https://en.wikipedia.org/w/" + + const url = + "https://en.wikipedia.org/w/" + "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + - "titles=" + filename + - "&format=json&origin=*"; - const data = await Utils.downloadJsonCached(url,365*24*60*60) - const licenseInfo = new LicenseInfo(); + "titles=" + + filename + + "&format=json&origin=*" + const data = await Utils.downloadJsonCached(url, 365 * 24 * 60 * 60) + const licenseInfo = new LicenseInfo() const pageInfo = data.query.pages[-1] if (pageInfo === undefined) { - return undefined; + return undefined } - - const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata; + + const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata if (license === undefined) { - console.warn("The file", filename, "has no usable metedata or license attached... Please fix the license info file yourself!") - return undefined; + console.warn( + "The file", + filename, + "has no usable metedata or license attached... Please fix the license info file yourself!" + ) + return undefined } let title = pageInfo.title @@ -145,26 +154,22 @@ export class WikimediaImageProvider extends ImageProvider { } licenseInfo.title = title - licenseInfo.artist = license.Artist?.value; - licenseInfo.license = license.License?.value; - licenseInfo.copyrighted = license.Copyrighted?.value; - licenseInfo.attributionRequired = license.AttributionRequired?.value; - licenseInfo.usageTerms = license.UsageTerms?.value; - licenseInfo.licenseShortName = license.LicenseShortName?.value; - licenseInfo.credit = license.Credit?.value; - licenseInfo.description = license.ImageDescription?.value; - licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/"+pageInfo.title) - return licenseInfo; - + licenseInfo.artist = license.Artist?.value + licenseInfo.license = license.License?.value + licenseInfo.copyrighted = license.Copyrighted?.value + licenseInfo.attributionRequired = license.AttributionRequired?.value + licenseInfo.usageTerms = license.UsageTerms?.value + licenseInfo.licenseShortName = license.LicenseShortName?.value + licenseInfo.credit = license.Credit?.value + licenseInfo.description = license.ImageDescription?.value + licenseInfo.informationLocation = new URL("https://en.wikipedia.org/wiki/" + pageInfo.title) + return licenseInfo } private UrlForImage(image: string): ProvidedImage { if (!image.startsWith("File:")) { image = "File:" + image } - return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this} + return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this } } - - } - diff --git a/Logic/Maproulette.ts b/Logic/Maproulette.ts index 470bb75d8..abaf9e65b 100644 --- a/Logic/Maproulette.ts +++ b/Logic/Maproulette.ts @@ -1,39 +1,39 @@ -import Constants from "../Models/Constants"; +import Constants from "../Models/Constants" export default class Maproulette { - /** - * The API endpoint to use - */ - endpoint: string; + /** + * The API endpoint to use + */ + endpoint: string - /** - * The API key to use for all requests - */ - private apiKey: string; + /** + * The API key to use for all requests + */ + private apiKey: string - /** - * Creates a new Maproulette instance - * @param endpoint The API endpoint to use - */ - constructor(endpoint: string = "https://maproulette.org/api/v2") { - this.endpoint = endpoint; - this.apiKey = Constants.MaprouletteApiKey; - } - - /** - * Close a task - * @param taskId The task to close - */ - async closeTask(taskId: number): Promise { - const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "apiKey": this.apiKey, - }, - }); - if (response.status !== 304) { - console.log(`Failed to close task: ${response.status}`); + /** + * Creates a new Maproulette instance + * @param endpoint The API endpoint to use + */ + constructor(endpoint: string = "https://maproulette.org/api/v2") { + this.endpoint = endpoint + this.apiKey = Constants.MaprouletteApiKey + } + + /** + * Close a task + * @param taskId The task to close + */ + async closeTask(taskId: number): Promise { + const response = await fetch(`${this.endpoint}/task/${taskId}/1`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + apiKey: this.apiKey, + }, + }) + if (response.status !== 304) { + console.log(`Failed to close task: ${response.status}`) + } } - } } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index f7224689f..10d5c285c 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -1,8 +1,7 @@ -import SimpleMetaTaggers, {SimpleMetaTagger} from "./SimpleMetaTagger"; -import {ExtraFuncParams, ExtraFunctions} from "./ExtraFunctions"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import {ElementStorage} from "./ElementStorage"; - +import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger" +import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import { ElementStorage } from "./ElementStorage" /** * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... @@ -10,10 +9,8 @@ import {ElementStorage} from "./ElementStorage"; * All metatags start with an underscore */ export default class MetaTagging { - - - private static errorPrintCount = 0; - private static readonly stopErrorOutputAt = 10; + private static errorPrintCount = 0 + private static readonly stopErrorOutputAt = 10 private static retaggingFuncCache = new Map void)[]>() /** @@ -22,17 +19,19 @@ export default class MetaTagging { * * Returns true if at least one feature has changed properties */ - public static addMetatags(features: { feature: any; freshness: Date }[], - params: ExtraFuncParams, - layer: LayerConfig, - state?: { allElements?: ElementStorage }, - options?: { - includeDates?: true | boolean, - includeNonDates?: true | boolean, - evaluateStrict?: false | boolean - }): boolean { + public static addMetatags( + features: { feature: any; freshness: Date }[], + params: ExtraFuncParams, + layer: LayerConfig, + state?: { allElements?: ElementStorage }, + options?: { + includeDates?: true | boolean + includeNonDates?: true | boolean + evaluateStrict?: false | boolean + } + ): boolean { if (features === undefined || features.length === 0) { - return; + return } console.log("Recalculating metatags...") @@ -52,51 +51,62 @@ export default class MetaTagging { // The calculated functions - per layer - which add the new keys const layerFuncs = this.createRetaggingFunc(layer, state) - let atLeastOneFeatureChanged = false; + let atLeastOneFeatureChanged = false for (let i = 0; i < features.length; i++) { - const ff = features[i]; + const ff = features[i] const feature = ff.feature const freshness = ff.freshness let somethingChanged = false let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) for (const metatag of metatagsToApply) { try { - if (!metatag.keys.some(key => feature.properties[key] === undefined)) { + if (!metatag.keys.some((key) => feature.properties[key] === undefined)) { // All keys are already defined, we probably already ran this one continue } if (metatag.isLazy) { - if (!metatag.keys.some(key => !definedTags.has(key))) { + if (!metatag.keys.some((key) => !definedTags.has(key))) { // All keys are defined - lets skip! continue } - somethingChanged = true; + somethingChanged = true metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) - if(options?.evaluateStrict){ + if (options?.evaluateStrict) { for (const key of metatag.keys) { feature.properties[key] } } } else { - const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer, state) + const newValueAdded = metatag.applyMetaTagsOnFeature( + feature, + freshness, + layer, + state + ) /* Note that the expression: - * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` - * Is WRONG - * - * IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR, - * thus not running an update! - */ + * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` + * Is WRONG + * + * IF something changed is `true` due to an earlier run, it will short-circuit and _not_ evaluate the right hand of the OR, + * thus not running an update! + */ somethingChanged = newValueAdded || somethingChanged } } catch (e) { - console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e, e.stack) + console.error( + "Could not calculate metatag for ", + metatag.keys.join(","), + ":", + e, + e.stack + ) } } if (layerFuncs !== undefined) { - let retaggingChanged = false; + let retaggingChanged = false try { retaggingChanged = layerFuncs(params, feature) } catch (e) { @@ -113,42 +123,62 @@ export default class MetaTagging { return atLeastOneFeatureChanged } - private static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { - const functions: ((feature: any) => any)[] = []; + private static createFunctionsForFeature( + layerId: string, + calculatedTags: [string, string, boolean][] + ): ((feature: any) => void)[] { + const functions: ((feature: any) => any)[] = [] for (const entry of calculatedTags) { const key = entry[0] - const code = entry[1]; + const code = entry[1] const isStrict = entry[2] if (code === undefined) { - continue; + continue } - const calculateAndAssign: ((feat: any) => any) = (feat) => { + const calculateAndAssign: (feat: any) => any = (feat) => { try { - let result = new Function("feat", "return " + code + ";")(feat); + let result = new Function("feat", "return " + code + ";")(feat) if (result === "") { result === undefined } if (result !== undefined && typeof result !== "string") { // Make sure it is a string! - result = JSON.stringify(result); + result = JSON.stringify(result) } delete feat.properties[key] - feat.properties[key] = result; + feat.properties[key] = result return result } catch (e) { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { - console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack) - MetaTagging.errorPrintCount++; + console.warn( + "Could not calculate a " + + (isStrict ? "strict " : "") + + " calculated tag for key " + + key + + " defined by " + + code + + " (in layer" + + layerId + + ") due to \n" + + e + + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", + e, + e.stack + ) + MetaTagging.errorPrintCount++ if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { - console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") + console.error( + "Got ", + MetaTagging.stopErrorOutputAt, + " errors calculating this metatagging - stopping output now" + ) } } - return undefined; + return undefined } } - if (isStrict) { functions.push(calculateAndAssign) continue @@ -162,15 +192,14 @@ export default class MetaTagging { enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this get: function () { return calculateAndAssign(feature) - } + }, }) return undefined } - functions.push(f) } - return functions; + return functions } /** @@ -179,39 +208,37 @@ export default class MetaTagging { * @param state * @private */ - private static createRetaggingFunc(layer: LayerConfig, state): - ((params: ExtraFuncParams, feature: any) => boolean) { - - const calculatedTags: [string, string, boolean][] = layer.calculatedTags; + private static createRetaggingFunc( + layer: LayerConfig, + state + ): (params: ExtraFuncParams, feature: any) => boolean { + const calculatedTags: [string, string, boolean][] = layer.calculatedTags if (calculatedTags === undefined || calculatedTags.length === 0) { - return undefined; + return undefined } - let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id); + let functions: ((feature: any) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) if (functions === undefined) { functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) MetaTagging.retaggingFuncCache.set(layer.id, functions) } - return (params: ExtraFuncParams, feature) => { const tags = feature.properties if (tags === undefined) { - return; + return } try { - ExtraFunctions.FullPatchFeature(params, feature); + ExtraFunctions.FullPatchFeature(params, feature) for (const f of functions) { - f(feature); + f(feature) } - state?.allElements?.getEventSourceById(feature.properties.id)?.ping(); + state?.allElements?.getEventSourceById(feature.properties.id)?.ping() } catch (e) { console.error("Invalid syntax in calculated tags or some other error: ", e) } - return true; // Something changed + return true // Something changed } } - - } diff --git a/Logic/Osm/Actions/ChangeDescription.ts b/Logic/Osm/Actions/ChangeDescription.ts index 1346e6201..844ba01b4 100644 --- a/Logic/Osm/Actions/ChangeDescription.ts +++ b/Logic/Osm/Actions/ChangeDescription.ts @@ -1,18 +1,17 @@ -import {OsmNode, OsmRelation, OsmWay} from "../OsmObject"; +import { OsmNode, OsmRelation, OsmWay } from "../OsmObject" /** * Represents a single change to an object */ export interface ChangeDescription { - /** * Metadata to be included in the changeset */ meta: { /* - * The theme with which this changeset was made - */ - theme: string, + * The theme with which this changeset was made + */ + theme: string /** * The type of the change */ @@ -20,22 +19,22 @@ export interface ChangeDescription { /** * THe motivation for the change, e.g. 'deleted because does not exist anymore' */ - specialMotivation?: string, + specialMotivation?: string /** * Added by Changes.ts */ distanceToObject?: number - }, + } /** * Identifier of the object */ - type: "node" | "way" | "relation", + type: "node" | "way" | "relation" /** * Identifier of the object * Negative for new objects */ - id: number, + id: number /** * All changes to tags @@ -43,7 +42,7 @@ export interface ChangeDescription { * * Note that this list will only contain the _changes_ to the tags, not the full set of tags */ - tags?: { k: string, v: string }[], + tags?: { k: string; v: string }[] /** * A change to the geometry: @@ -51,17 +50,20 @@ export interface ChangeDescription { * 2) Change of way geometry * 3) Change of relation members (untested) */ - changes?: { - lat: number, - lon: number - } | { - /* Coordinates are only used for rendering. They should be LON, LAT - * */ - coordinates: [number, number][] - nodes: number[], - } | { - members: { type: "node" | "way" | "relation", ref: number, role: string }[] - } + changes?: + | { + lat: number + lon: number + } + | { + /* Coordinates are only used for rendering. They should be LON, LAT + * */ + coordinates: [number, number][] + nodes: number[] + } + | { + members: { type: "node" | "way" | "relation"; ref: number; role: string }[] + } /* Set to delete the object @@ -70,7 +72,6 @@ export interface ChangeDescription { } export class ChangeDescriptionTools { - /** * Rewrites all the ids in a changeDescription * @@ -111,7 +112,7 @@ export class ChangeDescriptionTools { * const rewritten = ChangeDescriptionTools.rewriteIds(change, mapping) * rewritten.id // => 789 * rewritten.changes["nodes"] // => [42,43,44, 68453] - * + * * // should rewrite ids in relationship members * const change = { * type: "way", @@ -130,44 +131,49 @@ export class ChangeDescriptionTools { * rewritten.changes["members"] // => [{type: "way", ref: 42, role: "outer"},{type: "way", ref: 48, role: "outer"}] * */ - public static rewriteIds(change: ChangeDescription, mappings: Map): ChangeDescription { + public static rewriteIds( + change: ChangeDescription, + mappings: Map + ): ChangeDescription { const key = change.type + "/" + change.id - const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some(id => mappings.has("node/" + id)); - const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []) - .some((obj:{type: string, ref: number}) => mappings.has(obj.type+"/" + obj.ref)); + const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some((id) => + mappings.has("node/" + id) + ) + const relationHasChangedMembers = ((change.changes ?? {})["members"] ?? []).some( + (obj: { type: string; ref: number }) => mappings.has(obj.type + "/" + obj.ref) + ) - const hasSomeChange = mappings.has(key) - || wayHasChangedNode || relationHasChangedMembers - if(hasSomeChange){ - change = {...change} + const hasSomeChange = mappings.has(key) || wayHasChangedNode || relationHasChangedMembers + if (hasSomeChange) { + change = { ...change } } - + if (mappings.has(key)) { const [_, newId] = mappings.get(key).split("/") change.id = Number.parseInt(newId) } - if(wayHasChangedNode){ - change.changes = {...change.changes} - change.changes["nodes"] = change.changes["nodes"].map(id => { - const key = "node/"+id - if(!mappings.has(key)){ + if (wayHasChangedNode) { + change.changes = { ...change.changes } + change.changes["nodes"] = change.changes["nodes"].map((id) => { + const key = "node/" + id + if (!mappings.has(key)) { return id } const [_, newId] = mappings.get(key).split("/") return Number.parseInt(newId) }) } - if(relationHasChangedMembers){ - change.changes = {...change.changes} + if (relationHasChangedMembers) { + change.changes = { ...change.changes } change.changes["members"] = change.changes["members"].map( - (obj:{type: string, ref: number}) => { - const key = obj.type+"/"+obj.ref; - if(!mappings.has(key)){ + (obj: { type: string; ref: number }) => { + const key = obj.type + "/" + obj.ref + if (!mappings.has(key)) { return obj } const [_, newId] = mappings.get(key).split("/") - return {...obj, ref: Number.parseInt(newId)} + return { ...obj, ref: Number.parseInt(newId) } } ) } @@ -193,4 +199,4 @@ export class ChangeDescriptionTools { return r.asGeoJson().geometry } } -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/ChangeLocationAction.ts b/Logic/Osm/Actions/ChangeLocationAction.ts index 429bfb596..0ec12bc7b 100644 --- a/Logic/Osm/Actions/ChangeLocationAction.ts +++ b/Logic/Osm/Actions/ChangeLocationAction.ts @@ -1,41 +1,44 @@ -import {ChangeDescription} from "./ChangeDescription"; -import OsmChangeAction from "./OsmChangeAction"; -import {Changes} from "../Changes"; +import { ChangeDescription } from "./ChangeDescription" +import OsmChangeAction from "./OsmChangeAction" +import { Changes } from "../Changes" export default class ChangeLocationAction extends OsmChangeAction { - private readonly _id: number; - private readonly _newLonLat: [number, number]; - private readonly _meta: { theme: string; reason: string }; + private readonly _id: number + private readonly _newLonLat: [number, number] + private readonly _meta: { theme: string; reason: string } - constructor(id: string, newLonLat: [number, number], meta: { - theme: string, - reason: string - }) { - super(id, true); + constructor( + id: string, + newLonLat: [number, number], + meta: { + theme: string + reason: string + } + ) { + super(id, true) if (!id.startsWith("node/")) { throw "Invalid ID: only 'node/number' is accepted" } this._id = Number(id.substring("node/".length)) - this._newLonLat = newLonLat; - this._meta = meta; + this._newLonLat = newLonLat + this._meta = meta } protected async CreateChangeDescriptions(changes: Changes): Promise { - const d: ChangeDescription = { changes: { lat: this._newLonLat[1], - lon: this._newLonLat[0] + lon: this._newLonLat[0], }, type: "node", - id: this._id, meta: { + id: this._id, + meta: { changeType: "move", theme: this._meta.theme, - specialMotivation: this._meta.reason - } - + specialMotivation: this._meta.reason, + }, } return [d] } -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/ChangeTagAction.ts b/Logic/Osm/Actions/ChangeTagAction.ts index 199eab28d..bd17d01c1 100644 --- a/Logic/Osm/Actions/ChangeTagAction.ts +++ b/Logic/Osm/Actions/ChangeTagAction.ts @@ -1,65 +1,77 @@ -import OsmChangeAction from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import {TagsFilter} from "../../Tags/TagsFilter"; -import {OsmTags} from "../../../Models/OsmFeature"; +import OsmChangeAction from "./OsmChangeAction" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import { TagsFilter } from "../../Tags/TagsFilter" +import { OsmTags } from "../../../Models/OsmFeature" export default class ChangeTagAction extends OsmChangeAction { - private readonly _elementId: string; - private readonly _tagsFilter: TagsFilter; - private readonly _currentTags: Record | OsmTags; - private readonly _meta: { theme: string, changeType: string }; + private readonly _elementId: string + private readonly _tagsFilter: TagsFilter + private readonly _currentTags: Record | OsmTags + private readonly _meta: { theme: string; changeType: string } - constructor(elementId: string, - tagsFilter: TagsFilter, - currentTags: Record, meta: { - theme: string, - changeType: "answer" | "soft-delete" | "add-image" | string - }) { - super(elementId, true); - this._elementId = elementId; - this._tagsFilter = tagsFilter; - this._currentTags = currentTags; - this._meta = meta; + constructor( + elementId: string, + tagsFilter: TagsFilter, + currentTags: Record, + meta: { + theme: string + changeType: "answer" | "soft-delete" | "add-image" | string + } + ) { + super(elementId, true) + this._elementId = elementId + this._tagsFilter = tagsFilter + this._currentTags = currentTags + this._meta = meta } /** * Doublechecks that no stupid values are added */ - private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { - const key = kv.k; - const value = kv.v; + private static checkChange(kv: { k: string; v: string }): { k: string; v: string } { + const key = kv.k + const value = kv.v if (key === undefined || key === null) { - console.error("Invalid key:", key); - return undefined; + console.error("Invalid key:", key) + return undefined } if (value === undefined || value === null) { - console.error("Invalid value for ", key, ":", value); - return undefined; + console.error("Invalid value for ", key, ":", value) + return undefined } if (typeof value !== "string") { console.error("Invalid value for ", key, "as it is not a string:", value) - return undefined; + return undefined } - if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) { + if ( + key.startsWith(" ") || + value.startsWith(" ") || + value.endsWith(" ") || + key.endsWith(" ") + ) { console.warn("Tag starts with or ends with a space - trimming anyway") } - return {k: key.trim(), v: value.trim()}; + return { k: key.trim(), v: value.trim() } } async CreateChangeDescriptions(changes: Changes): Promise { - const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange) + const changedTags: { k: string; v: string }[] = this._tagsFilter + .asChange(this._currentTags) + .map(ChangeTagAction.checkChange) const typeId = this._elementId.split("/") const type = typeId[0] - const id = Number(typeId [1]) - return [{ - type: <"node" | "way" | "relation">type, - id: id, - tags: changedTags, - meta: this._meta - }] + const id = Number(typeId[1]) + return [ + { + type: <"node" | "way" | "relation">type, + id: id, + tags: changedTags, + meta: this._meta, + }, + ] } -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts index 73eb1f3a1..e7e625fd2 100644 --- a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts @@ -1,64 +1,69 @@ -import {OsmCreateAction} from "./OsmChangeAction"; -import {Tag} from "../../Tags/Tag"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import FeaturePipelineState from "../../State/FeaturePipelineState"; -import FeatureSource from "../../FeatureSource/FeatureSource"; -import CreateNewWayAction from "./CreateNewWayAction"; -import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"; -import {And} from "../../Tags/And"; -import {TagUtils} from "../../Tags/TagUtils"; - +import { OsmCreateAction } from "./OsmChangeAction" +import { Tag } from "../../Tags/Tag" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import FeaturePipelineState from "../../State/FeaturePipelineState" +import FeatureSource from "../../FeatureSource/FeatureSource" +import CreateNewWayAction from "./CreateNewWayAction" +import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" +import { And } from "../../Tags/And" +import { TagUtils } from "../../Tags/TagUtils" /** * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points */ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction { - public newElementId: string = undefined; - public newElementIdNumber: number = undefined; - private readonly _tags: Tag[]; + public newElementId: string = undefined + public newElementIdNumber: number = undefined + private readonly _tags: Tag[] private readonly createOuterWay: CreateWayWithPointReuseAction private readonly createInnerWays: CreateNewWayAction[] - private readonly geojsonPreview: any; - private readonly theme: string; - private readonly changeType: "import" | "create" | string; + private readonly geojsonPreview: any + private readonly theme: string + private readonly changeType: "import" | "create" | string - constructor(tags: Tag[], - outerRingCoordinates: [number, number][], - innerRingsCoordinates: [number, number][][], - state: FeaturePipelineState, - config: MergePointConfig[], - changeType: "import" | "create" | string + constructor( + tags: Tag[], + outerRingCoordinates: [number, number][], + innerRingsCoordinates: [number, number][][], + state: FeaturePipelineState, + config: MergePointConfig[], + changeType: "import" | "create" | string ) { - super(null, true); - this._tags = [...tags, new Tag("type", "multipolygon")]; - this.changeType = changeType; + super(null, true) + this._tags = [...tags, new Tag("type", "multipolygon")] + this.changeType = changeType this.theme = state?.layoutToUse?.id ?? "" - this.createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config) - this.createInnerWays = innerRingsCoordinates.map(ringCoordinates => - new CreateNewWayAction([], - ringCoordinates.map(([lon, lat]) => ({lat, lon})), - {theme: state?.layoutToUse?.id})) + this.createOuterWay = new CreateWayWithPointReuseAction( + [], + outerRingCoordinates, + state, + config + ) + this.createInnerWays = innerRingsCoordinates.map( + (ringCoordinates) => + new CreateNewWayAction( + [], + ringCoordinates.map(([lon, lat]) => ({ lat, lon })), + { theme: state?.layoutToUse?.id } + ) + ) this.geojsonPreview = { type: "Feature", properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})), geometry: { type: "Polygon", - coordinates: [ - outerRingCoordinates, - ...innerRingsCoordinates - ] - } + coordinates: [outerRingCoordinates, ...innerRingsCoordinates], + }, } - } public async getPreview(): Promise { const outerPreview = await this.createOuterWay.getPreview() outerPreview.features.data.push({ freshness: new Date(), - feature: this.geojsonPreview + feature: this.geojsonPreview, }) return outerPreview } @@ -66,13 +71,12 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct protected async CreateChangeDescriptions(changes: Changes): Promise { console.log("Running CMPWPRA") const descriptions: ChangeDescription[] = [] - descriptions.push(...await this.createOuterWay.CreateChangeDescriptions(changes)); + descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes))) for (const innerWay of this.createInnerWays) { - descriptions.push(...await innerWay.CreateChangeDescriptions(changes)) + descriptions.push(...(await innerWay.CreateChangeDescriptions(changes))) } - - this.newElementIdNumber = changes.getNewID(); + this.newElementIdNumber = changes.getNewID() this.newElementId = "relation/" + this.newElementIdNumber descriptions.push({ type: "relation", @@ -80,24 +84,25 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct tags: new And(this._tags).asChange({}), meta: { theme: this.theme, - changeType: this.changeType + changeType: this.changeType, }, changes: { members: [ { type: "way", ref: this.createOuterWay.newElementIdNumber, - role: "outer" + role: "outer", }, // @ts-ignore - ...this.createInnerWays.map(a => ({type: "way", ref: a.newElementIdNumber, role: "inner"})) - ] - } + ...this.createInnerWays.map((a) => ({ + type: "way", + ref: a.newElementIdNumber, + role: "inner", + })), + ], + }, }) - return descriptions } - - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts index 9ddc5a23e..a638bb554 100644 --- a/Logic/Osm/Actions/CreateNewNodeAction.ts +++ b/Logic/Osm/Actions/CreateNewNodeAction.ts @@ -1,13 +1,12 @@ -import {Tag} from "../../Tags/Tag"; -import {OsmCreateAction} from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import {And} from "../../Tags/And"; -import {OsmWay} from "../OsmObject"; -import {GeoOperations} from "../../GeoOperations"; +import { Tag } from "../../Tags/Tag" +import { OsmCreateAction } from "./OsmChangeAction" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import { And } from "../../Tags/And" +import { OsmWay } from "../OsmObject" +import { GeoOperations } from "../../GeoOperations" export default class CreateNewNodeAction extends OsmCreateAction { - /** * Maps previously created points onto their assigned ID, to reuse the point if uplaoded * "lat,lon" --> id @@ -15,46 +14,47 @@ export default class CreateNewNodeAction extends OsmCreateAction { private static readonly previouslyCreatedPoints = new Map() public newElementId: string = undefined public newElementIdNumber: number = undefined - private readonly _basicTags: Tag[]; - private readonly _lat: number; - private readonly _lon: number; - private readonly _snapOnto: OsmWay; - private readonly _reusePointDistance: number; - private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string }; - private readonly _reusePreviouslyCreatedPoint: boolean; + private readonly _basicTags: Tag[] + private readonly _lat: number + private readonly _lon: number + private readonly _snapOnto: OsmWay + private readonly _reusePointDistance: number + private meta: { changeType: "create" | "import"; theme: string; specialMotivation?: string } + private readonly _reusePreviouslyCreatedPoint: boolean - - constructor(basicTags: Tag[], - lat: number, lon: number, - options: { - allowReuseOfPreviouslyCreatedPoints?: boolean, - snapOnto?: OsmWay, - reusePointWithinMeters?: number, - theme: string, - changeType: "create" | "import" | null, - specialMotivation?: string - }) { + constructor( + basicTags: Tag[], + lat: number, + lon: number, + options: { + allowReuseOfPreviouslyCreatedPoints?: boolean + snapOnto?: OsmWay + reusePointWithinMeters?: number + theme: string + changeType: "create" | "import" | null + specialMotivation?: string + } + ) { super(null, basicTags !== undefined && basicTags.length > 0) - this._basicTags = basicTags; - this._lat = lat; - this._lon = lon; + this._basicTags = basicTags + this._lat = lat + this._lon = lon if (lat === undefined || lon === undefined) { throw "Lat or lon are undefined!" } - this._snapOnto = options?.snapOnto; + this._snapOnto = options?.snapOnto this._reusePointDistance = options?.reusePointWithinMeters ?? 1 - this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0) + this._reusePreviouslyCreatedPoint = + options?.allowReuseOfPreviouslyCreatedPoints ?? basicTags.length === 0 this.meta = { theme: options.theme, changeType: options.changeType, - specialMotivation: options.specialMotivation + specialMotivation: options.specialMotivation, } } async CreateChangeDescriptions(changes: Changes): Promise { - if (this._reusePreviouslyCreatedPoint) { - const key = this._lat + "," + this._lon const prev = CreateNewNodeAction.previouslyCreatedPoints if (prev.has(key)) { @@ -64,17 +64,23 @@ export default class CreateNewNodeAction extends OsmCreateAction { } } - const id = changes.getNewID() const properties = { - id: "node/" + id + id: "node/" + id, } this.setElementId(id) for (const kv of this._basicTags) { if (typeof kv.value !== "string") { - throw "Invalid value: don't use non-string value in a preset. The tag "+kv.key+"="+kv.value+" is not a string, the value is a "+typeof kv.value + throw ( + "Invalid value: don't use non-string value in a preset. The tag " + + kv.key + + "=" + + kv.value + + " is not a string, the value is a " + + typeof kv.value + ) } - properties[kv.key] = kv.value; + properties[kv.key] = kv.value } const newPointChange: ChangeDescription = { @@ -83,32 +89,31 @@ export default class CreateNewNodeAction extends OsmCreateAction { id: id, changes: { lat: this._lat, - lon: this._lon + lon: this._lon, }, - meta: this.meta + meta: this.meta, } if (this._snapOnto === undefined) { return [newPointChange] } - // Project the point onto the way console.log("Snapping a node onto an existing way...") const geojson = this._snapOnto.asGeoJson() const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat]) - const projectedCoor= <[number, number]>projected.geometry.coordinates + const projectedCoor = <[number, number]>projected.geometry.coordinates const index = projected.properties.index // We check that it isn't close to an already existing point - let reusedPointId = undefined; - let outerring : [number,number][]; - - if(geojson.geometry.type === "LineString"){ - outerring = <[number, number][]> geojson.geometry.coordinates - }else if(geojson.geometry.type === "Polygon"){ - outerring =<[number, number][]> geojson.geometry.coordinates[0] + let reusedPointId = undefined + let outerring: [number, number][] + + if (geojson.geometry.type === "LineString") { + outerring = <[number, number][]>geojson.geometry.coordinates + } else if (geojson.geometry.type === "Polygon") { + outerring = <[number, number][]>geojson.geometry.coordinates[0] } - - const prev= outerring[index] + + const prev = outerring[index] if (GeoOperations.distanceBetween(prev, projectedCoor) < this._reusePointDistance) { // We reuse this point instead! reusedPointId = this._snapOnto.nodes[index] @@ -120,20 +125,24 @@ export default class CreateNewNodeAction extends OsmCreateAction { } if (reusedPointId !== undefined) { this.setElementId(reusedPointId) - return [{ - tags: new And(this._basicTags).asChange(properties), - type: "node", - id: reusedPointId, - meta: this.meta - }] + return [ + { + tags: new And(this._basicTags).asChange(properties), + type: "node", + id: reusedPointId, + meta: this.meta, + }, + ] } - const locations = [...this._snapOnto.coordinates.map(([lat, lon]) =><[number,number]> [lon, lat])] + const locations = [ + ...this._snapOnto.coordinates.map(([lat, lon]) => <[number, number]>[lon, lat]), + ] const ids = [...this._snapOnto.nodes] locations.splice(index + 1, 0, [this._lon, this._lat]) ids.splice(index + 1, 0, id) - + // Allright, we have to insert a new point in the way return [ newPointChange, @@ -142,15 +151,15 @@ export default class CreateNewNodeAction extends OsmCreateAction { id: this._snapOnto.id, changes: { coordinates: locations, - nodes: ids + nodes: ids, }, - meta: this.meta - } + meta: this.meta, + }, ] } private setElementId(id: number) { - this.newElementIdNumber = id; + this.newElementIdNumber = id this.newElementId = "node/" + id if (!this._reusePreviouslyCreatedPoint) { return @@ -158,6 +167,4 @@ export default class CreateNewNodeAction extends OsmCreateAction { const key = this._lat + "," + this._lon CreateNewNodeAction.previouslyCreatedPoints.set(key, id) } - - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/CreateNewWayAction.ts b/Logic/Osm/Actions/CreateNewWayAction.ts index edc6dd0bf..8b44a0c2c 100644 --- a/Logic/Osm/Actions/CreateNewWayAction.ts +++ b/Logic/Osm/Actions/CreateNewWayAction.ts @@ -1,19 +1,18 @@ -import {ChangeDescription} from "./ChangeDescription"; -import {OsmCreateAction} from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {Tag} from "../../Tags/Tag"; -import CreateNewNodeAction from "./CreateNewNodeAction"; -import {And} from "../../Tags/And"; +import { ChangeDescription } from "./ChangeDescription" +import { OsmCreateAction } from "./OsmChangeAction" +import { Changes } from "../Changes" +import { Tag } from "../../Tags/Tag" +import CreateNewNodeAction from "./CreateNewNodeAction" +import { And } from "../../Tags/And" export default class CreateNewWayAction extends OsmCreateAction { public newElementId: string = undefined - public newElementIdNumber: number = undefined; - private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; - private readonly tags: Tag[]; + public newElementIdNumber: number = undefined + private readonly coordinates: { nodeId?: number; lat: number; lon: number }[] + private readonly tags: Tag[] private readonly _options: { theme: string - }; - + } /*** * Creates a new way to upload to OSM @@ -21,33 +20,44 @@ export default class CreateNewWayAction extends OsmCreateAction { * @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used * @param options */ - constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[], - options: { - theme: string - }) { + constructor( + tags: Tag[], + coordinates: { nodeId?: number; lat: number; lon: number }[], + options: { + theme: string + } + ) { super(null, true) - this.coordinates = []; + this.coordinates = [] for (const coordinate of coordinates) { /* The 'PointReuseAction' is a bit buggy and might generate duplicate ids. We filter those here, as the CreateWayWithPointReuseAction delegates the actual creation to here. Filtering here also prevents similar bugs in other actions */ - if(this.coordinates.length > 0 && coordinate.nodeId !== undefined && this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId){ + if ( + this.coordinates.length > 0 && + coordinate.nodeId !== undefined && + this.coordinates[this.coordinates.length - 1].nodeId === coordinate.nodeId + ) { // This is a duplicate id - console.warn("Skipping a node in createWay to avoid a duplicate node:", coordinate,"\nThe previous coordinates are: ", this.coordinates) + console.warn( + "Skipping a node in createWay to avoid a duplicate node:", + coordinate, + "\nThe previous coordinates are: ", + this.coordinates + ) continue } - + this.coordinates.push(coordinate) } - - this.tags = tags; - this._options = options; + + this.tags = tags + this._options = options } public async CreateChangeDescriptions(changes: Changes): Promise { - const newElements: ChangeDescription[] = [] const pointIds: number[] = [] @@ -60,16 +70,15 @@ export default class CreateNewWayAction extends OsmCreateAction { const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, { allowReuseOfPreviouslyCreatedPoints: true, changeType: null, - theme: this._options.theme + theme: this._options.theme, }) - newElements.push(...await newPoint.CreateChangeDescriptions(changes)) + newElements.push(...(await newPoint.CreateChangeDescriptions(changes))) pointIds.push(newPoint.newElementIdNumber) } // We have all created (or reused) all the points! // Time to create the actual way - const id = changes.getNewID() this.newElementIdNumber = id const newWay = { @@ -77,18 +86,16 @@ export default class CreateNewWayAction extends OsmCreateAction { type: "way", meta: { theme: this._options.theme, - changeType: "import" + changeType: "import", }, tags: new And(this.tags).asChange({}), changes: { nodes: pointIds, - coordinates: this.coordinates.map(c => [c.lon, c.lat]) - } + coordinates: this.coordinates.map((c) => [c.lon, c.lat]), + }, } newElements.push(newWay) this.newElementId = "way/" + id return newElements } - - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts index 273b511da..ed549d4af 100644 --- a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts @@ -1,20 +1,19 @@ -import {OsmCreateAction} from "./OsmChangeAction"; -import {Tag} from "../../Tags/Tag"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import FeaturePipelineState from "../../State/FeaturePipelineState"; -import {BBox} from "../../BBox"; -import {TagsFilter} from "../../Tags/TagsFilter"; -import {GeoOperations} from "../../GeoOperations"; -import FeatureSource from "../../FeatureSource/FeatureSource"; -import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; -import CreateNewNodeAction from "./CreateNewNodeAction"; -import CreateNewWayAction from "./CreateNewWayAction"; - +import { OsmCreateAction } from "./OsmChangeAction" +import { Tag } from "../../Tags/Tag" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import FeaturePipelineState from "../../State/FeaturePipelineState" +import { BBox } from "../../BBox" +import { TagsFilter } from "../../Tags/TagsFilter" +import { GeoOperations } from "../../GeoOperations" +import FeatureSource from "../../FeatureSource/FeatureSource" +import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" +import CreateNewNodeAction from "./CreateNewNodeAction" +import CreateNewWayAction from "./CreateNewWayAction" export interface MergePointConfig { - withinRangeOfM: number, - ifMatches: TagsFilter, + withinRangeOfM: number + ifMatches: TagsFilter mode: "reuse_osm_point" | "move_osm_point" } @@ -33,12 +32,12 @@ interface CoordinateInfo { /** * The new coordinate */ - lngLat: [number, number], + lngLat: [number, number] /** * If set: indicates that this point is identical to an earlier point in the way and that that point should be used. * This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo */ - identicalTo?: number, + identicalTo?: number /** * Information about the closebyNode which might be reused */ @@ -46,8 +45,8 @@ interface CoordinateInfo { /** * Distance in meters between the target coordinate and this candidate coordinate */ - d: number, - node: any, + d: number + node: any config: MergePointConfig }[] } @@ -56,54 +55,55 @@ interface CoordinateInfo { * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points */ export default class CreateWayWithPointReuseAction extends OsmCreateAction { - public newElementId: string = undefined; + public newElementId: string = undefined public newElementIdNumber: number = undefined - private readonly _tags: Tag[]; + private readonly _tags: Tag[] /** * lngLat-coordinates * @private */ - private _coordinateInfo: CoordinateInfo[]; - private _state: FeaturePipelineState; - private _config: MergePointConfig[]; + private _coordinateInfo: CoordinateInfo[] + private _state: FeaturePipelineState + private _config: MergePointConfig[] - constructor(tags: Tag[], - coordinates: [number, number][], - state: FeaturePipelineState, - config: MergePointConfig[] + constructor( + tags: Tag[], + coordinates: [number, number][], + state: FeaturePipelineState, + config: MergePointConfig[] ) { - super(null, true); - this._tags = tags; - this._state = state; - this._config = config; + super(null, true) + this._tags = tags + this._state = state + this._config = config // The main logic of this class: the coordinateInfo contains all the changes - this._coordinateInfo = this.CalculateClosebyNodes(coordinates); - + this._coordinateInfo = this.CalculateClosebyNodes(coordinates) } public async getPreview(): Promise { - const features = [] - let geometryMoved = false; + let geometryMoved = false for (let i = 0; i < this._coordinateInfo.length; i++) { - const coordinateInfo = this._coordinateInfo[i]; + const coordinateInfo = this._coordinateInfo[i] if (coordinateInfo.identicalTo !== undefined) { continue } - if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) { - + if ( + coordinateInfo.closebyNodes === undefined || + coordinateInfo.closebyNodes.length === 0 + ) { const newPoint = { type: "Feature", properties: { - "newpoint": "yes", - id: "new-geometry-with-reuse-" + i + newpoint: "yes", + id: "new-geometry-with-reuse-" + i, }, geometry: { type: "Point", - coordinates: coordinateInfo.lngLat - } - }; + coordinates: coordinateInfo.lngLat, + }, + } features.push(newPoint) continue } @@ -113,18 +113,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { const moveDescription = { type: "Feature", properties: { - "move": "yes", + move: "yes", "osm-id": reusedPoint.node.properties.id, - "id": "new-geometry-move-existing" + i, - "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) + id: "new-geometry-move-existing" + i, + distance: GeoOperations.distanceBetween( + coordinateInfo.lngLat, + reusedPoint.node.geometry.coordinates + ), }, geometry: { type: "LineString", - coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat] - } + coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat], + }, } features.push(moveDescription) - } else { // The geometry is moved, the point is reused geometryMoved = true @@ -132,22 +134,24 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { const reuseDescription = { type: "Feature", properties: { - "move": "no", + move: "no", "osm-id": reusedPoint.node.properties.id, - "id": "new-geometry-reuse-existing" + i, - "distance": GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates) + id: "new-geometry-reuse-existing" + i, + distance: GeoOperations.distanceBetween( + coordinateInfo.lngLat, + reusedPoint.node.geometry.coordinates + ), }, geometry: { type: "LineString", - coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates] - } + coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates], + }, } features.push(reuseDescription) } } if (geometryMoved) { - const coords: [number, number][] = [] for (const info of this._coordinateInfo) { if (info.identicalTo !== undefined) { @@ -166,21 +170,19 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { } else { coords.push(info.lngLat) } - } const newGeometry = { type: "Feature", properties: { "resulting-geometry": "yes", - "id": "new-geometry" + id: "new-geometry", }, geometry: { type: "LineString", - coordinates: coords - } + coordinates: coords, + }, } features.push(newGeometry) - } return StaticFeatureSource.fromGeojson(features) } @@ -188,7 +190,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { public async CreateChangeDescriptions(changes: Changes): Promise { const theme = this._state?.layoutToUse?.id const allChanges: ChangeDescription[] = [] - const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = [] + const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] for (let i = 0; i < this._coordinateInfo.length; i++) { const info = this._coordinateInfo[i] const lat = info.lngLat[1] @@ -202,17 +204,17 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { const newNodeAction = new CreateNewNodeAction([], lat, lon, { allowReuseOfPreviouslyCreatedPoints: true, changeType: null, - theme + theme, }) allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes))) nodeIdsToUse.push({ - lat, lon, - nodeId: newNodeAction.newElementIdNumber + lat, + lon, + nodeId: newNodeAction.newElementIdNumber, }) continue - } const closestPoint = info.closebyNodes[0] @@ -222,20 +224,20 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { type: "node", id, changes: { - lat, lon + lat, + lon, }, meta: { theme, - changeType: null - } + changeType: null, + }, }) } - nodeIdsToUse.push({lat, lon, nodeId: id}) + nodeIdsToUse.push({ lat, lon, nodeId: id }) } - const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { - theme + theme, }) allChanges.push(...(await newWay.CreateChangeDescriptions(changes))) @@ -248,27 +250,26 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { * Calculates the main changes. */ private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { - const bbox = new BBox(coordinates) const state = this._state - const allNodes = [].concat(...state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2))??[]) - const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM)) + const allNodes = [].concat( + ...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? []) + ) + const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) // Init coordianteinfo with undefined but the same length as coordinates const coordinateInfo: { - lngLat: [number, number], - identicalTo?: number, + lngLat: [number, number] + identicalTo?: number closebyNodes?: { - d: number, - node: any, + d: number + node: any config: MergePointConfig }[] - }[] = coordinates.map(_ => undefined) - + }[] = coordinates.map((_) => undefined) // First loop: gather all information... for (let i = 0; i < coordinates.length; i++) { - if (coordinateInfo[i] !== undefined) { // Already seen, probably a duplicate coordinate continue @@ -282,9 +283,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { coordinateInfo[j] = { lngLat: coor, - identicalTo: i + identicalTo: i, } - break; + break } } @@ -292,8 +293,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { // Lets search applicable points and determine the merge mode const closebyNodes: { - d: number, - node: any, + d: number + node: any config: MergePointConfig }[] = [] for (const node of allNodes) { @@ -310,7 +311,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { if (!config.ifMatches.matchesProperties(node.properties)) { continue } - closebyNodes.push({node, d, config}) + closebyNodes.push({ node, d, config }) } } @@ -322,18 +323,15 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { coordinateInfo[i] = { identicalTo: undefined, lngLat: coor, - closebyNodes + closebyNodes, } - } - // Second loop: figure out which point moves where without creating conflicts - let conflictFree = true; + let conflictFree = true do { - conflictFree = true; + conflictFree = true for (let i = 0; i < coordinateInfo.length; i++) { - const coorInfo = coordinateInfo[i] if (coorInfo.identicalTo !== undefined) { continue @@ -366,8 +364,6 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction { } } while (!conflictFree) - return coordinateInfo } - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/DeleteAction.ts b/Logic/Osm/Actions/DeleteAction.ts index e5cd05ddb..70fa949dd 100644 --- a/Logic/Osm/Actions/DeleteAction.ts +++ b/Logic/Osm/Actions/DeleteAction.ts @@ -1,61 +1,61 @@ -import {OsmObject} from "../OsmObject"; -import OsmChangeAction from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import ChangeTagAction from "./ChangeTagAction"; -import {TagsFilter} from "../../Tags/TagsFilter"; -import {And} from "../../Tags/And"; -import {Tag} from "../../Tags/Tag"; +import { OsmObject } from "../OsmObject" +import OsmChangeAction from "./OsmChangeAction" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import ChangeTagAction from "./ChangeTagAction" +import { TagsFilter } from "../../Tags/TagsFilter" +import { And } from "../../Tags/And" +import { Tag } from "../../Tags/Tag" export default class DeleteAction extends OsmChangeAction { - - private readonly _softDeletionTags: TagsFilter; + private readonly _softDeletionTags: TagsFilter private readonly meta: { - theme: string, - specialMotivation: string, + theme: string + specialMotivation: string changeType: "deletion" - }; - private readonly _id: string; - private _hardDelete: boolean; + } + private readonly _id: string + private _hardDelete: boolean - - constructor(id: string, - softDeletionTags: TagsFilter, - meta: { - theme: string, - specialMotivation: string - }, - hardDelete: boolean) { + constructor( + id: string, + softDeletionTags: TagsFilter, + meta: { + theme: string + specialMotivation: string + }, + hardDelete: boolean + ) { super(id, true) - this._id = id; - this._hardDelete = hardDelete; - this.meta = {...meta, changeType: "deletion"}; - this._softDeletionTags = new And([softDeletionTags, - new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`) - ]); - + this._id = id + this._hardDelete = hardDelete + this.meta = { ...meta, changeType: "deletion" } + this._softDeletionTags = new And([ + softDeletionTags, + new Tag( + "fixme", + `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})` + ), + ]) } public async CreateChangeDescriptions(changes: Changes): Promise { - const osmObject = await OsmObject.DownloadObjectAsync(this._id) if (this._hardDelete) { - return [{ - meta: this.meta, - doDelete: true, - type: osmObject.type, - id: osmObject.id, - }] - } else { - return await new ChangeTagAction( - this._id, this._softDeletionTags, osmObject.tags, + return [ { - ...this.meta, - changeType: "soft-delete" - } - ).CreateChangeDescriptions(changes) + meta: this.meta, + doDelete: true, + type: osmObject.type, + id: osmObject.id, + }, + ] + } else { + return await new ChangeTagAction(this._id, this._softDeletionTags, osmObject.tags, { + ...this.meta, + changeType: "soft-delete", + }).CreateChangeDescriptions(changes) } } - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/OsmChangeAction.ts b/Logic/Osm/Actions/OsmChangeAction.ts index 1754a91bc..e07086c1c 100644 --- a/Logic/Osm/Actions/OsmChangeAction.ts +++ b/Logic/Osm/Actions/OsmChangeAction.ts @@ -2,22 +2,21 @@ * An action is a change to the OSM-database * It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object */ -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" export default abstract class OsmChangeAction { - - public readonly trackStatistics: boolean; + public readonly trackStatistics: boolean /** * The ID of the object that is the center of this change. * Null if the action creates a new object (at initialization) * Undefined if such an id does not make sense */ - public readonly mainObjectId: string; + public readonly mainObjectId: string private isUsed = false constructor(mainObjectId: string, trackStatistics: boolean = true) { - this.trackStatistics = trackStatistics; + this.trackStatistics = trackStatistics this.mainObjectId = mainObjectId } @@ -25,7 +24,7 @@ export default abstract class OsmChangeAction { if (this.isUsed) { throw "This ChangeAction is already used" } - this.isUsed = true; + this.isUsed = true return this.CreateChangeDescriptions(changes) } @@ -33,8 +32,6 @@ export default abstract class OsmChangeAction { } export abstract class OsmCreateAction extends OsmChangeAction { - public newElementId: string public newElementIdNumber: number - } diff --git a/Logic/Osm/Actions/RelationSplitHandler.ts b/Logic/Osm/Actions/RelationSplitHandler.ts index 8e8f081fa..1fd6749b8 100644 --- a/Logic/Osm/Actions/RelationSplitHandler.ts +++ b/Logic/Osm/Actions/RelationSplitHandler.ts @@ -1,24 +1,24 @@ -import OsmChangeAction from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import {OsmObject, OsmRelation, OsmWay} from "../OsmObject"; +import OsmChangeAction from "./OsmChangeAction" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import { OsmObject, OsmRelation, OsmWay } from "../OsmObject" export interface RelationSplitInput { - relation: OsmRelation, - originalWayId: number, - allWayIdsInOrder: number[], - originalNodes: number[], + relation: OsmRelation + originalWayId: number + allWayIdsInOrder: number[] + originalNodes: number[] allWaysNodesInOrder: number[][] } abstract class AbstractRelationSplitHandler extends OsmChangeAction { - protected readonly _input: RelationSplitInput; - protected readonly _theme: string; + protected readonly _input: RelationSplitInput + protected readonly _theme: string constructor(input: RelationSplitInput, theme: string) { super("relation/" + input.relation.id, false) - this._input = input; - this._theme = theme; + this._input = input + this._theme = theme } /** @@ -44,7 +44,7 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { if (member.type === "relation") { return undefined } - return undefined; + return undefined } } @@ -52,7 +52,6 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { * When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant. */ export default class RelationSplitHandler extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { super(input, theme) } @@ -60,38 +59,43 @@ export default class RelationSplitHandler extends AbstractRelationSplitHandler { async CreateChangeDescriptions(changes: Changes): Promise { if (this._input.relation.tags["type"] === "restriction") { // This is a turn restriction - return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions(changes) + return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions( + changes + ) } - return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes) + return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( + changes + ) } - - } export class TurnRestrictionRSH extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { - super(input, theme); + super(input, theme) } public async CreateChangeDescriptions(changes: Changes): Promise { - const relation = this._input.relation const members = relation.members - const selfMembers = members.filter(m => m.type === "way" && m.ref === this._input.originalWayId) + const selfMembers = members.filter( + (m) => m.type === "way" && m.ref === this._input.originalWayId + ) if (selfMembers.length > 1) { - console.warn("Detected a turn restriction where this way has multiple occurances. This is an error") + console.warn( + "Detected a turn restriction where this way has multiple occurances. This is an error" + ) } const selfMember = selfMembers[0] if (selfMember.role === "via") { // A via way can be replaced in place - return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes); + return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( + changes + ) } - // We have to keep only the way with a common point with the rest of the relation // Let's figure out which member is neighbouring our way @@ -102,11 +106,12 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { let commonPoint = commonStartPoint ?? commonEndPoint // Let's select the way to keep - const idToKeep: { id: number } = this._input.allWaysNodesInOrder.map((nodes, i) => ({ - nodes: nodes, - id: this._input.allWayIdsInOrder[i] - })) - .filter(nodesId => { + const idToKeep: { id: number } = this._input.allWaysNodesInOrder + .map((nodes, i) => ({ + nodes: nodes, + id: this._input.allWayIdsInOrder[i], + })) + .filter((nodesId) => { const nds = nodesId.nodes return nds[0] == commonPoint || nds[nds.length - 1] == commonPoint })[0] @@ -123,36 +128,34 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { } const newMembers: { - ref: number, - type: "way" | "node" | "relation", + ref: number + type: "way" | "node" | "relation" role: string - } [] = relation.members.map(m => { + }[] = relation.members.map((m) => { if (m.type === "way" && m.ref === originalWayId) { return { ref: idToKeep.id, type: "way", - role: m.role + role: m.role, } } return m }) - return [ { type: "relation", id: relation.id, changes: { - members: newMembers + members: newMembers, }, meta: { theme: this._theme, - changeType: "relation-fix:turn_restriction" - } - } - ]; + changeType: "relation-fix:turn_restriction", + }, + }, + ] } - } /** @@ -163,26 +166,24 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { * Note that the feature might appear multiple times. */ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { - super(input, theme); + super(input, theme) } async CreateChangeDescriptions(changes: Changes): Promise { - const wayId = this._input.originalWayId const relation = this._input.relation const members = relation.members - const originalNodes = this._input.originalNodes; + const originalNodes = this._input.originalNodes const firstNode = originalNodes[0] const lastNode = originalNodes[originalNodes.length - 1] - const newMembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = [] + const newMembers: { type: "node" | "way" | "relation"; ref: number; role: string }[] = [] for (let i = 0; i < members.length; i++) { - const member = members[i]; + const member = members[i] if (member.type !== "way" || member.ref !== wayId) { newMembers.push(member) - continue; + continue } const nodeIdBefore = await this.targetNodeAt(i - 1, false) @@ -197,10 +198,10 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { newMembers.push({ ref: wId, type: "way", - role: member.role + role: member.role, }) } - continue; + continue } const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode @@ -209,14 +210,14 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { // We (probably) have a reversed situation, backward situation for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--) { // Iterate BACKWARDS - const wId = this._input.allWayIdsInOrder[i1]; + const wId = this._input.allWayIdsInOrder[i1] newMembers.push({ ref: wId, type: "way", - role: member.role + role: member.role, }) } - continue; + continue } // Euhm, allright... Something weird is going on, but let's not care too much @@ -225,21 +226,21 @@ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { newMembers.push({ ref: wId, type: "way", - role: member.role + role: member.role, }) } - } - return [{ - id: relation.id, - type: "relation", - changes: {members: newMembers}, - meta: { - changeType: "relation-fix", - theme: this._theme - } - }]; + return [ + { + id: relation.id, + type: "relation", + changes: { members: newMembers }, + meta: { + changeType: "relation-fix", + theme: this._theme, + }, + }, + ] } - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index 4e125f2a5..c2020d36d 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -1,59 +1,59 @@ -import OsmChangeAction from "./OsmChangeAction"; -import {Changes} from "../Changes"; -import {ChangeDescription} from "./ChangeDescription"; -import {Tag} from "../../Tags/Tag"; -import FeatureSource from "../../FeatureSource/FeatureSource"; -import {OsmNode, OsmObject, OsmWay} from "../OsmObject"; -import {GeoOperations} from "../../GeoOperations"; -import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"; -import CreateNewNodeAction from "./CreateNewNodeAction"; -import ChangeTagAction from "./ChangeTagAction"; -import {And} from "../../Tags/And"; -import {Utils} from "../../../Utils"; -import {OsmConnection} from "../OsmConnection"; -import {Feature} from "@turf/turf"; -import FeaturePipeline from "../../FeatureSource/FeaturePipeline"; +import OsmChangeAction from "./OsmChangeAction" +import { Changes } from "../Changes" +import { ChangeDescription } from "./ChangeDescription" +import { Tag } from "../../Tags/Tag" +import FeatureSource from "../../FeatureSource/FeatureSource" +import { OsmNode, OsmObject, OsmWay } from "../OsmObject" +import { GeoOperations } from "../../GeoOperations" +import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" +import CreateNewNodeAction from "./CreateNewNodeAction" +import ChangeTagAction from "./ChangeTagAction" +import { And } from "../../Tags/And" +import { Utils } from "../../../Utils" +import { OsmConnection } from "../OsmConnection" +import { Feature } from "@turf/turf" +import FeaturePipeline from "../../FeatureSource/FeaturePipeline" export default class ReplaceGeometryAction extends OsmChangeAction { /** * The target feature - mostly used for the metadata */ - private readonly feature: any; + private readonly feature: any private readonly state: { - osmConnection: OsmConnection, + osmConnection: OsmConnection featurePipeline: FeaturePipeline - }; - private readonly wayToReplaceId: string; - private readonly theme: string; + } + private readonly wayToReplaceId: string + private readonly theme: string /** * The target coordinates that should end up in OpenStreetMap. * This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0] * Format: [lon, lat] */ - private readonly targetCoordinates: [number, number][]; + private readonly targetCoordinates: [number, number][] /** * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index. */ private readonly identicalTo: number[] - private readonly newTags: Tag[] | undefined; + private readonly newTags: Tag[] | undefined constructor( state: { - osmConnection: OsmConnection, + osmConnection: OsmConnection featurePipeline: FeaturePipeline }, feature: any, wayToReplaceId: string, options: { - theme: string, + theme: string newTags?: Tag[] } ) { - super(wayToReplaceId, false); - this.state = state; - this.feature = feature; - this.wayToReplaceId = wayToReplaceId; - this.theme = options.theme; + super(wayToReplaceId, false) + this.state = state + this.feature = feature + this.wayToReplaceId = wayToReplaceId + this.theme = options.theme const geom = this.feature.geometry let coordinates: [number, number][] @@ -64,7 +64,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } this.targetCoordinates = coordinates - this.identicalTo = coordinates.map(_ => undefined) + this.identicalTo = coordinates.map((_) => undefined) for (let i = 0; i < coordinates.length; i++) { if (this.identicalTo[i] !== undefined) { @@ -82,7 +82,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction { // noinspection JSUnusedGlobalSymbols public async getPreview(): Promise { - const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds(); + const { closestIds, allNodesById, detachedNodes, reprojectedNodes } = + await this.GetClosestIds() const preview: Feature[] = closestIds.map((newId, i) => { if (this.identicalTo[i] !== undefined) { return undefined @@ -92,75 +93,73 @@ export default class ReplaceGeometryAction extends OsmChangeAction { return { type: "Feature", properties: { - "newpoint": "yes", - "id": "replace-geometry-move-" + i, + newpoint: "yes", + id: "replace-geometry-move-" + i, }, geometry: { type: "Point", - coordinates: this.targetCoordinates[i] - } - }; + coordinates: this.targetCoordinates[i], + }, + } } - const origNode = allNodesById.get(newId); + const origNode = allNodesById.get(newId) return { type: "Feature", properties: { - "move": "yes", + move: "yes", "osm-id": newId, - "id": "replace-geometry-move-" + i, - "original-node-tags": JSON.stringify(origNode.tags) + id: "replace-geometry-move-" + i, + "original-node-tags": JSON.stringify(origNode.tags), }, geometry: { type: "LineString", - coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]] - } - }; + coordinates: [[origNode.lon, origNode.lat], this.targetCoordinates[i]], + }, + } }) - - reprojectedNodes.forEach(({newLat, newLon, nodeId}) => { - - const origNode = allNodesById.get(nodeId); - const feature : Feature = { + reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => { + const origNode = allNodesById.get(nodeId) + const feature: Feature = { type: "Feature", properties: { - "move": "yes", - "reprojection": "yes", + move: "yes", + reprojection: "yes", "osm-id": nodeId, - "id": "replace-geometry-reproject-" + nodeId, - "original-node-tags": JSON.stringify(origNode.tags) + id: "replace-geometry-reproject-" + nodeId, + "original-node-tags": JSON.stringify(origNode.tags), }, geometry: { type: "LineString", - coordinates: [[origNode.lon, origNode.lat], [newLon, newLat]] - } - }; + coordinates: [ + [origNode.lon, origNode.lat], + [newLon, newLat], + ], + }, + } preview.push(feature) }) - - detachedNodes.forEach(({reason}, id) => { - const origNode = allNodesById.get(id); - const feature : Feature = { + detachedNodes.forEach(({ reason }, id) => { + const origNode = allNodesById.get(id) + const feature: Feature = { type: "Feature", properties: { - "detach": "yes", - "id": "replace-geometry-detach-" + id, + detach: "yes", + id: "replace-geometry-detach-" + id, "detach-reason": reason, - "original-node-tags": JSON.stringify(origNode.tags) + "original-node-tags": JSON.stringify(origNode.tags), }, geometry: { type: "Point", - coordinates: [origNode.lon, origNode.lat] - } - }; + coordinates: [origNode.lon, origNode.lat], + }, + } preview.push(feature) }) - return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) - } /** @@ -170,45 +169,52 @@ export default class ReplaceGeometryAction extends OsmChangeAction { * */ public async GetClosestIds(): Promise<{ - // A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created - closestIds: number[], - allNodesById: Map, - osmWay: OsmWay, - detachedNodes: Map, - reprojectedNodes: Map + closestIds: number[] + allNodesById: Map + osmWay: OsmWay + detachedNodes: Map< + number, + { + reason: string + hasTags: boolean + } + > + reprojectedNodes: Map< + number, + { + /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ + projectAfterIndex: number + distance: number + newLat: number + newLon: number + nodeId: number + } + > }> { // TODO FIXME: if a new point has to be created, snap to already existing ways - const nodeDb = this.state.featurePipeline.fullNodeDatabase; + const nodeDb = this.state.featurePipeline.fullNodeDatabase if (nodeDb === undefined) { throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" } - const self = this; - let parsed: OsmObject[]; + const self = this + let parsed: OsmObject[] { // Gather the needed OsmObjects - const splitted = this.wayToReplaceId.split("/"); - const type = splitted[0]; - const idN = Number(splitted[1]); + const splitted = this.wayToReplaceId.split("/") + const type = splitted[0] + const idN = Number(splitted[1]) if (idN < 0 || type !== "way") { throw "Invalid ID to conflate: " + this.wayToReplaceId } - const url = `${this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org"}/api/0.6/${this.wayToReplaceId}/full`; + const url = `${ + this.state.osmConnection?._oauth_config?.url ?? "https://openstreetmap.org" + }/api/0.6/${this.wayToReplaceId}/full` const rawData = await Utils.downloadJsonCached(url, 1000) - parsed = OsmObject.ParseObjects(rawData.elements); + parsed = OsmObject.ParseObjects(rawData.elements) } - const allNodes = parsed.filter(o => o.type === "node") + const allNodes = parsed.filter((o) => o.type === "node") const osmWay = parsed[parsed.length - 1] if (osmWay.type !== "way") { throw "WEIRD: expected an OSM-way as last element here!" @@ -228,38 +234,42 @@ export default class ReplaceGeometryAction extends OsmChangeAction { * * The Replace-geometry action should try its best to honour these. Some 'wiggling' is allowed (e.g. moving an entrance a bit), but these relations should not be broken.l */ - const distances = new Map distance (or undefined if a duplicate)*/ - number[]>(); + number[] + >() - const nodeInfo = new Map() + const nodeInfo = new Map< + number /* osmId*/, + { + distances: number[] + // Part of some other way then the one that should be replaced + partOfWay: boolean + hasTags: boolean + } + >() for (const node of allNodes) { - const parentWays = nodeDb.GetParentWays(node.id) if (parentWays === undefined) { throw "PANIC: the way to replace has a node which has no parents at all. Is it deleted in the meantime?" } - const parentWayIds = parentWays.data.map(w => w.type + "/" + w.id) + const parentWayIds = parentWays.data.map((w) => w.type + "/" + w.id) const idIndex = parentWayIds.indexOf(this.wayToReplaceId) if (idIndex < 0) { throw "PANIC: the way to replace has a node, which is _not_ part of this was according to the node..." } parentWayIds.splice(idIndex, 1) const partOfSomeWay = parentWayIds.length > 0 - const hasTags = Object.keys(node.tags).length > 1; + const hasTags = Object.keys(node.tags).length > 1 - const nodeDistances = this.targetCoordinates.map(_ => undefined) + const nodeDistances = this.targetCoordinates.map((_) => undefined) for (let i = 0; i < this.targetCoordinates.length; i++) { if (this.identicalTo[i] !== undefined) { - continue; + continue } - const targetCoordinate = this.targetCoordinates[i]; + const targetCoordinate = this.targetCoordinates[i] const cp = node.centerpoint() const d = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]]) if (d > 25) { @@ -268,37 +278,39 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } if (d < 3 || !(hasTags || partOfSomeWay)) { // If there is some relation: cap the move distance to 3m - nodeDistances[i] = d; + nodeDistances[i] = d } - } distances.set(node.id, nodeDistances) nodeInfo.set(node.id, { distances: nodeDistances, partOfWay: partOfSomeWay, - hasTags + hasTags, }) } - const closestIds = this.targetCoordinates.map(_ => undefined) - const unusedIds = new Map(); + const closestIds = this.targetCoordinates.map((_) => undefined) + const unusedIds = new Map< + number, + { + reason: string + hasTags: boolean + } + >() { // Search best merge candidate /** * Then, we search the node that has to move the least distance and add this as mapping. * We do this until no points are left */ - let candidate: number; - let moveDistance: number; + let candidate: number + let moveDistance: number /** * The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates */ do { - candidate = undefined; - moveDistance = Infinity; + candidate = undefined + moveDistance = Infinity distances.forEach((distances, nodeId) => { const minDist = Math.min(...Utils.NoNull(distances)) if (moveDistance > minDist) { @@ -310,14 +322,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { if (candidate !== undefined) { // We found a candidate... Search the corresponding target id: - let targetId: number = undefined; + let targetId: number = undefined let lowestDistance = Number.MAX_VALUE let nodeDistances = distances.get(candidate) for (let i = 0; i < nodeDistances.length; i++) { const d = nodeDistances[i] if (d !== undefined && d < lowestDistance) { - lowestDistance = d; - targetId = i; + lowestDistance = d + targetId = i } } @@ -330,14 +342,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction { closestIds[targetId] = candidate // To indicate that this targetCoordinate is taken, we remove them from the distances matrix - distances.forEach(dists => { + distances.forEach((dists) => { dists[targetId] = undefined }) } else { // Seems like all the targetCoordinates have found a source point unusedIds.set(candidate, { reason: "Unused by new way", - hasTags: nodeInfo.get(candidate).hasTags + hasTags: nodeInfo.get(candidate).hasTags, }) } } @@ -348,18 +360,21 @@ export default class ReplaceGeometryAction extends OsmChangeAction { distances.forEach((_, nodeId) => { unusedIds.set(nodeId, { reason: "Unused by new way", - hasTags: nodeInfo.get(nodeId).hasTags + hasTags: nodeInfo.get(nodeId).hasTags, }) }) - const reprojectedNodes = new Map(); + const reprojectedNodes = new Map< + number, + { + /*Move the node with this ID into the way as extra node, as it has some relation with the original object*/ + projectAfterIndex: number + distance: number + newLat: number + newLon: number + nodeId: number + } + >() { // Lets check the unused ids: can they be detached or do they signify some relation with the object? unusedIds.forEach(({}, id) => { @@ -379,36 +394,32 @@ export default class ReplaceGeometryAction extends OsmChangeAction { properties: {}, geometry: { type: "LineString", - coordinates: self.targetCoordinates - } - }; - const projected = GeoOperations.nearestPoint( - way, [node.lon, node.lat] - ) + coordinates: self.targetCoordinates, + }, + } + const projected = GeoOperations.nearestPoint(way, [node.lon, node.lat]) reprojectedNodes.set(id, { newLon: projected.geometry.coordinates[0], newLat: projected.geometry.coordinates[1], projectAfterIndex: projected.properties.index, distance: projected.properties.dist, - nodeId: id + nodeId: id, }) }) reprojectedNodes.forEach((_, nodeId) => unusedIds.delete(nodeId)) - } - - return {closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes}; + return { closestIds, allNodesById, osmWay, detachedNodes: unusedIds, reprojectedNodes } } protected async CreateChangeDescriptions(changes: Changes): Promise { - const nodeDb = this.state.featurePipeline.fullNodeDatabase; + const nodeDb = this.state.featurePipeline.fullNodeDatabase if (nodeDb === undefined) { throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" } - const {closestIds, osmWay, detachedNodes, reprojectedNodes} = await this.GetClosestIds() + const { closestIds, osmWay, detachedNodes, reprojectedNodes } = await this.GetClosestIds() const allChanges: ChangeDescription[] = [] const actualIdsToUse: number[] = [] for (let i = 0; i < closestIds.length; i++) { @@ -417,47 +428,43 @@ export default class ReplaceGeometryAction extends OsmChangeAction { actualIdsToUse.push(actualIdsToUse[j]) continue } - const closestId = closestIds[i]; + const closestId = closestIds[i] const [lon, lat] = this.targetCoordinates[i] if (closestId === undefined) { - - const newNodeAction = new CreateNewNodeAction( - [], - lat, lon, - { - allowReuseOfPreviouslyCreatedPoints: true, - theme: this.theme, changeType: null - }) + const newNodeAction = new CreateNewNodeAction([], lat, lon, { + allowReuseOfPreviouslyCreatedPoints: true, + theme: this.theme, + changeType: null, + }) const changeDescr = await newNodeAction.CreateChangeDescriptions(changes) allChanges.push(...changeDescr) actualIdsToUse.push(newNodeAction.newElementIdNumber) - } else { const change = { id: closestId, type: "node", meta: { theme: this.theme, - changeType: "move" + changeType: "move", }, - changes: {lon, lat} + changes: { lon, lat }, } actualIdsToUse.push(closestId) allChanges.push(change) } } - if (this.newTags !== undefined && this.newTags.length > 0) { const addExtraTags = new ChangeTagAction( this.wayToReplaceId, new And(this.newTags), - osmWay.tags, { + osmWay.tags, + { theme: this.theme, - changeType: "conflation" + changeType: "conflation", } ) - allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes)) + allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes))) } const newCoordinates = [...this.targetCoordinates] @@ -468,13 +475,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction { const proj = Array.from(reprojectedNodes.values()) proj.sort((a, b) => { // Sort descending - const diff = b.projectAfterIndex - a.projectAfterIndex; + const diff = b.projectAfterIndex - a.projectAfterIndex if (diff !== 0) { return diff } - return b.distance - a.distance; - - + return b.distance - a.distance }) for (const reprojectedNode of proj) { @@ -483,13 +488,20 @@ export default class ReplaceGeometryAction extends OsmChangeAction { type: "node", meta: { theme: this.theme, - changeType: "move" + changeType: "move", }, - changes: {lon: reprojectedNode.newLon, lat: reprojectedNode.newLat} + changes: { lon: reprojectedNode.newLon, lat: reprojectedNode.newLat }, } allChanges.push(change) - actualIdsToUse.splice(reprojectedNode.projectAfterIndex + 1, 0, reprojectedNode.nodeId) - newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [reprojectedNode.newLon, reprojectedNode.newLat]) + actualIdsToUse.splice( + reprojectedNode.projectAfterIndex + 1, + 0, + reprojectedNode.nodeId + ) + newCoordinates.splice(reprojectedNode.projectAfterIndex + 1, 0, [ + reprojectedNode.newLon, + reprojectedNode.newLat, + ]) } } @@ -499,42 +511,46 @@ export default class ReplaceGeometryAction extends OsmChangeAction { id: osmWay.id, changes: { nodes: actualIdsToUse, - coordinates: newCoordinates + coordinates: newCoordinates, }, meta: { theme: this.theme, - changeType: "conflation" - } + changeType: "conflation", + }, }) - // Some nodes might need to be deleted if (detachedNodes.size > 0) { - detachedNodes.forEach(({hasTags, reason}, nodeId) => { + detachedNodes.forEach(({ hasTags, reason }, nodeId) => { const parentWays = nodeDb.GetParentWays(nodeId) - const index = parentWays.data.map(w => w.id).indexOf(osmWay.id) + const index = parentWays.data.map((w) => w.id).indexOf(osmWay.id) if (index < 0) { - console.error("ReplaceGeometryAction is trying to detach node " + nodeId + ", but it isn't listed as being part of way " + osmWay.id) - return; + console.error( + "ReplaceGeometryAction is trying to detach node " + + nodeId + + ", but it isn't listed as being part of way " + + osmWay.id + ) + return } // We detachted this node - so we unregister parentWays.data.splice(index, 1) - parentWays.ping(); + parentWays.ping() if (hasTags) { // Has tags: we leave this node alone - return; + return } if (parentWays.data.length != 0) { // Still part of other ways: we leave this node alone! - return; + return } console.log("Removing node " + nodeId, "as it isn't needed anymore by any way") allChanges.push({ meta: { theme: this.theme, - changeType: "delete" + changeType: "delete", }, doDelete: true, type: "node", @@ -545,6 +561,4 @@ export default class ReplaceGeometryAction extends OsmChangeAction { return allChanges } - - -} \ No newline at end of file +} diff --git a/Logic/Osm/Actions/SplitAction.ts b/Logic/Osm/Actions/SplitAction.ts index 0b97e593f..81ec7a110 100644 --- a/Logic/Osm/Actions/SplitAction.ts +++ b/Logic/Osm/Actions/SplitAction.ts @@ -1,21 +1,21 @@ -import {OsmObject, OsmWay} from "../OsmObject"; -import {Changes} from "../Changes"; -import {GeoOperations} from "../../GeoOperations"; -import OsmChangeAction from "./OsmChangeAction"; -import {ChangeDescription} from "./ChangeDescription"; -import RelationSplitHandler from "./RelationSplitHandler"; +import { OsmObject, OsmWay } from "../OsmObject" +import { Changes } from "../Changes" +import { GeoOperations } from "../../GeoOperations" +import OsmChangeAction from "./OsmChangeAction" +import { ChangeDescription } from "./ChangeDescription" +import RelationSplitHandler from "./RelationSplitHandler" interface SplitInfo { - originalIndex?: number, // or negative for new elements - lngLat: [number, number], + originalIndex?: number // or negative for new elements + lngLat: [number, number] doSplit: boolean } export default class SplitAction extends OsmChangeAction { - private readonly wayId: string; - private readonly _splitPointsCoordinates: [number, number] []// lon, lat - private _meta: { theme: string, changeType: "split" }; - private _toleranceInMeters: number; + private readonly wayId: string + private readonly _splitPointsCoordinates: [number, number][] // lon, lat + private _meta: { theme: string; changeType: "split" } + private _toleranceInMeters: number /** * Create a changedescription for splitting a point. @@ -25,12 +25,17 @@ export default class SplitAction extends OsmChangeAction { * @param meta * @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point */ - constructor(wayId: string, splitPointCoordinates: [number, number][], meta: { theme: string }, toleranceInMeters = 5) { + constructor( + wayId: string, + splitPointCoordinates: [number, number][], + meta: { theme: string }, + toleranceInMeters = 5 + ) { super(wayId, true) - this.wayId = wayId; + this.wayId = wayId this._splitPointsCoordinates = splitPointCoordinates - this._toleranceInMeters = toleranceInMeters; - this._meta = {...meta, changeType: "split"}; + this._toleranceInMeters = toleranceInMeters + this._meta = { ...meta, changeType: "split" } } private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { @@ -47,16 +52,16 @@ export default class SplitAction extends OsmChangeAction { } } wayParts.push(currentPart) - return wayParts.filter(wp => wp.length > 0) + return wayParts.filter((wp) => wp.length > 0) } async CreateChangeDescriptions(changes: Changes): Promise { const originalElement = await OsmObject.DownloadObjectAsync(this.wayId) - const originalNodes = originalElement.nodes; + const originalNodes = originalElement.nodes // First, calculate splitpoints and remove points close to one another const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) - // Now we have a list with e.g. + // Now we have a list with e.g. // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}] // Lets change 'originalIndex' to the actual node id first (or assign a new id if needed): @@ -64,19 +69,19 @@ export default class SplitAction extends OsmChangeAction { if (element.originalIndex >= 0) { element.originalIndex = originalElement.nodes[element.originalIndex] } else { - element.originalIndex = changes.getNewID(); + element.originalIndex = changes.getNewID() } } // Next up is creating actual parts from this - const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo); + const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo) // Allright! At this point, we have our new ways! // Which one is the longest of them (and can keep the id)? - let longest = undefined; + let longest = undefined for (const wayPart of wayParts) { if (longest === undefined) { - longest = wayPart; + longest = wayPart continue } if (wayPart.length > longest.length) { @@ -88,16 +93,16 @@ export default class SplitAction extends OsmChangeAction { // Let's create the new points as needed for (const element of splitInfo) { if (element.originalIndex >= 0) { - continue; + continue } changeDescription.push({ type: "node", id: element.originalIndex, changes: { lon: element.lngLat[0], - lat: element.lngLat[1] + lat: element.lngLat[1], }, - meta: this._meta + meta: this._meta, }) } @@ -107,24 +112,23 @@ export default class SplitAction extends OsmChangeAction { const allWaysNodesInOrder: number[][] = [] // Lets create OsmWays based on them for (const wayPart of wayParts) { - let isOriginal = wayPart === longest if (isOriginal) { // We change the actual element! - const nodeIds = wayPart.map(p => p.originalIndex) + const nodeIds = wayPart.map((p) => p.originalIndex) changeDescription.push({ type: "way", id: originalElement.id, changes: { - coordinates: wayPart.map(p => p.lngLat), - nodes: nodeIds + coordinates: wayPart.map((p) => p.lngLat), + nodes: nodeIds, }, - meta: this._meta + meta: this._meta, }) allWayIdsInOrder.push(originalElement.id) allWaysNodesInOrder.push(nodeIds) } else { - let id = changes.getNewID(); + let id = changes.getNewID() // Copy the tags from the original object onto the new const kv = [] for (const k in originalElement.tags) { @@ -132,20 +136,20 @@ export default class SplitAction extends OsmChangeAction { continue } if (k.startsWith("_") || k === "id") { - continue; + continue } - kv.push({k: k, v: originalElement.tags[k]}) + kv.push({ k: k, v: originalElement.tags[k] }) } - const nodeIds = wayPart.map(p => p.originalIndex) + const nodeIds = wayPart.map((p) => p.originalIndex) changeDescription.push({ type: "way", id: id, tags: kv, changes: { - coordinates: wayPart.map(p => p.lngLat), - nodes: nodeIds + coordinates: wayPart.map((p) => p.lngLat), + nodes: nodeIds, }, - meta: this._meta + meta: this._meta, }) allWayIdsInOrder.push(id) @@ -157,13 +161,16 @@ export default class SplitAction extends OsmChangeAction { // At least, the order of the ways is identical, so we can keep the same roles const relations = await OsmObject.DownloadReferencingRelations(this.wayId) for (const relation of relations) { - const changDescrs = await new RelationSplitHandler({ - relation: relation, - allWayIdsInOrder: allWayIdsInOrder, - originalNodes: originalNodes, - allWaysNodesInOrder: allWaysNodesInOrder, - originalWayId: originalElement.id, - }, this._meta.theme).CreateChangeDescriptions(changes) + const changDescrs = await new RelationSplitHandler( + { + relation: relation, + allWayIdsInOrder: allWayIdsInOrder, + originalNodes: originalNodes, + allWaysNodesInOrder: allWaysNodesInOrder, + originalWayId: originalElement.id, + }, + this._meta.theme + ).CreateChangeDescriptions(changes) changeDescription.push(...changDescrs) } @@ -180,48 +187,47 @@ export default class SplitAction extends OsmChangeAction { private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { const wayGeoJson = osmWay.asGeoJson() // Should be [lon, lat][] - const originalPoints: [number, number][] = osmWay.coordinates.map(c => [c[1], c[0]]) + const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]]) const allPoints: { // lon, lat - coordinates: [number, number], - isSplitPoint: boolean, - originalIndex?: number, // Original index - dist: number, // Distance from the nearest point on the original line + coordinates: [number, number] + isSplitPoint: boolean + originalIndex?: number // Original index + dist: number // Distance from the nearest point on the original line location: number // Distance from the start of the way - }[] = this._splitPointsCoordinates.map(c => { + }[] = this._splitPointsCoordinates.map((c) => { // From the turf.js docs: - // The properties object will contain three values: + // The properties object will contain three values: // - `index`: closest point was found on nth line part, - // - `dist`: distance between pt and the closest point, + // - `dist`: distance between pt and the closest point, // `location`: distance along the line between start and the closest point. let projected = GeoOperations.nearestPoint(wayGeoJson, c) // c is lon lat - return ({ + return { coordinates: c, isSplitPoint: true, dist: projected.properties.dist, - location: projected.properties.location - }); + location: projected.properties.location, + } }) // We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ... // We project them onto the line (which should yield pretty much the same point and add them to allPoints for (let i = 0; i < originalPoints.length; i++) { - let originalPoint = originalPoints[i]; + let originalPoint = originalPoints[i] let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint) allPoints.push({ coordinates: originalPoint, isSplitPoint: false, location: projected.properties.location, originalIndex: i, - dist: projected.properties.dist + dist: projected.properties.dist, }) } // At this point, we have a list of both the split point and the old points, with some properties to discriminate between them // We sort this list so that the new points are at the same location allPoints.sort((a, b) => a.location - b.location) - for (let i = allPoints.length - 2; i >= 1; i--) { // We 'merge' points with already existing nodes if they are close enough to avoid closeby elements @@ -244,7 +250,7 @@ export default class SplitAction extends OsmChangeAction { if (distToNext * 1000 > toleranceInM && distToPrev * 1000 > toleranceInM) { // Both are too far away to mark them as the split point - continue; + continue } let closest = nextPoint @@ -256,9 +262,8 @@ export default class SplitAction extends OsmChangeAction { // We can not split on the first or last points... continue } - closest.isSplitPoint = true; + closest.isSplitPoint = true allPoints.splice(i, 1) - } const splitInfo: SplitInfo[] = [] @@ -267,19 +272,17 @@ export default class SplitAction extends OsmChangeAction { for (const p of allPoints) { let index = p.originalIndex if (index === undefined) { - index = nextId; - nextId--; + index = nextId + nextId-- } const splitInfoElement = { originalIndex: index, lngLat: p.coordinates, - doSplit: p.isSplitPoint + doSplit: p.isSplitPoint, } splitInfo.push(splitInfoElement) } return splitInfo } - - } diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index ff0cde50b..9f1054394 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -1,106 +1,110 @@ -import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; -import {UIEventSource} from "../UIEventSource"; -import Constants from "../../Models/Constants"; -import OsmChangeAction from "./Actions/OsmChangeAction"; -import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription"; -import {Utils} from "../../Utils"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; -import SimpleMetaTagger from "../SimpleMetaTagger"; -import CreateNewNodeAction from "./Actions/CreateNewNodeAction"; -import FeatureSource from "../FeatureSource/FeatureSource"; -import {ElementStorage} from "../ElementStorage"; -import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler"; -import {GeoOperations} from "../GeoOperations"; -import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler"; -import {OsmConnection} from "./OsmConnection"; +import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject" +import { UIEventSource } from "../UIEventSource" +import Constants from "../../Models/Constants" +import OsmChangeAction from "./Actions/OsmChangeAction" +import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription" +import { Utils } from "../../Utils" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import SimpleMetaTagger from "../SimpleMetaTagger" +import CreateNewNodeAction from "./Actions/CreateNewNodeAction" +import FeatureSource from "../FeatureSource/FeatureSource" +import { ElementStorage } from "../ElementStorage" +import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler" +import { GeoOperations } from "../GeoOperations" +import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" +import { OsmConnection } from "./OsmConnection" /** * Handles all changes made to OSM. * Needs an authenticator via OsmConnection */ export class Changes { - public readonly name = "Newly added features" /** * All the newly created features as featureSource + all the modified features */ - public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); - public readonly pendingChanges: UIEventSource = LocalStorageSource.GetParsed("pending-changes", []) + public features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) + public readonly pendingChanges: UIEventSource = + LocalStorageSource.GetParsed("pending-changes", []) public readonly allChanges = new UIEventSource(undefined) public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection } public readonly extraComment: UIEventSource = new UIEventSource(undefined) - + private historicalUserLocations: FeatureSource - private _nextId: number = -1; // Newly assigned ID's are negative - private readonly isUploading = new UIEventSource(false); + private _nextId: number = -1 // Newly assigned ID's are negative + private readonly isUploading = new UIEventSource(false) private readonly previouslyCreated: OsmObject[] = [] - private readonly _leftRightSensitive: boolean; - private _changesetHandler: ChangesetHandler; + private readonly _leftRightSensitive: boolean + private _changesetHandler: ChangesetHandler constructor( state?: { - allElements: ElementStorage, + allElements: ElementStorage osmConnection: OsmConnection }, - leftRightSensitive: boolean = false) { - this._leftRightSensitive = leftRightSensitive; + leftRightSensitive: boolean = false + ) { + this._leftRightSensitive = leftRightSensitive // We keep track of all changes just as well this.allChanges.setData([...this.pendingChanges.data]) // If a pending change contains a negative ID, we save that - this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? []) - this.state = state; - this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(state.allElements, this) + this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) + this.state = state + this._changesetHandler = state?.osmConnection?.CreateChangesetHandler( + state.allElements, + this + ) // Note: a changeset might be reused which was opened just before and might have already used some ids // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset } - static createChangesetFor(csId: string, - allChanges: { - modifiedObjects: OsmObject[], - newObjects: OsmObject[], - deletedObjects: OsmObject[] - }): string { - + static createChangesetFor( + csId: string, + allChanges: { + modifiedObjects: OsmObject[] + newObjects: OsmObject[] + deletedObjects: OsmObject[] + } + ): string { const changedElements = allChanges.modifiedObjects ?? [] const newElements = allChanges.newObjects ?? [] const deletedElements = allChanges.deletedObjects ?? [] - let changes = ``; + let changes = `` if (newElements.length > 0) { changes += "\n\n" + - newElements.map(e => e.ChangesetXML(csId)).join("\n") + - ""; + newElements.map((e) => e.ChangesetXML(csId)).join("\n") + + "" } if (changedElements.length > 0) { changes += "\n\n" + - changedElements.map(e => e.ChangesetXML(csId)).join("\n") + - "\n"; + changedElements.map((e) => e.ChangesetXML(csId)).join("\n") + + "\n" } if (deletedElements.length > 0) { changes += "\n\n" + - deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + + deletedElements.map((e) => e.ChangesetXML(csId)).join("\n") + "\n" } - changes += ""; - return changes; + changes += "" + return changes } private static GetNeededIds(changes: ChangeDescription[]) { - return Utils.Dedup(changes.filter(c => c.id >= 0) - .map(c => c.type + "/" + c.id)) + return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id)) } /** * Returns a new ID and updates the value for the next ID */ public getNewID() { - return this._nextId--; + return this._nextId-- } /** @@ -109,64 +113,71 @@ export class Changes { */ public async flushChanges(flushreason: string = undefined): Promise { if (this.pendingChanges.data.length === 0) { - return; + return } if (this.isUploading.data) { console.log("Is already uploading... Abort") - return; + return } - console.log("Uploading changes due to: ", flushreason) this.isUploading.setData(true) try { const csNumber = await this.flushChangesAsync() this.isUploading.setData(false) - console.log("Changes flushed. Your changeset is " + csNumber); + console.log("Changes flushed. Your changeset is " + csNumber) } catch (e) { this.isUploading.setData(false) - console.error("Flushing changes failed due to", e); + console.error("Flushing changes failed due to", e) } } public async applyAction(action: OsmChangeAction): Promise { const changeDescriptions = await action.Perform(this) - changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges(action, changeDescriptions) + changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges( + action, + changeDescriptions + ) this.applyChanges(changeDescriptions) } public applyChanges(changes: ChangeDescription[]) { console.log("Received changes:", changes) - this.pendingChanges.data.push(...changes); - this.pendingChanges.ping(); + this.pendingChanges.data.push(...changes) + this.pendingChanges.ping() this.allChanges.data.push(...changes) this.allChanges.ping() } - - private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { + private calculateDistanceToChanges( + change: OsmChangeAction, + changeDescriptions: ChangeDescription[] + ) { const locations = this.historicalUserLocations?.features?.data if (locations === undefined) { // No state loaded or no locations -> we can't calculate... - return; + return } if (!change.trackStatistics) { // Probably irrelevant, such as a new helper node - return; + return } const now = new Date() - const recentLocationPoints = locations.map(ff => ff.feature) - .filter(feat => feat.geometry.type === "Point") - .filter(feat => { - const visitTime = new Date((feat.properties).date) + const recentLocationPoints = locations + .map((ff) => ff.feature) + .filter((feat) => feat.geometry.type === "Point") + .filter((feat) => { + const visitTime = new Date( + ((feat.properties)).date + ) // In seconds const diff = (now.getTime() - visitTime.getTime()) / 1000 - return diff < Constants.nearbyVisitTime; + return diff < Constants.nearbyVisitTime }) if (recentLocationPoints.length === 0) { - // Probably no GPS enabled/no fix - return; + // Probably no GPS enabled/no fix + return } // The applicable points, contain information in their properties about location, time and GPS accuracy @@ -182,7 +193,10 @@ export class Changes { } for (const changeDescription of changeDescriptions) { - const chng: { lat: number, lon: number } | { coordinates: [number, number][] } | { members } = changeDescription.changes + const chng: + | { lat: number; lon: number } + | { coordinates: [number, number][] } + | { members } = changeDescription.changes if (chng === undefined) { continue } @@ -194,61 +208,85 @@ export class Changes { } } - return Math.min(...changedObjectCoordinates.map(coor => - Math.min(...recentLocationPoints.map(gpsPoint => { - const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) - return GeoOperations.distanceBetween(coor, otherCoor) - })) - )) + return Math.min( + ...changedObjectCoordinates.map((coor) => + Math.min( + ...recentLocationPoints.map((gpsPoint) => { + const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) + return GeoOperations.distanceBetween(coor, otherCoor) + }) + ) + ) + ) } /** * UPload the selected changes to OSM. * Returns 'true' if successfull and if they can be removed */ - private async flushSelectChanges(pending: ChangeDescription[], openChangeset: UIEventSource): Promise { - const self = this; + private async flushSelectChanges( + pending: ChangeDescription[], + openChangeset: UIEventSource + ): Promise { + const self = this const neededIds = Changes.GetNeededIds(pending) - const osmObjects = Utils.NoNull(await Promise.all(neededIds.map(async id => - OsmObject.DownloadObjectAsync(id).catch(e => { - console.error("Could not download OSM-object", id, " dropping it from the changes ("+e+")") - pending = pending.filter(ch => ch.type + "/" + ch.id !== id) - return undefined; - })))); + const osmObjects = Utils.NoNull( + await Promise.all( + neededIds.map(async (id) => + OsmObject.DownloadObjectAsync(id).catch((e) => { + console.error( + "Could not download OSM-object", + id, + " dropping it from the changes (" + e + ")" + ) + pending = pending.filter((ch) => ch.type + "/" + ch.id !== id) + return undefined + }) + ) + ) + ) if (this._leftRightSensitive) { - osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags)) + osmObjects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags)) } console.log("Got the fresh objects!", osmObjects, "pending: ", pending) - if(pending.length == 0){ + if (pending.length == 0) { console.log("No pending changes...") - return true; + return true } - + const perType = Array.from( - Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) - .map(descr => descr.meta.changeType)), ([key, count]) => ( - { - key: key, - value: count, - aggregate: true - })) - const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined) - .map(descr => ({ + Utils.Hist( + pending + .filter( + (descr) => + descr.meta.changeType !== undefined && descr.meta.changeType !== null + ) + .map((descr) => descr.meta.changeType) + ), + ([key, count]) => ({ + key: key, + value: count, + aggregate: true, + }) + ) + const motivations = pending + .filter((descr) => descr.meta.specialMotivation !== undefined) + .map((descr) => ({ key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, - value: descr.meta.specialMotivation + value: descr.meta.specialMotivation, })) - const distances = Utils.NoNull(pending.map(descr => descr.meta.distanceToObject)); + const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) distances.sort((a, b) => a - b) - const perBinCount = Constants.distanceToChangeObjectBins.map(_ => 0) + const perBinCount = Constants.distanceToChangeObjectBins.map((_) => 0) - let j = 0; + let j = 0 const maxDistances = Constants.distanceToChangeObjectBins for (let i = 0; i < maxDistances.length; i++) { - const maxDistance = maxDistances[i]; + const maxDistance = maxDistances[i] // distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too while (j < distances.length && distances[j] < maxDistance) { perBinCount[i]++ @@ -256,21 +294,23 @@ export class Changes { } } - const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => { - if (count === 0) { - return undefined - } - const maxD = maxDistances[i] - let key = `change_within_${maxD}m` - if (maxD === Number.MAX_VALUE) { - key = `change_over_${maxDistances[i - 1]}m` - } - return { - key, - value: count, - aggregate: true - } - })) + const perBinMessage = Utils.NoNull( + perBinCount.map((count, i) => { + if (count === 0) { + return undefined + } + const maxD = maxDistances[i] + let key = `change_within_${maxD}m` + if (maxD === Number.MAX_VALUE) { + key = `change_over_${maxDistances[i - 1]}m` + } + return { + key, + value: count, + aggregate: true, + } + }) + ) // This method is only called with changedescriptions for this theme const theme = pending[0].meta.theme @@ -279,46 +319,47 @@ export class Changes { comment += "\n\n" + this.extraComment.data } - const metatags: ChangesetTag[] = [{ - key: "comment", - value: comment - }, + const metatags: ChangesetTag[] = [ + { + key: "comment", + value: comment, + }, { key: "theme", - value: theme + value: theme, }, ...perType, ...motivations, - ...perBinMessage + ...perBinMessage, ] await this._changesetHandler.UploadChangeset( - (csId, remappings) =>{ - if(remappings.size > 0){ + (csId, remappings) => { + if (remappings.size > 0) { console.log("Rewriting pending changes from", pending, "with", remappings) - pending = pending.map(ch => ChangeDescriptionTools.rewriteIds(ch, remappings)) + pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings)) console.log("Result is", pending) } const changes: { - newObjects: OsmObject[], + newObjects: OsmObject[] modifiedObjects: OsmObject[] deletedObjects: OsmObject[] } = self.CreateChangesetObjects(pending, osmObjects) - return Changes.createChangesetFor("" + csId, changes) + return Changes.createChangesetFor("" + csId, changes) }, metatags, openChangeset ) console.log("Upload successfull!") - return true; + return true } private async flushChangesAsync(): Promise { - const self = this; + const self = this try { // At last, we build the changeset and upload - const pending = self.pendingChanges.data; + const pending = self.pendingChanges.data const pendingPerTheme = new Map() for (const changeDescription of pending) { @@ -329,50 +370,62 @@ export class Changes { pendingPerTheme.get(theme).push(changeDescription) } - const successes = await Promise.all(Array.from(pendingPerTheme, - async ([theme, pendingChanges]) => { + const successes = await Promise.all( + Array.from(pendingPerTheme, async ([theme, pendingChanges]) => { try { - const openChangeset = this.state.osmConnection.GetPreference("current-open-changeset-" + theme).sync( - str => { - const n = Number(str); - if (isNaN(n)) { - return undefined - } - return n - }, [], n => "" + n - ); - console.log("Using current-open-changeset-" + theme + " from the preferences, got " + openChangeset.data) + const openChangeset = this.state.osmConnection + .GetPreference("current-open-changeset-" + theme) + .sync( + (str) => { + const n = Number(str) + if (isNaN(n)) { + return undefined + } + return n + }, + [], + (n) => "" + n + ) + console.log( + "Using current-open-changeset-" + + theme + + " from the preferences, got " + + openChangeset.data + ) - return await self.flushSelectChanges(pendingChanges, openChangeset); + return await self.flushSelectChanges(pendingChanges, openChangeset) } catch (e) { console.error("Could not upload some changes:", e) return false } - })) + }) + ) - if (!successes.some(s => s == false)) { + if (!successes.some((s) => s == false)) { // All changes successfull, we clear the data! - this.pendingChanges.setData([]); + this.pendingChanges.setData([]) } - } catch (e) { - console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e) + console.error( + "Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", + e + ) self.pendingChanges.setData([]) } finally { self.isUploading.setData(false) } - - } - public CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { - newObjects: OsmObject[], + public CreateChangesetObjects( + changes: ChangeDescription[], + downloadedOsmObjects: OsmObject[] + ): { + newObjects: OsmObject[] modifiedObjects: OsmObject[] deletedObjects: OsmObject[] - } { const objects: Map = new Map() - const states: Map = new Map(); + const states: Map = new Map() for (const o of downloadedOsmObjects) { objects.set(o.type + "/" + o.id, o) @@ -385,7 +438,7 @@ export class Changes { } for (const change of changes) { - let changed = false; + let changed = false const id = change.type + "/" + change.id if (!objects.has(id)) { // The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition @@ -400,24 +453,24 @@ export class Changes { // This is a new object that should be created states.set(id, "created") console.log("Creating object for changeDescription", change) - let osmObj: OsmObject = undefined; + let osmObj: OsmObject = undefined switch (change.type) { case "node": const n = new OsmNode(change.id) n.lat = change.changes["lat"] n.lon = change.changes["lon"] osmObj = n - break; + break case "way": const w = new OsmWay(change.id) w.nodes = change.changes["nodes"] osmObj = w - break; + break case "relation": const r = new OsmRelation(change.id) r.members = change.changes["members"] osmObj = r - break; + break } if (osmObj === undefined) { throw "Hmm? This is a bug" @@ -442,55 +495,57 @@ export class Changes { let v = kv.v if (v === "") { - v = undefined; + v = undefined } const oldV = obj.tags[k] if (oldV === v) { - continue; + continue } - obj.tags[k] = v; - changed = true; - - + obj.tags[k] = v + changed = true } if (change.changes !== undefined) { switch (change.type) { case "node": // @ts-ignore - const nlat = change.changes.lat; + const nlat = change.changes.lat // @ts-ignore - const nlon = change.changes.lon; + const nlon = change.changes.lon const n = obj if (n.lat !== nlat || n.lon !== nlon) { - n.lat = nlat; - n.lon = nlon; - changed = true; + n.lat = nlat + n.lon = nlon + changed = true } - break; + break case "way": const nnodes = change.changes["nodes"] const w = obj if (!Utils.Identical(nnodes, w.nodes)) { w.nodes = nnodes - changed = true; + changed = true } - break; + break case "relation": - const nmembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = change.changes["members"] + const nmembers: { + type: "node" | "way" | "relation" + ref: number + role: string + }[] = change.changes["members"] const r = obj - if (!Utils.Identical(nmembers, r.members, (a, b) => { - return a.role === b.role && a.type === b.type && a.ref === b.ref - })) { - r.members = nmembers; - changed = true; + if ( + !Utils.Identical(nmembers, r.members, (a, b) => { + return a.role === b.role && a.type === b.type && a.ref === b.ref + }) + ) { + r.members = nmembers + changed = true } - break; - + break } - } if (changed && states.get(id) === "unchanged") { @@ -498,15 +553,13 @@ export class Changes { } } - const result = { newObjects: [], modifiedObjects: [], - deletedObjects: [] + deletedObjects: [], } objects.forEach((v, id) => { - const state = states.get(id) if (state === "created") { result.newObjects.push(v) @@ -517,14 +570,21 @@ export class Changes { if (state === "deleted") { result.deletedObjects.push(v) } - }) - console.debug("Calculated the pending changes: ", result.newObjects.length, "new; ", result.modifiedObjects.length, "modified;", result.deletedObjects, "deleted") + console.debug( + "Calculated the pending changes: ", + result.newObjects.length, + "new; ", + result.modifiedObjects.length, + "modified;", + result.deletedObjects, + "deleted" + ) return result } - - public setHistoricalUserLocations(locations: FeatureSource ){ + + public setHistoricalUserLocations(locations: FeatureSource) { this.historicalUserLocations = locations } -} \ No newline at end of file +} diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index 13723c449..689d173cc 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -1,28 +1,26 @@ -import escapeHtml from "escape-html"; -import UserDetails, {OsmConnection} from "./OsmConnection"; -import {UIEventSource} from "../UIEventSource"; -import {ElementStorage} from "../ElementStorage"; -import Locale from "../../UI/i18n/Locale"; -import Constants from "../../Models/Constants"; -import {Changes} from "./Changes"; -import {Utils} from "../../Utils"; +import escapeHtml from "escape-html" +import UserDetails, { OsmConnection } from "./OsmConnection" +import { UIEventSource } from "../UIEventSource" +import { ElementStorage } from "../ElementStorage" +import Locale from "../../UI/i18n/Locale" +import Constants from "../../Models/Constants" +import { Changes } from "./Changes" +import { Utils } from "../../Utils" export interface ChangesetTag { - key: string, - value: string | number, + key: string + value: string | number aggregate?: boolean } export class ChangesetHandler { - - private readonly allElements: ElementStorage; - private osmConnection: OsmConnection; - private readonly changes: Changes; - private readonly _dryRun: UIEventSource; - private readonly userDetails: UIEventSource; - private readonly auth: any; - private readonly backend: string; - + private readonly allElements: ElementStorage + private osmConnection: OsmConnection + private readonly changes: Changes + private readonly _dryRun: UIEventSource + private readonly userDetails: UIEventSource + private readonly auth: any + private readonly backend: string /** * Contains previously rewritten IDs @@ -30,7 +28,6 @@ export class ChangesetHandler { */ private readonly _remappings = new Map() - /** * Use 'osmConnection.CreateChangesetHandler' instead * @param dryRun @@ -39,36 +36,36 @@ export class ChangesetHandler { * @param changes * @param auth */ - constructor(dryRun: UIEventSource, - osmConnection: OsmConnection, - allElements: ElementStorage, - changes: Changes, - auth) { - this.osmConnection = osmConnection; - this.allElements = allElements; - this.changes = changes; - this._dryRun = dryRun; - this.userDetails = osmConnection.userDetails; + constructor( + dryRun: UIEventSource, + osmConnection: OsmConnection, + allElements: ElementStorage, + changes: Changes, + auth + ) { + this.osmConnection = osmConnection + this.allElements = allElements + this.changes = changes + this._dryRun = dryRun + this.userDetails = osmConnection.userDetails this.backend = osmConnection._oauth_config.url - this.auth = auth; + this.auth = auth if (dryRun) { - console.log("DRYRUN ENABLED"); + console.log("DRYRUN ENABLED") } - } - /** * Creates a new list which contains every key at most once - * + * * ChangesetHandler.removeDuplicateMetaTags([{key: "k", value: "v"}, {key: "k0", value: "v0"}, {key: "k", value:"v"}] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}] */ - public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[]{ - const r : ChangesetTag[] = [] + public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] { + const r: ChangesetTag[] = [] const seen = new Set() for (const extraMetaTag of extraMetaTags) { - if(seen.has(extraMetaTag.key)){ + if (seen.has(extraMetaTag.key)) { continue } r.push(extraMetaTag) @@ -86,7 +83,7 @@ export class ChangesetHandler { * @private */ static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map) { - let hasChange = false; + let hasChange = false for (const tag of extraMetaTags) { const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/) if (match == null) { @@ -115,40 +112,48 @@ export class ChangesetHandler { public async UploadChangeset( generateChangeXML: (csid: number, remappings: Map) => string, extraMetaTags: ChangesetTag[], - openChangeset: UIEventSource): Promise { - - if (!extraMetaTags.some(tag => tag.key === "comment") || !extraMetaTags.some(tag => tag.key === "theme")) { + openChangeset: UIEventSource + ): Promise { + if ( + !extraMetaTags.some((tag) => tag.key === "comment") || + !extraMetaTags.some((tag) => tag.key === "theme") + ) { throw "The meta tags should at least contain a `comment` and a `theme`" } - + extraMetaTags = [...extraMetaTags, ...this.defaultChangesetTags()] extraMetaTags = ChangesetHandler.removeDuplicateMetaTags(extraMetaTags) if (this.userDetails.data.csCount == 0) { // The user became a contributor! - this.userDetails.data.csCount = 1; - this.userDetails.ping(); + this.userDetails.data.csCount = 1 + this.userDetails.ping() } if (this._dryRun.data) { - const changesetXML = generateChangeXML(123456, this._remappings); + const changesetXML = generateChangeXML(123456, this._remappings) console.log("Metatags are", extraMetaTags) - console.log(changesetXML); - return; + console.log(changesetXML) + return } if (openChangeset.data === undefined) { // We have to open a new changeset try { const csId = await this.OpenChangeset(extraMetaTags) - openChangeset.setData(csId); - const changeset = generateChangeXML(csId, this._remappings); - console.trace("Opened a new changeset (openChangeset.data is undefined):", changeset); + openChangeset.setData(csId) + const changeset = generateChangeXML(csId, this._remappings) + console.trace( + "Opened a new changeset (openChangeset.data is undefined):", + changeset + ) const changes = await this.UploadChange(csId, changeset) - const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes) - if(hasSpecialMotivationChanges){ + const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( + extraMetaTags, + changes + ) + if (hasSpecialMotivationChanges) { // At this point, 'extraMetaTags' will have changed - we need to set the tags again this.UpdateTags(csId, extraMetaTags) } - } catch (e) { console.error("Could not open/upload changeset due to ", e) openChangeset.setData(undefined) @@ -156,29 +161,32 @@ export class ChangesetHandler { } else { // There still exists an open changeset (or at least we hope so) // Let's check! - const csId = openChangeset.data; + const csId = openChangeset.data try { - const oldChangesetMeta = await this.GetChangesetMeta(csId) if (!oldChangesetMeta.open) { // Mark the CS as closed... console.log("Could not fetch the metadata from the already open changeset") - openChangeset.setData(undefined); - // ... and try again. As the cs is closed, no recursive loop can exist + openChangeset.setData(undefined) + // ... and try again. As the cs is closed, no recursive loop can exist await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset) - return; + return } const rewritings = await this.UploadChange( csId, - generateChangeXML(csId, this._remappings)) + generateChangeXML(csId, this._remappings) + ) - const rewrittenTags = this.RewriteTagsOf(extraMetaTags, rewritings, oldChangesetMeta) + const rewrittenTags = this.RewriteTagsOf( + extraMetaTags, + rewritings, + oldChangesetMeta + ) await this.UpdateTags(csId, rewrittenTags) - } catch (e) { - console.warn("Could not upload, changeset is probably closed: ", e); - openChangeset.setData(undefined); + console.warn("Could not upload, changeset is probably closed: ", e) + openChangeset.setData(undefined) } } } @@ -190,17 +198,17 @@ export class ChangesetHandler { * @param rewriteIds: the mapping of ids * @param oldChangesetMeta: the metadata-object of the already existing changeset */ - public RewriteTagsOf(extraMetaTags: ChangesetTag[], - rewriteIds: Map, - oldChangesetMeta: { - open: boolean, - id: number - uid: number, // User ID - changes_count: number, - tags: any - }) : ChangesetTag[] { - - + public RewriteTagsOf( + extraMetaTags: ChangesetTag[], + rewriteIds: Map, + oldChangesetMeta: { + open: boolean + id: number + uid: number // User ID + changes_count: number + tags: any + } + ): ChangesetTag[] { // Note: extraMetaTags is where all the tags are collected into // same as 'extraMetaTag', but indexed @@ -221,7 +229,7 @@ export class ChangesetHandler { if (newMetaTag === undefined) { extraMetaTags.push({ key: key, - value: oldCsTags[key] + value: oldCsTags[key], }) continue } @@ -242,10 +250,8 @@ export class ChangesetHandler { } } - ChangesetHandler.rewriteMetaTags(extraMetaTags, rewriteIds) return extraMetaTags - } /** @@ -255,28 +261,28 @@ export class ChangesetHandler { * @private */ private static parseIdRewrite(node: any, type: string): [string, string] { - const oldId = parseInt(node.attributes.old_id.value); + const oldId = parseInt(node.attributes.old_id.value) if (node.attributes.new_id === undefined) { - return [type+"/"+oldId, undefined]; + return [type + "/" + oldId, undefined] } - const newId = parseInt(node.attributes.new_id.value); + const newId = parseInt(node.attributes.new_id.value) // The actual mapping const result: [string, string] = [type + "/" + oldId, type + "/" + newId] - if(oldId === newId){ - return undefined; + if (oldId === newId) { + return undefined } - return result; + return result } /** - * Given a diff-result XML of the form + * Given a diff-result XML of the form * * * * , * will: - * + * * - create a mapping `{'node/-1' --> "node/9650458521", 'way/-2' --> "way/9650458521"} * - Call this.changes.registerIdRewrites * - Call handleIdRewrites as needed @@ -284,9 +290,9 @@ export class ChangesetHandler { * @private */ private parseUploadChangesetResponse(response: XMLDocument): Map { - const nodes = response.getElementsByTagName("node"); - const mappings : [string, string][]= [] - + const nodes = response.getElementsByTagName("node") + const mappings: [string, string][] = [] + for (const node of Array.from(nodes)) { const mapping = ChangesetHandler.parseIdRewrite(node, "node") if (mapping !== undefined) { @@ -294,7 +300,7 @@ export class ChangesetHandler { } } - const ways = response.getElementsByTagName("way"); + const ways = response.getElementsByTagName("way") for (const way of Array.from(ways)) { const mapping = ChangesetHandler.parseIdRewrite(way, "way") if (mapping !== undefined) { @@ -303,40 +309,41 @@ export class ChangesetHandler { } for (const mapping of mappings) { const [oldId, newId] = mapping - this.allElements.addAlias(oldId, newId); - if(newId !== undefined) { + this.allElements.addAlias(oldId, newId) + if (newId !== undefined) { this._remappings.set(mapping[0], mapping[1]) } } return new Map(mappings) - } private async CloseChangeset(changesetId: number = undefined): Promise { const self = this return new Promise(function (resolve, reject) { if (changesetId === undefined) { - return; + return } - self.auth.xhr({ - method: 'PUT', - path: '/api/0.6/changeset/' + changesetId + '/close', - }, function (err, response) { - if (response == null) { - - console.log("err", err); + self.auth.xhr( + { + method: "PUT", + path: "/api/0.6/changeset/" + changesetId + "/close", + }, + function (err, response) { + if (response == null) { + console.log("err", err) + } + console.log("Closed changeset ", changesetId) + resolve() } - console.log("Closed changeset ", changesetId) - resolve() - }); + ) }) } async GetChangesetMeta(csId: number): Promise<{ - id: number, - open: boolean, - uid: number, - changes_count: number, + id: number + open: boolean + uid: number + changes_count: number tags: any }> { const url = `${this.backend}/api/0.6/changeset/${csId}` @@ -344,47 +351,59 @@ export class ChangesetHandler { return csData.elements[0] } - /** * Puts the specified tags onto the changesets as they are. * This method will erase previously set tags */ - private async UpdateTags( - csId: number, - tags: ChangesetTag[]) { + private async UpdateTags(csId: number, tags: ChangesetTag[]) { tags = ChangesetHandler.removeDuplicateMetaTags(tags) - const self = this; + const self = this return new Promise(function (resolve, reject) { + tags = Utils.NoNull(tags).filter( + (tag) => + tag.key !== undefined && + tag.value !== undefined && + tag.key !== "" && + tag.value !== "" + ) + const metadata = tags.map((kv) => ``) - tags = Utils.NoNull(tags).filter(tag => tag.key !== undefined && tag.value !== undefined && tag.key !== "" && tag.value !== "") - const metadata = tags.map(kv => ``) - - self.auth.xhr({ - method: 'PUT', - path: '/api/0.6/changeset/' + csId, - options: {header: {'Content-Type': 'text/xml'}}, - content: [``, - metadata, - ``].join("") - }, function (err, response) { - if (response === undefined) { - console.error("Updating the tags of changeset "+csId+" failed:", err); - reject(err) - } else { - resolve(response); + self.auth.xhr( + { + method: "PUT", + path: "/api/0.6/changeset/" + csId, + options: { header: { "Content-Type": "text/xml" } }, + content: [``, metadata, ``].join(""), + }, + function (err, response) { + if (response === undefined) { + console.error("Updating the tags of changeset " + csId + " failed:", err) + reject(err) + } else { + resolve(response) + } } - }); + ) }) } - - private defaultChangesetTags() : ChangesetTag[]{ - return [ ["created_by", `MapComplete ${Constants.vNumber}`], + + private defaultChangesetTags(): ChangesetTag[] { + return [ + ["created_by", `MapComplete ${Constants.vNumber}`], ["locale", Locale.language.data], ["host", `${window.location.origin}${window.location.pathname}`], - ["source", this.changes.state["currentUserLocation"]?.features?.data?.length > 0 ? "survey" : undefined], - ["imagery", this.changes.state["backgroundLayer"]?.data?.id]].map(([key, value]) => ({ - key, value, aggretage: false + [ + "source", + this.changes.state["currentUserLocation"]?.features?.data?.length > 0 + ? "survey" + : undefined, + ], + ["imagery", this.changes.state["backgroundLayer"]?.data?.id], + ].map(([key, value]) => ({ + key, + value, + aggretage: false, })) } @@ -394,61 +413,57 @@ export class ChangesetHandler { * @constructor * @private */ - private OpenChangeset( - changesetTags: ChangesetTag[] - ): Promise { - const self = this; + private OpenChangeset(changesetTags: ChangesetTag[]): Promise { + const self = this return new Promise(function (resolve, reject) { - - const metadata = changesetTags.map(cstag => [cstag.key, cstag.value]) - .filter(kv => (kv[1] ?? "") !== "") - .map(kv => ``) + const metadata = changesetTags + .map((cstag) => [cstag.key, cstag.value]) + .filter((kv) => (kv[1] ?? "") !== "") + .map((kv) => ``) .join("\n") - - self.auth.xhr({ - method: 'PUT', - path: '/api/0.6/changeset/create', - options: {header: {'Content-Type': 'text/xml'}}, - content: [``, - metadata, - ``].join("") - }, function (err, response) { - if (response === undefined) { - console.error("Opening a changeset failed:", err); - reject(err) - } else { - resolve(Number(response)); + self.auth.xhr( + { + method: "PUT", + path: "/api/0.6/changeset/create", + options: { header: { "Content-Type": "text/xml" } }, + content: [``, metadata, ``].join(""), + }, + function (err, response) { + if (response === undefined) { + console.error("Opening a changeset failed:", err) + reject(err) + } else { + resolve(Number(response)) + } } - }); + ) }) - } /** * Upload a changesetXML */ - private UploadChange(changesetId: number, - changesetXML: string): Promise> { - const self = this; + private UploadChange(changesetId: number, changesetXML: string): Promise> { + const self = this return new Promise(function (resolve, reject) { - self.auth.xhr({ - method: 'POST', - options: {header: {'Content-Type': 'text/xml'}}, - path: '/api/0.6/changeset/' + changesetId + '/upload', - content: changesetXML - }, function (err, response) { - if (response == null) { - console.error("Uploading an actual change failed", err); - reject(err); + self.auth.xhr( + { + method: "POST", + options: { header: { "Content-Type": "text/xml" } }, + path: "/api/0.6/changeset/" + changesetId + "/upload", + content: changesetXML, + }, + function (err, response) { + if (response == null) { + console.error("Uploading an actual change failed", err) + reject(err) + } + const changes = self.parseUploadChangesetResponse(response) + console.log("Uploaded changeset ", changesetId) + resolve(changes) } - const changes = self.parseUploadChangesetResponse(response); - console.log("Uploaded changeset ", changesetId); - resolve(changes); - }); + ) }) - } - - } diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index bb6687faa..4e349d8ba 100644 --- a/Logic/Osm/Geocoding.ts +++ b/Logic/Osm/Geocoding.ts @@ -1,23 +1,27 @@ -import State from "../../State"; -import {Utils} from "../../Utils"; -import {BBox} from "../BBox"; +import State from "../../State" +import { Utils } from "../../Utils" +import { BBox } from "../BBox" export interface GeoCodeResult { - display_name: string, - lat: number, lon: number, boundingbox: number[], - osm_type: "node" | "way" | "relation", + display_name: string + lat: number + lon: number + boundingbox: number[] + osm_type: "node" | "way" | "relation" osm_id: string } export class Geocoding { - - private static readonly host = "https://nominatim.openstreetmap.org/search?"; + private static readonly host = "https://nominatim.openstreetmap.org/search?" static async Search(query: string): Promise { - const b = State?.state?.currentBounds?.data ?? BBox.global; - const url = Geocoding.host + "format=json&limit=1&viewbox=" + + const b = State?.state?.currentBounds?.data ?? BBox.global + const url = + Geocoding.host + + "format=json&limit=1&viewbox=" + `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + - "&accept-language=nl&q=" + query; - return Utils.downloadJson(url) + "&accept-language=nl&q=" + + query + return Utils.downloadJson(url) } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 6699e17d6..cfa247e68 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -1,153 +1,161 @@ -import osmAuth from "osm-auth"; -import {Store, Stores, UIEventSource} from "../UIEventSource"; -import {OsmPreferences} from "./OsmPreferences"; -import {ChangesetHandler} from "./ChangesetHandler"; -import {ElementStorage} from "../ElementStorage"; -import Svg from "../../Svg"; -import Img from "../../UI/Base/Img"; -import {Utils} from "../../Utils"; -import {OsmObject} from "./OsmObject"; -import {Changes} from "./Changes"; +import osmAuth from "osm-auth" +import { Store, Stores, UIEventSource } from "../UIEventSource" +import { OsmPreferences } from "./OsmPreferences" +import { ChangesetHandler } from "./ChangesetHandler" +import { ElementStorage } from "../ElementStorage" +import Svg from "../../Svg" +import Img from "../../UI/Base/Img" +import { Utils } from "../../Utils" +import { OsmObject } from "./OsmObject" +import { Changes } from "./Changes" 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 = 0; - home: { lon: number; lat: number }; - public backend: string; + public loggedIn = false + public name = "Not logged in" + public uid: number + public csCount = 0 + public img: string + public unreadMessages = 0 + public totalMessages = 0 + home: { lon: number; lat: number } + public backend: string constructor(backend: string) { - this.backend = backend; + this.backend = backend } } export class OsmConnection { - public static readonly oauth_configs = { - "osm": { - oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', - oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', - url: "https://www.openstreetmap.org" + osm: { + oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem", + oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI", + url: "https://www.openstreetmap.org", }, "osm-test": { - oauth_consumer_key: 'Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2', - oauth_secret: '3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn', - url: "https://master.apis.dev.openstreetmap.org" - } - - + oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2", + oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn", + url: "https://master.apis.dev.openstreetmap.org", + }, } - public auth; - public userDetails: UIEventSource; + public auth + public userDetails: UIEventSource public isLoggedIn: Store - public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">("not-attempted") - public preferencesHandler: OsmPreferences; + public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( + "not-attempted" + ) + public preferencesHandler: OsmPreferences public readonly _oauth_config: { - oauth_consumer_key: string, - oauth_secret: string, + oauth_consumer_key: string + oauth_secret: string url: string - }; - private readonly _dryRun: UIEventSource; - private fakeUser: boolean; - private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; - private readonly _iframeMode: Boolean | boolean; - private readonly _singlePage: boolean; - private isChecking = false; + } + private readonly _dryRun: UIEventSource + private fakeUser: boolean + private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] + private readonly _iframeMode: Boolean | boolean + private readonly _singlePage: boolean + private isChecking = false constructor(options: { - dryRun?: UIEventSource, - 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 - } - ) { - this.fakeUser = options.fakeUser ?? false; - this._singlePage = options.singlePage ?? true; - this._oauth_config = OsmConnection.oauth_configs[options.osmConfiguration ?? 'osm'] ?? OsmConnection.oauth_configs.osm; + dryRun?: UIEventSource + 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 + }) { + 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) OsmObject.SetBackendUrl(this._oauth_config.url + "/") - this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; + this._iframeMode = Utils.runningFromConsole ? false : window !== window.top - this.userDetails = new UIEventSource(new UserDetails(this._oauth_config.url), "userDetails"); + this.userDetails = new UIEventSource( + new UserDetails(this._oauth_config.url), + "userDetails" + ) if (options.fakeUser) { - const ud = this.userDetails.data; + const ud = this.userDetails.data ud.csCount = 5678 - ud.loggedIn = true; + ud.loggedIn = true ud.unreadMessages = 0 ud.name = "Fake user" - ud.totalMessages = 42; + ud.totalMessages = 42 } - const self = this; - this.isLoggedIn = this.userDetails.map(user => user.loggedIn); - this.isLoggedIn.addCallback(isLoggedIn => { + const self = this + this.isLoggedIn = this.userDetails.map((user) => user.loggedIn) + 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._dryRun = options.dryRun ?? new UIEventSource(false) - this.preferencesHandler = new OsmPreferences(this.auth, this); + 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, + 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); + self.AttemptLogin() + }, + this.auth + ) + options.oauth_token.setData(undefined) } - if (this.auth.authenticated() && (options.attemptLogin !== false)) { - this.AttemptLogin(); // Also updates the user badge + if (this.auth.authenticated() && options.attemptLogin !== false) { + this.AttemptLogin() // Also updates the user badge } else { - console.log("Not authenticated"); + console.log("Not authenticated") } } - - public CreateChangesetHandler(allElements: ElementStorage, changes: Changes){ - return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth); + + public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) { + return new ChangesetHandler(this._dryRun, this, allElements, changes, this.auth) } - public GetPreference(key: string, defaultValue: string = undefined, prefix: string = "mapcomplete-"): UIEventSource { - return this.preferencesHandler.GetPreference(key, defaultValue, prefix); + public GetPreference( + key: string, + defaultValue: string = undefined, + prefix: string = "mapcomplete-" + ): UIEventSource { + return this.preferencesHandler.GetPreference(key, defaultValue, prefix) } public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - return this.preferencesHandler.GetLongPreference(key, prefix); + return this.preferencesHandler.GetLongPreference(key, prefix) } public OnLoggedIn(action: (userDetails: UserDetails) => void) { - this._onLoggedIn.push(action); + this._onLoggedIn.push(action) } public LogOut() { - this.auth.logout(); - this.userDetails.data.loggedIn = false; - this.userDetails.data.csCount = 0; - this.userDetails.data.name = ""; - this.userDetails.ping(); + 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") } - + public Backend(): string { - return this._oauth_config.url; + return this._oauth_config.url } public AttemptLogin() { @@ -155,76 +163,81 @@ export class OsmConnection { if (this.fakeUser) { this.loadingStatus.setData("logged-in") console.log("AttemptLogin called, but ignored as fakeUser is set") - return; + return } - const self = this; - console.log("Trying to log in..."); - this.updateAuthObject(); - 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 - // Reset all the tokens - const tokens = [ - "https://www.openstreetmap.orgoauth_request_token_secret", - "https://www.openstreetmap.orgoauth_token", - "https://www.openstreetmap.orgoauth_token_secret"] - tokens.forEach(token => localStorage.removeItem(token)) + const self = this + console.log("Trying to log in...") + this.updateAuthObject() + 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 + // Reset all the tokens + const tokens = [ + "https://www.openstreetmap.orgoauth_request_token_secret", + "https://www.openstreetmap.orgoauth_token", + "https://www.openstreetmap.orgoauth_token_secret", + ] + tokens.forEach((token) => localStorage.removeItem(token)) + } + return } - 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 moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml"); + + let data = self.userDetails.data + data.loggedIn = true + console.log("Login completed, userinfo is ", userInfo) + data.name = userInfo.getAttribute("display_name") + data.uid = Number(userInfo.getAttribute("id")) + data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count") + + data.img = undefined + const imgEl = userInfo.getElementsByTagName("img") + if (imgEl !== undefined && imgEl[0] !== undefined) { + data.img = imgEl[0].getAttribute("href") + } + data.img = data.img ?? Img.AsData(Svg.osm_logo) + + 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 = [] } - - 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 moreDetails = new DOMParser().parseFromString(userInfo.innerHTML, "text/xml"); - - let data = self.userDetails.data; - data.loggedIn = true; - console.log("Login completed, userinfo is ", userInfo); - data.name = userInfo.getAttribute('display_name'); - data.uid = Number(userInfo.getAttribute("id")) - data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); - - data.img = undefined; - const imgEl = userInfo.getElementsByTagName("img"); - if (imgEl !== undefined && imgEl[0] !== undefined) { - data.img = imgEl[0].getAttribute("href"); - } - data.img = data.img ?? Img.AsData(Svg.osm_logo); - - 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 = []; - - }); + ) } public closeNote(id: number | string, text?: string): Promise { @@ -236,22 +249,23 @@ export class OsmConnection { console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) return new Promise((ok) => { ok() - }); + }) } return new Promise((ok, error) => { - this.auth.xhr({ - method: 'POST', - path: `/api/0.6/notes/${id}/close${textSuffix}`, - }, function (err, _) { - if (err !== null) { - error(err) - } else { - ok() + this.auth.xhr( + { + method: "POST", + path: `/api/0.6/notes/${id}/close${textSuffix}`, + }, + function (err, _) { + if (err !== null) { + error(err) + } else { + ok() + } } - }) - + ) }) - } public reopenNote(id: number | string, text?: string): Promise { @@ -259,110 +273,118 @@ export class OsmConnection { 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 new Promise((ok, error) => { - this.auth.xhr({ - method: 'POST', - path: `/api/0.6/notes/${id}/reopen${textSuffix}` - }, function (err, _) { - if (err !== null) { - error(err) - } else { - ok() + this.auth.xhr( + { + method: "POST", + path: `/api/0.6/notes/${id}/reopen${textSuffix}`, + }, + function (err, _) { + if (err !== null) { + error(err) + } else { + ok() + } } - }) - + ) }) - } public 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) - }); - } - const auth = this.auth; - const content = {lat, lon, text} - return new Promise((ok, error) => { - auth.xhr({ - method: 'POST', - path: `/api/0.6/notes.json`, - options: { - header: - {'Content-Type': 'application/json'} - }, - content: JSON.stringify(content) - - }, function ( - err, - response: string) { - console.log("RESPONSE IS", response) - if (err !== null) { - error(err) - } else { - const parsed = JSON.parse(response) - const id = parsed.properties.id - console.log("OPENED NOTE", id) - ok({id}) - } + window.setTimeout( + () => ok({ id: Math.floor(Math.random() * 1000) }), + Math.random() * 5000 + ) }) - + } + const auth = this.auth + const content = { lat, lon, text } + return new Promise((ok, error) => { + auth.xhr( + { + method: "POST", + path: `/api/0.6/notes.json`, + options: { + header: { "Content-Type": "application/json" }, + }, + content: JSON.stringify(content), + }, + function (err, response: string) { + console.log("RESPONSE IS", response) + if (err !== null) { + error(err) + } else { + const parsed = JSON.parse(response) + const id = parsed.properties.id + console.log("OPENED NOTE", id) + ok({ id }) + } + } + ) }) - } - + 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', + this.auth.xhr( + { + method: "POST", - path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` - }, function (err, _) { - if (err !== null) { - error(err) - } else { - ok() + path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, + }, + function (err, _) { + if (err !== null) { + error(err) + } else { + ok() + } } - }) - + ) }) - } private updateAuthObject() { - let pwaStandAloneMode = false; + 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; + } 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") + console.warn( + "Detecting standalone mode failed", + e, + ". Assuming in browser and not worrying furhter" + ) } - const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage; + const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... // Same for an iframe... - this.auth = new osmAuth({ oauth_consumer_key: this._oauth_config.oauth_consumer_key, oauth_secret: this._oauth_config.oauth_secret, @@ -370,22 +392,20 @@ export class OsmConnection { landing: standalone ? undefined : window.location.href, singlepage: !standalone, auto: true, - - }); + }) } private CheckForMessagesContinuously() { - const self = this; + const self = this if (this.isChecking) { - return; + return } - this.isChecking = true; - Stores.Chronic(5 * 60 * 1000).addCallback(_ => { + this.isChecking = true + Stores.Chronic(5 * 60 * 1000).addCallback((_) => { if (self.isLoggedIn.data) { console.log("Checking for messages") - self.AttemptLogin(); + self.AttemptLogin() } - }); - + }) } -} \ No newline at end of file +} diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 25f0e3f09..a74bb3f3e 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -1,33 +1,32 @@ -import {Utils} from "../../Utils"; -import * as polygon_features from "../../assets/polygon-features.json"; -import {Store, UIEventSource} from "../UIEventSource"; -import {BBox} from "../BBox"; -import * as OsmToGeoJson from "osmtogeojson"; -import {NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId} from "../../Models/OsmFeature"; +import { Utils } from "../../Utils" +import * as polygon_features from "../../assets/polygon-features.json" +import { Store, UIEventSource } from "../UIEventSource" +import { BBox } from "../BBox" +import * as OsmToGeoJson from "osmtogeojson" +import { NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId } from "../../Models/OsmFeature" export abstract class OsmObject { - private static defaultBackend = "https://www.openstreetmap.org/" - protected static backendURL = OsmObject.defaultBackend; + protected static backendURL = OsmObject.defaultBackend private static polygonFeatures = OsmObject.constructPolygonFeatures() - private static objectCache = new Map>(); - private static historyCache = new Map>(); - type: "node" | "way" | "relation"; - id: number; + private static objectCache = new Map>() + private static historyCache = new Map>() + type: "node" | "way" | "relation" + id: number /** * The OSM tags as simple object */ - tags: OsmTags ; - version: number; - public changed: boolean = false; - timestamp: Date; + tags: OsmTags + version: number + public changed: boolean = false + timestamp: Date protected constructor(type: string, id: number) { - this.id = id; + this.id = id // @ts-ignore - this.type = type; + this.type = type this.tags = { - id: `${this.type}/${id}` + id: `${this.type}/${id}`, } } @@ -38,63 +37,63 @@ export abstract class OsmObject { if (!url.startsWith("http")) { throw "Backend URL must begin with http" } - this.backendURL = url; + this.backendURL = url } public static DownloadObject(id: string, forceRefresh: boolean = false): Store { - let src: UIEventSource; + let src: UIEventSource if (OsmObject.objectCache.has(id)) { src = OsmObject.objectCache.get(id) if (forceRefresh) { src.setData(undefined) } else { - return src; + return src } } else { src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id)) } - OsmObject.objectCache.set(id, src); - return src; + OsmObject.objectCache.set(id, src) + return src } static async DownloadPropertiesOf(id: string): Promise { - const splitted = id.split("/"); - const idN = Number(splitted[1]); + const splitted = id.split("/") + const idN = Number(splitted[1]) if (idN < 0) { - return undefined; + return undefined } - const url = `${OsmObject.backendURL}api/0.6/${id}`; + const url = `${OsmObject.backendURL}api/0.6/${id}` const rawData = await Utils.downloadJsonCached(url, 1000) return rawData.elements[0].tags } - static async DownloadObjectAsync(id: NodeId): Promise; - static async DownloadObjectAsync(id: WayId): Promise; - static async DownloadObjectAsync(id: RelationId): Promise; - static async DownloadObjectAsync(id: OsmId): Promise; - static async DownloadObjectAsync(id: string): Promise; - static async DownloadObjectAsync(id: string): Promise{ - const splitted = id.split("/"); - const type = splitted[0]; - const idN = Number(splitted[1]); + static async DownloadObjectAsync(id: NodeId): Promise + static async DownloadObjectAsync(id: WayId): Promise + static async DownloadObjectAsync(id: RelationId): Promise + static async DownloadObjectAsync(id: OsmId): Promise + static async DownloadObjectAsync(id: string): Promise + static async DownloadObjectAsync(id: string): Promise { + const splitted = id.split("/") + const type = splitted[0] + const idN = Number(splitted[1]) if (idN < 0) { - return undefined; + return undefined } - const full = (!id.startsWith("node")) ? "/full" : ""; - const url = `${OsmObject.backendURL}api/0.6/${id}${full}`; + const full = !id.startsWith("node") ? "/full" : "" + const url = `${OsmObject.backendURL}api/0.6/${id}${full}` const rawData = await Utils.downloadJsonCached(url, 10000) if (rawData === undefined) { return undefined } // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way) - const parsed = OsmObject.ParseObjects(rawData.elements); + const parsed = OsmObject.ParseObjects(rawData.elements) // Lets fetch the object we need for (const osmObject of parsed) { if (osmObject.type !== type) { - continue; + continue } if (osmObject.id !== idN) { continue @@ -103,25 +102,23 @@ export abstract class OsmObject { return osmObject } throw "PANIC: requested object is not part of the response" - - } - /** * Downloads the ways that are using this node. * Beware: their geometry will be incomplete! */ public static DownloadReferencingWays(id: string): Promise { - return Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/ways`, 60 * 1000).then( - data => { - return data.elements.map(wayInfo => { - const way = new OsmWay(wayInfo.id) - way.LoadData(wayInfo) - return way - }) - } - ) + return Utils.downloadJsonCached( + `${OsmObject.backendURL}api/0.6/${id}/ways`, + 60 * 1000 + ).then((data) => { + return data.elements.map((wayInfo) => { + const way = new OsmWay(wayInfo.id) + way.LoadData(wayInfo) + return way + }) + }) } /** @@ -129,8 +126,11 @@ export abstract class OsmObject { * Beware: their geometry will be incomplete! */ public static async DownloadReferencingRelations(id: string): Promise { - const data = await Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${id}/relations`, 60 * 1000) - return data.elements.map(wayInfo => { + const data = await Utils.downloadJsonCached( + `${OsmObject.backendURL}api/0.6/${id}/relations`, + 60 * 1000 + ) + return data.elements.map((wayInfo) => { const rel = new OsmRelation(wayInfo.id) rel.LoadData(wayInfo) rel.SaveExtraData(wayInfo, undefined) @@ -138,78 +138,85 @@ export abstract class OsmObject { }) } - public static DownloadHistory(id: string): UIEventSource { + public static DownloadHistory(id: string): UIEventSource { if (OsmObject.historyCache.has(id)) { return OsmObject.historyCache.get(id) } - const splitted = id.split("/"); - const type = splitted[0]; - const idN = Number(splitted[1]); - const src = new UIEventSource([]); - OsmObject.historyCache.set(id, src); - Utils.downloadJsonCached(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, 10 * 60 * 1000).then(data => { - const elements: any[] = data.elements; + const splitted = id.split("/") + const type = splitted[0] + const idN = Number(splitted[1]) + const src = new UIEventSource([]) + OsmObject.historyCache.set(id, src) + Utils.downloadJsonCached( + `${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, + 10 * 60 * 1000 + ).then((data) => { + const elements: any[] = data.elements const osmObjects: OsmObject[] = [] for (const element of elements) { let osmObject: OsmObject = null switch (type) { - case("node"): - osmObject = new OsmNode(idN); - break; - case("way"): - osmObject = new OsmWay(idN); - break; - case("relation"): - osmObject = new OsmRelation(idN); - break; + case "node": + osmObject = new OsmNode(idN) + break + case "way": + osmObject = new OsmWay(idN) + break + case "relation": + osmObject = new OsmRelation(idN) + break } - osmObject?.LoadData(element); - osmObject?.SaveExtraData(element, []); + osmObject?.LoadData(element) + osmObject?.SaveExtraData(element, []) osmObjects.push(osmObject) } src.setData(osmObjects) }) - return src; + return src } // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds) public static async LoadArea(bbox: BBox): Promise { const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` const data = await Utils.downloadJson(url) - const elements: any[] = data.elements; - return OsmObject.ParseObjects(elements); + const elements: any[] = data.elements + return OsmObject.ParseObjects(elements) } public static ParseObjects(elements: any[]): OsmObject[] { - const objects: OsmObject[] = []; + const objects: OsmObject[] = [] const allNodes: Map = new Map() for (const element of elements) { - const type = element.type; - const idN = element.id; + const type = element.type + const idN = element.id let osmObject: OsmObject = null switch (type) { - case("node"): - const node = new OsmNode(idN); - allNodes.set(idN, node); + case "node": + const node = new OsmNode(idN) + allNodes.set(idN, node) osmObject = node - node.SaveExtraData(element); - break; - case("way"): - osmObject = new OsmWay(idN); - const nodes = element.nodes.map(i => allNodes.get(i)); + node.SaveExtraData(element) + break + case "way": + osmObject = new OsmWay(idN) + const nodes = element.nodes.map((i) => allNodes.get(i)) osmObject.SaveExtraData(element, nodes) - break; - case("relation"): - osmObject = new OsmRelation(idN); - const allGeojsons = OsmToGeoJson.default({elements}, + break + case "relation": + osmObject = new OsmRelation(idN) + const allGeojsons = OsmToGeoJson.default( + { elements }, // @ts-ignore { - flatProperties: true - }); - const feature = allGeojsons.features.find(f => f.id === osmObject.type + "/" + osmObject.id) + flatProperties: true, + } + ) + const feature = allGeojsons.features.find( + (f) => f.id === osmObject.type + "/" + osmObject.id + ) osmObject.SaveExtraData(element, feature) - break; + break } if (osmObject !== undefined && OsmObject.backendURL !== OsmObject.defaultBackend) { @@ -219,12 +226,12 @@ export abstract class OsmObject { osmObject?.LoadData(element) objects.push(osmObject) } - return objects; + return objects } /** * Uses the list of polygon features to determine if the given tags are a polygon or not. - * + * * OsmObject.isPolygon({"building":"yes"}) // => true * OsmObject.isPolygon({"highway":"residential"}) // => false * */ @@ -233,11 +240,12 @@ export abstract class OsmObject { if (!tags.hasOwnProperty(tagsKey)) { continue } - const polyGuide: { values: Set; blacklist: boolean } = OsmObject.polygonFeatures.get(tagsKey) + const polyGuide: { values: Set; blacklist: boolean } = + OsmObject.polygonFeatures.get(tagsKey) if (polyGuide === undefined) { continue } - if ((polyGuide.values === null)) { + if (polyGuide.values === null) { // .values is null, thus merely _having_ this key is enough to be a polygon (or if blacklist, being a line) return !polyGuide.blacklist } @@ -249,156 +257,178 @@ export abstract class OsmObject { return doesMatch } - return false; + return false } - private static constructPolygonFeatures(): Map, blacklist: boolean }> { - const result = new Map, blacklist: boolean }>(); - for (const polygonFeature of (polygon_features["default"] ?? polygon_features)) { - const key = polygonFeature.key; + private static constructPolygonFeatures(): Map< + string, + { values: Set; blacklist: boolean } + > { + const result = new Map; blacklist: boolean }>() + for (const polygonFeature of polygon_features["default"] ?? polygon_features) { + const key = polygonFeature.key if (polygonFeature.polygon === "all") { - result.set(key, {values: null, blacklist: false}) + result.set(key, { values: null, blacklist: false }) continue } const blacklist = polygonFeature.polygon === "blacklist" - result.set(key, {values: new Set(polygonFeature.values), blacklist: blacklist}) - + result.set(key, { + values: new Set(polygonFeature.values), + blacklist: blacklist, + }) } - return result; + return result } // The centerpoint of the feature, as [lat, lon] - public abstract centerpoint(): [number, number]; + public abstract centerpoint(): [number, number] - public abstract asGeoJson(): any; + public abstract asGeoJson(): any - abstract SaveExtraData(element: any, allElements: OsmObject[] | any); + abstract SaveExtraData(element: any, allElements: OsmObject[] | any) /** * Generates the changeset-XML for tags * @constructor */ TagsXML(): string { - let tags = ""; + let tags = "" for (const key in this.tags) { if (key.startsWith("_")) { - continue; + continue } if (key === "id") { - continue; + continue } - const v = this.tags[key]; + const v = this.tags[key] if (v !== "" && v !== undefined) { - tags += ' \n' + tags += + ' \n' } } - return tags; + return tags } - abstract ChangesetXML(changesetId: string): string; + abstract ChangesetXML(changesetId: string): string protected VersionXML() { if (this.version === undefined) { - return ""; + return "" } - return 'version="' + this.version + '"'; + return 'version="' + this.version + '"' } private LoadData(element: any): void { - this.tags = element.tags ?? this.tags; - this.version = element.version; - this.timestamp = element.timestamp; - const tgs = this.tags; + this.tags = element.tags ?? this.tags + this.version = element.version + this.timestamp = element.timestamp + const tgs = this.tags if (element.tags === undefined) { // Simple node which is part of a way - not important - return; + return } tgs["_last_edit:contributor"] = element.user tgs["_last_edit:contributor:uid"] = element.uid tgs["_last_edit:changeset"] = element.changeset tgs["_last_edit:timestamp"] = element.timestamp tgs["_version_number"] = element.version - tgs["id"] = ( this.type + "/" + this.id); + tgs["id"] = (this.type + "/" + this.id) } } - export class OsmNode extends OsmObject { - - lat: number; - lon: number; + lat: number + lon: number constructor(id: number) { - super("node", id); - + super("node", id) } ChangesetXML(changesetId: string): string { - let tags = this.TagsXML(); + let tags = this.TagsXML() - return ' \n' + + return ( + ' \n' + tags + - ' \n'; + " \n" + ) } SaveExtraData(element) { - this.lat = element.lat; - this.lon = element.lon; + this.lat = element.lat + this.lon = element.lon } centerpoint(): [number, number] { - return [this.lat, this.lon]; + return [this.lat, this.lon] } - asGeoJson() : OsmFeature{ + asGeoJson(): OsmFeature { return { - "type": "Feature", - "properties": this.tags, - "geometry": { - "type": "Point", - "coordinates": [ - this.lon, - this.lat - ] - } + type: "Feature", + properties: this.tags, + geometry: { + type: "Point", + coordinates: [this.lon, this.lat], + }, } } } export class OsmWay extends OsmObject { - - nodes: number[] = []; + nodes: number[] = [] // The coordinates of the way, [lat, lon][] coordinates: [number, number][] = [] - lat: number; - lon: number; + lat: number + lon: number constructor(id: number) { - super("way", id); + super("way", id) } centerpoint(): [number, number] { - return [this.lat, this.lon]; + return [this.lat, this.lon] } ChangesetXML(changesetId: string): string { - let tags = this.TagsXML(); - let nds = ""; + let tags = this.TagsXML() + let nds = "" for (const node in this.nodes) { - nds += ' \n'; + nds += ' \n' } - return ' \n' + + return ( + ' \n" + nds + tags + - ' \n'; + " \n" + ) } SaveExtraData(element, allNodes: OsmNode[]) { - let latSum = 0 let lonSum = 0 @@ -416,88 +446,96 @@ export class OsmWay extends OsmObject { if (node === undefined) { console.error("Error: node ", nodeId, "not found in ", nodeDict) // This is probably part of a relation which hasn't been fully downloaded - continue; + continue } - this.coordinates.push(node.centerpoint()); + this.coordinates.push(node.centerpoint()) latSum += node.lat lonSum += node.lon } - let count = this.coordinates.length; - this.lat = latSum / count; - this.lon = lonSum / count; - this.nodes = element.nodes; + let count = this.coordinates.length + this.lat = latSum / count + this.lon = lonSum / count + this.nodes = element.nodes } public asGeoJson() { - let coordinates: ([number, number][] | [number, number][][]) = this.coordinates.map(([lat, lon]) => [lon, lat]); + let coordinates: [number, number][] | [number, number][][] = this.coordinates.map( + ([lat, lon]) => [lon, lat] + ) if (this.isPolygon()) { coordinates = [coordinates] } return { - "type": "Feature", - "properties": this.tags, - "geometry": { - "type": this.isPolygon() ? "Polygon" : "LineString", - "coordinates": coordinates - } + type: "Feature", + properties: this.tags, + geometry: { + type: this.isPolygon() ? "Polygon" : "LineString", + coordinates: coordinates, + }, } } private isPolygon(): boolean { // Compare lat and lon seperately, as the coordinate array might not be a reference to the same object - if (this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] || - this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1]) { - return false; // Not closed + if ( + this.coordinates[0][0] !== this.coordinates[this.coordinates.length - 1][0] || + this.coordinates[0][1] !== this.coordinates[this.coordinates.length - 1][1] + ) { + return false // Not closed } return OsmObject.isPolygon(this.tags) - } } export class OsmRelation extends OsmObject { - public members: { - type: "node" | "way" | "relation", - ref: number, + type: "node" | "way" | "relation" + ref: number role: string - }[]; + }[] private geojson = undefined constructor(id: number) { - super("relation", id); + super("relation", id) } centerpoint(): [number, number] { - return [0, 0]; // TODO + return [0, 0] // TODO } ChangesetXML(changesetId: string): string { - let members = ""; + let members = "" for (const member of this.members) { - members += ' \n'; + members += + ' \n' } - let tags = this.TagsXML(); + let tags = this.TagsXML() let cs = "" if (changesetId !== undefined) { cs = `changeset="${changesetId}"` } return ` ${members}${tags} -`; - +` } SaveExtraData(element, geojson) { - this.members = element.members; + this.members = element.members this.geojson = geojson } asGeoJson(): any { if (this.geojson !== undefined) { - return this.geojson; + return this.geojson } throw "Not Implemented" } -} \ No newline at end of file +} diff --git a/Logic/Osm/OsmPreferences.ts b/Logic/Osm/OsmPreferences.ts index 550066062..8ce6d6997 100644 --- a/Logic/Osm/OsmPreferences.ts +++ b/Logic/Osm/OsmPreferences.ts @@ -1,22 +1,21 @@ -import {UIEventSource} from "../UIEventSource"; -import UserDetails, {OsmConnection} from "./OsmConnection"; -import {Utils} from "../../Utils"; -import {DomEvent} from "leaflet"; -import preventDefault = DomEvent.preventDefault; +import { UIEventSource } from "../UIEventSource" +import UserDetails, { OsmConnection } from "./OsmConnection" +import { Utils } from "../../Utils" +import { DomEvent } from "leaflet" +import preventDefault = DomEvent.preventDefault export class OsmPreferences { - - public preferences = new UIEventSource>({}, "all-osm-preferences"); + public preferences = new UIEventSource>({}, "all-osm-preferences") private readonly preferenceSources = new Map>() - private auth: any; - private userDetails: UIEventSource; - private longPreferences = {}; + private auth: any + private userDetails: UIEventSource + private longPreferences = {} constructor(auth, osmConnection: OsmConnection) { - this.auth = auth; - this.userDetails = osmConnection.userDetails; - const self = this; - osmConnection.OnLoggedIn(() => self.UpdatePreferences()); + this.auth = auth + this.userDetails = osmConnection.userDetails + const self = this + osmConnection.OnLoggedIn(() => self.UpdatePreferences()) } /** @@ -26,42 +25,44 @@ export class OsmPreferences { * @constructor */ public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - if (this.longPreferences[prefix + key] !== undefined) { - return this.longPreferences[prefix + key]; + return this.longPreferences[prefix + key] } + const source = new UIEventSource(undefined, "long-osm-preference:" + prefix + key) + this.longPreferences[prefix + key] = source - const source = new UIEventSource(undefined, "long-osm-preference:" + prefix + key); - this.longPreferences[prefix + key] = source; - - const allStartWith = prefix + key + "-combined"; + const allStartWith = prefix + key + "-combined" // Gives the number of combined preferences - const length = this.GetPreference(allStartWith + "-length", "", ""); + const length = this.GetPreference(allStartWith + "-length", "", "") - if( (allStartWith + "-length").length > 255){ - throw "This preference key is too long, it has "+key.length+" characters, but at most "+(255 - "-length".length - "-combined".length - prefix.length)+" characters are allowed" - } + if ((allStartWith + "-length").length > 255) { + throw ( + "This preference key is too long, it has " + + key.length + + " characters, but at most " + + (255 - "-length".length - "-combined".length - prefix.length) + + " characters are allowed" + ) + } - const self = this; - source.addCallback(str => { + const self = this + source.addCallback((str) => { if (str === undefined || str === "") { - return; + return } if (str === null) { - console.error("Deleting " + allStartWith); - let count = parseInt(length.data); + console.error("Deleting " + allStartWith) + let count = parseInt(length.data) for (let i = 0; i < count; i++) { // Delete all the preferences - self.GetPreference(allStartWith + "-" + i, "", "") - .setData(""); + self.GetPreference(allStartWith + "-" + i, "", "").setData("") } - self.GetPreference(allStartWith + "-length", "", "") - .setData(""); + self.GetPreference(allStartWith + "-length", "", "").setData("") return } - let i = 0; + let i = 0 while (str !== "") { if (str === undefined || str === "undefined") { throw "Long pref became undefined?" @@ -69,79 +70,91 @@ export class OsmPreferences { if (i > 100) { throw "This long preference is getting very long... " } - self.GetPreference(allStartWith + "-" + i, "","").setData(str.substr(0, 255)); - str = str.substr(255); - i++; + self.GetPreference(allStartWith + "-" + i, "", "").setData(str.substr(0, 255)) + str = str.substr(255) + i++ } - length.setData("" + i); // We use I, the number of preference fields used - }); - - - function updateData(l: number) { - if(Object.keys(self.preferences.data).length === 0){ - // The preferences are still empty - they are not yet updated, so we delay updating for now - return - } - const prefsCount = Number(l); - if (prefsCount > 100) { - throw "Length to long"; - } - let str = ""; - for (let i = 0; i < prefsCount; i++) { - const key = allStartWith + "-" + i - if(self.preferences.data[key] === undefined){ - console.warn("Detected a broken combined preference:", key, "is undefined", self.preferences) - } - str += self.preferences.data[key] ?? ""; - } - - source.setData(str); - } - - length.addCallback(l => { - updateData(Number(l)); - }); - this.preferences.addCallbackAndRun(_ => { - updateData(Number(length.data)); + length.setData("" + i) // We use I, the number of preference fields used }) - return source; + function updateData(l: number) { + if (Object.keys(self.preferences.data).length === 0) { + // The preferences are still empty - they are not yet updated, so we delay updating for now + return + } + const prefsCount = Number(l) + if (prefsCount > 100) { + throw "Length to long" + } + let str = "" + for (let i = 0; i < prefsCount; i++) { + const key = allStartWith + "-" + i + if (self.preferences.data[key] === undefined) { + console.warn( + "Detected a broken combined preference:", + key, + "is undefined", + self.preferences + ) + } + str += self.preferences.data[key] ?? "" + } + + source.setData(str) + } + + length.addCallback((l) => { + updateData(Number(l)) + }) + this.preferences.addCallbackAndRun((_) => { + updateData(Number(length.data)) + }) + + return source } - public GetPreference(key: string, defaultValue : string = undefined, prefix: string = "mapcomplete-"): UIEventSource { - if(key.startsWith(prefix) && prefix !== ""){ - console.trace("A preference was requested which has a duplicate prefix in its key. This is probably a bug") + public GetPreference( + key: string, + defaultValue: string = undefined, + prefix: string = "mapcomplete-" + ): UIEventSource { + if (key.startsWith(prefix) && prefix !== "") { + console.trace( + "A preference was requested which has a duplicate prefix in its key. This is probably a bug" + ) } - key = prefix + key; - key = key.replace(/[:\\\/"' {}.%]/g, '') + key = prefix + key + key = key.replace(/[:\\\/"' {}.%]/g, "") if (key.length >= 255) { - throw "Preferences: key length to big"; + throw "Preferences: key length to big" } const cached = this.preferenceSources.get(key) if (cached !== undefined) { - return cached; + return cached } if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) { - this.UpdatePreferences(); + this.UpdatePreferences() } - - const pref = new UIEventSource(this.preferences.data[key] ?? defaultValue, "osm-preference:" + key); - pref.addCallback((v) => { - this.UploadPreference(key, v); - }); - + const pref = new UIEventSource( + this.preferences.data[key] ?? defaultValue, + "osm-preference:" + key + ) + pref.addCallback((v) => { + this.UploadPreference(key, v) + }) + this.preferenceSources.set(key, pref) - return pref; + return pref } public ClearPreferences() { - let isRunning = false; - const self = this; - this.preferences.addCallback(prefs => { + let isRunning = false + const self = this + this.preferences.addCallback((prefs) => { console.log("Cleaning preferences...") if (Object.keys(prefs).length == 0) { - return; + return } if (isRunning) { return @@ -149,94 +162,98 @@ export class OsmPreferences { isRunning = true const prefixes = ["mapcomplete-"] for (const key in prefs) { - const matches = prefixes.some(prefix => key.startsWith(prefix)) + const matches = prefixes.some((prefix) => key.startsWith(prefix)) if (matches) { console.log("Clearing ", key) self.GetPreference(key, "", "").setData("") - } } - isRunning = false; - return; + isRunning = false + return }) } private UpdatePreferences() { - const self = this; - this.auth.xhr({ - method: 'GET', - path: '/api/0.6/user/preferences' - }, function (error, value: XMLDocument) { - if (error) { - console.log("Could not load preferences", error); - return; - } - const prefs = value.getElementsByTagName("preference"); - for (let i = 0; i < prefs.length; i++) { - const pref = prefs[i]; - const k = pref.getAttribute("k"); - const v = pref.getAttribute("v"); - self.preferences.data[k] = v; - } - - // We merge all the preferences: new keys are uploaded - // For differing values, the server overrides local changes - self.preferenceSources.forEach((preference, key) => { - const osmValue = self.preferences.data[key] - if(osmValue === undefined && preference.data !== undefined){ - // OSM doesn't know this value yet - self.UploadPreference(key, preference.data) - } else { - // OSM does have a value - set it - preference.setData(osmValue) + const self = this + this.auth.xhr( + { + method: "GET", + path: "/api/0.6/user/preferences", + }, + function (error, value: XMLDocument) { + if (error) { + console.log("Could not load preferences", error) + return } - }) - - self.preferences.ping(); - }); + const prefs = value.getElementsByTagName("preference") + for (let i = 0; i < prefs.length; i++) { + const pref = prefs[i] + const k = pref.getAttribute("k") + const v = pref.getAttribute("v") + self.preferences.data[k] = v + } + + // We merge all the preferences: new keys are uploaded + // For differing values, the server overrides local changes + self.preferenceSources.forEach((preference, key) => { + const osmValue = self.preferences.data[key] + if (osmValue === undefined && preference.data !== undefined) { + // OSM doesn't know this value yet + self.UploadPreference(key, preference.data) + } else { + // OSM does have a value - set it + preference.setData(osmValue) + } + }) + + self.preferences.ping() + } + ) } private UploadPreference(k: string, v: string) { if (!this.userDetails.data.loggedIn) { - console.debug(`Not saving preference ${k}: user not logged in`); - return; + console.debug(`Not saving preference ${k}: user not logged in`) + return } if (this.preferences.data[k] === v) { - return; + return } - console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)); + console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15)) if (v === undefined || v === "") { - this.auth.xhr({ - method: 'DELETE', - path: '/api/0.6/user/preferences/' + encodeURIComponent(k), - options: {header: {'Content-Type': 'text/plain'}}, - }, function (error) { - if (error) { - console.warn("Could not remove preference", error); - return; + this.auth.xhr( + { + method: "DELETE", + path: "/api/0.6/user/preferences/" + encodeURIComponent(k), + options: { header: { "Content-Type": "text/plain" } }, + }, + function (error) { + if (error) { + console.warn("Could not remove preference", error) + return + } + console.debug("Preference ", k, "removed!") } - console.debug("Preference ", k, "removed!"); - - }); - return; + ) + return } - - this.auth.xhr({ - method: 'PUT', - path: '/api/0.6/user/preferences/' + encodeURIComponent(k), - options: {header: {'Content-Type': 'text/plain'}}, - content: v - }, function (error) { - if (error) { - console.warn(`Could not set preference "${k}"'`, error); - return; + this.auth.xhr( + { + method: "PUT", + path: "/api/0.6/user/preferences/" + encodeURIComponent(k), + options: { header: { "Content-Type": "text/plain" } }, + content: v, + }, + function (error) { + if (error) { + console.warn(`Could not set preference "${k}"'`, error) + return + } + console.debug(`Preference ${k} written!`) } - console.debug(`Preference ${k} written!`); - }); + ) } - - -} \ No newline at end of file +} diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index d4a5621a0..7bb1dbf8f 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,56 +1,67 @@ -import {TagsFilter} from "../Tags/TagsFilter"; -import RelationsTracker from "./RelationsTracker"; -import {Utils} from "../../Utils"; -import {ImmutableStore, Store} from "../UIEventSource"; -import {BBox} from "../BBox"; -import * as osmtogeojson from "osmtogeojson"; -import {FeatureCollection} from "@turf/turf"; +import { TagsFilter } from "../Tags/TagsFilter" +import RelationsTracker from "./RelationsTracker" +import { Utils } from "../../Utils" +import { ImmutableStore, Store } from "../UIEventSource" +import { BBox } from "../BBox" +import * as osmtogeojson from "osmtogeojson" +import { FeatureCollection } from "@turf/turf" /** * Interfaces overpass to get all the latest data */ export class Overpass { private _filter: TagsFilter - private readonly _interpreterUrl: string; - private readonly _timeout: Store; - private readonly _extraScripts: string[]; - private _includeMeta: boolean; - private _relationTracker: RelationsTracker; + private readonly _interpreterUrl: string + private readonly _timeout: Store + private readonly _extraScripts: string[] + private _includeMeta: boolean + private _relationTracker: RelationsTracker - constructor(filter: TagsFilter, - extraScripts: string[], - interpreterUrl: string, - timeout?: Store, - relationTracker?: RelationsTracker, - includeMeta = true) { - this._timeout = timeout ?? new ImmutableStore(90); - this._interpreterUrl = interpreterUrl; + constructor( + filter: TagsFilter, + extraScripts: string[], + interpreterUrl: string, + timeout?: Store, + relationTracker?: RelationsTracker, + includeMeta = true + ) { + this._timeout = timeout ?? new ImmutableStore(90) + this._interpreterUrl = interpreterUrl const optimized = filter.optimize() - if(optimized === true || optimized === false){ + if (optimized === true || optimized === false) { throw "Invalid filter: optimizes to true of false" } this._filter = optimized - this._extraScripts = extraScripts; - this._includeMeta = includeMeta; + this._extraScripts = extraScripts + this._includeMeta = includeMeta this._relationTracker = relationTracker } public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { - const bbox = "[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]"; + const bbox = + "[bbox:" + + bounds.getSouth() + + "," + + bounds.getWest() + + "," + + bounds.getNorth() + + "," + + bounds.getEast() + + "]" const query = this.buildScript(bbox) - return this.ExecuteQuery(query); + return this.ExecuteQuery(query) } - - public buildUrl(query: string){ + + public buildUrl(query: string) { return `${this._interpreterUrl}?data=${encodeURIComponent(query)}` } - - public async ExecuteQuery(query: string):Promise<[FeatureCollection, Date]> { - const self = this; + + public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> { + const self = this const json = await Utils.downloadJson(this.buildUrl(query)) if (json.elements.length === 0 && json.remark !== undefined) { - console.warn("Timeout or other runtime error while querying overpass", json.remark); + console.warn("Timeout or other runtime error while querying overpass", json.remark) throw `Runtime error (timeout or similar)${json.remark}` } if (json.elements.length === 0) { @@ -58,77 +69,81 @@ export class Overpass { } self._relationTracker?.RegisterRelations(json) - const geojson = osmtogeojson.default(json); - const osmTime = new Date(json.osm3s.timestamp_osm_base); - return [ geojson, osmTime]; + const geojson = osmtogeojson.default(json) + const osmTime = new Date(json.osm3s.timestamp_osm_base) + return [geojson, osmTime] } /** * Constructs the actual script to execute on Overpass * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' - * + * * import {Tag} from "../Tags/Tag"; - * + * * new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;` */ public buildScript(bbox: string, postCall: string = "", pretty = false): string { const filters = this._filter.asOverpass() let filter = "" for (const filterOr of filters) { - if(pretty){ + if (pretty) { filter += " " } - filter += 'nwr' + filterOr + postCall + ';' - if(pretty){ - filter+="\n" + filter += "nwr" + filterOr + postCall + ";" + if (pretty) { + filter += "\n" } } for (const extraScript of this._extraScripts) { - filter += '(' + extraScript + ');'; + filter += "(" + extraScript + ");" } - return`[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` + return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${ + this._includeMeta ? "out meta;" : "" + }>;out skel qt;` } /** * Constructs the actual script to execute on Overpass with geocoding * 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink' * */ - public buildScriptInArea(area: {osm_type: "way" | "relation", osm_id: number}, pretty = false): string { + public buildScriptInArea( + area: { osm_type: "way" | "relation"; osm_id: number }, + pretty = false + ): string { const filters = this._filter.asOverpass() let filter = "" for (const filterOr of filters) { - if(pretty){ + if (pretty) { filter += " " } - filter += 'nwr' + filterOr + '(area.searchArea);' - if(pretty){ - filter+="\n" + filter += "nwr" + filterOr + "(area.searchArea);" + if (pretty) { + filter += "\n" } } for (const extraScript of this._extraScripts) { - filter += '(' + extraScript + ');'; + filter += "(" + extraScript + ");" } - let id = area.osm_id; - if(area.osm_type === "relation"){ + let id = area.osm_id + if (area.osm_type === "relation") { id += 3600000000 } - return`[out:json][timeout:${this._timeout.data}]; + return `[out:json][timeout:${this._timeout.data}]; area(id:${id})->.searchArea; (${filter}); - out body;${this._includeMeta ? 'out meta;' : ''}>;out skel qt;` + out body;${this._includeMeta ? "out meta;" : ""}>;out skel qt;` } - - + public buildQuery(bbox: string) { return this.buildUrl(this.buildScript(bbox)) } - + /** * Little helper method to quickly open overpass-turbo in the browser */ - public static AsOverpassTurboLink(tags: TagsFilter){ + public static AsOverpassTurboLink(tags: TagsFilter) { const overpass = new Overpass(tags, [], "", undefined, undefined, false) - const script = overpass.buildScript("","({{bbox}})", true) + const script = overpass.buildScript("", "({{bbox}})", true) const url = "http://overpass-turbo.eu/?Q=" return url + encodeURIComponent(script) } diff --git a/Logic/Osm/RelationsTracker.ts b/Logic/Osm/RelationsTracker.ts index eb776907b..e4c65ba87 100644 --- a/Logic/Osm/RelationsTracker.ts +++ b/Logic/Osm/RelationsTracker.ts @@ -1,24 +1,25 @@ -import {UIEventSource} from "../UIEventSource"; +import { UIEventSource } from "../UIEventSource" export interface Relation { - id: number, + id: number type: "relation" members: { - type: ("way" | "node" | "relation"), - ref: number, + type: "way" | "node" | "relation" + ref: number role: string - }[], - tags: any, + }[] + tags: any // Alias for tags; tags == properties properties: any } export default class RelationsTracker { + public knownRelations = new UIEventSource>( + new Map(), + "Relation memberships" + ) - public knownRelations = new UIEventSource>(new Map(), "Relation memberships"); - - constructor() { - } + constructor() {} /** * Gets an overview of the relations - except for multipolygons. We don't care about those @@ -26,8 +27,9 @@ export default class RelationsTracker { * @constructor */ private static GetRelationElements(overpassJson: any): Relation[] { - const relations = overpassJson.elements - .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") + const relations = overpassJson.elements.filter( + (element) => element.type === "relation" && element.tags.type !== "multipolygon" + ) for (const relation of relations) { relation.properties = relation.tags } @@ -45,12 +47,12 @@ export default class RelationsTracker { */ private UpdateMembershipTable(relations: Relation[]): void { const memberships = this.knownRelations.data - let changed = false; + let changed = false for (const relation of relations) { for (const member of relation.members) { const role = { role: member.role, - relation: relation + relation: relation, } const key = member.type + "/" + member.ref if (!memberships.has(key)) { @@ -58,19 +60,17 @@ export default class RelationsTracker { } const knownRelations = memberships.get(key) - const alreadyExists = knownRelations.some(knownRole => { + const alreadyExists = knownRelations.some((knownRole) => { return knownRole.role === role.role && knownRole.relation === role.relation }) if (!alreadyExists) { knownRelations.push(role) - changed = true; + changed = true } } } if (changed) { this.knownRelations.ping() } - } - -} \ No newline at end of file +} diff --git a/Logic/Osm/aspectedRouting.ts b/Logic/Osm/aspectedRouting.ts index 31ab66e59..bd2fa9f7f 100644 --- a/Logic/Osm/aspectedRouting.ts +++ b/Logic/Osm/aspectedRouting.ts @@ -1,13 +1,12 @@ export default class AspectedRouting { - public readonly name: string public readonly description: string public readonly units: string public readonly program: any public constructor(program) { - this.name = program.name; - this.description = program.description; + this.name = program.name + this.description = program.description this.units = program.unit this.program = JSON.parse(JSON.stringify(program)) delete this.program.name @@ -20,40 +19,41 @@ export default class AspectedRouting { */ public static interpret(program: any, properties: any) { if (typeof program !== "object") { - return program; + return program } - let functionName /*: string*/ = undefined; + let functionName /*: string*/ = undefined let functionArguments /*: any */ = undefined let otherValues = {} // @ts-ignore - Object.entries(program).forEach(tag => { - const [key, value] = tag; - if (key.startsWith("$")) { - functionName = key - functionArguments = value - } else { - otherValues[key] = value - } + Object.entries(program).forEach((tag) => { + const [key, value] = tag + if (key.startsWith("$")) { + functionName = key + functionArguments = value + } else { + otherValues[key] = value } - ) + }) if (functionName === undefined) { return AspectedRouting.interpretAsDictionary(program, properties) } - if (functionName === '$multiply') { - return AspectedRouting.multiplyScore(properties, functionArguments); - } else if (functionName === '$firstMatchOf') { - return AspectedRouting.getFirstMatchScore(properties, functionArguments); - } else if (functionName === '$min') { - return AspectedRouting.getMinValue(properties, functionArguments); - } else if (functionName === '$max') { - return AspectedRouting.getMaxValue(properties, functionArguments); - } else if (functionName === '$default') { + if (functionName === "$multiply") { + return AspectedRouting.multiplyScore(properties, functionArguments) + } else if (functionName === "$firstMatchOf") { + return AspectedRouting.getFirstMatchScore(properties, functionArguments) + } else if (functionName === "$min") { + return AspectedRouting.getMinValue(properties, functionArguments) + } else if (functionName === "$max") { + return AspectedRouting.getMaxValue(properties, functionArguments) + } else if (functionName === "$default") { return AspectedRouting.defaultV(functionArguments, otherValues, properties) } else { - console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`); + console.error( + `Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}` + ) } } @@ -70,7 +70,7 @@ export default class AspectedRouting { * surface: { * sett : 0.9 * } - * + * * } * * in combination with the tags {highway: residential}, @@ -86,8 +86,8 @@ export default class AspectedRouting { */ private static interpretAsDictionary(program, tags) { // @ts-ignore - return Object.entries(tags).map(tag => { - const [key, value] = tag; + return Object.entries(tags).map((tag) => { + const [key, value] = tag const propertyValue = program[key] if (propertyValue === undefined) { return undefined @@ -97,7 +97,7 @@ export default class AspectedRouting { } // @ts-ignore return propertyValue[value] - }); + }) } private static defaultV(subProgram, otherArgs, tags) { @@ -105,7 +105,7 @@ export default class AspectedRouting { const normalProgram = Object.entries(otherArgs)[0][1] const value = AspectedRouting.interpret(normalProgram, tags) if (value !== undefined) { - return value; + return value } return AspectedRouting.interpret(subProgram, tags) } @@ -121,13 +121,15 @@ export default class AspectedRouting { let subResults: any[] if (subprograms.length !== undefined) { - subResults = AspectedRouting.concatMap(subprograms, subprogram => AspectedRouting.interpret(subprogram, tags)) + subResults = AspectedRouting.concatMap(subprograms, (subprogram) => + AspectedRouting.interpret(subprogram, tags) + ) } else { subResults = AspectedRouting.interpret(subprograms, tags) } - subResults.filter(r => r !== undefined).forEach(r => number *= parseFloat(r)) - return number.toFixed(2); + subResults.filter((r) => r !== undefined).forEach((r) => (number *= parseFloat(r))) + return number.toFixed(2) } private static getFirstMatchScore(tags, order: any) { @@ -136,12 +138,12 @@ export default class AspectedRouting { for (let key of order) { // @ts-ignore for (let entry of Object.entries(JSON.parse(tags))) { - const [tagKey, value] = entry; + const [tagKey, value] = entry if (key === tagKey) { // We have a match... let's evaluate the subprogram const evaluated = AspectedRouting.interpret(value, tags) if (evaluated !== undefined) { - return evaluated; + return evaluated } } } @@ -152,26 +154,30 @@ export default class AspectedRouting { } private static getMinValue(tags, subprogram) { - const minArr = subprogram.map(part => { - if (typeof (part) === 'object') { - const calculatedValue = this.interpret(part, tags) - return parseFloat(calculatedValue) - } else { - return parseFloat(part); - } - }).filter(v => !isNaN(v)); - return Math.min(...minArr); + const minArr = subprogram + .map((part) => { + if (typeof part === "object") { + const calculatedValue = this.interpret(part, tags) + return parseFloat(calculatedValue) + } else { + return parseFloat(part) + } + }) + .filter((v) => !isNaN(v)) + return Math.min(...minArr) } private static getMaxValue(tags, subprogram) { - const maxArr = subprogram.map(part => { - if (typeof (part) === 'object') { - return parseFloat(AspectedRouting.interpret(part, tags)) - } else { - return parseFloat(part); - } - }).filter(v => !isNaN(v)); - return Math.max(...maxArr); + const maxArr = subprogram + .map((part) => { + if (typeof part === "object") { + return parseFloat(AspectedRouting.interpret(part, tags)) + } else { + return parseFloat(part) + } + }) + .filter((v) => !isNaN(v)) + return Math.max(...maxArr) } private static concatMap(list, f): any[] { @@ -185,11 +191,10 @@ export default class AspectedRouting { result.push(elem) } } - return result; + return result } public evaluate(properties) { return AspectedRouting.interpret(this.program, properties) } - -} \ No newline at end of file +} diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index fd9fe1914..2e2d257f0 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -1,107 +1,125 @@ -import {GeoOperations} from "./GeoOperations"; -import {Utils} from "../Utils"; -import opening_hours from "opening_hours"; -import Combine from "../UI/Base/Combine"; -import BaseUIElement from "../UI/BaseUIElement"; -import Title from "../UI/Base/Title"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import {CountryCoder} from "latlon2country" -import Constants from "../Models/Constants"; -import {TagUtils} from "./Tags/TagUtils"; - +import { GeoOperations } from "./GeoOperations" +import { Utils } from "../Utils" +import opening_hours from "opening_hours" +import Combine from "../UI/Base/Combine" +import BaseUIElement from "../UI/BaseUIElement" +import Title from "../UI/Base/Title" +import { FixedUiElement } from "../UI/Base/FixedUiElement" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import { CountryCoder } from "latlon2country" +import Constants from "../Models/Constants" +import { TagUtils } from "./Tags/TagUtils" export class SimpleMetaTagger { - public readonly keys: string[]; - public readonly doc: string; - public readonly isLazy: boolean; + public readonly keys: string[] + public readonly doc: string + public readonly isLazy: boolean public readonly includesDates: boolean - public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean; + public readonly applyMetaTagsOnFeature: ( + feature: any, + freshness: Date, + layer: LayerConfig, + state + ) => boolean /*** * A function that adds some extra data to a feature * @param docs: what does this extra data do? * @param f: apply the changes. Returns true if something changed */ - constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean, cleanupRetagger?: boolean }, - f: ((feature: any, freshness: Date, layer: LayerConfig, state) => boolean)) { - this.keys = docs.keys; - this.doc = docs.doc; + constructor( + docs: { + keys: string[] + doc: string + includesDates?: boolean + isLazy?: boolean + cleanupRetagger?: boolean + }, + f: (feature: any, freshness: Date, layer: LayerConfig, state) => boolean + ) { + this.keys = docs.keys + this.doc = docs.doc this.isLazy = docs.isLazy - this.applyMetaTagsOnFeature = f; - this.includesDates = docs.includesDates ?? false; + this.applyMetaTagsOnFeature = f + this.includesDates = docs.includesDates ?? false if (!docs.cleanupRetagger) { for (const key of docs.keys) { - if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { + if (!key.startsWith("_") && key.toLowerCase().indexOf("theme") < 0) { throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` } } } } - } export class CountryTagger extends SimpleMetaTagger { - private static readonly coder = new CountryCoder(Constants.countryCoderEndpoint, Utils.downloadJson); - public runningTasks: Set; + private static readonly coder = new CountryCoder( + Constants.countryCoderEndpoint, + Utils.downloadJson + ) + public runningTasks: Set constructor() { - const runningTasks = new Set(); - super - ( + const runningTasks = new Set() + super( { keys: ["_country"], doc: "The country code of the property (with latlon2country)", - includesDates: false + includesDates: false, }, - ((feature, _, __, state) => { - let centerPoint: any = GeoOperations.centerpoint(feature); - const lat = centerPoint.geometry.coordinates[1]; - const lon = centerPoint.geometry.coordinates[0]; + (feature, _, __, state) => { + let centerPoint: any = GeoOperations.centerpoint(feature) + const lat = centerPoint.geometry.coordinates[1] + const lon = centerPoint.geometry.coordinates[0] runningTasks.add(feature) - CountryTagger.coder.GetCountryCodeAsync(lon, lat).then( - countries => { + CountryTagger.coder + .GetCountryCodeAsync(lon, lat) + .then((countries) => { runningTasks.delete(feature) try { - const oldCountry = feature.properties["_country"]; - feature.properties["_country"] = countries[0].trim().toLowerCase(); + const oldCountry = feature.properties["_country"] + feature.properties["_country"] = countries[0].trim().toLowerCase() if (oldCountry !== feature.properties["_country"]) { - const tagsSource = state?.allElements?.getEventSourceById(feature.properties.id); - tagsSource?.ping(); + const tagsSource = state?.allElements?.getEventSourceById( + feature.properties.id + ) + tagsSource?.ping() } } catch (e) { console.warn(e) } - } - ).catch(_ => { - runningTasks.delete(feature) - }) - return false; - }) + }) + .catch((_) => { + runningTasks.delete(feature) + }) + return false + } ) - this.runningTasks = runningTasks; + this.runningTasks = runningTasks } } export default class SimpleMetaTaggers { - public static readonly objectMetaInfo = new SimpleMetaTagger( { - keys: ["_last_edit:contributor", + keys: [ + "_last_edit:contributor", "_last_edit:contributor:uid", "_last_edit:changeset", "_last_edit:timestamp", "_version_number", - "_backend"], - doc: "Information about the last edit of this object." + "_backend", + ], + doc: "Information about the last edit of this object.", }, - (feature) => {/*Note: also called by 'UpdateTagsFromOsmAPI'*/ + (feature) => { + /*Note: also called by 'UpdateTagsFromOsmAPI'*/ - const tgs = feature.properties; + const tgs = feature.properties function move(src: string, target: string) { if (tgs[src] === undefined) { - return; + return } tgs[target] = tgs[src] delete tgs[src] @@ -112,7 +130,7 @@ export default class SimpleMetaTaggers { move("changeset", "_last_edit:changeset") move("timestamp", "_last_edit:timestamp") move("version", "_version_number") - return true; + return true } ) public static country = new CountryTagger() @@ -122,32 +140,45 @@ export default class SimpleMetaTaggers { doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`", }, (feature, _) => { - const changed = feature.properties["_geometry:type"] === feature.geometry.type; - feature.properties["_geometry:type"] = feature.geometry.type; + const changed = feature.properties["_geometry:type"] === feature.geometry.type + feature.properties["_geometry:type"] = feature.geometry.type return changed } ) private static readonly cardinalDirections = { - N: 0, NNE: 22.5, NE: 45, ENE: 67.5, - E: 90, ESE: 112.5, SE: 135, SSE: 157.5, - S: 180, SSW: 202.5, SW: 225, WSW: 247.5, - W: 270, WNW: 292.5, NW: 315, NNW: 337.5 + N: 0, + NNE: 22.5, + NE: 45, + ENE: 67.5, + E: 90, + ESE: 112.5, + SE: 135, + SSE: 157.5, + S: 180, + SSW: 202.5, + SW: 225, + WSW: 247.5, + W: 270, + WNW: 292.5, + NW: 315, + NNW: 337.5, } - private static latlon = new SimpleMetaTagger({ + private static latlon = new SimpleMetaTagger( + { keys: ["_lat", "_lon"], - doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)" + doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)", }, - (feature => { - const centerPoint = GeoOperations.centerpoint(feature); - const lat = centerPoint.geometry.coordinates[1]; - const lon = centerPoint.geometry.coordinates[0]; - feature.properties["_lat"] = "" + lat; - feature.properties["_lon"] = "" + lon; - feature._lon = lon; // This is dirty, I know - feature._lat = lat; - return true; - }) - ); + (feature) => { + const centerPoint = GeoOperations.centerpoint(feature) + const lat = centerPoint.geometry.coordinates[1] + const lon = centerPoint.geometry.coordinates[0] + feature.properties["_lat"] = "" + lat + feature.properties["_lon"] = "" + lon + feature._lon = lon // This is dirty, I know + feature._lat = lat + return true + } + ) private static layerInfo = new SimpleMetaTagger( { doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.", @@ -156,98 +187,101 @@ export default class SimpleMetaTaggers { }, (feature, freshness, layer) => { if (feature.properties._layer === layer.id) { - return false; + return false } feature.properties._layer = layer.id - return true; + return true } ) private static noBothButLeftRight = new SimpleMetaTagger( { - keys: ["sidewalk:left", "sidewalk:right", "generic_key:left:property", "generic_key:right:property"], + keys: [ + "sidewalk:left", + "sidewalk:right", + "generic_key:left:property", + "generic_key:right:property", + ], doc: "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined", includesDates: false, - cleanupRetagger: true + cleanupRetagger: true, }, - ((feature, state, layer) => { - - if (!layer.lineRendering.some(lr => lr.leftRightSensitive)) { - return; + (feature, state, layer) => { + if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) { + return } return SimpleMetaTaggers.removeBothTagging(feature.properties) - }) + } ) private static surfaceArea = new SimpleMetaTagger( { keys: ["_surface", "_surface:ha"], doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways", - isLazy: true + isLazy: true, }, - (feature => { - + (feature) => { Object.defineProperty(feature.properties, "_surface", { enumerable: false, configurable: true, get: () => { - const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature); + const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature) delete feature.properties["_surface"] - feature.properties["_surface"] = sqMeters; + feature.properties["_surface"] = sqMeters return sqMeters - } + }, }) Object.defineProperty(feature.properties, "_surface:ha", { enumerable: false, configurable: true, get: () => { - const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); - const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10; + const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature) + const sqMetersHa = "" + Math.floor(sqMeters / 1000) / 10 delete feature.properties["_surface:ha"] - feature.properties["_surface:ha"] = sqMetersHa; + feature.properties["_surface:ha"] = sqMetersHa return sqMetersHa - } + }, }) - return true; - }) - ); + return true + } + ) private static levels = new SimpleMetaTagger( { doc: "Extract the 'level'-tag into a normalized, ';'-separated value", - keys: ["_level"] + keys: ["_level"], }, - ((feature) => { + (feature) => { if (feature.properties["level"] === undefined) { - return false; + return false } - + const l = feature.properties["level"] const newValue = TagUtils.LevelsParser(l).join(";") - if(l === newValue) { - return false; + if (l === newValue) { + return false } feature.properties["level"] = newValue return true - - }) + } ) private static canonicalize = new SimpleMetaTagger( { doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)", keys: ["Theme-defined keys"], - }, - ((feature, _, __, state) => { - const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? [])); + (feature, _, __, state) => { + const units = Utils.NoNull( + [].concat(...(state?.layoutToUse?.layers?.map((layer) => layer.units) ?? [])) + ) if (units.length == 0) { - return; + return } - let rewritten = false; + let rewritten = false for (const key in feature.properties) { if (!feature.properties.hasOwnProperty(key)) { - continue; + continue } for (const unit of units) { if (unit === undefined) { @@ -258,56 +292,59 @@ export default class SimpleMetaTaggers { continue } if (!unit.appliesToKeys.has(key)) { - continue; + continue } const value = feature.properties[key] const denom = unit.findDenomination(value, () => feature.properties["_country"]) if (denom === undefined) { // no valid value found - break; + break } - const [, denomination] = denom; - const defaultDenom = unit.getDefaultDenomination(() => feature.properties["_country"]) - let canonical = denomination?.canonicalValue(value, defaultDenom == denomination) ?? undefined; + const [, denomination] = denom + const defaultDenom = unit.getDefaultDenomination( + () => feature.properties["_country"] + ) + let canonical = + denomination?.canonicalValue(value, defaultDenom == denomination) ?? + undefined if (canonical === value) { - break; + break } console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) if (canonical === undefined && !unit.eraseInvalid) { - break; + break } - feature.properties[key] = canonical; - rewritten = true; - break; + feature.properties[key] = canonical + rewritten = true + break } - } return rewritten - }) + } ) private static lngth = new SimpleMetaTagger( { keys: ["_length", "_length:km"], - doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter" + doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter", }, - (feature => { + (feature) => { const l = GeoOperations.lengthInMeters(feature) feature.properties["_length"] = "" + l const km = Math.floor(l / 1000) const kmRest = Math.round((l - km * 1000) / 100) feature.properties["_length:km"] = "" + km + "." + kmRest - return true; - }) + return true + } ) private static isOpen = new SimpleMetaTagger( { keys: ["_isOpen"], doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", includesDates: true, - isLazy: true + isLazy: true, }, - ((feature, _, __, state) => { + (feature, _, __, state) => { if (Utils.runningFromConsole) { // We are running from console, thus probably creating a cache // isOpen is irrelevant @@ -315,7 +352,7 @@ export default class SimpleMetaTaggers { } if (feature.properties.opening_hours === "24/7") { feature.properties._isOpen = "yes" - return true; + return true } // _isOpen is calculated dynamically on every call @@ -325,92 +362,92 @@ export default class SimpleMetaTaggers { get: () => { const tags = feature.properties if (tags.opening_hours === undefined) { - return; + return } if (tags._country === undefined) { - return; + return } try { const [lon, lat] = GeoOperations.centerpointCoordinates(feature) - const oh = new opening_hours(tags["opening_hours"], { - lat: lat, - lon: lon, - address: { - country_code: tags._country.toLowerCase(), - state: undefined - } - }, {tag_key: "opening_hours"}); + const oh = new opening_hours( + tags["opening_hours"], + { + lat: lat, + lon: lon, + address: { + country_code: tags._country.toLowerCase(), + state: undefined, + }, + }, + { tag_key: "opening_hours" } + ) // Recalculate! - return oh.getState() ? "yes" : "no"; - + return oh.getState() ? "yes" : "no" } catch (e) { - console.warn("Error while parsing opening hours of ", tags.id, e); + console.warn("Error while parsing opening hours of ", tags.id, e) delete tags._isOpen - tags["_isOpen"] = "parse_error"; + tags["_isOpen"] = "parse_error" } - } - }); + }, + }) - - const tagsSource = state.allElements.getEventSourceById(feature.properties.id); - - - }) + const tagsSource = state.allElements.getEventSourceById(feature.properties.id) + } ) private static directionSimplified = new SimpleMetaTagger( { keys: ["_direction:numerical", "_direction:leftright"], - doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map" + doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", }, - (feature => { - const tags = feature.properties; - const direction = tags["camera:direction"] ?? tags["direction"]; + (feature) => { + const tags = feature.properties + const direction = tags["camera:direction"] ?? tags["direction"] if (direction === undefined) { - return false; + return false } - const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction); + const n = SimpleMetaTaggers.cardinalDirections[direction] ?? Number(direction) if (isNaN(n)) { - return false; + return false } // The % operator has range (-360, 360). We apply a trick to get [0, 360). - const normalized = ((n % 360) + 360) % 360; + const normalized = ((n % 360) + 360) % 360 - tags["_direction:numerical"] = normalized; - tags["_direction:leftright"] = normalized <= 180 ? "right" : "left"; - return true; - }) + tags["_direction:numerical"] = normalized + tags["_direction:leftright"] = normalized <= 180 ? "right" : "left" + return true + } ) private static currentTime = new SimpleMetaTagger( { keys: ["_now:date", "_now:datetime", "_loaded:date", "_loaded:_datetime"], doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely", - includesDates: true + includesDates: true, }, (feature, freshness) => { - const now = new Date(); + const now = new Date() if (typeof freshness === "string") { freshness = new Date(freshness) } function date(d: Date) { - return d.toISOString().slice(0, 10); + return d.toISOString().slice(0, 10) } function datetime(d: Date) { - return d.toISOString().slice(0, -5).replace("T", " "); + return d.toISOString().slice(0, -5).replace("T", " ") } - feature.properties["_now:date"] = date(now); - feature.properties["_now:datetime"] = datetime(now); - feature.properties["_loaded:date"] = date(freshness); - feature.properties["_loaded:datetime"] = datetime(freshness); - return true; + feature.properties["_now:date"] = date(now) + feature.properties["_now:datetime"] = datetime(now) + feature.properties["_loaded:date"] = date(freshness) + feature.properties["_loaded:datetime"] = datetime(freshness) + return true } - ); + ) public static metatags: SimpleMetaTagger[] = [ SimpleMetaTaggers.latlon, SimpleMetaTaggers.layerInfo, @@ -424,11 +461,11 @@ export default class SimpleMetaTaggers { SimpleMetaTaggers.objectMetaInfo, SimpleMetaTaggers.noBothButLeftRight, SimpleMetaTaggers.geometryType, - SimpleMetaTaggers.levels - - ]; - public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy) - .map(tagger => tagger.keys)); + SimpleMetaTaggers.levels, + ] + public static readonly lazyTags: string[] = [].concat( + ...SimpleMetaTaggers.metatags.filter((tagger) => tagger.isLazy).map((tagger) => tagger.keys) + ) /** * Edits the given object to rewrite 'both'-tagging into a 'left-right' tagging scheme. @@ -451,36 +488,34 @@ export default class SimpleMetaTaggers { } if (tags["sidewalk"]) { - const v = tags["sidewalk"] switch (v) { case "none": case "no": - set("sidewalk:left", "no"); - set("sidewalk:right", "no"); + set("sidewalk:left", "no") + set("sidewalk:right", "no") break case "both": - set("sidewalk:left", "yes"); - set("sidewalk:right", "yes"); - break; + set("sidewalk:left", "yes") + set("sidewalk:right", "yes") + break case "left": - set("sidewalk:left", "yes"); - set("sidewalk:right", "no"); - break; + set("sidewalk:left", "yes") + set("sidewalk:right", "no") + break case "right": - set("sidewalk:left", "no"); - set("sidewalk:right", "yes"); - break; + set("sidewalk:left", "no") + set("sidewalk:right", "yes") + break default: - set("sidewalk:left", v); - set("sidewalk:right", v); - break; + set("sidewalk:left", v) + set("sidewalk:right", v) + break } delete tags["sidewalk"] somethingChanged = true } - const regex = /\([^:]*\):both:\(.*\)/ for (const key in tags) { const v = tags[key] @@ -503,7 +538,6 @@ export default class SimpleMetaTaggers { } } - return somethingChanged } @@ -512,13 +546,16 @@ export default class SimpleMetaTaggers { new Combine([ "Metatags are extra tags available, in order to display more data or to give better questions.", "They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", - "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object" - ]).SetClass("flex-col") - - ]; + "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object", + ]).SetClass("flex-col"), + ] subElements.push(new Title("Metatags calculated by MapComplete", 2)) - subElements.push(new FixedUiElement("The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme")) + subElements.push( + new FixedUiElement( + "The following values are always calculated, by default, by MapComplete and are available automatically on all elements in every theme" + ) + ) for (const metatag of SimpleMetaTaggers.metatags) { subElements.push( new Title(metatag.keys.join(", "), 3), @@ -529,5 +566,4 @@ export default class SimpleMetaTaggers { return new Combine(subElements).SetClass("flex-col") } - } diff --git a/Logic/State/ElementsState.ts b/Logic/State/ElementsState.ts index f92891358..cecb04009 100644 --- a/Logic/State/ElementsState.ts +++ b/Logic/State/ElementsState.ts @@ -1,89 +1,91 @@ -import FeatureSwitchState from "./FeatureSwitchState"; -import {ElementStorage} from "../ElementStorage"; -import {Changes} from "../Osm/Changes"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {UIEventSource} from "../UIEventSource"; -import Loc from "../../Models/Loc"; -import {BBox} from "../BBox"; -import {QueryParameters} from "../Web/QueryParameters"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; -import {Utils} from "../../Utils"; -import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; -import PendingChangesUploader from "../Actors/PendingChangesUploader"; +import FeatureSwitchState from "./FeatureSwitchState" +import { ElementStorage } from "../ElementStorage" +import { Changes } from "../Osm/Changes" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { UIEventSource } from "../UIEventSource" +import Loc from "../../Models/Loc" +import { BBox } from "../BBox" +import { QueryParameters } from "../Web/QueryParameters" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { Utils } from "../../Utils" +import ChangeToElementsActor from "../Actors/ChangeToElementsActor" +import PendingChangesUploader from "../Actors/PendingChangesUploader" /** * The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc */ export default class ElementsState extends FeatureSwitchState { - /** The mapping from id -> UIEventSource */ - public allElements: ElementStorage = new ElementStorage(); - + public allElements: ElementStorage = new ElementStorage() + /** The latest element that was selected */ - public readonly selectedElement = new UIEventSource( - undefined, - "Selected element" - ); - + public readonly selectedElement = new UIEventSource(undefined, "Selected element") /** * The map location: currently centered lat, lon and zoom */ - public readonly locationControl = new UIEventSource(undefined, "locationControl"); + public readonly locationControl = new UIEventSource(undefined, "locationControl") /** * The current visible extent of the screen */ public readonly currentBounds = new UIEventSource(undefined) - constructor(layoutToUse: LayoutConfig) { - super(layoutToUse); - - - function localStorageSynced(key: string, deflt: number, docs: string ): UIEventSource{ - const localStorage = LocalStorageSource.Get(key) - const previousValue = localStorage.data - const src = UIEventSource.asFloat( - QueryParameters.GetQueryParameter( - key, - "" + deflt, - docs - ).syncWith(localStorage) - ); - - if(src.data === deflt){ - const prev = Number(previousValue) - if(!isNaN(prev)){ - src.setData(prev) - } + super(layoutToUse) + + function localStorageSynced( + key: string, + deflt: number, + docs: string + ): UIEventSource { + const localStorage = LocalStorageSource.Get(key) + const previousValue = localStorage.data + const src = UIEventSource.asFloat( + QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage) + ) + + if (src.data === deflt) { + const prev = Number(previousValue) + if (!isNaN(prev)) { + src.setData(prev) } - - return src; } - // -- Location control initialization - const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level") - const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude") - const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app") + return src + } + // -- Location control initialization + const zoom = localStorageSynced( + "z", + layoutToUse?.startZoom ?? 1, + "The initial/current zoom level" + ) + const lat = localStorageSynced( + "lat", + layoutToUse?.startLat ?? 0, + "The initial/current latitude" + ) + const lon = localStorageSynced( + "lon", + layoutToUse?.startLon ?? 0, + "The initial/current longitude of the app" + ) - this.locationControl.setData({ - zoom: Utils.asFloat(zoom.data), - lat: Utils.asFloat(lat.data), - lon: Utils.asFloat(lon.data), - }) - this.locationControl.addCallback((latlonz) => { - // Sync the location controls - zoom.setData(latlonz.zoom); - lat.setData(latlonz.lat); - lon.setData(latlonz.lon); - }); - - + this.locationControl.setData({ + zoom: Utils.asFloat(zoom.data), + lat: Utils.asFloat(lat.data), + lon: Utils.asFloat(lon.data), + }) + this.locationControl.addCallback((latlonz) => { + // Sync the location controls + zoom.setData(latlonz.zoom) + lat.setData(latlonz.lat) + lon.setData(latlonz.lon) + }) } -} \ No newline at end of file +} diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index cbf8db78b..8c56a0aac 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -1,37 +1,39 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import FeaturePipeline from "../FeatureSource/FeaturePipeline"; -import {Tiles} from "../../Models/TileRange"; -import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"; -import {TileHierarchyAggregator} from "../../UI/ShowDataLayer/TileHierarchyAggregator"; -import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"; -import {UIEventSource} from "../UIEventSource"; -import MapState from "./MapState"; -import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; -import Hash from "../Web/Hash"; -import {BBox} from "../BBox"; -import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; -import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"; -import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import FeaturePipeline from "../FeatureSource/FeaturePipeline" +import { Tiles } from "../../Models/TileRange" +import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer" +import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator" +import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo" +import { UIEventSource } from "../UIEventSource" +import MapState from "./MapState" +import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" +import Hash from "../Web/Hash" +import { BBox } from "../BBox" +import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" +import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" +import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" export default class FeaturePipelineState extends MapState { - /** * The piece of code which fetches data from various sources and shows it on the background map */ - public readonly featurePipeline: FeaturePipeline; - private readonly featureAggregator: TileHierarchyAggregator; + public readonly featurePipeline: FeaturePipeline + private readonly featureAggregator: TileHierarchyAggregator private readonly metatagRecalculator: MetaTagRecalculator - private readonly popups : Map = new Map(); - + private readonly popups: Map = new Map< + string, + ScrollableFullScreen + >() + constructor(layoutToUse: LayoutConfig) { - super(layoutToUse); + super(layoutToUse) const clustering = layoutToUse?.clustering - this.featureAggregator = TileHierarchyAggregator.createHierarchy(this); + this.featureAggregator = TileHierarchyAggregator.createHierarchy(this) const clusterCounter = this.featureAggregator - const self = this; + const self = this /** * We are a bit in a bind: @@ -51,26 +53,26 @@ export default class FeaturePipelineState extends MapState { self.metatagRecalculator.registerSource(source) } } - - - function registerSource(source: FeatureSourceForLayer & Tiled) { + function registerSource(source: FeatureSourceForLayer & Tiled) { clusterCounter.addTile(source) - const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) + const sourceBBox = source.features.map((allFeatures) => + BBox.bboxAroundAll(allFeatures.map((f) => BBox.get(f.feature))) + ) // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering const doShowFeatures = source.features.map( - f => { + (f) => { const z = self.locationControl.data.zoom if (!source.layer.isDisplayed.data) { - return false; + return false } const bounds = self.currentBounds.data if (bounds === undefined) { // Map is not yet displayed - return false; + return false } if (!sourceBBox.data.overlapsWith(bounds)) { @@ -78,10 +80,9 @@ export default class FeaturePipelineState extends MapState { return false } - if (z < source.layer.layerDef.minzoom) { // Layer is always hidden for this zoom level - return false; + return false } if (z > clustering.maxZoom) { @@ -93,55 +94,55 @@ export default class FeaturePipelineState extends MapState { return false } - let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex); + let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) if (tileZ >= z) { - while (tileZ > z) { tileZ-- tileX = Math.floor(tileX / 2) tileY = Math.floor(tileY / 2) } - if (clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))?.totalValue > clustering.minNeededElements) { + if ( + clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY)) + ?.totalValue > clustering.minNeededElements + ) { // To much elements return false } } - return true - }, [self.currentBounds, source.layer.isDisplayed, sourceBBox] + }, + [self.currentBounds, source.layer.isDisplayed, sourceBBox] ) - new ShowDataLayer( - { - features: source, - leafletMap: self.leafletMap, - layerToShow: source.layer.layerDef, - doShowLayer: doShowFeatures, - selectedElement: self.selectedElement, - state: self, - popup: (tags, layer) => self.CreatePopup(tags, layer) - } - ) + new ShowDataLayer({ + features: source, + leafletMap: self.leafletMap, + layerToShow: source.layer.layerDef, + doShowLayer: doShowFeatures, + selectedElement: self.selectedElement, + state: self, + popup: (tags, layer) => self.CreatePopup(tags, layer), + }) } - - this.featurePipeline = new FeaturePipeline(registerSource, this, {handleRawFeatureSource: registerRaw}); + this.featurePipeline = new FeaturePipeline(registerSource, this, { + handleRawFeatureSource: registerRaw, + }) this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) this.metatagRecalculator.registerSource(this.currentView, true) - sourcesToRegister.forEach(source => self.metatagRecalculator.registerSource(source)) + sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source)) new SelectedFeatureHandler(Hash.hash, this) this.AddClusteringToMap(this.leafletMap) - } - - public CreatePopup(tags:UIEventSource , layer: LayerConfig): ScrollableFullScreen{ - if(this.popups.has(tags.data.id)){ - return this.popups.get(tags.data.id) + + public CreatePopup(tags: UIEventSource, layer: LayerConfig): ScrollableFullScreen { + if (this.popups.has(tags.data.id)) { + return this.popups.get(tags.data.id) } const popup = new FeatureInfoBox(tags, layer, this) this.popups.set(tags.data.id, popup) @@ -155,15 +156,19 @@ export default class FeaturePipelineState extends MapState { */ public AddClusteringToMap(leafletMap: UIEventSource) { const clustering = this.layoutToUse.clustering - const self = this; + const self = this new ShowDataLayer({ - features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements), + features: this.featureAggregator.getCountsForZoom( + clustering, + this.locationControl, + clustering.minNeededElements + ), leafletMap: leafletMap, layerToShow: ShowTileInfo.styling, - popup: this.featureSwitchIsDebugging.data ? (tags, layer) => new FeatureInfoBox(tags, layer, self) : undefined, - state: this + popup: this.featureSwitchIsDebugging.data + ? (tags, layer) => new FeatureInfoBox(tags, layer, self) + : undefined, + state: this, }) } - - -} \ No newline at end of file +} diff --git a/Logic/State/FeatureSwitchState.ts b/Logic/State/FeatureSwitchState.ts index b14c2fd76..c3d9a0bb9 100644 --- a/Logic/State/FeatureSwitchState.ts +++ b/Logic/State/FeatureSwitchState.ts @@ -1,45 +1,43 @@ /** * The part of the global state which initializes the feature switches, based on default values and on the layoutToUse */ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {UIEventSource} from "../UIEventSource"; -import {QueryParameters} from "../Web/QueryParameters"; -import Constants from "../../Models/Constants"; -import {Utils} from "../../Utils"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { UIEventSource } from "../UIEventSource" +import { QueryParameters } from "../Web/QueryParameters" +import Constants from "../../Models/Constants" +import { Utils } from "../../Utils" export default class FeatureSwitchState { - /** * The layout that is being used in this run */ - public readonly layoutToUse: LayoutConfig; + public readonly layoutToUse: LayoutConfig - public readonly featureSwitchUserbadge: UIEventSource; - public readonly featureSwitchSearch: UIEventSource; - public readonly featureSwitchBackgroundSelection: UIEventSource; - public readonly featureSwitchAddNew: UIEventSource; - public readonly featureSwitchWelcomeMessage: UIEventSource; - public readonly featureSwitchExtraLinkEnabled: UIEventSource; - public readonly featureSwitchMoreQuests: UIEventSource; - public readonly featureSwitchShareScreen: UIEventSource; - public readonly featureSwitchGeolocation: UIEventSource; - public readonly featureSwitchIsTesting: UIEventSource; - public readonly featureSwitchIsDebugging: UIEventSource; - public readonly featureSwitchShowAllQuestions: UIEventSource; - public readonly featureSwitchApiURL: UIEventSource; - public readonly featureSwitchFilter: UIEventSource; - public readonly featureSwitchEnableExport: UIEventSource; - public readonly featureSwitchFakeUser: UIEventSource; - public readonly featureSwitchExportAsPdf: UIEventSource; - public readonly overpassUrl: UIEventSource; - public readonly overpassTimeout: UIEventSource; - public readonly overpassMaxZoom: UIEventSource; - public readonly osmApiTileSize: UIEventSource; - public readonly backgroundLayerId: UIEventSource; + public readonly featureSwitchUserbadge: UIEventSource + public readonly featureSwitchSearch: UIEventSource + public readonly featureSwitchBackgroundSelection: UIEventSource + public readonly featureSwitchAddNew: UIEventSource + public readonly featureSwitchWelcomeMessage: UIEventSource + public readonly featureSwitchExtraLinkEnabled: UIEventSource + public readonly featureSwitchMoreQuests: UIEventSource + public readonly featureSwitchShareScreen: UIEventSource + public readonly featureSwitchGeolocation: UIEventSource + public readonly featureSwitchIsTesting: UIEventSource + public readonly featureSwitchIsDebugging: UIEventSource + public readonly featureSwitchShowAllQuestions: UIEventSource + public readonly featureSwitchApiURL: UIEventSource + public readonly featureSwitchFilter: UIEventSource + public readonly featureSwitchEnableExport: UIEventSource + public readonly featureSwitchFakeUser: UIEventSource + public readonly featureSwitchExportAsPdf: UIEventSource + public readonly overpassUrl: UIEventSource + public readonly overpassTimeout: UIEventSource + public readonly overpassMaxZoom: UIEventSource + public readonly osmApiTileSize: UIEventSource + public readonly backgroundLayerId: UIEventSource public constructor(layoutToUse: LayoutConfig) { - this.layoutToUse = layoutToUse; - + this.layoutToUse = layoutToUse // Helper function to initialize feature switches function featSw( @@ -47,104 +45,104 @@ export default class FeatureSwitchState { deflt: (layout: LayoutConfig) => boolean, documentation: string ): UIEventSource { - - const defaultValue = deflt(layoutToUse); + const defaultValue = deflt(layoutToUse) const queryParam = QueryParameters.GetQueryParameter( key, "" + defaultValue, documentation - ); - - // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened - return queryParam.sync((str) => - str === undefined ? defaultValue : str !== "false", [], - b => b == defaultValue ? undefined : (""+b) ) + // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened + return queryParam.sync( + (str) => (str === undefined ? defaultValue : str !== "false"), + [], + (b) => (b == defaultValue ? undefined : "" + b) + ) } this.featureSwitchUserbadge = featSw( "fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true, "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode." - ); + ) this.featureSwitchSearch = featSw( "fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true, "Disables/Enables the search bar" - ); + ) this.featureSwitchBackgroundSelection = featSw( "fs-background", (layoutToUse) => layoutToUse?.enableBackgroundLayerSelection ?? true, "Disables/Enables the background layer control" - ); + ) this.featureSwitchFilter = featSw( "fs-filter", (layoutToUse) => layoutToUse?.enableLayers ?? true, "Disables/Enables the filter view" - ); + ) this.featureSwitchAddNew = featSw( "fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)" - ); + ) this.featureSwitchWelcomeMessage = featSw( "fs-welcome-message", () => true, "Disables/enables the help menu or welcome message" - ); + ) this.featureSwitchExtraLinkEnabled = featSw( "fs-iframe-popout", - _ => true, + (_) => true, "Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)" - ); + ) this.featureSwitchMoreQuests = featSw( "fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, "Disables/Enables the 'More Quests'-tab in the welcome message" - ); + ) this.featureSwitchShareScreen = featSw( "fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true, "Disables/Enables the 'Share-screen'-tab in the welcome message" - ); + ) this.featureSwitchGeolocation = featSw( "fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true, "Disables/Enables the geolocation button" - ); + ) this.featureSwitchShowAllQuestions = featSw( "fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, "Always show all questions" - ); + ) this.featureSwitchEnableExport = featSw( "fs-export", (layoutToUse) => layoutToUse?.enableExportButton ?? false, "Enable the export as GeoJSON and CSV button" - ); + ) this.featureSwitchExportAsPdf = featSw( "fs-pdf", (layoutToUse) => layoutToUse?.enablePdfDownload ?? false, "Enable the PDF download button" - ); + ) 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'" - ); + ) - - let testingDefaultValue = false; - if (this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole && - (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { + let testingDefaultValue = false + if ( + this.featureSwitchApiURL.data !== "osm-test" && + !Utils.runningFromConsole && + (location.hostname === "localhost" || location.hostname === "127.0.0.1") + ) { testingDefaultValue = true } - this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter( "test", testingDefaultValue, @@ -157,31 +155,47 @@ export default class FeatureSwitchState { "If true, shows some extra debugging help such as all the available tags on every object" ) - this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter("fake-user", false, - "If true, 'dryrun' mode is activated and a fake user account is loaded") + this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter( + "fake-user", + false, + "If true, 'dryrun' mode is activated and a fake user account is loaded" + ) - - this.overpassUrl = QueryParameters.GetQueryParameter("overpassUrl", + this.overpassUrl = QueryParameters.GetQueryParameter( + "overpassUrl", (layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","), "Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter" - ).sync(param => param.split(","), [], urls => urls.join(",")) + ).sync( + (param) => param.split(","), + [], + (urls) => urls.join(",") + ) - this.overpassTimeout = UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassTimeout", - "" + layoutToUse?.overpassTimeout, - "Set a different timeout (in seconds) for queries in overpass")) + this.overpassTimeout = UIEventSource.asFloat( + QueryParameters.GetQueryParameter( + "overpassTimeout", + "" + layoutToUse?.overpassTimeout, + "Set a different timeout (in seconds) for queries in overpass" + ) + ) - - this.overpassMaxZoom = - UIEventSource.asFloat(QueryParameters.GetQueryParameter("overpassMaxZoom", + this.overpassMaxZoom = UIEventSource.asFloat( + QueryParameters.GetQueryParameter( + "overpassMaxZoom", "" + layoutToUse?.overpassMaxZoom, - " point to switch between OSM-api and overpass")) + " point to switch between OSM-api and overpass" + ) + ) - this.osmApiTileSize = - UIEventSource.asFloat(QueryParameters.GetQueryParameter("osmApiTileSize", + this.osmApiTileSize = UIEventSource.asFloat( + QueryParameters.GetQueryParameter( + "osmApiTileSize", "" + layoutToUse?.osmApiTileSize, - "Tilesize when the OSM-API is used to fetch data within a BBOX")) + "Tilesize when the OSM-API is used to fetch data within a BBOX" + ) + ) - this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { + this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => { if (!userbadge) { this.featureSwitchAddNew.setData(false) } @@ -191,9 +205,6 @@ export default class FeatureSwitchState { "background", layoutToUse?.defaultBackgroundId ?? "osm", "The id of the background layer to start with" - ); - + ) } - - -} \ No newline at end of file +} diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 4fddb0af7..9c52a09d2 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -1,34 +1,33 @@ -import UserRelatedState from "./UserRelatedState"; -import {Store, Stores, UIEventSource} from "../UIEventSource"; -import BaseLayer from "../../Models/BaseLayer"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import AvailableBaseLayers from "../Actors/AvailableBaseLayers"; -import Attribution from "../../UI/BigComponents/Attribution"; -import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; -import {Tiles} from "../../Models/TileRange"; -import BaseUIElement from "../../UI/BaseUIElement"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; -import {QueryParameters} from "../Web/QueryParameters"; -import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; -import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; -import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; -import {GeoOperations} from "../GeoOperations"; -import TitleHandler from "../Actors/TitleHandler"; -import {BBox} from "../BBox"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; -import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; -import {Tag} from "../Tags/Tag"; -import {OsmConnection} from "../Osm/OsmConnection"; - +import UserRelatedState from "./UserRelatedState" +import { Store, Stores, UIEventSource } from "../UIEventSource" +import BaseLayer from "../../Models/BaseLayer" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import AvailableBaseLayers from "../Actors/AvailableBaseLayers" +import Attribution from "../../UI/BigComponents/Attribution" +import Minimap, { MinimapObj } from "../../UI/Base/Minimap" +import { Tiles } from "../../Models/TileRange" +import BaseUIElement from "../../UI/BaseUIElement" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" +import { QueryParameters } from "../Web/QueryParameters" +import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer" +import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" +import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { GeoOperations } from "../GeoOperations" +import TitleHandler from "../Actors/TitleHandler" +import { BBox } from "../BBox" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource" +import { Translation, TypedTranslation } from "../../UI/i18n/Translation" +import { Tag } from "../Tags/Tag" +import { OsmConnection } from "../Osm/OsmConnection" export interface GlobalFilter { - filter: FilterState, - id: string, + filter: FilterState + id: string onNewPoint: { - safetyCheck: Translation, + safetyCheck: Translation confirmAddNew: TypedTranslation<{ preset: Translation }> tags: Tag[] } @@ -38,60 +37,64 @@ export interface GlobalFilter { * Contains all the leaflet-map related state */ export default class MapState extends UserRelatedState { - /** The leaflet instance of the big basemap */ - public leafletMap = new UIEventSource(undefined, "leafletmap"); + public leafletMap = new UIEventSource(undefined, "leafletmap") /** * A list of currently available background layers */ - public availableBackgroundLayers: Store; + public availableBackgroundLayers: Store /** * The current background layer */ - public backgroundLayer: UIEventSource; + public backgroundLayer: UIEventSource /** * Last location where a click was registered */ public readonly LastClickLocation: UIEventSource<{ - lat: number; - lon: number; - }> = new UIEventSource<{ lat: number; lon: number }>(undefined); + lat: number + lon: number + }> = new UIEventSource<{ lat: number; lon: number }>(undefined) /** * The bounds of the current map view */ - public currentView: FeatureSourceForLayer & Tiled; + public currentView: FeatureSourceForLayer & Tiled /** * The location as delivered by the GPS */ - public currentUserLocation: SimpleFeatureSource; + public currentUserLocation: SimpleFeatureSource /** * All previously visited points */ - public historicalUserLocations: SimpleFeatureSource; + public historicalUserLocations: SimpleFeatureSource /** * The number of seconds that the GPS-locations are stored in memory. * Time in seconds */ - public gpsLocationHistoryRetentionTime = new UIEventSource(7 * 24 * 60 * 60, "gps_location_retention") - public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled; + public gpsLocationHistoryRetentionTime = new UIEventSource( + 7 * 24 * 60 * 60, + "gps_location_retention" + ) + public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled /** * A feature source containing the current home location of the user */ public homeLocation: FeatureSourceForLayer & Tiled - public readonly mainMapObject: BaseUIElement & MinimapObj; - + public readonly mainMapObject: BaseUIElement & MinimapObj /** * Which layers are enabled in the current theme and what filters are applied onto them */ - public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); + public filteredLayers: UIEventSource = new UIEventSource( + [], + "filteredLayers" + ) /** * Filters which apply onto all layers @@ -101,31 +104,30 @@ export default class MapState extends UserRelatedState { /** * Which overlays are shown */ - public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource }[] - + public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource }[] constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { - super(layoutToUse, options); + super(layoutToUse, options) - this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); + this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl) let defaultLayer = AvailableBaseLayers.osmCarto - const available = this.availableBackgroundLayers.data; + const available = this.availableBackgroundLayers.data for (const layer of available) { if (this.backgroundLayerId.data === layer.id) { - defaultLayer = layer; + defaultLayer = layer } } const self = this this.backgroundLayer = new UIEventSource(defaultLayer) - this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id)) + this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id)) const attr = new Attribution( this.locationControl, this.osmConnection.userDetails, this.layoutToUse, this.currentBounds - ); + ) // Will write into this.leafletMap this.mainMapObject = Minimap.createMiniMap({ @@ -134,18 +136,23 @@ export default class MapState extends UserRelatedState { leafletMap: this.leafletMap, bounds: this.currentBounds, attribution: attr, - lastClickLocation: this.LastClickLocation + lastClickLocation: this.LastClickLocation, }) - - this.overlayToggles = this.layoutToUse?.tileLayerSources - ?.filter(c => c.name !== undefined) - ?.map(c => ({ - config: c, - isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown") - })) ?? [] - this.filteredLayers = new UIEventSource( MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)) - + this.overlayToggles = + this.layoutToUse?.tileLayerSources + ?.filter((c) => c.name !== undefined) + ?.map((c) => ({ + config: c, + isDisplayed: QueryParameters.GetBooleanQueryParameter( + "overlay-" + c.id, + c.defaultState, + "Wether or not the overlay " + c.id + " is shown" + ), + })) ?? [] + this.filteredLayers = new UIEventSource( + MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection) + ) this.lockBounds() this.AddAllOverlaysToMap(this.leafletMap) @@ -155,7 +162,7 @@ export default class MapState extends UserRelatedState { this.initUserLocationTrail() this.initCurrentView() - new TitleHandler(this); + new TitleHandler(this) } public AddAllOverlaysToMap(leafletMap: UIEventSource) { @@ -171,15 +178,14 @@ export default class MapState extends UserRelatedState { } new ShowOverlayLayer(tileLayerSource, leafletMap) } - } private lockBounds() { - const layout = this.layoutToUse; + const layout = this.layoutToUse if (!layout?.lockLocation) { - return; + return } - console.warn("Locking the bounds to ", layout.lockLocation); + console.warn("Locking the bounds to ", layout.lockLocation) this.mainMapObject.installBounds( new BBox(layout.lockLocation), this.featureSwitchIsTesting.data @@ -187,69 +193,82 @@ export default class MapState extends UserRelatedState { } private initCurrentView() { - let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "current_view")[0] + let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter( + (l) => l.layerDef.id === "current_view" + )[0] if (currentViewLayer === undefined) { // This layer is not needed by the theme and thus unloaded - return; + return } - let i = 0 - const self = this; - const features: Store<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { - if (bounds === undefined) { - return [] - } - i++ - const feature = { - freshness: new Date(), - feature: { - type: "Feature", - properties: { - id: "current_view-" + i, - "current_view": "yes", - "zoom": "" + self.locationControl.data.zoom - }, - geometry: { - type: "Polygon", - coordinates: [[ - [bounds.maxLon, bounds.maxLat], - [bounds.minLon, bounds.maxLat], - [bounds.minLon, bounds.minLat], - [bounds.maxLon, bounds.minLat], - [bounds.maxLon, bounds.maxLat], - ]] - } + const self = this + const features: Store<{ feature: any; freshness: Date }[]> = this.currentBounds.map( + (bounds) => { + if (bounds === undefined) { + return [] } + i++ + const feature = { + freshness: new Date(), + feature: { + type: "Feature", + properties: { + id: "current_view-" + i, + current_view: "yes", + zoom: "" + self.locationControl.data.zoom, + }, + geometry: { + type: "Polygon", + coordinates: [ + [ + [bounds.maxLon, bounds.maxLat], + [bounds.minLon, bounds.maxLat], + [bounds.minLon, bounds.minLat], + [bounds.maxLon, bounds.minLat], + [bounds.maxLon, bounds.maxLat], + ], + ], + }, + }, + } + return [feature] } - return [feature] - }) + ) - this.currentView = new TiledStaticFeatureSource(features, currentViewLayer); + this.currentView = new TiledStaticFeatureSource(features, currentViewLayer) } private initGpsLocation() { // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler - let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location")[0] + let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( + (l) => l.layerDef.id === "gps_location" + )[0] if (gpsLayerDef === undefined) { return } - this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)); + this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)) } private initUserLocationTrail() { - const features = LocalStorageSource.GetParsed<{ feature: any, freshness: Date }[]>("gps_location_history", []) + const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>( + "gps_location_history", + [] + ) const now = new Date().getTime() features.data = features.data - .map(ff => ({feature: ff.feature, freshness: new Date(ff.freshness)})) - .filter(ff => (now - ff.freshness.getTime()) < 1000 * this.gpsLocationHistoryRetentionTime.data) + .map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) })) + .filter( + (ff) => + now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data + ) features.ping() - const self = this; + const self = this let i = 0 this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { if (location === undefined) { - return; + return } const previousLocation = features.data[features.data.length - 1] @@ -261,30 +280,37 @@ export default class MapState extends UserRelatedState { let timeDiff = Number.MAX_VALUE // in seconds const olderLocation = features.data[features.data.length - 2] if (olderLocation !== undefined) { - timeDiff = (new Date(previousLocation.freshness).getTime() - new Date(olderLocation.freshness).getTime()) / 1000 + timeDiff = + (new Date(previousLocation.freshness).getTime() - + new Date(olderLocation.freshness).getTime()) / + 1000 } if (d < 20 && timeDiff < 60) { // Do not append changes less then 20m - it's probably noise anyway - return; + return } } const feature = JSON.parse(JSON.stringify(location.feature)) feature.properties.id = "gps/" + features.data.length i++ - features.data.push({feature, freshness: new Date()}) + features.data.push({ feature, freshness: new Date() }) features.ping() }) - - let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] + let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter( + (l) => l.layerDef.id === "gps_location_history" + )[0] if (gpsLayerDef !== undefined) { - this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); + this.historicalUserLocations = new SimpleFeatureSource( + gpsLayerDef, + Tiles.tile_index(0, 0, 0), + features + ) this.changes.setHistoricalUserLocations(this.historicalUserLocations) } - - const asLine = features.map(allPoints => { + const asLine = features.map((allPoints) => { if (allPoints === undefined || allPoints.length < 2) { return [] } @@ -292,136 +318,184 @@ export default class MapState extends UserRelatedState { const feature = { type: "Feature", properties: { - "id": "location_track", + id: "location_track", "_date:now": new Date().toISOString(), }, geometry: { type: "LineString", - coordinates: allPoints.map(ff => ff.feature.geometry.coordinates) - } + coordinates: allPoints.map((ff) => ff.feature.geometry.coordinates), + }, } self.allElements.ContainingFeatures.set(feature.properties.id, feature) - return [{ - feature, - freshness: new Date() - }] + return [ + { + feature, + freshness: new Date(), + }, + ] }) - let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0] + let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter( + (l) => l.layerDef.id === "gps_track" + )[0] if (gpsLineLayerDef !== undefined) { - this.historicalUserLocationsTrack = new TiledStaticFeatureSource(asLine, gpsLineLayerDef); + this.historicalUserLocationsTrack = new TiledStaticFeatureSource( + asLine, + gpsLineLayerDef + ) } } private initHomeLocation() { const empty = [] - const feature = Stores.ListStabilized(this.osmConnection.userDetails.map(userDetails => { - - if (userDetails === undefined) { - return undefined; - } - const home = userDetails.home; - if (home === undefined) { - return undefined; - } - return [home.lon, home.lat] - })).map(homeLonLat => { + const feature = Stores.ListStabilized( + this.osmConnection.userDetails.map((userDetails) => { + if (userDetails === undefined) { + return undefined + } + const home = userDetails.home + if (home === undefined) { + return undefined + } + return [home.lon, home.lat] + }) + ).map((homeLonLat) => { if (homeLonLat === undefined) { return empty } - return [{ - feature: { - "type": "Feature", - "properties": { - "id": "home", - "user:home": "yes", - "_lon": homeLonLat[0], - "_lat": homeLonLat[1] + return [ + { + feature: { + type: "Feature", + properties: { + id: "home", + "user:home": "yes", + _lon: homeLonLat[0], + _lat: homeLonLat[1], + }, + geometry: { + type: "Point", + coordinates: homeLonLat, + }, }, - "geometry": { - "type": "Point", - "coordinates": homeLonLat - } - }, freshness: new Date() - }] + freshness: new Date(), + }, + ] }) - const flayer = this.filteredLayers.data.filter(l => l.layerDef.id === "home_location")[0] + const flayer = this.filteredLayers.data.filter((l) => l.layerDef.id === "home_location")[0] if (flayer !== undefined) { this.homeLocation = new TiledStaticFeatureSource(feature, flayer) } - } - private static getPref(osmConnection: OsmConnection, key: string, layer: LayerConfig): UIEventSource { - return osmConnection - .GetPreference(key, layer.shownByDefault + "") - .sync(v => { + private static getPref( + osmConnection: OsmConnection, + key: string, + layer: LayerConfig + ): UIEventSource { + return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( + (v) => { if (v === undefined) { return undefined } - return v === "true"; - }, [], b => { + return v === "true" + }, + [], + (b) => { if (b === undefined) { return undefined } - return "" + b; - }) + return "" + b + } + ) } - public static InitializeFilteredLayers(layoutToUse: {layers: LayerConfig[], id: string}, osmConnection: OsmConnection): FilteredLayer[] { + public static InitializeFilteredLayers( + layoutToUse: { layers: LayerConfig[]; id: string }, + osmConnection: OsmConnection + ): FilteredLayer[] { if (layoutToUse === undefined) { return [] } - const flayers: FilteredLayer[] = []; + const flayers: FilteredLayer[] = [] for (const layer of layoutToUse.layers) { let isDisplayed: UIEventSource if (layer.syncSelection === "local") { - isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) + isDisplayed = LocalStorageSource.GetParsed( + layoutToUse.id + "-layer-" + layer.id + "-enabled", + layer.shownByDefault + ) } else if (layer.syncSelection === "theme-only") { - isDisplayed = MapState.getPref(osmConnection, layoutToUse.id + "-layer-" + layer.id + "-enabled", layer) + isDisplayed = MapState.getPref( + osmConnection, + layoutToUse.id + "-layer-" + layer.id + "-enabled", + layer + ) } else if (layer.syncSelection === "global") { - isDisplayed = MapState.getPref(osmConnection,"layer-" + layer.id + "-enabled", layer) + isDisplayed = MapState.getPref( + osmConnection, + "layer-" + layer.id + "-enabled", + layer + ) } else { - isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown") + isDisplayed = QueryParameters.GetBooleanQueryParameter( + "layer-" + layer.id, + layer.shownByDefault, + "Wether or not layer " + layer.id + " is shown" + ) } const flayer: FilteredLayer = { isDisplayed, layerDef: layer, - appliedFilters: new UIEventSource>(new Map()) - }; - layer.filters.forEach(filterConfig => { + appliedFilters: new UIEventSource>( + new Map() + ), + } + layer.filters.forEach((filterConfig) => { const stateSrc = filterConfig.initState() - stateSrc.addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) - flayer.appliedFilters.map(dict => dict.get(filterConfig.id)) - .addCallback(state => stateSrc.setData(state)) + stateSrc.addCallbackAndRun((state) => + flayer.appliedFilters.data.set(filterConfig.id, state) + ) + flayer.appliedFilters + .map((dict) => dict.get(filterConfig.id)) + .addCallback((state) => stateSrc.setData(state)) }) - flayers.push(flayer); + flayers.push(flayer) } for (const layer of layoutToUse.layers) { if (layer.filterIsSameAs === undefined) { continue } - const toReuse = flayers.find(l => l.layerDef.id === layer.filterIsSameAs) + const toReuse = flayers.find((l) => l.layerDef.id === layer.filterIsSameAs) if (toReuse === undefined) { - throw "Error in layer " + layer.id + ": it defines that it should be use the filters of " + layer.filterIsSameAs + ", but this layer was not loaded" + throw ( + "Error in layer " + + layer.id + + ": it defines that it should be use the filters of " + + layer.filterIsSameAs + + ", but this layer was not loaded" + ) } - console.warn("Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs) - const selfLayer = flayers.findIndex(l => l.layerDef.id === layer.id) + console.warn( + "Linking filter and isDisplayed-states of " + + layer.id + + " and " + + layer.filterIsSameAs + ) + const selfLayer = flayers.findIndex((l) => l.layerDef.id === layer.id) flayers[selfLayer] = { isDisplayed: toReuse.isDisplayed, layerDef: layer, - appliedFilters: toReuse.appliedFilters - }; + appliedFilters: toReuse.appliedFilters, + } } - return flayers; + return flayers } - - -} \ No newline at end of file +} diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index affa076e4..141ef72b3 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -1,50 +1,48 @@ -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {OsmConnection} from "../Osm/OsmConnection"; -import {MangroveIdentity} from "../Web/MangroveReviews"; -import {Store, UIEventSource} from "../UIEventSource"; -import {QueryParameters} from "../Web/QueryParameters"; -import {LocalStorageSource} from "../Web/LocalStorageSource"; -import {Utils} from "../../Utils"; -import Locale from "../../UI/i18n/Locale"; -import ElementsState from "./ElementsState"; -import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"; -import {Changes} from "../Osm/Changes"; -import ChangeToElementsActor from "../Actors/ChangeToElementsActor"; -import PendingChangesUploader from "../Actors/PendingChangesUploader"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { OsmConnection } from "../Osm/OsmConnection" +import { MangroveIdentity } from "../Web/MangroveReviews" +import { Store, UIEventSource } from "../UIEventSource" +import { QueryParameters } from "../Web/QueryParameters" +import { LocalStorageSource } from "../Web/LocalStorageSource" +import { Utils } from "../../Utils" +import Locale from "../../UI/i18n/Locale" +import ElementsState from "./ElementsState" +import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater" +import { Changes } from "../Osm/Changes" +import ChangeToElementsActor from "../Actors/ChangeToElementsActor" +import PendingChangesUploader from "../Actors/PendingChangesUploader" import * as translators from "../../assets/translators.json" -import Maproulette from "../Maproulette"; - +import Maproulette from "../Maproulette" + /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, * which layers they enabled, ... */ export default class UserRelatedState extends ElementsState { - - /** The user credentials */ - public osmConnection: OsmConnection; + public osmConnection: OsmConnection /** THe change handler */ - public changes: Changes; + public changes: Changes /** * The key for mangrove */ - public mangroveIdentity: MangroveIdentity; + public mangroveIdentity: MangroveIdentity /** * Maproulette connection */ - public maprouletteConnection: Maproulette; + public maprouletteConnection: Maproulette + + public readonly isTranslator: Store - public readonly isTranslator : Store; - public readonly installedUserThemes: Store - + constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { - super(layoutToUse); + super(layoutToUse) this.osmConnection = new OsmConnection({ dryRun: this.featureSwitchIsTesting, @@ -54,138 +52,147 @@ export default class UserRelatedState extends ElementsState { undefined, "Used to complete the login" ), - osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, - attemptLogin: options?.attemptLogin + osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data, + attemptLogin: options?.attemptLogin, }) - const translationMode = this.osmConnection.GetPreference("translation-mode").sync(str => str === undefined ? undefined : str === "true", [], b => b === undefined ? undefined : b+"") - + const translationMode = this.osmConnection.GetPreference("translation-mode").sync( + (str) => (str === undefined ? undefined : str === "true"), + [], + (b) => (b === undefined ? undefined : b + "") + ) + translationMode.syncWith(Locale.showLinkToWeblate) - - this.isTranslator = this.osmConnection.userDetails.map(ud => { - if(!ud.loggedIn){ - return false; + + this.isTranslator = this.osmConnection.userDetails.map((ud) => { + if (!ud.loggedIn) { + return false } - const name= ud.name.toLowerCase().replace(/\s+/g, '') - return translators.contributors.some(c => c.contributor.toLowerCase().replace(/\s+/g, '') === name) + const name = ud.name.toLowerCase().replace(/\s+/g, "") + return translators.contributors.some( + (c) => c.contributor.toLowerCase().replace(/\s+/g, "") === name + ) }) - - this.isTranslator.addCallbackAndRunD(ud => { - if(ud){ + + this.isTranslator.addCallbackAndRunD((ud) => { + if (ud) { Locale.showLinkToWeblate.setData(true) } - }); - + }) + this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) - new ChangeToElementsActor(this.changes, this.allElements) - new PendingChangesUploader(this.changes, this.selectedElement); - + new PendingChangesUploader(this.changes, this.selectedElement) + this.mangroveIdentity = new MangroveIdentity( this.osmConnection.GetLongPreference("identity", "mangrove") - ); + ) - this.maprouletteConnection = new Maproulette(); + this.maprouletteConnection = new Maproulette() if (layoutToUse?.hideFromOverview) { - this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => { + this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => { if (loggedIn) { this.osmConnection .GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled") - .setData("true"); - return true; + .setData("true") + return true } }) } if (this.layoutToUse !== undefined && !this.layoutToUse.official) { console.log("Marking unofficial theme as visited") - this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id) - .setData(JSON.stringify({ + this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData( + JSON.stringify({ id: this.layoutToUse.id, icon: this.layoutToUse.icon, title: this.layoutToUse.title.translations, shortDescription: this.layoutToUse.shortDescription.translations, - definition: this.layoutToUse["definition"] - })) + definition: this.layoutToUse["definition"], + }) + ) } - this.InitializeLanguage(); + this.InitializeLanguage() new SelectedElementTagsUpdater(this) - this.installedUserThemes = this.InitInstalledUserThemes(); - + this.installedUserThemes = this.InitInstalledUserThemes() } private InitializeLanguage() { - const layoutToUse = this.layoutToUse; - Locale.language.syncWith(this.osmConnection.GetPreference("language")); - Locale.language - .addCallback((currentLanguage) => { - if (layoutToUse === undefined) { - return; - } - if(Locale.showLinkToWeblate.data){ - return true; // Disable auto switching as we are in translators mode - } - if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { - console.log( - "Resetting language to", - layoutToUse.language[0], - "as", - currentLanguage, - " is unsupported" - ); - // The current language is not supported -> switch to a supported one - Locale.language.setData(layoutToUse.language[0]); - } - }) - Locale.language.ping(); + const layoutToUse = this.layoutToUse + Locale.language.syncWith(this.osmConnection.GetPreference("language")) + Locale.language.addCallback((currentLanguage) => { + if (layoutToUse === undefined) { + return + } + if (Locale.showLinkToWeblate.data) { + return true // Disable auto switching as we are in translators mode + } + if (this.layoutToUse.language.indexOf(currentLanguage) < 0) { + console.log( + "Resetting language to", + layoutToUse.language[0], + "as", + currentLanguage, + " is unsupported" + ) + // The current language is not supported -> switch to a supported one + Locale.language.setData(layoutToUse.language[0]) + } + }) + Locale.language.ping() } - - private InitInstalledUserThemes(): Store{ - const prefix = "mapcomplete-unofficial-theme-"; + + private InitInstalledUserThemes(): Store { + const prefix = "mapcomplete-unofficial-theme-" const postfix = "-combined-length" - return this.osmConnection.preferencesHandler.preferences.map(prefs => + return this.osmConnection.preferencesHandler.preferences.map((prefs) => Object.keys(prefs) - .filter(k => k.startsWith(prefix) && k.endsWith(postfix)) - .map(k => k.substring(prefix.length, k.length - postfix.length)) + .filter((k) => k.startsWith(prefix) && k.endsWith(postfix)) + .map((k) => k.substring(prefix.length, k.length - postfix.length)) ) } - - public GetUnofficialTheme(id: string): { - id: string - icon: string, - title: any, - shortDescription: any, - definition?: any, - isOfficial: boolean - } | undefined { + + public GetUnofficialTheme(id: string): + | { + id: string + icon: string + title: any + shortDescription: any + definition?: any + isOfficial: boolean + } + | undefined { console.log("GETTING UNOFFICIAL THEME") - const pref = this.osmConnection.GetLongPreference("unofficial-theme-"+id) + const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) const str = pref.data - + if (str === undefined || str === "undefined" || str === "") { pref.setData(null) return undefined } - + try { const value: { id: string - icon: string, - title: any, - shortDescription: any, - definition?: any, + icon: string + title: any + shortDescription: any + definition?: any isOfficial: boolean } = JSON.parse(str) value.isOfficial = false - return value; + return value } catch (e) { - console.warn("Removing theme " + id + " as it could not be parsed from the preferences; the content is:", str) + console.warn( + "Removing theme " + + id + + " as it could not be parsed from the preferences; the content is:", + str + ) pref.setData(null) return undefined } - } - -} \ No newline at end of file +} diff --git a/Logic/Tags/And.ts b/Logic/Tags/And.ts index e98e92e71..771dffe53 100644 --- a/Logic/Tags/And.ts +++ b/Logic/Tags/And.ts @@ -1,15 +1,14 @@ -import {TagsFilter} from "./TagsFilter"; -import {Or} from "./Or"; -import {TagUtils} from "./TagUtils"; -import {Tag} from "./Tag"; -import {RegexTag} from "./RegexTag"; +import { TagsFilter } from "./TagsFilter" +import { Or } from "./Or" +import { TagUtils } from "./TagUtils" +import { Tag } from "./Tag" +import { RegexTag } from "./RegexTag" export class And extends TagsFilter { - public and: TagsFilter[] constructor(and: TagsFilter[]) { - super(); + super() this.and = and } @@ -21,11 +20,11 @@ export class And extends TagsFilter { } private static combine(filter: string, choices: string[]): string[] { - const values = []; + const values = [] for (const or of choices) { - values.push(filter + or); + values.push(filter + or) } - return values; + return values } normalize() { @@ -43,11 +42,11 @@ export class And extends TagsFilter { matchesProperties(tags: any): boolean { for (const tagsFilter of this.and) { if (!tagsFilter.matchesProperties(tags)) { - return false; + return false } } - return true; + return true } /** @@ -56,36 +55,37 @@ export class And extends TagsFilter { * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ] */ asOverpass(): string[] { - let allChoices: string[] = null; + let allChoices: string[] = null for (const andElement of this.and) { - const andElementFilter = andElement.asOverpass(); + const andElementFilter = andElement.asOverpass() if (allChoices === null) { - allChoices = andElementFilter; - continue; + allChoices = andElementFilter + continue } - const newChoices: string[] = []; + const newChoices: string[] = [] for (const choice of allChoices) { - newChoices.push( - ...And.combine(choice, andElementFilter) - ) + newChoices.push(...And.combine(choice, andElementFilter)) } - allChoices = newChoices; + allChoices = newChoices } - return allChoices; + return allChoices } asHumanString(linkToWiki: boolean, shorten: boolean, properties) { - return this.and.map(t => t.asHumanString(linkToWiki, shorten, properties)).filter(x => x !== "").join("&"); + return this.and + .map((t) => t.asHumanString(linkToWiki, shorten, properties)) + .filter((x) => x !== "") + .join("&") } isUsableAsAnswer(): boolean { for (const t of this.and) { if (!t.isUsableAsAnswer()) { - return false; + return false } } - return true; + return true } /** @@ -107,45 +107,44 @@ export class And extends TagsFilter { */ shadows(other: TagsFilter): boolean { if (!(other instanceof And)) { - return false; + return false } for (const selfTag of this.and) { - let matchFound = false; + let matchFound = false for (const otherTag of other.and) { - matchFound = selfTag.shadows(otherTag); + matchFound = selfTag.shadows(otherTag) if (matchFound) { - break; + break } } if (!matchFound) { - return false; + return false } } for (const otherTag of other.and) { - let matchFound = false; + let matchFound = false for (const selfTag of this.and) { - matchFound = selfTag.shadows(otherTag); + matchFound = selfTag.shadows(otherTag) if (matchFound) { - break; + break } } if (!matchFound) { - return false; + return false } } - - return true; + return true } usedKeys(): string[] { - return [].concat(...this.and.map(subkeys => subkeys.usedKeys())); + return [].concat(...this.and.map((subkeys) => subkeys.usedKeys())) } usedTags(): { key: string; value: string }[] { - return [].concat(...this.and.map(subkeys => subkeys.usedTags())); + return [].concat(...this.and.map((subkeys) => subkeys.usedTags())) } asChange(properties: any): { k: string; v: string }[] { @@ -153,7 +152,7 @@ export class And extends TagsFilter { for (const tagsFilter of this.and) { result.push(...tagsFilter.asChange(properties)) } - return result; + return result } /** @@ -187,7 +186,7 @@ export class And extends TagsFilter { continue } if (r === false) { - return false; + return false } newAnds.push(r) continue @@ -203,7 +202,6 @@ export class And extends TagsFilter { continue } if (!value && tag.shadows(knownExpression)) { - /** * We know that knownExpression is unmet. * if the tag shadows 'knownExpression' (which is the case when control flows gets here), @@ -228,49 +226,50 @@ export class And extends TagsFilter { if (this.and.length === 0) { return true } - const optimizedRaw = this.and.map(t => t.optimize()) - .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/) - if (optimizedRaw.some(t => t === false)) { + const optimizedRaw = this.and + .map((t) => t.optimize()) + .filter((t) => t !== true /* true is the neutral element in an AND, we drop them*/) + if (optimizedRaw.some((t) => t === false)) { // We have an AND with a contained false: this is always 'false' - return false; + return false } - const optimized = optimizedRaw; + const optimized = optimizedRaw { // Conflicting keys do return false - const properties: object = {} + const properties: object = {} for (const opt of optimized) { if (opt instanceof Tag) { properties[opt.key] = opt.value } } - for (const opt of optimized) { - if(opt instanceof Tag ){ - const k = opt.key - const v = properties[k] - if(v === undefined){ - continue - } - if(v !== opt.value){ - // detected an internal conflict - return false - } - } - if(opt instanceof RegexTag ){ - const k = opt.key - if(typeof k !== "string"){ - continue - } - const v = properties[k] - if(v === undefined){ - continue - } - if(v !== opt.value){ - // detected an internal conflict - return false - } - } - } + for (const opt of optimized) { + if (opt instanceof Tag) { + const k = opt.key + const v = properties[k] + if (v === undefined) { + continue + } + if (v !== opt.value) { + // detected an internal conflict + return false + } + } + if (opt instanceof RegexTag) { + const k = opt.key + if (typeof k !== "string") { + continue + } + const v = properties[k] + if (v === undefined) { + continue + } + if (v !== opt.value) { + // detected an internal conflict + return false + } + } + } } const newAnds: TagsFilter[] = [] @@ -287,7 +286,7 @@ export class And extends TagsFilter { } { - let dirty = false; + let dirty = false do { const cleanedContainedOrs: Or[] = [] outer: for (let containedOr of containedOrs) { @@ -310,8 +309,8 @@ export class And extends TagsFilter { } // the 'or' dissolved into a normal tag -> it has to be added to the newAnds newAnds.push(cleaned) - dirty = true; // rerun this algo later on - continue outer; + dirty = true // rerun this algo later on + continue outer } cleanedContainedOrs.push(containedOr) } @@ -319,30 +318,32 @@ export class And extends TagsFilter { } while (dirty) } - - containedOrs = containedOrs.filter(ca => { + containedOrs = containedOrs.filter((ca) => { const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) // If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all // XY & (XY | AB) === XY - return !isShadowed; + return !isShadowed }) // Extract common keys from the OR if (containedOrs.length === 1) { newAnds.push(containedOrs[0]) } else if (containedOrs.length > 1) { - let commonValues: TagsFilter [] = containedOrs[0].or + let commonValues: TagsFilter[] = containedOrs[0].or for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) { - const containedOr = containedOrs[i]; - commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) + const containedOr = containedOrs[i] + commonValues = commonValues.filter((cv) => + containedOr.or.some((candidate) => candidate.shadows(cv)) + ) } if (commonValues.length === 0) { newAnds.push(...containedOrs) } else { const newOrs: TagsFilter[] = [] for (const containedOr of containedOrs) { - const elements = containedOr.or - .filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) + const elements = containedOr.or.filter( + (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) + ) newOrs.push(Or.construct(elements)) } @@ -371,12 +372,11 @@ export class And extends TagsFilter { } isNegative(): boolean { - return !this.and.some(t => !t.isNegative()); + return !this.and.some((t) => !t.isNegative()) } visit(f: (TagsFilter: any) => void) { f(this) - this.and.forEach(sub => sub.visit(f)) + this.and.forEach((sub) => sub.visit(f)) } - -} \ No newline at end of file +} diff --git a/Logic/Tags/ComparingTag.ts b/Logic/Tags/ComparingTag.ts index 8abbdbb16..db66a1fdc 100644 --- a/Logic/Tags/ComparingTag.ts +++ b/Logic/Tags/ComparingTag.ts @@ -1,14 +1,18 @@ -import {TagsFilter} from "./TagsFilter"; +import { TagsFilter } from "./TagsFilter" export default class ComparingTag implements TagsFilter { - private readonly _key: string; - private readonly _predicate: (value: string) => boolean; - private readonly _representation: string; + private readonly _key: string + private readonly _predicate: (value: string) => boolean + private readonly _representation: string - constructor(key: string, predicate: (value: string | undefined) => boolean, representation: string = "") { - this._key = key; - this._predicate = predicate; - this._representation = representation; + constructor( + key: string, + predicate: (value: string | undefined) => boolean, + representation: string = "" + ) { + this._key = key + this._predicate = predicate + this._representation = representation } asChange(properties: any): { k: string; v: string }[] { @@ -24,16 +28,16 @@ export default class ComparingTag implements TagsFilter { } shadows(other: TagsFilter): boolean { - return other === this; + return other === this } isUsableAsAnswer(): boolean { - return false; + return false } /** * Checks if the properties match - * + * * const t = new ComparingTag("key", (x => Number(x) < 42)) * t.matchesProperties({key: 42}) // => false * t.matchesProperties({key: 41}) // => true @@ -41,26 +45,26 @@ export default class ComparingTag implements TagsFilter { * t.matchesProperties({differentKey: 42}) // => false */ matchesProperties(properties: any): boolean { - return this._predicate(properties[this._key]); + return this._predicate(properties[this._key]) } usedKeys(): string[] { - return [this._key]; + return [this._key] } - + usedTags(): { key: string; value: string }[] { - return []; + return [] } optimize(): TagsFilter | boolean { - return this; + return this } - + isNegative(): boolean { - return true; + return true } - + visit(f: (TagsFilter) => void) { f(this) } -} \ No newline at end of file +} diff --git a/Logic/Tags/Or.ts b/Logic/Tags/Or.ts index d8b4feda7..d2cca35a1 100644 --- a/Logic/Tags/Or.ts +++ b/Logic/Tags/Or.ts @@ -1,88 +1,85 @@ -import {TagsFilter} from "./TagsFilter"; -import {TagUtils} from "./TagUtils"; -import {And} from "./And"; - +import { TagsFilter } from "./TagsFilter" +import { TagUtils } from "./TagUtils" +import { And } from "./And" export class Or extends TagsFilter { public or: TagsFilter[] constructor(or: TagsFilter[]) { - super(); - this.or = or; + super() + this.or = or } - public static construct(or: TagsFilter[]): TagsFilter{ - if(or.length === 1){ + public static construct(or: TagsFilter[]): TagsFilter { + if (or.length === 1) { return or[0] } return new Or(or) } - matchesProperties(properties: any): boolean { for (const tagsFilter of this.or) { if (tagsFilter.matchesProperties(properties)) { - return true; + return true } } - return false; + return false } /** * * import {Tag} from "./Tag"; * import {RegexTag} from "./RegexTag"; - * + * * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) * const or = new Or([and, new Tag("leisure", "nature_reserve"]) * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]", "[\"leisure\"=\"nature_reserve\"]" ] - * + * * // should fuse nested ors into a single list * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) * or.asOverpass() // => [ `["key"="value"]`, `["key1"="value1"]`, `["key2"="value2"]` ] */ asOverpass(): string[] { - const choices = []; + const choices = [] for (const tagsFilter of this.or) { - const subChoices = tagsFilter.asOverpass(); + const subChoices = tagsFilter.asOverpass() choices.push(...subChoices) } - return choices; + return choices } asHumanString(linkToWiki: boolean, shorten: boolean, properties) { - return this.or.map(t => t.asHumanString(linkToWiki, shorten, properties)).join("|"); + return this.or.map((t) => t.asHumanString(linkToWiki, shorten, properties)).join("|") } isUsableAsAnswer(): boolean { - return false; + return false } shadows(other: TagsFilter): boolean { if (other instanceof Or) { - for (const selfTag of this.or) { - let matchFound = false; + let matchFound = false for (let i = 0; i < other.or.length && !matchFound; i++) { - let otherTag = other.or[i]; - matchFound = selfTag.shadows(otherTag); + let otherTag = other.or[i] + matchFound = selfTag.shadows(otherTag) } if (!matchFound) { - return false; + return false } } - return true; + return true } - return false; + return false } usedKeys(): string[] { - return [].concat(...this.or.map(subkeys => subkeys.usedKeys())); + return [].concat(...this.or.map((subkeys) => subkeys.usedKeys())) } usedTags(): { key: string; value: string }[] { - return [].concat(...this.or.map(subkeys => subkeys.usedTags())); + return [].concat(...this.or.map((subkeys) => subkeys.usedTags())) } asChange(properties: any): { k: string; v: string }[] { @@ -90,7 +87,7 @@ export class Or extends TagsFilter { for (const tagsFilter of this.or) { result.push(...tagsFilter.asChange(properties)) } - return result; + return result } /** @@ -99,7 +96,7 @@ export class Or extends TagsFilter { * ^---------^ * When the evaluation hits (A=B | X=Y), we know _for sure_ that X=Y _does match, as it would have failed the first clause otherwise. * This means we can safely ignore this in the OR - * + * * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // =>true * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => new Tag("other_key","value") * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true @@ -109,21 +106,21 @@ export class Or extends TagsFilter { removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { const newOrs: TagsFilter[] = [] for (const tag of this.or) { - if(tag instanceof Or){ + if (tag instanceof Or) { throw "Optimize expressions before using removePhraseConsideredKnown" } - if(tag instanceof And){ + if (tag instanceof And) { const r = tag.removePhraseConsideredKnown(knownExpression, value) - if(r === false){ + if (r === false) { continue } - if(r === true){ - return true; + if (r === true) { + return true } newOrs.push(r) continue } - if(value && knownExpression.shadows(tag)){ + if (value && knownExpression.shadows(tag)) { /** * At this point, we do know that 'knownExpression' is true in every case * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, @@ -131,10 +128,9 @@ export class Or extends TagsFilter { * * "True" is the absorbing element in an OR, so we can return true */ - return true; + return true } - if(!value && tag.shadows(knownExpression)){ - + if (!value && tag.shadows(knownExpression)) { /** * We know that knownExpression is unmet. * if the tag shadows 'knownExpression' (which is the case when control flows gets here), @@ -143,49 +139,48 @@ export class Or extends TagsFilter { * This implies that 'tag' must be false too! * false is the neutral element in an OR */ - continue + continue } newOrs.push(tag) } - if(newOrs.length === 0){ + if (newOrs.length === 0) { return false } return Or.construct(newOrs) } - - optimize(): TagsFilter | boolean { - - if(this.or.length === 0){ - return false; - } - - const optimizedRaw = this.or.map(t => t.optimize()) - .filter(t => t !== false /* false is the neutral element in an OR, we drop them*/ ) - if(optimizedRaw.some(t => t === true)){ - // We have an OR with a contained true: this is always 'true' - return true; - } - const optimized = optimizedRaw; - - const newOrs : TagsFilter[] = [] - let containedAnds : And[] = [] + optimize(): TagsFilter | boolean { + if (this.or.length === 0) { + return false + } + + const optimizedRaw = this.or + .map((t) => t.optimize()) + .filter((t) => t !== false /* false is the neutral element in an OR, we drop them*/) + if (optimizedRaw.some((t) => t === true)) { + // We have an OR with a contained true: this is always 'true' + return true + } + const optimized = optimizedRaw + + const newOrs: TagsFilter[] = [] + let containedAnds: And[] = [] for (const tf of optimized) { - if(tf instanceof Or){ + if (tf instanceof Or) { // expand all the nested ors... newOrs.push(...tf.or) - }else if(tf instanceof And){ + } else if (tf instanceof And) { // partition of all the ands containedAnds.push(tf) } else { newOrs.push(tf) } } - + { - let dirty = false; + let dirty = false do { - const cleanedContainedANds : And[] = [] + const cleanedContainedANds: And[] = [] outer: for (let containedAnd of containedAnds) { for (const known of newOrs) { // input for optimazation: (K=V | (X=Y & K=V)) @@ -206,66 +201,67 @@ export class Or extends TagsFilter { } // the 'and' dissolved into a normal tag -> it has to be added to the newOrs newOrs.push(cleaned) - dirty = true; // rerun this algo later on - continue outer; + dirty = true // rerun this algo later on + continue outer } cleanedContainedANds.push(containedAnd) } containedAnds = cleanedContainedANds - } while(dirty) + } while (dirty) } // Extract common keys from the ANDS - if(containedAnds.length === 1){ + if (containedAnds.length === 1) { newOrs.push(containedAnds[0]) - } else if(containedAnds.length > 1){ - let commonValues : TagsFilter [] = containedAnds[0].and - for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ - const containedAnd = containedAnds[i]; - commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.shadows(cv))) + } else if (containedAnds.length > 1) { + let commonValues: TagsFilter[] = containedAnds[0].and + for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++) { + const containedAnd = containedAnds[i] + commonValues = commonValues.filter((cv) => + containedAnd.and.some((candidate) => candidate.shadows(cv)) + ) } - if(commonValues.length === 0){ + if (commonValues.length === 0) { newOrs.push(...containedAnds) - }else{ + } else { const newAnds: TagsFilter[] = [] for (const containedAnd of containedAnds) { - const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) + const elements = containedAnd.and.filter( + (candidate) => !commonValues.some((cv) => cv.shadows(candidate)) + ) newAnds.push(And.construct(elements)) } commonValues.push(Or.construct(newAnds)) const result = new And(commonValues).optimize() - if(result === true){ + if (result === true) { return true - }else if(result === false){ + } else if (result === false) { // neutral element: skip - }else{ + } else { newOrs.push(And.construct(commonValues)) } } } - if(newOrs.length === 0){ + if (newOrs.length === 0) { return false } - if(TagUtils.ContainsOppositeTags(newOrs)){ + if (TagUtils.ContainsOppositeTags(newOrs)) { return true } - + TagUtils.sortFilters(newOrs, false) return Or.construct(newOrs) } - + isNegative(): boolean { - return this.or.some(t => t.isNegative()); + return this.or.some((t) => t.isNegative()) } visit(f: (TagsFilter: any) => void) { f(this) - this.or.forEach(t => t.visit(f)) + this.or.forEach((t) => t.visit(f)) } - } - - diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index 91dfb06a0..e68e89b09 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -1,88 +1,87 @@ -import {Tag} from "./Tag"; -import {TagsFilter} from "./TagsFilter"; +import { Tag } from "./Tag" +import { TagsFilter } from "./TagsFilter" export class RegexTag extends TagsFilter { - public readonly key: RegExp | string; - public readonly value: RegExp | string; - public readonly invert: boolean; + public readonly key: RegExp | string + public readonly value: RegExp | string + public readonly invert: boolean public readonly matchesEmpty: boolean constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { - super(); - this.key = key; - this.value = value; - this.invert = invert; - this.matchesEmpty = RegexTag.doesMatch("", this.value); + super() + this.key = key + this.value = value + this.invert = invert + this.matchesEmpty = RegexTag.doesMatch("", this.value) } private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean { if (fromTag === undefined) { - return; + return } if (typeof fromTag === "number") { - fromTag = "" + fromTag; + fromTag = "" + fromTag } if (typeof possibleRegex === "string") { - return fromTag === possibleRegex; + return fromTag === possibleRegex } - return fromTag.match(possibleRegex) !== null; + return fromTag.match(possibleRegex) !== null } private static source(r: string | RegExp) { - if (typeof (r) === "string") { - return r; + if (typeof r === "string") { + return r } - return r.source; + return r.source } /** * new RegexTag("a", /^[xyz]$/).asOverpass() // => [ `["a"~"^[xyz]$"]` ] - * + * * // A wildcard regextag should only give the key * new RegexTag("a", /^..*$/).asOverpass() // => [ `["a"]` ] - * + * * // A regextag with a regex key should give correct output * new RegexTag(/a.*x/, /^..*$/).asOverpass() // => [ `[~"a.*x"~\"^..*$\"]` ] - * + * * // A regextag with a case invariant flag should signal this to overpass * new RegexTag("key", /^.*value.*$/i).asOverpass() // => [ `["key"~\"^.*value.*$\",i]` ] */ asOverpass(): string[] { - const inv =this.invert ? "!" : "" + const inv = this.invert ? "!" : "" if (typeof this.key !== "string") { // The key is a regex too - return [`[~"${this.key.source}"${inv}~"${RegexTag.source(this.value)}"]`]; + return [`[~"${this.key.source}"${inv}~"${RegexTag.source(this.value)}"]`] } - - if(this.value instanceof RegExp){ - const src =this.value.source - if(src === "^..*$"){ + + if (this.value instanceof RegExp) { + const src = this.value.source + if (src === "^..*$") { // anything goes return [`[${inv}"${this.key}"]`] } const modifier = this.value.ignoreCase ? ",i" : "" return [`["${this.key}"${inv}~"${src}"${modifier}]`] - }else{ + } else { // Normal key and normal value - return [`["${this.key}"${inv}="${this.value}"]`]; + return [`["${this.key}"${inv}="${this.value}"]`] } - } isUsableAsAnswer(): boolean { - return false; + return false } - /** + /** * Checks if this tag matches the given properties - * + * * const isNotEmpty = new RegexTag("key",/^$/, true); * isNotEmpty.matchesProperties({"key": "value"}) // => true * isNotEmpty.matchesProperties({"key": "other_value"}) // => true * isNotEmpty.matchesProperties({"key": ""}) // => false * isNotEmpty.matchesProperties({"other_key": ""}) // => false * isNotEmpty.matchesProperties({"other_key": "value"}) // => false - * + * * const isNotEmpty = new RegexTag("key",/^..*$/, true); * isNotEmpty.matchesProperties({"key": "value"}) // => false * isNotEmpty.matchesProperties({"key": "other_value"}) // => false @@ -95,7 +94,7 @@ export class RegexTag extends TagsFilter { * notRegex.matchesProperties({"x": "z"}) // => true * notRegex.matchesProperties({"x": ""}) // => true * notRegex.matchesProperties({}) // => true - * + * * const bicycleTubeRegex = new RegexTag("vending", /^.*bicycle_tube.*$/) * bicycleTubeRegex.matchesProperties({"vending": "bicycle_tube"}) // => true * bicycleTubeRegex.matchesProperties({"vending": "something;bicycle_tube"}) // => true @@ -112,59 +111,59 @@ export class RegexTag extends TagsFilter { * notEmptyList.matchesProperties({"xyz": undefined}) // => true * notEmptyList.matchesProperties({"xyz": "[]"}) // => false * notEmptyList.matchesProperties({"xyz": "[\"abc\"]"}) // => true - * + * * const importMatch = new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/) * importMatch.matchesProperties({"tags": "amenity=public_bookcase;name=test"}) // =>true * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false - * + * * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true */ matchesProperties(tags: any): boolean { if (typeof this.key === "string") { const value = tags[this.key] ?? "" - return RegexTag.doesMatch(value, this.value) != this.invert; + return RegexTag.doesMatch(value, this.value) != this.invert } for (const key in tags) { if (key === undefined) { - continue; + continue } if (RegexTag.doesMatch(key, this.key)) { - const value = tags[key] ?? ""; - return RegexTag.doesMatch(value, this.value) != this.invert; + const value = tags[key] ?? "" + return RegexTag.doesMatch(value, this.value) != this.invert } } if (this.matchesEmpty) { // The value is 'empty' - return !this.invert; + return !this.invert } // The matching key was not found - return this.invert; + return this.invert } asHumanString() { if (typeof this.key === "string") { const oper = typeof this.value === "string" ? "=" : "~" - return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}`; + return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}` } return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` } /** - * + * * new RegexTag("key","value").shadows(new Tag("key","value")) // => true * new RegexTag("key",/value/).shadows(new RegexTag("key","value")) // => true * new RegexTag("key",/^..*$/).shadows(new Tag("key","value")) // => false * new RegexTag("key",/^..*$/).shadows(new Tag("other_key","value")) // => false * new RegexTag("key", /^a+$/).shadows(new Tag("key", "a")) // => false - * - * + * + * * // should not shadow too eagerly: the first tag might match 'key=abc', the second won't * new RegexTag("key", /^..*$/).shadows(new Tag("key", "some_value")) // => false - * + * * // should handle 'invert' * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","value")) // => false * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true @@ -173,50 +172,51 @@ export class RegexTag extends TagsFilter { */ shadows(other: TagsFilter): boolean { if (other instanceof RegexTag) { - if((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key) ){ + if ((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key)) { // Keys don't match, never shadowing return false } - if((other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && this.invert == other.invert ){ + if ( + (other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && + this.invert == other.invert + ) { // Values (and inverts) match return true } - if(typeof other.value ==="string"){ + if (typeof other.value === "string") { const valuesMatch = RegexTag.doesMatch(other.value, this.value) - if(!this.invert && !other.invert){ + if (!this.invert && !other.invert) { // this: key~value, other: key=value return valuesMatch } - if(this.invert && !other.invert){ + if (this.invert && !other.invert) { // this: key!~value, other: key=value return !valuesMatch } - if(!this.invert && other.invert){ + if (!this.invert && other.invert) { // this: key~value, other: key!=value return !valuesMatch } - if(!this.invert && !other.invert){ + if (!this.invert && !other.invert) { // this: key!~value, other: key!=value return valuesMatch } - } - return false; + return false } if (other instanceof Tag) { - if(!RegexTag.doesMatch(other.key, this.key)){ + if (!RegexTag.doesMatch(other.key, this.key)) { // Keys don't match - return false; + return false } - - - if(this.value["source"] === "^..*$") { - if(this.invert){ + + if (this.value["source"] === "^..*$") { + if (this.invert) { return other.value === "" } return false } - + if (this.invert) { /* * this: "a!=b" @@ -224,23 +224,23 @@ export class RegexTag extends TagsFilter { * actual property: a=x * In other words: shadowing will never occur here */ - return false; + return false } // Unless the values are the same, it is pretty hard to figure out if they are shadowing. This is future work - return (this.value["source"] ?? this.value) === other.value; + return (this.value["source"] ?? this.value) === other.value } - return false; + return false } usedKeys(): string[] { if (typeof this.key === "string") { - return [this.key]; + return [this.key] } throw "Key cannot be determined as it is a regex" } - + usedTags(): { key: string; value: string }[] { - return []; + return [] } asChange(properties: any): { k: string; v: string }[] { @@ -249,26 +249,26 @@ export class RegexTag extends TagsFilter { } if (typeof this.key === "string") { if (typeof this.value === "string") { - return [{k: this.key, v: this.value}] + return [{ k: this.key, v: this.value }] } if (this.value.toString() != "/^..*$/") { console.warn("Regex value in tag; using wildcard:", this.key, this.value) } - return [{k: this.key, v: undefined}] + return [{ k: this.key, v: undefined }] } console.error("Cannot export regex tag to asChange; ", this.key, this.value) return [] } optimize(): TagsFilter | boolean { - return this; + return this } - + isNegative(): boolean { - return this.invert; + return this.invert } - + visit(f: (TagsFilter) => void) { f(this) } -} \ No newline at end of file +} diff --git a/Logic/Tags/SubstitutingTag.ts b/Logic/Tags/SubstitutingTag.ts index d121aaab5..50ea30193 100644 --- a/Logic/Tags/SubstitutingTag.ts +++ b/Logic/Tags/SubstitutingTag.ts @@ -1,6 +1,6 @@ -import {TagsFilter} from "./TagsFilter"; -import {Tag} from "./Tag"; -import {Utils} from "../../Utils"; +import { TagsFilter } from "./TagsFilter" +import { Tag } from "./Tag" +import { Utils } from "../../Utils" /** * The substituting-tag uses the tags of a feature a variables and replaces them. @@ -12,32 +12,37 @@ import {Utils} from "../../Utils"; * This cannot be used to query features */ export default class SubstitutingTag implements TagsFilter { - private readonly _key: string; - private readonly _value: string; + private readonly _key: string + private readonly _value: string private readonly _invert: boolean constructor(key: string, value: string, invert = false) { - this._key = key; - this._value = value; + this._key = key + this._value = value this._invert = invert } private static substituteString(template: string, dict: any): string { for (const k in dict) { - template = template.replace(new RegExp("\\{" + k + "\\}", 'g'), dict[k]) + template = template.replace(new RegExp("\\{" + k + "\\}", "g"), dict[k]) } - return template.replace(/{.*}/g, ""); + return template.replace(/{.*}/g, "") } - asTag(currentProperties: Record){ - if(this._invert){ + asTag(currentProperties: Record) { + if (this._invert) { throw "Cannot convert an inverted substituting tag" } return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties)) } - + asHumanString(linkToWiki: boolean, shorten: boolean, properties) { - return this._key + (this._invert ? '!' : '') + "=" + SubstitutingTag.substituteString(this._value, properties); + return ( + this._key + + (this._invert ? "!" : "") + + "=" + + SubstitutingTag.substituteString(this._value, properties) + ) } asOverpass(): string[] { @@ -46,13 +51,17 @@ export default class SubstitutingTag implements TagsFilter { shadows(other: TagsFilter): boolean { if (!(other instanceof SubstitutingTag)) { - return false; + return false } - return other._key === this._key && other._value === this._value && other._invert === this._invert; + return ( + other._key === this._key && + other._value === this._value && + other._invert === this._invert + ) } isUsableAsAnswer(): boolean { - return !this._invert; + return !this._invert } /** @@ -64,16 +73,16 @@ export default class SubstitutingTag implements TagsFilter { * assign.matchesProperties({"some_key": "2021-03-29"}) // => false */ matchesProperties(properties: any): boolean { - const value = properties[this._key]; + const value = properties[this._key] if (value === undefined || value === "") { - return false; + return false } - const expectedValue = SubstitutingTag.substituteString(this._value, properties); - return (value === expectedValue) !== this._invert; + const expectedValue = SubstitutingTag.substituteString(this._value, properties) + return (value === expectedValue) !== this._invert } usedKeys(): string[] { - return [this._key]; + return [this._key] } usedTags(): { key: string; value: string }[] { @@ -84,22 +93,22 @@ export default class SubstitutingTag implements TagsFilter { if (this._invert) { throw "An inverted substituting tag can not be used to create a change" } - const v = SubstitutingTag.substituteString(this._value, properties); + const v = SubstitutingTag.substituteString(this._value, properties) if (v.match(/{.*}/) !== null) { throw "Could not calculate all the substitutions: still have " + v } - return [{k: this._key, v: v}]; + return [{ k: this._key, v: v }] } optimize(): TagsFilter | boolean { - return this; + return this } - + isNegative(): boolean { - return false; + return false } visit(f: (TagsFilter: any) => void) { f(this) } -} \ No newline at end of file +} diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index 005fa95ba..f9cb2ff87 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -1,6 +1,5 @@ -import {Utils} from "../../Utils"; -import {TagsFilter} from "./TagsFilter"; - +import { Utils } from "../../Utils" +import { TagsFilter } from "./TagsFilter" export class Tag extends TagsFilter { public key: string @@ -10,56 +9,57 @@ export class Tag extends TagsFilter { this.key = key this.value = value if (key === undefined || key === "") { - throw "Invalid key: undefined or empty"; + throw "Invalid key: undefined or empty" } if (value === undefined) { - throw `Invalid value while constructing a Tag with key '${key}': value is undefined`; + throw `Invalid value while constructing a Tag with key '${key}': value is undefined` } if (value === "*") { console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}~* ?`) } - if(value.indexOf("&") >= 0){ - const tags = (key + "="+value).split("&") - throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags.map(kv => "\"" + kv +"\"").join(', ')}]}'` + if (value.indexOf("&") >= 0) { + const tags = (key + "=" + value).split("&") + throw `Invalid value for a tag: it contains '&'. You probably meant to use '{"and":[${tags + .map((kv) => '"' + kv + '"') + .join(", ")}]}'` } } - /** - * imort - * + * imort + * * const tag = new Tag("key","value") * tag.matchesProperties({"key": "value"}) // => true * tag.matchesProperties({"key": "z"}) // => false * tag.matchesProperties({"key": ""}) // => false * tag.matchesProperties({"other_key": ""}) // => false * tag.matchesProperties({"other_key": "value"}) // => false - * + * * const isEmpty = new Tag("key","") * isEmpty.matchesProperties({"key": "value"}) // => false * isEmpty.matchesProperties({"key": ""}) // => true * isEmpty.matchesProperties({"other_key": ""}) // => true * isEmpty.matchesProperties({"other_key": "value"}) // => true * isEmpty.matchesProperties({"key": undefined}) // => true - * + * */ matchesProperties(properties: any): boolean { const foundValue = properties[this.key] if (foundValue === undefined && (this.value === "" || this.value === undefined)) { // The tag was not found // and it shouldn't be found! - return true; + return true } - return foundValue === this.value; + return foundValue === this.value } asOverpass(): string[] { if (this.value === "") { // NOT having this key - return ['[!"' + this.key + '"]']; + return ['[!"' + this.key + '"]'] } - return [`["${this.key}"="${this.value}"]`]; + return [`["${this.key}"="${this.value}"]`] } /** @@ -69,11 +69,11 @@ export class Tag extends TagsFilter { t.asHumanString(true) // => "key=value" */ asHumanString(linkToWiki?: boolean, shorten?: boolean, currentProperties?: any) { - let v = this.value; + let v = this.value if (shorten) { - v = Utils.EllipsesAfter(v, 25); + v = Utils.EllipsesAfter(v, 25) } - if (v === "" || v === undefined && currentProperties !== undefined) { + if (v === "" || (v === undefined && currentProperties !== undefined)) { // This tag will be removed if in the properties, so we indicate this with special rendering if (currentProperties !== undefined && (currentProperties[this.key] ?? "") === "") { // This tag is not present in the current properties, so this tag doesn't change anything @@ -82,21 +82,23 @@ export class Tag extends TagsFilter { return "" + this.key + "" } if (linkToWiki) { - return `${this.key}` + + return ( + `${this.key}` + `=` + `${v}` + ) } - return this.key + "=" + v; + return this.key + "=" + v } isUsableAsAnswer(): boolean { - return true; + return true } /** - * + * * import {RegexTag} from "./RegexTag"; - * + * * // should handle advanced regexes * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false @@ -107,38 +109,38 @@ export class Tag extends TagsFilter { * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false */ shadows(other: TagsFilter): boolean { - if(other["key"] !== undefined){ - if(other["key"] !== this.key){ + if (other["key"] !== undefined) { + if (other["key"] !== this.key) { return false } } - return other.matchesProperties({[this.key]: this.value}); + return other.matchesProperties({ [this.key]: this.value }) } usedKeys(): string[] { - return [this.key]; + return [this.key] } usedTags(): { key: string; value: string }[] { - if(this.value == ""){ + if (this.value == "") { return [] } return [this] } asChange(properties: any): { k: string; v: string }[] { - return [{k: this.key, v: this.value}]; + return [{ k: this.key, v: this.value }] } optimize(): TagsFilter | boolean { - return this; + return this } - + isNegative(): boolean { - return false; + return false } - + visit(f: (TagsFilter) => void) { f(this) } -} \ No newline at end of file +} diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index 1b975cff4..32e506430 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -1,23 +1,21 @@ -import {Tag} from "./Tag"; -import {TagsFilter} from "./TagsFilter"; -import {And} from "./And"; -import {Utils} from "../../Utils"; -import ComparingTag from "./ComparingTag"; -import {RegexTag} from "./RegexTag"; -import SubstitutingTag from "./SubstitutingTag"; -import {Or} from "./Or"; -import {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; -import {isRegExp} from "util"; +import { Tag } from "./Tag" +import { TagsFilter } from "./TagsFilter" +import { And } from "./And" +import { Utils } from "../../Utils" +import ComparingTag from "./ComparingTag" +import { RegexTag } from "./RegexTag" +import SubstitutingTag from "./SubstitutingTag" +import { Or } from "./Or" +import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" +import { isRegExp } from "util" import * as key_counts from "../../assets/key_totals.json" type Tags = Record -export type UploadableTag = Tag | SubstitutingTag | And +export type UploadableTag = Tag | SubstitutingTag | And export class TagUtils { - private static keyCounts: { keys: any, tags: any } = key_counts["default"] ?? key_counts - private static comparators - : [string, (a: number, b: number) => boolean][] - = [ + private static keyCounts: { keys: any; tags: any } = key_counts["default"] ?? key_counts + private static comparators: [string, (a: number, b: number) => boolean][] = [ ["<=", (a, b) => a <= b], [">=", (a, b) => a >= b], ["<", (a, b) => a < b], @@ -25,14 +23,14 @@ export class TagUtils { ] static KVtoProperties(tags: Tag[]): any { - const properties = {}; + const properties = {} for (const tag of tags) { properties[tag.key] = tag.value } - return properties; + return properties } - static changeAsProperties(kvs: { k: string, v: string }[]): any { + static changeAsProperties(kvs: { k: string; v: string }[]): any { const tags = {} for (const kv of kvs) { tags[kv.k] = kv.v @@ -47,20 +45,20 @@ export class TagUtils { for (const neededKey in neededTags) { const availableValues: string[] = availableTags[neededKey] if (availableValues === undefined) { - return false; + return false } - const neededValues: string[] = neededTags[neededKey]; + const neededValues: string[] = neededTags[neededKey] for (const neededValue of neededValues) { if (availableValues.indexOf(neededValue) < 0) { - return false; + return false } } } - return true; + return true } static SplitKeys(tagsFilters: UploadableTag[]): Record { - return this.SplitKeysRegex(tagsFilters, false); + return this.SplitKeysRegex(tagsFilters, false) } /*** @@ -68,69 +66,72 @@ export class TagUtils { * * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]} */ - static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: boolean): Record { + static SplitKeysRegex( + tagsFilters: UploadableTag[], + allowRegex: boolean + ): Record { const keyValues: Record = {} tagsFilters = [...tagsFilters] // copy all, use as queue while (tagsFilters.length > 0) { - const tagsFilter = tagsFilters.shift(); + const tagsFilter = tagsFilters.shift() if (tagsFilter === undefined) { - continue; + continue } if (tagsFilter instanceof And) { - tagsFilters.push(...tagsFilter.and); - continue; + tagsFilters.push(...(tagsFilter.and)) + continue } if (tagsFilter instanceof Tag) { if (keyValues[tagsFilter.key] === undefined) { - keyValues[tagsFilter.key] = []; + keyValues[tagsFilter.key] = [] } - keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map(s => s.trim())); - continue; + keyValues[tagsFilter.key].push(...tagsFilter.value.split(";").map((s) => s.trim())) + continue } if (allowRegex && tagsFilter instanceof RegexTag) { const key = tagsFilter.key if (isRegExp(key)) { - console.error("Invalid type to flatten the multiAnswer: key is a regex too", tagsFilter); + console.error( + "Invalid type to flatten the multiAnswer: key is a regex too", + tagsFilter + ) throw "Invalid type to FlattenMultiAnswer" } const keystr = key if (keyValues[keystr] === undefined) { - keyValues[keystr] = []; + keyValues[keystr] = [] } - keyValues[keystr].push(tagsFilter); - continue; + keyValues[keystr].push(tagsFilter) + continue } - - console.error("Invalid type to flatten the multiAnswer", tagsFilter); + console.error("Invalid type to flatten the multiAnswer", tagsFilter) throw "Invalid type to FlattenMultiAnswer" } - return keyValues; + return keyValues } /** * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags */ - static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record): Tag[]{ - const tags : Tag[] = [] + static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record): Tag[] { + const tags: Tag[] = [] tagFilters.visit((tf: UploadableTag) => { - if(tf instanceof Tag){ + if (tf instanceof Tag) { tags.push(tf) } - if(tf instanceof SubstitutingTag){ + if (tf instanceof SubstitutingTag) { tags.push(tf.asTag(currentProperties)) } }) return tags } - - - /** + /** * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. * E.g: * @@ -152,17 +153,17 @@ export class TagUtils { */ static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And { if (tagsFilters === undefined) { - return new And([]); + return new And([]) } - let keyValues = TagUtils.SplitKeys(tagsFilters); + let keyValues = TagUtils.SplitKeys(tagsFilters) const and: UploadableTag[] = [] for (const key in keyValues) { - const values = Utils.Dedup(keyValues[key]).filter(v => v !== "") + const values = Utils.Dedup(keyValues[key]).filter((v) => v !== "") values.sort() - and.push(new Tag(key, values.join(";"))); + and.push(new Tag(key, values.join(";"))) } - return new And(and); + return new And(and) } /** @@ -177,16 +178,15 @@ export class TagUtils { * TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true */ static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { - const splitted = TagUtils.SplitKeysRegex([tag], true); + const splitted = TagUtils.SplitKeysRegex([tag], true) for (const splitKey in splitted) { - const neededValues = splitted[splitKey]; + const neededValues = splitted[splitKey] if (properties[splitKey] === undefined) { - return false; + return false } - const actualValue = properties[splitKey].split(";").map(s => s.trim()); + const actualValue = properties[splitKey].split(";").map((s) => s.trim()) for (const neededValue of neededValues) { - if (neededValue instanceof RegexTag) { if (!neededValue.matchesProperties(properties)) { return false @@ -194,19 +194,19 @@ export class TagUtils { continue } if (actualValue.indexOf(neededValue) < 0) { - return false; + return false } } } - return true; + return true } public static SimpleTag(json: string, context?: string): Tag { - const tag = Utils.SplitFirst(json, "="); + const tag = Utils.SplitFirst(json, "=") if (tag.length !== 2) { throw `Invalid tag: no (or too much) '=' found (in ${context ?? "unkown context"})` } - return new Tag(tag[0], tag[1]); + return new Tag(tag[0], tag[1]) } /** @@ -269,30 +269,34 @@ export class TagUtils { */ public static Tag(json: TagConfigJson, context: string = ""): TagsFilter { try { - return this.ParseTagUnsafe(json, context); + return this.ParseTagUnsafe(json, context) } catch (e) { console.error("Could not parse tag", json, "in context", context, "due to ", e) - throw e; + throw e } } public static ParseUploadableTag(json: TagConfigJson, context: string = ""): UploadableTag { - const t = this.Tag(json, context); - - t.visit((t : TagsFilter)=> { - if( t instanceof And){ - return - } - if(t instanceof Tag){ - return - } - if(t instanceof SubstitutingTag){ - return - } - throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(false, false, {})}` - }) - - return t + const t = this.Tag(json, context) + + t.visit((t: TagsFilter) => { + if (t instanceof And) { + return + } + if (t instanceof Tag) { + return + } + if (t instanceof SubstitutingTag) { + return + } + throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString( + false, + false, + {} + )}` + }) + + return t } /** @@ -308,11 +312,10 @@ export class TagUtils { return TagUtils.Tag(json, context) } - /** * INLINE sort of the given list */ - public static sortFilters(filters: TagsFilter [], usePopularity: boolean): void { + public static sortFilters(filters: TagsFilter[], usePopularity: boolean): void { filters.sort((a, b) => TagUtils.order(a, b, usePopularity)) } @@ -346,42 +349,42 @@ export class TagUtils { * TagUtils.parseRegexOperator("tileId~*") // => {invert: false, key: "tileId", value: "*", modifier: ""} */ public static parseRegexOperator(tag: string): { - invert: boolean; - key: string; - value: string; - modifier: "i" | ""; + invert: boolean + key: string + value: string + modifier: "i" | "" } | null { - const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/); + const match = tag.match(/^([_a-zA-Z0-9: -]+)(!)?~([i]~)?(.*)$/) if (match == null) { - return null; + return null } - const [ , key, invert, modifier, value] = match; - return {key, value, invert: invert == "!", modifier: (modifier == "i~" ? "i" : "")}; + const [, key, invert, modifier, value] = match + return { key, value, invert: invert == "!", modifier: modifier == "i~" ? "i" : "" } } private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { - if (json === undefined) { - throw new Error(`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`) + throw new Error( + `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression` + ) } - if (typeof (json) != "string") { + if (typeof json != "string") { if (json["and"] !== undefined && json["or"] !== undefined) { throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined` } if (json["and"] !== undefined) { - return new And(json["and"].map(t => TagUtils.Tag(t, context))); + return new And(json["and"].map((t) => TagUtils.Tag(t, context))) } if (json["or"] !== undefined) { - return new Or(json["or"].map(t => TagUtils.Tag(t, context))); + return new Or(json["or"].map((t) => TagUtils.Tag(t, context))) } throw `At ${context}: unrecognized tag: ${JSON.stringify(json)}` } - - const tag = json as string; + const tag = json as string for (const [operator, comparator] of TagUtils.comparators) { if (tag.indexOf(operator) >= 0) { - const split = Utils.SplitFirst(tag, operator); + const split = Utils.SplitFirst(tag, operator) let val = Number(split[1].trim()) if (isNaN(val)) { @@ -390,7 +393,7 @@ export class TagUtils { const f = (value: string | number | undefined) => { if (value === undefined) { - return false; + return false } let b: number if (typeof value === "number") { @@ -413,14 +416,14 @@ export class TagUtils { } if (tag.indexOf("~~") >= 0) { - const split = Utils.SplitFirst(tag, "~~"); + const split = Utils.SplitFirst(tag, "~~") if (split[1] === "*") { split[1] = "..*" } return new RegexTag( new RegExp("^" + split[0] + "$"), new RegExp("^" + split[1] + "$", "s") - ); + ) } const withRegex = TagUtils.parseRegexOperator(tag) if (withRegex != null) { @@ -428,10 +431,16 @@ export class TagUtils { throw `Don't use 'key!~*' - use 'key=' instead (empty string as value (in the tag ${tag} while parsing ${context})` } if (withRegex.value === "") { - throw "Detected a regextag with an empty regex; this is not allowed. Use '" + withRegex.key + "='instead (at " + context + ")" + throw ( + "Detected a regextag with an empty regex; this is not allowed. Use '" + + withRegex.key + + "='instead (at " + + context + + ")" + ) } - let value: string | RegExp = withRegex.value; + let value: string | RegExp = withRegex.value if (value === "*") { value = "..*" } @@ -439,39 +448,40 @@ export class TagUtils { withRegex.key, new RegExp("^" + value + "$", "s" + withRegex.modifier), withRegex.invert - ); + ) } if (tag.indexOf("!:=") >= 0) { - const split = Utils.SplitFirst(tag, "!:="); - return new SubstitutingTag(split[0], split[1], true); + const split = Utils.SplitFirst(tag, "!:=") + return new SubstitutingTag(split[0], split[1], true) } if (tag.indexOf(":=") >= 0) { - const split = Utils.SplitFirst(tag, ":="); - return new SubstitutingTag(split[0], split[1]); + const split = Utils.SplitFirst(tag, ":=") + return new SubstitutingTag(split[0], split[1]) } if (tag.indexOf("!=") >= 0) { - const split = Utils.SplitFirst(tag, "!="); + const split = Utils.SplitFirst(tag, "!=") if (split[1] === "*") { - throw "At " + context + ": invalid tag " + tag + ". To indicate a missing tag, use '" + split[0] + "!=' instead" + throw ( + "At " + + context + + ": invalid tag " + + tag + + ". To indicate a missing tag, use '" + + split[0] + + "!=' instead" + ) } if (split[1] === "") { split[1] = "..*" return new RegexTag(split[0], /^..*$/s) } - return new RegexTag( - split[0], - split[1], - true - ); + return new RegexTag(split[0], split[1], true) } - if (tag.indexOf("=") >= 0) { - - - const split = Utils.SplitFirst(tag, "="); + const split = Utils.SplitFirst(tag, "=") if (split[1] == "*") { throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead` } @@ -524,7 +534,7 @@ export class TagUtils { } private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) { - const joined = tfs.map(e => TagUtils.toString(e, false)).join(seperator) + const joined = tfs.map((e) => TagUtils.toString(e, false)).join(seperator) if (toplevel) { return joined } @@ -542,14 +552,14 @@ export class TagUtils { * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", "value", true)]) // => true * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", /value/, true)]) // => true */ - public static ContainsOppositeTags(tags: (TagsFilter)[]): boolean { + public static ContainsOppositeTags(tags: TagsFilter[]): boolean { for (let i = 0; i < tags.length; i++) { - const tag = tags[i]; + const tag = tags[i] if (!(tag instanceof Tag || tag instanceof RegexTag)) { continue } for (let j = i + 1; j < tags.length; j++) { - const guard = tags[j]; + const guard = tags[j] if (!(guard instanceof Tag || guard instanceof RegexTag)) { continue } @@ -579,8 +589,11 @@ export class TagUtils { * * TagUtils.removeShadowedElementsFrom([new Tag("key","value")], [new Tag("key","value"), new Tag("other_key","value")]) // => [new Tag("other_key","value")] */ - public static removeShadowedElementsFrom(blacklist: TagsFilter[], listToFilter: TagsFilter[]): TagsFilter[] { - return listToFilter.filter(tf => !blacklist.some(guard => guard.shadows(tf))) + public static removeShadowedElementsFrom( + blacklist: TagsFilter[], + listToFilter: TagsFilter[] + ): TagsFilter[] { + return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf))) } /** @@ -591,15 +604,15 @@ export class TagUtils { public static removeEquivalents(listToFilter: (Tag | RegexTag)[]): TagsFilter[] { const result: TagsFilter[] = [] outer: for (let i = 0; i < listToFilter.length; i++) { - const tag = listToFilter[i]; + const tag = listToFilter[i] for (let j = 0; j < listToFilter.length; j++) { if (i === j) { continue } - const guard = listToFilter[j]; + const guard = listToFilter[j] if (guard.shadows(tag)) { // the guard 'kills' the tag: we continue the outer loop without adding the tag - continue outer; + continue outer } } result.push(tag) @@ -615,10 +628,9 @@ export class TagUtils { * TagUtils.containsEquivalents([new Tag("key","value")], [ new Tag("key","other_value")]) // => false */ public static containsEquivalents(guards: TagsFilter[], listToFilter: TagsFilter[]): boolean { - return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) + return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf))) } - /** * Parses a level specifier to the various available levels * @@ -633,24 +645,24 @@ export class TagUtils { */ public static LevelsParser(level: string): string[] { let spec = Utils.NoNull([level]) - spec = [].concat(...spec.map(s => s?.split(";"))) - spec = [].concat(...spec.map(s => { - s = s.trim() - if (s.indexOf("-") < 0 || s.startsWith("-")) { - return s - } - const [start, end] = s.split("-").map(s => Number(s.trim())) - if (isNaN(start) || isNaN(end)) { - return undefined - } - const values = [] - for (let i = start; i <= end; i++) { - values.push(i + "") - } - return values - })) - return Utils.NoNull(spec); + spec = [].concat(...spec.map((s) => s?.split(";"))) + spec = [].concat( + ...spec.map((s) => { + s = s.trim() + if (s.indexOf("-") < 0 || s.startsWith("-")) { + return s + } + const [start, end] = s.split("-").map((s) => Number(s.trim())) + if (isNaN(start) || isNaN(end)) { + return undefined + } + const values = [] + for (let i = start; i <= end; i++) { + values.push(i + "") + } + return values + }) + ) + return Utils.NoNull(spec) } - - -} \ No newline at end of file +} diff --git a/Logic/Tags/TagsFilter.ts b/Logic/Tags/TagsFilter.ts index e02739c7b..4f8944ff3 100644 --- a/Logic/Tags/TagsFilter.ts +++ b/Logic/Tags/TagsFilter.ts @@ -1,26 +1,25 @@ export abstract class TagsFilter { - abstract asOverpass(): string[] - abstract isUsableAsAnswer(): boolean; + abstract isUsableAsAnswer(): boolean /** * Indicates some form of equivalency: * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties */ - abstract shadows(other: TagsFilter): boolean; + abstract shadows(other: TagsFilter): boolean - abstract matchesProperties(properties: any): boolean; + abstract matchesProperties(properties: any): boolean - abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any): string; + abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any): string - abstract usedKeys(): string[]; + abstract usedKeys(): string[] /** * Returns all normal key/value pairs * Regex tags, substitutions, comparisons, ... are exempt */ - abstract usedTags(): { key: string, value: string }[]; + abstract usedTags(): { key: string; value: string }[] /** * Converts the tagsFilter into a list of key-values that should be uploaded to OSM. @@ -28,12 +27,12 @@ export abstract class TagsFilter { * * Note: properties are the already existing tags-object. It is only used in the substituting tag */ - abstract asChange(properties: any): { k: string, v: string }[] + abstract asChange(properties: any): { k: string; v: string }[] /** * Returns an optimized version (or self) of this tagsFilter */ - abstract optimize(): TagsFilter | boolean; + abstract optimize(): TagsFilter | boolean /** * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). @@ -55,6 +54,5 @@ export abstract class TagsFilter { /** * Walks the entire tree, every tagsFilter will be passed into the function once */ - abstract visit(f: ((TagsFilter) => void)); - -} \ No newline at end of file + abstract visit(f: (TagsFilter) => void) +} diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 479940aa1..ab1261fd9 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -1,25 +1,27 @@ -import {Utils} from "../Utils"; +import { Utils } from "../Utils" /** * Various static utils */ export class Stores { public static Chronic(millis: number, asLong: () => boolean = undefined): Store { - const source = new UIEventSource(undefined); + const source = new UIEventSource(undefined) function run() { - source.setData(new Date()); + source.setData(new Date()) if (asLong === undefined || asLong()) { - window.setTimeout(run, millis); + window.setTimeout(run, millis) } } - run(); - return source; + run() + return source } - public static FromPromiseWithErr(promise: Promise): Store<{ success: T } | { error: any }> { - return UIEventSource.FromPromiseWithErr(promise); + public static FromPromiseWithErr( + promise: Promise + ): Store<{ success: T } | { error: any }> { + return UIEventSource.FromPromiseWithErr(promise) } /** @@ -30,13 +32,13 @@ export class Stores { */ public static FromPromise(promise: Promise): Store { const src = new UIEventSource(undefined) - promise?.then(d => src.setData(d)) - promise?.catch(err => console.warn("Promise failed:", err)) + promise?.then((d) => src.setData(d)) + promise?.catch((err) => console.warn("Promise failed:", err)) return src } public static flatten(source: Store>, possibleSources?: Store[]): Store { - return UIEventSource.flatten(source, possibleSources); + return UIEventSource.flatten(source, possibleSources) } /** @@ -55,50 +57,49 @@ export class Stores { */ public static ListStabilized(src: Store): Store { const stable = new UIEventSource(undefined) - src.addCallbackAndRun(list => { + src.addCallbackAndRun((list) => { if (list === undefined) { stable.setData(undefined) - return; + return } const oldList = stable.data if (oldList === list) { - return; + return } - if(oldList == list){ - return; + if (oldList == list) { + return } if (oldList === undefined || oldList.length !== list.length) { - stable.setData(list); - return; + stable.setData(list) + return } for (let i = 0; i < list.length; i++) { if (oldList[i] !== list[i]) { - stable.setData(list); - return; + stable.setData(list) + return } } // No actual changes, so we don't do anything - return; + return }) return stable } } export abstract class Store { - abstract readonly data: T; + abstract readonly data: T /** * OPtional value giving a title to the UIEventSource, mainly used for debugging */ - public readonly tag: string | undefined; - + public readonly tag: string | undefined constructor(tag: string = undefined) { - this.tag = tag; - if ((tag === undefined || tag === "")) { - let createStack = Utils.runningFromConsole; + this.tag = tag + if (tag === undefined || tag === "") { + let createStack = Utils.runningFromConsole if (!Utils.runningFromConsole) { createStack = window.location.hostname === "127.0.0.1" } @@ -109,49 +110,51 @@ export abstract class Store { } } - abstract map(f: ((t: T) => J)): Store - abstract map(f: ((t: T) => J), extraStoresToWatch: Store[]): Store + abstract map(f: (t: T) => J): Store + abstract map(f: (t: T) => J, extraStoresToWatch: Store[]): Store /** * Add a callback function which will run on future data changes */ - abstract addCallback(callback: (data: T) => void): (() => void); + abstract addCallback(callback: (data: T) => void): () => void /** * Adds a callback function, which will be run immediately. * Only triggers if the current data is defined */ - abstract addCallbackAndRunD(callback: (data: T) => void): (() => void); + abstract addCallbackAndRunD(callback: (data: T) => void): () => void /** * Add a callback function which will run on future data changes * Only triggers if the data is defined */ - abstract addCallbackD(callback: (data: T) => void): (() => void); + abstract addCallbackD(callback: (data: T) => void): () => void /** * Adds a callback function, which will be run immediately. * Only triggers if the current data is defined */ - abstract addCallbackAndRun(callback: (data: T) => void): (() => void); + abstract addCallbackAndRun(callback: (data: T) => void): () => void - public withEqualityStabilized(comparator: (t: T | undefined, t1: T | undefined) => boolean): Store { - let oldValue = undefined; - return this.map(v => { + public withEqualityStabilized( + comparator: (t: T | undefined, t1: T | undefined) => boolean + ): Store { + let oldValue = undefined + return this.map((v) => { if (v == oldValue) { return oldValue } if (comparator(oldValue, v)) { return oldValue } - oldValue = v; - return v; + oldValue = v + return v }) } /** * Monadic bind function - * + * * // simple test with bound and immutablestores * const src = new UIEventSource(3) * const bound = src.bind(i => new ImmutableStore(i * 2)) @@ -160,7 +163,7 @@ export abstract class Store { * lastValue // => 6 * src.setData(21) * lastValue // => 42 - * + * * // simple test with bind over a mapped value * const src = new UIEventSource(0) * const srcs : UIEventSource[] = [new UIEventSource("a"), new UIEventSource("b")] @@ -176,9 +179,9 @@ export abstract class Store { * lastValue // => "xyz" * src.setData(0) * lastValue // => "def" - * - * - * + * + * + * * // advanced test with bound * const src = new UIEventSource(0) * const srcs : UIEventSource[] = [new UIEventSource("a"), new UIEventSource("b")] @@ -195,20 +198,20 @@ export abstract class Store { * src.setData(0) * lastValue // => "def" */ - public bind(f: ((t: T) => Store)): Store { + public bind(f: (t: T) => Store): Store { const mapped = this.map(f) const sink = new UIEventSource(undefined) - const seenEventSources = new Set>(); - mapped.addCallbackAndRun(newEventSource => { + const seenEventSources = new Set>() + mapped.addCallbackAndRun((newEventSource) => { if (newEventSource === null) { sink.setData(null) } else if (newEventSource === undefined) { sink.setData(undefined) } else if (!seenEventSources.has(newEventSource)) { seenEventSources.add(newEventSource) - newEventSource.addCallbackAndRun(resultData => { + newEventSource.addCallbackAndRun((resultData) => { if (mapped.data === newEventSource) { - sink.setData(resultData); + sink.setData(resultData) } }) } else { @@ -217,67 +220,66 @@ export abstract class Store { } }) - return sink; + return sink } public stabilized(millisToStabilize): Store { if (Utils.runningFromConsole) { - return this; + return this } - const newSource = new UIEventSource(this.data); + const newSource = new UIEventSource(this.data) - this.addCallback(latestData => { + this.addCallback((latestData) => { window.setTimeout(() => { - if (this.data == latestData) { // compare by reference - newSource.setData(latestData); + if (this.data == latestData) { + // compare by reference + newSource.setData(latestData) } }, millisToStabilize) - }); + }) - return newSource; + return newSource } - public AsPromise(condition?: ((t: T) => boolean)): Promise { - const self = this; - condition = condition ?? (t => t !== undefined) + public AsPromise(condition?: (t: T) => boolean): Promise { + const self = this + condition = condition ?? ((t) => t !== undefined) return new Promise((resolve) => { if (condition(self.data)) { resolve(self.data) } else { - self.addCallbackD(data => { + self.addCallbackD((data) => { resolve(data) - return true; // return true to unregister as we only need to be called once + return true // return true to unregister as we only need to be called once }) } }) } - } export class ImmutableStore extends Store { - public readonly data: T; + public readonly data: T - private static readonly pass: (() => void) = () => { - } + private static readonly pass: () => void = () => {} constructor(data: T) { - super(); - this.data = data; + super() + this.data = data } - addCallback(callback: (data: T) => void): (() => void) { + addCallback(callback: (data: T) => void): () => void { // pass: data will never change return ImmutableStore.pass } - addCallbackAndRun(callback: (data: T) => void): (() => void) { + addCallbackAndRun(callback: (data: T) => void): () => void { callback(this.data) // no callback registry: data will never change return ImmutableStore.pass } - addCallbackAndRunD(callback: (data: T) => void): (() => void) { + addCallbackAndRunD(callback: (data: T) => void): () => void { if (this.data !== undefined) { callback(this.data) } @@ -285,38 +287,35 @@ export class ImmutableStore extends Store { return ImmutableStore.pass } - addCallbackD(callback: (data: T) => void): (() => void) { + addCallbackD(callback: (data: T) => void): () => void { // pass: data will never change return ImmutableStore.pass } - map(f: (t: T) => J, extraStores: Store[] = undefined): ImmutableStore { - if(extraStores?.length > 0){ + if (extraStores?.length > 0) { return new MappedStore(this, f, extraStores, undefined, f(this.data)) } - return new ImmutableStore(f(this.data)); + return new ImmutableStore(f(this.data)) } - - } /** * Keeps track of the callback functions */ class ListenerTracker { - private readonly _callbacks: ((t: T) => (boolean | void | any)) [] = []; - - public pingCount = 0; + private readonly _callbacks: ((t: T) => boolean | void | any)[] = [] + + public pingCount = 0 /** * Adds a callback which can be called; a function to unregister is returned */ - public addCallback(callback: (t: T) => (boolean | void | any)): (() => void) { + public addCallback(callback: (t: T) => boolean | void | any): () => void { if (callback === console.log) { // This ^^^ actually works! throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." } - this._callbacks.push(callback); + this._callbacks.push(callback) // Give back an unregister-function! return () => { @@ -332,9 +331,9 @@ class ListenerTracker { * Returns the number of registered callbacks */ public ping(data: T): number { - this.pingCount ++; + this.pingCount++ let toDelete = undefined - let startTime = new Date().getTime() / 1000; + let startTime = new Date().getTime() / 1000 for (const callback of this._callbacks) { if (callback(data) === true) { // This callback wants to be deleted @@ -347,8 +346,10 @@ class ListenerTracker { } } let endTime = new Date().getTime() / 1000 - if ((endTime - startTime) > 500) { - console.trace("Warning: a ping took more then 500ms; this is probably a performance issue") + if (endTime - startTime > 500) { + console.trace( + "Warning: a ping took more then 500ms; this is probably a performance issue" + ) } if (toDelete !== undefined) { for (const toDeleteElement of toDelete) { @@ -363,55 +364,57 @@ class ListenerTracker { } } - /** * The mapped store is a helper type which does the mapping of a function. * It'll fuse */ class MappedStore extends Store { + private _upstream: Store + private _upstreamCallbackHandler: ListenerTracker | undefined + private _upstreamPingCount: number = -1 + private _unregisterFromUpstream: () => void - private _upstream: Store; - private _upstreamCallbackHandler: ListenerTracker | undefined; - private _upstreamPingCount: number = -1; - private _unregisterFromUpstream: (() => void) - - private _f: (t: TIn) => T; - private readonly _extraStores: Store[] | undefined; + private _f: (t: TIn) => T + private readonly _extraStores: Store[] | undefined private _unregisterFromExtraStores: (() => void)[] | undefined private _callbacks: ListenerTracker = new ListenerTracker() private static readonly pass: () => {} - - constructor(upstream: Store, f: (t: TIn) => T, extraStores: Store[], - upstreamListenerHandler: ListenerTracker | undefined, initialState: T) { - super(); - this._upstream = upstream; + constructor( + upstream: Store, + f: (t: TIn) => T, + extraStores: Store[], + upstreamListenerHandler: ListenerTracker | undefined, + initialState: T + ) { + super() + this._upstream = upstream this._upstreamCallbackHandler = upstreamListenerHandler - this._f = f; + this._f = f this._data = initialState this._upstreamPingCount = upstreamListenerHandler?.pingCount - this._extraStores = extraStores; + this._extraStores = extraStores this.registerCallbacksToUpstream() } - private _data: T; + private _data: T private _callbacksAreRegistered = false /** * Gets the current data from the store - * + * * const src = new UIEventSource(21) * const mapped = src.map(i => i * 2) * src.setData(3) * mapped.data // => 6 - * + * */ - get data(): T { + get data(): T { if (!this._callbacksAreRegistered) { // Callbacks are not registered, so we haven't been listening for updates from the upstream which might have changed - if(this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount){ + if (this._upstreamCallbackHandler?.pingCount != this._upstreamPingCount) { // Upstream has pinged - let's update our data first this._data = this._f(this._upstream.data) } @@ -420,8 +423,7 @@ class MappedStore extends Store { return this._data } - - map(f: (t: T) => J, extraStores: (Store)[] = undefined): Store { + map(f: (t: T) => J, extraStores: Store[] = undefined): Store { let stores: Store[] = undefined if (extraStores?.length > 0 || this._extraStores?.length > 0) { stores = [] @@ -430,7 +432,7 @@ class MappedStore extends Store { stores.push(...extraStores) } if (this._extraStores?.length > 0) { - this._extraStores?.forEach(store => { + this._extraStores?.forEach((store) => { if (stores.indexOf(store) < 0) { stores.push(store) } @@ -442,39 +444,37 @@ class MappedStore extends Store { stores, this._callbacks, f(this.data) - ); + ) } private unregisterFromUpstream() { console.log("Unregistering callbacks for", this.tag) - this._callbacksAreRegistered = false; + this._callbacksAreRegistered = false this._unregisterFromUpstream() - this._unregisterFromExtraStores?.forEach(unr => unr()) + this._unregisterFromExtraStores?.forEach((unr) => unr()) } - + private registerCallbacksToUpstream() { const self = this - - this._unregisterFromUpstream = this._upstream.addCallback( - _ => self.update() + + this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update()) + this._unregisterFromExtraStores = this._extraStores?.map((store) => + store?.addCallback((_) => self.update()) ) - this._unregisterFromExtraStores = this._extraStores?.map(store => - store?.addCallback(_ => self.update()) - ) - this._callbacksAreRegistered = true; + this._callbacksAreRegistered = true } private update(): void { const newData = this._f(this._upstream.data) this._upstreamPingCount = this._upstreamCallbackHandler?.pingCount if (this._data == newData) { - return; + return } this._data = newData this._callbacks.ping(this._data) } - addCallback(callback: (data: T) => (any | boolean | void)): (() => void) { + addCallback(callback: (data: T) => any | boolean | void): () => void { if (!this._callbacksAreRegistered) { // This is the first callback that is added // We register this 'map' to the upstream object and all the streams @@ -489,7 +489,7 @@ class MappedStore extends Store { } } - addCallbackAndRun(callback: (data: T) => (any | boolean | void)): (() => void) { + addCallbackAndRun(callback: (data: T) => any | boolean | void): () => void { const unregister = this.addCallback(callback) const doRemove = callback(this.data) if (doRemove === true) { @@ -499,71 +499,74 @@ class MappedStore extends Store { return unregister } - addCallbackAndRunD(callback: (data: T) => (any | boolean | void)): (() => void) { - return this.addCallbackAndRun(data => { + addCallbackAndRunD(callback: (data: T) => any | boolean | void): () => void { + return this.addCallbackAndRun((data) => { if (data !== undefined) { return callback(data) } }) } - addCallbackD(callback: (data: T) => (any | boolean | void)): (() => void) { - return this.addCallback(data => { + addCallbackD(callback: (data: T) => any | boolean | void): () => void { + return this.addCallback((data) => { if (data !== undefined) { return callback(data) } }) } - - } export class UIEventSource extends Store { - - public data: T; + public data: T _callbacks: ListenerTracker = new ListenerTracker() private static readonly pass: () => {} constructor(data: T, tag: string = "") { - super(tag); - this.data = data; + super(tag) + this.data = data } - public static flatten(source: Store>, possibleSources?: Store[]): UIEventSource { - const sink = new UIEventSource(source.data?.data); + public static flatten( + source: Store>, + possibleSources?: Store[] + ): UIEventSource { + const sink = new UIEventSource(source.data?.data) source.addCallback((latestData) => { - sink.setData(latestData?.data); - latestData.addCallback(data => { + sink.setData(latestData?.data) + latestData.addCallback((data) => { if (source.data !== latestData) { - return true; + return true } sink.setData(data) }) - }); + }) for (const possibleSource of possibleSources ?? []) { possibleSource?.addCallback(() => { - sink.setData(source.data?.data); + sink.setData(source.data?.data) }) } - return sink; + return sink } /** * Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated. * If the promise fails, the value will stay undefined, but 'onError' will be called */ - public static FromPromise(promise: Promise, onError: ((e: any) => void) = undefined): UIEventSource { + public static FromPromise( + promise: Promise, + onError: (e: any) => void = undefined + ): UIEventSource { const src = new UIEventSource(undefined) - promise?.then(d => src.setData(d)) - promise?.catch(err => { + promise?.then((d) => src.setData(d)) + promise?.catch((err) => { if (onError !== undefined) { onError(err) } else { - console.warn("Promise failed:", err); + console.warn("Promise failed:", err) } }) return src @@ -575,25 +578,27 @@ export class UIEventSource extends Store { * @param promise * @constructor */ - public static FromPromiseWithErr(promise: Promise): UIEventSource<{ success: T } | { error: any }> { + public static FromPromiseWithErr( + promise: Promise + ): UIEventSource<{ success: T } | { error: any }> { const src = new UIEventSource<{ success: T } | { error: any }>(undefined) - promise?.then(d => src.setData({success: d})) - promise?.catch(err => src.setData({error: err})) + promise?.then((d) => src.setData({ success: d })) + promise?.catch((err) => src.setData({ error: err })) return src } public static asFloat(source: UIEventSource): UIEventSource { return source.sync( (str) => { - let parsed = parseFloat(str); - return isNaN(parsed) ? undefined : parsed; + let parsed = parseFloat(str) + return isNaN(parsed) ? undefined : parsed }, [], (fl) => { if (fl === undefined || isNaN(fl)) { - return undefined; + return undefined } - return ("" + fl).substr(0, 8); + return ("" + fl).substr(0, 8) } ) } @@ -604,29 +609,29 @@ export class UIEventSource extends Store { * If the result of the callback is 'true', the callback is considered finished and will be removed again * @param callback */ - public addCallback(callback: ((latestData: T) => (boolean | void | any))): (() => void) { - return this._callbacks.addCallback(callback); + public addCallback(callback: (latestData: T) => boolean | void | any): () => void { + return this._callbacks.addCallback(callback) } - public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): (() => void) { - const doDeleteCallback = callback(this.data); + public addCallbackAndRun(callback: (latestData: T) => boolean | void | any): () => void { + const doDeleteCallback = callback(this.data) if (doDeleteCallback !== true) { - return this.addCallback(callback); + return this.addCallback(callback) } else { return UIEventSource.pass } } - public addCallbackAndRunD(callback: (data: T) => void): (() => void) { - return this.addCallbackAndRun(data => { + public addCallbackAndRunD(callback: (data: T) => void): () => void { + return this.addCallbackAndRun((data) => { if (data !== undefined && data !== null) { return callback(data) } }) } - public addCallbackD(callback: (data: T) => void): (() => void) { - return this.addCallback(data => { + public addCallbackD(callback: (data: T) => void): () => void { + return this.addCallback((data) => { if (data !== undefined && data !== null) { return callback(data) } @@ -634,12 +639,13 @@ export class UIEventSource extends Store { } public setData(t: T): UIEventSource { - if (this.data == t) { // MUST COMPARE BY REFERENCE! - return; + if (this.data == t) { + // MUST COMPARE BY REFERENCE! + return } - this.data = t; + this.data = t this._callbacks.ping(t) - return this; + return this } public ping(): void { @@ -669,9 +675,8 @@ export class UIEventSource extends Store { * srcSeen // => 21 * lastSeen // => 42 */ - public map(f: ((t: T) => J), - extraSources: Store[] = []): Store { - return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)); + public map(f: (t: T) => J, extraSources: Store[] = []): Store { + return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)) } /** @@ -682,53 +687,51 @@ export class UIEventSource extends Store { * @param g: a 'backfunction to let the sync run in two directions. (data of the new UIEVEntSource, currentData) => newData * @param allowUnregister: if set, the update will be halted if no listeners are registered */ - public sync(f: ((t: T) => J), - extraSources: Store[], - g: ((j: J, t: T) => T), - allowUnregister = false): UIEventSource { - const self = this; + public sync( + f: (t: T) => J, + extraSources: Store[], + g: (j: J, t: T) => T, + allowUnregister = false + ): UIEventSource { + const self = this - const stack = new Error().stack.split("\n"); + const stack = new Error().stack.split("\n") const callee = stack[1] - const newSource = new UIEventSource( - f(this.data), - "map(" + this.tag + ")@" + callee - ); + const newSource = new UIEventSource(f(this.data), "map(" + this.tag + ")@" + callee) const update = function () { - newSource.setData(f(self.data)); + newSource.setData(f(self.data)) return allowUnregister && newSource._callbacks.length() === 0 } - this.addCallback(update); + this.addCallback(update) for (const extraSource of extraSources) { - extraSource?.addCallback(update); + extraSource?.addCallback(update) } if (g !== undefined) { newSource.addCallback((latest) => { - self.setData(g(latest, self.data)); + self.setData(g(latest, self.data)) }) } - return newSource; + return newSource } public syncWith(otherSource: UIEventSource, reverseOverride = false): UIEventSource { - this.addCallback((latest) => otherSource.setData(latest)); - const self = this; - otherSource.addCallback((latest) => self.setData(latest)); + this.addCallback((latest) => otherSource.setData(latest)) + const self = this + otherSource.addCallback((latest) => self.setData(latest)) if (reverseOverride) { if (otherSource.data !== undefined) { - this.setData(otherSource.data); + this.setData(otherSource.data) } } else if (this.data === undefined) { - this.setData(otherSource.data); + this.setData(otherSource.data) } else { - otherSource.setData(this.data); + otherSource.setData(this.data) } - return this; + return this } - } diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts index 0c463c689..f89e61630 100644 --- a/Logic/Web/Hash.ts +++ b/Logic/Web/Hash.ts @@ -1,12 +1,11 @@ -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource" +import { Utils } from "../../Utils" /** * Wrapper around the hash to create an UIEventSource from it */ export default class Hash { - - public static hash: UIEventSource = Hash.Get(); + public static hash: UIEventSource = Hash.Get() /** * Gets the current string, including the pound sign if there is any @@ -16,48 +15,46 @@ export default class Hash { if (Hash.hash.data === undefined || Hash.hash.data === "") { return "" } else { - return "#" + Hash.hash.data; + return "#" + Hash.hash.data } } private static Get(): UIEventSource { if (Utils.runningFromConsole) { - return new UIEventSource(undefined); + return new UIEventSource(undefined) } - const hash = new UIEventSource(window.location.hash.substr(1)); - hash.addCallback(h => { + const hash = new UIEventSource(window.location.hash.substr(1)) + hash.addCallback((h) => { if (h === "undefined") { console.warn("Got a literal 'undefined' as hash, ignoring") - h = undefined; + h = undefined } if (h === undefined || h === "") { - window.location.hash = ""; - return; + window.location.hash = "" + return } history.pushState({}, "") - window.location.hash = "#" + h; - }); - + window.location.hash = "#" + h + }) window.onhashchange = () => { - let newValue = window.location.hash.substr(1); + let newValue = window.location.hash.substr(1) if (newValue === "") { - newValue = undefined; + newValue = undefined } hash.setData(newValue) } - window.addEventListener('popstate', _ => { - let newValue = window.location.hash.substr(1); + window.addEventListener("popstate", (_) => { + let newValue = window.location.hash.substr(1) if (newValue === "") { - newValue = undefined; + newValue = undefined } hash.setData(newValue) }) - return hash; + return hash } - -} \ No newline at end of file +} diff --git a/Logic/Web/IdbLocalStorage.ts b/Logic/Web/IdbLocalStorage.ts index f3d929866..2d23db76e 100644 --- a/Logic/Web/IdbLocalStorage.ts +++ b/Logic/Web/IdbLocalStorage.ts @@ -1,38 +1,41 @@ -import {UIEventSource} from "../UIEventSource"; +import { UIEventSource } from "../UIEventSource" import * as idb from "idb-keyval" -import {Utils} from "../../Utils"; +import { Utils } from "../../Utils" /** * UIEventsource-wrapper around indexedDB key-value */ export class IdbLocalStorage { - private static readonly _sourceCache: Record> = {} - - public static Get(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource { - if(IdbLocalStorage._sourceCache[key] !== undefined){ + + public static Get( + key: string, + options?: { defaultValue?: T; whenLoaded?: (t: T | null) => void } + ): UIEventSource { + if (IdbLocalStorage._sourceCache[key] !== undefined) { return IdbLocalStorage._sourceCache[key] } const src = new UIEventSource(options?.defaultValue, "idb-local-storage:" + key) if (Utils.runningFromConsole) { - return src; + return src } - src.addCallback(v => idb.set(key, v)) - - idb.get(key).then(v => { - src.setData(v ?? options?.defaultValue); - if (options?.whenLoaded !== undefined) { - options?.whenLoaded(v) - } - }).catch(err => { - console.warn("Loading from local storage failed due to", err) - if (options?.whenLoaded !== undefined) { - options?.whenLoaded(null) - } - }) - IdbLocalStorage._sourceCache[key] = src; - return src; + src.addCallback((v) => idb.set(key, v)) + idb.get(key) + .then((v) => { + src.setData(v ?? options?.defaultValue) + if (options?.whenLoaded !== undefined) { + options?.whenLoaded(v) + } + }) + .catch((err) => { + console.warn("Loading from local storage failed due to", err) + if (options?.whenLoaded !== undefined) { + options?.whenLoaded(null) + } + }) + IdbLocalStorage._sourceCache[key] = src + return src } public static SetDirectly(key: string, value) { diff --git a/Logic/Web/LiveQueryHandler.ts b/Logic/Web/LiveQueryHandler.ts index a75703a95..29a05e0cd 100644 --- a/Logic/Web/LiveQueryHandler.ts +++ b/Logic/Web/LiveQueryHandler.ts @@ -1,51 +1,47 @@ -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource" +import { Utils } from "../../Utils" /** * Fetches data from random data sources, used in the metatagging */ export default class LiveQueryHandler { - private static neededShorthands = {} // url -> (shorthand:paths)[] - public static FetchLiveData(url: string, shorthands: string[]): UIEventSource string */> { - + public static FetchLiveData( + url: string, + shorthands: string[] + ): UIEventSource string */> { const shorthandsSet: string[] = LiveQueryHandler.neededShorthands[url] ?? [] for (const shorthand of shorthands) { if (shorthandsSet.indexOf(shorthand) < 0) { - shorthandsSet.push(shorthand); + shorthandsSet.push(shorthand) } } - LiveQueryHandler.neededShorthands[url] = shorthandsSet; - + LiveQueryHandler.neededShorthands[url] = shorthandsSet if (LiveQueryHandler[url] === undefined) { - const source = new UIEventSource({}); - LiveQueryHandler[url] = source; + const source = new UIEventSource({}) + LiveQueryHandler[url] = source console.log("Fetching live data from a third-party (unknown) API:", url) - Utils.downloadJson(url).then(data => { + Utils.downloadJson(url).then((data) => { for (const shorthandDescription of shorthandsSet) { - - const descr = shorthandDescription.trim().split(":"); - const shorthand = descr[0]; - const path = descr[1]; - const parts = path.split("."); - let trail = data; + const descr = shorthandDescription.trim().split(":") + const shorthand = descr[0] + const path = descr[1] + const parts = path.split(".") + let trail = data for (const part of parts) { if (trail !== undefined) { - trail = trail[part]; + trail = trail[part] } } - source.data[shorthand] = trail; + source.data[shorthand] = trail } - source.ping(); - + source.ping() }) - } - return LiveQueryHandler[url]; + return LiveQueryHandler[url] } - -} \ No newline at end of file +} diff --git a/Logic/Web/LocalStorageSource.ts b/Logic/Web/LocalStorageSource.ts index 2de23d413..8d83dafb1 100644 --- a/Logic/Web/LocalStorageSource.ts +++ b/Logic/Web/LocalStorageSource.ts @@ -1,13 +1,12 @@ -import {UIEventSource} from "../UIEventSource"; +import { UIEventSource } from "../UIEventSource" /** * UIEventsource-wrapper around localStorage */ export class LocalStorageSource { - static GetParsed(key: string, defaultValue: T): UIEventSource { return LocalStorageSource.Get(key).sync( - str => { + (str) => { if (str === undefined) { return defaultValue } @@ -16,29 +15,29 @@ export class LocalStorageSource { } catch { return defaultValue } - }, [], - value => JSON.stringify(value) + }, + [], + (value) => JSON.stringify(value) ) } static Get(key: string, defaultValue: string = undefined): UIEventSource { try { - const saved = localStorage.getItem(key); - const source = new UIEventSource(saved ?? defaultValue, "localstorage:" + key); + const saved = localStorage.getItem(key) + const source = new UIEventSource(saved ?? defaultValue, "localstorage:" + key) source.addCallback((data) => { try { - localStorage.setItem(key, data); + localStorage.setItem(key, data) } catch (e) { // Probably exceeded the quota with this item! // Lets nuke everything localStorage.clear() } - - }); - return source; + }) + return source } catch (e) { - return new UIEventSource(defaultValue); + return new UIEventSource(defaultValue) } } } diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index 328e6d6af..e54bf4b7f 100644 --- a/Logic/Web/MangroveReviews.ts +++ b/Logic/Web/MangroveReviews.ts @@ -1,31 +1,31 @@ -import * as mangrove from 'mangrove-reviews' -import {UIEventSource} from "../UIEventSource"; -import {Review} from "./Review"; -import {Utils} from "../../Utils"; +import * as mangrove from "mangrove-reviews" +import { UIEventSource } from "../UIEventSource" +import { Review } from "./Review" +import { Utils } from "../../Utils" export class MangroveIdentity { - public keypair: any = undefined; - public readonly kid: UIEventSource = new UIEventSource(undefined); - private readonly _mangroveIdentity: UIEventSource; + public keypair: any = undefined + public readonly kid: UIEventSource = new UIEventSource(undefined) + private readonly _mangroveIdentity: UIEventSource constructor(mangroveIdentity: UIEventSource) { - const self = this; - this._mangroveIdentity = mangroveIdentity; - mangroveIdentity.addCallbackAndRunD(str => { + const self = this + this._mangroveIdentity = mangroveIdentity + mangroveIdentity.addCallbackAndRunD((str) => { if (str === "") { - return; + return } - mangrove.jwkToKeypair(JSON.parse(str)).then(keypair => { - self.keypair = keypair; - mangrove.publicToPem(keypair.publicKey).then(pem => { + mangrove.jwkToKeypair(JSON.parse(str)).then((keypair) => { + self.keypair = keypair + mangrove.publicToPem(keypair.publicKey).then((pem) => { console.log("Identity loaded") - self.kid.setData(pem); + self.kid.setData(pem) }) }) }) try { if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { - this.CreateIdentity(); + this.CreateIdentity() } } catch (e) { console.error("Could not create identity: ", e) @@ -41,58 +41,62 @@ export class MangroveIdentity { if ("" !== (this._mangroveIdentity.data ?? "")) { throw "Identity already defined - not creating a new one" } - const self = this; - mangrove.generateKeypair().then( - keypair => { - self.keypair = keypair; - mangrove.keypairToJwk(keypair).then(jwk => { - self._mangroveIdentity.setData(JSON.stringify(jwk)); - }) - }); + const self = this + mangrove.generateKeypair().then((keypair) => { + self.keypair = keypair + mangrove.keypairToJwk(keypair).then((jwk) => { + self._mangroveIdentity.setData(JSON.stringify(jwk)) + }) + }) } - } export default class MangroveReviews { - private static _reviewsCache = {}; - private static didWarn = false; - private readonly _lon: number; - private readonly _lat: number; - private readonly _name: string; - private readonly _reviews: UIEventSource = new UIEventSource([]); - private _dryRun: boolean; - private _mangroveIdentity: MangroveIdentity; - private _lastUpdate: Date = undefined; + private static _reviewsCache = {} + private static didWarn = false + private readonly _lon: number + private readonly _lat: number + private readonly _name: string + private readonly _reviews: UIEventSource = new UIEventSource([]) + private _dryRun: boolean + private _mangroveIdentity: MangroveIdentity + private _lastUpdate: Date = undefined - private constructor(lon: number, lat: number, name: string, - identity: MangroveIdentity, - dryRun?: boolean) { - - this._lon = lon; - this._lat = lat; - this._name = name; - this._mangroveIdentity = identity; - this._dryRun = dryRun; + private constructor( + lon: number, + lat: number, + name: string, + identity: MangroveIdentity, + dryRun?: boolean + ) { + this._lon = lon + this._lat = lat + this._name = name + this._mangroveIdentity = identity + this._dryRun = dryRun if (dryRun && !MangroveReviews.didWarn) { - MangroveReviews.didWarn = true; + MangroveReviews.didWarn = true console.warn("Mangrove reviews will _not_ be saved as dryrun is specified") } - } - public static Get(lon: number, lat: number, name: string, - identity: MangroveIdentity, - dryRun?: boolean) { - const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun); + public static Get( + lon: number, + lat: number, + name: string, + identity: MangroveIdentity, + dryRun?: boolean + ) { + const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun) - const uri = newReviews.GetSubjectUri(); - const cached = MangroveReviews._reviewsCache[uri]; + const uri = newReviews.GetSubjectUri() + const cached = MangroveReviews._reviewsCache[uri] if (cached !== undefined) { - return cached; + return cached } - MangroveReviews._reviewsCache[uri] = newReviews; + MangroveReviews._reviewsCache[uri] = newReviews - return newReviews; + return newReviews } /** @@ -100,63 +104,64 @@ export default class MangroveReviews { * @constructor */ public GetSubjectUri() { - let uri = `geo:${this._lat},${this._lon}?u=50`; + let uri = `geo:${this._lat},${this._lon}?u=50` if (this._name !== undefined && this._name !== null) { - uri += "&q=" + this._name; + uri += "&q=" + this._name } - return uri; + return uri } - /** * Gives a UIEVentsource with all reviews. * Note: rating is between 1 and 100 */ public GetReviews(): UIEventSource { - - if (this._lastUpdate !== undefined && this._reviews.data !== undefined && - (new Date().getTime() - this._lastUpdate.getTime()) < 15000 + if ( + this._lastUpdate !== undefined && + this._reviews.data !== undefined && + new Date().getTime() - this._lastUpdate.getTime() < 15000 ) { // Last update was pretty recent - return this._reviews; + return this._reviews } - this._lastUpdate = new Date(); + this._lastUpdate = new Date() - const self = this; - mangrove.getReviews({sub: this.GetSubjectUri()}).then( - (data) => { - const reviews = []; - const reviewsByUser = []; - for (const review of data.reviews) { - const r = review.payload; + const self = this + mangrove.getReviews({ sub: this.GetSubjectUri() }).then((data) => { + const reviews = [] + const reviewsByUser = [] + for (const review of data.reviews) { + const r = review.payload - - console.log("PublicKey is ", self._mangroveIdentity.kid.data, "reviews.kid is", review.kid); - const byUser = self._mangroveIdentity.kid.map(data => data === review.signature); - const rev: Review = { - made_by_user: byUser, - date: new Date(r.iat * 1000), - comment: r.opinion, - author: r.metadata.nickname, - affiliated: r.metadata.is_affiliated, - rating: r.rating // percentage points - }; - - - (rev.made_by_user ? reviewsByUser : reviews).push(rev); + console.log( + "PublicKey is ", + self._mangroveIdentity.kid.data, + "reviews.kid is", + review.kid + ) + const byUser = self._mangroveIdentity.kid.map((data) => data === review.signature) + const rev: Review = { + made_by_user: byUser, + date: new Date(r.iat * 1000), + comment: r.opinion, + author: r.metadata.nickname, + affiliated: r.metadata.is_affiliated, + rating: r.rating, // percentage points } - self._reviews.setData(reviewsByUser.concat(reviews)) + + ;(rev.made_by_user ? reviewsByUser : reviews).push(rev) } - ); - return this._reviews; + self._reviews.setData(reviewsByUser.concat(reviews)) + }) + return this._reviews } - AddReview(r: Review, callback?: (() => void)) { - - - callback = callback ?? (() => { - return undefined; - }); + AddReview(r: Review, callback?: () => void) { + callback = + callback ?? + (() => { + return undefined + }) const payload = { sub: this.GetSubjectUri(), @@ -164,35 +169,29 @@ export default class MangroveReviews { opinion: r.comment, metadata: { nickname: r.author, - } - }; + }, + } if (r.affiliated) { // @ts-ignore - payload.metadata.is_affiliated = true; + payload.metadata.is_affiliated = true } if (this._dryRun) { - console.warn("DRYRUNNING mangrove reviews: ", payload); + console.warn("DRYRUNNING mangrove reviews: ", payload) if (callback) { if (callback) { - callback(); + callback() } - this._reviews.data.push(r); - this._reviews.ping(); - + this._reviews.data.push(r) + this._reviews.ping() } } else { mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => { if (callback) { - callback(); + callback() } - this._reviews.data.push(r); - this._reviews.ping(); - + this._reviews.data.push(r) + this._reviews.ping() }) } - - } - - -} \ No newline at end of file +} diff --git a/Logic/Web/PlantNet.ts b/Logic/Web/PlantNet.ts index d980cf403..d22cae763 100644 --- a/Logic/Web/PlantNet.ts +++ b/Logic/Web/PlantNet.ts @@ -1,346 +1,1020 @@ -import {Utils} from "../../Utils"; +import { Utils } from "../../Utils" export default class PlantNet { - private static baseUrl = "https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe" + private static baseUrl = + "https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe" - public static query(imageUrls: string[]) : Promise{ + public static query(imageUrls: string[]): Promise { if (imageUrls.length > 5) { throw "At most 5 images can be given to PlantNet.query" } if (imageUrls.length == 0) { throw "At least one image should be given to PlantNet.query" } - let url = PlantNet. baseUrl; + let url = PlantNet.baseUrl for (const image of imageUrls) { - url += "&images="+encodeURIComponent(image) + url += "&images=" + encodeURIComponent(image) } - return Utils.downloadJsonCached(url, 365*24*60*60*1000) + return Utils.downloadJsonCached(url, 365 * 24 * 60 * 60 * 1000) } public static exampleResult: PlantNetResult = { - "query": { - "project": "all", - "images": ["https://my.plantnet.org/images/image_1.jpeg", "https://my.plantnet.org/images/image_2.jpeg"], - "organs": ["flower", "leaf"], - "includeRelatedImages": false + query: { + project: "all", + images: [ + "https://my.plantnet.org/images/image_1.jpeg", + "https://my.plantnet.org/images/image_2.jpeg", + ], + organs: ["flower", "leaf"], + includeRelatedImages: false, }, - "language": "en", - "preferedReferential": "the-plant-list", - "bestMatch": "Hibiscus rosa-sinensis L.", - "results": [{ - "score": 0.91806, - "species": { - "scientificNameWithoutAuthor": "Hibiscus rosa-sinensis", - "scientificNameAuthorship": "L.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + language: "en", + preferedReferential: "the-plant-list", + bestMatch: "Hibiscus rosa-sinensis L.", + results: [ + { + score: 0.91806, + species: { + scientificNameWithoutAuthor: "Hibiscus rosa-sinensis", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Hawaiian hibiscus", "Hibiscus", "Chinese hibiscus"], + scientificName: "Hibiscus rosa-sinensis L.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Hawaiian hibiscus", "Hibiscus", "Chinese hibiscus"], - "scientificName": "Hibiscus rosa-sinensis L." + gbif: { id: "3152559" }, }, - "gbif": {"id": "3152559"} - }, { - "score": 0.00759, - "species": { - "scientificNameWithoutAuthor": "Hibiscus moscheutos", - "scientificNameAuthorship": "L.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00759, + species: { + scientificNameWithoutAuthor: "Hibiscus moscheutos", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Crimsoneyed rosemallow", "Mallow-rose", "Swamp rose-mallow"], + scientificName: "Hibiscus moscheutos L.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Crimsoneyed rosemallow", "Mallow-rose", "Swamp rose-mallow"], - "scientificName": "Hibiscus moscheutos L." + gbif: { id: "3152596" }, }, - "gbif": {"id": "3152596"} - }, { - "score": 0.00676, - "species": { - "scientificNameWithoutAuthor": "Hibiscus schizopetalus", - "scientificNameAuthorship": "(Dyer) Hook.f.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00676, + species: { + scientificNameWithoutAuthor: "Hibiscus schizopetalus", + scientificNameAuthorship: "(Dyer) Hook.f.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Campanilla", "Chinese lantern", "Fringed rosemallow"], + scientificName: "Hibiscus schizopetalus (Dyer) Hook.f.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Campanilla", "Chinese lantern", "Fringed rosemallow"], - "scientificName": "Hibiscus schizopetalus (Dyer) Hook.f." + gbif: { id: "9064581" }, }, - "gbif": {"id": "9064581"} - }, { - "score": 0.00544, - "species": { - "scientificNameWithoutAuthor": "Hibiscus palustris", - "scientificNameAuthorship": "L.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00544, + species: { + scientificNameWithoutAuthor: "Hibiscus palustris", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Swamp Rose Mallow", "Hardy Hidiscus", "Twisted Hibiscus"], + scientificName: "Hibiscus palustris L.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Swamp Rose Mallow", "Hardy Hidiscus", "Twisted Hibiscus"], - "scientificName": "Hibiscus palustris L." + gbif: { id: "6377046" }, }, - "gbif": {"id": "6377046"} - }, { - "score": 0.0047, - "species": { - "scientificNameWithoutAuthor": "Hibiscus sabdariffa", - "scientificNameAuthorship": "L.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.0047, + species: { + scientificNameWithoutAuthor: "Hibiscus sabdariffa", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Indian-sorrel", "Roselle", "Jamaica-sorrel"], + scientificName: "Hibiscus sabdariffa L.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Indian-sorrel", "Roselle", "Jamaica-sorrel"], - "scientificName": "Hibiscus sabdariffa L." + gbif: { id: "3152582" }, }, - "gbif": {"id": "3152582"} - }, { - "score": 0.0037, - "species": { - "scientificNameWithoutAuthor": "Abelmoschus moschatus", - "scientificNameAuthorship": "Medik.", - "genus": { - "scientificNameWithoutAuthor": "Abelmoschus", - "scientificNameAuthorship": "", - "scientificName": "Abelmoschus" + { + score: 0.0037, + species: { + scientificNameWithoutAuthor: "Abelmoschus moschatus", + scientificNameAuthorship: "Medik.", + genus: { + scientificNameWithoutAuthor: "Abelmoschus", + scientificNameAuthorship: "", + scientificName: "Abelmoschus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Musk okra", "Musk-mallow", "Tropical jewel-hibiscus"], + scientificName: "Abelmoschus moschatus Medik.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Musk okra", "Musk-mallow", "Tropical jewel-hibiscus"], - "scientificName": "Abelmoschus moschatus Medik." + gbif: { id: "8312665" }, }, - "gbif": {"id": "8312665"} - }, { - "score": 0.00278, - "species": { - "scientificNameWithoutAuthor": "Hibiscus grandiflorus", - "scientificNameAuthorship": "Michx.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00278, + species: { + scientificNameWithoutAuthor: "Hibiscus grandiflorus", + scientificNameAuthorship: "Michx.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Swamp rosemallow", "Swamp Rose-Mallow"], + scientificName: "Hibiscus grandiflorus Michx.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Swamp rosemallow", "Swamp Rose-Mallow"], - "scientificName": "Hibiscus grandiflorus Michx." + gbif: { id: "3152592" }, }, - "gbif": {"id": "3152592"} - }, { - "score": 0.00265, - "species": { - "scientificNameWithoutAuthor": "Hibiscus acetosella", - "scientificNameAuthorship": "Welw. ex Hiern", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00265, + species: { + scientificNameWithoutAuthor: "Hibiscus acetosella", + scientificNameAuthorship: "Welw. ex Hiern", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["False roselle", "Red-leaf hibiscus", "African rosemallow"], + scientificName: "Hibiscus acetosella Welw. ex Hiern", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["False roselle", "Red-leaf hibiscus", "African rosemallow"], - "scientificName": "Hibiscus acetosella Welw. ex Hiern" + gbif: { id: "3152551" }, }, - "gbif": {"id": "3152551"} - }, { - "score": 0.00253, - "species": { - "scientificNameWithoutAuthor": "Bixa orellana", - "scientificNameAuthorship": "L.", - "genus": { - "scientificNameWithoutAuthor": "Bixa", - "scientificNameAuthorship": "", - "scientificName": "Bixa" + { + score: 0.00253, + species: { + scientificNameWithoutAuthor: "Bixa orellana", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Bixa", + scientificNameAuthorship: "", + scientificName: "Bixa", + }, + family: { + scientificNameWithoutAuthor: "Bixaceae", + scientificNameAuthorship: "", + scientificName: "Bixaceae", + }, + commonNames: ["Arnatto", "Lipsticktree", "Annatto"], + scientificName: "Bixa orellana L.", }, - "family": { - "scientificNameWithoutAuthor": "Bixaceae", - "scientificNameAuthorship": "", - "scientificName": "Bixaceae" - }, - "commonNames": ["Arnatto", "Lipsticktree", "Annatto"], - "scientificName": "Bixa orellana L." + gbif: { id: "2874863" }, }, - "gbif": {"id": "2874863"} - }, { - "score": 0.00179, - "species": { - "scientificNameWithoutAuthor": "Malvaviscus penduliflorus", - "scientificNameAuthorship": "Moc. & Sessé ex DC.", - "genus": { - "scientificNameWithoutAuthor": "Malvaviscus", - "scientificNameAuthorship": "", - "scientificName": "Malvaviscus" + { + score: 0.00179, + species: { + scientificNameWithoutAuthor: "Malvaviscus penduliflorus", + scientificNameAuthorship: "Moc. & Sessé ex DC.", + genus: { + scientificNameWithoutAuthor: "Malvaviscus", + scientificNameAuthorship: "", + scientificName: "Malvaviscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Mazapan"], + scientificName: "Malvaviscus penduliflorus Moc. & Sessé ex DC.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Mazapan"], - "scientificName": "Malvaviscus penduliflorus Moc. & Sessé ex DC." + gbif: { id: "3152776" }, }, - "gbif": {"id": "3152776"} - }, { - "score": 0.00145, - "species": { - "scientificNameWithoutAuthor": "Hibiscus diversifolius", - "scientificNameAuthorship": "Jacq.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00145, + species: { + scientificNameWithoutAuthor: "Hibiscus diversifolius", + scientificNameAuthorship: "Jacq.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: ["Cape hibiscus", "Swamp hibiscus", "Comfortroot"], + scientificName: "Hibiscus diversifolius Jacq.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Cape hibiscus", "Swamp hibiscus", "Comfortroot"], - "scientificName": "Hibiscus diversifolius Jacq." + gbif: { id: "7279239" }, }, - "gbif": {"id": "7279239"} - }, { - "score": 0.00141, - "species": { - "scientificNameWithoutAuthor": "Hippeastrum reginae", - "scientificNameAuthorship": "(L.) Herb.", - "genus": { - "scientificNameWithoutAuthor": "Hippeastrum", - "scientificNameAuthorship": "", - "scientificName": "Hippeastrum" + { + score: 0.00141, + species: { + scientificNameWithoutAuthor: "Hippeastrum reginae", + scientificNameAuthorship: "(L.) Herb.", + genus: { + scientificNameWithoutAuthor: "Hippeastrum", + scientificNameAuthorship: "", + scientificName: "Hippeastrum", + }, + family: { + scientificNameWithoutAuthor: "Amaryllidaceae", + scientificNameAuthorship: "", + scientificName: "Amaryllidaceae", + }, + commonNames: ["Amaryllis", "Cheryl's Treasure", "Easter lily"], + scientificName: "Hippeastrum reginae (L.) Herb.", }, - "family": { - "scientificNameWithoutAuthor": "Amaryllidaceae", - "scientificNameAuthorship": "", - "scientificName": "Amaryllidaceae" - }, - "commonNames": ["Amaryllis", "Cheryl's Treasure", "Easter lily"], - "scientificName": "Hippeastrum reginae (L.) Herb." + gbif: { id: "2854474" }, }, - "gbif": {"id": "2854474"} - }, { - "score": 0.00114, - "species": { - "scientificNameWithoutAuthor": "Hibiscus martianus", - "scientificNameAuthorship": "Zucc.", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00114, + species: { + scientificNameWithoutAuthor: "Hibiscus martianus", + scientificNameAuthorship: "Zucc.", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: [ + "Heartleaf rosemallow", + "Mountain rosemallow", + "Heartleaf rose-mallow", + ], + scientificName: "Hibiscus martianus Zucc.", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["Heartleaf rosemallow", "Mountain rosemallow", "Heartleaf rose-mallow"], - "scientificName": "Hibiscus martianus Zucc." + gbif: { id: "3152578" }, }, - "gbif": {"id": "3152578"} - }, { - "score": 0.00109, - "species": { - "scientificNameWithoutAuthor": "Acalypha hispida", - "scientificNameAuthorship": "Burm.f.", - "genus": { - "scientificNameWithoutAuthor": "Acalypha", - "scientificNameAuthorship": "", - "scientificName": "Acalypha" + { + score: 0.00109, + species: { + scientificNameWithoutAuthor: "Acalypha hispida", + scientificNameAuthorship: "Burm.f.", + genus: { + scientificNameWithoutAuthor: "Acalypha", + scientificNameAuthorship: "", + scientificName: "Acalypha", + }, + family: { + scientificNameWithoutAuthor: "Euphorbiaceae", + scientificNameAuthorship: "", + scientificName: "Euphorbiaceae", + }, + commonNames: ["Philippine-medusa", "Bristly copperleaf", "Chenilleplant"], + scientificName: "Acalypha hispida Burm.f.", }, - "family": { - "scientificNameWithoutAuthor": "Euphorbiaceae", - "scientificNameAuthorship": "", - "scientificName": "Euphorbiaceae" - }, - "commonNames": ["Philippine-medusa", "Bristly copperleaf", "Chenilleplant"], - "scientificName": "Acalypha hispida Burm.f." + gbif: { id: "3056375" }, }, - "gbif": {"id": "3056375"} - }, { - "score": 0.00071, - "species": { - "scientificNameWithoutAuthor": "Hibiscus arnottianus", - "scientificNameAuthorship": "A. Gray", - "genus": { - "scientificNameWithoutAuthor": "Hibiscus", - "scientificNameAuthorship": "", - "scientificName": "Hibiscus" + { + score: 0.00071, + species: { + scientificNameWithoutAuthor: "Hibiscus arnottianus", + scientificNameAuthorship: "A. Gray", + genus: { + scientificNameWithoutAuthor: "Hibiscus", + scientificNameAuthorship: "", + scientificName: "Hibiscus", + }, + family: { + scientificNameWithoutAuthor: "Malvaceae", + scientificNameAuthorship: "", + scientificName: "Malvaceae", + }, + commonNames: [ + "White rosemallow", + "Native Hawaiian White Hibiscus", + "Native White Rose-Mallow", + ], + scientificName: "Hibiscus arnottianus A. Gray", }, - "family": { - "scientificNameWithoutAuthor": "Malvaceae", - "scientificNameAuthorship": "", - "scientificName": "Malvaceae" - }, - "commonNames": ["White rosemallow", "Native Hawaiian White Hibiscus", "Native White Rose-Mallow"], - "scientificName": "Hibiscus arnottianus A. Gray" + gbif: { id: "3152543" }, }, - "gbif": {"id": "3152543"} - }], - "version": "2022-06-14 (6.0)", - "remainingIdentificationRequests": 499 + ], + version: "2022-06-14 (6.0)", + remainingIdentificationRequests: 499, + } + public static exampleResultPrunus: PlantNetResult = { + query: { + project: "all", + images: ["https://i.imgur.com/VJp1qG1.jpg"], + organs: ["auto"], + includeRelatedImages: false, + }, + language: "en", + preferedReferential: "the-plant-list", + bestMatch: "Malus halliana Koehne", + results: [ + { + score: 0.23548, + species: { + scientificNameWithoutAuthor: "Malus halliana", + scientificNameAuthorship: "Koehne", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Hall crab apple", "Adirondack Crabapple", "Hall's crabapple"], + scientificName: "Malus halliana Koehne", + }, + gbif: { id: "3001220" }, + }, + { + score: 0.1514, + species: { + scientificNameWithoutAuthor: "Prunus campanulata", + scientificNameAuthorship: "Maxim.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Formosan cherry", "Bellflower cherry", "Taiwan cherry"], + scientificName: "Prunus campanulata Maxim.", + }, + gbif: { id: "3021408" }, + }, + { + score: 0.14758, + species: { + scientificNameWithoutAuthor: "Malus coronaria", + scientificNameAuthorship: "(L.) Mill.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Sweet crab apple", "American crabapple", "Fragrant crabapple"], + scientificName: "Malus coronaria (L.) Mill.", + }, + gbif: { id: "3001166" }, + }, + { + score: 0.13092, + species: { + scientificNameWithoutAuthor: "Prunus serrulata", + scientificNameAuthorship: "Lindl.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Japanese flowering cherry", + "Japanese cherry", + "Oriental cherry", + ], + scientificName: "Prunus serrulata Lindl.", + }, + gbif: { id: "3022609" }, + }, + { + score: 0.10147, + species: { + scientificNameWithoutAuthor: "Malus floribunda", + scientificNameAuthorship: "Siebold ex Van Houtte", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Japanese flowering Crabapple", + "Japanese crab", + "Japanese crab apple", + ], + scientificName: "Malus floribunda Siebold ex Van Houtte", + }, + gbif: { id: "3001365" }, + }, + { + score: 0.05122, + species: { + scientificNameWithoutAuthor: "Prunus sargentii", + scientificNameAuthorship: "Rehder", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Sargent's cherry", + "Northern Japanese hill cherry", + "Sargent Cherry", + ], + scientificName: "Prunus sargentii Rehder", + }, + gbif: { id: "3020955" }, + }, + { + score: 0.02576, + species: { + scientificNameWithoutAuthor: "Malus × spectabilis", + scientificNameAuthorship: "(Sol.) Borkh.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Asiatic apple", "Chinese crab", "Chinese flowering apple"], + scientificName: "Malus × spectabilis (Sol.) Borkh.", + }, + gbif: { id: "3001108" }, + }, + { + score: 0.01802, + species: { + scientificNameWithoutAuthor: "Prunus triloba", + scientificNameAuthorship: "Lindl.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Flowering almond", "Flowering plum"], + scientificName: "Prunus triloba Lindl.", + }, + gbif: { id: "3023130" }, + }, + { + score: 0.01206, + species: { + scientificNameWithoutAuthor: "Prunus japonica", + scientificNameAuthorship: "Thunb.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Chinese bush cherry", + "Japanese bush cherry", + "Oriental bush cherry", + ], + scientificName: "Prunus japonica Thunb.", + }, + gbif: { id: "3020565" }, + }, + { + score: 0.01161, + species: { + scientificNameWithoutAuthor: "Prunus × yedoensis", + scientificNameAuthorship: "Matsum.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Yoshino cherry", "Potomac cherry", "Tokyo cherry"], + scientificName: "Prunus × yedoensis Matsum.", + }, + gbif: { id: "3021335" }, + }, + { + score: 0.00914, + species: { + scientificNameWithoutAuthor: "Prunus mume", + scientificNameAuthorship: "(Siebold) Siebold & Zucc.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Japanese apricot", "Ume", "Chinese Plum"], + scientificName: "Prunus mume (Siebold) Siebold & Zucc.", + }, + gbif: { id: "3021046" }, + }, + { + score: 0.0088, + species: { + scientificNameWithoutAuthor: "Malus niedzwetzkyana", + scientificNameAuthorship: "Dieck ex Koehne", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Apple", "Paradise apple", "Kulturapfel"], + scientificName: "Malus niedzwetzkyana Dieck ex Koehne", + }, + gbif: { id: "3001327" }, + }, + { + score: 0.00734, + species: { + scientificNameWithoutAuthor: "Malus hupehensis", + scientificNameAuthorship: "(Pamp.) Rehder", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Chinese crab apple", "Hupeh crab", "Tea crab apple"], + scientificName: "Malus hupehensis (Pamp.) Rehder", + }, + gbif: { id: "3001077" }, + }, + { + score: 0.00688, + species: { + scientificNameWithoutAuthor: "Malus angustifolia", + scientificNameAuthorship: "(Aiton) Michx.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Southern crab apple", + "Narrow-leaved crabapple", + "Southern crabapple", + ], + scientificName: "Malus angustifolia (Aiton) Michx.", + }, + gbif: { id: "3001548" }, + }, + { + score: 0.00614, + species: { + scientificNameWithoutAuthor: "Prunus subhirtella", + scientificNameAuthorship: "Miq.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Rosebud cherry", "Spring cherry", "Autumn cherry"], + scientificName: "Prunus subhirtella Miq.", + }, + gbif: { id: "3021229" }, + }, + { + score: 0.00267, + species: { + scientificNameWithoutAuthor: "Robinia viscosa", + scientificNameAuthorship: "Vent.", + genus: { + scientificNameWithoutAuthor: "Robinia", + scientificNameAuthorship: "", + scientificName: "Robinia", + }, + family: { + scientificNameWithoutAuthor: "Leguminosae", + scientificNameAuthorship: "", + scientificName: "Leguminosae", + }, + commonNames: ["Clammy locust", "Rose acacia", "Clammy-bark locust"], + scientificName: "Robinia viscosa Vent.", + }, + gbif: { id: "5352245" }, + }, + { + score: 0.0026, + species: { + scientificNameWithoutAuthor: "Handroanthus impetiginosus", + scientificNameAuthorship: "(Mart. ex DC.) Mattos", + genus: { + scientificNameWithoutAuthor: "Handroanthus", + scientificNameAuthorship: "", + scientificName: "Handroanthus", + }, + family: { + scientificNameWithoutAuthor: "Bignoniaceae", + scientificNameAuthorship: "", + scientificName: "Bignoniaceae", + }, + commonNames: ["Pink trumpet-tree", "Taheebo", "Pink Trumpet Tree"], + scientificName: "Handroanthus impetiginosus (Mart. ex DC.) Mattos", + }, + gbif: { id: "4092242" }, + }, + { + score: 0.00187, + species: { + scientificNameWithoutAuthor: "Prunus glandulosa", + scientificNameAuthorship: "Thunb.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [ + "Chinese bush cherry", + "Dwarf flowering almond", + "Flowering almond", + ], + scientificName: "Prunus glandulosa Thunb.", + }, + gbif: { id: "3022160" }, + }, + { + score: 0.00162, + species: { + scientificNameWithoutAuthor: "Prunus persica", + scientificNameAuthorship: "(L.) Batsch", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Peach", "هلو", "Peach tree"], + scientificName: "Prunus persica (L.) Batsch", + }, + gbif: { id: "3022511" }, + }, + { + score: 0.00162, + species: { + scientificNameWithoutAuthor: "Prunus cerasifera", + scientificNameAuthorship: "Ehrh.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Cherry plum, myrobalan", "Cherry plum", "Myrobalan plum"], + scientificName: "Prunus cerasifera Ehrh.", + }, + gbif: { id: "3021730" }, + }, + { + score: 0.00159, + species: { + scientificNameWithoutAuthor: "Malus prattii", + scientificNameAuthorship: "(Hemsl.) C.K.Schneid.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Pratt apple", "Pratt's Crab Apple"], + scientificName: "Malus prattii (Hemsl.) C.K.Schneid.", + }, + gbif: { id: "3001504" }, + }, + { + score: 0.00159, + species: { + scientificNameWithoutAuthor: "Prunus pedunculata", + scientificNameAuthorship: "(Pall.) Maxim.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: [], + scientificName: "Prunus pedunculata (Pall.) Maxim.", + }, + gbif: { id: "3022277" }, + }, + { + score: 0.00153, + species: { + scientificNameWithoutAuthor: "Cercis siliquastrum", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Cercis", + scientificNameAuthorship: "", + scientificName: "Cercis", + }, + family: { + scientificNameWithoutAuthor: "Leguminosae", + scientificNameAuthorship: "", + scientificName: "Leguminosae", + }, + commonNames: ["Judastree", "Lovetree", "Judas-tree"], + scientificName: "Cercis siliquastrum L.", + }, + gbif: { id: "5353590" }, + }, + { + score: 0.00128, + species: { + scientificNameWithoutAuthor: "Malus sylvestris", + scientificNameAuthorship: "(L.) Mill.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Crab apple", "European crab apple", "Lopâr"], + scientificName: "Malus sylvestris (L.) Mill.", + }, + gbif: { id: "3001509" }, + }, + { + score: 0.0012, + species: { + scientificNameWithoutAuthor: "Magnolia × soulangeana", + scientificNameAuthorship: "Soul.-Bod.", + genus: { + scientificNameWithoutAuthor: "Magnolia", + scientificNameAuthorship: "", + scientificName: "Magnolia", + }, + family: { + scientificNameWithoutAuthor: "Magnoliaceae", + scientificNameAuthorship: "", + scientificName: "Magnoliaceae", + }, + commonNames: ["Chinese magnolia", "Saucer magnolia"], + scientificName: "Magnolia × soulangeana Soul.-Bod.", + }, + gbif: { id: "7925303" }, + }, + { + score: 0.00118, + species: { + scientificNameWithoutAuthor: "Cercis canadensis", + scientificNameAuthorship: "L.", + genus: { + scientificNameWithoutAuthor: "Cercis", + scientificNameAuthorship: "", + scientificName: "Cercis", + }, + family: { + scientificNameWithoutAuthor: "Leguminosae", + scientificNameAuthorship: "", + scientificName: "Leguminosae", + }, + commonNames: ["Eastern redbud", "Judastree", "Redbud"], + scientificName: "Cercis canadensis L.", + }, + gbif: { id: "5353583" }, + }, + { + score: 0.00114, + species: { + scientificNameWithoutAuthor: "Malus × prunifolia", + scientificNameAuthorship: "(Willd.) Borkh.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Plumleaf crab apple", "Chinese apple", "Crab apple"], + scientificName: "Malus × prunifolia (Willd.) Borkh.", + }, + gbif: { id: "3001157" }, + }, + { + score: 0.00111, + species: { + scientificNameWithoutAuthor: "Prunus serrula", + scientificNameAuthorship: "Franch.", + genus: { + scientificNameWithoutAuthor: "Prunus", + scientificNameAuthorship: "", + scientificName: "Prunus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Birchbark cherry"], + scientificName: "Prunus serrula Franch.", + }, + gbif: { id: "3023582" }, + }, + { + score: 0.00106, + species: { + scientificNameWithoutAuthor: "Malus pumila", + scientificNameAuthorship: "Mill.", + genus: { + scientificNameWithoutAuthor: "Malus", + scientificNameAuthorship: "", + scientificName: "Malus", + }, + family: { + scientificNameWithoutAuthor: "Rosaceae", + scientificNameAuthorship: "", + scientificName: "Rosaceae", + }, + commonNames: ["Apple", "Paradise apple", "Kulturapfel"], + scientificName: "Malus pumila Mill.", + }, + gbif: { id: "3001093" }, + }, + { + score: 0.00101, + species: { + scientificNameWithoutAuthor: "Viburnum farreri", + scientificNameAuthorship: "Stearn", + genus: { + scientificNameWithoutAuthor: "Viburnum", + scientificNameAuthorship: "", + scientificName: "Viburnum", + }, + family: { + scientificNameWithoutAuthor: "Adoxaceae", + scientificNameAuthorship: "", + scientificName: "Adoxaceae", + }, + commonNames: ["Fragrant viburnum", "Culver's root", "Farrer's Viburnum"], + scientificName: "Viburnum farreri Stearn", + }, + gbif: { id: "6369599" }, + }, + ], + version: "2022-06-14 (6.0)", + remainingIdentificationRequests: 498, } -public static exampleResultPrunus : PlantNetResult = {"query":{"project":"all","images":["https://i.imgur.com/VJp1qG1.jpg"],"organs":["auto"],"includeRelatedImages":false},"language":"en","preferedReferential":"the-plant-list","bestMatch":"Malus halliana Koehne","results":[{"score":0.23548,"species":{"scientificNameWithoutAuthor":"Malus halliana","scientificNameAuthorship":"Koehne","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Hall crab apple","Adirondack Crabapple","Hall's crabapple"],"scientificName":"Malus halliana Koehne"},"gbif":{"id":"3001220"}},{"score":0.1514,"species":{"scientificNameWithoutAuthor":"Prunus campanulata","scientificNameAuthorship":"Maxim.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Formosan cherry","Bellflower cherry","Taiwan cherry"],"scientificName":"Prunus campanulata Maxim."},"gbif":{"id":"3021408"}},{"score":0.14758,"species":{"scientificNameWithoutAuthor":"Malus coronaria","scientificNameAuthorship":"(L.) Mill.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Sweet crab apple","American crabapple","Fragrant crabapple"],"scientificName":"Malus coronaria (L.) Mill."},"gbif":{"id":"3001166"}},{"score":0.13092,"species":{"scientificNameWithoutAuthor":"Prunus serrulata","scientificNameAuthorship":"Lindl.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Japanese flowering cherry","Japanese cherry","Oriental cherry"],"scientificName":"Prunus serrulata Lindl."},"gbif":{"id":"3022609"}},{"score":0.10147,"species":{"scientificNameWithoutAuthor":"Malus floribunda","scientificNameAuthorship":"Siebold ex Van Houtte","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Japanese flowering Crabapple","Japanese crab","Japanese crab apple"],"scientificName":"Malus floribunda Siebold ex Van Houtte"},"gbif":{"id":"3001365"}},{"score":0.05122,"species":{"scientificNameWithoutAuthor":"Prunus sargentii","scientificNameAuthorship":"Rehder","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Sargent's cherry","Northern Japanese hill cherry","Sargent Cherry"],"scientificName":"Prunus sargentii Rehder"},"gbif":{"id":"3020955"}},{"score":0.02576,"species":{"scientificNameWithoutAuthor":"Malus × spectabilis","scientificNameAuthorship":"(Sol.) Borkh.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Asiatic apple","Chinese crab","Chinese flowering apple"],"scientificName":"Malus × spectabilis (Sol.) Borkh."},"gbif":{"id":"3001108"}},{"score":0.01802,"species":{"scientificNameWithoutAuthor":"Prunus triloba","scientificNameAuthorship":"Lindl.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Flowering almond","Flowering plum"],"scientificName":"Prunus triloba Lindl."},"gbif":{"id":"3023130"}},{"score":0.01206,"species":{"scientificNameWithoutAuthor":"Prunus japonica","scientificNameAuthorship":"Thunb.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Chinese bush cherry","Japanese bush cherry","Oriental bush cherry"],"scientificName":"Prunus japonica Thunb."},"gbif":{"id":"3020565"}},{"score":0.01161,"species":{"scientificNameWithoutAuthor":"Prunus × yedoensis","scientificNameAuthorship":"Matsum.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Yoshino cherry","Potomac cherry","Tokyo cherry"],"scientificName":"Prunus × yedoensis Matsum."},"gbif":{"id":"3021335"}},{"score":0.00914,"species":{"scientificNameWithoutAuthor":"Prunus mume","scientificNameAuthorship":"(Siebold) Siebold & Zucc.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Japanese apricot","Ume","Chinese Plum"],"scientificName":"Prunus mume (Siebold) Siebold & Zucc."},"gbif":{"id":"3021046"}},{"score":0.0088,"species":{"scientificNameWithoutAuthor":"Malus niedzwetzkyana","scientificNameAuthorship":"Dieck ex Koehne","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Apple","Paradise apple","Kulturapfel"],"scientificName":"Malus niedzwetzkyana Dieck ex Koehne"},"gbif":{"id":"3001327"}},{"score":0.00734,"species":{"scientificNameWithoutAuthor":"Malus hupehensis","scientificNameAuthorship":"(Pamp.) Rehder","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Chinese crab apple","Hupeh crab","Tea crab apple"],"scientificName":"Malus hupehensis (Pamp.) Rehder"},"gbif":{"id":"3001077"}},{"score":0.00688,"species":{"scientificNameWithoutAuthor":"Malus angustifolia","scientificNameAuthorship":"(Aiton) Michx.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Southern crab apple","Narrow-leaved crabapple","Southern crabapple"],"scientificName":"Malus angustifolia (Aiton) Michx."},"gbif":{"id":"3001548"}},{"score":0.00614,"species":{"scientificNameWithoutAuthor":"Prunus subhirtella","scientificNameAuthorship":"Miq.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Rosebud cherry","Spring cherry","Autumn cherry"],"scientificName":"Prunus subhirtella Miq."},"gbif":{"id":"3021229"}},{"score":0.00267,"species":{"scientificNameWithoutAuthor":"Robinia viscosa","scientificNameAuthorship":"Vent.","genus":{"scientificNameWithoutAuthor":"Robinia","scientificNameAuthorship":"","scientificName":"Robinia"},"family":{"scientificNameWithoutAuthor":"Leguminosae","scientificNameAuthorship":"","scientificName":"Leguminosae"},"commonNames":["Clammy locust","Rose acacia","Clammy-bark locust"],"scientificName":"Robinia viscosa Vent."},"gbif":{"id":"5352245"}},{"score":0.0026,"species":{"scientificNameWithoutAuthor":"Handroanthus impetiginosus","scientificNameAuthorship":"(Mart. ex DC.) Mattos","genus":{"scientificNameWithoutAuthor":"Handroanthus","scientificNameAuthorship":"","scientificName":"Handroanthus"},"family":{"scientificNameWithoutAuthor":"Bignoniaceae","scientificNameAuthorship":"","scientificName":"Bignoniaceae"},"commonNames":["Pink trumpet-tree","Taheebo","Pink Trumpet Tree"],"scientificName":"Handroanthus impetiginosus (Mart. ex DC.) Mattos"},"gbif":{"id":"4092242"}},{"score":0.00187,"species":{"scientificNameWithoutAuthor":"Prunus glandulosa","scientificNameAuthorship":"Thunb.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Chinese bush cherry","Dwarf flowering almond","Flowering almond"],"scientificName":"Prunus glandulosa Thunb."},"gbif":{"id":"3022160"}},{"score":0.00162,"species":{"scientificNameWithoutAuthor":"Prunus persica","scientificNameAuthorship":"(L.) Batsch","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Peach","هلو","Peach tree"],"scientificName":"Prunus persica (L.) Batsch"},"gbif":{"id":"3022511"}},{"score":0.00162,"species":{"scientificNameWithoutAuthor":"Prunus cerasifera","scientificNameAuthorship":"Ehrh.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Cherry plum, myrobalan","Cherry plum","Myrobalan plum"],"scientificName":"Prunus cerasifera Ehrh."},"gbif":{"id":"3021730"}},{"score":0.00159,"species":{"scientificNameWithoutAuthor":"Malus prattii","scientificNameAuthorship":"(Hemsl.) C.K.Schneid.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Pratt apple","Pratt's Crab Apple"],"scientificName":"Malus prattii (Hemsl.) C.K.Schneid."},"gbif":{"id":"3001504"}},{"score":0.00159,"species":{"scientificNameWithoutAuthor":"Prunus pedunculata","scientificNameAuthorship":"(Pall.) Maxim.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":[],"scientificName":"Prunus pedunculata (Pall.) Maxim."},"gbif":{"id":"3022277"}},{"score":0.00153,"species":{"scientificNameWithoutAuthor":"Cercis siliquastrum","scientificNameAuthorship":"L.","genus":{"scientificNameWithoutAuthor":"Cercis","scientificNameAuthorship":"","scientificName":"Cercis"},"family":{"scientificNameWithoutAuthor":"Leguminosae","scientificNameAuthorship":"","scientificName":"Leguminosae"},"commonNames":["Judastree","Lovetree","Judas-tree"],"scientificName":"Cercis siliquastrum L."},"gbif":{"id":"5353590"}},{"score":0.00128,"species":{"scientificNameWithoutAuthor":"Malus sylvestris","scientificNameAuthorship":"(L.) Mill.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Crab apple","European crab apple","Lopâr"],"scientificName":"Malus sylvestris (L.) Mill."},"gbif":{"id":"3001509"}},{"score":0.0012,"species":{"scientificNameWithoutAuthor":"Magnolia × soulangeana","scientificNameAuthorship":"Soul.-Bod.","genus":{"scientificNameWithoutAuthor":"Magnolia","scientificNameAuthorship":"","scientificName":"Magnolia"},"family":{"scientificNameWithoutAuthor":"Magnoliaceae","scientificNameAuthorship":"","scientificName":"Magnoliaceae"},"commonNames":["Chinese magnolia","Saucer magnolia"],"scientificName":"Magnolia × soulangeana Soul.-Bod."},"gbif":{"id":"7925303"}},{"score":0.00118,"species":{"scientificNameWithoutAuthor":"Cercis canadensis","scientificNameAuthorship":"L.","genus":{"scientificNameWithoutAuthor":"Cercis","scientificNameAuthorship":"","scientificName":"Cercis"},"family":{"scientificNameWithoutAuthor":"Leguminosae","scientificNameAuthorship":"","scientificName":"Leguminosae"},"commonNames":["Eastern redbud","Judastree","Redbud"],"scientificName":"Cercis canadensis L."},"gbif":{"id":"5353583"}},{"score":0.00114,"species":{"scientificNameWithoutAuthor":"Malus × prunifolia","scientificNameAuthorship":"(Willd.) Borkh.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Plumleaf crab apple","Chinese apple","Crab apple"],"scientificName":"Malus × prunifolia (Willd.) Borkh."},"gbif":{"id":"3001157"}},{"score":0.00111,"species":{"scientificNameWithoutAuthor":"Prunus serrula","scientificNameAuthorship":"Franch.","genus":{"scientificNameWithoutAuthor":"Prunus","scientificNameAuthorship":"","scientificName":"Prunus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Birchbark cherry"],"scientificName":"Prunus serrula Franch."},"gbif":{"id":"3023582"}},{"score":0.00106,"species":{"scientificNameWithoutAuthor":"Malus pumila","scientificNameAuthorship":"Mill.","genus":{"scientificNameWithoutAuthor":"Malus","scientificNameAuthorship":"","scientificName":"Malus"},"family":{"scientificNameWithoutAuthor":"Rosaceae","scientificNameAuthorship":"","scientificName":"Rosaceae"},"commonNames":["Apple","Paradise apple","Kulturapfel"],"scientificName":"Malus pumila Mill."},"gbif":{"id":"3001093"}},{"score":0.00101,"species":{"scientificNameWithoutAuthor":"Viburnum farreri","scientificNameAuthorship":"Stearn","genus":{"scientificNameWithoutAuthor":"Viburnum","scientificNameAuthorship":"","scientificName":"Viburnum"},"family":{"scientificNameWithoutAuthor":"Adoxaceae","scientificNameAuthorship":"","scientificName":"Adoxaceae"},"commonNames":["Fragrant viburnum","Culver's root","Farrer's Viburnum"],"scientificName":"Viburnum farreri Stearn"},"gbif":{"id":"6369599"}}],"version":"2022-06-14 (6.0)","remainingIdentificationRequests":498} } export interface PlantNetResult { - "query": { - "project": string, "images": string[], - "organs": string[], - "includeRelatedImages": boolean - }, - "language": string, - "preferedReferential": string, - "bestMatch": string, - "results": { - "score": number, - "gbif": { "id": string /*Actually a number*/ } - "species": - { - "scientificNameWithoutAuthor": string, - "scientificNameAuthorship": string, - "genus": { "scientificNameWithoutAuthor": string, scientificNameAuthorship: string, "scientificName": string }, - "family": { "scientificNameWithoutAuthor": string, scientificNameAuthorship: string, "scientificName": string }, - "commonNames": string [], - "scientificName": string - }, - }[], - "version": string, - "remainingIdentificationRequests": number + query: { + project: string + images: string[] + organs: string[] + includeRelatedImages: boolean + } + language: string + preferedReferential: string + bestMatch: string + results: { + score: number + gbif: { id: string /*Actually a number*/ } + species: { + scientificNameWithoutAuthor: string + scientificNameAuthorship: string + genus: { + scientificNameWithoutAuthor: string + scientificNameAuthorship: string + scientificName: string + } + family: { + scientificNameWithoutAuthor: string + scientificNameAuthorship: string + scientificName: string + } + commonNames: string[] + scientificName: string + } + }[] + version: string + remainingIdentificationRequests: number } diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 2257bf51f..96903106c 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -1,42 +1,52 @@ /** * Wraps the query parameters into UIEventSources */ -import {UIEventSource} from "../UIEventSource"; -import Hash from "./Hash"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource" +import Hash from "./Hash" +import { Utils } from "../../Utils" export class QueryParameters { - static defaults = {} static documentation: Map = new Map() - private static order: string [] = ["layout", "test", "z", "lat", "lon"]; + private static order: string[] = ["layout", "test", "z", "lat", "lon"] private static _wasInitialized: Set = new Set() - private static knownSources = {}; - private static initialized = false; + private static knownSources = {} + private static initialized = false - public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource { + public static GetQueryParameter( + key: string, + deflt: string, + documentation?: string + ): UIEventSource { if (!this.initialized) { - this.init(); + this.init() } - QueryParameters.documentation.set(key, documentation); + QueryParameters.documentation.set(key, documentation) if (deflt !== undefined) { - QueryParameters.defaults[key] = deflt; + QueryParameters.defaults[key] = deflt } if (QueryParameters.knownSources[key] !== undefined) { - return QueryParameters.knownSources[key]; + return QueryParameters.knownSources[key] } - QueryParameters.addOrder(key); - const source = new UIEventSource(deflt, "&" + key); - QueryParameters.knownSources[key] = source; + QueryParameters.addOrder(key) + const source = new UIEventSource(deflt, "&" + key) + QueryParameters.knownSources[key] = source source.addCallback(() => QueryParameters.Serialize()) - return source; + return source } - public static GetBooleanQueryParameter(key: string, deflt: boolean, documentation?: string): UIEventSource { - return QueryParameters.GetQueryParameter(key, ""+ deflt, documentation).sync(str => str === "true", [], b => "" + b) + public static GetBooleanQueryParameter( + key: string, + deflt: boolean, + documentation?: string + ): UIEventSource { + return QueryParameters.GetQueryParameter(key, "" + deflt, documentation).sync( + (str) => str === "true", + [], + (b) => "" + b + ) } - public static wasInitialized(key: string): boolean { return QueryParameters._wasInitialized.has(key) } @@ -48,53 +58,54 @@ export class QueryParameters { } private static init() { - if (this.initialized) { - return; + return } - this.initialized = true; + this.initialized = true if (Utils.runningFromConsole) { - return; + return } if (window?.location?.search) { - const params = window.location.search.substr(1).split("&"); + const params = window.location.search.substr(1).split("&") for (const param of params) { - const kv = param.split("="); - const key = decodeURIComponent(kv[0]); + const kv = param.split("=") + const key = decodeURIComponent(kv[0]) QueryParameters.addOrder(key) QueryParameters._wasInitialized.add(key) - const v = decodeURIComponent(kv[1]); - const source = new UIEventSource(v); + const v = decodeURIComponent(kv[1]) + const source = new UIEventSource(v) source.addCallback(() => QueryParameters.Serialize()) - QueryParameters.knownSources[key] = source; + QueryParameters.knownSources[key] = source } } - } private static Serialize() { const parts = [] for (const key of QueryParameters.order) { if (QueryParameters.knownSources[key]?.data === undefined) { - continue; + continue } if (QueryParameters.knownSources[key].data === "undefined") { - continue; + continue } if (QueryParameters.knownSources[key].data === QueryParameters.defaults[key]) { - continue; + continue } - parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) + parts.push( + encodeURIComponent(key) + + "=" + + encodeURIComponent(QueryParameters.knownSources[key].data) + ) } - if(!Utils.runningFromConsole){ + if (!Utils.runningFromConsole) { // Don't pollute the history every time a parameter changes - history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()); + history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()) } - } -} \ No newline at end of file +} diff --git a/Logic/Web/Review.ts b/Logic/Web/Review.ts index 4fe467b91..9834192cd 100644 --- a/Logic/Web/Review.ts +++ b/Logic/Web/Review.ts @@ -1,13 +1,13 @@ -import {Store} from "../UIEventSource"; +import { Store } from "../UIEventSource" export interface Review { - comment?: string, - author: string, - date: Date, - rating: number, - affiliated: boolean, + comment?: string + author: string + date: Date + rating: number + affiliated: boolean /** * True if the current logged in user is the creator of this comment */ made_by_user: Store -} \ No newline at end of file +} diff --git a/Logic/Web/Wikidata.ts b/Logic/Web/Wikidata.ts index 910b26d0d..cd09485d5 100644 --- a/Logic/Web/Wikidata.ts +++ b/Logic/Web/Wikidata.ts @@ -1,5 +1,5 @@ -import {Utils} from "../../Utils"; -import {UIEventSource} from "../UIEventSource"; +import { Utils } from "../../Utils" +import { UIEventSource } from "../UIEventSource" import * as wds from "wikidata-sdk" export class WikidataResponse { @@ -18,14 +18,12 @@ export class WikidataResponse { wikisites: Map, commons: string ) { - this.id = id this.labels = labels this.descriptions = descriptions this.claims = claims this.wikisites = wikisites this.commons = commons - } public static fromJson(entity: any): WikidataResponse { @@ -41,7 +39,7 @@ export class WikidataResponse { descr.set(labelName, entity.descriptions[labelName].value) } - const sitelinks = new Map(); + const sitelinks = new Map() for (const labelName in entity.sitelinks) { // labelName is `${language}wiki` const language = labelName.substring(0, labelName.length - 4) @@ -51,28 +49,19 @@ export class WikidataResponse { const commons = sitelinks.get("commons") sitelinks.delete("commons") - const claims = WikidataResponse.extractClaims(entity.claims); - return new WikidataResponse( - entity.id, - labels, - descr, - claims, - sitelinks, - commons - ) - + const claims = WikidataResponse.extractClaims(entity.claims) + return new WikidataResponse(entity.id, labels, descr, claims, sitelinks, commons) } static extractClaims(claimsJson: any): Map> { - const simplified = wds.simplify.claims(claimsJson, { - timeConverter: 'simple-day' + timeConverter: "simple-day", }) - const claims = new Map>(); + const claims = new Map>() for (const claimId in simplified) { const claimsList: any[] = simplified[claimId] - claims.set(claimId, new Set(claimsList)); + claims.set(claimId, new Set(claimsList)) } return claims } @@ -84,7 +73,6 @@ export class WikidataLexeme { senses: Map claims: Map> - constructor(json) { this.id = json.id this.claims = WikidataResponse.extractClaims(json.claims) @@ -117,36 +105,40 @@ export class WikidataLexeme { this.claims, new Map(), undefined - ); + ) } } export interface WikidataSearchoptions { - lang?: "en" | string, + lang?: "en" | string maxCount?: 20 | number } export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { - instanceOf?: number[]; + instanceOf?: number[] notInstanceOf?: number[] } - /** * Utility functions around wikidata */ export default class Wikidata { - - private static readonly _identifierPrefixes = ["Q", "L"].map(str => str.toLowerCase()) - private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:", + private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase()) + private static readonly _prefixesToRemove = [ + "https://www.wikidata.org/wiki/Lexeme:", "https://www.wikidata.org/wiki/", "http://www.wikidata.org/entity/", - "Lexeme:"].map(str => str.toLowerCase()) + "Lexeme:", + ].map((str) => str.toLowerCase()) + private static readonly _cache = new Map< + string, + UIEventSource<{ success: WikidataResponse } | { error: any }> + >() - private static readonly _cache = new Map>() - - public static LoadWikidataEntry(value: string | number): UIEventSource<{ success: WikidataResponse } | { error: any }> { + public static LoadWikidataEntry( + value: string | number + ): UIEventSource<{ success: WikidataResponse } | { error: any }> { const key = this.ExtractKey(value) const cached = Wikidata._cache.get(key) if (cached !== undefined) { @@ -154,27 +146,31 @@ export default class Wikidata { } const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) Wikidata._cache.set(key, src) - return src; + return src } /** * Given a search text, searches for the relevant wikidata entries, excluding pages "outside of the main tree", e.g. disambiguation pages. * Optionally, an 'instance of' can be given to limit the scope, e.g. instanceOf:5 (humans) will only search for humans */ - public static async searchAdvanced(text: string, options: WikidataAdvancedSearchoptions): Promise<{ - id: string, - relevance?: number, - label: string, - description?: string - }[]> { + public static async searchAdvanced( + text: string, + options: WikidataAdvancedSearchoptions + ): Promise< + { + id: string + relevance?: number + label: string + description?: string + }[] + > { let instanceOf = "" if (options?.instanceOf !== undefined && options.instanceOf.length > 0) { - const phrases = options.instanceOf.map(q => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) - instanceOf = "{"+ phrases.join(" UNION ") + "}" + const phrases = options.instanceOf.map((q) => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`) + instanceOf = "{" + phrases.join(" UNION ") + "}" } - const forbidden = (options?.notInstanceOf ?? []) - .concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages - const minusPhrases = forbidden.map(q => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`) + const forbidden = (options?.notInstanceOf ?? []).concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages + const minusPhrases = forbidden.map((q) => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`) const sparql = `SELECT * WHERE { SERVICE wikibase:mwapi { bd:serviceParam wikibase:api "EntitySearch" . @@ -183,7 +179,11 @@ export default class Wikidata { bd:serviceParam mwapi:language "${options.lang}" . ?item wikibase:apiOutputItem mwapi:item . ?num wikibase:apiOrdinal true . - bd:serviceParam wikibase:limit ${Math.round((options.maxCount ?? 20) * 1.5) /*Some padding for disambiguation pages */} . + bd:serviceParam wikibase:limit ${ + Math.round( + (options.maxCount ?? 20) * 1.5 + ) /*Some padding for disambiguation pages */ + } . ?label wikibase:apiOutput mwapi:label . ?description wikibase:apiOutput "@description" . } @@ -195,11 +195,11 @@ export default class Wikidata { const result = await Utils.downloadJson(url) /*The full uri of the wikidata-item*/ - return result.results.bindings.map(({item, label, description, num}) => ({ + return result.results.bindings.map(({ item, label, description, num }) => ({ relevance: num?.value, id: item?.value, label: label?.value, - description: description?.value + description: description?.value, })) } @@ -207,47 +207,47 @@ export default class Wikidata { search: string, options?: WikidataSearchoptions, page = 1 - ): Promise<{ - id: string, - label: string, - description: string - }[]> { + ): Promise< + { + id: string + label: string + description: string + }[] + > { const maxCount = options?.maxCount ?? 20 let pageCount = Math.min(maxCount, 50) - const start = page * pageCount - pageCount; - const lang = (options?.lang ?? "en") + const start = page * pageCount - pageCount + const lang = options?.lang ?? "en" const url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" + search + "&language=" + lang + - "&limit=" + pageCount + "&continue=" + + "&limit=" + + pageCount + + "&continue=" + start + "&format=json&uselang=" + lang + "&type=item&origin=*" + - "&props=";// props= removes some unused values in the result + "&props=" // props= removes some unused values in the result const response = await Utils.downloadJsonCached(url, 10000) const result: any[] = response.search if (result.length < pageCount) { // No next page - return result; + return result } if (result.length < maxCount) { - const newOptions = {...options} + const newOptions = { ...options } newOptions.maxCount = maxCount - result.length - result.push(...await Wikidata.search(search, - newOptions, - page + 1 - )) + result.push(...(await Wikidata.search(search, newOptions, page + 1))) } - return result; + return result } - public static async searchAndFetch( search: string, options?: WikidataAdvancedSearchoptions @@ -255,16 +255,17 @@ export default class Wikidata { // We provide some padding to filter away invalid values const searchResults = await Wikidata.searchAdvanced(search, options) const maybeResponses = await Promise.all( - searchResults.map(async r => { + searchResults.map(async (r) => { try { console.log("Loading ", r.id) return await Wikidata.LoadWikidataEntry(r.id).AsPromise() } catch (e) { console.error(e) - return undefined; + return undefined } - })) - return Utils.NoNull(maybeResponses.map(r => r["success"])) + }) + ) + return Utils.NoNull(maybeResponses.map((r) => r["success"])) } /** @@ -279,7 +280,7 @@ export default class Wikidata { } if (value === undefined) { console.error("ExtractKey: value is undefined") - return undefined; + return undefined } value = value.trim().toLowerCase() @@ -296,7 +297,7 @@ export default class Wikidata { for (const identifierPrefix of Wikidata._identifierPrefixes) { if (value.startsWith(identifierPrefix)) { - const trimmed = value.substring(identifierPrefix.length); + const trimmed = value.substring(identifierPrefix.length) if (trimmed === "") { return undefined } @@ -304,7 +305,7 @@ export default class Wikidata { if (isNaN(n)) { return undefined } - return value.toUpperCase(); + return value.toUpperCase() } } @@ -312,7 +313,7 @@ export default class Wikidata { return "Q" + value } - return undefined; + return undefined } /** @@ -326,10 +327,10 @@ export default class Wikidata { * Wikidata.QIdToNumber(123) // => 123 */ public static QIdToNumber(q: string | number): number | undefined { - if(q === undefined || q === null){ + if (q === undefined || q === null) { return } - if(typeof q === "number"){ + if (typeof q === "number") { return q } q = q.trim() @@ -356,17 +357,23 @@ export default class Wikidata { /** * Build a SPARQL-query, return the result - * + * * @param keys: how variables are named. Every key not ending with 'Label' should appear in at least one statement * @param statements * @constructor */ - public static async Sparql(keys: string[], statements: string[]):Promise< (T & Record) []> { - const query = "SELECT "+keys.map(k => k.startsWith("?") ? k : "?"+k).join(" ")+"\n" + + public static async Sparql( + keys: string[], + statements: string[] + ): Promise<(T & Record)[]> { + const query = + "SELECT " + + keys.map((k) => (k.startsWith("?") ? k : "?" + k)).join(" ") + + "\n" + "WHERE\n" + "{\n" + - statements.map(stmt => stmt.endsWith(".") ? stmt : stmt+".").join("\n") + - " SERVICE wikibase:label { bd:serviceParam wikibase:language \"[AUTO_LANGUAGE]\". }\n" + + statements.map((stmt) => (stmt.endsWith(".") ? stmt : stmt + ".")).join("\n") + + ' SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }\n' + "}" const url = wds.sparqlQuery(query) const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) @@ -384,7 +391,7 @@ export default class Wikidata { return undefined } - const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json"; + const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json" const entities = (await Utils.downloadJsonCached(url, 10000)).entities const firstKey = Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect const response = entities[firstKey] @@ -396,5 +403,4 @@ export default class Wikidata { return WikidataResponse.fromJson(response) } - } diff --git a/Logic/Web/Wikimedia.ts b/Logic/Web/Wikimedia.ts index 56ad0596b..c0f34b44e 100644 --- a/Logic/Web/Wikimedia.ts +++ b/Logic/Web/Wikimedia.ts @@ -1,4 +1,4 @@ -import {Utils} from "../../Utils"; +import { Utils } from "../../Utils" export default class Wikimedia { /** @@ -8,40 +8,48 @@ export default class Wikimedia { * @param maxLoad: the maximum amount of images to return * @param continueParameter: if the page indicates that more pages should be loaded, this uses a token to continue. Provided by wikimedia */ - public static async GetCategoryContents(categoryName: string, - maxLoad = 10, - continueParameter: string = undefined): Promise { + public static async GetCategoryContents( + categoryName: string, + maxLoad = 10, + continueParameter: string = undefined + ): Promise { if (categoryName === undefined || categoryName === null || categoryName === "") { - return []; + return [] } if (!categoryName.startsWith("Category:")) { - categoryName = "Category:" + categoryName; + categoryName = "Category:" + categoryName } - let url = "https://commons.wikimedia.org/w/api.php?" + + let url = + "https://commons.wikimedia.org/w/api.php?" + "action=query&list=categorymembers&format=json&" + "&origin=*" + - "&cmtitle=" + encodeURIComponent(categoryName); + "&cmtitle=" + + encodeURIComponent(categoryName) if (continueParameter !== undefined) { - url = `${url}&cmcontinue=${continueParameter}`; + url = `${url}&cmcontinue=${continueParameter}` } const response = await Utils.downloadJson(url) - const members = response.query?.categorymembers ?? []; - const imageOverview: string[] = members.map(member => member.title); + const members = response.query?.categorymembers ?? [] + const imageOverview: string[] = members.map((member) => member.title) if (response.continue === undefined) { // We are done crawling through the category - no continuation in sight - return imageOverview; + return imageOverview } if (maxLoad - imageOverview.length <= 0) { console.debug(`Recursive wikimedia category load stopped for ${categoryName}`) - return imageOverview; + return imageOverview } // We do have a continue token - let's load the next page - const recursive = await Wikimedia.GetCategoryContents(categoryName, maxLoad - imageOverview.length, response.continue.cmcontinue) + const recursive = await Wikimedia.GetCategoryContents( + categoryName, + maxLoad - imageOverview.length, + response.continue.cmcontinue + ) imageOverview.push(...recursive) return imageOverview } -} \ No newline at end of file +} diff --git a/Logic/Web/Wikipedia.ts b/Logic/Web/Wikipedia.ts index 4a079c251..6be26931c 100644 --- a/Logic/Web/Wikipedia.ts +++ b/Logic/Web/Wikipedia.ts @@ -1,12 +1,11 @@ /** * Some usefull utility functions around the wikipedia API */ -import {Utils} from "../../Utils"; -import {UIEventSource} from "../UIEventSource"; -import {WikipediaBoxOptions} from "../../UI/Wikipedia/WikipediaBox"; +import { Utils } from "../../Utils" +import { UIEventSource } from "../UIEventSource" +import { WikipediaBoxOptions } from "../../UI/Wikipedia/WikipediaBox" export default class Wikipedia { - /** * When getting a wikipedia page data result, some elements (e.g. navigation, infoboxes, ...) should be removed if 'removeInfoBoxes' is set. * We do this based on the classes. This set contains a blacklist of the classes to remove @@ -15,26 +14,27 @@ export default class Wikipedia { private static readonly classesToRemove = [ "shortdescription", "sidebar", - "infobox", "infobox_v2", + "infobox", + "infobox_v2", "noprint", "ambox", "mw-editsection", "mw-selflink", "mw-empty-elt", - "hatnote" // Often redirects + "hatnote", // Often redirects ] - private static readonly idsToRemove = [ - "sjabloon_zie" - ] + private static readonly idsToRemove = ["sjabloon_zie"] - private static readonly _cache = new Map>() + private static readonly _cache = new Map< + string, + UIEventSource<{ success: string } | { error: any }> + >() + public readonly backend: string - public readonly backend: string; - - constructor(options?: ({ language?: "en" | string } | { backend?: string })) { - this.backend = Wikipedia.getBackendUrl(options ?? {}); + constructor(options?: { language?: "en" | string } | { backend?: string }) { + this.backend = Wikipedia.getBackendUrl(options ?? {}) } /** @@ -43,30 +43,31 @@ export default class Wikipedia { * Wikipedia.extractLanguageAndName("qsdf") // => undefined * Wikipedia.extractLanguageAndName("nl:Warandeputten") // => {language: "nl", pageName: "Warandeputten"} */ - public static extractLanguageAndName(input: string): { language: string, pageName: string } { + public static extractLanguageAndName(input: string): { language: string; pageName: string } { const matched = input.match("([^:]+):(.*)") if (matched === undefined || matched === null) { return undefined } const [_, language, pageName] = matched return { - language, pageName + language, + pageName, } } /** * Extracts the actual pagename; returns undefined if this came from a different wikimedia entry - * + * * new Wikipedia({backend: "https://wiki.openstreetmap.org"}).extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => "NL:Speelbos" * new Wikipedia().extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => undefined */ - public extractPageName(input: string):string | undefined{ - if(!input.startsWith(this.backend)){ + public extractPageName(input: string): string | undefined { + if (!input.startsWith(this.backend)) { return undefined } - input = input.substring(this.backend.length); - - const matched = input.match("/?wiki/\(.+\)") + input = input.substring(this.backend.length) + + const matched = input.match("/?wiki/(.+)") if (matched === undefined || matched === null) { return undefined } @@ -74,7 +75,9 @@ export default class Wikipedia { return pageName } - private static getBackendUrl(options: { language?: "en" | string } | { backend?: "en.wikipedia.org" | string }): string { + private static getBackendUrl( + options: { language?: "en" | string } | { backend?: "en.wikipedia.org" | string } + ): string { let backend = "en.wikipedia.org" if (options["backend"]) { backend = options["backend"] @@ -87,7 +90,10 @@ export default class Wikipedia { return backend } - public GetArticle(pageName: string, options: WikipediaBoxOptions): UIEventSource<{ success: string } | { error: any }> { + public GetArticle( + pageName: string, + options: WikipediaBoxOptions + ): UIEventSource<{ success: string } | { error: any }> { const key = this.backend + ":" + pageName + ":" + (options.firstParagraphOnly ?? false) const cached = Wikipedia._cache.get(key) if (cached !== undefined) { @@ -95,11 +101,13 @@ export default class Wikipedia { } const v = UIEventSource.FromPromiseWithErr(this.GetArticleAsync(pageName, options)) Wikipedia._cache.set(key, v) - return v; + return v } public getDataUrl(pageName: string): string { - return `${this.backend}/w/api.php?action=parse&format=json&origin=*&prop=text&page=` + pageName + return ( + `${this.backend}/w/api.php?action=parse&format=json&origin=*&prop=text&page=` + pageName + ) } public getPageUrl(pageName: string): string { @@ -110,9 +118,12 @@ export default class Wikipedia { * Textual search of the specified wiki-instance. If searching Wikipedia, we recommend using wikidata.search instead * @param searchTerm */ - public async search(searchTerm: string): Promise<{ title: string, snippet: string }[]> { - const url = this.backend + "/w/api.php?action=query&format=json&list=search&srsearch=" + encodeURIComponent(searchTerm); - return (await Utils.downloadJson(url))["query"]["search"]; + public async search(searchTerm: string): Promise<{ title: string; snippet: string }[]> { + const url = + this.backend + + "/w/api.php?action=query&format=json&list=search&srsearch=" + + encodeURIComponent(searchTerm) + return (await Utils.downloadJson(url))["query"]["search"] } /** @@ -120,46 +131,55 @@ export default class Wikipedia { * This gives better results then via the API * @param searchTerm */ - public async searchViaIndex(searchTerm: string): Promise<{ title: string, snippet: string, url: string } []> { + public async searchViaIndex( + searchTerm: string + ): Promise<{ title: string; snippet: string; url: string }[]> { const url = `${this.backend}/w/index.php?search=${encodeURIComponent(searchTerm)}&ns0=1` - const result = await Utils.downloadAdvanced(url); - if(result["redirect"] ){ + const result = await Utils.downloadAdvanced(url) + if (result["redirect"]) { const targetUrl = result["redirect"] // This is an exact match - return [{ - title: this.extractPageName(targetUrl)?.trim(), - url: targetUrl, - snippet: "" - }] + return [ + { + title: this.extractPageName(targetUrl)?.trim(), + url: targetUrl, + snippet: "", + }, + ] } - const el = document.createElement('html'); - el.innerHTML = result["content"].replace(/href="\//g, "href=\""+this.backend+"/"); + const el = document.createElement("html") + el.innerHTML = result["content"].replace(/href="\//g, 'href="' + this.backend + "/") const searchResults = el.getElementsByClassName("mw-search-results") - const individualResults = Array.from(searchResults[0]?.getElementsByClassName("mw-search-result") ?? []) - return individualResults.map(result => { + const individualResults = Array.from( + searchResults[0]?.getElementsByClassName("mw-search-result") ?? [] + ) + return individualResults.map((result) => { const toRemove = Array.from(result.getElementsByClassName("searchalttitle")) for (const toRm of toRemove) { toRm.parentElement.removeChild(toRm) } - + return { - title: result.getElementsByClassName("mw-search-result-heading")[0].textContent.trim(), + title: result + .getElementsByClassName("mw-search-result-heading")[0] + .textContent.trim(), url: result.getElementsByTagName("a")[0].href, - snippet: result.getElementsByClassName("searchresult")[0].textContent + snippet: result.getElementsByClassName("searchresult")[0].textContent, } }) } - public async GetArticleAsync(pageName: string, options: - { + public async GetArticleAsync( + pageName: string, + options: { firstParagraphOnly?: false | boolean - }): Promise { - + } + ): Promise { const response = await Utils.downloadJson(this.getDataUrl(pageName)) if (response?.parse?.text === undefined) { return undefined } - const html = response["parse"]["text"]["*"]; + const html = response["parse"]["text"]["*"] if (html === undefined) { return undefined } @@ -179,15 +199,16 @@ export default class Wikipedia { toRemove?.parentElement?.removeChild(toRemove) } - const links = Array.from(content.getElementsByTagName("a")) // Rewrite relative links to absolute links + open them in a new tab - links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false).forEach(link => { - link.target = '_blank' - // note: link.getAttribute("href") gets the textual value, link.href is the rewritten version which'll contain the host for relative paths - link.href = `${this.backend}${link.getAttribute("href")}`; - }) + links + .filter((link) => link.getAttribute("href")?.startsWith("/") ?? false) + .forEach((link) => { + link.target = "_blank" + // note: link.getAttribute("href") gets the textual value, link.href is the rewritten version which'll contain the host for relative paths + link.href = `${this.backend}${link.getAttribute("href")}` + }) if (options?.firstParagraphOnly) { return content.getElementsByTagName("p").item(0).innerHTML @@ -195,5 +216,4 @@ export default class Wikipedia { return content.innerHTML } - -} \ No newline at end of file +} diff --git a/Models/BaseLayer.ts b/Models/BaseLayer.ts index c89f9d4e9..dd249998b 100644 --- a/Models/BaseLayer.ts +++ b/Models/BaseLayer.ts @@ -1,12 +1,12 @@ -import {TileLayer} from "leaflet"; +import { TileLayer } from "leaflet" export default interface BaseLayer { - id: string, - name: string, - layer: () => TileLayer, - max_zoom: number, - min_zoom: number; - feature: any, - isBest?: boolean, + id: string + name: string + layer: () => TileLayer + max_zoom: number + min_zoom: number + feature: any + isBest?: boolean category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string -} \ No newline at end of file +} diff --git a/Models/Constants.ts b/Models/Constants.ts index 4b30085d2..327b0b8d4 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,20 +1,20 @@ -import {Utils} from "../Utils"; +import { Utils } from "../Utils" export default class Constants { + public static vNumber = "0.23.2" - public static vNumber = "0.23.2"; - - public static ImgurApiKey = '7070e7167f0a25a' - public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" + public static ImgurApiKey = "7070e7167f0a25a" + public static readonly mapillary_client_token_v4 = + "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" /** * API key for Maproulette - * + * * Currently there is no user-friendly way to get the user's API key. * See https://github.com/maproulette/maproulette2/issues/476 for more information. * Using an empty string however does work for most actions, but will attribute all actions to the Superuser. */ - public static readonly MaprouletteApiKey = ""; + public static readonly MaprouletteApiKey = "" public static defaultOverpassUrls = [ // The official instance, 10000 queries per day per project allowed @@ -26,14 +26,30 @@ export default class Constants { // Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter" ] - - public static readonly added_by_default: string[] = ["gps_location", "gps_location_history", "home_location", "gps_track"] - public static readonly no_include: string[] = ["conflation", "left_right_style", "split_point", "current_view", "matchpoint"] + public static readonly added_by_default: string[] = [ + "gps_location", + "gps_location_history", + "home_location", + "gps_track", + ] + public static readonly no_include: string[] = [ + "conflation", + "left_right_style", + "split_point", + "current_view", + "matchpoint", + ] /** * Layer IDs of layers which have special properties through built-in hooks */ - public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "note", "import_candidate", "direction", ...Constants.no_include] - + public static readonly priviliged_layers: string[] = [ + ...Constants.added_by_default, + "type_node", + "note", + "import_candidate", + "direction", + ...Constants.no_include, + ] // The user journey states thresholds when a new feature gets unlocked public static userJourney = { @@ -48,45 +64,66 @@ export default class Constants { themeGeneratorReadOnlyUnlock: 50, themeGeneratorFullUnlock: 500, addNewPointWithUnreadMessagesUnlock: 500, - minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19), + minZoomLevelToAddNewPoints: Constants.isRetina() ? 18 : 19, - importHelperUnlock: 5000 - }; + importHelperUnlock: 5000, + } /** * Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes. * (Note that pendingChanges might upload sooner if the popup is closed or similar) */ - static updateTimeoutSec: number = 30; + static updateTimeoutSec: number = 30 /** * If the contributor has their GPS location enabled and makes a change, * the points visited less then `nearbyVisitTime`-seconds ago will be inspected. * The point closest to the changed feature will be considered and this distance will be tracked. * ALl these distances are used to calculate a nearby-score */ - static nearbyVisitTime: number = 30 * 60; + static nearbyVisitTime: number = 30 * 60 /** * If a user makes a change, the distance to the changed object is calculated. * If a user makes multiple changes, all these distances are put into multiple bins, depending on this distance. * For every bin, the totals are uploaded as metadata */ static distanceToChangeObjectBins = [25, 50, 100, 500, 1000, 5000, Number.MAX_VALUE] - static themeOrder = ["personal", "cyclofix", "waste" , "etymology", "food","cafes_and_pubs", "playgrounds", "hailhydrant", "toilets", "aed", "bookcases"]; + static themeOrder = [ + "personal", + "cyclofix", + "waste", + "etymology", + "food", + "cafes_and_pubs", + "playgrounds", + "hailhydrant", + "toilets", + "aed", + "bookcases", + ] /** * Upon initialization, the GPS will search the location. * If the location is found within the given timout, it'll automatically fly to it. - * + * * In seconds */ - static zoomToLocationTimeout = 60; - static countryCoderEndpoint: string = "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country"; + static zoomToLocationTimeout = 60 + static countryCoderEndpoint: string = + "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country" private static isRetina(): boolean { - if(Utils.runningFromConsole){ - return false; + if (Utils.runningFromConsole) { + return false } // The cause for this line of code: https://github.com/pietervdvn/MapComplete/issues/115 // See https://stackoverflow.com/questions/19689715/what-is-the-best-way-to-detect-retina-support-on-a-device-using-javascript - return ((window.matchMedia && (window.matchMedia('only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)').matches)) || (window.devicePixelRatio && window.devicePixelRatio >= 2)); + return ( + (window.matchMedia && + (window.matchMedia( + "only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)" + ).matches || + window.matchMedia( + "only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)" + ).matches)) || + (window.devicePixelRatio && window.devicePixelRatio >= 2) + ) } - } diff --git a/Models/Denomination.ts b/Models/Denomination.ts index a22204180..03770ef6c 100644 --- a/Models/Denomination.ts +++ b/Models/Denomination.ts @@ -1,20 +1,19 @@ -import {Translation} from "../UI/i18n/Translation"; -import {DenominationConfigJson} from "./ThemeConfig/Json/UnitConfigJson"; -import Translations from "../UI/i18n/Translations"; -import {Store} from "../Logic/UIEventSource"; -import BaseUIElement from "../UI/BaseUIElement"; -import Toggle from "../UI/Input/Toggle"; +import { Translation } from "../UI/i18n/Translation" +import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson" +import Translations from "../UI/i18n/Translations" +import { Store } from "../Logic/UIEventSource" +import BaseUIElement from "../UI/BaseUIElement" +import Toggle from "../UI/Input/Toggle" export class Denomination { - public readonly canonical: string; - public readonly _canonicalSingular: string; + public readonly canonical: string + public readonly _canonicalSingular: string public readonly useAsDefaultInput: boolean | string[] - public readonly useIfNoUnitGiven : boolean | string[] - public readonly prefix: boolean; - public readonly alternativeDenominations: string []; - private readonly _human: Translation; - private readonly _humanSingular?: Translation; - + public readonly useIfNoUnitGiven: boolean | string[] + public readonly prefix: boolean + public readonly alternativeDenominations: string[] + private readonly _human: Translation + private readonly _humanSingular?: Translation constructor(json: DenominationConfigJson, context: string) { context = `${context}.unit(${json.canonicalDenomination})` @@ -24,26 +23,24 @@ export class Denomination { } this._canonicalSingular = json.canonicalDenominationSingular?.trim() - json.alternativeDenomination.forEach((v, i) => { - if (((v?.trim() ?? "") === "")) { + if ((v?.trim() ?? "") === "") { throw `${context}.alternativeDenomination.${i}: invalid alternative denomination: undefined, null or only whitespace` } }) - this.alternativeDenominations = json.alternativeDenomination?.map(v => v.trim()) ?? [] + this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? [] - if(json["default"] !== undefined) { + if (json["default"] !== undefined) { throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead` } this.useIfNoUnitGiven = json.useIfNoUnitGiven this.useAsDefaultInput = json.useAsDefaultInput ?? json.useIfNoUnitGiven - + this._human = Translations.T(json.human, context + "human") this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular") - this.prefix = json.prefix ?? false; - + this.prefix = json.prefix ?? false } get human(): Translation { @@ -58,18 +55,14 @@ export class Denomination { if (this._humanSingular === undefined) { return this.human } - return new Toggle( - this.humanSingular, - this.human, - isSingular - ) + return new Toggle(this.humanSingular, this.human, isSingular) } /** * Create a representation of the given value * @param value: the value from OSM * @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed - * + * * const unit = new Denomination({ * canonicalDenomination: "m", * alternativeDenomination: ["meter"], @@ -83,7 +76,7 @@ export class Denomination { * unit.canonicalValue("42 meter", true) // =>"42 m" * unit.canonicalValue("42m", true) // =>"42 m" * unit.canonicalValue("42", true) // =>"42 m" - * + * * // Should be trimmed if canonical is empty * const unit = new Denomination({ * canonicalDenomination: "", @@ -97,18 +90,18 @@ export class Denomination { * unit.canonicalValue("42 m", true) // =>"42" * unit.canonicalValue("42 meter", true) // =>"42" */ - public canonicalValue(value: string, actAsDefault: boolean) : string { + public canonicalValue(value: string, actAsDefault: boolean): string { if (value === undefined) { - return undefined; + return undefined } const stripped = this.StrippedValue(value, actAsDefault) if (stripped === null) { - return null; + return null } if (stripped === "1" && this._canonicalSingular !== undefined) { return ("1 " + this._canonicalSingular).trim() } - return (stripped + " " + this.canonical).trim(); + return (stripped + " " + this.canonical).trim() } /** @@ -119,13 +112,12 @@ export class Denomination { * Returns null if it doesn't match this unit */ public StrippedValue(value: string, actAsDefault: boolean): string { - if (value === undefined) { - return undefined; + return undefined } value = value.toLowerCase() - const self = this; + const self = this function startsWith(key) { if (self.prefix) { @@ -147,36 +139,39 @@ export class Denomination { return substr(this.canonical) } - if (this._canonicalSingular !== undefined && this._canonicalSingular !== "" && startsWith(this._canonicalSingular)) { + if ( + this._canonicalSingular !== undefined && + this._canonicalSingular !== "" && + startsWith(this._canonicalSingular) + ) { return substr(this._canonicalSingular) } for (const alternativeValue of this.alternativeDenominations) { if (startsWith(alternativeValue)) { - return substr(alternativeValue); + return substr(alternativeValue) } } if (!actAsDefault) { return null } - + const parsed = Number(value.trim()) if (!isNaN(parsed)) { - return value.trim(); + return value.trim() } - return null; + return null } - isDefaultUnit(country: () => string) { - if(this.useIfNoUnitGiven === true){ + if (this.useIfNoUnitGiven === true) { return true } - if(this.useIfNoUnitGiven === false){ + if (this.useIfNoUnitGiven === false) { return false } return this.useIfNoUnitGiven.indexOf(country()) >= 0 } -} \ No newline at end of file +} diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts index 6890915f8..3263c35ae 100644 --- a/Models/FilteredLayer.ts +++ b/Models/FilteredLayer.ts @@ -1,14 +1,14 @@ -import {UIEventSource} from "../Logic/UIEventSource"; -import LayerConfig from "./ThemeConfig/LayerConfig"; -import {TagsFilter} from "../Logic/Tags/TagsFilter"; +import { UIEventSource } from "../Logic/UIEventSource" +import LayerConfig from "./ThemeConfig/LayerConfig" +import { TagsFilter } from "../Logic/Tags/TagsFilter" export interface FilterState { - currentFilter: TagsFilter, + currentFilter: TagsFilter state: string | number } export default interface FilteredLayer { - readonly isDisplayed: UIEventSource; - readonly appliedFilters: UIEventSource>; - readonly layerDef: LayerConfig; -} \ No newline at end of file + readonly isDisplayed: UIEventSource + readonly appliedFilters: UIEventSource> + readonly layerDef: LayerConfig +} diff --git a/Models/LeafletMap.ts b/Models/LeafletMap.ts index 76ff1e38d..e3f0ae850 100644 --- a/Models/LeafletMap.ts +++ b/Models/LeafletMap.ts @@ -1,4 +1,3 @@ export default interface LeafletMap { - - getBounds(): [[number, number], [number, number]]; -} \ No newline at end of file + getBounds(): [[number, number], [number, number]] +} diff --git a/Models/Loc.ts b/Models/Loc.ts index f0d3f8186..7b78d642a 100644 --- a/Models/Loc.ts +++ b/Models/Loc.ts @@ -1,5 +1,5 @@ export default interface Loc { - lat: number, - lon: number, + lat: number + lon: number zoom: number -} \ No newline at end of file +} diff --git a/Models/OsmFeature.ts b/Models/OsmFeature.ts index e8ad80488..aee5f08fb 100644 --- a/Models/OsmFeature.ts +++ b/Models/OsmFeature.ts @@ -1,9 +1,9 @@ -import {Feature, Geometry} from "@turf/turf"; +import { Feature, Geometry } from "@turf/turf" export type RelationId = `relation/${number}` export type WayId = `way/${number}` export type NodeId = `node/${number}` export type OsmId = NodeId | WayId | RelationId -export type OsmTags = Record & {id: string} -export type OsmFeature = Feature \ No newline at end of file +export type OsmTags = Record & { id: string } +export type OsmFeature = Feature diff --git a/Models/ThemeConfig/Conversion/AddContextToTranslations.ts b/Models/ThemeConfig/Conversion/AddContextToTranslations.ts index 335f3d761..937399ced 100644 --- a/Models/ThemeConfig/Conversion/AddContextToTranslations.ts +++ b/Models/ThemeConfig/Conversion/AddContextToTranslations.ts @@ -1,13 +1,17 @@ -import {DesugaringStep} from "./Conversion"; -import {Utils} from "../../../Utils"; -import Translations from "../../../UI/i18n/Translations"; +import { DesugaringStep } from "./Conversion" +import { Utils } from "../../../Utils" +import Translations from "../../../UI/i18n/Translations" export class AddContextToTranslations extends DesugaringStep { - private readonly _prefix: string; + private readonly _prefix: string constructor(prefix = "") { - super("Adds a '_context' to every object that is probably a translation", ["_context"], "AddContextToTranslation"); - this._prefix = prefix; + super( + "Adds a '_context' to every object that is probably a translation", + ["_context"], + "AddContextToTranslation" + ) + this._prefix = prefix } /** @@ -21,7 +25,7 @@ export class AddContextToTranslations extends DesugaringStep { * } * } * } - * ] + * ] * } * const rewritten = new AddContextToTranslations("prefix:").convert(theme, "context").result * const expected = { @@ -35,10 +39,10 @@ export class AddContextToTranslations extends DesugaringStep { * } * } * } - * ] + * ] * } * rewritten // => expected - * + * * // should use the ID if one is present instead of the index * const theme = { * layers: [ @@ -51,7 +55,7 @@ export class AddContextToTranslations extends DesugaringStep { * } * ] * } - * ] + * ] * } * const rewritten = new AddContextToTranslations("prefix:").convert(theme, "context").result * const expected = { @@ -66,10 +70,10 @@ export class AddContextToTranslations extends DesugaringStep { * } * ] * } - * ] + * ] * } * rewritten // => expected - * + * * // should preserve nulls * const theme = { * layers: [ @@ -79,7 +83,7 @@ export class AddContextToTranslations extends DesugaringStep { * name:null * } * } - * ] + * ] * } * const rewritten = new AddContextToTranslations("prefix:").convert(theme, "context").result * const expected = { @@ -90,11 +94,11 @@ export class AddContextToTranslations extends DesugaringStep { * name: null * } * } - * ] + * ] * } * rewritten // => expected - * - * + * + * * // Should ignore all if '#dont-translate' is set * const theme = { * "#dont-translate": "*", @@ -107,43 +111,47 @@ export class AddContextToTranslations extends DesugaringStep { * } * } * } - * ] + * ] * } * const rewritten = new AddContextToTranslations("prefix:").convert(theme, "context").result * rewritten // => theme - * + * */ - convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } { - - if(json["#dont-translate"] === "*"){ - return {result: json} + convert( + json: T, + context: string + ): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } { + if (json["#dont-translate"] === "*") { + return { result: json } } - - const result = Utils.WalkJson(json, (leaf, path) => { - if(leaf === undefined || leaf === null){ - return leaf - } - if (typeof leaf === "object") { - - // follow the path. If we encounter a number, check that there is no ID we can use instead - let breadcrumb = json; - for (let i = 0; i < path.length; i++) { - const pointer = path[i] - breadcrumb = breadcrumb[pointer] - if(pointer.match("[0-9]+") && breadcrumb["id"] !== undefined){ - path[i] = breadcrumb["id"] - } + + const result = Utils.WalkJson( + json, + (leaf, path) => { + if (leaf === undefined || leaf === null) { + return leaf } - - return {...leaf, _context: this._prefix + context + "." + path.join(".")} - } else { - return leaf - } - }, obj => obj === undefined || obj === null || Translations.isProbablyATranslation(obj)) + if (typeof leaf === "object") { + // follow the path. If we encounter a number, check that there is no ID we can use instead + let breadcrumb = json + for (let i = 0; i < path.length; i++) { + const pointer = path[i] + breadcrumb = breadcrumb[pointer] + if (pointer.match("[0-9]+") && breadcrumb["id"] !== undefined) { + path[i] = breadcrumb["id"] + } + } + + return { ...leaf, _context: this._prefix + context + "." + path.join(".") } + } else { + return leaf + } + }, + (obj) => obj === undefined || obj === null || Translations.isProbablyATranslation(obj) + ) return { - result - }; + result, + } } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/Conversion.ts b/Models/ThemeConfig/Conversion/Conversion.ts index 48177827f..91a89a545 100644 --- a/Models/ThemeConfig/Conversion/Conversion.ts +++ b/Models/ThemeConfig/Conversion/Conversion.ts @@ -1,37 +1,41 @@ -import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import {Utils} from "../../../Utils"; +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import { Utils } from "../../../Utils" export interface DesugaringContext { tagRenderings: Map - sharedLayers: Map, + sharedLayers: Map publicLayers?: Set } export abstract class Conversion { - public readonly modifiedAttributes: string[]; + public readonly modifiedAttributes: string[] public readonly name: string - protected readonly doc: string; + protected readonly doc: string constructor(doc: string, modifiedAttributes: string[] = [], name: string) { - this.modifiedAttributes = modifiedAttributes; - this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", "); + this.modifiedAttributes = modifiedAttributes + this.doc = doc + "\n\nModified attributes are\n" + modifiedAttributes.join(", ") this.name = name } - public static strict(fixed: { errors?: string[], warnings?: string[], information?: string[], result?: T }): T { - - fixed.information?.forEach(i => console.log(" ", i)) + public static strict(fixed: { + errors?: string[] + warnings?: string[] + information?: string[] + result?: T + }): T { + fixed.information?.forEach((i) => console.log(" ", i)) const yellow = (s) => "\x1b[33m" + s + "\x1b[0m" - const red = s => '\x1b[31m' + s + '\x1b[0m' - fixed.warnings?.forEach(w => console.warn(red(` `), yellow(w))) + const red = (s) => "\x1b[31m" + s + "\x1b[0m" + fixed.warnings?.forEach((w) => console.warn(red(` `), yellow(w))) if (fixed?.errors !== undefined && fixed?.errors?.length > 0) { - fixed.errors?.forEach(e => console.error(red(`ERR ` + e))) + fixed.errors?.forEach((e) => console.error(red(`ERR ` + e))) throw "Detected one or more errors, stopping now" } - return fixed.result; + return fixed.result } public convertStrict(json: TIn, context: string): TOut { @@ -39,7 +43,13 @@ export abstract class Conversion { return DesugaringStep.strict(fixed) } - public convertJoin(json: TIn, context: string, errors: string[], warnings?: string[], information?: string[]): TOut { + public convertJoin( + json: TIn, + context: string, + errors: string[], + warnings?: string[], + information?: string[] + ): TOut { const fixed = this.convert(json, context) errors?.push(...(fixed.errors ?? [])) warnings?.push(...(fixed.warnings ?? [])) @@ -47,41 +57,41 @@ export abstract class Conversion { return fixed.result } - public andThenF(f: (tout:TOut) => X ): Conversion{ - return new Pipe( - this, - new Pure(f) - ) + public andThenF(f: (tout: TOut) => X): Conversion { + return new Pipe(this, new Pure(f)) } - - abstract convert(json: TIn, context: string): { result: TOut, errors?: string[], warnings?: string[], information?: string[] } + + abstract convert( + json: TIn, + context: string + ): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } } -export abstract class DesugaringStep extends Conversion { - -} +export abstract class DesugaringStep extends Conversion {} class Pipe extends Conversion { - private readonly _step0: Conversion; - private readonly _step1: Conversion; - constructor(step0: Conversion, step1: Conversion) { - super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`); - this._step0 = step0; - this._step1 = step1; + private readonly _step0: Conversion + private readonly _step1: Conversion + constructor(step0: Conversion, step1: Conversion) { + super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) + this._step0 = step0 + this._step1 = step1 } - convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } { - - const r0 = this._step0.convert(json, context); - const {result, errors, information, warnings } = r0; - if(result === undefined && errors.length > 0){ + convert( + json: TIn, + context: string + ): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } { + const r0 = this._step0.convert(json, context) + const { result, errors, information, warnings } = r0 + if (result === undefined && errors.length > 0) { return { ...r0, - result: undefined - }; + result: undefined, + } } - - const r = this._step1.convert(result, context); + + const r = this._step1.convert(result, context) errors.push(...r.errors) information.push(...r.information) warnings.push(...r.warnings) @@ -89,35 +99,44 @@ class Pipe extends Conversion { result: r.result, errors, warnings, - information + information, } } } class Pure extends Conversion { - private readonly _f: (t: TIn) => TOut; - constructor(f: ((t:TIn) => TOut)) { - super("Wrapper around a pure function",[], "Pure"); - this._f = f; + private readonly _f: (t: TIn) => TOut + constructor(f: (t: TIn) => TOut) { + super("Wrapper around a pure function", [], "Pure") + this._f = f } - - convert(json: TIn, context: string): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } { - return {result: this._f(json)}; + + convert( + json: TIn, + context: string + ): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } { + return { result: this._f(json) } } - } export class Each extends Conversion { - private readonly _step: Conversion; + private readonly _step: Conversion constructor(step: Conversion) { - super("Applies the given step on every element of the list", [], "OnEach(" + step.name + ")"); - this._step = step; + super( + "Applies the given step on every element of the list", + [], + "OnEach(" + step.name + ")" + ) + this._step = step } - convert(values: X[], context: string): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + values: X[], + context: string + ): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } { if (values === undefined || values === null) { - return {result: undefined} + return { result: undefined } } const information: string[] = [] const warnings: string[] = [] @@ -132,68 +151,83 @@ export class Each extends Conversion { result.push(r.result) } return { - information, errors, warnings, - result - }; + information, + errors, + warnings, + result, + } } - } export class On extends DesugaringStep { - private readonly key: string; - private readonly step: ((t: T) => Conversion); + private readonly key: string + private readonly step: (t: T) => Conversion - constructor(key: string, step: Conversion | ((t: T )=> Conversion)) { - super("Applies " + step.name + " onto property `"+key+"`", [key], `On(${key}, ${step.name})`); - if(typeof step === "function"){ - this.step = step; - }else{ - this.step = _ => step + constructor(key: string, step: Conversion | ((t: T) => Conversion)) { + super( + "Applies " + step.name + " onto property `" + key + "`", + [key], + `On(${key}, ${step.name})` + ) + if (typeof step === "function") { + this.step = step + } else { + this.step = (_) => step } - this.key = key; + this.key = key } - convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } { - json = {...json} + convert( + json: T, + context: string + ): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } { + json = { ...json } const step = this.step(json) - const key = this.key; + const key = this.key const value: P = json[key] if (value === undefined || value === null) { - return { result: json }; + return { result: json } } const r = step.convert(value, context + "." + key) json[key] = r.result return { ...r, result: json, - }; - + } } } export class Pass extends Conversion { constructor(message?: string) { - super(message??"Does nothing, often to swap out steps in testing", [], "Pass"); + super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass") } - - convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: T, + context: string + ): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } { return { - result: json - }; + result: json, + } } - } export class Concat extends Conversion { - private readonly _step: Conversion; + private readonly _step: Conversion constructor(step: Conversion) { - super("Executes the given step, flattens the resulting list", [], "Concat(" + step.name + ")"); - this._step = step; + super( + "Executes the given step, flattens the resulting list", + [], + "Concat(" + step.name + ")" + ) + this._step = step } - convert(values: X[], context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + values: X[], + context: string + ): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } { if (values === undefined || values === null) { // Move on - nothing to see here! return { @@ -208,56 +242,68 @@ export class Concat extends Conversion { return { ...r, result: flattened, - }; + } } } -export class FirstOf extends Conversion{ - private readonly _conversion: Conversion; - +export class FirstOf extends Conversion { + private readonly _conversion: Conversion + constructor(conversion: Conversion) { - super("Picks the first result of the conversion step", [], "FirstOf("+conversion.name+")"); - this._conversion = conversion; + super( + "Picks the first result of the conversion step", + [], + "FirstOf(" + conversion.name + ")" + ) + this._conversion = conversion } - convert(json: T, context: string): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } { - const reslt = this._conversion.convert(json, context); + convert( + json: T, + context: string + ): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } { + const reslt = this._conversion.convert(json, context) return { ...reslt, - result: reslt.result[0] - }; + result: reslt.result[0], + } } - } export class Fuse extends DesugaringStep { - private readonly steps: DesugaringStep[]; + private readonly steps: DesugaringStep[] constructor(doc: string, ...steps: DesugaringStep[]) { - super((doc ?? "") + "This fused pipeline of the following steps: " + steps.map(s => s.name).join(", "), - Utils.Dedup([].concat(...steps.map(step => step.modifiedAttributes))), - "Fuse of " + steps.map(s => s.name).join(", ") - ); - this.steps = Utils.NoNull(steps); + super( + (doc ?? "") + + "This fused pipeline of the following steps: " + + steps.map((s) => s.name).join(", "), + Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))), + "Fuse of " + steps.map((s) => s.name).join(", ") + ) + this.steps = Utils.NoNull(steps) } - convert(json: T, context: string): { result: T; errors: string[]; warnings: string[], information: string[] } { + convert( + json: T, + context: string + ): { result: T; errors: string[]; warnings: string[]; information: string[] } { const errors = [] const warnings = [] const information = [] for (let i = 0; i < this.steps.length; i++) { - const step = this.steps[i]; - try{ + const step = this.steps[i] + try { let r = step.convert(json, "While running step " + step.name + ": " + context) - errors.push(...r.errors ?? []) - warnings.push(...r.warnings ?? []) - information.push(...r.information ?? []) + errors.push(...(r.errors ?? [])) + warnings.push(...(r.warnings ?? [])) + information.push(...(r.information ?? [])) json = r.result if (errors.length > 0) { - break; + break } - }catch(e){ - console.error("Step "+step.name+" failed due to ",e,e.stack); + } catch (e) { + console.error("Step " + step.name + " failed due to ", e, e.stack) throw e } } @@ -265,32 +311,31 @@ export class Fuse extends DesugaringStep { result: json, errors, warnings, - information - }; + information, + } } - } export class SetDefault extends DesugaringStep { - private readonly value: any; - private readonly key: string; - private readonly _overrideEmptyString: boolean; + private readonly value: any + private readonly key: string + private readonly _overrideEmptyString: boolean constructor(key: string, value: any, overrideEmptyString = false) { - super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key); - this.key = key; - this.value = value; - this._overrideEmptyString = overrideEmptyString; + super("Sets " + key + " to a default value if undefined", [], "SetDefault of " + key) + this.key = key + this.value = value + this._overrideEmptyString = overrideEmptyString } convert(json: T, context: string): { result: T } { if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { - json = {...json} + json = { ...json } json[this.key] = this.value } return { - result: json - }; + result: json, + } } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts index 076c898af..609e3fafc 100644 --- a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts +++ b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts @@ -1,27 +1,31 @@ -import {Conversion} from "./Conversion"; -import LayerConfig from "../LayerConfig"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import Translations from "../../../UI/i18n/Translations"; -import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"; -import {Translation, TypedTranslation} from "../../../UI/i18n/Translation"; +import { Conversion } from "./Conversion" +import LayerConfig from "../LayerConfig" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import Translations from "../../../UI/i18n/Translations" +import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" +import { Translation, TypedTranslation } from "../../../UI/i18n/Translation" export default class CreateNoteImportLayer extends Conversion { /** * A closed note is included if it is less then 'n'-days closed * @private */ - private readonly _includeClosedNotesDays: number; + private readonly _includeClosedNotesDays: number constructor(includeClosedNotesDays = 0) { - super([ - "Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').", - "The import buttons and matches will be based on the presets of the given theme", - ].join("\n\n"), [], "CreateNoteImportLayer") - this._includeClosedNotesDays = includeClosedNotesDays; + super( + [ + "Advanced conversion which deducts a layer showing all notes that are 'importable' (i.e. a note that contains a link to some MapComplete theme, with hash '#import').", + "The import buttons and matches will be based on the presets of the given theme", + ].join("\n\n"), + [], + "CreateNoteImportLayer" + ) + this._includeClosedNotesDays = includeClosedNotesDays } convert(layerJson: LayerConfigJson, context: string): { result: LayerConfigJson } { - const t = Translations.t.importLayer; + const t = Translations.t.importLayer /** * The note itself will contain `tags=k=v;k=v;k=v;... @@ -35,14 +39,16 @@ export default class CreateNoteImportLayer extends Conversion r !== null && r["location"] !== undefined); - const firstRender = (pointRenderings [0]) + const pointRenderings = (layerJson.mapRendering ?? []).filter( + (r) => r !== null && r["location"] !== undefined + ) + const firstRender = pointRenderings[0] if (firstRender === undefined) { throw `Layer ${layerJson.id} does not have a pointRendering: ` + context } @@ -50,7 +56,10 @@ export default class CreateNoteImportLayer extends Conversion(translation: TypedTranslation, subs: T): object { - return {...translation.Subs(subs).translations, "_context": translation.context} + return { ...translation.Subs(subs).translations, _context: translation.context } } const result: LayerConfigJson = { - "id": "note_import_" + layer.id, + id: "note_import_" + layer.id, // By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations, - "description": trs(t.description, {title: layer.title.render}), - "source": { - "osmTags": { - "and": [ - "id~*" - ] + description: trs(t.description, { title: layer.title.render }), + source: { + osmTags: { + and: ["id~*"], }, - "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + this._includeClosedNotesDays + "&bbox={x_min},{y_min},{x_max},{y_max}", - "geoJsonZoomLevel": 10, - "maxCacheAge": 0 + geoJson: + "https://api.openstreetmap.org/api/0.6/notes.json?limit=10000&closed=" + + this._includeClosedNotesDays + + "&bbox={x_min},{y_min},{x_max},{y_max}", + geoJsonZoomLevel: 10, + maxCacheAge: 0, }, - "minzoom": Math.min(12, layerJson.minzoom - 2), - "title": { - "render": trs(t.popupTitle, {title}) + minzoom: Math.min(12, layerJson.minzoom - 2), + title: { + render: trs(t.popupTitle, { title }), }, - "calculatedTags": [ + calculatedTags: [ "_first_comment=feat.get('comments')[0].text.toLowerCase()", "_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\)?.*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()", "_comments_count=feat.get('comments').length", "_intro=(() => {const lines = feat.get('comments')[0].text.split('\\n'); lines.splice(feat.get('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('
');})()", - "_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()" + "_tags=(() => {let lines = feat.get('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, feat.get('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()", ], - "isShown": { - and: - ["_trigger_index~*", - {or: isShownIfAny} - ] + isShown: { + and: ["_trigger_index~*", { or: isShownIfAny }], }, - "titleIcons": [ + titleIcons: [ { - "render": "" - } + render: "", + }, ], - "tagRenderings": [ + tagRenderings: [ { - "id": "Intro", - render: "{_intro}" + id: "Intro", + render: "{_intro}", }, { - "id": "conversation", - "render": "{visualize_note_comments(comments,1)}", - condition: "_comments_count>1" + id: "conversation", + render: "{visualize_note_comments(comments,1)}", + condition: "_comments_count>1", }, { - "id": "import", - "render": importButton, - condition: "closed_at=" + id: "import", + render: importButton, + condition: "closed_at=", }, { - "id": "close_note_", - "render": embed( - "{close_note(", t.notFound.Subs({title}), ", ./assets/svg/close.svg, id, This feature does not exist, 18)}"), - condition: "closed_at=" + id: "close_note_", + render: embed( + "{close_note(", + t.notFound.Subs({ title }), + ", ./assets/svg/close.svg, id, This feature does not exist, 18)}" + ), + condition: "closed_at=", }, { - "id": "close_note_mapped", - "render": embed("{close_note(", t.alreadyMapped.Subs({title}), ", ./assets/svg/duplicate.svg, id, Already mapped, 18)}"), - condition: "closed_at=" + id: "close_note_mapped", + render: embed( + "{close_note(", + t.alreadyMapped.Subs({ title }), + ", ./assets/svg/duplicate.svg, id, Already mapped, 18)}" + ), + condition: "closed_at=", }, { - "id": "handled", - "render": tr(t.importHandled), - condition: "closed_at~*" + id: "handled", + render: tr(t.importHandled), + condition: "closed_at~*", }, { - "id": "comment", - "render": "{add_note_comment()}" + id: "comment", + render: "{add_note_comment()}", }, { - "id": "add_image", - "render": "{add_image_to_note()}" + id: "add_image", + render: "{add_image_to_note()}", }, { - "id": "nearby_images", - render: tr(t.nearbyImagesIntro) - - } + id: "nearby_images", + render: tr(t.nearbyImagesIntro), + }, ], - "mapRendering": [ + mapRendering: [ { - "location": [ - "point" - ], - "icon": { - "render": "circle:white;help:black", - mappings: [{ - if: {or: ["closed_at~*", "_imported=yes"]}, - then: "circle:white;checkmark:black" - }] + location: ["point"], + icon: { + render: "circle:white;help:black", + mappings: [ + { + if: { or: ["closed_at~*", "_imported=yes"] }, + then: "circle:white;checkmark:black", + }, + ], }, - "iconSize": "40,40,center" - } - ] + iconSize: "40,40,center", + }, + ], } - return { - result - }; + result, + } } - - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/FixImages.ts b/Models/ThemeConfig/Conversion/FixImages.ts index f58294cbb..fbd7d07c2 100644 --- a/Models/ThemeConfig/Conversion/FixImages.ts +++ b/Models/ThemeConfig/Conversion/FixImages.ts @@ -1,31 +1,37 @@ -import {Conversion, DesugaringStep} from "./Conversion"; -import {LayoutConfigJson} from "../Json/LayoutConfigJson"; -import {Utils} from "../../../Utils"; -import * as metapaths from "../../../assets/layoutconfigmeta.json"; -import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"; -import Translations from "../../../UI/i18n/Translations"; +import { Conversion, DesugaringStep } from "./Conversion" +import { LayoutConfigJson } from "../Json/LayoutConfigJson" +import { Utils } from "../../../Utils" +import * as metapaths from "../../../assets/layoutconfigmeta.json" +import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json" +import Translations from "../../../UI/i18n/Translations" export class ExtractImages extends Conversion { - private _isOfficial: boolean; - private _sharedTagRenderings: Map; - - private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths) - .filter(mp => (ExtractImages.mightBeTagRendering(mp)) || mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) - private static readonly tagRenderingMetaPaths = (tagrenderingmetapaths["default"] ?? tagrenderingmetapaths) + private _isOfficial: boolean + private _sharedTagRenderings: Map + private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths).filter( + (mp) => + ExtractImages.mightBeTagRendering(mp) || + (mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) + ) + private static readonly tagRenderingMetaPaths = + tagrenderingmetapaths["default"] ?? tagrenderingmetapaths constructor(isOfficial: boolean, sharedTagRenderings: Map) { - super("Extract all images from a layoutConfig using the meta paths.",[],"ExctractImages"); - this._isOfficial = isOfficial; - this._sharedTagRenderings = sharedTagRenderings; + super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages") + this._isOfficial = isOfficial + this._sharedTagRenderings = sharedTagRenderings } - - public static mightBeTagRendering(metapath: {type: string | string[]}) : boolean{ - if(!Array.isArray(metapath.type)){ + + public static mightBeTagRendering(metapath: { type: string | string[] }): boolean { + if (!Array.isArray(metapath.type)) { return false } - return metapath.type.some(t => - t["$ref"] == "#/definitions/TagRenderingConfigJson" || t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson") + return metapath.type.some( + (t) => + t["$ref"] == "#/definitions/TagRenderingConfigJson" || + t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson" + ) } /** @@ -61,105 +67,131 @@ export class ExtractImages extends Conversion { * images.length // => 2 * images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") // => 0 * images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") // => 1 - * + * * // should not pickup rotation, should drop color * const images = new ExtractImages(true, new Map()).convert({"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}] * }, "test").result * images.length // => 1 * images[0] // => "pin" - * + * */ - convert(json: LayoutConfigJson, context: string): { result: string[], errors: string[], warnings: string[] } { - const allFoundImages : string[] = [] + convert( + json: LayoutConfigJson, + context: string + ): { result: string[]; errors: string[]; warnings: string[] } { + const allFoundImages: string[] = [] const errors = [] const warnings = [] for (const metapath of ExtractImages.layoutMetaPaths) { const mightBeTr = ExtractImages.mightBeTagRendering(metapath) - const allRenderedValuesAreImages = metapath.typeHint === "icon" || metapath.typeHint === "image" + const allRenderedValuesAreImages = + metapath.typeHint === "icon" || metapath.typeHint === "image" const found = Utils.CollectPath(metapath.path, json) if (mightBeTr) { // We might have tagRenderingConfigs containing icons here for (const el of found) { const path = el.path - const foundImage = el.leaf; - if (typeof foundImage === "string") { - - if(!allRenderedValuesAreImages){ - continue - } - - if(foundImage == ""){ - warnings.push(context+"."+path.join(".")+" Found an empty image") + const foundImage = el.leaf + if (typeof foundImage === "string") { + if (!allRenderedValuesAreImages) { + continue } - - if(this._sharedTagRenderings?.has(foundImage)){ + + if (foundImage == "") { + warnings.push(context + "." + path.join(".") + " Found an empty image") + } + + if (this._sharedTagRenderings?.has(foundImage)) { // This is not an image, but a shared tag rendering // At key positions for checking, they'll be expanded already, so we can safely ignore them here continue } - + allFoundImages.push(foundImage) - } else{ + } else { // This is a tagRendering. - // Either every rendered value might be an icon + // Either every rendered value might be an icon // or -in the case of a normal tagrendering- only the 'icons' in the mappings have an icon (or exceptionally an '' tag in the translation for (const trpath of ExtractImages.tagRenderingMetaPaths) { // Inspect all the rendered values const fromPath = Utils.CollectPath(trpath.path, foundImage) const isRendered = trpath.typeHint === "rendered" - const isImage = trpath.typeHint === "icon" || trpath.typeHint === "image" + const isImage = + trpath.typeHint === "icon" || trpath.typeHint === "image" for (const img of fromPath) { if (allRenderedValuesAreImages && isRendered) { // What we found is an image - if(img.leaf === "" || img.leaf["path"] == ""){ - warnings.push(context+[...path,...img.path].join(".")+": Found an empty image at ") - }else if(typeof img.leaf !== "string"){ - (this._isOfficial ? errors: warnings).push(context+"."+img.path.join(".")+": found an image path that is not a string: " + JSON.stringify(img.leaf)) - }else{ + if (img.leaf === "" || img.leaf["path"] == "") { + warnings.push( + context + + [...path, ...img.path].join(".") + + ": Found an empty image at " + ) + } else if (typeof img.leaf !== "string") { + ;(this._isOfficial ? errors : warnings).push( + context + + "." + + img.path.join(".") + + ": found an image path that is not a string: " + + JSON.stringify(img.leaf) + ) + } else { allFoundImages.push(img.leaf) } - } - if(!allRenderedValuesAreImages && isImage){ + } + if (!allRenderedValuesAreImages && isImage) { // Extract images from the translations - allFoundImages.push(...(Translations.T(img.leaf, "extract_images from "+img.path.join(".")).ExtractImages(false))) + allFoundImages.push( + ...Translations.T( + img.leaf, + "extract_images from " + img.path.join(".") + ).ExtractImages(false) + ) } } } - } + } } } else { for (const foundElement of found) { - if(foundElement.leaf === ""){ - warnings.push(context+"."+foundElement.path.join(".")+" Found an empty image") + if (foundElement.leaf === "") { + warnings.push( + context + "." + foundElement.path.join(".") + " Found an empty image" + ) continue } allFoundImages.push(foundElement.leaf) } - } } - const splitParts = [].concat(...Utils.NoNull(allFoundImages) - .map(img => img["path"] ?? img) - .map(img => img.split(";"))) - .map(img => img.split(":")[0]) - .filter(img => img !== "") - return {result: Utils.Dedup(splitParts), errors, warnings}; + const splitParts = [] + .concat( + ...Utils.NoNull(allFoundImages) + .map((img) => img["path"] ?? img) + .map((img) => img.split(";")) + ) + .map((img) => img.split(":")[0]) + .filter((img) => img !== "") + return { result: Utils.Dedup(splitParts), errors, warnings } } - } export class FixImages extends DesugaringStep { - private readonly _knownImages: Set; + private readonly _knownImages: Set constructor(knownImages: Set) { - super("Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL",[],"fixImages"); - this._knownImages = knownImages; + super( + "Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL", + [], + "fixImages" + ) + this._knownImages = knownImages } /** * If the id is an URL to a json file, replaces "./" in images with the path to the json file - * + * * const theme = { * "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/verkeerdeborden.json" * "layers": [ @@ -191,43 +223,50 @@ export class FixImages extends DesugaringStep { * fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg" * fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg" */ - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson, warnings?: string[] } { - let url: URL; + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; warnings?: string[] } { + let url: URL try { url = new URL(json.id) } catch (e) { // Not a URL, we don't rewrite - return {result: json} + return { result: json } } const warnings: string[] = [] const absolute = url.protocol + "//" + url.host let relative = url.protocol + "//" + url.host + url.pathname relative = relative.substring(0, relative.lastIndexOf("/")) - const self = this; - - if(relative.endsWith("assets/generated/themes")){ - warnings.push("Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative") + const self = this + + if (relative.endsWith("assets/generated/themes")) { + warnings.push( + "Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative" + ) relative = absolute } function replaceString(leaf: string) { if (self._knownImages.has(leaf)) { - return leaf; + return leaf } - - if(typeof leaf !== "string"){ - warnings.push("Found a non-string object while replacing images: "+JSON.stringify(leaf)) - return leaf; + + if (typeof leaf !== "string") { + warnings.push( + "Found a non-string object while replacing images: " + JSON.stringify(leaf) + ) + return leaf } - + if (leaf.startsWith("./")) { return relative + leaf.substring(1) } if (leaf.startsWith("/")) { return absolute + leaf } - return leaf; + return leaf } json = Utils.Clone(json) @@ -252,21 +291,19 @@ export class FixImages extends DesugaringStep { if (trpath.typeHint !== "rendered") { continue } - Utils.WalkPath(trpath.path, leaf, (rendered => { + Utils.WalkPath(trpath.path, leaf, (rendered) => { return replaceString(rendered) - })) + }) } } - - return leaf; + return leaf }) } - return { warnings, - result: json - }; + result: json, + } } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts b/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts index 4a779da3c..3b5ac7f8e 100644 --- a/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts +++ b/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts @@ -1,42 +1,51 @@ -import {LayoutConfigJson} from "../Json/LayoutConfigJson"; -import {Utils} from "../../../Utils"; -import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import {DesugaringStep, Each, Fuse, On} from "./Conversion"; - -export class UpdateLegacyLayer extends DesugaringStep { +import { LayoutConfigJson } from "../Json/LayoutConfigJson" +import { Utils } from "../../../Utils" +import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import { DesugaringStep, Each, Fuse, On } from "./Conversion" +export class UpdateLegacyLayer extends DesugaringStep< + LayerConfigJson | string | { builtin; override } +> { constructor() { - super("Updates various attributes from the old data format to the new to provide backwards compatibility with the formats", + super( + "Updates various attributes from the old data format to the new to provide backwards compatibility with the formats", ["overpassTags", "source.osmtags", "tagRenderings[*].id", "mapRendering"], - "UpdateLegacyLayer"); + "UpdateLegacyLayer" + ) } - convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { + convert( + json: LayerConfigJson, + context: string + ): { result: LayerConfigJson; errors: string[]; warnings: string[] } { const warnings = [] if (typeof json === "string" || json["builtin"] !== undefined) { // Reuse of an already existing layer; return as-is - return {result: json, errors: [], warnings: []} + return { result: json, errors: [], warnings: [] } } - let config = {...json}; + let config = { ...json } if (config["overpassTags"]) { config.source = config.source ?? { - osmTags: config["overpassTags"] + osmTags: config["overpassTags"], } config.source.osmTags = config["overpassTags"] delete config["overpassTags"] } if (config.tagRenderings !== undefined) { - let i = 0; + let i = 0 for (const tagRendering of config.tagRenderings) { - i++; - if (typeof tagRendering === "string" || tagRendering["builtin"] !== undefined || tagRendering["rewrite"] !== undefined) { + i++ + if ( + typeof tagRendering === "string" || + tagRendering["builtin"] !== undefined || + tagRendering["rewrite"] !== undefined + ) { continue } if (tagRendering["id"] === undefined) { - if (tagRendering["#"] !== undefined) { tagRendering["id"] = tagRendering["#"] delete tagRendering["#"] @@ -49,7 +58,6 @@ export class UpdateLegacyLayer extends DesugaringStep{ color: config["color"], width: config["width"], - dashArray: config["dashArray"] + dashArray: config["dashArray"], } if (Object.keys(lineRenderConfig).length > 0) { config.mapRendering.push(lineRenderConfig) } } if (config.mapRendering.length === 0) { - throw "Could not convert the legacy theme into a new theme: no renderings defined for layer " + config.id + throw ( + "Could not convert the legacy theme into a new theme: no renderings defined for layer " + + config.id + ) } - } - delete config["color"] delete config["width"] delete config["dashArray"] @@ -100,7 +108,7 @@ export class UpdateLegacyLayer extends DesugaringStep { constructor() { - super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme"); + super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme") } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { - const oldThemeConfig = {...json} + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { + const oldThemeConfig = { ...json } if (oldThemeConfig.socialImage === "") { delete oldThemeConfig.socialImage } - if (oldThemeConfig["roamingRenderings"] !== undefined) { - if (oldThemeConfig["roamingRenderings"].length == 0) { delete oldThemeConfig["roamingRenderings"] } else { return { result: null, - errors: [context + ": The theme contains roamingRenderings. These are not supported anymore"], - warnings: [] + errors: [ + context + + ": The theme contains roamingRenderings. These are not supported anymore", + ], + warnings: [], } } } @@ -152,8 +163,12 @@ class UpdateLegacyTheme extends DesugaringStep { delete oldThemeConfig["version"] if (oldThemeConfig["maintainer"] !== undefined) { - - console.log("Maintainer: ", oldThemeConfig["maintainer"], "credits: ", oldThemeConfig["credits"]) + console.log( + "Maintainer: ", + oldThemeConfig["maintainer"], + "credits: ", + oldThemeConfig["credits"] + ) if (oldThemeConfig.credits === undefined) { oldThemeConfig["credits"] = oldThemeConfig["maintainer"] delete oldThemeConfig["maintainer"] @@ -167,7 +182,7 @@ class UpdateLegacyTheme extends DesugaringStep { return { errors: [], warnings: [], - result: oldThemeConfig + result: oldThemeConfig, } } } @@ -178,8 +193,6 @@ export class FixLegacyTheme extends Fuse { "Fixes a legacy theme to the modern JSON format geared to humans. Syntactic sugars are kept (i.e. no tagRenderings are expandend, no dependencies are automatically gathered)", new UpdateLegacyTheme(), new On("layers", new Each(new UpdateLegacyLayer())) - ); + ) } } - - diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index 74d7174b8..e8c8b9044 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -1,50 +1,74 @@ -import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault} from "./Conversion"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; -import {Utils} from "../../../Utils"; -import RewritableConfigJson from "../Json/RewritableConfigJson"; -import SpecialVisualizations from "../../../UI/SpecialVisualizations"; -import Translations from "../../../UI/i18n/Translations"; -import {Translation} from "../../../UI/i18n/Translation"; +import { + Concat, + Conversion, + DesugaringContext, + DesugaringStep, + Each, + FirstOf, + Fuse, + On, + SetDefault, +} from "./Conversion" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" +import { Utils } from "../../../Utils" +import RewritableConfigJson from "../Json/RewritableConfigJson" +import SpecialVisualizations from "../../../UI/SpecialVisualizations" +import Translations from "../../../UI/i18n/Translations" +import { Translation } from "../../../UI/i18n/Translation" import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json" -import {AddContextToTranslations} from "./AddContextToTranslations"; +import { AddContextToTranslations } from "./AddContextToTranslations" - -class ExpandTagRendering extends Conversion { - private readonly _state: DesugaringContext; - private readonly _self: LayerConfigJson; +class ExpandTagRendering extends Conversion< + string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, + TagRenderingConfigJson[] +> { + private readonly _state: DesugaringContext + private readonly _self: LayerConfigJson private readonly _options: { /* If true, will copy the 'osmSource'-tags into the condition */ - applyCondition?: true | boolean; - }; - - constructor(state: DesugaringContext, self: LayerConfigJson, options?: { applyCondition?: true | boolean;}) { - super("Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question", [], "ExpandTagRendering"); - this._state = state; - this._self = self; - this._options = options; + applyCondition?: true | boolean } - convert(json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, context: string): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { + constructor( + state: DesugaringContext, + self: LayerConfigJson, + options?: { applyCondition?: true | boolean } + ) { + super( + "Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question", + [], + "ExpandTagRendering" + ) + this._state = state + this._self = self + this._options = options + } + + convert( + json: string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, + context: string + ): { result: TagRenderingConfigJson[]; errors: string[]; warnings: string[] } { const errors = [] const warnings = [] return { result: this.convertUntilStable(json, warnings, errors, context), - errors, warnings - }; + errors, + warnings, + } } private lookup(name: string): TagRenderingConfigJson[] { - const state = this._state; + const state = this._state if (state.tagRenderings.has(name)) { return [state.tagRenderings.get(name)] } if (name.indexOf(".") < 0) { - return undefined; + return undefined } - const spl = name.split("."); + const spl = name.split(".") let layer = state.sharedLayers.get(spl[0]) if (spl[0] === this._self.id) { layer = this._self @@ -54,29 +78,30 @@ class ExpandTagRendering extends Conversionlayer.tagRenderings.filter(tr => tr["id"] !== undefined) + const layerTrs = ( + layer.tagRenderings.filter((tr) => tr["id"] !== undefined) + ) let matchingTrs: TagRenderingConfigJson[] if (id === "*") { matchingTrs = layerTrs } else if (id.startsWith("*")) { const id_ = id.substring(1) - matchingTrs = layerTrs.filter(tr => tr.group === id_ || tr.labels?.indexOf(id_) >= 0) + matchingTrs = layerTrs.filter((tr) => tr.group === id_ || tr.labels?.indexOf(id_) >= 0) } else { - matchingTrs = layerTrs.filter(tr => tr.id === id) + matchingTrs = layerTrs.filter((tr) => tr.id === id) } - const contextWriter = new AddContextToTranslations("layers:") for (let i = 0; i < matchingTrs.length; i++) { - let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i]); - if(this._options?.applyCondition){ + let found: TagRenderingConfigJson = Utils.Clone(matchingTrs[i]) + if (this._options?.applyCondition) { // The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown if (found.condition === undefined) { found.condition = layer.source.osmTags } else { - found.condition = {and: [found.condition, layer.source.osmTags]} + found.condition = { and: [found.condition, layer.source.osmTags] } } } @@ -87,29 +112,37 @@ class ExpandTagRendering extends Conversion s) + const candidates = Utils.sortedByLevenshteinDistance( + layerName, + Array.from(state.sharedLayers.keys()), + (s) => s + ) if (state.sharedLayers.size === 0) { - warnings.push(ctx + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) + warnings.push( + ctx + + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + + name + + ": layer " + + layerName + + " not found. Maybe you meant on of " + + candidates.slice(0, 3).join(", ") + ) } else { - errors.push(ctx + ": While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) + errors.push( + ctx + + ": While reusing tagrendering: " + + name + + ": layer " + + layerName + + " not found. Maybe you meant on of " + + candidates.slice(0, 3).join(", ") + ) } continue } - candidates = Utils.NoNull(layer.tagRenderings.map(tr => tr["id"])).map(id => layerName + "." + id) + candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map( + (id) => layerName + "." + id + ) } - candidates = Utils.sortedByLevenshteinDistance(name, candidates, i => i); - errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + candidates.join(", ") + "?") + candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i) + errors.push( + ctx + + ": The tagRendering with identifier " + + name + + " was not found.\n\tDid you mean one of " + + candidates.join(", ") + + "?" + ) continue } for (let foundTr of lookup) { @@ -159,36 +233,44 @@ class ExpandTagRendering extends Conversion extends Conversion, T[]> { - constructor() { - super("Applies a rewrite", [], "ExpandRewrite"); + super("Applies a rewrite", [], "ExpandRewrite") } - /** * Used for left|right group creation and replacement. * Every 'keyToRewrite' will be replaced with 'target' recursively. This substitution will happen in place in the object 'tr' @@ -210,7 +292,6 @@ export class ExpandRewrite extends Conversion, T[ const targetIsTranslation = Translations.isProbablyATranslation(target) function replaceRecursive(obj: string | any, target) { - if (obj === keyToRewrite) { return target } @@ -224,11 +305,11 @@ export class ExpandRewrite extends Conversion, T[ } if (Array.isArray(obj)) { // This is a list of items - return obj.map(o => replaceRecursive(o, target)) + return obj.map((o) => replaceRecursive(o, target)) } if (typeof obj === "object") { - obj = {...obj} + obj = { ...obj } const isTr = targetIsTranslation && Translations.isProbablyATranslation(obj) @@ -257,7 +338,7 @@ export class ExpandRewrite extends Conversion, T[ * sourceString: ["xyz","abc"], * into: [ * ["X", "A"], - * ["Y", "B"], + * ["Y", "B"], * ["Z", "C"]], * }, * renderings: "The value of xyz is abc" @@ -286,25 +367,27 @@ export class ExpandRewrite extends Conversion, T[ * ] * new ExpandRewrite().convertStrict(spec, "test") // => expected */ - convert(json: T | RewritableConfigJson, context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } { - + convert( + json: T | RewritableConfigJson, + context: string + ): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } { if (json === null || json === undefined) { - return {result: []} + return { result: [] } } if (json["rewrite"] === undefined) { - // not a rewrite - return {result: [(json)]} + return { result: [json] } } - const rewrite = >json; + const rewrite = >json const keysToRewrite = rewrite.rewrite const ts: T[] = [] - {// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered + { + // sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered for (let i = 0; i < keysToRewrite.sourceString.length; i++) { - const guard = keysToRewrite.sourceString[i]; + const guard = keysToRewrite.sourceString[i] for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) { const toRewrite = keysToRewrite.sourceString[j] if (toRewrite.indexOf(guard) >= 0) { @@ -314,12 +397,12 @@ export class ExpandRewrite extends Conversion, T[ } } - {// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case + { + // sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case for (let i = 0; i < rewrite.rewrite.into.length; i++) { const into = keysToRewrite.into[i] if (into.length !== rewrite.rewrite.sourceString.length) { throw `${context}.into.${i} Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values` - } } } @@ -327,17 +410,15 @@ export class ExpandRewrite extends Conversion, T[ for (let i = 0; i < keysToRewrite.into.length; i++) { let t = Utils.Clone(rewrite.renderings) for (let j = 0; j < keysToRewrite.sourceString.length; j++) { - const key = keysToRewrite.sourceString[j]; + const key = keysToRewrite.sourceString[j] const target = keysToRewrite.into[i][j] t = ExpandRewrite.RewriteParts(key, target, t) } ts.push(t) } - - return {result: ts}; + return { result: ts } } - } /** @@ -345,7 +426,11 @@ export class ExpandRewrite extends Conversion, T[ */ export class RewriteSpecial extends DesugaringStep { constructor() { - super("Converts a 'special' translation into a regular translation which uses parameters", ["special"], "RewriteSpecial"); + super( + "Converts a 'special' translation into a regular translation which uses parameters", + ["special"], + "RewriteSpecial" + ) } /** @@ -406,7 +491,11 @@ export class RewriteSpecial extends DesugaringStep { * RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"en": "

Entrances

This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An entrance of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"} * errors // => [] */ - private static convertIfNeeded(input: (object & { special: { type: string } }) | any, errors: string[], context: string): any { + private static convertIfNeeded( + input: (object & { special: { type: string } }) | any, + errors: string[], + context: string + ): any { const special = input["special"] if (special === undefined) { return input @@ -414,37 +503,55 @@ export class RewriteSpecial extends DesugaringStep { const type = special["type"] if (type === undefined) { - errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used") + errors.push( + "A 'special'-block should define 'type' to indicate which visualisation should be used" + ) return undefined } - const vis = SpecialVisualizations.specialVisualizations.find(sp => sp.funcName === type) + const vis = SpecialVisualizations.specialVisualizations.find((sp) => sp.funcName === type) if (vis === undefined) { - const options = Utils.sortedByLevenshteinDistance(type, SpecialVisualizations.specialVisualizations, sp => sp.funcName) - errors.push(`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`) + const options = Utils.sortedByLevenshteinDistance( + type, + SpecialVisualizations.specialVisualizations, + (sp) => sp.funcName + ) + errors.push( + `Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md` + ) return undefined } - errors.push(... - Array.from(Object.keys(input)).filter(k => k !== "special" && k !== "before" && k !== "after") - .map(k => { - return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`; - })) + errors.push( + ...Array.from(Object.keys(input)) + .filter((k) => k !== "special" && k !== "before" && k !== "after") + .map((k) => { + return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?` + }) + ) - const argNamesList = vis.args.map(a => a.name) + const argNamesList = vis.args.map((a) => a.name) const argNames = new Set(argNamesList) // Check for obsolete and misspelled arguments - errors.push(...Object.keys(special) - .filter(k => !argNames.has(k)) - .filter(k => k !== "type" && k !== "before" && k !== "after") - .map(wrongArg => { - const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x) - return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${argNamesList.join(", ")}`; - })) + errors.push( + ...Object.keys(special) + .filter((k) => !argNames.has(k)) + .filter((k) => k !== "type" && k !== "before" && k !== "after") + .map((wrongArg) => { + const byDistance = Utils.sortedByLevenshteinDistance( + wrongArg, + argNamesList, + (x) => x + ) + return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${ + byDistance[0] + }?\n\tAll known arguments are ${argNamesList.join(", ")}` + }) + ) // Check that all obligated arguments are present. They are obligated if they don't have a preset value for (const arg of vis.args) { if (arg.required !== true) { - continue; + continue } const param = special[arg.name] if (param === undefined) { @@ -453,9 +560,10 @@ export class RewriteSpecial extends DesugaringStep { } const foundLanguages = new Set() - const translatedArgs = argNamesList.map(nm => special[nm]) - .filter(v => v !== undefined) - .filter(v => Translations.isProbablyATranslation(v)) + const translatedArgs = argNamesList + .map((nm) => special[nm]) + .filter((v) => v !== undefined) + .filter((v) => Translations.isProbablyATranslation(v)) for (const translatedArg of translatedArgs) { for (const ln of Object.keys(translatedArg)) { foundLanguages.add(ln) @@ -473,9 +581,9 @@ export class RewriteSpecial extends DesugaringStep { } if (foundLanguages.size === 0) { - const args = argNamesList.map(nm => special[nm] ?? "").join(",") + const args = argNamesList.map((nm) => special[nm] ?? "").join(",") return { - '*': `{${type}(${args})}` + "*": `{${type}(${args})}`, } } @@ -487,16 +595,16 @@ export class RewriteSpecial extends DesugaringStep { for (const argName of argNamesList) { let v = special[argName] ?? "" if (Translations.isProbablyATranslation(v)) { - v = new Translation(v).textFor(ln) - - } - + v = new Translation(v).textFor(ln) + } + if (typeof v === "string") { - const txt = v.replace(/,/g, "&COMMA") + const txt = v + .replace(/,/g, "&COMMA") .replace(/\{/g, "&LBRACE") .replace(/}/g, "&RBRACE") .replace(/\(/g, "&LPARENS") - .replace(/\)/g, '&RPARENS') + .replace(/\)/g, "&RPARENS") args.push(txt) } else if (typeof v === "object") { args.push(JSON.stringify(v)) @@ -506,7 +614,7 @@ export class RewriteSpecial extends DesugaringStep { } const beforeText = before?.textFor(ln) ?? "" const afterText = after?.textFor(ln) ?? "" - result[ln] = `${beforeText}{${type}(${args.map(a => a).join(",")})}${afterText}` + result[ln] = `${beforeText}{${type}(${args.map((a) => a).join(",")})}${afterText}` } return result } @@ -541,23 +649,33 @@ export class RewriteSpecial extends DesugaringStep { * const expected = {render: {'en': "{image_carousel(image)}Some footer"}} * result // => expected */ - convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: TagRenderingConfigJson, + context: string + ): { + result: TagRenderingConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { const errors = [] json = Utils.Clone(json) - const paths: { path: string[], type?: any, typeHint?: string }[] = tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta + const paths: { path: string[]; type?: any; typeHint?: string }[] = + tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta for (const path of paths) { if (path.typeHint !== "rendered") { continue } - Utils.WalkPath(path.path, json, ((leaf, travelled) => RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join(".")))) + Utils.WalkPath(path.path, json, (leaf, travelled) => + RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join(".")) + ) } return { result: json, - errors - }; + errors, + } } - } export class PrepareLayer extends Fuse { @@ -566,11 +684,22 @@ export class PrepareLayer extends Fuse { "Fully prepares and expands a layer for the LayerConfig.", new On("tagRenderings", new Each(new RewriteSpecial())), new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), - new On("tagRenderings", layer => new Concat(new ExpandTagRendering(state, layer))), + new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))), new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), - new On("mapRendering", layer => new Each(new On("icon", new FirstOf(new ExpandTagRendering(state, layer, {applyCondition: false}))))), + new On( + "mapRendering", + (layer) => + new Each( + new On( + "icon", + new FirstOf( + new ExpandTagRendering(state, layer, { applyCondition: false }) + ) + ) + ) + ), new SetDefault("titleIcons", ["defaults"]), - new On("titleIcons", layer => new Concat(new ExpandTagRendering(state, layer))) - ); + new On("titleIcons", (layer) => new Concat(new ExpandTagRendering(state, layer))) + ) } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/PrepareTheme.ts b/Models/ThemeConfig/Conversion/PrepareTheme.ts index df0807120..f65a768da 100644 --- a/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -1,42 +1,59 @@ -import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault} from "./Conversion"; -import {LayoutConfigJson} from "../Json/LayoutConfigJson"; -import {PrepareLayer} from "./PrepareLayer"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import {Utils} from "../../../Utils"; -import Constants from "../../Constants"; -import CreateNoteImportLayer from "./CreateNoteImportLayer"; -import LayerConfig from "../LayerConfig"; -import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; -import {SubstitutedTranslation} from "../../../UI/SubstitutedTranslation"; -import DependencyCalculator from "../DependencyCalculator"; -import {AddContextToTranslations} from "./AddContextToTranslations"; +import { + Concat, + Conversion, + DesugaringContext, + DesugaringStep, + Each, + Fuse, + On, + Pass, + SetDefault, +} from "./Conversion" +import { LayoutConfigJson } from "../Json/LayoutConfigJson" +import { PrepareLayer } from "./PrepareLayer" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import { Utils } from "../../../Utils" +import Constants from "../../Constants" +import CreateNoteImportLayer from "./CreateNoteImportLayer" +import LayerConfig from "../LayerConfig" +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" +import { SubstitutedTranslation } from "../../../UI/SubstitutedTranslation" +import DependencyCalculator from "../DependencyCalculator" +import { AddContextToTranslations } from "./AddContextToTranslations" -class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> { - private readonly _state: DesugaringContext; +class SubstituteLayer extends Conversion { + private readonly _state: DesugaringContext - constructor( - state: DesugaringContext, - ) { - super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", [], "SubstituteLayer"); - this._state = state; + constructor(state: DesugaringContext) { + super( + "Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", + [], + "SubstituteLayer" + ) + this._state = state } - convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], information?: string[] } { + convert( + json: string | LayerConfigJson, + context: string + ): { result: LayerConfigJson[]; errors: string[]; information?: string[] } { const errors = [] const information = [] const state = this._state function reportNotFound(name: string) { const knownLayers = Array.from(state.sharedLayers.keys()) - const withDistance = knownLayers.map(lname => [lname, Utils.levenshteinDistance(name, lname)]) + const withDistance = knownLayers.map((lname) => [ + lname, + Utils.levenshteinDistance(name, lname), + ]) withDistance.sort((a, b) => a[1] - b[1]) - const ids = withDistance.map(n => n[0]) + const ids = withDistance.map((n) => n[0]) // Known builtin layers are "+.join(",")+"\n For more information, see " errors.push(`${context}: The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}? For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`) } - if (typeof json === "string") { const found = state.sharedLayers.get(json) if (found === undefined) { @@ -48,7 +65,7 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig } return { result: [found], - errors + errors, } } @@ -65,49 +82,80 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig reportNotFound(name) continue } - if (json["override"]["tagRenderings"] !== undefined && (found["tagRenderings"] ?? []).length > 0) { - errors.push(`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`) + if ( + json["override"]["tagRenderings"] !== undefined && + (found["tagRenderings"] ?? []).length > 0 + ) { + errors.push( + `At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.` + ) } try { - Utils.Merge(json["override"], found); + Utils.Merge(json["override"], found) layers.push(found) } catch (e) { - errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`) + errors.push( + `At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify( + json["override"] + )}` + ) } if (json["hideTagRenderingsWithLabels"]) { const hideLabels: Set = new Set(json["hideTagRenderingsWithLabels"]) // These labels caused at least one deletion - const usedLabels: Set = new Set(); + const usedLabels: Set = new Set() const filtered = [] for (const tr of found.tagRenderings) { const labels = tr["labels"] if (labels !== undefined) { - const forbiddenLabel = labels.findIndex(l => hideLabels.has(l)) + const forbiddenLabel = labels.findIndex((l) => hideLabels.has(l)) if (forbiddenLabel >= 0) { usedLabels.add(labels[forbiddenLabel]) - information.push(context + ": Dropping tagRendering " + tr["id"] + " as it has a forbidden label: " + labels[forbiddenLabel]) + information.push( + context + + ": Dropping tagRendering " + + tr["id"] + + " as it has a forbidden label: " + + labels[forbiddenLabel] + ) continue } } if (hideLabels.has(tr["id"])) { usedLabels.add(tr["id"]) - information.push(context + ": Dropping tagRendering " + tr["id"] + " as its id is a forbidden label") + information.push( + context + + ": Dropping tagRendering " + + tr["id"] + + " as its id is a forbidden label" + ) continue } if (hideLabels.has(tr["group"])) { usedLabels.add(tr["group"]) - information.push(context + ": Dropping tagRendering " + tr["id"] + " as its group `" + tr["group"] + "` is a forbidden label") + information.push( + context + + ": Dropping tagRendering " + + tr["id"] + + " as its group `" + + tr["group"] + + "` is a forbidden label" + ) continue } filtered.push(tr) } - const unused = Array.from(hideLabels).filter(l => !usedLabels.has(l)) + const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l)) if (unused.length > 0) { - errors.push("This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + unused.join(", ") + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore") + errors.push( + "This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + + unused.join(", ") + + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore" + ) } found.tagRenderings = filtered } @@ -115,33 +163,38 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig return { result: layers, errors, - information + information, } - } return { result: [json], - errors - }; + errors, + } } - } class AddDefaultLayers extends DesugaringStep { - private _state: DesugaringContext; + private _state: DesugaringContext constructor(state: DesugaringContext) { - super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"], "AddDefaultLayers"); - this._state = state; + super( + "Adds the default layers, namely: " + Constants.added_by_default.join(", "), + ["layers"], + "AddDefaultLayers" + ) + this._state = state } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { const errors = [] const warnings = [] const state = this._state json.layers = [...json.layers] - const alreadyLoaded = new Set(json.layers.map(l => l["id"])) + const alreadyLoaded = new Set(json.layers.map((l) => l["id"])) for (const layerName of Constants.added_by_default) { const v = state.sharedLayers.get(layerName) @@ -150,7 +203,13 @@ class AddDefaultLayers extends DesugaringStep { continue } if (alreadyLoaded.has(v.id)) { - warnings.push("Layout " + context + " already has a layer with name " + v.id + "; skipping inclusion of this builtin layer") + warnings.push( + "Layout " + + context + + " already has a layer with name " + + v.id + + "; skipping inclusion of this builtin layer" + ) continue } json.layers.push(v) @@ -159,34 +218,43 @@ class AddDefaultLayers extends DesugaringStep { return { result: json, errors, - warnings - }; + warnings, + } } - } class AddImportLayers extends DesugaringStep { constructor() { - super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"], "AddImportLayers"); + super( + "For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", + ["layers"], + "AddImportLayers" + ) } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[], warnings?: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { if (!(json.enableNoteImports ?? true)) { return { - warnings: ["Not creating a note import layers for theme "+json.id+" as they are disabled"], - result: json - }; + warnings: [ + "Not creating a note import layers for theme " + + json.id + + " as they are disabled", + ], + result: json, + } } const errors = [] - json = {...json} - const allLayers: LayerConfigJson[] = json.layers; + json = { ...json } + const allLayers: LayerConfigJson[] = json.layers json.layers = [...json.layers] - const creator = new CreateNoteImportLayer() for (let i1 = 0; i1 < allLayers.length; i1++) { - const layer = allLayers[i1]; + const layer = allLayers[i1] if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { // Priviliged layers are skipped continue @@ -204,12 +272,14 @@ class AddImportLayers extends DesugaringStep { if (layer.presets === undefined || layer.presets.length == 0) { // A preset is needed to be able to generate a new point - continue; + continue } try { - - const importLayerResult = creator.convert(layer, context + ".(noteimportlayer)[" + i1 + "]") + const importLayerResult = creator.convert( + layer, + context + ".(noteimportlayer)[" + i1 + "]" + ) if (importLayerResult.result !== undefined) { json.layers.push(importLayerResult.result) } @@ -220,18 +290,21 @@ class AddImportLayers extends DesugaringStep { return { errors, - result: json - }; + result: json, + } } } - export class AddMiniMap extends DesugaringStep { - private readonly _state: DesugaringContext; + private readonly _state: DesugaringContext - constructor(state: DesugaringContext,) { - super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"], "AddMiniMap"); - this._state = state; + constructor(state: DesugaringContext) { + super( + "Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", + ["tagRenderings"], + "AddMiniMap" + ) + this._state = state } /** @@ -249,72 +322,94 @@ export class AddMiniMap extends DesugaringStep { * AddMiniMap.hasMinimap({render: "Some random value {minimap}"}) // => false */ static hasMinimap(renderingConfig: TagRenderingConfigJson): boolean { - const translations: any[] = Utils.NoNull([renderingConfig.render, ...(renderingConfig.mappings ?? []).map(m => m.then)]); + const translations: any[] = Utils.NoNull([ + renderingConfig.render, + ...(renderingConfig.mappings ?? []).map((m) => m.then), + ]) for (let translation of translations) { if (typeof translation == "string") { - translation = {"*": translation} + translation = { "*": translation } } for (const key in translation) { if (!translation.hasOwnProperty(key)) { continue } - + const template = translation[key] const parts = SubstitutedTranslation.ExtractSpecialComponents(template) - const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") + const hasMiniMap = parts + .filter((part) => part.special !== undefined) + .some((special) => special.special.func.funcName === "minimap") if (hasMiniMap) { - return true; + return true } } } - return false; + return false } convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } { - - const state = this._state; - const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(tr)) ?? true + const state = this._state + const hasMinimap = + layerConfig.tagRenderings?.some((tr) => + AddMiniMap.hasMinimap(tr) + ) ?? true if (!hasMinimap) { - layerConfig = {...layerConfig} + layerConfig = { ...layerConfig } layerConfig.tagRenderings = [...layerConfig.tagRenderings] layerConfig.tagRenderings.push(state.tagRenderings.get("questions")) layerConfig.tagRenderings.push(state.tagRenderings.get("minimap")) } return { - result: layerConfig - }; + result: layerConfig, + } } } -class AddContextToTransltionsInLayout extends DesugaringStep { - +class AddContextToTransltionsInLayout extends DesugaringStep { constructor() { - super("Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too", ["_context"], "AddContextToTranlationsInLayout"); + super( + "Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too", + ["_context"], + "AddContextToTranlationsInLayout" + ) } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { + result: LayoutConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { const conversion = new AddContextToTranslations("themes:") - return conversion.convert(json, json.id); + return conversion.convert(json, json.id) } - } class ApplyOverrideAll extends DesugaringStep { - constructor() { - super("Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", ["overrideAll", "layers"], "ApplyOverrideAll"); + super( + "Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", + ["overrideAll", "layers"], + "ApplyOverrideAll" + ) } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { - - const overrideAll = json.overrideAll; + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { + const overrideAll = json.overrideAll if (overrideAll === undefined) { - return {result: json, warnings: [], errors: []} + return { result: json, warnings: [], errors: [] } } - json = {...json} + json = { ...json } delete json.overrideAll const newLayers = [] @@ -325,157 +420,215 @@ class ApplyOverrideAll extends DesugaringStep { } json.layers = newLayers - - return {result: json, warnings: [], errors: []}; + return { result: json, warnings: [], errors: [] } } - } class AddDependencyLayersToTheme extends DesugaringStep { - private readonly _state: DesugaringContext; + private readonly _state: DesugaringContext - constructor(state: DesugaringContext,) { + constructor(state: DesugaringContext) { super( `If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically) Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature. Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers. Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too. - `, ["layers"], "AddDependencyLayersToTheme"); - this._state = state; + `, + ["layers"], + "AddDependencyLayersToTheme" + ) + this._state = state } - private static CalculateDependencies(alreadyLoaded: LayerConfigJson[], allKnownLayers: Map, themeId: string): - {config: LayerConfigJson, reason: string}[] { - const dependenciesToAdd: {config: LayerConfigJson, reason: string}[] = [] - const loadedLayerIds: Set = new Set(alreadyLoaded.map(l => l.id)); + private static CalculateDependencies( + alreadyLoaded: LayerConfigJson[], + allKnownLayers: Map, + themeId: string + ): { config: LayerConfigJson; reason: string }[] { + const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = [] + const loadedLayerIds: Set = new Set(alreadyLoaded.map((l) => l.id)) // Verify cross-dependencies - let unmetDependencies: { neededLayer: string, neededBy: string, reason: string, context?: string }[] = [] + let unmetDependencies: { + neededLayer: string + neededBy: string + reason: string + context?: string + }[] = [] do { - const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = [] + const dependencies: { + neededLayer: string + reason: string + context?: string + neededBy: string + }[] = [] for (const layerConfig of alreadyLoaded) { try { - const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig, themeId+"(dependencies)")) + const layerDeps = DependencyCalculator.getLayerDependencies( + new LayerConfig(layerConfig, themeId + "(dependencies)") + ) dependencies.push(...layerDeps) } catch (e) { console.error(e) - throw "Detecting layer dependencies for " + layerConfig.id + " failed due to " + e + throw ( + "Detecting layer dependencies for " + layerConfig.id + " failed due to " + e + ) } } for (const dependency of dependencies) { if (loadedLayerIds.has(dependency.neededLayer)) { // We mark the needed layer as 'mustLoad' - alreadyLoaded.find(l => l.id === dependency.neededLayer).forceLoad = true + alreadyLoaded.find((l) => l.id === dependency.neededLayer).forceLoad = true } } // During the generate script, builtin layers are verified but not loaded - so we have to add them manually here // Their existence is checked elsewhere, so this is fine - unmetDependencies = dependencies.filter(dep => !loadedLayerIds.has(dep.neededLayer)) + unmetDependencies = dependencies.filter((dep) => !loadedLayerIds.has(dep.neededLayer)) for (const unmetDependency of unmetDependencies) { if (loadedLayerIds.has(unmetDependency.neededLayer)) { continue } const dep = Utils.Clone(allKnownLayers.get(unmetDependency.neededLayer)) - const reason = "This layer is needed by " + unmetDependency.neededBy +" because " + - unmetDependency.reason + " (at " + unmetDependency.context + ")"; + const reason = + "This layer is needed by " + + unmetDependency.neededBy + + " because " + + unmetDependency.reason + + " (at " + + unmetDependency.context + + ")" if (dep === undefined) { - const message = - ["Loading a dependency failed: layer " + unmetDependency.neededLayer + " is not found, neither as layer of " + themeId + " nor as builtin layer.", - reason, - "Loaded layers are: " + alreadyLoaded.map(l => l.id).join(",") - - ] - throw message.join("\n\t"); + const message = [ + "Loading a dependency failed: layer " + + unmetDependency.neededLayer + + " is not found, neither as layer of " + + themeId + + " nor as builtin layer.", + reason, + "Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","), + ] + throw message.join("\n\t") } - - dep.forceLoad = true; - dep.passAllFeatures = true; - dep.description = reason; + + dep.forceLoad = true + dep.passAllFeatures = true + dep.description = reason dependenciesToAdd.unshift({ config: dep, - reason + reason, }) - loadedLayerIds.add(dep.id); - unmetDependencies = unmetDependencies.filter(d => d.neededLayer !== unmetDependency.neededLayer) + loadedLayerIds.add(dep.id) + unmetDependencies = unmetDependencies.filter( + (d) => d.neededLayer !== unmetDependency.neededLayer + ) } - } while (unmetDependencies.length > 0) return dependenciesToAdd } - convert(theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; information: string[] } { + convert( + theme: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; information: string[] } { const state = this._state - const allKnownLayers: Map = state.sharedLayers; - const knownTagRenderings: Map = state.tagRenderings; - const information = []; - const layers: LayerConfigJson[] = theme.layers; // Layers should be expanded at this point + const allKnownLayers: Map = state.sharedLayers + const knownTagRenderings: Map = state.tagRenderings + const information = [] + const layers: LayerConfigJson[] = theme.layers // Layers should be expanded at this point knownTagRenderings.forEach((value, key) => { - value.id = key; + value.id = key }) - const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id); + const dependencies = AddDependencyLayersToTheme.CalculateDependencies( + layers, + allKnownLayers, + theme.id + ) for (const dependency of dependencies) { - } if (dependencies.length > 0) { for (const dependency of dependencies) { - information.push(context + ": added " + dependency.config.id + " to the theme. "+dependency.reason) - + information.push( + context + + ": added " + + dependency.config.id + + " to the theme. " + + dependency.reason + ) } } - layers.unshift(...dependencies.map(l => l.config)); + layers.unshift(...dependencies.map((l) => l.config)) return { result: { ...theme, - layers: layers + layers: layers, }, - information - }; + information, + } } } class PreparePersonalTheme extends DesugaringStep { - private readonly _state: DesugaringContext; + private readonly _state: DesugaringContext constructor(state: DesugaringContext) { - super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme"); - this._state = state; + super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme") + this._state = state } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { + result: LayoutConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { if (json.id !== "personal") { - return {result: json} + return { result: json } } - + // The only thing this _really_ does, is adding the layer-ids into 'layers' // All other preparations are done by the 'override-all'-block in personal.json json.layers = Array.from(this._state.sharedLayers.keys()) - .filter(l => Constants.priviliged_layers.indexOf(l) < 0) - .filter(l => this._state.publicLayers.has(l)) - return {result: json, information: [ - "The personal theme has "+json.layers.length+" public layers" - ]}; + .filter((l) => Constants.priviliged_layers.indexOf(l) < 0) + .filter((l) => this._state.publicLayers.has(l)) + return { + result: json, + information: ["The personal theme has " + json.layers.length + " public layers"], + } } - } class WarnForUnsubstitutedLayersInTheme extends DesugaringStep { - constructor() { - super("Generates a warning if a theme uses an unsubstituted layer", ["layers"], "WarnForUnsubstitutedLayersInTheme"); + super( + "Generates a warning if a theme uses an unsubstituted layer", + ["layers"], + "WarnForUnsubstitutedLayersInTheme" + ) } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { + result: LayoutConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { if (json.hideFromOverview === true) { - return {result: json} + return { result: json } } const warnings = [] for (const layer of json.layers) { @@ -490,21 +643,28 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep continue } - const wrn = "The theme " + json.id + " has an inline layer: " + layer["id"] + ". This is discouraged." + const wrn = + "The theme " + + json.id + + " has an inline layer: " + + layer["id"] + + ". This is discouraged." warnings.push(wrn) } return { result: json, - warnings - }; + warnings, + } } - } export class PrepareTheme extends Fuse { - constructor(state: DesugaringContext, options?: { - skipDefaultLayers: false | boolean - }) { + constructor( + state: DesugaringContext, + options?: { + skipDefaultLayers: false | boolean + } + ) { super( "Fully prepares and expands a theme", @@ -519,10 +679,12 @@ export class PrepareTheme extends Fuse { new ApplyOverrideAll(), // And then we prepare all the layers _again_ in case that an override all contained unexpanded tagrenderings! new On("layers", new Each(new PrepareLayer(state))), - options?.skipDefaultLayers ? new Pass("AddDefaultLayers is disabled due to the set flag") : new AddDefaultLayers(state), + options?.skipDefaultLayers + ? new Pass("AddDefaultLayers is disabled due to the set flag") + : new AddDefaultLayers(state), new AddDependencyLayersToTheme(state), new AddImportLayers(), new On("layers", new Each(new AddMiniMap(state))) - ); + ) } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 44b1763f6..d7d2acd09 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -1,96 +1,119 @@ -import {DesugaringStep, Each, Fuse, On} from "./Conversion"; -import {LayerConfigJson} from "../Json/LayerConfigJson"; -import LayerConfig from "../LayerConfig"; -import {Utils} from "../../../Utils"; -import Constants from "../../Constants"; -import {Translation} from "../../../UI/i18n/Translation"; -import {LayoutConfigJson} from "../Json/LayoutConfigJson"; -import LayoutConfig from "../LayoutConfig"; -import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; -import {TagUtils} from "../../../Logic/Tags/TagUtils"; -import {ExtractImages} from "./FixImages"; -import ScriptUtils from "../../../scripts/ScriptUtils"; -import {And} from "../../../Logic/Tags/And"; -import Translations from "../../../UI/i18n/Translations"; -import Svg from "../../../Svg"; -import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson"; - +import { DesugaringStep, Each, Fuse, On } from "./Conversion" +import { LayerConfigJson } from "../Json/LayerConfigJson" +import LayerConfig from "../LayerConfig" +import { Utils } from "../../../Utils" +import Constants from "../../Constants" +import { Translation } from "../../../UI/i18n/Translation" +import { LayoutConfigJson } from "../Json/LayoutConfigJson" +import LayoutConfig from "../LayoutConfig" +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" +import { TagUtils } from "../../../Logic/Tags/TagUtils" +import { ExtractImages } from "./FixImages" +import ScriptUtils from "../../../scripts/ScriptUtils" +import { And } from "../../../Logic/Tags/And" +import Translations from "../../../UI/i18n/Translations" +import Svg from "../../../Svg" +import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" class ValidateLanguageCompleteness extends DesugaringStep { - - private readonly _languages: string[]; + private readonly _languages: string[] constructor(...languages: string[]) { - super("Checks that the given object is fully translated in the specified languages", [], "ValidateLanguageCompleteness"); - this._languages = languages ?? ["en"]; + super( + "Checks that the given object is fully translated in the specified languages", + [], + "ValidateLanguageCompleteness" + ) + this._languages = languages ?? ["en"] } convert(obj: any, context: string): { result: LayerConfig; errors: string[] } { const errors = [] - const translations = Translation.ExtractAllTranslationsFrom( - obj - ) + const translations = Translation.ExtractAllTranslationsFrom(obj) for (const neededLanguage of this._languages) { translations - .filter(t => t.tr.translations[neededLanguage] === undefined && t.tr.translations["*"] === undefined) - .forEach(missing => { - errors.push(context + "A theme should be translation-complete for " + neededLanguage + ", but it lacks a translation for " + missing.context + ".\n\tThe known translation is " + missing.tr.textFor('en')) + .filter( + (t) => + t.tr.translations[neededLanguage] === undefined && + t.tr.translations["*"] === undefined + ) + .forEach((missing) => { + errors.push( + context + + "A theme should be translation-complete for " + + neededLanguage + + ", but it lacks a translation for " + + missing.context + + ".\n\tThe known translation is " + + missing.tr.textFor("en") + ) }) } return { result: obj, - errors - }; + errors, + } } } export class DoesImageExist extends DesugaringStep { + private readonly _knownImagePaths: Set + private readonly doesPathExist: (path: string) => boolean = undefined - private readonly _knownImagePaths: Set; - private readonly doesPathExist: (path: string) => boolean = undefined; - - constructor(knownImagePaths: Set, checkExistsSync: (path: string) => boolean = undefined) { - super("Checks if an image exists", [], "DoesImageExist"); - this._knownImagePaths = knownImagePaths; - this.doesPathExist = checkExistsSync; + constructor( + knownImagePaths: Set, + checkExistsSync: (path: string) => boolean = undefined + ) { + super("Checks if an image exists", [], "DoesImageExist") + this._knownImagePaths = knownImagePaths + this.doesPathExist = checkExistsSync } - convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + image: string, + context: string + ): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } { const errors = [] const warnings = [] const information = [] if (image.indexOf("{") >= 0) { information.push("Ignoring image with { in the path: " + image) - return {result: image} + return { result: image } } if (image === "assets/SocialImage.png") { - return {result: image} + return { result: image } } if (image.match(/[a-z]*/)) { - if (Svg.All[image + ".svg"] !== undefined) { // This is a builtin img, e.g. 'checkmark' or 'crosshair' - return {result: image}; + return { result: image } } } - + if (!this._knownImagePaths.has(image)) { if (this.doesPathExist === undefined) { - errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`) + errors.push( + `Image with path ${image} not found or not attributed; it is used in ${context}` + ) } else if (!this.doesPathExist(image)) { - errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`) + errors.push( + `Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.` + ) } else { - errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`) + errors.push( + `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` + ) } } return { result: image, - errors, warnings, information + errors, + warnings, + information, } } - } class ValidateTheme extends DesugaringStep { @@ -98,20 +121,28 @@ class ValidateTheme extends DesugaringStep { * The paths where this layer is originally saved. Triggers some extra checks * @private */ - private readonly _path?: string; - private readonly _isBuiltin: boolean; - private _sharedTagRenderings: Map; - private readonly _validateImage: DesugaringStep; + private readonly _path?: string + private readonly _isBuiltin: boolean + private _sharedTagRenderings: Map + private readonly _validateImage: DesugaringStep - constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { - super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme"); - this._validateImage = doesImageExist; - this._path = path; - this._isBuiltin = isBuiltin; - this._sharedTagRenderings = sharedTagRenderings; + constructor( + doesImageExist: DoesImageExist, + path: string, + isBuiltin: boolean, + sharedTagRenderings: Map + ) { + super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme") + this._validateImage = doesImageExist + this._path = path + this._isBuiltin = isBuiltin + this._sharedTagRenderings = sharedTagRenderings } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } { const errors = [] const warnings = [] const information = [] @@ -119,55 +150,77 @@ class ValidateTheme extends DesugaringStep { const theme = new LayoutConfig(json, true) { - // Legacy format checks + // Legacy format checks if (this._isBuiltin) { if (json["units"] !== undefined) { - errors.push("The theme " + json.id + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ") + errors.push( + "The theme " + + json.id + + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " + ) } if (json["roamingRenderings"] !== undefined) { - errors.push("Theme " + json.id + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead") + errors.push( + "Theme " + + json.id + + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead" + ) } } } { // Check images: are they local, are the licenses there, is the theme icon square, ... - const images = new ExtractImages(this._isBuiltin, this._sharedTagRenderings).convertStrict(json, "validation") - const remoteImages = images.filter(img => img.indexOf("http") == 0) + const images = new ExtractImages( + this._isBuiltin, + this._sharedTagRenderings + ).convertStrict(json, "validation") + const remoteImages = images.filter((img) => img.indexOf("http") == 0) for (const remoteImage of remoteImages) { - errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.") + errors.push( + "Found a remote image: " + + remoteImage + + " in theme " + + json.id + + ", please download it." + ) } for (const image of images) { - this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information) + this._validateImage.convertJoin( + image, + context === undefined ? "" : ` in a layer defined in the theme ${context}`, + errors, + warnings, + information + ) } if (json.icon.endsWith(".svg")) { try { - ScriptUtils.ReadSvgSync(json.icon, svg => { - const width: string = svg.$.width; - const height: string = svg.$.height; + ScriptUtils.ReadSvgSync(json.icon, (svg) => { + const width: string = svg.$.width + const height: string = svg.$.height if (width !== height) { - const e = `the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` + - ` Width = ${width} height = ${height}`; - (json.hideFromOverview ? warnings : errors).push(e) + const e = + `the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` + + ` Width = ${width} height = ${height}` + ;(json.hideFromOverview ? warnings : errors).push(e) } - const w = parseInt(width); + const w = parseInt(width) const h = parseInt(height) if (w < 370 || h < 370) { const e: string = [ `the icon for theme ${json.id} is too small. Please rescale the icon at ${json.icon}`, `Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images.`, ` Width = ${width} height = ${height}; we recommend a size of at least 500px * 500px and to use a square aspect ratio.`, - ].join("\n"); - (json.hideFromOverview ? warnings : errors).push(e) + ].join("\n") + ;(json.hideFromOverview ? warnings : errors).push(e) } - }) } catch (e) { console.error("Could not read " + json.icon + " due to " + e) } } - } try { @@ -175,36 +228,53 @@ class ValidateTheme extends DesugaringStep { errors.push("Theme ids should be in lowercase, but it is " + theme.id) } - const filename = this._path.substring(this._path.lastIndexOf("/") + 1, this._path.length - 5) + const filename = this._path.substring( + this._path.lastIndexOf("/") + 1, + this._path.length - 5 + ) if (theme.id !== filename) { - errors.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + this._path + ")") + errors.push( + "Theme ids should be the same as the name.json, but we got id: " + + theme.id + + " and filename " + + filename + + " (" + + this._path + + ")" + ) } - this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information); - const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"])) + this._validateImage.convertJoin( + theme.icon, + context + ".icon", + errors, + warnings, + information + ) + const dups = Utils.Dupiclates(json.layers.map((layer) => layer["id"])) if (dups.length > 0) { - errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`) + errors.push( + `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` + ) } if (json["mustHaveLanguage"] !== undefined) { - const checked = new ValidateLanguageCompleteness(...json["mustHaveLanguage"]) - .convert(theme, theme.id) + const checked = new ValidateLanguageCompleteness( + ...json["mustHaveLanguage"] + ).convert(theme, theme.id) errors.push(...checked.errors) } if (!json.hideFromOverview && theme.id !== "personal") { - // The first key in the the title-field must be english, otherwise the title in the loading page will be the different language const targetLanguage = theme.title.SupportedLanguages()[0] if (targetLanguage !== "en") { - warnings.push(`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`) + warnings.push( + `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key` + ) } // Official, public themes must have a full english translation - const checked = new ValidateLanguageCompleteness("en") - .convert(theme, theme.id) + const checked = new ValidateLanguageCompleteness("en").convert(theme, theme.id) errors.push(...checked.errors) - - } - } catch (e) { errors.push(e) } @@ -213,61 +283,86 @@ class ValidateTheme extends DesugaringStep { result: json, errors, warnings, - information - }; + information, + } } } export class ValidateThemeAndLayers extends Fuse { - constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { - super("Validates a theme and the contained layers", + constructor( + doesImageExist: DoesImageExist, + path: string, + isBuiltin: boolean, + sharedTagRenderings: Map + ) { + super( + "Validates a theme and the contained layers", new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings), new On("layers", new Each(new ValidateLayer(undefined, false, doesImageExist))) - ); + ) } } - class OverrideShadowingCheck extends DesugaringStep { - constructor() { - super("Checks that an 'overrideAll' does not override a single override", [], "OverrideShadowingCheck"); + super( + "Checks that an 'overrideAll' does not override a single override", + [], + "OverrideShadowingCheck" + ) } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { - - const overrideAll = json.overrideAll; + convert( + json: LayoutConfigJson, + context: string + ): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { + const overrideAll = json.overrideAll if (overrideAll === undefined) { - return {result: json} + return { result: json } } const errors = [] - const withOverride = json.layers.filter(l => l["override"] !== undefined) + const withOverride = json.layers.filter((l) => l["override"] !== undefined) for (const layer of withOverride) { for (const key in overrideAll) { - if(key.endsWith("+") || key.startsWith("+")){ + if (key.endsWith("+") || key.startsWith("+")) { // This key will _add_ to the list, not overwrite it - so no warning is needed continue } - if (layer["override"][key] !== undefined || layer["override"]["=" + key] !== undefined) { - const w = "The override of layer " + JSON.stringify(layer["builtin"]) + " has a shadowed property: " + key + " is overriden by overrideAll of the theme"; + if ( + layer["override"][key] !== undefined || + layer["override"]["=" + key] !== undefined + ) { + const w = + "The override of layer " + + JSON.stringify(layer["builtin"]) + + " has a shadowed property: " + + key + + " is overriden by overrideAll of the theme" errors.push(w) } } } - return {result: json, errors} + return { result: json, errors } } - } class MiscThemeChecks extends DesugaringStep { constructor() { - super("Miscelleanous checks on the theme", [], "MiscThemesChecks"); + super("Miscelleanous checks on the theme", [], "MiscThemesChecks") } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: LayoutConfigJson, + context: string + ): { + result: LayoutConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { const warnings = [] const errors = [] if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) { @@ -279,29 +374,27 @@ class MiscThemeChecks extends DesugaringStep { return { result: json, warnings, - errors - }; + errors, + } } } export class PrevalidateTheme extends Fuse { - constructor() { - super("Various consistency checks on the raw JSON", + super( + "Various consistency checks on the raw JSON", new MiscThemeChecks(), new OverrideShadowingCheck() - ); - + ) } - } export class DetectShadowedMappings extends DesugaringStep { - private readonly _calculatedTagNames: string[]; + private readonly _calculatedTagNames: string[] constructor(layerConfig?: LayerConfigJson) { - super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings"); - this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig); + super("Checks that the mappings don't shadow each other", [], "DetectShadowedMappings") + this._calculatedTagNames = DetectShadowedMappings.extractCalculatedTagNames(layerConfig) } /** @@ -309,14 +402,17 @@ export class DetectShadowedMappings extends DesugaringStep ["_abc"] * DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"] */ - private static extractCalculatedTagNames(layerConfig?: LayerConfigJson | { calculatedTags: string [] }) { - return layerConfig?.calculatedTags?.map(ct => { - if (ct.indexOf(':=') >= 0) { - return ct.split(':=')[0] - } - return ct.split("=")[0] - }) ?? [] - + private static extractCalculatedTagNames( + layerConfig?: LayerConfigJson | { calculatedTags: string[] } + ) { + return ( + layerConfig?.calculatedTags?.map((ct) => { + if (ct.indexOf(":=") >= 0) { + return ct.split(":=")[0] + } + return ct.split("=")[0] + }) ?? [] + ) } /** @@ -352,20 +448,28 @@ export class DetectShadowedMappings extends DesugaringStep 1 * r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true */ - convert(json: QuestionableTagRenderingConfigJson, context: string): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } { + convert( + json: QuestionableTagRenderingConfigJson, + context: string + ): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } { const errors = [] const warnings = [] if (json.mappings === undefined || json.mappings.length === 0) { - return {result: json} + return { result: json } } const defaultProperties = {} for (const calculatedTagName of this._calculatedTagNames) { - defaultProperties[calculatedTagName] = "some_calculated_tag_value_for_" + calculatedTagName + defaultProperties[calculatedTagName] = + "some_calculated_tag_value_for_" + calculatedTagName } const parsedConditions = json.mappings.map((m, i) => { const ctx = `${context}.mappings[${i}]` - const ifTags = TagUtils.Tag(m.if, ctx); - if (m.hideInAnswer !== undefined && m.hideInAnswer !== false && m.hideInAnswer !== true) { + const ifTags = TagUtils.Tag(m.if, ctx) + if ( + m.hideInAnswer !== undefined && + m.hideInAnswer !== false && + m.hideInAnswer !== true + ) { let conditionTags = TagUtils.Tag(m.hideInAnswer) // Merge the condition too! return new And([conditionTags, ifTags]) @@ -378,19 +482,29 @@ export class DetectShadowedMappings extends DesugaringStep { + keyValues.forEach(({ k, v }) => { properties[k] = v }) for (let j = 0; j < i; j++) { const doesMatch = parsedConditions[j].matchesProperties(properties) - if (doesMatch && json.mappings[j].hideInAnswer === true && json.mappings[i].hideInAnswer !== true) { - warnings.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`) + if ( + doesMatch && + json.mappings[j].hideInAnswer === true && + json.mappings[i].hideInAnswer !== true + ) { + warnings.push( + `At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.` + ) } else if (doesMatch) { // The current mapping is shadowed! errors.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j} and will thus never be shown: - The mapping ${parsedConditions[i].asHumanString(false, false, {})} is fully matched by a previous mapping (namely ${j}), which matches: + The mapping ${parsedConditions[i].asHumanString( + false, + false, + {} + )} is fully matched by a previous mapping (namely ${j}), which matches: ${parsedConditions[j].asHumanString(false, false, {})}. To fix this problem, you can try to: @@ -404,23 +518,26 @@ export class DetectShadowedMappings extends DesugaringStep { - private readonly _doesImageExist: DoesImageExist; + private readonly _doesImageExist: DoesImageExist constructor(doesImageExist: DoesImageExist) { - super("Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", [], "DetectMappingsWithImages"); - this._doesImageExist = doesImageExist; + super( + "Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", + [], + "DetectMappingsWithImages" + ) + this._doesImageExist = doesImageExist } /** @@ -443,31 +560,44 @@ export class DetectMappingsWithImages extends DesugaringStep 0 // => true * r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true */ - convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } { + convert( + json: TagRenderingConfigJson, + context: string + ): { + result: TagRenderingConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { const errors: string[] = [] const warnings: string[] = [] const information: string[] = [] if (json.mappings === undefined || json.mappings.length === 0) { - return {result: json} + return { result: json } } const ignoreToken = "ignore-image-in-then" for (let i = 0; i < json.mappings.length; i++) { - const mapping = json.mappings[i] const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0 const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? []) const ctx = `${context}.mappings[${i}]` if (images.length > 0) { if (!ignore) { - errors.push(`${ctx}: A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": \` instead. The images found are ${images.join(", ")}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`) + errors.push( + `${ctx}: A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": \` instead. The images found are ${images.join( + ", " + )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged` + ) } else { - information.push(`${ctx}: Ignored image ${images.join(", ")} in 'then'-clause of a mapping as this check has been disabled`) + information.push( + `${ctx}: Ignored image ${images.join( + ", " + )} in 'then'-clause of a mapping as this check has been disabled` + ) for (const image of images) { - this._doesImageExist.convertJoin(image, ctx, errors, warnings, information); - + this._doesImageExist.convertJoin(image, ctx, errors, warnings, information) } - } } else if (ignore) { warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`) @@ -478,17 +608,18 @@ export class DetectMappingsWithImages extends DesugaringStep { constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) { - super("Various validation on tagRenderingConfigs", + super( + "Various validation on tagRenderingConfigs", new DetectShadowedMappings(layerConfig), new DetectMappingsWithImages(doesImageExist) - ); + ) } } @@ -497,36 +628,45 @@ export class ValidateLayer extends DesugaringStep { * The paths where this layer is originally saved. Triggers some extra checks * @private */ - private readonly _path?: string; - private readonly _isBuiltin: boolean; - private readonly _doesImageExist: DoesImageExist; + private readonly _path?: string + private readonly _isBuiltin: boolean + private readonly _doesImageExist: DoesImageExist constructor(path: string, isBuiltin: boolean, doesImageExist: DoesImageExist) { - super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer"); - this._path = path; - this._isBuiltin = isBuiltin; + super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer") + this._path = path + this._isBuiltin = isBuiltin this._doesImageExist = doesImageExist } - convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } { + convert( + json: LayerConfigJson, + context: string + ): { result: LayerConfigJson; errors: string[]; warnings?: string[]; information?: string[] } { const errors = [] const warnings = [] const information = [] - context = "While validating a layer: "+context + context = "While validating a layer: " + context if (typeof json === "string") { errors.push(context + ": This layer hasn't been expanded: " + json) return { result: null, - errors + errors, } } - - if(json.tagRenderings !== undefined && json.tagRenderings.length > 0){ - if(json.title === undefined){ - errors.push(context + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.") + + if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) { + if (json.title === undefined) { + errors.push( + context + + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error." + ) } - if(json.title === null){ - information.push(context + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.") + if (json.title === null) { + information.push( + context + + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." + ) } } @@ -534,20 +674,28 @@ export class ValidateLayer extends DesugaringStep { errors.push(context + ": This layer hasn't been expanded: " + json) return { result: null, - errors + errors, } } - - if(json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints ){ - (json.presets?.length > 0 ? errors : warnings).push(`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`) + + if (json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints) { + ;(json.presets?.length > 0 ? errors : warnings).push( + `At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates` + ) } { // duplicate ids in tagrenderings check - const duplicates = Utils.Dedup(Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map(tr => tr["id"])))) - .filter(dupl => dupl !== "questions") + const duplicates = Utils.Dedup( + Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) + ).filter((dupl) => dupl !== "questions") if (duplicates.length > 0) { - errors.push("At " + context + ": some tagrenderings have a duplicate id: " + duplicates.join(", ")) + errors.push( + "At " + + context + + ": some tagrenderings have a duplicate id: " + + duplicates.join(", ") + ) } } @@ -556,18 +704,46 @@ export class ValidateLayer extends DesugaringStep { // Some checks for legacy elements if (json["overpassTags"] !== undefined) { - errors.push("Layer " + json.id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": }' instead of \"overpassTags\": (note: this isn't your fault, the custom theme generator still spits out the old format)") + errors.push( + "Layer " + + json.id + + 'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": }\' instead of "overpassTags": (note: this isn\'t your fault, the custom theme generator still spits out the old format)' + ) } - const forbiddenTopLevel = ["icon", "wayHandling", "roamingRenderings", "roamingRendering", "label", "width", "color", "colour", "iconOverlays"] + const forbiddenTopLevel = [ + "icon", + "wayHandling", + "roamingRenderings", + "roamingRendering", + "label", + "width", + "color", + "colour", + "iconOverlays", + ] for (const forbiddenKey of forbiddenTopLevel) { if (json[forbiddenKey] !== undefined) - errors.push(context + ": layer " + json.id + " still has a forbidden key " + forbiddenKey) + errors.push( + context + + ": layer " + + json.id + + " still has a forbidden key " + + forbiddenKey + ) } if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) { - errors.push(context + ": layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'") + errors.push( + context + + ": layer " + + json.id + + " contains an old 'hideUnderlayingFeaturesMinPercentage'" + ) } - - if(json.isShown !== undefined && (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)){ + + if ( + json.isShown !== undefined && + (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined) + ) { warnings.push(context + " has a tagRendering as `isShown`") } } @@ -575,83 +751,109 @@ export class ValidateLayer extends DesugaringStep { // Check location of layer file const expected: string = `assets/layers/${json.id}/${json.id}.json` if (this._path != undefined && this._path.indexOf(expected) < 0) { - errors.push("Layer is in an incorrect place. The path is " + this._path + ", but expected " + expected) + errors.push( + "Layer is in an incorrect place. The path is " + + this._path + + ", but expected " + + expected + ) } } if (this._isBuiltin) { // Check for correct IDs - if (json.tagRenderings?.some(tr => tr["id"] === "")) { + if (json.tagRenderings?.some((tr) => tr["id"] === "")) { const emptyIndexes: number[] = [] for (let i = 0; i < json.tagRenderings.length; i++) { - const tagRendering = json.tagRenderings[i]; + const tagRendering = json.tagRenderings[i] if (tagRendering["id"] === "") { emptyIndexes.push(i) } } - errors.push(`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(",")}])`) + errors.push( + `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join( + "," + )}])` + ) } - const duplicateIds = Utils.Dupiclates((json.tagRenderings ?? [])?.map(f => f["id"]).filter(id => id !== "questions")) + const duplicateIds = Utils.Dupiclates( + (json.tagRenderings ?? []) + ?.map((f) => f["id"]) + .filter((id) => id !== "questions") + ) if (duplicateIds.length > 0 && !Utils.runningFromConsole) { - errors.push(`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`) + errors.push( + `Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)` + ) } - if (json.description === undefined) { - if (Constants.priviliged_layers.indexOf(json.id) >= 0) { - errors.push( - context + ": A priviliged layer must have a description" - ) + errors.push(context + ": A priviliged layer must have a description") } else { - warnings.push( - context + ": A builtin layer should have a description" - ) + warnings.push(context + ": A builtin layer should have a description") } } } if (json.tagRenderings !== undefined) { - const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this._doesImageExist))).convert(json, context) + const r = new On( + "tagRenderings", + new Each(new ValidateTagRenderings(json, this._doesImageExist)) + ).convert(json, context) warnings.push(...(r.warnings ?? [])) errors.push(...(r.errors ?? [])) information.push(...(r.information ?? [])) } { - const hasCondition = json.mapRendering?.filter(mr => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined) - if(hasCondition?.length > 0){ - errors.push("At "+context+":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n"+JSON.stringify(hasCondition, null, " ")) + const hasCondition = json.mapRendering?.filter( + (mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined + ) + if (hasCondition?.length > 0) { + errors.push( + "At " + + context + + ":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" + + JSON.stringify(hasCondition, null, " ") + ) } } if (json.presets !== undefined) { - // Check that a preset will be picked up by the layer itself const baseTags = TagUtils.Tag(json.source.osmTags) for (let i = 0; i < json.presets.length; i++) { - const preset = json.presets[i]; - const tags: { k: string, v: string }[] = new And(preset.tags.map(t => TagUtils.Tag(t))).asChange({id: "node/-1"}) + const preset = json.presets[i] + const tags: { k: string; v: string }[] = new And( + preset.tags.map((t) => TagUtils.Tag(t)) + ).asChange({ id: "node/-1" }) const properties = {} for (const tag of tags) { properties[tag.k] = tag.v } const doMatch = baseTags.matchesProperties(properties) if (!doMatch) { - errors.push(context + ".presets[" + i + "]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + JSON.stringify(properties) + "\n The required tags are: " + baseTags.asHumanString(false, false, {})) + errors.push( + context + + ".presets[" + + i + + "]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + + JSON.stringify(properties) + + "\n The required tags are: " + + baseTags.asHumanString(false, false, {}) + ) } } } - } catch (e) { errors.push(e) } - return { result: json, errors, warnings, - information - }; + information, + } } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/DeleteConfig.ts b/Models/ThemeConfig/DeleteConfig.ts index 44c0543d0..36f651693 100644 --- a/Models/ThemeConfig/DeleteConfig.ts +++ b/Models/ThemeConfig/DeleteConfig.ts @@ -1,42 +1,43 @@ -import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import {DeleteConfigJson} from "./Json/DeleteConfigJson"; -import Translations from "../../UI/i18n/Translations"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; +import { Translation, TypedTranslation } from "../../UI/i18n/Translation" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import { DeleteConfigJson } from "./Json/DeleteConfigJson" +import Translations from "../../UI/i18n/Translations" +import { TagUtils } from "../../Logic/Tags/TagUtils" export default class DeleteConfig { - public static readonly defaultDeleteReasons : {changesetMessage: string, explanation: Translation} [] = [ + public static readonly defaultDeleteReasons: { + changesetMessage: string + explanation: Translation + }[] = [ { changesetMessage: "testing point", - explanation: Translations.t.delete.reasons.test + explanation: Translations.t.delete.reasons.test, }, { - changesetMessage:"disused", - explanation: Translations.t.delete.reasons.disused + changesetMessage: "disused", + explanation: Translations.t.delete.reasons.disused, }, { changesetMessage: "not found", - explanation: Translations.t.delete.reasons.notFound + explanation: Translations.t.delete.reasons.notFound, }, { changesetMessage: "duplicate", - explanation:Translations.t.delete.reasons.duplicate - } + explanation: Translations.t.delete.reasons.duplicate, + }, ] - public readonly extraDeleteReasons?: { - explanation: TypedTranslation, + explanation: TypedTranslation changesetMessage: string }[] - public readonly nonDeleteMappings?: { if: TagsFilter, then: TypedTranslation }[] + public readonly nonDeleteMappings?: { if: TagsFilter; then: TypedTranslation }[] public readonly softDeletionTags?: TagsFilter public readonly neededChangesets?: number constructor(json: DeleteConfigJson, context: string) { - this.extraDeleteReasons = (json.extraDeleteReasons ?? []).map((reason, i) => { const ctx = `${context}.extraDeleteReasons[${i}]` if ((reason.changesetMessage ?? "").length <= 5) { @@ -44,21 +45,23 @@ export default class DeleteConfig { } return { explanation: Translations.T(reason.explanation, ctx + ".explanation"), - changesetMessage: reason.changesetMessage + changesetMessage: reason.changesetMessage, } }) - this.nonDeleteMappings = (json.nonDeleteMappings??[]).map((nonDelete, i) => { + this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => { const ctx = `${context}.extraDeleteReasons[${i}]` return { if: TagUtils.Tag(nonDelete.if, ctx + ".if"), - then: Translations.T(nonDelete.then, ctx + ".then") + then: Translations.T(nonDelete.then, ctx + ".then"), } }) - this.softDeletionTags = undefined; + this.softDeletionTags = undefined if (json.softDeletionTags !== undefined) { - this.softDeletionTags = TagUtils.Tag(json.softDeletionTags, `${context}.softDeletionTags`) - + this.softDeletionTags = TagUtils.Tag( + json.softDeletionTags, + `${context}.softDeletionTags` + ) } if (json["hardDeletionTags"] !== undefined) { @@ -66,6 +69,4 @@ export default class DeleteConfig { } this.neededChangesets = json.neededChangesets } - - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/DependencyCalculator.ts b/Models/ThemeConfig/DependencyCalculator.ts index 895f26779..8a8106d4d 100644 --- a/Models/ThemeConfig/DependencyCalculator.ts +++ b/Models/ThemeConfig/DependencyCalculator.ts @@ -1,48 +1,50 @@ -import {SpecialVisualization} from "../../UI/SpecialVisualizations"; -import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; -import TagRenderingConfig from "./TagRenderingConfig"; -import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions"; -import LayerConfig from "./LayerConfig"; +import { SpecialVisualization } from "../../UI/SpecialVisualizations" +import { SubstitutedTranslation } from "../../UI/SubstitutedTranslation" +import TagRenderingConfig from "./TagRenderingConfig" +import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions" +import LayerConfig from "./LayerConfig" export default class DependencyCalculator { - public static GetTagRenderingDependencies(tr: TagRenderingConfig): string[] { - if (tr === undefined) { throw "Got undefined tag rendering in getTagRenderingDependencies" } const deps: string[] = [] // All translated snippets - const parts: string[] = [].concat(...(tr.EnumerateTranslations().map(tr => tr.AllValues()))) + const parts: string[] = [].concat(...tr.EnumerateTranslations().map((tr) => tr.AllValues())) for (const part of parts) { - const specialVizs: { func: SpecialVisualization, args: string[] }[] - = SubstitutedTranslation.ExtractSpecialComponents(part).map(o => o.special) - .filter(o => o?.func?.getLayerDependencies !== undefined) + const specialVizs: { func: SpecialVisualization; args: string[] }[] = + SubstitutedTranslation.ExtractSpecialComponents(part) + .map((o) => o.special) + .filter((o) => o?.func?.getLayerDependencies !== undefined) for (const specialViz of specialVizs) { deps.push(...specialViz.func.getLayerDependencies(specialViz.args)) } } - return deps; + return deps } /** * Returns a set of all other layer-ids that this layer needs to function. * E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned */ - public static getLayerDependencies(layer: LayerConfig): { neededLayer: string, reason: string, context?: string, neededBy: string }[] { - const deps: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = [] + public static getLayerDependencies( + layer: LayerConfig + ): { neededLayer: string; reason: string; context?: string; neededBy: string }[] { + const deps: { neededLayer: string; reason: string; context?: string; neededBy: string }[] = + [] for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) { - const preset = layer.presets[i]; - preset.preciseInput?.snapToLayers?.forEach(id => { + const preset = layer.presets[i] + preset.preciseInput?.snapToLayers?.forEach((id) => { deps.push({ neededLayer: id, reason: "a preset snaps to this layer", context: "presets[" + i + "]", - neededBy: layer.id - }); + neededBy: layer.id, + }) }) } @@ -52,7 +54,7 @@ export default class DependencyCalculator { neededLayer: dep, reason: "a tagrendering needs this layer", context: tr.id, - neededBy: layer.id + neededBy: layer.id, }) } } @@ -62,51 +64,53 @@ export default class DependencyCalculator { type: "Feature", geometry: { type: "Point", - coordinates: [0, 0] + coordinates: [0, 0], }, properties: { - id: "node/1" - } + id: "node/1", + }, } let currentKey = undefined let currentLine = undefined const params: ExtraFuncParams = { - getFeatureById: _ => undefined, + getFeatureById: (_) => undefined, getFeaturesWithin: (layerId, _) => { - - if (layerId === '*') { + if (layerId === "*") { // This is a wildcard return [] } // The important line: steal the dependencies! deps.push({ - neededLayer: layerId, reason: "a calculated tag loads features from this layer", - context: "calculatedTag[" + currentLine + "] which calculates the value for " + currentKey, - neededBy: layer.id + neededLayer: layerId, + reason: "a calculated tag loads features from this layer", + context: + "calculatedTag[" + + currentLine + + "] which calculates the value for " + + currentKey, + neededBy: layer.id, }) return [] }, - memberships: undefined + memberships: undefined, } // Init the extra patched functions... ExtraFunctions.FullPatchFeature(params, obj) // ... Run the calculated tag code, which will trigger the getFeaturesWithin above... for (let i = 0; i < layer.calculatedTags.length; i++) { - const [key, code] = layer.calculatedTags[i]; - currentLine = i; // Leak the state... - currentKey = key; + const [key, code] = layer.calculatedTags[i] + currentLine = i // Leak the state... + currentKey = key try { - - const func = new Function("feat", "return " + code + ";"); + const func = new Function("feat", "return " + code + ";") const result = func(obj) - obj.properties[key] = JSON.stringify(result); - } catch (e) { - } + obj.properties[key] = JSON.stringify(result) + } catch (e) {} } } return deps } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/ExtraLinkConfig.ts b/Models/ThemeConfig/ExtraLinkConfig.ts index 9c792ab11..333d74d2d 100644 --- a/Models/ThemeConfig/ExtraLinkConfig.ts +++ b/Models/ThemeConfig/ExtraLinkConfig.ts @@ -1,31 +1,43 @@ -import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson"; -import {Translation} from "../../UI/i18n/Translation"; -import Translations from "../../UI/i18n/Translations"; +import ExtraLinkConfigJson from "./Json/ExtraLinkConfigJson" +import { Translation } from "../../UI/i18n/Translation" +import Translations from "../../UI/i18n/Translations" export default class ExtraLinkConfig { public readonly icon?: string public readonly text?: Translation public readonly href: string public readonly newTab?: false | boolean - public readonly requirements?: Set<("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")> + public readonly requirements?: Set< + "iframe" | "no-iframe" | "welcome-message" | "no-welcome-message" + > constructor(configJson: ExtraLinkConfigJson, context) { this.icon = configJson.icon - this.text = Translations.T(configJson.text, "themes:"+context+".text") + this.text = Translations.T(configJson.text, "themes:" + context + ".text") this.href = configJson.href this.newTab = configJson.newTab this.requirements = new Set(configJson.requirements) for (let requirement of configJson.requirements) { - if (this.requirements.has(("no-" + requirement))) { - throw "Conflicting requirements found for " + context + ".extraLink: both '" + requirement + "' and 'no-" + requirement + "' found" + throw ( + "Conflicting requirements found for " + + context + + ".extraLink: both '" + + requirement + + "' and 'no-" + + requirement + + "' found" + ) } } if (this.icon === undefined && this.text === undefined) { - throw "At " + context + ".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed" + throw ( + "At " + + context + + ".extraLink: define at least an icon or a text to show. Both are undefined, this is not allowed" + ) } } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index 6b1d826a7..8c8efa85f 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -1,27 +1,27 @@ -import {Translation} from "../../UI/i18n/Translation"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import FilterConfigJson from "./Json/FilterConfigJson"; -import Translations from "../../UI/i18n/Translations"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import ValidatedTextField from "../../UI/Input/ValidatedTextField"; -import {TagConfigJson} from "./Json/TagConfigJson"; -import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"; -import {FilterState} from "../FilteredLayer"; -import {QueryParameters} from "../../Logic/Web/QueryParameters"; -import {Utils} from "../../Utils"; -import {RegexTag} from "../../Logic/Tags/RegexTag"; -import BaseUIElement from "../../UI/BaseUIElement"; -import {InputElement} from "../../UI/Input/InputElement"; +import { Translation } from "../../UI/i18n/Translation" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import FilterConfigJson from "./Json/FilterConfigJson" +import Translations from "../../UI/i18n/Translations" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import ValidatedTextField from "../../UI/Input/ValidatedTextField" +import { TagConfigJson } from "./Json/TagConfigJson" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" +import { FilterState } from "../FilteredLayer" +import { QueryParameters } from "../../Logic/Web/QueryParameters" +import { Utils } from "../../Utils" +import { RegexTag } from "../../Logic/Tags/RegexTag" +import BaseUIElement from "../../UI/BaseUIElement" +import { InputElement } from "../../UI/Input/InputElement" export default class FilterConfig { public readonly id: string public readonly options: { - question: Translation; - osmTags: TagsFilter | undefined; + question: Translation + osmTags: TagsFilter | undefined originalTagsSpec: TagConfigJson - fields: { name: string, type: string }[] - }[]; - public readonly defaultSelection? : number + fields: { name: string; type: string }[] + }[] + public readonly defaultSelection?: number constructor(json: FilterConfigJson, context: string) { if (json.options === undefined) { @@ -37,99 +37,114 @@ export default class FilterConfig { if (json.options.map === undefined) { throw `A filter was given where the options aren't a list at ${context}` } - this.id = json.id; - let defaultSelection : number = undefined + this.id = json.id + let defaultSelection: number = undefined this.options = json.options.map((option, i) => { - const ctx = `${context}.options.${i}`; - const question = Translations.T( - option.question, - `${ctx}.question` - ); - let osmTags: undefined | TagsFilter = undefined; + const ctx = `${context}.options.${i}` + const question = Translations.T(option.question, `${ctx}.question`) + let osmTags: undefined | TagsFilter = undefined if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) { - osmTags = TagUtils.Tag( - option.osmTags, - `${ctx}.osmTags` - ); + osmTags = TagUtils.Tag(option.osmTags, `${ctx}.osmTags`) FilterConfig.validateSearch(osmTags, ctx) } if (question === undefined) { throw `Invalid filter: no question given at ${ctx}` } - const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => { + const fields: { name: string; type: string }[] = (option.fields ?? []).map((f, i) => { const type = f.type ?? "string" if (!ValidatedTextField.ForType(type) === undefined) { - throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AvailableTypes()).join(",")}` + throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from( + ValidatedTextField.AvailableTypes() + ).join(",")}` } if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) { throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]` } return { name: f.name, - type + type, } }) for (const field of fields) { question.OnEveryLanguage((txt, language) => { - if(txt.indexOf("{"+field.name+"}")<0){ - throw "Error in filter with fields at "+context+".question."+language+": The question text should contain every field, but it doesn't contain `{"+field+"}`: "+txt + if (txt.indexOf("{" + field.name + "}") < 0) { + throw ( + "Error in filter with fields at " + + context + + ".question." + + language + + ": The question text should contain every field, but it doesn't contain `{" + + field + + "}`: " + + txt + ) } return txt }) } - if(option.default){ - if(defaultSelection === undefined){ - defaultSelection = i; - }else{ + if (option.default) { + if (defaultSelection === undefined) { + defaultSelection = i + } else { throw `Invalid filter: multiple filters are set as default, namely ${i} and ${defaultSelection} at ${context}` } } - - if(option.osmTags !== undefined){ + + if (option.osmTags !== undefined) { FilterConfig.validateSearch(TagUtils.Tag(option.osmTags), ctx) } - return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags}; - }); - - this.defaultSelection = defaultSelection + return { + question: question, + osmTags: osmTags, + fields, + originalTagsSpec: option.osmTags, + } + }) - if (this.options.some(o => o.fields.length > 0) && this.options.length > 1) { + this.defaultSelection = defaultSelection + + if (this.options.some((o) => o.fields.length > 0) && this.options.length > 1) { throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.` } if (this.options.length > 1 && this.options[0].osmTags !== undefined) { - throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters" + throw ( + "Error in " + + context + + "." + + this.id + + ": the first option of a multi-filter should always be the 'reset' option and not have any filters" + ) } - - } - private static validateSearch(osmTags: TagsFilter, ctx: string){ - osmTags.visit(t => { + private static validateSearch(osmTags: TagsFilter, ctx: string) { + osmTags.visit((t) => { if (!(t instanceof RegexTag)) { - return; + return } - if(typeof t.value == "string"){ - return; - } - - if(t.value.source == '^..*$' || t.value.source == '^[\\s\\S][\\s\\S]*$' /*Compiled regex with 'm'*/){ + if (typeof t.value == "string") { return } - if(!t.value.ignoreCase) { - throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive` + if ( + t.value.source == "^..*$" || + t.value.source == "^[\\s\\S][\\s\\S]*$" /*Compiled regex with 'm'*/ + ) { + return } + if (!t.value.ignoreCase) { + throw `At ${ctx}: The filter for key '${t.key}' uses a regex '${t.value}', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive` + } }) } - - public initState(): UIEventSource { + public initState(): UIEventSource { function reset(state: FilterState): string { if (state === undefined) { return "" @@ -138,47 +153,54 @@ export default class FilterConfig { } let defaultValue = "" - if(this.options.length > 1){ - defaultValue = ""+(this.defaultSelection ?? 0) - }else{ + if (this.options.length > 1) { + defaultValue = "" + (this.defaultSelection ?? 0) + } else { // Only a single option - if(this.defaultSelection === 0){ + if (this.defaultSelection === 0) { defaultValue = "true" } } - const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id) + const qp = QueryParameters.GetQueryParameter( + "filter-" + this.id, + defaultValue, + "State of filter " + this.id + ) if (this.options.length > 1) { // This is a multi-option filter; state should be a number which selects the correct entry - const possibleStates: FilterState [] = this.options.map((opt, i) => ({ + const possibleStates: FilterState[] = this.options.map((opt, i) => ({ currentFilter: opt.osmTags, - state: i + state: i, })) // We map the query parameter for this case - return qp.sync(str => { - const parsed = Number(str) - if (isNaN(parsed)) { - // Nope, not a correct number! - return undefined - } - return possibleStates[parsed] - }, [], reset) + return qp.sync( + (str) => { + const parsed = Number(str) + if (isNaN(parsed)) { + // Nope, not a correct number! + return undefined + } + return possibleStates[parsed] + }, + [], + reset + ) } - const option = this.options[0] if (option.fields.length > 0) { - return qp.sync(str => { - // There are variables in play! - // str should encode a json-hash - try { - const props = JSON.parse(str) + return qp.sync( + (str) => { + // There are variables in play! + // str should encode a json-hash + try { + const props = JSON.parse(str) - const origTags = option.originalTagsSpec - const rewrittenTags = Utils.WalkJson(origTags, - v => { + const origTags = option.originalTagsSpec + const rewrittenTags = Utils.WalkJson(origTags, (v) => { if (typeof v !== "string") { return v } @@ -186,34 +208,36 @@ export default class FilterConfig { v = (v).replace("{" + key + "}", props[key]) } return v + }) + const parsed = TagUtils.Tag(rewrittenTags) + return { + currentFilter: parsed, + state: str, } - ) - const parsed = TagUtils.Tag(rewrittenTags) - return { - currentFilter: parsed, - state: str + } catch (e) { + return undefined } - } catch (e) { - return undefined - } - - }, [], reset) + }, + [], + reset + ) } // The last case is pretty boring: it is checked or it isn't const filterState: FilterState = { currentFilter: option.osmTags, - state: "true" + state: "true", } return qp.sync( - str => { + (str) => { // Only a single option exists here if (str === "true") { return filterState } return undefined - }, [], + }, + [], reset ) } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/DeleteConfigJson.ts b/Models/ThemeConfig/Json/DeleteConfigJson.ts index 88ae47e4f..84e2ec889 100644 --- a/Models/ThemeConfig/Json/DeleteConfigJson.ts +++ b/Models/ThemeConfig/Json/DeleteConfigJson.ts @@ -1,7 +1,6 @@ -import {TagConfigJson} from "./TagConfigJson"; +import { TagConfigJson } from "./TagConfigJson" export interface DeleteConfigJson { - /*** * By default, three reasons to delete a point are shown: * @@ -21,7 +20,7 @@ export interface DeleteConfigJson { /** * The text that will be shown to the user - translatable */ - explanation: string | any, + explanation: string | any /** * The text that will be uploaded into the changeset or will be used in the fixme in case of a soft deletion * Should be a few words, in english @@ -41,12 +40,12 @@ export interface DeleteConfigJson { * The tags that will be given to the object. * This must remove tags so that the 'source/osmTags' won't match anymore */ - if: TagConfigJson, + if: TagConfigJson /** * The human explanation for the options */ - then: string | any, - }[], + then: string | any + }[] /** * In some cases, the contributor is not allowed to delete the current feature (e.g. because it isn't a point, the point is referenced by a relation or the user isn't experienced enough). @@ -67,11 +66,10 @@ export interface DeleteConfigJson { * } * ``` */ - softDeletionTags?: TagConfigJson, + softDeletionTags?: TagConfigJson /*** * By default, the contributor needs 20 previous changesets to delete points edited by others. * For some small features (e.g. bicycle racks) this is too much and this requirement can be lowered or dropped, which can be done here. */ neededChangesets?: number - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/ExtraLinkConfigJson.ts b/Models/ThemeConfig/Json/ExtraLinkConfigJson.ts index c48455aeb..71a9dd294 100644 --- a/Models/ThemeConfig/Json/ExtraLinkConfigJson.ts +++ b/Models/ThemeConfig/Json/ExtraLinkConfigJson.ts @@ -1,7 +1,7 @@ export default interface ExtraLinkConfigJson { - icon?: string, - text?: string | any, - href: string, - newTab?: false | boolean, + icon?: string + text?: string | any + href: string + newTab?: false | boolean requirements?: ("iframe" | "no-iframe" | "welcome-message" | "no-welcome-message")[] -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/FilterConfigJson.ts b/Models/ThemeConfig/Json/FilterConfigJson.ts index db0e258a8..dda710b14 100644 --- a/Models/ThemeConfig/Json/FilterConfigJson.ts +++ b/Models/ThemeConfig/Json/FilterConfigJson.ts @@ -1,10 +1,10 @@ -import {TagConfigJson} from "./TagConfigJson"; +import { TagConfigJson } from "./TagConfigJson" export default interface FilterConfigJson { /** * An id/name for this filter, used to set the URL parameters */ - id: string, + id: string /** * The options for a filter * If there are multiple options these will be a list of radio buttons @@ -12,15 +12,15 @@ export default interface FilterConfigJson { * Filtering is done based on the given osmTags that are compared to the objects in that layer. */ options: { - question: string | any; - osmTags?: TagConfigJson, - default?: boolean, + question: string | any + osmTags?: TagConfigJson + default?: boolean fields?: { /** * If name is `search`, use "_first_comment~.*{search}.*" as osmTags */ - name: string, + name: string type?: string | "string" }[] - }[]; -} \ No newline at end of file + }[] +} diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index e464ff305..4c6b601a2 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -1,13 +1,13 @@ -import {TagConfigJson} from "./TagConfigJson"; -import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; -import FilterConfigJson from "./FilterConfigJson"; -import {DeleteConfigJson} from "./DeleteConfigJson"; -import UnitConfigJson from "./UnitConfigJson"; -import MoveConfigJson from "./MoveConfigJson"; -import PointRenderingConfigJson from "./PointRenderingConfigJson"; -import LineRenderingConfigJson from "./LineRenderingConfigJson"; -import {QuestionableTagRenderingConfigJson} from "./QuestionableTagRenderingConfigJson"; -import RewritableConfigJson from "./RewritableConfigJson"; +import { TagConfigJson } from "./TagConfigJson" +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" +import FilterConfigJson from "./FilterConfigJson" +import { DeleteConfigJson } from "./DeleteConfigJson" +import UnitConfigJson from "./UnitConfigJson" +import MoveConfigJson from "./MoveConfigJson" +import PointRenderingConfigJson from "./PointRenderingConfigJson" +import LineRenderingConfigJson from "./LineRenderingConfigJson" +import { QuestionableTagRenderingConfigJson } from "./QuestionableTagRenderingConfigJson" +import RewritableConfigJson from "./RewritableConfigJson" /** * Configuration for a single layer @@ -17,7 +17,7 @@ export interface LayerConfigJson { * The id of this layer. * This should be a simple, lowercase, human readable string that is used to identify the layer. */ - id: string; + id: string /** * The name of this layer @@ -31,8 +31,7 @@ export interface LayerConfigJson { * A description for this layer. * Shown in the layer selections and in the personel theme */ - description?: string | any; - + description?: string | any /** * This determines where the data for the layer is fetched: from OSM or from an external geojson dataset. @@ -42,69 +41,69 @@ export interface LayerConfigJson { * Every source _must_ define which tags _must_ be present in order to be picked up. * */ - source: - ({ - /** - * Every source must set which tags have to be present in order to load the given layer. - */ - osmTags: TagConfigJson - /** - * The maximum amount of seconds that a tile is allowed to linger in the cache - */ - maxCacheAge?: number - }) & - ({ - /** - * If set, this custom overpass-script will be used instead of building one by using the OSM-tags. - * Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline. - * _This should be really rare_. - * - * For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible: - * ``` - * "source": { - * "overpassScript": - * "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );", - * "osmTags": "access=yes" - * } - * ``` - * - */ - overpassScript?: string - } | - { - /** - * The actual source of the data to load, if loaded via geojson. - * - * # A single geojson-file - * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} - * fetches a geojson from a third party source - * - * # A tiled geojson source - * source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} - * to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer - * - * Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max} - */ - geoJson: string, - /** - * To load a tiled geojson layer, set the zoomlevel of the tiles - */ - geoJsonZoomLevel?: number, - /** - * Indicates that the upstream geojson data is OSM-derived. - * Useful for e.g. merging or for scripts generating this cache - */ - isOsmCache?: boolean, - /** - * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this - */ - mercatorCrs?: boolean, - /** - * Some API's have an id-field, but give it a different name. - * Setting this key will rename this field into 'id' - */ - idKey?: string - }) + source: { + /** + * Every source must set which tags have to be present in order to load the given layer. + */ + osmTags: TagConfigJson + /** + * The maximum amount of seconds that a tile is allowed to linger in the cache + */ + maxCacheAge?: number + } & ( + | { + /** + * If set, this custom overpass-script will be used instead of building one by using the OSM-tags. + * Specifying OSM-tags is still obligatory and will still hide non-matching items and they will be used for the rest of the pipeline. + * _This should be really rare_. + * + * For example, when you want to fetch all grass-areas in parks and which are marked as publicly accessible: + * ``` + * "source": { + * "overpassScript": + * "way[\"leisure\"=\"park\"];node(w);is_in;area._[\"leisure\"=\"park\"];(way(area)[\"landuse\"=\"grass\"]; node(w); );", + * "osmTags": "access=yes" + * } + * ``` + * + */ + overpassScript?: string + } + | { + /** + * The actual source of the data to load, if loaded via geojson. + * + * # A single geojson-file + * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} + * fetches a geojson from a third party source + * + * # A tiled geojson source + * source: {geoJson: "https://my.source.net/some-tile-geojson-{layer}-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} + * to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted by the location; {layer} is substituted with the id of the loaded layer + * + * Some API's use a BBOX instead of a tile, this can be used by specifying {y_min}, {y_max}, {x_min} and {x_max} + */ + geoJson: string + /** + * To load a tiled geojson layer, set the zoomlevel of the tiles + */ + geoJsonZoomLevel?: number + /** + * Indicates that the upstream geojson data is OSM-derived. + * Useful for e.g. merging or for scripts generating this cache + */ + isOsmCache?: boolean + /** + * Some API's use a mercator-projection (EPSG:900913) instead of WGS84. Set the flag `mercatorCrs: true` in the source for this + */ + mercatorCrs?: boolean + /** + * Some API's have an id-field, but give it a different name. + * Setting this key will rename this field into 'id' + */ + idKey?: string + } + ) /** * @@ -126,13 +125,13 @@ export interface LayerConfigJson { * ] * */ - calculatedTags?: string[]; + calculatedTags?: string[] /** * If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers. * Works well together with 'passAllFeatures', to add decoration */ - doNotDownload?: boolean; + doNotDownload?: boolean /** * If set, only features matching this extra tag will be shown. @@ -143,7 +142,7 @@ export interface LayerConfigJson { * * The default value is 'yes' */ - isShown?: TagConfigJson; + isShown?: TagConfigJson /** * Advanced option - might be set by the theme compiler @@ -152,30 +151,28 @@ export interface LayerConfigJson { */ forceLoad?: false | boolean - /** * The minimum needed zoomlevel required before loading of the data start * Default: 0 */ - minzoom?: number; - + minzoom?: number /** * Indicates if this layer is shown by default; * can be used to hide a layer from start, or to load the layer but only to show it where appropriate (e.g. for snapping to it) */ - shownByDefault?: true | boolean; + shownByDefault?: true | boolean /** * The zoom level at which point the data is hidden again * Default: 100 (thus: always visible */ - minzoomVisible?: number; + minzoomVisible?: number /** * The title shown in a popup for elements of this layer. */ - title?: string | TagRenderingConfigJson; + title?: string | TagRenderingConfigJson /** * Small icons shown next to the title. @@ -185,12 +182,23 @@ export interface LayerConfigJson { * * Type: icon[] */ - titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"]; + titleIcons?: (string | TagRenderingConfigJson)[] | ["defaults"] /** * Visualisation of the items on the map */ - mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson | RewritableConfigJson)[] + mapRendering: + | null + | ( + | PointRenderingConfigJson + | LineRenderingConfigJson + | RewritableConfigJson< + | LineRenderingConfigJson + | PointRenderingConfigJson + | LineRenderingConfigJson[] + | PointRenderingConfigJson[] + > + )[] /** * If set, this layer will pass all the features it receives onto the next layer. @@ -220,18 +228,18 @@ export interface LayerConfigJson { * * Do _not_ indicate 'new': 'add a new shop here' is incorrect, as the shop might have existed forever, it could just be unmapped! */ - title: string | any, + title: string | any /** * The tags to add. It determines the icon too */ - tags: string[], + tags: string[] /** * The _first sentence_ of the description is shown on the button of the `add` menu. * The full description is shown in the confirmation dialog. * * (The first sentence is until the first '.'-character in the description) */ - description?: string | any, + description?: string | any /** * Example images, which show real-life pictures of what such a feature might look like @@ -246,24 +254,32 @@ export interface LayerConfigJson { * * If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category. */ - preciseInput?: true | { - /** - * The type of background picture - */ - preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string[], - /** - * If specified, these layers will be shown to and the new point will be snapped towards it - */ - snapToLayer?: string | string[], - /** - * If specified, a new point will only be snapped if it is within this range. - * Distance in meter - * - * Default: 10 - */ - maxSnapDistance?: number - } - }[], + preciseInput?: + | true + | { + /** + * The type of background picture + */ + preferredBackground: + | "osmbasedmap" + | "photo" + | "historicphoto" + | "map" + | string + | string[] + /** + * If specified, these layers will be shown to and the new point will be snapped towards it + */ + snapToLayer?: string | string[] + /** + * If specified, a new point will only be snapped if it is within this range. + * Distance in meter + * + * Default: 10 + */ + maxSnapDistance?: number + } + }[] /** * All the tag renderings. @@ -285,19 +301,24 @@ export interface LayerConfigJson { * This is mainly create questions for a 'left' and a 'right' side of the road. * These will be grouped and questions will be asked together */ - tagRenderings?: - (string - | { builtin: string | string[], override: Partial } - | { id: string, builtin: string[], override: Partial } - | QuestionableTagRenderingConfigJson - | (RewritableConfigJson<(string | { builtin: string, override: Partial } | QuestionableTagRenderingConfigJson)[]> & {id: string}) - ) [], - + tagRenderings?: ( + | string + | { builtin: string | string[]; override: Partial } + | { id: string; builtin: string[]; override: Partial } + | QuestionableTagRenderingConfigJson + | (RewritableConfigJson< + ( + | string + | { builtin: string; override: Partial } + | QuestionableTagRenderingConfigJson + )[] + > & { id: string }) + )[] /** * All the extra questions for filtering */ - filter?: (FilterConfigJson) [] | { sameAs: string }, + filter?: FilterConfigJson[] | { sameAs: string } /** * This block defines under what circumstances the delete dialog is shown for objects of this layer. @@ -435,4 +456,4 @@ export interface LayerConfigJson { * global: all layers with this ID will be synced accross all themes */ syncSelection?: "no" | "local" | "theme-only" | "global" -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index 08f4e895d..4a2a761b7 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -1,6 +1,6 @@ -import {LayerConfigJson} from "./LayerConfigJson"; -import TilesourceConfigJson from "./TilesourceConfigJson"; -import ExtraLinkConfigJson from "./ExtraLinkConfigJson"; +import { LayerConfigJson } from "./LayerConfigJson" +import TilesourceConfigJson from "./TilesourceConfigJson" +import ExtraLinkConfigJson from "./ExtraLinkConfigJson" /** * Defines the entire theme. @@ -15,7 +15,6 @@ import ExtraLinkConfigJson from "./ExtraLinkConfigJson"; * General remark: a type (string | any) indicates either a fixed or a translatable string. */ export interface LayoutConfigJson { - /** * The id of this layout. * @@ -25,16 +24,16 @@ export interface LayoutConfigJson { * On official themes, it'll become the name of the page, e.g. * 'cyclestreets' which become 'cyclestreets.html' */ - id: string; + id: string /** * Who helped to create this theme and should be attributed? */ - credits?: string; - + credits?: string + /** * Only used in 'generateLayerOverview': if present, every translation will be checked to make sure it is fully translated. - * + * * This must be a list of two-letter, lowercase codes which identifies the language, e.g. "en", "nl", ... */ mustHaveLanguage?: string[] @@ -42,49 +41,49 @@ export interface LayoutConfigJson { /** * The title, as shown in the welcome message and the more-screen. */ - title: string | any; + title: string | any /** * A short description, showed as social description and in the 'more theme'-buttons. * Note that if this one is not defined, the first sentence of 'description' is used */ - shortDescription?: string | any; + shortDescription?: string | any /** * The description, as shown in the welcome message and the more-screen */ - description: string | any; + description: string | any /** * A part of the description, shown under the login-button. */ - descriptionTail?: string | any; + descriptionTail?: string | any /** * The icon representing this theme. * Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ... * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64) - * + * * Type: icon */ - icon: string; + icon: string /** * Link to a 'social image' which is included as og:image-tag on official themes. * Useful to share the theme on social media. * See https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit for more information$ - * + * * Type: image */ - socialImage?: string; + socialImage?: string /** * Default location and zoom to start. * Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used */ - startZoom: number; - startLat: number; - startLon: number; + startZoom: number + startLat: number + startLon: number /** * When a query is run, the data within bounds of the visible map is loaded. @@ -93,7 +92,7 @@ export interface LayoutConfigJson { * * IF widenfactor is 1, this feature is disabled. A recommended value is between 1 and 3 */ - widenFactor?: number; + widenFactor?: number /** * At low zoom levels, overpass is used to query features. * At high zoom level, the OSM api is used to fetch one or more BBOX aligning with a slippy tile. @@ -139,12 +138,12 @@ export interface LayoutConfigJson { * * In the above scenario, `sometagrendering` will be added at the beginning of the tagrenderings of every layer */ - overrideAll?: Partial; + overrideAll?: Partial /** * The id of the default background. BY default: vanilla OSM */ - defaultBackgroundId?: string; + defaultBackgroundId?: string /** * Define some (overlay) slippy map tilesources @@ -174,7 +173,7 @@ export interface LayoutConfigJson { * ``` * "layer": { * "builtin": "nature_reserve", - * "override": {"source": + * "override": {"source": * {"osmTags": { * "+and":["operator=Natuurpunt"] * } @@ -192,122 +191,129 @@ export interface LayoutConfigJson { * } *``` */ - layers: (LayerConfigJson | string | - { builtin: string | string[], - override: any, - /** - * TagRenderings with any of these labels will be removed from the layer. - * Note that the 'id' and 'group' are considered labels too - */ - hideTagRenderingsWithLabels?: string[]})[], + layers: ( + | LayerConfigJson + | string + | { + builtin: string | string[] + override: any + /** + * TagRenderings with any of these labels will be removed from the layer. + * Note that the 'id' and 'group' are considered labels too + */ + hideTagRenderingsWithLabels?: string[] + } + )[] /** * If defined, data will be clustered. * Defaults to {maxZoom: 16, minNeeded: 500} */ - clustering?: { - /** - * All zoom levels above 'maxzoom' are not clustered anymore. - * Defaults to 18 - */ - maxZoom?: number, - /** - * The number of elements per tile needed to start clustering - * If clustering is defined, defaults to 250 - */ - minNeededElements?: number - } | false, + clustering?: + | { + /** + * All zoom levels above 'maxzoom' are not clustered anymore. + * Defaults to 18 + */ + maxZoom?: number + /** + * The number of elements per tile needed to start clustering + * If clustering is defined, defaults to 250 + */ + minNeededElements?: number + } + | false /** * The URL of a custom CSS stylesheet to modify the layout */ - customCss?: string; + customCss?: string /** * If set to true, this layout will not be shown in the overview with more themes */ - hideFromOverview?: boolean; + hideFromOverview?: boolean /** * If set to true, the basemap will not scroll outside of the area visible on initial zoom. * If set to [[lon, lat], [lon, lat]], the map will not scroll outside of those bounds. * Off by default, which will enable panning to the entire world */ - lockLocation?: [[number, number], [number, number]] | number[][]; + lockLocation?: [[number, number], [number, number]] | number[][] /** * Adds an additional button on the top-left of the application. * This can link to an arbitrary location. - * + * * Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced - * - * Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]}, - * + * + * Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]}, + * */ extraLink?: ExtraLinkConfigJson - + /** * If set to false, disables logging in. * The userbadge will be hidden, all login-buttons will be hidden and editing will be disabled */ - enableUserBadge?: true | boolean; + enableUserBadge?: true | boolean /** * If false, hides the tab 'share'-tab in the welcomeMessage */ - enableShareScreen?: true | boolean; + enableShareScreen?: true | boolean /** * Hides the tab with more themes in the welcomeMessage */ - enableMoreQuests?: true | boolean; + enableMoreQuests?: true | boolean /** * If false, the layer selection/filter view will be hidden * The corresponding URL-parameter is 'fs-filters' instead of 'fs-layers' */ - enableLayers?: true | boolean; + enableLayers?: true | boolean /** * If set to false, hides the search bar */ - enableSearch?: true | boolean; + enableSearch?: true | boolean /** * If set to false, the ability to add new points or nodes will be disabled. * Editing already existing features will still be possible */ - enableAddNewPoints?: true | boolean; + enableAddNewPoints?: true | boolean /** * If set to false, the 'geolocation'-button will be hidden. */ - enableGeolocation?: true | boolean; + enableGeolocation?: true | boolean /** * Enable switching the backgroundlayer. - * If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well + * If false, the quickswitch-buttons are removed (bottom left) and the dropdown in the layer selection is removed as well */ - enableBackgroundLayerSelection?: true | boolean; + enableBackgroundLayerSelection?: true | boolean /** * If set to true, will show _all_ unanswered questions in a popup instead of just the next one */ - enableShowAllQuestions?: false | boolean; + enableShowAllQuestions?: false | boolean /** * If set to true, download button for the data will be shown (offers downloading as geojson and csv) */ - enableDownload?: false | boolean; + enableDownload?: false | boolean /** * If set to true, exporting a pdf is enabled */ - enablePdfDownload?: false | boolean; + enablePdfDownload?: false | boolean /** * If true, notes will be loaded and parsed. If a note is an import (as created by the import_helper.html-tool from mapcomplete), * these notes will be shown if a relevant layer is present. - * + * * Default is true for official layers and false for unofficial (sideloaded) layers */ - enableNoteImports?: true | boolean; + enableNoteImports?: true | boolean /** * Set one or more overpass URLs to use for this theme.. */ - overpassUrl?: string | string[]; + overpassUrl?: string | string[] /** * Set a different timeout for overpass queries - in seconds. Default: 30s */ overpassTimeout?: number -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/LineRenderingConfigJson.ts b/Models/ThemeConfig/Json/LineRenderingConfigJson.ts index bb31da29e..6a359792b 100644 --- a/Models/ThemeConfig/Json/LineRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/LineRenderingConfigJson.ts @@ -1,4 +1,4 @@ -import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" /** * The LineRenderingConfig gives all details onto how to render a single line of a feature. @@ -9,16 +9,15 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; * - The feature is an area */ export default interface LineRenderingConfigJson { - /** * The color for way-elements and SVG-elements. * If the value starts with "--", the style of the body element will be queried for the corresponding variable instead */ - color?: string | TagRenderingConfigJson; + color?: string | TagRenderingConfigJson /** * The stroke-width for way-elements */ - width?: string | number | TagRenderingConfigJson; + width?: string | number | TagRenderingConfigJson /** * A dasharray, e.g. "5 6" diff --git a/Models/ThemeConfig/Json/MoveConfigJson.ts b/Models/ThemeConfig/Json/MoveConfigJson.ts index 3ae9bf70c..a3ecacdf1 100644 --- a/Models/ThemeConfig/Json/MoveConfigJson.ts +++ b/Models/ThemeConfig/Json/MoveConfigJson.ts @@ -9,4 +9,4 @@ export default interface MoveConfigJson { * Set to false to disable this reason */ enableRelocation?: true | boolean -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 9f372b880..77088b5df 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -1,5 +1,5 @@ -import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; -import {TagConfigJson} from "./TagConfigJson"; +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" +import { TagConfigJson } from "./TagConfigJson" /** * The PointRenderingConfig gives all details onto how to render a single point of a feature. @@ -10,7 +10,6 @@ import {TagConfigJson} from "./TagConfigJson"; * - To render something at the centroid of an area, or at the start, end or projected centroid of a way */ export default interface PointRenderingConfigJson { - /** * All the locations that this point should be rendered at. * Using `location: ["point", "centroid"] will always render centerpoint. @@ -30,7 +29,7 @@ export default interface PointRenderingConfigJson { * Type: icon */ - icon?: string | TagRenderingConfigJson; + icon?: string | TagRenderingConfigJson /** * A list of extra badges to show next to the icon as small badge @@ -38,26 +37,25 @@ export default interface PointRenderingConfigJson { * * Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle */ - iconBadges?: { - if: TagConfigJson, + iconBadges?: { + if: TagConfigJson /** * Badge to show * Type: icon */ - then: string | TagRenderingConfigJson + then: string | TagRenderingConfigJson }[] - /** * A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ... * Default is '40,40,center' */ - iconSize?: string | TagRenderingConfigJson; + iconSize?: string | TagRenderingConfigJson /** * The rotation of an icon, useful for e.g. directions. * Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)`` */ - rotation?: string | TagRenderingConfigJson; + rotation?: string | TagRenderingConfigJson /** * A HTML-fragment that is shown below the icon, for example: *
{name}
@@ -65,5 +63,5 @@ export default interface PointRenderingConfigJson { * If the icon is undefined, then the label is shown in the center of the feature. * Note that, if the wayhandling hides the icon then no label is shown as well. */ - label?: string | TagRenderingConfigJson; -} \ No newline at end of file + label?: string | TagRenderingConfigJson +} diff --git a/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index df83ea3f4..a435d0502 100644 --- a/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -1,33 +1,33 @@ -import {TagConfigJson} from "./TagConfigJson"; -import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; - +import { TagConfigJson } from "./TagConfigJson" +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" export interface MappingConfigJson { - /** * @inheritDoc */ - if: TagConfigJson, + if: TagConfigJson /** * Shown if the 'if is fulfilled * Type: rendered */ - then: string | any, + then: string | any /** * An extra icon supporting the choice * Type: icon */ - icon?: string | { - /** - * The path to the icon - * Type: icon - */ - path: string, - /** - * Size of the image - */ - class: "small" | "medium" | "large" | string - } + icon?: + | string + | { + /** + * The path to the icon + * Type: icon + */ + path: string + /** + * Size of the image + */ + class: "small" | "medium" | "large" | string + } /** * In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation). @@ -78,7 +78,7 @@ export interface MappingConfigJson { * {"if":"changing_table:location=female","then":"In the female restroom"}, * {"if":"changing_table:location=male","then":"In the male restroom"}, * {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"}, - * + * * ] * } * @@ -89,7 +89,7 @@ export interface MappingConfigJson { * hideInAnswer: "_country!=be" * } */ - hideInAnswer?: boolean | TagConfigJson, + hideInAnswer?: boolean | TagConfigJson /** * Only applicable if 'multiAnswer' is set. * This is for situations such as: @@ -103,7 +103,7 @@ export interface MappingConfigJson { /** * If chosen as answer, these tags will be applied as well onto the object. * Not compatible with multiAnswer. - * + * * This can be used e.g. to erase other keys which indicate the 'not' value: *```json * { @@ -112,13 +112,13 @@ export interface MappingConfigJson { * "addExtraTags": "not:crossing:marking=" * } * ``` - * + * */ addExtraTags?: string[] /** * If there are many options, the mappings-radiobuttons will be replaced by an element with a searchfunction - * + * * Searchterms (per language) allow to easily find an option if there are many options */ searchTerms?: Record @@ -128,7 +128,6 @@ export interface MappingConfigJson { * Use this sparingly */ priorityIf?: TagConfigJson - } /** @@ -136,19 +135,16 @@ export interface MappingConfigJson { * If the desired tags are missing and a question is defined, a question will be shown instead. */ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson { - /** * If it turns out that this tagRendering doesn't match _any_ value, then we show this question. * If undefined, the question is never asked and this tagrendering is read-only */ - question?: string | any, - + question?: string | any /** * Allow freeform text input from the user */ freeform?: { - /** * @inheritDoc */ @@ -158,7 +154,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values */ - type?: string, + type?: string /** * A (translated) text that is shown (as gray text) within the textfield */ @@ -168,12 +164,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * Extra parameters to initialize the input helper arguments. * For semantics, see the 'SpecialInputElements.md' */ - helperArgs?: (string | number | boolean | any)[]; + helperArgs?: (string | number | boolean | any)[] /** * If a value is added with the textfield, these extra tag is addded. * Useful to add a 'fixme=freeform textfield used - to be checked' **/ - addExtraTags?: string[]; + addExtraTags?: string[] /** * When set, influences the way a question is asked. @@ -188,15 +184,15 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * Normally undefined (aka do not enter anything) */ default?: string - }, + } /** * If true, use checkboxes instead of radio buttons when asking the question */ - multiAnswer?: boolean, + multiAnswer?: boolean /** * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes */ mappings?: MappingConfigJson[] -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/RewritableConfigJson.ts b/Models/ThemeConfig/Json/RewritableConfigJson.ts index 9bba9ddff..287c8a3ee 100644 --- a/Models/ThemeConfig/Json/RewritableConfigJson.ts +++ b/Models/ThemeConfig/Json/RewritableConfigJson.ts @@ -1,12 +1,12 @@ /** * Rewrites and multiplies the given renderings of type T. - * + * * This can be used for introducing many similar questions automatically, * which also makes translations easier. - * - * (Note that the key does _not_ need to be wrapped in {}. + * + * (Note that the key does _not_ need to be wrapped in {}. * However, we recommend to use them if the key is used in a translation, as missing keys will be picked up and warned for by the translation scripts) - * + * * For example: * * ``` @@ -25,7 +25,7 @@ * } * ``` * will result in _three_ copies (as the values to rewrite into have three values, namely: - * + * * [ * { * # The first pair: key --> X, a|b|c --> 0 @@ -37,15 +37,15 @@ * { * "Z": 2 * } - * + * * ] - * + * * @see ExpandRewrite */ export default interface RewritableConfigJson { rewrite: { - sourceString: string[], + sourceString: string[] into: (string | any)[][] - }, + } renderings: T -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/Json/TagConfigJson.ts b/Models/ThemeConfig/Json/TagConfigJson.ts index 265312c45..b3390edc9 100644 --- a/Models/ThemeConfig/Json/TagConfigJson.ts +++ b/Models/ThemeConfig/Json/TagConfigJson.ts @@ -4,7 +4,6 @@ */ export type TagConfigJson = string | AndTagConfigJson | OrTagConfigJson - /** * Chain many tags, to match, all of these should be true * See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation @@ -14,7 +13,7 @@ export type OrTagConfigJson = { } /** * Chain many tags, to match, a single of these should be true - * See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation + * See https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for documentation */ export type AndTagConfigJson = { and: TagConfigJson[] diff --git a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index 39ff7ea32..460fa1be2 100644 --- a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -1,18 +1,17 @@ -import {TagConfigJson} from "./TagConfigJson"; +import { TagConfigJson } from "./TagConfigJson" /** * A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. * For an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one */ export interface TagRenderingConfigJson { - /** * The id of the tagrendering, should be an unique string. * Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise. * * Use 'questions' to trigger the question box of this group (if a group is defined) */ - id?: string, + id?: string /** * If 'group' is defined on many tagRenderings, these are grouped together when shown. The questions are grouped together as well. @@ -37,15 +36,15 @@ export interface TagRenderingConfigJson { * Note that this is a HTML-interpreted value, so you can add links as e.g. '{website}' or include images such as `This is of type A
` * type: rendered */ - render?: string | any, - + render?: string | any + /** * Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`. * * This is useful to ask a follow-up question. * For example, within toilets, asking _where_ the diaper changing table is is only useful _if_ there is one. * This can be done by adding `"condition": "changing_table=yes"` - * + * * A full example would be: * ```json * { @@ -78,25 +77,23 @@ export interface TagRenderingConfigJson { * }, * ``` * */ - condition?: TagConfigJson; + condition?: TagConfigJson /** * Allow freeform text input from the user */ freeform?: { - /** * If this key is present, then 'render' is used to display the value. * If this is undefined, the rendering is _always_ shown */ - key: string, - }, + key: string + } /** * Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes */ mappings?: { - /** * If this condition is met, then the text under `then` will be shown. * If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM. @@ -105,29 +102,30 @@ export interface TagRenderingConfigJson { * * This can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'} */ - if: TagConfigJson, + if: TagConfigJson /** * If the condition `if` is met, the text `then` will be rendered. * If not known yet, the user will be presented with `then` as an option * Type: rendered */ - then: string | any, + then: string | any /** * An icon supporting this mapping; typically shown pretty small * Type: icon */ - icon?: string | { - /** - * The path to the icon - * Type: icon - */ - path: string, - /** - * A hint to mapcomplete on how to render this icon within the mapping. - * This is translated to 'mapping-icon-', so defining your own in combination with a custom CSS is possible (but discouraged) - */ - class: "small" | "medium" | "large" | string - } - + icon?: + | string + | { + /** + * The path to the icon + * Type: icon + */ + path: string + /** + * A hint to mapcomplete on how to render this icon within the mapping. + * This is translated to 'mapping-icon-', so defining your own in combination with a custom CSS is possible (but discouraged) + */ + class: "small" | "medium" | "large" | string + } }[] } diff --git a/Models/ThemeConfig/Json/TilesourceConfigJson.ts b/Models/ThemeConfig/Json/TilesourceConfigJson.ts index 8780ed1ac..ca80ed0c6 100644 --- a/Models/ThemeConfig/Json/TilesourceConfigJson.ts +++ b/Models/ThemeConfig/Json/TilesourceConfigJson.ts @@ -2,19 +2,18 @@ * Configuration for a tilesource config */ export default interface TilesourceConfigJson { - /** * Id of this overlay, used in the URL-parameters to set the state */ - id: string, + id: string /** * The path, where {x}, {y} and {z} will be substituted */ - source: string, + source: string /** * Wether or not this is an overlay. Default: true */ - isOverlay?: boolean, + isOverlay?: boolean /** * How this will be shown in the selection menu. @@ -32,10 +31,8 @@ export default interface TilesourceConfigJson { */ maxZoom?: number - /** * The default state, set to false to hide by default */ - defaultState: boolean; - -} \ No newline at end of file + defaultState: boolean +} diff --git a/Models/ThemeConfig/Json/UnitConfigJson.ts b/Models/ThemeConfig/Json/UnitConfigJson.ts index 269cd8eb7..a997ae164 100644 --- a/Models/ThemeConfig/Json/UnitConfigJson.ts +++ b/Models/ThemeConfig/Json/UnitConfigJson.ts @@ -1,33 +1,29 @@ export default interface UnitConfigJson { - /** * Every key from this list will be normalized. - * - * To render a united value properly, use + * + * To render a united value properly, use */ - appliesToKey: string[], + appliesToKey: string[] /** * If set, invalid values will be erased in the MC application (but not in OSM of course!) * Be careful with setting this */ - eraseInvalidValues?: boolean; + eraseInvalidValues?: boolean /** * The possible denominations */ applicableUnits: DenominationConfigJson[] - } export interface DenominationConfigJson { - - /** * If this evaluates to true and the value to interpret has _no_ unit given, assumes that this unit is meant. * Alternatively, a list of country codes can be given where this acts as the default interpretation - * + * * E.g., a denomination using "meter" would probably set this flag to "true"; * a denomination for "mp/h" will use the condition "_country=gb" to indicate that it is the default in the UK. - * + * * If none of the units indicate that they are the default, the first denomination will be used instead */ useIfNoUnitGiven?: boolean | string[] @@ -42,24 +38,22 @@ export interface DenominationConfigJson { * The canonical value for this denomination which will be added to the value in OSM. * e.g. "m" for meters * If the user inputs '42', the canonical value will be added and it'll become '42m'. - * + * * Important: often, _no_ canonical values are expected, e.g. in the case of 'maxspeed' where 'km/h' is the default. * In this case, an empty string should be used */ - canonicalDenomination: string, + canonicalDenomination: string - /** * The canonical denomination in the case that the unit is precisely '1'. * Used for display purposes */ - canonicalDenominationSingular?: string, - + canonicalDenominationSingular?: string /** * A list of alternative values which can occur in the OSM database - used for parsing. */ - alternativeDenomination?: string[], + alternativeDenomination?: string[] /** * The value for humans in the dropdown. This should not use abbreviations and should be translated, e.g. @@ -84,6 +78,4 @@ export interface DenominationConfigJson { * Note that if all values use 'prefix', the dropdown might move to before the text field */ prefix?: boolean - - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index c7d76d93e..389a7854f 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -1,86 +1,80 @@ -import {Translation} from "../../UI/i18n/Translation"; -import SourceConfig from "./SourceConfig"; -import TagRenderingConfig from "./TagRenderingConfig"; -import PresetConfig, {PreciseInput} from "./PresetConfig"; -import {LayerConfigJson} from "./Json/LayerConfigJson"; -import Translations from "../../UI/i18n/Translations"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import FilterConfig from "./FilterConfig"; -import {Unit} from "../Unit"; -import DeleteConfig from "./DeleteConfig"; -import MoveConfig from "./MoveConfig"; -import PointRenderingConfig from "./PointRenderingConfig"; -import WithContextLoader from "./WithContextLoader"; -import LineRenderingConfig from "./LineRenderingConfig"; -import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"; -import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"; -import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; -import BaseUIElement from "../../UI/BaseUIElement"; -import Combine from "../../UI/Base/Combine"; -import Title from "../../UI/Base/Title"; -import List from "../../UI/Base/List"; -import Link from "../../UI/Base/Link"; -import {Utils} from "../../Utils"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import Table from "../../UI/Base/Table"; -import FilterConfigJson from "./Json/FilterConfigJson"; -import {And} from "../../Logic/Tags/And"; -import {Overpass} from "../../Logic/Osm/Overpass"; -import Constants from "../Constants"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import Svg from "../../Svg"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmTags} from "../OsmFeature"; +import { Translation } from "../../UI/i18n/Translation" +import SourceConfig from "./SourceConfig" +import TagRenderingConfig from "./TagRenderingConfig" +import PresetConfig, { PreciseInput } from "./PresetConfig" +import { LayerConfigJson } from "./Json/LayerConfigJson" +import Translations from "../../UI/i18n/Translations" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import FilterConfig from "./FilterConfig" +import { Unit } from "../Unit" +import DeleteConfig from "./DeleteConfig" +import MoveConfig from "./MoveConfig" +import PointRenderingConfig from "./PointRenderingConfig" +import WithContextLoader from "./WithContextLoader" +import LineRenderingConfig from "./LineRenderingConfig" +import PointRenderingConfigJson from "./Json/PointRenderingConfigJson" +import LineRenderingConfigJson from "./Json/LineRenderingConfigJson" +import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" +import BaseUIElement from "../../UI/BaseUIElement" +import Combine from "../../UI/Base/Combine" +import Title from "../../UI/Base/Title" +import List from "../../UI/Base/List" +import Link from "../../UI/Base/Link" +import { Utils } from "../../Utils" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import Table from "../../UI/Base/Table" +import FilterConfigJson from "./Json/FilterConfigJson" +import { And } from "../../Logic/Tags/And" +import { Overpass } from "../../Logic/Osm/Overpass" +import Constants from "../Constants" +import { FixedUiElement } from "../../UI/Base/FixedUiElement" +import Svg from "../../Svg" +import { UIEventSource } from "../../Logic/UIEventSource" +import { OsmTags } from "../OsmFeature" export default class LayerConfig extends WithContextLoader { - - public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const; - public readonly id: string; - public readonly name: Translation; - public readonly description: Translation; - public readonly source: SourceConfig; - public readonly calculatedTags: [string, string, boolean][]; - public readonly doNotDownload: boolean; - public readonly passAllFeatures: boolean; - public readonly isShown: TagsFilter; - public minzoom: number; - public minzoomVisible: number; - public readonly maxzoom: number; - public readonly title?: TagRenderingConfig; - public readonly titleIcons: TagRenderingConfig[]; + public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const + public readonly id: string + public readonly name: Translation + public readonly description: Translation + public readonly source: SourceConfig + public readonly calculatedTags: [string, string, boolean][] + public readonly doNotDownload: boolean + public readonly passAllFeatures: boolean + public readonly isShown: TagsFilter + public minzoom: number + public minzoomVisible: number + public readonly maxzoom: number + public readonly title?: TagRenderingConfig + public readonly titleIcons: TagRenderingConfig[] public readonly mapRendering: PointRenderingConfig[] public readonly lineRendering: LineRenderingConfig[] - public readonly units: Unit[]; - public readonly deletion: DeleteConfig | null; + public readonly units: Unit[] + public readonly deletion: DeleteConfig | null public readonly allowMove: MoveConfig | null public readonly allowSplit: boolean - public readonly shownByDefault: boolean; + public readonly shownByDefault: boolean /** * In seconds */ public readonly maxAgeOfCache: number - public readonly presets: PresetConfig[]; - public readonly tagRenderings: TagRenderingConfig[]; - public readonly filters: FilterConfig[]; - public readonly filterIsSameAs: string; - public readonly forceLoad: boolean; - public readonly syncSelection: (typeof LayerConfig.syncSelectionAllowed)[number] // this is a trick to conver a constant array of strings into a type union of these values + public readonly presets: PresetConfig[] + public readonly tagRenderings: TagRenderingConfig[] + public readonly filters: FilterConfig[] + public readonly filterIsSameAs: string + public readonly forceLoad: boolean + public readonly syncSelection: typeof LayerConfig.syncSelectionAllowed[number] // this is a trick to conver a constant array of strings into a type union of these values - constructor( - json: LayerConfigJson, - context?: string, - official: boolean = true - ) { - context = context + "." + json.id; + constructor(json: LayerConfigJson, context?: string, official: boolean = true) { + context = context + "." + json.id const translationContext = "layers:" + json.id super(json, context) - this.id = json.id; + this.id = json.id if (typeof json === "string") { throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})` } - if (json.id === undefined) { throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})` } @@ -89,9 +83,14 @@ export default class LayerConfig extends WithContextLoader { throw "Layer " + this.id + " does not define a source section (" + context + ")" } - if (json.source.osmTags === undefined) { - throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")" + throw ( + "Layer " + + this.id + + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + + context + + ")" + ) } if (json.id.toLowerCase() !== json.id) { @@ -102,28 +101,38 @@ export default class LayerConfig extends WithContextLoader { } this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30 - if (json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0) { - throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'" + if ( + json.syncSelection !== undefined && + LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0 + ) { + throw ( + context + + " Invalid sync-selection: must be one of " + + LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + + " but got '" + + json.syncSelection + + "'" + ) } - this.syncSelection = json.syncSelection ?? "no"; - const osmTags = TagUtils.Tag( - json.source.osmTags, - context + "source.osmTags" - ); + this.syncSelection = json.syncSelection ?? "no" + const osmTags = TagUtils.Tag(json.source.osmTags, context + "source.osmTags") if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) { - throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + osmTags.asHumanString(false, false, {}); + throw ( + context + + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + + osmTags.asHumanString(false, false, {}) + ) } if (json.source["geoJsonSource"] !== undefined) { - throw context + "Use 'geoJson' instead of 'geoJsonSource'"; + throw context + "Use 'geoJson' instead of 'geoJsonSource'" } if (json.source["geojson"] !== undefined) { - throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; + throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)" } - this.source = new SourceConfig( { osmTags: osmTags, @@ -132,74 +141,80 @@ export default class LayerConfig extends WithContextLoader { overpassScript: json.source["overpassScript"], isOsmCache: json.source["isOsmCache"], mercatorCrs: json.source["mercatorCrs"], - idKey: json.source["idKey"] - + idKey: json.source["idKey"], }, Constants.priviliged_layers.indexOf(this.id) > 0, json.id - ); + ) - - this.allowSplit = json.allowSplit ?? false; - this.name = Translations.T(json.name, translationContext + ".name"); + this.allowSplit = json.allowSplit ?? false + this.name = Translations.T(json.name, translationContext + ".name") if (json.units !== undefined && !Array.isArray(json.units)) { - throw "At " + context + ".units: the 'units'-section should be a list; you probably have an object there" + throw ( + "At " + + context + + ".units: the 'units'-section should be a list; you probably have an object there" + ) } - this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) + this.units = (json.units ?? []).map((unitJson, i) => + Unit.fromJson(unitJson, `${context}.unit[${i}]`) + ) if (json.description !== undefined) { if (Object.keys(json.description).length === 0) { - json.description = undefined; + json.description = undefined } } - this.description = Translations.T( - json.description, - translationContext + ".description" - ); + this.description = Translations.T(json.description, translationContext + ".description") - - this.calculatedTags = undefined; + this.calculatedTags = undefined if (json.calculatedTags !== undefined) { if (!official) { console.warn( `Unofficial theme ${this.id} with custom javascript! This is a security risk` - ); + ) } - this.calculatedTags = []; + this.calculatedTags = [] for (const kv of json.calculatedTags) { - const index = kv.indexOf("="); - let key = kv.substring(0, index).trim(); + const index = kv.indexOf("=") + let key = kv.substring(0, index).trim() const r = "[a-z_][a-z0-9:]*" if (key.match(r) === null) { - throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r + throw ( + "At " + + context + + " invalid key for calculated tag: " + + key + + "; it should match " + + r + ) } - const isStrict = key.endsWith(':') + const isStrict = key.endsWith(":") if (isStrict) { key = key.substr(0, key.length - 1) } - const code = kv.substring(index + 1); + const code = kv.substring(index + 1) try { - new Function("feat", "return " + code + ";"); + new Function("feat", "return " + code + ";") } catch (e) { throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}` } - - this.calculatedTags.push([key, code, isStrict]); + this.calculatedTags.push([key, code, isStrict]) } } - this.doNotDownload = json.doNotDownload ?? false; - this.passAllFeatures = json.passAllFeatures ?? false; - this.minzoom = json.minzoom ?? 0; + this.doNotDownload = json.doNotDownload ?? false + this.passAllFeatures = json.passAllFeatures ?? false + this.minzoom = json.minzoom ?? 0 if (json["minZoom"] !== undefined) { throw "At " + context + ": minzoom is written all lowercase" } - this.minzoomVisible = json.minzoomVisible ?? this.minzoom; - this.shownByDefault = json.shownByDefault ?? true; - this.forceLoad = json.forceLoad ?? false; + this.minzoomVisible = json.minzoomVisible ?? this.minzoom + this.shownByDefault = json.shownByDefault ?? true + this.forceLoad = json.forceLoad ?? false if (json.presets !== undefined && json.presets?.map === undefined) { throw "Presets should be a list of items (at " + context + ")" } @@ -207,23 +222,29 @@ export default class LayerConfig extends WithContextLoader { let preciseInput: PreciseInput = { preferredBackground: ["photo"], snapToLayers: undefined, - maxSnapDistance: undefined - }; + maxSnapDistance: undefined, + } if (pr.preciseInput !== undefined) { if (pr.preciseInput === true) { pr.preciseInput = { - preferredBackground: undefined + preferredBackground: undefined, } } - let snapToLayers: string[]; + let snapToLayers: string[] if (typeof pr.preciseInput.snapToLayer === "string") { snapToLayers = [pr.preciseInput.snapToLayer] } else { snapToLayers = pr.preciseInput.snapToLayer } - let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[] + let preferredBackground: ( + | "map" + | "photo" + | "osmbasedmap" + | "historicphoto" + | string + )[] if (typeof pr.preciseInput.preferredBackground === "string") { preferredBackground = [pr.preciseInput.preferredBackground] } else { @@ -232,19 +253,22 @@ export default class LayerConfig extends WithContextLoader { preciseInput = { preferredBackground, snapToLayers, - maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10 + maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10, } } const config: PresetConfig = { title: Translations.T(pr.title, `${translationContext}.presets.${i}.title`), tags: pr.tags.map((t) => TagUtils.SimpleTag(t)), - description: Translations.T(pr.description, `${translationContext}.presets.${i}.description`), + description: Translations.T( + pr.description, + `${translationContext}.presets.${i}.description` + ), preciseInput: preciseInput, - exampleImages: pr.exampleImages + exampleImages: pr.exampleImages, } - return config; - }); + return config + }) if (json.mapRendering === undefined) { throw "MapRendering is undefined in " + context @@ -255,41 +279,89 @@ export default class LayerConfig extends WithContextLoader { this.lineRendering = [] } else { this.mapRendering = Utils.NoNull(json.mapRendering) - .filter(r => r["location"] !== undefined) - .map((r, i) => new PointRenderingConfig(r, context + ".mapRendering[" + i + "]")) + .filter((r) => r["location"] !== undefined) + .map( + (r, i) => + new PointRenderingConfig( + r, + context + ".mapRendering[" + i + "]" + ) + ) this.lineRendering = Utils.NoNull(json.mapRendering) - .filter(r => r["location"] === undefined) - .map((r, i) => new LineRenderingConfig(r, context + ".mapRendering[" + i + "]")) + .filter((r) => r["location"] === undefined) + .map( + (r, i) => + new LineRenderingConfig( + r, + context + ".mapRendering[" + i + "]" + ) + ) - const hasCenterRendering = this.mapRendering.some(r => r.location.has("centroid") || r.location.has("start") || r.location.has("end")) + const hasCenterRendering = this.mapRendering.some( + (r) => + r.location.has("centroid") || r.location.has("start") || r.location.has("end") + ) if (this.lineRendering.length === 0 && this.mapRendering.length === 0) { - throw("The layer " + this.id + " does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'") - } else if (!hasCenterRendering && this.lineRendering.length === 0 && !this.source.geojsonSource?.startsWith("https://api.openstreetmap.org/api/0.6/notes.json")) { - throw "The layer " + this.id + " might not render ways. This might result in dropped information (at " + context + ")" + throw ( + "The layer " + + this.id + + " does not have any maprenderings defined and will thus not show up on the map at all. If this is intentional, set maprenderings to 'null' instead of '[]'" + ) + } else if ( + !hasCenterRendering && + this.lineRendering.length === 0 && + !this.source.geojsonSource?.startsWith( + "https://api.openstreetmap.org/api/0.6/notes.json" + ) + ) { + throw ( + "The layer " + + this.id + + " might not render ways. This might result in dropped information (at " + + context + + ")" + ) } } - const missingIds = Utils.NoNull(json.tagRenderings)?.filter(tr => typeof tr !== "string" && tr["builtin"] === undefined && tr["id"] === undefined && tr["rewrite"] === undefined) ?? []; + const missingIds = + Utils.NoNull(json.tagRenderings)?.filter( + (tr) => + typeof tr !== "string" && + tr["builtin"] === undefined && + tr["id"] === undefined && + tr["rewrite"] === undefined + ) ?? [] if (missingIds?.length > 0 && official) { console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds) throw "Missing ids in tagrenderings" } - this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map((tr, i) => new TagRenderingConfig(tr, this.id + ".tagRenderings[" + i + "]")) + this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map( + (tr, i) => + new TagRenderingConfig( + tr, + this.id + ".tagRenderings[" + i + "]" + ) + ) - if (json.filter !== undefined && json.filter !== null && json.filter["sameAs"] !== undefined) { + if ( + json.filter !== undefined && + json.filter !== null && + json.filter["sameAs"] !== undefined + ) { this.filterIsSameAs = json.filter["sameAs"] this.filters = [] } else { this.filters = (json.filter ?? []).map((option, i) => { return new FilterConfig(option, `layers:${this.id}.filter.${i}`) - }); + }) } { - const duplicateIds = Utils.Dupiclates(this.filters.map(f => f.id)) + const duplicateIds = Utils.Dupiclates(this.filters.map((f) => f.id)) if (duplicateIds.length > 0) { throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)` } @@ -299,46 +371,44 @@ export default class LayerConfig extends WithContextLoader { throw "Error in " + context + ": use 'filter' instead of 'filters'" } + this.titleIcons = this.ParseTagRenderings(json.titleIcons, { + readOnlyMode: true, + }) - this.titleIcons = this.ParseTagRenderings((json.titleIcons), { - readOnlyMode: true - }); + this.title = this.tr("title", undefined) + this.isShown = TagUtils.TagD(json.isShown, context + ".isShown") - this.title = this.tr("title", undefined); - this.isShown = TagUtils.TagD(json.isShown, context+".isShown") - - this.deletion = null; + this.deletion = null if (json.deletion === true) { - json.deletion = {}; + json.deletion = {} } if (json.deletion !== undefined && json.deletion !== false) { - this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`); + this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`) } this.allowMove = null if (json.allowMove === false) { - this.allowMove = null; + this.allowMove = null } else if (json.allowMove === true) { this.allowMove = new MoveConfig({}, context + ".allowMove") } else if (json.allowMove !== undefined && json.allowMove !== false) { this.allowMove = new MoveConfig(json.allowMove, context + ".allowMove") } - if (json["showIf"] !== undefined) { throw ( "Invalid key on layerconfig " + this.id + ": showIf. Did you mean 'isShown' instead?" - ); + ) } } public defaultIcon(): BaseUIElement | undefined { if (this.mapRendering === undefined || this.mapRendering === null) { - return undefined; + return undefined } - const mapRendering = this.mapRendering.filter(r => r.location.has("point"))[0] + const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0] if (mapRendering === undefined) { return undefined } @@ -346,64 +416,104 @@ export default class LayerConfig extends WithContextLoader { } public GetBaseTags(): any { - return TagUtils.changeAsProperties(this.source.osmTags.asChange({id: "node/-1"})) + return TagUtils.changeAsProperties(this.source.osmTags.asChange({ id: "node/-1" })) } - public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map, dependencies: { - context?: string; - reason: string; - neededLayer: string; - }[] = [] - , addedByDefault = false, canBeIncluded = true): BaseUIElement { - const extraProps : (string | BaseUIElement)[] = [] + public GenerateDocumentation( + usedInThemes: string[], + layerIsNeededBy?: Map, + dependencies: { + context?: string + reason: string + neededLayer: string + }[] = [], + addedByDefault = false, + canBeIncluded = true + ): BaseUIElement { + const extraProps: (string | BaseUIElement)[] = [] extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher") if (canBeIncluded) { if (addedByDefault) { - extraProps.push("**This layer is included automatically in every theme. This layer might contain no points**") + extraProps.push( + "**This layer is included automatically in every theme. This layer might contain no points**" + ) } if (this.shownByDefault === false) { - extraProps.push('This layer is not visible by default and must be enabled in the filter by the user. ') + extraProps.push( + "This layer is not visible by default and must be enabled in the filter by the user. " + ) } if (this.title === undefined) { - extraProps.push("Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.") + extraProps.push( + "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable." + ) } if (this.name === undefined && this.shownByDefault === false) { - extraProps.push("This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-=true") + extraProps.push( + "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-=true" + ) } if (this.name === undefined) { - extraProps.push("Not visible in the layer selection by default. If you want to make this layer toggable, override `name`") + extraProps.push( + "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`" + ) } if (this.mapRendering.length === 0) { - extraProps.push("Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`") + extraProps.push( + "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`" + ) } if (this.source.geojsonSource !== undefined) { extraProps.push( new Combine([ - Utils.runningFromConsole ? "" : undefined, - "This layer is loaded from an external source, namely ", - new FixedUiElement( this.source.geojsonSource).SetClass("code")])); + Utils.runningFromConsole + ? "" + : undefined, + "This layer is loaded from an external source, namely ", + new FixedUiElement(this.source.geojsonSource).SetClass("code"), + ]) + ) } } else { - extraProps.push("This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.") + extraProps.push( + "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data." + ) } - let usingLayer: BaseUIElement[] = [] if (usedInThemes?.length > 0 && !addedByDefault) { - usingLayer = [new Title("Themes using this layer", 4), - new List((usedInThemes ?? []).map(id => new Link(id, "https://mapcomplete.osm.be/" + id))) + usingLayer = [ + new Title("Themes using this layer", 4), + new List( + (usedInThemes ?? []).map( + (id) => new Link(id, "https://mapcomplete.osm.be/" + id) + ) + ), ] } for (const dep of dependencies) { - extraProps.push(new Combine(["This layer will automatically load ", new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"), " into the layout as it depends on it: ", dep.reason, "(" + dep.context + ")"])) + extraProps.push( + new Combine([ + "This layer will automatically load ", + new Link(dep.neededLayer, "./" + dep.neededLayer + ".md"), + " into the layout as it depends on it: ", + dep.reason, + "(" + dep.context + ")", + ]) + ) } for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) { - extraProps.push(new Combine(["This layer is needed as dependency for layer", new Link(revDep, "#" + revDep)])) + extraProps.push( + new Combine([ + "This layer is needed as dependency for layer", + new Link(revDep, "#" + revDep), + ]) + ) } let neededTags: TagsFilter[] = [this.source.osmTags] @@ -411,86 +521,110 @@ export default class LayerConfig extends WithContextLoader { neededTags = this.source.osmTags["and"] } - let tableRows = Utils.NoNull(this.tagRenderings.map(tr => tr.FreeformValues()) - .map(values => { - if (values == undefined) { - return undefined - } - const embedded: (Link | string)[] = values.values?.map(v => Link.OsmWiki(values.key, v, true).SetClass("mr-2")) ?? ["_no preset options defined, or no values in them_"] - return [ - new Combine([ - new Link( - Utils.runningFromConsole ? "" : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"), - "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", true - ), Link.OsmWiki(values.key) - ]).SetClass("flex"), - values.type === undefined ? "Multiple choice" : new Link(values.type, "../SpecialInputElements.md#" + values.type), - new Combine(embedded).SetClass("flex") - ]; - })) + let tableRows = Utils.NoNull( + this.tagRenderings + .map((tr) => tr.FreeformValues()) + .map((values) => { + if (values == undefined) { + return undefined + } + const embedded: (Link | string)[] = values.values?.map((v) => + Link.OsmWiki(values.key, v, true).SetClass("mr-2") + ) ?? ["_no preset options defined, or no values in them_"] + return [ + new Combine([ + new Link( + Utils.runningFromConsole + ? "" + : Svg.statistics_svg().SetClass("w-4 h-4 mr-2"), + "https://taginfo.openstreetmap.org/keys/" + values.key + "#values", + true + ), + Link.OsmWiki(values.key), + ]).SetClass("flex"), + values.type === undefined + ? "Multiple choice" + : new Link(values.type, "../SpecialInputElements.md#" + values.type), + new Combine(embedded).SetClass("flex"), + ] + }) + ) - let quickOverview: BaseUIElement = undefined; + let quickOverview: BaseUIElement = undefined if (tableRows.length > 0) { quickOverview = new Combine([ new FixedUiElement("Warning: ").SetClass("bold"), "this quick overview is incomplete", - new Table(["attribute", "type", "values which are supported by this layer"], tableRows).SetClass("zebra-table") + new Table( + ["attribute", "type", "values which are supported by this layer"], + tableRows + ).SetClass("zebra-table"), ]).SetClass("flex-col flex") } - let iconImg: BaseUIElement = new FixedUiElement("") if (Utils.runningFromConsole) { const icon = this.mapRendering - .filter(mr => mr.location.has("point")) - .map(mr => mr.icon?.render?.txt) - .find(i => i !== undefined) + .filter((mr) => mr.location.has("point")) + .map((mr) => mr.icon?.render?.txt) + .find((i) => i !== undefined) // This is for the documentation in a markdown-file, so we have to use raw HTML if (icon !== undefined) { - iconImg = new FixedUiElement(` `) + iconImg = new FixedUiElement( + ` ` + ) } } else { iconImg = this.mapRendering - .filter(mr => mr.location.has("point")) - .map(mr => mr.GenerateLeafletStyle(new UIEventSource({id:"node/-1"}), false, {includeBadges: false}).html) - .find(i => i !== undefined) + .filter((mr) => mr.location.has("point")) + .map( + (mr) => + mr.GenerateLeafletStyle( + new UIEventSource({ id: "node/-1" }), + false, + { includeBadges: false } + ).html + ) + .find((i) => i !== undefined) } - let overpassLink: BaseUIElement = undefined; + let overpassLink: BaseUIElement = undefined if (Constants.priviliged_layers.indexOf(this.id) < 0) { try { - overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(new And(neededTags).optimize())) + overpassLink = new Link( + "Execute on overpass", + Overpass.AsOverpassTurboLink(new And(neededTags).optimize()) + ) } catch (e) { console.error("Could not generate overpasslink for " + this.id) } } return new Combine([ - new Combine([ - new Title(this.id, 1), - iconImg, - this.description, - "\n" - ]).SetClass("flex flex-col"), + new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass( + "flex flex-col" + ), new List(extraProps), ...usingLayer, new Title("Basic tags for this layer", 2), "Elements must have the all of following tags to be shown on this layer:", - new List(neededTags.map(t => t.asHumanString(true, false, {}))), + new List(neededTags.map((t) => t.asHumanString(true, false, {}))), overpassLink, new Title("Supported attributes", 2), quickOverview, - ...this.tagRenderings.map(tr => tr.GenerateDocumentation()) - ]).SetClass("flex-col").SetClass("link-underline") + ...this.tagRenderings.map((tr) => tr.GenerateDocumentation()), + ]) + .SetClass("flex-col") + .SetClass("link-underline") } public CustomCodeSnippets(): string[] { if (this.calculatedTags === undefined) { - return []; + return [] } - return this.calculatedTags.map((code) => code[1]); + return this.calculatedTags.map((code) => code[1]) } AllTagRenderings(): TagRenderingConfig[] { @@ -498,6 +632,6 @@ export default class LayerConfig extends WithContextLoader { } public isLeftRightSensitive(): boolean { - return this.lineRendering.some(lr => lr.leftRightSensitive) + return this.lineRendering.some((lr) => lr.leftRightSensitive) } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index 5af307ea0..801a69811 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -1,68 +1,72 @@ -import {Translation} from "../../UI/i18n/Translation"; -import {LayoutConfigJson} from "./Json/LayoutConfigJson"; -import LayerConfig from "./LayerConfig"; -import {LayerConfigJson} from "./Json/LayerConfigJson"; -import Constants from "../Constants"; -import TilesourceConfig from "./TilesourceConfig"; -import {ExtractImages} from "./Conversion/FixImages"; -import ExtraLinkConfig from "./ExtraLinkConfig"; +import { Translation } from "../../UI/i18n/Translation" +import { LayoutConfigJson } from "./Json/LayoutConfigJson" +import LayerConfig from "./LayerConfig" +import { LayerConfigJson } from "./Json/LayerConfigJson" +import Constants from "../Constants" +import TilesourceConfig from "./TilesourceConfig" +import { ExtractImages } from "./Conversion/FixImages" +import ExtraLinkConfig from "./ExtraLinkConfig" export default class LayoutConfig { public static readonly defaultSocialImage = "assets/SocialImage.png" - public readonly id: string; - public readonly credits?: string; - public readonly language: string[]; - public readonly title: Translation; - public readonly shortDescription: Translation; - public readonly description: Translation; - public readonly descriptionTail?: Translation; - public readonly icon: string; - public readonly socialImage?: string; - public readonly startZoom: number; - public readonly startLat: number; - public readonly startLon: number; - public readonly widenFactor: number; - public readonly defaultBackgroundId?: string; - public layers: LayerConfig[]; + public readonly id: string + public readonly credits?: string + public readonly language: string[] + public readonly title: Translation + public readonly shortDescription: Translation + public readonly description: Translation + public readonly descriptionTail?: Translation + public readonly icon: string + public readonly socialImage?: string + public readonly startZoom: number + public readonly startLat: number + public readonly startLon: number + public readonly widenFactor: number + public readonly defaultBackgroundId?: string + public layers: LayerConfig[] public tileLayerSources: TilesourceConfig[] public readonly clustering?: { - maxZoom: number, - minNeededElements: number, - }; - public readonly hideFromOverview: boolean; - public lockLocation: boolean | [[number, number], [number, number]]; - public readonly enableUserBadge: boolean; - public readonly enableShareScreen: boolean; - public readonly enableMoreQuests: boolean; - public readonly enableAddNewPoints: boolean; - public readonly enableLayers: boolean; - public readonly enableSearch: boolean; - public readonly enableGeolocation: boolean; - public readonly enableBackgroundLayerSelection: boolean; - public readonly enableShowAllQuestions: boolean; - public readonly enableExportButton: boolean; - public readonly enablePdfDownload: boolean; + maxZoom: number + minNeededElements: number + } + public readonly hideFromOverview: boolean + public lockLocation: boolean | [[number, number], [number, number]] + public readonly enableUserBadge: boolean + public readonly enableShareScreen: boolean + public readonly enableMoreQuests: boolean + public readonly enableAddNewPoints: boolean + public readonly enableLayers: boolean + public readonly enableSearch: boolean + public readonly enableGeolocation: boolean + public readonly enableBackgroundLayerSelection: boolean + public readonly enableShowAllQuestions: boolean + public readonly enableExportButton: boolean + public readonly enablePdfDownload: boolean - public readonly customCss?: string; + public readonly customCss?: string - public readonly overpassUrl: string[]; - public readonly overpassTimeout: number; + public readonly overpassUrl: string[] + public readonly overpassTimeout: number public readonly overpassMaxZoom: number public readonly osmApiTileSize: number - public readonly official: boolean; + public readonly official: boolean public readonly usedImages: string[] public readonly extraLink?: ExtraLinkConfig - public readonly definedAtUrl?: string; - public readonly definitionRaw?: string; + public readonly definedAtUrl?: string + public readonly definitionRaw?: string - constructor(json: LayoutConfigJson, official = true, options?: { - definedAtUrl?: string, - definitionRaw?: string - }) { - this.official = official; - this.id = json.id; + constructor( + json: LayoutConfigJson, + official = true, + options?: { + definedAtUrl?: string + definitionRaw?: string + } + ) { + this.official = official + this.id = json.id this.definedAtUrl = options?.definedAtUrl this.definitionRaw = options?.definitionRaw if (official) { @@ -74,73 +78,108 @@ export default class LayoutConfig { } } const context = this.id - this.credits = json.credits; - this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title)); - this.usedImages = Array.from(new ExtractImages(official, undefined).convertStrict(json, "while extracting the images of " + json.id + " " + context ?? "")).sort() + this.credits = json.credits + this.language = json.mustHaveLanguage ?? Array.from(Object.keys(json.title)) + this.usedImages = Array.from( + new ExtractImages(official, undefined).convertStrict( + json, + "while extracting the images of " + json.id + " " + context ?? "" + ) + ).sort() { if (typeof json.title === "string") { - throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${this.id}; the offending object is ${JSON.stringify(json.title)} which is a ${typeof json.title})` + throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${ + this.id + }; the offending object is ${JSON.stringify( + json.title + )} which is a ${typeof json.title})` } if (this.language.length == 0) { throw `No languages defined. Define at least one language. (${context}.languages)` } if (json.title === undefined) { - throw "Title not defined in " + this.id; + throw "Title not defined in " + this.id } if (json.description === undefined) { - throw "Description not defined in " + this.id; + throw "Description not defined in " + this.id } if (json.widenFactor <= 0) { throw "Widenfactor too small, shoud be > 0" } if (json.widenFactor > 20) { - throw "Widenfactor is very big, use a value between 1 and 5 (current value is " + json.widenFactor + ") at " + context + throw ( + "Widenfactor is very big, use a value between 1 and 5 (current value is " + + json.widenFactor + + ") at " + + context + ) } if (json["hideInOverview"]) { - throw "The json for " + this.id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?" + throw ( + "The json for " + + this.id + + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?" + ) } if (json.layers === undefined) { throw "Got undefined layers for " + json.id + " at " + context } } - this.title = new Translation(json.title, "themes:" + context + ".title"); - this.description = new Translation(json.description, "themes:" + context + ".description"); - this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, "themes:" + context + ".shortdescription"); - this.descriptionTail = json.descriptionTail === undefined ? undefined : new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail"); - this.icon = json.icon; - this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage; + this.title = new Translation(json.title, "themes:" + context + ".title") + this.description = new Translation(json.description, "themes:" + context + ".description") + this.shortDescription = + json.shortDescription === undefined + ? this.description.FirstSentence() + : new Translation(json.shortDescription, "themes:" + context + ".shortdescription") + this.descriptionTail = + json.descriptionTail === undefined + ? undefined + : new Translation(json.descriptionTail, "themes:" + context + ".descriptionTail") + this.icon = json.icon + this.socialImage = json.socialImage ?? LayoutConfig.defaultSocialImage if (this.socialImage === "") { if (official) { throw "Theme " + json.id + " has empty string as social image" } } - this.startZoom = json.startZoom; - this.startLat = json.startLat; - this.startLon = json.startLon; - this.widenFactor = json.widenFactor ?? 1.5; + this.startZoom = json.startZoom + this.startLat = json.startLat + this.startLon = json.startLon + this.widenFactor = json.widenFactor ?? 1.5 - this.defaultBackgroundId = json.defaultBackgroundId; - this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`)) + this.defaultBackgroundId = json.defaultBackgroundId + this.tileLayerSources = (json.tileLayerSources ?? []).map( + (config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`) + ) // At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert - this.layers = json.layers.map(lyrJson => new LayerConfig(lyrJson, json.id + ".layers." + lyrJson["id"], official)); - - this.extraLink = new ExtraLinkConfig(json.extraLink ?? { - icon: "./assets/svg/pop-out.svg", - href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", - newTab: true, - requirements: ["iframe", "no-welcome-message"] - }, context + ".extraLink") + this.layers = json.layers.map( + (lyrJson) => + new LayerConfig( + lyrJson, + json.id + ".layers." + lyrJson["id"], + official + ) + ) + this.extraLink = new ExtraLinkConfig( + json.extraLink ?? { + icon: "./assets/svg/pop-out.svg", + href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", + newTab: true, + requirements: ["iframe", "no-welcome-message"], + }, + context + ".extraLink" + ) this.clustering = { maxZoom: 16, minNeededElements: 250, - }; + } if (json.clustering === false) { this.clustering = { maxZoom: 0, minNeededElements: 100000, - }; + } } else if (json.clustering) { this.clustering = { maxZoom: json.clustering.maxZoom ?? 18, @@ -148,20 +187,20 @@ export default class LayoutConfig { } } - this.hideFromOverview = json.hideFromOverview ?? false; - this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined; - this.enableUserBadge = json.enableUserBadge ?? true; - this.enableShareScreen = json.enableShareScreen ?? true; - this.enableMoreQuests = json.enableMoreQuests ?? true; - this.enableLayers = json.enableLayers ?? true; - this.enableSearch = json.enableSearch ?? true; - this.enableGeolocation = json.enableGeolocation ?? true; - this.enableAddNewPoints = json.enableAddNewPoints ?? true; - this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; - this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; - this.enableExportButton = json.enableDownload ?? false; - this.enablePdfDownload = json.enablePdfDownload ?? false; - this.customCss = json.customCss; + this.hideFromOverview = json.hideFromOverview ?? false + this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined + this.enableUserBadge = json.enableUserBadge ?? true + this.enableShareScreen = json.enableShareScreen ?? true + this.enableMoreQuests = json.enableMoreQuests ?? true + this.enableLayers = json.enableLayers ?? true + this.enableSearch = json.enableSearch ?? true + this.enableGeolocation = json.enableGeolocation ?? true + this.enableAddNewPoints = json.enableAddNewPoints ?? true + this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true + this.enableShowAllQuestions = json.enableShowAllQuestions ?? false + this.enableExportButton = json.enableDownload ?? false + this.enablePdfDownload = json.enablePdfDownload ?? false + this.customCss = json.customCss this.overpassUrl = Constants.defaultOverpassUrls if (json.overpassUrl !== undefined) { if (typeof json.overpassUrl === "string") { @@ -173,27 +212,27 @@ export default class LayoutConfig { this.overpassTimeout = json.overpassTimeout ?? 30 this.overpassMaxZoom = json.overpassMaxZoom ?? 16 this.osmApiTileSize = json.osmApiTileSize ?? this.overpassMaxZoom + 1 - } public CustomCodeSnippets(): string[] { if (this.official) { - return []; + return [] } - const msg = "
This layout uses custom javascript, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:
" - const custom = []; + const msg = + "
This layout uses custom javascript, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:
" + const custom = [] for (const layer of this.layers) { - custom.push(...layer.CustomCodeSnippets().map(code => code + "
")) + custom.push(...layer.CustomCodeSnippets().map((code) => code + "
")) } if (custom.length === 0) { - return custom; + return custom } - custom.splice(0, 0, msg); - return custom; + custom.splice(0, 0, msg) + return custom } public isLeftRightSensitive() { - return this.layers.some(l => l.isLeftRightSensitive()) + return this.layers.some((l) => l.isLeftRightSensitive()) } public getMatchingLayer(tags: any): LayerConfig | undefined { @@ -207,5 +246,4 @@ export default class LayoutConfig { } return undefined } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/LineRenderingConfig.ts b/Models/ThemeConfig/LineRenderingConfig.ts index fbe1c6756..652be51e2 100644 --- a/Models/ThemeConfig/LineRenderingConfig.ts +++ b/Models/ThemeConfig/LineRenderingConfig.ts @@ -1,43 +1,49 @@ -import WithContextLoader from "./WithContextLoader"; -import TagRenderingConfig from "./TagRenderingConfig"; -import {Utils} from "../../Utils"; -import LineRenderingConfigJson from "./Json/LineRenderingConfigJson"; +import WithContextLoader from "./WithContextLoader" +import TagRenderingConfig from "./TagRenderingConfig" +import { Utils } from "../../Utils" +import LineRenderingConfigJson from "./Json/LineRenderingConfigJson" export default class LineRenderingConfig extends WithContextLoader { - - - public readonly color: TagRenderingConfig; - public readonly width: TagRenderingConfig; - public readonly dashArray: TagRenderingConfig; - public readonly lineCap: TagRenderingConfig; - public readonly offset: TagRenderingConfig; - public readonly fill: TagRenderingConfig; - public readonly fillColor: TagRenderingConfig; + public readonly color: TagRenderingConfig + public readonly width: TagRenderingConfig + public readonly dashArray: TagRenderingConfig + public readonly lineCap: TagRenderingConfig + public readonly offset: TagRenderingConfig + public readonly fill: TagRenderingConfig + public readonly fillColor: TagRenderingConfig public readonly leftRightSensitive: boolean constructor(json: LineRenderingConfigJson, context: string) { super(json, context) - this.color = this.tr("color", "#0000ff"); - this.width = this.tr("width", "7"); - this.dashArray = this.tr("dashArray", ""); - this.lineCap = this.tr("lineCap", "round"); - this.fill = this.tr("fill", undefined); - this.fillColor = this.tr("fillColor", undefined); + this.color = this.tr("color", "#0000ff") + this.width = this.tr("width", "7") + this.dashArray = this.tr("dashArray", "") + this.lineCap = this.tr("lineCap", "round") + this.fill = this.tr("fill", undefined) + this.fillColor = this.tr("fillColor", undefined) - this.leftRightSensitive = json.offset !== undefined && json.offset !== 0 && json.offset !== "0" + this.leftRightSensitive = + json.offset !== undefined && json.offset !== 0 && json.offset !== "0" - this.offset = this.tr("offset", "0"); + this.offset = this.tr("offset", "0") } - public GenerateLeafletStyle(tags: {}): - { fillColor?: string; color: string; lineCap: string; offset: number; weight: number; dashArray: string; fill?: boolean } { + public GenerateLeafletStyle(tags: {}): { + fillColor?: string + color: string + lineCap: string + offset: number + weight: number + dashArray: string + fill?: boolean + } { function rendernum(tr: TagRenderingConfig, deflt: number) { - const str = Number(render(tr, "" + deflt)); - const n = Number(str); + const str = Number(render(tr, "" + deflt)) + const n = Number(str) if (isNaN(n)) { - return deflt; + return deflt } - return n; + return n } function render(tr: TagRenderingConfig, deflt?: string) { @@ -47,19 +53,17 @@ export default class LineRenderingConfig extends WithContextLoader { if (tr === undefined) { return deflt } - const str = tr?.GetRenderValue(tags)?.txt ?? deflt; + const str = tr?.GetRenderValue(tags)?.txt ?? deflt if (str === "") { return deflt } - return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, ""); + return Utils.SubstituteKeys(str, tags)?.replace(/{.*}/g, "") } - const dashArray = render(this.dashArray); - let color = render(this.color, "#00f"); + const dashArray = render(this.dashArray) + let color = render(this.color, "#00f") if (color.startsWith("--")) { - color = getComputedStyle(document.body).getPropertyValue( - "--catch-detail-color" - ); + color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") } const style = { @@ -67,7 +71,7 @@ export default class LineRenderingConfig extends WithContextLoader { dashArray, weight: rendernum(this.width, 5), lineCap: render(this.lineCap), - offset: rendernum(this.offset, 0) + offset: rendernum(this.offset, 0), } const fillStr = render(this.fill, undefined) @@ -80,7 +84,5 @@ export default class LineRenderingConfig extends WithContextLoader { style["fillColor"] = fillColorStr } return style - } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/MoveConfig.ts b/Models/ThemeConfig/MoveConfig.ts index b2cb08260..f388acf82 100644 --- a/Models/ThemeConfig/MoveConfig.ts +++ b/Models/ThemeConfig/MoveConfig.ts @@ -1,7 +1,6 @@ -import MoveConfigJson from "./Json/MoveConfigJson"; +import MoveConfigJson from "./Json/MoveConfigJson" export default class MoveConfig { - public readonly enableImproveAccuracy: boolean public readonly enableRelocation: boolean @@ -12,6 +11,4 @@ export default class MoveConfig { throw "At least one default move reason should be allowed (at " + context + ")" } } - - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index c0c3cf962..81181c34b 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -1,29 +1,35 @@ -import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"; -import TagRenderingConfig from "./TagRenderingConfig"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {Utils} from "../../Utils"; -import Svg from "../../Svg"; -import WithContextLoader from "./WithContextLoader"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../../UI/BaseUIElement"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import Img from "../../UI/Base/Img"; -import Combine from "../../UI/Base/Combine"; -import {VariableUiElement} from "../../UI/Base/VariableUIElement"; - +import PointRenderingConfigJson from "./Json/PointRenderingConfigJson" +import TagRenderingConfig from "./TagRenderingConfig" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import SharedTagRenderings from "../../Customizations/SharedTagRenderings" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { Utils } from "../../Utils" +import Svg from "../../Svg" +import WithContextLoader from "./WithContextLoader" +import { UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../../UI/BaseUIElement" +import { FixedUiElement } from "../../UI/Base/FixedUiElement" +import Img from "../../UI/Base/Img" +import Combine from "../../UI/Base/Combine" +import { VariableUiElement } from "../../UI/Base/VariableUIElement" export default class PointRenderingConfig extends WithContextLoader { + private static readonly allowed_location_codes = new Set([ + "point", + "centroid", + "start", + "end", + "projected_centerpoint", + ]) + public readonly location: Set< + "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string + > - private static readonly allowed_location_codes = new Set(["point", "centroid", "start", "end","projected_centerpoint"]) - public readonly location: Set<"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string> - - public readonly icon: TagRenderingConfig; - public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]; - public readonly iconSize: TagRenderingConfig; - public readonly label: TagRenderingConfig; - public readonly rotation: TagRenderingConfig; + public readonly icon: TagRenderingConfig + public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[] + public readonly iconSize: TagRenderingConfig + public readonly label: TagRenderingConfig + public readonly rotation: TagRenderingConfig constructor(json: PointRenderingConfigJson, context: string) { super(json, context) @@ -34,10 +40,12 @@ export default class PointRenderingConfig extends WithContextLoader { this.location = new Set(json.location) - this.location.forEach(l => { + this.location.forEach((l) => { const allowed = PointRenderingConfig.allowed_location_codes if (!allowed.has(l)) { - throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from(allowed).join(", ")} (at ${context}.location)` + throw `A point rendering has an invalid location: '${l}' is not one of ${Array.from( + allowed + ).join(", ")} (at ${context}.location)` } }) @@ -46,36 +54,39 @@ export default class PointRenderingConfig extends WithContextLoader { } if (this.location.size == 0) { - throw "A pointRendering should have at least one 'location' to defined where it should be rendered. (At " + context + ".location)" + throw ( + "A pointRendering should have at least one 'location' to defined where it should be rendered. (At " + + context + + ".location)" + ) } - this.icon = this.tr("icon", undefined); + this.icon = this.tr("icon", undefined) this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => { - let tr: TagRenderingConfig; - if (typeof overlay.then === "string" && - SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) { - tr = SharedTagRenderings.SharedIcons.get(overlay.then); + let tr: TagRenderingConfig + if ( + typeof overlay.then === "string" && + SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined + ) { + tr = SharedTagRenderings.SharedIcons.get(overlay.then) } else { - tr = new TagRenderingConfig( - overlay.then, - `iconBadges.${i}` - ); + tr = new TagRenderingConfig(overlay.then, `iconBadges.${i}`) } return { if: TagUtils.Tag(overlay.if), - then: tr - }; - }); + then: tr, + } + }) - const iconPath = this.icon?.GetRenderValue({id: "node/-1"})?.txt; + const iconPath = this.icon?.GetRenderValue({ id: "node/-1" })?.txt if (iconPath !== undefined && iconPath.startsWith(Utils.assets_path)) { - const iconKey = iconPath.substr(Utils.assets_path.length); + const iconKey = iconPath.substr(Utils.assets_path.length) if (Svg.All[iconKey] === undefined) { - throw context + ": builtin SVG asset not found: " + iconPath; + throw context + ": builtin SVG asset not found: " + iconPath } } - this.iconSize = this.tr("iconSize", "40,40,center"); - this.label = this.tr("label", undefined); - this.rotation = this.tr("rotation", "0"); + this.iconSize = this.tr("iconSize", "40,40,center") + this.label = this.tr("label", undefined) + this.rotation = this.tr("rotation", "0") } /** @@ -84,40 +95,47 @@ export default class PointRenderingConfig extends WithContextLoader { */ private static FromHtmlSpec(htmlSpec: string, style: string, isBadge = false): BaseUIElement { if (htmlSpec === undefined) { - return undefined; + return undefined } - const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/); + const match = htmlSpec.match(/([a-zA-Z0-9_]*):([^;]*)/) if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { - const svg = (Svg.All[match[1] + ".svg"] as string) + const svg = Svg.All[match[1] + ".svg"] as string const targetColor = match[2] - const img = new Img(svg - .replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor), true) - .SetStyle(style) + const img = new Img( + svg.replace(/(rgb\(0%,0%,0%\)|#000000|#000)/g, targetColor), + true + ).SetStyle(style) if (isBadge) { img.SetClass("badge") } return img } else if (Svg.All[htmlSpec + ".svg"] !== undefined) { - const svg = (Svg.All[htmlSpec + ".svg"] as string) - const img = new Img(svg, true) - .SetStyle(style) + const svg = Svg.All[htmlSpec + ".svg"] as string + const img = new Img(svg, true).SetStyle(style) if (isBadge) { img.SetClass("badge") } return img } else { - return new FixedUiElement(``); + return new FixedUiElement(``) } } - private static FromHtmlMulti(multiSpec: string, rotation: string, isBadge: boolean, defaultElement: BaseUIElement = undefined) { + private static FromHtmlMulti( + multiSpec: string, + rotation: string, + isBadge: boolean, + defaultElement: BaseUIElement = undefined + ) { if (multiSpec === undefined) { return defaultElement } - const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; + const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0` const htmlDefs = multiSpec.trim()?.split(";") ?? [] - const elements = Utils.NoEmpty(htmlDefs).map(def => PointRenderingConfig.FromHtmlSpec(def, style, isBadge)) + const elements = Utils.NoEmpty(htmlDefs).map((def) => + PointRenderingConfig.FromHtmlSpec(def, style, isBadge) + ) if (elements.length === 0) { return defaultElement } else { @@ -126,93 +144,95 @@ export default class PointRenderingConfig extends WithContextLoader { } public GetBaseIcon(tags?: any): BaseUIElement { - tags = tags ?? {id: "node/-1"} + tags = tags ?? { id: "node/-1" } let defaultPin: BaseUIElement = undefined if (this.label === undefined) { defaultPin = Svg.teardrop_with_hole_green_svg() } - if(this.icon === undefined){ - return defaultPin; + if (this.icon === undefined) { + return defaultPin } - const rotation = Utils.SubstituteKeys(this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", tags) + const rotation = Utils.SubstituteKeys( + this.rotation?.GetRenderValue(tags)?.txt ?? "0deg", + tags + ) const htmlDefs = Utils.SubstituteKeys(this.icon?.GetRenderValue(tags)?.txt, tags) - if(htmlDefs === undefined){ + if (htmlDefs === undefined) { // This layer doesn't want to show an icon right now return undefined } - return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) + return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) } public GetSimpleIcon(tags: UIEventSource): BaseUIElement { - const self = this; + const self = this if (this.icon === undefined) { - return undefined; + return undefined } - return new VariableUiElement(tags.map(tags => self.GetBaseIcon(tags))).SetClass("w-full h-full block") + return new VariableUiElement(tags.map((tags) => self.GetBaseIcon(tags))).SetClass( + "w-full h-full block" + ) } public GenerateLeafletStyle( tags: UIEventSource, clickable: boolean, options?: { - noSize?: false | boolean, + noSize?: false | boolean includeBadges?: true | boolean } - ): - { - html: BaseUIElement; - iconSize: [number, number]; - iconAnchor: [number, number]; - popupAnchor: [number, number]; - iconUrl: string; - className: string; - } { + ): { + html: BaseUIElement + iconSize: [number, number] + iconAnchor: [number, number] + popupAnchor: [number, number] + iconUrl: string + className: string + } { function num(str, deflt = 40) { - const n = Number(str); + const n = Number(str) if (isNaN(n)) { - return deflt; + return deflt } - return n; + return n } function render(tr: TagRenderingConfig, deflt?: string) { if (tags === undefined) { return deflt } - const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt; - return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); + const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt + return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "") } - const iconSize = render(this.iconSize, "40,40,center").split(","); + const iconSize = render(this.iconSize, "40,40,center").split(",") - const iconW = num(iconSize[0]); - let iconH = num(iconSize[1]); - const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"; + const iconW = num(iconSize[0]) + let iconH = num(iconSize[1]) + const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" - let anchorW = iconW / 2; - let anchorH = iconH / 2; + let anchorW = iconW / 2 + let anchorH = iconH / 2 if (mode === "left") { - anchorW = 0; + anchorW = 0 } if (mode === "right") { - anchorW = iconW; + anchorW = iconW } if (mode === "top") { - anchorH = 0; + anchorH = 0 } if (mode === "bottom") { - anchorH = iconH; + anchorH = iconH } - const icon = this.GetSimpleIcon(tags) - let badges = undefined; + let badges = undefined if (options?.includeBadges ?? true) { badges = this.GetBadges(tags) } - const iconAndBadges = new Combine([icon, badges]) - .SetClass("block relative") + const iconAndBadges = new Combine([icon, badges]).SetClass("block relative") if (!options?.noSize) { iconAndBadges.SetStyle(`width: ${iconW}px; height: ${iconH}px`) @@ -221,7 +241,7 @@ export default class PointRenderingConfig extends WithContextLoader { } let label = this.GetLabel(tags) - let htmlEl: BaseUIElement; + let htmlEl: BaseUIElement if (icon === undefined && label === undefined) { htmlEl = undefined } else if (icon === undefined) { @@ -238,10 +258,8 @@ export default class PointRenderingConfig extends WithContextLoader { iconAnchor: [anchorW, anchorH], popupAnchor: [0, 3 - anchorH], iconUrl: undefined, - className: clickable - ? "leaflet-div-icon" - : "leaflet-div-icon unclickable", - }; + className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable", + } } private GetBadges(tags: UIEventSource): BaseUIElement { @@ -249,41 +267,46 @@ export default class PointRenderingConfig extends WithContextLoader { return undefined } return new VariableUiElement( - tags.map(tags => { - - const badgeElements = this.iconBadges.map(badge => { - + tags.map((tags) => { + const badgeElements = this.iconBadges.map((badge) => { if (!badge.if.matchesProperties(tags)) { // Doesn't match... return undefined } - const htmlDefs = Utils.SubstituteKeys(badge.then.GetRenderValue(tags)?.txt, tags) - const badgeElement = PointRenderingConfig.FromHtmlMulti(htmlDefs, "0", true)?.SetClass("block relative") + const htmlDefs = Utils.SubstituteKeys( + badge.then.GetRenderValue(tags)?.txt, + tags + ) + const badgeElement = PointRenderingConfig.FromHtmlMulti( + htmlDefs, + "0", + true + )?.SetClass("block relative") if (badgeElement === undefined) { - return undefined; + return undefined } return new Combine([badgeElement]).SetStyle("width: 1.5rem").SetClass("block") - }) return new Combine(badgeElements).SetClass("inline-flex h-full") - })).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") + }) + ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") } private GetLabel(tags: UIEventSource): BaseUIElement { if (this.label === undefined) { - return undefined; + return undefined } - const self = this; - return new VariableUiElement(tags.map(tags => { - const label = self.label - ?.GetRenderValue(tags) - ?.Subs(tags) - ?.SetClass("block text-center") - return new Combine([label]).SetClass("flex flex-col items-center mt-1") - })) - + const self = this + return new VariableUiElement( + tags.map((tags) => { + const label = self.label + ?.GetRenderValue(tags) + ?.Subs(tags) + ?.SetClass("block text-center") + return new Combine([label]).SetClass("flex flex-col items-center mt-1") + }) + ) } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/PresetConfig.ts b/Models/ThemeConfig/PresetConfig.ts index a9e68050c..fffe852ec 100644 --- a/Models/ThemeConfig/PresetConfig.ts +++ b/Models/ThemeConfig/PresetConfig.ts @@ -1,19 +1,19 @@ -import {Translation} from "../../UI/i18n/Translation"; -import {Tag} from "../../Logic/Tags/Tag"; +import { Translation } from "../../UI/i18n/Translation" +import { Tag } from "../../Logic/Tags/Tag" export interface PreciseInput { - preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[], - snapToLayers?: string[], + preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[] + snapToLayers?: string[] maxSnapDistance?: number } export default interface PresetConfig { - title: Translation, - tags: Tag[], - description?: Translation, - exampleImages?: string[], + title: Translation + tags: Tag[] + description?: Translation + exampleImages?: string[] /** * If precise input is set, then an extra map is shown in which the user can drag the map to the precise location */ preciseInput?: PreciseInput -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/SourceConfig.ts b/Models/ThemeConfig/SourceConfig.ts index 928284436..d5e7e82b8 100644 --- a/Models/ThemeConfig/SourceConfig.ts +++ b/Models/ThemeConfig/SourceConfig.ts @@ -1,63 +1,79 @@ -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import {RegexTag} from "../../Logic/Tags/RegexTag"; +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import { RegexTag } from "../../Logic/Tags/RegexTag" export default class SourceConfig { + public readonly osmTags?: TagsFilter + public readonly overpassScript?: string + public geojsonSource?: string + public geojsonZoomLevel?: number + public isOsmCacheLayer: boolean + public readonly mercatorCrs: boolean + public readonly idKey: string - public readonly osmTags?: TagsFilter; - public readonly overpassScript?: string; - public geojsonSource?: string; - public geojsonZoomLevel?: number; - public isOsmCacheLayer: boolean; - public readonly mercatorCrs: boolean; - public readonly idKey : string - - constructor(params: { - mercatorCrs?: boolean; - osmTags?: TagsFilter, - overpassScript?: string, - geojsonSource?: string, - isOsmCache?: boolean, - geojsonSourceLevel?: number, - idKey?: string - }, isSpecialLayer: boolean, context?: string) { - - let defined = 0; + constructor( + params: { + mercatorCrs?: boolean + osmTags?: TagsFilter + overpassScript?: string + geojsonSource?: string + isOsmCache?: boolean + geojsonSourceLevel?: number + idKey?: string + }, + isSpecialLayer: boolean, + context?: string + ) { + let defined = 0 if (params.osmTags) { - defined++; + defined++ } if (params.overpassScript) { - defined++; + defined++ } if (params.geojsonSource) { - defined++; + defined++ } if (defined == 0) { - throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify(params)})` + throw `Source: nothing correct defined in the source (in ${context}) (the params are ${JSON.stringify( + params + )})` } if (params.isOsmCache && params.geojsonSource == undefined) { console.error(params) throw `Source said it is a OSM-cached layer, but didn't define the actual source of the cache (in context ${context})` } if (params.geojsonSource !== undefined && params.geojsonSourceLevel !== undefined) { - if (!["x", "y", "x_min", "x_max", "y_min", "Y_max"].some(toSearch => params.geojsonSource.indexOf(toSearch) > 0)) { + if ( + !["x", "y", "x_min", "x_max", "y_min", "Y_max"].some( + (toSearch) => params.geojsonSource.indexOf(toSearch) > 0 + ) + ) { throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` } } - if(params.osmTags !== undefined && !isSpecialLayer){ + if (params.osmTags !== undefined && !isSpecialLayer) { const optimized = params.osmTags.optimize() - if(optimized === false){ - throw "Error at "+context+": the specified tags are conflicting with each other: they will never match anything at all" + if (optimized === false) { + throw ( + "Error at " + + context + + ": the specified tags are conflicting with each other: they will never match anything at all" + ) } - if(optimized === true){ - throw "Error at "+context+": the specified tags are very wide: they will always match everything" + if (optimized === true) { + throw ( + "Error at " + + context + + ": the specified tags are very wide: they will always match everything" + ) } } - this.osmTags = params.osmTags ?? new RegexTag("id", /.*/); - this.overpassScript = params.overpassScript; - this.geojsonSource = params.geojsonSource; - this.geojsonZoomLevel = params.geojsonSourceLevel; - this.isOsmCacheLayer = params.isOsmCache ?? false; - this.mercatorCrs = params.mercatorCrs ?? false; - this.idKey= params.idKey + this.osmTags = params.osmTags ?? new RegexTag("id", /.*/) + this.overpassScript = params.overpassScript + this.geojsonSource = params.geojsonSource + this.geojsonZoomLevel = params.geojsonSourceLevel + this.isOsmCacheLayer = params.isOsmCache ?? false + this.mercatorCrs = params.mercatorCrs ?? false + this.idKey = params.idKey } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 155581a2c..a191e9f26 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -1,29 +1,39 @@ -import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import Translations from "../../UI/i18n/Translations"; -import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"; -import {And} from "../../Logic/Tags/And"; -import ValidatedTextField from "../../UI/Input/ValidatedTextField"; -import {Utils} from "../../Utils"; -import {Tag} from "../../Logic/Tags/Tag"; -import BaseUIElement from "../../UI/BaseUIElement"; -import Combine from "../../UI/Base/Combine"; -import Title from "../../UI/Base/Title"; -import Link from "../../UI/Base/Link"; -import List from "../../UI/Base/List"; -import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; -import {Paragraph} from "../../UI/Base/Paragraph"; +import { Translation, TypedTranslation } from "../../UI/i18n/Translation" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import Translations from "../../UI/i18n/Translations" +import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" +import { And } from "../../Logic/Tags/And" +import ValidatedTextField from "../../UI/Input/ValidatedTextField" +import { Utils } from "../../Utils" +import { Tag } from "../../Logic/Tags/Tag" +import BaseUIElement from "../../UI/BaseUIElement" +import Combine from "../../UI/Base/Combine" +import Title from "../../UI/Base/Title" +import Link from "../../UI/Base/Link" +import List from "../../UI/Base/List" +import { + MappingConfigJson, + QuestionableTagRenderingConfigJson, +} from "./Json/QuestionableTagRenderingConfigJson" +import { FixedUiElement } from "../../UI/Base/FixedUiElement" +import { Paragraph } from "../../UI/Base/Paragraph" export interface Mapping { - readonly if: UploadableTag, - readonly ifnot?: UploadableTag, - readonly then: TypedTranslation, - readonly icon: string, - readonly iconClass: string | "small" | "medium" | "large" | "small-height" | "medium-height" | "large-height", + readonly if: UploadableTag + readonly ifnot?: UploadableTag + readonly then: TypedTranslation + readonly icon: string + readonly iconClass: + | string + | "small" + | "medium" + | "large" + | "small-height" + | "medium-height" + | "large-height" readonly hideInAnswer: boolean | TagsFilter - readonly addExtraTags: Tag[], - readonly searchTerms?: Record, + readonly addExtraTags: Tag[] + readonly searchTerms?: Record readonly priorityIf?: TagsFilter } @@ -32,52 +42,49 @@ export interface Mapping { * Identical data, but with some methods and validation */ export default class TagRenderingConfig { - - public readonly id: string; - public readonly group: string; - public readonly render?: TypedTranslation; - public readonly question?: TypedTranslation; - public readonly condition?: TagsFilter; - public readonly description?: Translation; + public readonly id: string + public readonly group: string + public readonly render?: TypedTranslation + public readonly question?: TypedTranslation + public readonly condition?: TagsFilter + public readonly description?: Translation public readonly configuration_warnings: string[] = [] public readonly freeform?: { - readonly key: string, - readonly type: string, - readonly placeholder: Translation, - readonly addExtraTags: UploadableTag[]; - readonly inline: boolean, - readonly default?: string, + readonly key: string + readonly type: string + readonly placeholder: Translation + readonly addExtraTags: UploadableTag[] + readonly inline: boolean + readonly default?: string readonly helperArgs?: (string | number | boolean)[] - }; + } - public readonly multiAnswer: boolean; + public readonly multiAnswer: boolean public readonly mappings?: Mapping[] public readonly labels: string[] - constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) { if (json === undefined) { - throw "Initing a TagRenderingConfig with undefined in " + context; + throw "Initing a TagRenderingConfig with undefined in " + context } if (json === "questions") { // Very special value - this.render = null; - this.question = null; - this.condition = null; + this.render = null + this.question = null + this.condition = null this.id = "questions" this.group = "" - return; + return } - if (typeof json === "number") { json = "" + json } - let translationKey = context; + let translationKey = context if (json["id"] !== undefined) { const layerId = context.split(".")[0] if (json["source"]) { @@ -91,43 +98,54 @@ export default class TagRenderingConfig { } } - if (typeof json === "string") { - this.render = Translations.T(json, translationKey + ".render"); - this.multiAnswer = false; - return; + this.render = Translations.T(json, translationKey + ".render") + this.multiAnswer = false + return } - - this.id = json.id ?? ""; // Some tagrenderings - especially for the map rendering - don't need an ID + this.id = json.id ?? "" // Some tagrenderings - especially for the map rendering - don't need an ID if (this.id.match(/^[a-zA-Z0-9 ()?\/=:;,_-]*$/) === null) { - throw "Invalid ID in " + context + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + this.id + throw ( + "Invalid ID in " + + context + + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + + this.id + ) } - - this.group = json.group ?? ""; + this.group = json.group ?? "" this.labels = json.labels ?? [] - this.render = Translations.T(json.render, translationKey + ".render"); - this.question = Translations.T(json.question, translationKey + ".question"); - this.description = Translations.T(json.description, translationKey + ".description"); - this.condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`); + this.render = Translations.T(json.render, translationKey + ".render") + this.question = Translations.T(json.question, translationKey + ".question") + this.description = Translations.T(json.description, translationKey + ".description") + this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`) if (json.freeform) { - - if (json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined) { + if ( + json.freeform.addExtraTags !== undefined && + json.freeform.addExtraTags.map === undefined + ) { throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})` } const type = json.freeform.type ?? "string" if (ValidatedTextField.AvailableTypes().indexOf(type) < 0) { - throw "At " + context + ".freeform.type is an unknown type: " + type + "; try one of " + ValidatedTextField.AvailableTypes().join(", ") + throw ( + "At " + + context + + ".freeform.type is an unknown type: " + + type + + "; try one of " + + ValidatedTextField.AvailableTypes().join(", ") + ) } let placeholder: Translation = Translations.T(json.freeform.placeholder) if (placeholder === undefined) { const typeDescription = Translations.t.validation[type]?.description - const key = json.freeform.key; + const key = json.freeform.key if (typeDescription !== undefined) { - placeholder = typeDescription.OnEveryLanguage(l => key + " (" + l + ")") + placeholder = typeDescription.OnEveryLanguage((l) => key + " (" + l + ")") } else { placeholder = Translations.T(key + " (" + type + ")") } @@ -137,12 +155,13 @@ export default class TagRenderingConfig { key: json.freeform.key, type, placeholder, - addExtraTags: json.freeform.addExtraTags?.map((tg, i) => - TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)) ?? [], + addExtraTags: + json.freeform.addExtraTags?.map((tg, i) => + TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`) + ) ?? [], inline: json.freeform.inline ?? false, default: json.freeform.default, - helperArgs: json.freeform.helperArgs - + helperArgs: json.freeform.helperArgs, } if (json.freeform["extraTags"] !== undefined) { throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` @@ -152,7 +171,6 @@ export default class TagRenderingConfig { } if (json.freeform["args"] !== undefined) { throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})` - } if (json.freeform.key === "questions") { @@ -161,28 +179,42 @@ export default class TagRenderingConfig { } } - - if (this.freeform.type !== undefined && ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0) { - const knownKeys = ValidatedTextField.AvailableTypes().join(", "); + if ( + this.freeform.type !== undefined && + ValidatedTextField.AvailableTypes().indexOf(this.freeform.type) < 0 + ) { + const knownKeys = ValidatedTextField.AvailableTypes().join(", ") throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` } if (this.freeform.addExtraTags) { - const usedKeys = new And(this.freeform.addExtraTags).usedKeys(); + const usedKeys = new And(this.freeform.addExtraTags).usedKeys() if (usedKeys.indexOf(this.freeform.key) >= 0) { - throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`; + throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}` } } } this.multiAnswer = json.multiAnswer ?? false if (json.mappings) { - if (!Array.isArray(json.mappings)) { throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")" } - const commonIconSize = Utils.NoNull(json.mappings.map(m => m.icon !== undefined ? m.icon["class"] : undefined))[0] ?? "small" - this.mappings = json.mappings.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, translationKey, context, this.multiAnswer, this.question !== undefined, commonIconSize)); + const commonIconSize = + Utils.NoNull( + json.mappings.map((m) => (m.icon !== undefined ? m.icon["class"] : undefined)) + )[0] ?? "small" + this.mappings = json.mappings.map((m, i) => + TagRenderingConfig.ExtractMapping( + m, + i, + translationKey, + context, + this.multiAnswer, + this.question !== undefined, + commonIconSize + ) + ) } if (this.question && this.freeform?.key === undefined && this.mappings === undefined) { @@ -196,14 +228,12 @@ export default class TagRenderingConfig { continue } throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!` - } if (this.freeform?.key !== undefined && this.freeform?.key !== "questions") { throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together` } } - if (this.freeform) { if (this.render === undefined) { throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}` @@ -222,21 +252,25 @@ export default class TagRenderingConfig { if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) { continue } - if (this.freeform.type === "opening_hours" && txt.indexOf("{opening_hours_table(") >= 0) { + if ( + this.freeform.type === "opening_hours" && + txt.indexOf("{opening_hours_table(") >= 0 + ) { continue } - if (this.freeform.type === "wikidata" && txt.indexOf("{wikipedia(" + this.freeform.key) >= 0) { + if ( + this.freeform.type === "wikidata" && + txt.indexOf("{wikipedia(" + this.freeform.key) >= 0 + ) { continue } if (this.freeform.key === "wikidata" && txt.indexOf("{wikipedia()") >= 0) { continue } throw `${context}: The rendering for language ${ln} does not contain the freeform key {${this.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} ` - } } - if (this.render && this.question && this.freeform === undefined) { throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}; the question is ${this.question.txt}` } @@ -244,7 +278,7 @@ export default class TagRenderingConfig { if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) { let keys = [] for (let i = 0; i < this.mappings.length; i++) { - const mapping = this.mappings[i]; + const mapping = this.mappings[i] if (mapping.if === undefined) { throw `${context}.mappings[${i}].if is undefined` } @@ -252,15 +286,17 @@ export default class TagRenderingConfig { } keys = Utils.Dedup(keys) for (let i = 0; i < this.mappings.length; i++) { - const mapping = this.mappings[i]; + const mapping = this.mappings[i] if (mapping.hideInAnswer) { continue } - const usedKeys = mapping.if.usedKeys(); + const usedKeys = mapping.if.usedKeys() for (const expectedKey of keys) { if (usedKeys.indexOf(expectedKey) < 0) { - const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}` + const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join( + ", " + )}, but it should also give a value for ${expectedKey}` this.configuration_warnings.push(msg) } } @@ -272,22 +308,21 @@ export default class TagRenderingConfig { throw `${context} MultiAnswer is set, but no mappings are defined` } - let allKeys = []; - let allHaveIfNot = true; + let allKeys = [] + let allHaveIfNot = true for (const mapping of this.mappings) { if (mapping.hideInAnswer) { - continue; + continue } if (mapping.ifnot === undefined) { - allHaveIfNot = false; + allHaveIfNot = false } - allKeys = allKeys.concat(mapping.if.usedKeys()); + allKeys = allKeys.concat(mapping.if.usedKeys()) } - allKeys = Utils.Dedup(allKeys); + allKeys = Utils.Dedup(allKeys) if (allKeys.length > 1 && !allHaveIfNot) { throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping` } - } } @@ -296,17 +331,24 @@ export default class TagRenderingConfig { * tr.if // => new Tag("a","b") * tr.priorityIf // => new Tag("_country","be") */ - public static ExtractMapping(mapping: MappingConfigJson, i: number, translationKey: string, - context: string, - multiAnswer?: boolean, isQuestionable?: boolean, commonSize: string = "small") { - + public static ExtractMapping( + mapping: MappingConfigJson, + i: number, + translationKey: string, + context: string, + multiAnswer?: boolean, + isQuestionable?: boolean, + commonSize: string = "small" + ) { const ctx = `${translationKey}.mappings.${i}` if (mapping.if === undefined) { throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}` } if (mapping.then === undefined) { if (mapping["render"] !== undefined) { - throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}` + throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify( + mapping + )}` } throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}` } @@ -315,7 +357,9 @@ export default class TagRenderingConfig { } if (mapping["render"] !== undefined) { - throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}` + throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify( + mapping + )}` } if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) { throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": } or {"or": } instead` @@ -325,18 +369,23 @@ export default class TagRenderingConfig { throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed` } - let hideInAnswer: boolean | TagsFilter = false; + let hideInAnswer: boolean | TagsFilter = false if (typeof mapping.hideInAnswer === "boolean") { - hideInAnswer = mapping.hideInAnswer; + hideInAnswer = mapping.hideInAnswer } else if (mapping.hideInAnswer !== undefined) { - hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`); + hideInAnswer = TagUtils.Tag( + mapping.hideInAnswer, + `${context}.mapping[${i}].hideInAnswer` + ) } - const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)); + const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => + TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`) + ) if (hideInAnswer === true && addExtraTags.length > 0) { throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.` } - let icon = undefined; + let icon = undefined let iconClass = commonSize if (mapping.icon !== undefined) { if (typeof mapping.icon === "string" && mapping.icon !== "") { @@ -346,18 +395,22 @@ export default class TagRenderingConfig { iconClass = mapping.icon["class"] ?? iconClass } } - const prioritySearch = mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined; + const prioritySearch = + mapping.priorityIf !== undefined ? TagUtils.Tag(mapping.priorityIf) : undefined const mp = { if: TagUtils.Tag(mapping.if, `${ctx}.if`), - ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined), + ifnot: + mapping.ifnot !== undefined + ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) + : undefined, then: Translations.T(mapping.then, `${ctx}.then`), hideInAnswer, icon, iconClass, addExtraTags, searchTerms: mapping.searchTerms, - priorityIf: prioritySearch - }; + priorityIf: prioritySearch, + } if (isQuestionable) { if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) { throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` @@ -368,7 +421,7 @@ export default class TagRenderingConfig { } } - return mp; + return mp } /** @@ -376,15 +429,14 @@ export default class TagRenderingConfig { * @constructor */ public IsKnown(tags: Record): boolean { - if (this.condition && - !this.condition.matchesProperties(tags)) { + if (this.condition && !this.condition.matchesProperties(tags)) { // Filtered away by the condition, so it is kindof known - return true; + return true } if (this.multiAnswer) { for (const m of this.mappings ?? []) { if (TagUtils.MatchesMultiAnswer(m.if, tags)) { - return true; + return true } } @@ -394,15 +446,14 @@ export default class TagRenderingConfig { return value !== undefined && value !== "" } return false - } if (this.GetRenderValue(tags) !== undefined) { // This value is known and can be rendered - return true; + return true } - return false; + return false } /** @@ -411,39 +462,49 @@ export default class TagRenderingConfig { * @param tags * @constructor */ - public GetRenderValues(tags: Record): { then: Translation, icon?: string, iconClass?: string }[] { + public GetRenderValues( + tags: Record + ): { then: Translation; icon?: string; iconClass?: string }[] { if (!this.multiAnswer) { return [this.GetRenderValueWithImage(tags)] } - // A flag to check that the freeform key isn't matched multiple times + // A flag to check that the freeform key isn't matched multiple times // If it is undefined, it is "used" already, or at least we don't have to check for it anymore - let freeformKeyDefined = this.freeform?.key !== undefined; + let freeformKeyDefined = this.freeform?.key !== undefined let usedFreeformValues = new Set() // We run over all the mappings first, to check if the mapping matches - const applicableMappings: { then: TypedTranslation>, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => { - if (mapping.if === undefined) { - return mapping; - } - if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { - if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) { - // THe freeform key is defined: what value does it use though? - // We mark the value to see if we have any leftovers - const value = mapping.if.asChange({}).find(kv => kv.k === this.freeform.key).v - usedFreeformValues.add(value) + const applicableMappings: { + then: TypedTranslation> + img?: string + }[] = Utils.NoNull( + (this.mappings ?? [])?.map((mapping) => { + if (mapping.if === undefined) { + return mapping } - return mapping; - } - return undefined; - })) + if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { + if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) { + // THe freeform key is defined: what value does it use though? + // We mark the value to see if we have any leftovers + const value = mapping.if + .asChange({}) + .find((kv) => kv.k === this.freeform.key).v + usedFreeformValues.add(value) + } + return mapping + } + return undefined + }) + ) if (freeformKeyDefined && tags[this.freeform.key] !== undefined) { const freeformValues = tags[this.freeform.key].split(";") - const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v)) + const leftovers = freeformValues.filter((v) => !usedFreeformValues.has(v)) for (const leftover of leftovers) { applicableMappings.push({ - then: - new TypedTranslation(this.render.replace("{" + this.freeform.key + "}", leftover).translations) + then: new TypedTranslation( + this.render.replace("{" + this.freeform.key + "}", leftover).translations + ), }) } } @@ -451,7 +512,10 @@ export default class TagRenderingConfig { return applicableMappings } - public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation | undefined { + public GetRenderValue( + tags: any, + defltValue: any = undefined + ): TypedTranslation | undefined { return this.GetRenderValueWithImage(tags, defltValue)?.then } @@ -460,7 +524,10 @@ export default class TagRenderingConfig { * Not compatible with multiAnswer - use GetRenderValueS instead in that case * @constructor */ - public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation, icon?: string } | undefined { + public GetRenderValueWithImage( + tags: any, + defltValue: any = undefined + ): { then: TypedTranslation; icon?: string } | undefined { if (this.condition !== undefined) { if (!this.condition.matchesProperties(tags)) { return undefined @@ -470,22 +537,23 @@ export default class TagRenderingConfig { if (this.mappings !== undefined && !this.multiAnswer) { for (const mapping of this.mappings) { if (mapping.if === undefined) { - return mapping; + return mapping } if (mapping.if.matchesProperties(tags)) { - return mapping; + return mapping } } } - if (this.id === "questions" || + if ( + this.id === "questions" || this.freeform?.key === undefined || tags[this.freeform.key] !== undefined ) { - return {then: this.render} + return { then: this.render } } - return {then: defltValue}; + return { then: defltValue } } /** @@ -498,52 +566,57 @@ export default class TagRenderingConfig { const translations: Translation[] = [] for (const key in this) { if (!this.hasOwnProperty(key)) { - continue; + continue } const o = this[key] if (o instanceof Translation) { translations.push(o) } } - return translations; + return translations } - FreeformValues(): { key: string, type?: string, values?: string [] } { + FreeformValues(): { key: string; type?: string; values?: string[] } { try { - const key = this.freeform?.key - const answerMappings = this.mappings?.filter(m => m.hideInAnswer !== true) + const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true) if (key === undefined) { - - let values: { k: string, v: string }[][] = Utils.NoNull(answerMappings?.map(m => m.if.asChange({})) ?? []) + let values: { k: string; v: string }[][] = Utils.NoNull( + answerMappings?.map((m) => m.if.asChange({})) ?? [] + ) if (values.length === 0) { - return; + return } - const allKeys = values.map(arr => arr.map(o => o.k)) - let common = allKeys[0]; + const allKeys = values.map((arr) => arr.map((o) => o.k)) + let common = allKeys[0] for (const keyset of allKeys) { - common = common.filter(item => keyset.indexOf(item) >= 0) + common = common.filter((item) => keyset.indexOf(item) >= 0) } const commonKey = common[0] if (commonKey === undefined) { - return undefined; + return undefined } return { key: commonKey, - values: Utils.NoNull(values.map(arr => arr.filter(item => item.k === commonKey)[0]?.v)) + values: Utils.NoNull( + values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v) + ), } - } - let values = Utils.NoNull(answerMappings?.map(m => m.if.asChange({}).filter(item => item.k === key)[0]?.v) ?? []) + let values = Utils.NoNull( + answerMappings?.map( + (m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v + ) ?? [] + ) if (values.length === undefined) { values = undefined } return { key, type: this.freeform.type, - values + values, } } catch (e) { console.error("Could not create FreeformValues for tagrendering", this.id) @@ -552,80 +625,93 @@ export default class TagRenderingConfig { } GenerateDocumentation(): BaseUIElement { - - let withRender: (BaseUIElement | string)[] = []; + let withRender: (BaseUIElement | string)[] = [] if (this.freeform?.key !== undefined) { withRender = [ `This rendering asks information about the property `, Link.OsmWiki(this.freeform.key), - new Paragraph(new Combine([ - "This is rendered with ", - new FixedUiElement(this.render.txt).SetClass("literalcode bold") - ])) - + new Paragraph( + new Combine([ + "This is rendered with ", + new FixedUiElement(this.render.txt).SetClass("literalcode bold"), + ]) + ), ] } - let mappings: BaseUIElement = undefined; + let mappings: BaseUIElement = undefined if (this.mappings !== undefined) { mappings = new List( - [].concat(...this.mappings.map(m => { + [].concat( + ...this.mappings.map((m) => { const msgs: (string | BaseUIElement)[] = [ - new Combine( - [ - new FixedUiElement(m.then.txt).SetClass("bold"), - " corresponds with ", - new FixedUiElement( m.if.asHumanString(true, false, {})).SetClass("code") - ] - ) + new Combine([ + new FixedUiElement(m.then.txt).SetClass("bold"), + " corresponds with ", + new FixedUiElement(m.if.asHumanString(true, false, {})).SetClass( + "code" + ), + ]), ] if (m.hideInAnswer === true) { - msgs.push(new FixedUiElement("This option cannot be chosen as answer").SetClass("italic")) + msgs.push( + new FixedUiElement( + "This option cannot be chosen as answer" + ).SetClass("italic") + ) } if (m.ifnot !== undefined) { - msgs.push("Unselecting this answer will add " + m.ifnot.asHumanString(true, false, {})) + msgs.push( + "Unselecting this answer will add " + + m.ifnot.asHumanString(true, false, {}) + ) } - return msgs; - } - )) + return msgs + }) + ) ) } let condition: BaseUIElement = undefined if (this.condition !== undefined && !this.condition?.matchesProperties({})) { - condition = new Combine(["Only visible if ", - new FixedUiElement(this.condition.asHumanString(false, false, {}) - ).SetClass("code") - , " is shown"]) + condition = new Combine([ + "Only visible if ", + new FixedUiElement(this.condition.asHumanString(false, false, {})).SetClass("code"), + " is shown", + ]) } let group: BaseUIElement = undefined if (this.group !== undefined && this.group !== "") { group = new Combine([ - "This tagrendering is part of group ", new FixedUiElement(this.group).SetClass("code") + "This tagrendering is part of group ", + new FixedUiElement(this.group).SetClass("code"), ]) } let labels: BaseUIElement = undefined if (this.labels?.length > 0) { labels = new Combine([ "This tagrendering has labels ", - ...this.labels.map(label => new FixedUiElement(label).SetClass("code")) + ...this.labels.map((label) => new FixedUiElement(label).SetClass("code")), ]).SetClass("flex") } - + return new Combine([ new Title(this.id, 3), this.description, - this.question !== undefined ? - new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("font-bold bold")]) : - new FixedUiElement( - "This tagrendering has no question and is thus read-only" - ).SetClass("italic"), + this.question !== undefined + ? new Combine([ + "The question is ", + new FixedUiElement(this.question.txt).SetClass("font-bold bold"), + ]) + : new FixedUiElement( + "This tagrendering has no question and is thus read-only" + ).SetClass("italic"), new Combine(withRender), mappings, condition, group, - labels - ]).SetClass("flex flex-col"); + labels, + ]).SetClass("flex flex-col") } -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/TilesourceConfig.ts b/Models/ThemeConfig/TilesourceConfig.ts index 661172bcf..4cfca68be 100644 --- a/Models/ThemeConfig/TilesourceConfig.ts +++ b/Models/ThemeConfig/TilesourceConfig.ts @@ -1,6 +1,6 @@ -import TilesourceConfigJson from "./Json/TilesourceConfigJson"; -import Translations from "../../UI/i18n/Translations"; -import {Translation} from "../../UI/i18n/Translation"; +import TilesourceConfigJson from "./Json/TilesourceConfigJson" +import Translations from "../../UI/i18n/Translations" +import { Translation } from "../../UI/i18n/Translation" export default class TilesourceConfig { public readonly source: string @@ -9,21 +9,23 @@ export default class TilesourceConfig { public readonly name: Translation public readonly minzoom: number public readonly maxzoom: number - public readonly defaultState: boolean; + public readonly defaultState: boolean constructor(config: TilesourceConfigJson, ctx: string = "") { this.id = config.id - this.source = config.source; - this.isOverlay = config.isOverlay ?? false; + this.source = config.source + this.isOverlay = config.isOverlay ?? false this.name = Translations.T(config.name) this.minzoom = config.minZoom ?? 0 this.maxzoom = config.maxZoom ?? 999 - this.defaultState = config.defaultState ?? true; + this.defaultState = config.defaultState ?? true if (this.id === undefined) { throw "An id is obligated" } if (this.minzoom > this.maxzoom) { - throw "Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")" + throw ( + "Invalid tilesourceConfig: minzoom should be smaller then maxzoom (at " + ctx + ")" + ) } if (this.minzoom < 0) { throw "minzoom should be > 0 (at " + ctx + ")" @@ -38,5 +40,4 @@ export default class TilesourceConfig { throw "Disabling an overlay without a name is not possible" } } - -} \ No newline at end of file +} diff --git a/Models/ThemeConfig/WithContextLoader.ts b/Models/ThemeConfig/WithContextLoader.ts index 5f59e65c8..ad481a57f 100644 --- a/Models/ThemeConfig/WithContextLoader.ts +++ b/Models/ThemeConfig/WithContextLoader.ts @@ -1,14 +1,14 @@ -import TagRenderingConfig from "./TagRenderingConfig"; -import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; -import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; +import TagRenderingConfig from "./TagRenderingConfig" +import SharedTagRenderings from "../../Customizations/SharedTagRenderings" +import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" export default class WithContextLoader { - protected readonly _context: string; - private readonly _json: any; + protected readonly _context: string + private readonly _json: any constructor(json: any, context: string) { - this._json = json; - this._context = context; + this._json = json + this._context = context } /** Given a key, gets the corresponding property from the json (or the default if not found @@ -16,26 +16,20 @@ export default class WithContextLoader { * The found value is interpreted as a tagrendering and fetched/parsed * */ public tr(key: string, deflt) { - const v = this._json[key]; + const v = this._json[key] if (v === undefined || v === null) { if (deflt === undefined) { - return undefined; + return undefined } - return new TagRenderingConfig( - deflt, - `${this._context}.${key}.default value` - ); + return new TagRenderingConfig(deflt, `${this._context}.${key}.default value`) } if (typeof v === "string") { - const shared = SharedTagRenderings.SharedTagRendering.get(v); + const shared = SharedTagRenderings.SharedTagRendering.get(v) if (shared) { - return shared; + return shared } } - return new TagRenderingConfig( - v, - `${this._context}.${key}` - ); + return new TagRenderingConfig(v, `${this._context}.${key}`) } /** @@ -48,27 +42,29 @@ export default class WithContextLoader { /** * Throw an error if 'question' is defined */ - readOnlyMode?: boolean, + readOnlyMode?: boolean requiresId?: boolean - prepConfig?: ((config: TagRenderingConfigJson) => TagRenderingConfigJson) - + prepConfig?: (config: TagRenderingConfigJson) => TagRenderingConfigJson } ): TagRenderingConfig[] { if (tagRenderings === undefined) { - return []; + return [] } const context = this._context options = options ?? {} if (options.prepConfig === undefined) { - options.prepConfig = c => c + options.prepConfig = (c) => c } const renderings: TagRenderingConfig[] = [] for (let i = 0; i < tagRenderings.length; i++) { - const preparedConfig = tagRenderings[i]; - const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`); + const preparedConfig = tagRenderings[i] + const tr = new TagRenderingConfig(preparedConfig, `${context}.tagrendering[${i}]`) if (options.readOnlyMode && tr.question !== undefined) { - throw "A question is defined for " + `${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label` + throw ( + "A question is defined for " + + `${context}.tagrendering[${i}], but this is not allowed at this position - probably because this rendering is an icon, badge or label` + ) } if (options.requiresId && tr.id === "") { throw `${context}.tagrendering[${i}] has an invalid ID - make sure it is defined and not empty` @@ -77,6 +73,6 @@ export default class WithContextLoader { renderings.push(tr) } - return renderings; + return renderings } -} \ No newline at end of file +} diff --git a/Models/TileRange.ts b/Models/TileRange.ts index 7f282be6b..2454ed471 100644 --- a/Models/TileRange.ts +++ b/Models/TileRange.ts @@ -1,14 +1,13 @@ export interface TileRange { - xstart: number, - ystart: number, - xend: number, - yend: number, - total: number, + xstart: number + ystart: number + xend: number + yend: number + total: number zoomlevel: number } export class Tiles { - public static MapRange(tileRange: TileRange, f: (x: number, y: number) => T): T[] { const result: T[] = [] const total = tileRange.total @@ -17,11 +16,11 @@ export class Tiles { } for (let x = tileRange.xstart; x <= tileRange.xend; x++) { for (let y = tileRange.ystart; y <= tileRange.yend; y++) { - const t = f(x, y); + const t = f(x, y) result.push(t) } } - return result; + return result } /** @@ -32,11 +31,21 @@ export class Tiles { * @returns [[maxlat, minlon], [minlat, maxlon]] */ static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] { - return [[Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)]] + return [ + [Tiles.tile2lat(y, z), Tiles.tile2long(x, z)], + [Tiles.tile2lat(y + 1, z), Tiles.tile2long(x + 1, z)], + ] } - static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] { - return [[Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)]] + static tile_bounds_lon_lat( + z: number, + x: number, + y: number + ): [[number, number], [number, number]] { + return [ + [Tiles.tile2long(x, z), Tiles.tile2lat(y, z)], + [Tiles.tile2long(x + 1, z), Tiles.tile2lat(y + 1, z)], + ] } /** @@ -46,11 +55,14 @@ export class Tiles { * @param y */ static centerPointOf(z: number, x: number, y: number): [number, number] { - return [(Tiles.tile2long(x, z) + Tiles.tile2long(x + 1, z)) / 2, (Tiles.tile2lat(y, z) + Tiles.tile2lat(y + 1, z)) / 2] + return [ + (Tiles.tile2long(x, z) + Tiles.tile2long(x + 1, z)) / 2, + (Tiles.tile2lat(y, z) + Tiles.tile2lat(y + 1, z)) / 2, + ] } static tile_index(z: number, x: number, y: number): number { - return ((x * (2 << z)) + y) * 100 + z + return (x * (2 << z) + y) * 100 + z } /** @@ -59,7 +71,7 @@ export class Tiles { * @returns 'zxy' */ static tile_from_index(index: number): [number, number, number] { - const z = index % 100; + const z = index % 100 const factor = 2 << z index = Math.floor(index / 100) const x = Math.floor(index / factor) @@ -69,11 +81,17 @@ export class Tiles { /** * Return x, y of the tile containing (lat, lon) on the given zoom level */ - static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } { - return {x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z} + static embedded_tile(lat: number, lon: number, z: number): { x: number; y: number; z: number } { + return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z } } - static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange { + static TileRangeBetween( + zoomlevel: number, + lat0: number, + lon0: number, + lat1: number, + lon1: number + ): TileRange { const t0 = Tiles.embedded_tile(lat0, lon0, zoomlevel) const t1 = Tiles.embedded_tile(lat1, lon1, zoomlevel) @@ -89,26 +107,30 @@ export class Tiles { ystart: ystart, yend: yend, total: total, - zoomlevel: zoomlevel + zoomlevel: zoomlevel, } } private static tile2long(x, z) { - return (x / Math.pow(2, z) * 360 - 180); + return (x / Math.pow(2, z)) * 360 - 180 } private static tile2lat(y, z) { - const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); - return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); + const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z) + return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))) } private static lon2tile(lon, zoom) { - return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom))); + return Math.floor(((lon + 180) / 360) * Math.pow(2, zoom)) } private static lat2tile(lat, zoom) { - return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); + return Math.floor( + ((1 - + Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) / + Math.PI) / + 2) * + Math.pow(2, zoom) + ) } - - -} \ No newline at end of file +} diff --git a/Models/Unit.ts b/Models/Unit.ts index 93247752c..7a4871975 100644 --- a/Models/Unit.ts +++ b/Models/Unit.ts @@ -1,32 +1,41 @@ -import BaseUIElement from "../UI/BaseUIElement"; -import {FixedUiElement} from "../UI/Base/FixedUiElement"; -import Combine from "../UI/Base/Combine"; -import {Denomination} from "./Denomination"; -import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"; +import BaseUIElement from "../UI/BaseUIElement" +import { FixedUiElement } from "../UI/Base/FixedUiElement" +import Combine from "../UI/Base/Combine" +import { Denomination } from "./Denomination" +import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson" export class Unit { - public readonly appliesToKeys: Set; - public readonly denominations: Denomination[]; - public readonly denominationsSorted: Denomination[]; - public readonly eraseInvalid: boolean; + public readonly appliesToKeys: Set + public readonly denominations: Denomination[] + public readonly denominationsSorted: Denomination[] + public readonly eraseInvalid: boolean - constructor(appliesToKeys: string[], applicableDenominations: Denomination[], eraseInvalid: boolean) { - this.appliesToKeys = new Set(appliesToKeys); - this.denominations = applicableDenominations; + constructor( + appliesToKeys: string[], + applicableDenominations: Denomination[], + eraseInvalid: boolean + ) { + this.appliesToKeys = new Set(appliesToKeys) + this.denominations = applicableDenominations this.eraseInvalid = eraseInvalid - const seenUnitExtensions = new Set(); + const seenUnitExtensions = new Set() for (const denomination of this.denominations) { if (seenUnitExtensions.has(denomination.canonical)) { - throw "This canonical unit is already defined in another denomination: " + denomination.canonical + throw ( + "This canonical unit is already defined in another denomination: " + + denomination.canonical + ) } - const duplicate = denomination.alternativeDenominations.filter(denom => seenUnitExtensions.has(denom)) + const duplicate = denomination.alternativeDenominations.filter((denom) => + seenUnitExtensions.has(denom) + ) if (duplicate.length > 0) { throw "A denomination is used multiple times: " + duplicate.join(", ") } seenUnitExtensions.add(denomination.canonical) - denomination.alternativeDenominations.forEach(d => seenUnitExtensions.add(d)) + denomination.alternativeDenominations.forEach((d) => seenUnitExtensions.add(d)) } this.denominationsSorted = [...this.denominations] this.denominationsSorted.sort((a, b) => b.canonical.length - a.canonical.length) @@ -51,37 +60,38 @@ export class Unit { } } - static fromJson(json: UnitConfigJson, ctx: string) { const appliesTo = json.appliesToKey for (let i = 0; i < appliesTo.length; i++) { - let key = appliesTo[i]; + let key = appliesTo[i] if (key.trim() !== key) { throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace` } } if ((json.applicableUnits ?? []).length === 0) { - throw `${ctx}: define at least one applicable unit` + throw `${ctx}: define at least one applicable unit` } // Some keys do have unit handling - if(json.applicableUnits.some(denom => denom.useAsDefaultInput !== undefined)){ - json.applicableUnits.forEach(denom => { + if (json.applicableUnits.some((denom) => denom.useAsDefaultInput !== undefined)) { + json.applicableUnits.forEach((denom) => { denom.useAsDefaultInput = denom.useAsDefaultInput ?? false }) } - - const applicable = json.applicableUnits.map((u, i) => new Denomination(u, `${ctx}.units[${i}]`)) + + const applicable = json.applicableUnits.map( + (u, i) => new Denomination(u, `${ctx}.units[${i}]`) + ) return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false) } isApplicableToKey(key: string | undefined): boolean { if (key === undefined) { - return false; + return false } - return this.appliesToKeys.has(key); + return this.appliesToKeys.has(key) } /** @@ -89,7 +99,7 @@ export class Unit { */ findDenomination(valueWithDenom: string, country: () => string): [string, Denomination] { if (valueWithDenom === undefined) { - return undefined; + return undefined } const defaultDenom = this.getDefaultDenomination(country) for (const denomination of this.denominationsSorted) { @@ -103,27 +113,28 @@ export class Unit { asHumanLongValue(value: string, country: () => string): BaseUIElement { if (value === undefined) { - return undefined; + return undefined } const [stripped, denom] = this.findDenomination(value, country) const human = stripped === "1" ? denom?.humanSingular : denom?.human if (human === undefined) { - return new FixedUiElement(stripped ?? value); + return new FixedUiElement(stripped ?? value) } - const elems = denom.prefix ? [human, stripped] : [stripped, human]; + const elems = denom.prefix ? [human, stripped] : [stripped, human] return new Combine(elems) - } - public getDefaultInput(country: () => string | string[]) { console.log("Searching the default denomination for input", country) for (const denomination of this.denominations) { if (denomination.useAsDefaultInput === true) { return denomination } - if (denomination.useAsDefaultInput === undefined || denomination.useAsDefaultInput === false) { + if ( + denomination.useAsDefaultInput === undefined || + denomination.useAsDefaultInput === false + ) { continue } let countries: string | string[] = country() @@ -131,19 +142,22 @@ export class Unit { countries = countries.split(",") } const denominationCountries: string[] = denomination.useAsDefaultInput - if (countries.some(country => denominationCountries.indexOf(country) >= 0)) { + if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) { return denomination } } return this.denominations[0] } - - public getDefaultDenomination(country: () => string){ + + public getDefaultDenomination(country: () => string) { for (const denomination of this.denominations) { if (denomination.useIfNoUnitGiven === true || denomination.canonical === "") { return denomination } - if (denomination.useIfNoUnitGiven === undefined || denomination.useIfNoUnitGiven === false) { + if ( + denomination.useIfNoUnitGiven === undefined || + denomination.useIfNoUnitGiven === false + ) { continue } let countries: string | string[] = country() @@ -151,11 +165,10 @@ export class Unit { countries = countries.split(",") } const denominationCountries: string[] = denomination.useIfNoUnitGiven - if (countries.some(country => denominationCountries.indexOf(country) >= 0)) { + if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) { return denomination } } return this.denominations[0] } - -} \ No newline at end of file +} diff --git a/Models/smallLicense.ts b/Models/smallLicense.ts index e87473a0e..f495c236e 100644 --- a/Models/smallLicense.ts +++ b/Models/smallLicense.ts @@ -1,6 +1,6 @@ export default interface SmallLicense { - path: string, - authors: string[], - license: string, + path: string + authors: string[] + license: string sources: string[] -} \ No newline at end of file +} diff --git a/State.ts b/State.ts index 9a0f47816..2572dc84a 100644 --- a/State.ts +++ b/State.ts @@ -1,5 +1,5 @@ -import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; -import FeaturePipelineState from "./Logic/State/FeaturePipelineState"; +import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" +import FeaturePipelineState from "./Logic/State/FeaturePipelineState" /** * Contains the global state: a bunch of UI-event sources @@ -8,11 +8,9 @@ import FeaturePipelineState from "./Logic/State/FeaturePipelineState"; export default class State extends FeaturePipelineState { /* The singleton of the global state */ - public static state: FeaturePipelineState; + public static state: FeaturePipelineState constructor(layoutToUse: LayoutConfig) { super(layoutToUse) } - - } diff --git a/UI/AllTagsPanel.ts b/UI/AllTagsPanel.ts index 7a6322f67..32b8a2c84 100644 --- a/UI/AllTagsPanel.ts +++ b/UI/AllTagsPanel.ts @@ -1,46 +1,47 @@ -import {VariableUiElement} from "./Base/VariableUIElement"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Table from "./Base/Table"; +import { VariableUiElement } from "./Base/VariableUIElement" +import { UIEventSource } from "../Logic/UIEventSource" +import Table from "./Base/Table" export class AllTagsPanel extends VariableUiElement { - constructor(tags: UIEventSource, state?) { - const calculatedTags = [].concat( - // SimpleMetaTagger.lazyTags, - ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) + // SimpleMetaTagger.lazyTags, + ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? + []) + ) + super( + tags.map((tags) => { + const parts = [] + for (const key in tags) { + if (!tags.hasOwnProperty(key)) { + continue + } + let v = tags[key] + if (v === "") { + v = "empty string" + } + parts.push([key, v ?? "undefined"]) + } - super(tags.map(tags => { - const parts = []; - for (const key in tags) { - if (!tags.hasOwnProperty(key)) { - continue + for (const key of calculatedTags) { + const value = tags[key] + if (value === undefined) { + continue + } + let type = "" + if (typeof value !== "string") { + type = " " + typeof value + "" + } + parts.push(["" + key + "", value]) } - let v = tags[key] - if (v === "") { - v = "empty string" - } - parts.push([key, v ?? "undefined"]); - } - for (const key of calculatedTags) { - const value = tags[key] - if (value === undefined) { - continue - } - let type = ""; - if (typeof value !== "string") { - type = " " + (typeof value) + "" - } - parts.push(["" + key + "", value]) - } - - return new Table( - ["key", "value"], - parts - ) - .SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") - })) + return new Table(["key", "value"], parts) + .SetStyle( + "border: 1px solid black; border-radius: 1em;padding:1em;display:block;" + ) + .SetClass("zebra-table") + }) + ) } -} \ No newline at end of file +} diff --git a/UI/AllThemesGui.ts b/UI/AllThemesGui.ts index 2c2371ec9..a517306e2 100644 --- a/UI/AllThemesGui.ts +++ b/UI/AllThemesGui.ts @@ -1,58 +1,69 @@ -import UserRelatedState from "../Logic/State/UserRelatedState"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import Combine from "./Base/Combine"; -import MoreScreen from "./BigComponents/MoreScreen"; -import Translations from "./i18n/Translations"; -import Constants from "../Models/Constants"; -import {Utils} from "../Utils"; -import LanguagePicker1 from "./LanguagePicker"; -import IndexText from "./BigComponents/IndexText"; -import FeaturedMessage from "./BigComponents/FeaturedMessage"; -import Toggle from "./Input/Toggle"; -import {SubtleButton} from "./Base/SubtleButton"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import Svg from "../Svg"; +import UserRelatedState from "../Logic/State/UserRelatedState" +import { FixedUiElement } from "./Base/FixedUiElement" +import Combine from "./Base/Combine" +import MoreScreen from "./BigComponents/MoreScreen" +import Translations from "./i18n/Translations" +import Constants from "../Models/Constants" +import { Utils } from "../Utils" +import LanguagePicker1 from "./LanguagePicker" +import IndexText from "./BigComponents/IndexText" +import FeaturedMessage from "./BigComponents/FeaturedMessage" +import Toggle from "./Input/Toggle" +import { SubtleButton } from "./Base/SubtleButton" +import { VariableUiElement } from "./Base/VariableUIElement" +import Svg from "../Svg" export default class AllThemesGui { setup() { try { - new FixedUiElement("").AttachTo("centermessage") - const state = new UserRelatedState(undefined); + const state = new UserRelatedState(undefined) const intro = new Combine([ - - new LanguagePicker1(Translations.t.index.title.SupportedLanguages(), "") - - .SetClass("flex absolute top-2 right-3"), - new IndexText() - ]); + new LanguagePicker1(Translations.t.index.title.SupportedLanguages(), "").SetClass( + "flex absolute top-2 right-3" + ), + new IndexText(), + ]) new Combine([ intro, new FeaturedMessage().SetClass("mb-4 block"), new MoreScreen(state, true), new Toggle( undefined, - new SubtleButton(undefined, Translations.t.index.logIn).SetStyle("height:min-content").onClick(() => state.osmConnection.AttemptLogin()), - state.osmConnection.isLoggedIn), - new VariableUiElement(state.osmConnection.userDetails.map(ud => { - if (ud.csCount < Constants.userJourney.importHelperUnlock) { - return undefined; - } - return new Combine([ - new SubtleButton(undefined, Translations.t.importHelper.title, {url: "import_helper.html"}), - new SubtleButton(Svg.note_svg(), Translations.t.importInspector.title, {url: "import_viewer.html"}) - ]).SetClass("p-4 border-2 border-gray-500 m-4 block") - })), + new SubtleButton(undefined, Translations.t.index.logIn) + .SetStyle("height:min-content") + .onClick(() => state.osmConnection.AttemptLogin()), + state.osmConnection.isLoggedIn + ), + new VariableUiElement( + state.osmConnection.userDetails.map((ud) => { + if (ud.csCount < Constants.userJourney.importHelperUnlock) { + return undefined + } + return new Combine([ + new SubtleButton(undefined, Translations.t.importHelper.title, { + url: "import_helper.html", + }), + new SubtleButton(Svg.note_svg(), Translations.t.importInspector.title, { + url: "import_viewer.html", + }), + ]).SetClass("p-4 border-2 border-gray-500 m-4 block") + }) + ), Translations.t.general.aboutMapcomplete - .Subs({"osmcha_link": Utils.OsmChaLinkFor(7)}) + .Subs({ osmcha_link: Utils.OsmChaLinkFor(7) }) .SetClass("link-underline"), - new FixedUiElement("v" + Constants.vNumber) - ]).SetClass("block m-5 lg:w-3/4 lg:ml-40") + new FixedUiElement("v" + Constants.vNumber), + ]) + .SetClass("block m-5 lg:w-3/4 lg:ml-40") .SetStyle("pointer-events: all;") - .AttachTo("topleft-tools"); + .AttachTo("topleft-tools") } catch (e) { console.error(">>>> CRITICAL", e) - new FixedUiElement("Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!").SetClass("alert") + new FixedUiElement( + "Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!" + ) + .SetClass("alert") .AttachTo("centermessage") } } diff --git a/UI/AutomatonGui.ts b/UI/AutomatonGui.ts index 41559e875..8a0233d86 100644 --- a/UI/AutomatonGui.ts +++ b/UI/AutomatonGui.ts @@ -1,51 +1,60 @@ -import BaseUIElement from "./BaseUIElement"; -import Combine from "./Base/Combine"; -import Svg from "../Svg"; -import Title from "./Base/Title"; -import Toggle from "./Input/Toggle"; -import {SubtleButton} from "./Base/SubtleButton"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import ValidatedTextField from "./Input/ValidatedTextField"; -import {Utils} from "../Utils"; -import {UIEventSource} from "../Logic/UIEventSource"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import {Tiles} from "../Models/TileRange"; -import {LocalStorageSource} from "../Logic/Web/LocalStorageSource"; -import {DropDown} from "./Input/DropDown"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import MinimapImplementation from "./Base/MinimapImplementation"; -import {OsmConnection} from "../Logic/Osm/OsmConnection"; -import {BBox} from "../Logic/BBox"; -import MapState from "../Logic/State/MapState"; -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; -import FeatureSource from "../Logic/FeatureSource/FeatureSource"; -import List from "./Base/List"; -import {QueryParameters} from "../Logic/Web/QueryParameters"; -import {SubstitutedTranslation} from "./SubstitutedTranslation"; -import {AutoAction} from "./Popup/AutoApplyButton"; -import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource"; +import BaseUIElement from "./BaseUIElement" +import Combine from "./Base/Combine" +import Svg from "../Svg" +import Title from "./Base/Title" +import Toggle from "./Input/Toggle" +import { SubtleButton } from "./Base/SubtleButton" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import ValidatedTextField from "./Input/ValidatedTextField" +import { Utils } from "../Utils" +import { UIEventSource } from "../Logic/UIEventSource" +import { VariableUiElement } from "./Base/VariableUIElement" +import { FixedUiElement } from "./Base/FixedUiElement" +import { Tiles } from "../Models/TileRange" +import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" +import { DropDown } from "./Input/DropDown" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import MinimapImplementation from "./Base/MinimapImplementation" +import { OsmConnection } from "../Logic/Osm/OsmConnection" +import { BBox } from "../Logic/BBox" +import MapState from "../Logic/State/MapState" +import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" +import FeatureSource from "../Logic/FeatureSource/FeatureSource" +import List from "./Base/List" +import { QueryParameters } from "../Logic/Web/QueryParameters" +import { SubstitutedTranslation } from "./SubstitutedTranslation" +import { AutoAction } from "./Popup/AutoApplyButton" +import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource" import * as themeOverview from "../assets/generated/theme_overview.json" - class AutomationPanel extends Combine { - private static readonly openChangeset = new UIEventSource(undefined); + private static readonly openChangeset = new UIEventSource(undefined) - constructor(layoutToUse: LayoutConfig, indices: number[], extraCommentText: UIEventSource, tagRenderingToAutomate: { layer: LayerConfig, tagRendering: TagRenderingConfig }) { + constructor( + layoutToUse: LayoutConfig, + indices: number[], + extraCommentText: UIEventSource, + tagRenderingToAutomate: { layer: LayerConfig; tagRendering: TagRenderingConfig } + ) { const layerId = tagRenderingToAutomate.layer.id const trId = tagRenderingToAutomate.tagRendering.id - const tileState = LocalStorageSource.GetParsed("automation-tile_state-" + layerId + "-" + trId, {}) + const tileState = LocalStorageSource.GetParsed( + "automation-tile_state-" + layerId + "-" + trId, + {} + ) const logMessages = new UIEventSource([]) if (indices === undefined) { - throw ("No tiles loaded - can not automate") + throw "No tiles loaded - can not automate" } - const openChangeset = AutomationPanel.openChangeset; + const openChangeset = AutomationPanel.openChangeset - openChangeset.addCallbackAndRun(cs => console.trace("Sync current open changeset to:", cs)) + openChangeset.addCallbackAndRun((cs) => + console.trace("Sync current open changeset to:", cs) + ) - const nextTileToHandle = tileState.map(handledTiles => { + const nextTileToHandle = tileState.map((handledTiles) => { for (const index of indices) { if (handledTiles[index] !== undefined) { // Already handled @@ -55,53 +64,70 @@ class AutomationPanel extends Combine { } return undefined }) - nextTileToHandle.addCallback(t => console.warn("Next tile to handle is", t)) + nextTileToHandle.addCallback((t) => console.warn("Next tile to handle is", t)) const neededTimes = new UIEventSource([]) - const automaton = new VariableUiElement(nextTileToHandle.map(tileIndex => { - if (tileIndex === undefined) { - return new FixedUiElement("All done!").SetClass("thanks") - } - console.warn("Triggered map on nextTileToHandle", tileIndex) - const start = new Date() - return AutomationPanel.TileHandler(layoutToUse, tileIndex, layerId, tagRenderingToAutomate.tagRendering, extraCommentText, - (result, logMessage) => { - const end = new Date() - const timeNeeded = (end.getTime() - start.getTime()) / 1000; - neededTimes.data.push(timeNeeded) - neededTimes.ping() - tileState.data[tileIndex] = result - tileState.ping(); - if (logMessage !== undefined) { - logMessages.data.push(logMessage) - logMessages.ping(); + const automaton = new VariableUiElement( + nextTileToHandle.map((tileIndex) => { + if (tileIndex === undefined) { + return new FixedUiElement("All done!").SetClass("thanks") + } + console.warn("Triggered map on nextTileToHandle", tileIndex) + const start = new Date() + return AutomationPanel.TileHandler( + layoutToUse, + tileIndex, + layerId, + tagRenderingToAutomate.tagRendering, + extraCommentText, + (result, logMessage) => { + const end = new Date() + const timeNeeded = (end.getTime() - start.getTime()) / 1000 + neededTimes.data.push(timeNeeded) + neededTimes.ping() + tileState.data[tileIndex] = result + tileState.ping() + if (logMessage !== undefined) { + logMessages.data.push(logMessage) + logMessages.ping() + } } - }); - })) - - - const statistics = new VariableUiElement(tileState.map(states => { - let total = 0 - const perResult = new Map() - for (const key in states) { - total++ - const result = states[key] - perResult.set(result, (perResult.get(result) ?? 0) + 1) - } - - let sum = 0 - neededTimes.data.forEach(v => { - sum = sum + v + ) }) - let timePerTile = sum / neededTimes.data.length + ) - return new Combine(["Handled " + total + "/" + indices.length + " tiles: ", - new List(Array.from(perResult.keys()).map(key => key + ": " + perResult.get(key))), - "Handling one tile needs " + (Math.floor(timePerTile * 100) / 100) + "s on average. Estimated time left: " + Utils.toHumanTime((indices.length - total) * timePerTile) - ]).SetClass("flex flex-col") - })) + const statistics = new VariableUiElement( + tileState.map((states) => { + let total = 0 + const perResult = new Map() + for (const key in states) { + total++ + const result = states[key] + perResult.set(result, (perResult.get(result) ?? 0) + 1) + } - super([statistics, automaton, + let sum = 0 + neededTimes.data.forEach((v) => { + sum = sum + v + }) + let timePerTile = sum / neededTimes.data.length + + return new Combine([ + "Handled " + total + "/" + indices.length + " tiles: ", + new List( + Array.from(perResult.keys()).map((key) => key + ": " + perResult.get(key)) + ), + "Handling one tile needs " + + Math.floor(timePerTile * 100) / 100 + + "s on average. Estimated time left: " + + Utils.toHumanTime((indices.length - total) * timePerTile), + ]).SetClass("flex flex-col") + }) + ) + + super([ + statistics, + automaton, new SubtleButton(undefined, "Clear fixed").onClick(() => { const st = tileState.data for (const tileIndex in st) { @@ -110,54 +136,62 @@ class AutomationPanel extends Combine { } } - tileState.ping(); + tileState.ping() }), - new VariableUiElement(logMessages.map(logMessages => new List(logMessages)))]) + new VariableUiElement(logMessages.map((logMessages) => new List(logMessages))), + ]) this.SetClass("flex flex-col") } - private static TileHandler(layoutToUse: LayoutConfig, tileIndex: number, targetLayer: string, targetAction: TagRenderingConfig, extraCommentText: UIEventSource, - whenDone: ((result: string, logMessage?: string) => void)): BaseUIElement { - - const state = new MapState(layoutToUse, {attemptLogin: false}) + private static TileHandler( + layoutToUse: LayoutConfig, + tileIndex: number, + targetLayer: string, + targetAction: TagRenderingConfig, + extraCommentText: UIEventSource, + whenDone: (result: string, logMessage?: string) => void + ): BaseUIElement { + const state = new MapState(layoutToUse, { attemptLogin: false }) extraCommentText.syncWith(state.changes.extraComment) const [z, x, y] = Tiles.tile_from_index(tileIndex) state.locationControl.setData({ zoom: z, lon: x, - lat: y + lat: y, }) - state.currentBounds.setData( - BBox.fromTileIndex(tileIndex) - ) + state.currentBounds.setData(BBox.fromTileIndex(tileIndex)) let targetTiles: UIEventSource = new UIEventSource([]) - const pipeline = new FeaturePipeline((tile => { + const pipeline = new FeaturePipeline((tile) => { const layerId = tile.layer.layerDef.id if (layerId === targetLayer) { targetTiles.data.push(tile) targetTiles.ping() } - }), state) + }, state) - state.locationControl.ping(); - state.currentBounds.ping(); + state.locationControl.ping() + state.currentBounds.ping() const stateToShow = new UIEventSource("") pipeline.runningQuery.map( - async isRunning => { + async (isRunning) => { if (targetTiles.data.length === 0) { stateToShow.setData("No data loaded yet...") - return; + return } if (isRunning) { - stateToShow.setData("Waiting for all layers to be loaded... Has " + targetTiles.data.length + " tiles already") - return; + stateToShow.setData( + "Waiting for all layers to be loaded... Has " + + targetTiles.data.length + + " tiles already" + ) + return } if (targetTiles.data.length === 0) { stateToShow.setData("No features found to apply the action") whenDone("empty") - return true; + return true } stateToShow.setData("Gathering applicable elements") @@ -165,37 +199,62 @@ class AutomationPanel extends Combine { let inspected = 0 let log = [] for (const targetTile of targetTiles.data) { - for (const ffs of targetTile.features.data) { inspected++ if (inspected % 10 === 0) { - stateToShow.setData("Inspected " + inspected + " features, updated " + handled + " features") + stateToShow.setData( + "Inspected " + + inspected + + " features, updated " + + handled + + " features" + ) } const feature = ffs.feature const renderingTr = targetAction.GetRenderValue(feature.properties) const rendering = renderingTr.txt - log.push("" + feature.properties.id + ": " + new SubstitutedTranslation(renderingTr, new UIEventSource(feature.properties), undefined).ConstructElement().textContent) - const actions = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering) - .map(obj => obj.special)) + log.push( + "" + + feature.properties.id + + ": " + + new SubstitutedTranslation( + renderingTr, + new UIEventSource(feature.properties), + undefined + ).ConstructElement().textContent + ) + const actions = Utils.NoNull( + SubstitutedTranslation.ExtractSpecialComponents(rendering).map( + (obj) => obj.special + ) + ) for (const action of actions) { const auto = action.func if (auto.supportsAutoAction !== true) { continue } - await auto.applyActionOn({ - layoutToUse: state.layoutToUse, - changes: state.changes - }, state.allElements.getEventSourceById(feature.properties.id), action.args) + await auto.applyActionOn( + { + layoutToUse: state.layoutToUse, + changes: state.changes, + }, + state.allElements.getEventSourceById(feature.properties.id), + action.args + ) handled++ } } } - stateToShow.setData("Done! Inspected " + inspected + " features, updated " + handled + " features") + stateToShow.setData( + "Done! Inspected " + inspected + " features, updated " + handled + " features" + ) if (inspected === 0) { whenDone("empty") - return true; + return true } if (handled === 0) { @@ -203,61 +262,73 @@ class AutomationPanel extends Combine { } else { state.osmConnection.AttemptLogin() state.changes.flushChanges("handled tile automatically, time to flush!") - whenDone("fixed", "Updated " + handled + " elements, inspected " + inspected + ": " + log.join("; ")) + whenDone( + "fixed", + "Updated " + + handled + + " elements, inspected " + + inspected + + ": " + + log.join("; ") + ) } - return true; - - }, [targetTiles]) + return true + }, + [targetTiles] + ) return new Combine([ new Title("Performing action for tile " + tileIndex, 1), - new VariableUiElement(stateToShow)]).SetClass("flex flex-col") + new VariableUiElement(stateToShow), + ]).SetClass("flex flex-col") } - } - class AutomatonGui { - constructor() { - const osmConnection = new OsmConnection({ singlePage: false, - oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token") - }); + oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token"), + }) new Combine([ - new Combine([Svg.robot_svg().SetClass("w-24 h-24 p-4 rounded-full subtle-background"), - new Combine([new Title("MapComplete Automaton", 1), - "This page helps to automate certain tasks for a theme. Expert use only." - ]).SetClass("flex flex-col m-4") + new Combine([ + Svg.robot_svg().SetClass("w-24 h-24 p-4 rounded-full subtle-background"), + new Combine([ + new Title("MapComplete Automaton", 1), + "This page helps to automate certain tasks for a theme. Expert use only.", + ]).SetClass("flex flex-col m-4"), ]).SetClass("flex"), new Toggle( AutomatonGui.GenerateMainPanel(), - new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() => osmConnection.AttemptLogin()), + new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() => + osmConnection.AttemptLogin() + ), osmConnection.isLoggedIn - )]).SetClass("block p-4") + ), + ]) + .SetClass("block p-4") .AttachTo("main") } - private static GenerateMainPanel(): BaseUIElement { - - const themeSelect = new DropDown("Select a theme", - Array.from(themeOverview).map(l => ({value: l.id, shown: l.id})) + const themeSelect = new DropDown( + "Select a theme", + Array.from(themeOverview).map((l) => ({ value: l.id, shown: l.id })) ) - LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith(themeSelect.GetValue()) + LocalStorageSource.Get("automation-theme-id", "missing_streets").syncWith( + themeSelect.GetValue() + ) const tilepath = ValidatedTextField.ForType("url").ConstructInputElement({ placeholder: "Specifiy the path of the overview", - inputStyle: "width: 100%" + inputStyle: "width: 100%", }) tilepath.SetClass("w-full") LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true) - - let tilesToRunOver = tilepath.GetValue().bind(path => { + let tilesToRunOver = tilepath.GetValue().bind((path) => { if (path === undefined) { return undefined } @@ -266,12 +337,11 @@ class AutomatonGui { const targetZoom = 14 - const tilesPerIndex = tilesToRunOver.map(tiles => { - + const tilesPerIndex = tilesToRunOver.map((tiles) => { if (tiles === undefined || tiles["error"] !== undefined) { return undefined } - let indexes: number[] = []; + let indexes: number[] = [] const tilesS = tiles["success"] DynamicGeoJsonTileSource.RegisterWhitelist(tilepath.GetValue().data, tilesS) const z = Number(tilesS["zoom"]) @@ -281,7 +351,7 @@ class AutomatonGui { } const x = Number(key) const ys = tilesS[key] - indexes.push(...ys.map(y => Tiles.tile_index(z, x, y))) + indexes.push(...ys.map((y) => Tiles.tile_index(z, x, y))) } console.log("Got ", indexes.length, "indexes") @@ -296,7 +366,6 @@ class AutomatonGui { rezoomed.add(Tiles.tile_index(z, x, y)) } - return Array.from(rezoomed) }) @@ -309,69 +378,99 @@ class AutomatonGui { tilepath, "Add an extra comment:", extraComment, - new VariableUiElement(extraComment.GetValue().map(c => "Your comment is " + (c?.length ?? 0) + "/200 characters long")).SetClass("subtle"), - new VariableUiElement(tilesToRunOver.map(t => { - if (t === undefined) { - return "No path given or still loading..." - } - if (t["error"] !== undefined) { - return new FixedUiElement("Invalid URL or data: " + t["error"]).SetClass("alert") - } - - return new FixedUiElement("Loaded " + tilesPerIndex.data.length + " tiles to automated over").SetClass("thanks") - })), - new VariableUiElement(themeSelect.GetValue().map(id => AllKnownLayouts.allKnownLayouts.get(id)).map(layoutToUse => { - if (layoutToUse === undefined) { - return new FixedUiElement("Select a valid layout") - } - if (tilesPerIndex.data === undefined || tilesPerIndex.data.length === 0) { - return "No tiles given" - } - - const automatableTagRenderings: { layer: LayerConfig, tagRendering: TagRenderingConfig }[] = [] - for (const layer of layoutToUse.layers) { - for (const tagRendering of layer.tagRenderings) { - if (tagRendering.group === "auto") { - automatableTagRenderings.push({layer, tagRendering: tagRendering}) - } + new VariableUiElement( + extraComment + .GetValue() + .map((c) => "Your comment is " + (c?.length ?? 0) + "/200 characters long") + ).SetClass("subtle"), + new VariableUiElement( + tilesToRunOver.map((t) => { + if (t === undefined) { + return "No path given or still loading..." } - } - console.log("Automatable tag renderings:", automatableTagRenderings) - if (automatableTagRenderings.length === 0) { - return new FixedUiElement('This theme does not have any tagRendering with "group": "auto" set').SetClass("alert") - } - const pickAuto = new DropDown("Pick the action to automate", - [ - { - value: undefined, - shown: "Pick an option" - }, - ...automatableTagRenderings.map(config => ( - { - shown: config.layer.id + " - " + config.tagRendering.id, - value: config + if (t["error"] !== undefined) { + return new FixedUiElement("Invalid URL or data: " + t["error"]).SetClass( + "alert" + ) + } + + return new FixedUiElement( + "Loaded " + tilesPerIndex.data.length + " tiles to automated over" + ).SetClass("thanks") + }) + ), + new VariableUiElement( + themeSelect + .GetValue() + .map((id) => AllKnownLayouts.allKnownLayouts.get(id)) + .map( + (layoutToUse) => { + if (layoutToUse === undefined) { + return new FixedUiElement("Select a valid layout") + } + if ( + tilesPerIndex.data === undefined || + tilesPerIndex.data.length === 0 + ) { + return "No tiles given" } - )) - ] - ) - - - return new Combine([ - pickAuto, - new VariableUiElement(pickAuto.GetValue().map(auto => auto === undefined ? undefined : new AutomationPanel(layoutToUse, tilesPerIndex.data, extraComment.GetValue(), auto)))]) - - }, [tilesPerIndex])).SetClass("flex flex-col") + const automatableTagRenderings: { + layer: LayerConfig + tagRendering: TagRenderingConfig + }[] = [] + for (const layer of layoutToUse.layers) { + for (const tagRendering of layer.tagRenderings) { + if (tagRendering.group === "auto") { + automatableTagRenderings.push({ + layer, + tagRendering: tagRendering, + }) + } + } + } + console.log("Automatable tag renderings:", automatableTagRenderings) + if (automatableTagRenderings.length === 0) { + return new FixedUiElement( + 'This theme does not have any tagRendering with "group": "auto" set' + ).SetClass("alert") + } + const pickAuto = new DropDown("Pick the action to automate", [ + { + value: undefined, + shown: "Pick an option", + }, + ...automatableTagRenderings.map((config) => ({ + shown: config.layer.id + " - " + config.tagRendering.id, + value: config, + })), + ]) + return new Combine([ + pickAuto, + new VariableUiElement( + pickAuto + .GetValue() + .map((auto) => + auto === undefined + ? undefined + : new AutomationPanel( + layoutToUse, + tilesPerIndex.data, + extraComment.GetValue(), + auto + ) + ) + ), + ]) + }, + [tilesPerIndex] + ) + ).SetClass("flex flex-col"), ]).SetClass("flex flex-col") - - } - } - MinimapImplementation.initialize() new AutomatonGui() - diff --git a/UI/Base/AsyncLazy.ts b/UI/Base/AsyncLazy.ts index 04b2e3166..d09c9769f 100644 --- a/UI/Base/AsyncLazy.ts +++ b/UI/Base/AsyncLazy.ts @@ -1,21 +1,21 @@ -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "./VariableUIElement"; -import {Stores, UIEventSource} from "../../Logic/UIEventSource"; -import Loading from "./Loading"; +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "./VariableUIElement" +import { Stores, UIEventSource } from "../../Logic/UIEventSource" +import Loading from "./Loading" export default class AsyncLazy extends BaseUIElement { - private readonly _f: () => Promise; + private readonly _f: () => Promise constructor(f: () => Promise) { - super(); - this._f = f; + super() + this._f = f } protected InnerConstructElement(): HTMLElement { // The caching of the BaseUIElement will guarantee that _f will only be called once return new VariableUiElement( - Stores.FromPromise(this._f()).map(el => { + Stores.FromPromise(this._f()).map((el) => { if (el === undefined) { return new Loading() } @@ -23,5 +23,4 @@ export default class AsyncLazy extends BaseUIElement { }) ).ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Base/Button.ts b/UI/Base/Button.ts index aa5fc299e..ea514be65 100644 --- a/UI/Base/Button.ts +++ b/UI/Base/Button.ts @@ -1,26 +1,25 @@ -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" export class Button extends BaseUIElement { - private _text: BaseUIElement; + private _text: BaseUIElement - constructor(text: string | BaseUIElement, onclick: (() => void | Promise)) { - super(); - this._text = Translations.W(text); + constructor(text: string | BaseUIElement, onclick: () => void | Promise) { + super() + this._text = Translations.W(text) this.onClick(onclick) } protected InnerConstructElement(): HTMLElement { - const el = this._text.ConstructElement(); + const el = this._text.ConstructElement() if (el === undefined) { - return undefined; + return undefined } const form = document.createElement("form") const button = document.createElement("button") button.type = "button" button.appendChild(el) form.appendChild(button) - return form; + return form } - -} \ No newline at end of file +} diff --git a/UI/Base/CenterFlexedElement.ts b/UI/Base/CenterFlexedElement.ts index 6546541b1..6e1af34b0 100644 --- a/UI/Base/CenterFlexedElement.ts +++ b/UI/Base/CenterFlexedElement.ts @@ -1,32 +1,32 @@ -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" export class CenterFlexedElement extends BaseUIElement { - private _html: string; + private _html: string constructor(html: string) { - super(); - this._html = html ?? ""; + super() + this._html = html ?? "" } InnerRender(): string { - return this._html; + return this._html } AsMarkdown(): string { - return this._html; + return this._html } protected InnerConstructElement(): HTMLElement { - const e = document.createElement("div"); - e.innerHTML = this._html; - e.style.display = "flex"; - e.style.height = "100%"; - e.style.width = "100%"; - e.style.flexDirection = "column"; - e.style.flexWrap = "nowrap"; - e.style.alignContent = "center"; - e.style.justifyContent = "center"; - e.style.alignItems = "center"; - return e; + const e = document.createElement("div") + e.innerHTML = this._html + e.style.display = "flex" + e.style.height = "100%" + e.style.width = "100%" + e.style.flexDirection = "column" + e.style.flexWrap = "nowrap" + e.style.alignContent = "center" + e.style.justifyContent = "center" + e.style.alignItems = "center" + return e } } diff --git a/UI/Base/ChartJs.ts b/UI/Base/ChartJs.ts index 670637334..051335e4c 100644 --- a/UI/Base/ChartJs.ts +++ b/UI/Base/ChartJs.ts @@ -1,35 +1,38 @@ -import BaseUIElement from "../BaseUIElement"; -import {Chart, ChartConfiguration, ChartType, DefaultDataPoint, registerables} from 'chart.js'; -Chart?.register(...(registerables ?? [])); - +import BaseUIElement from "../BaseUIElement" +import { Chart, ChartConfiguration, ChartType, DefaultDataPoint, registerables } from "chart.js" +Chart?.register(...(registerables ?? [])) export default class ChartJs< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown - > extends BaseUIElement{ - private readonly _config: ChartConfiguration; - +> extends BaseUIElement { + private readonly _config: ChartConfiguration + constructor(config: ChartConfiguration) { - super(); - this._config = config; + super() + this._config = config } - + protected InnerConstructElement(): HTMLElement { - const canvas = document.createElement("canvas"); + const canvas = document.createElement("canvas") // A bit exceptional: we apply the styles before giving them to 'chartJS' - if(this.style !== undefined){ + if (this.style !== undefined) { canvas.style.cssText = this.style } if (this.clss?.size > 0) { try { canvas.classList.add(...Array.from(this.clss)) } catch (e) { - console.error("Invalid class name detected in:", Array.from(this.clss).join(" "), "\nErr msg is ", e) + console.error( + "Invalid class name detected in:", + Array.from(this.clss).join(" "), + "\nErr msg is ", + e + ) } } - new Chart(canvas, this._config); - return canvas; + new Chart(canvas, this._config) + return canvas } - -} \ No newline at end of file +} diff --git a/UI/Base/Combine.ts b/UI/Base/Combine.ts index 5c3c8a1eb..2ef8b6585 100644 --- a/UI/Base/Combine.ts +++ b/UI/Base/Combine.ts @@ -1,31 +1,30 @@ -import {FixedUiElement} from "./FixedUiElement"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; +import { FixedUiElement } from "./FixedUiElement" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" export default class Combine extends BaseUIElement { - private readonly uiElements: BaseUIElement[]; + private readonly uiElements: BaseUIElement[] constructor(uiElements: (string | BaseUIElement)[]) { - super(); - this.uiElements = Utils.NoNull(uiElements) - .map(el => { - if (typeof el === "string") { - return new FixedUiElement(el); - } - return el; - }); + super() + this.uiElements = Utils.NoNull(uiElements).map((el) => { + if (typeof el === "string") { + return new FixedUiElement(el) + } + return el + }) } AsMarkdown(): string { - let sep = " "; + let sep = " " if (this.HasClass("flex-col")) { sep = "\n\n" } - return this.uiElements.map(el => el.AsMarkdown()).join(sep); + return this.uiElements.map((el) => el.AsMarkdown()).join(sep) } Destroy() { - super.Destroy(); + super.Destroy() for (const uiElement of this.uiElements) { uiElement.Destroy() } @@ -38,15 +37,17 @@ export default class Combine extends BaseUIElement { protected InnerConstructElement(): HTMLElement { const el = document.createElement("span") try { - if(this.uiElements === undefined){ - console.error("PANIC: this.uiElements is undefined. (This might indicate a constructor which did not call 'super'. The constructor name is", this.constructor/*Disable code quality: used for debugging*/.name+")") + if (this.uiElements === undefined) { + console.error( + "PANIC: this.uiElements is undefined. (This might indicate a constructor which did not call 'super'. The constructor name is", + this.constructor /*Disable code quality: used for debugging*/.name + ")" + ) } for (const subEl of this.uiElements) { if (subEl === undefined || subEl === null) { - continue; + continue } try { - const subHtml = subEl.ConstructElement() if (subHtml !== undefined) { el.appendChild(subHtml) @@ -58,11 +59,13 @@ export default class Combine extends BaseUIElement { } catch (e) { const domExc = e as DOMException console.error("DOMException: ", domExc.name) - el.appendChild(new FixedUiElement("Could not generate this combine!").SetClass("alert").ConstructElement()) + el.appendChild( + new FixedUiElement("Could not generate this combine!") + .SetClass("alert") + .ConstructElement() + ) } - return el; + return el } - - -} \ No newline at end of file +} diff --git a/UI/Base/FilteredCombine.ts b/UI/Base/FilteredCombine.ts index 06da9c88e..6ed315298 100644 --- a/UI/Base/FilteredCombine.ts +++ b/UI/Base/FilteredCombine.ts @@ -1,12 +1,11 @@ -import BaseUIElement from "../BaseUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {VariableUiElement} from "./VariableUIElement"; -import Combine from "./Combine"; -import Locale from "../i18n/Locale"; -import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import { VariableUiElement } from "./VariableUIElement" +import Combine from "./Combine" +import Locale from "../i18n/Locale" +import { Utils } from "../../Utils" export default class FilteredCombine extends VariableUiElement { - /** * Only shows item matching the search * If predicate of an item is undefined, it will be filtered out as soon as a non-null or non-empty search term is given @@ -14,27 +13,38 @@ export default class FilteredCombine extends VariableUiElement { * @param searchedValue * @param options */ - constructor(entries: { - element: BaseUIElement | string, - predicate?: (s: string) => boolean - }[], - searchedValue: UIEventSource, - options?: { - onEmpty?: BaseUIElement | string, - innerClasses: string - } + constructor( + entries: { + element: BaseUIElement | string + predicate?: (s: string) => boolean + }[], + searchedValue: UIEventSource, + options?: { + onEmpty?: BaseUIElement | string + innerClasses: string + } ) { entries = Utils.NoNull(entries) - super(searchedValue.map(searchTerm => { - if(searchTerm === undefined || searchTerm === ""){ - return new Combine(entries.map(e => e.element)).SetClass(options?.innerClasses ?? "") - } - const kept = entries.filter(entry => entry?.predicate !== undefined && entry.predicate(searchTerm)) - if (kept.length === 0) { - return options?.onEmpty - } - return new Combine(kept.map(entry => entry.element)).SetClass(options?.innerClasses ?? "") - }, [Locale.language])) + super( + searchedValue.map( + (searchTerm) => { + if (searchTerm === undefined || searchTerm === "") { + return new Combine(entries.map((e) => e.element)).SetClass( + options?.innerClasses ?? "" + ) + } + const kept = entries.filter( + (entry) => entry?.predicate !== undefined && entry.predicate(searchTerm) + ) + if (kept.length === 0) { + return options?.onEmpty + } + return new Combine(kept.map((entry) => entry.element)).SetClass( + options?.innerClasses ?? "" + ) + }, + [Locale.language] + ) + ) } - -} \ No newline at end of file +} diff --git a/UI/Base/FixedUiElement.ts b/UI/Base/FixedUiElement.ts index 1b2c7c4d3..0f9ddeec3 100644 --- a/UI/Base/FixedUiElement.ts +++ b/UI/Base/FixedUiElement.ts @@ -1,29 +1,27 @@ -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" export class FixedUiElement extends BaseUIElement { - public readonly content: string; + public readonly content: string constructor(html: string) { - super(); - this.content = html ?? ""; + super() + this.content = html ?? "" } InnerRender(): string { - return this.content; + return this.content } AsMarkdown(): string { - if(this.HasClass("code")){ - return "`"+this.content+"`" + if (this.HasClass("code")) { + return "`" + this.content + "`" } - return this.content; + return this.content } protected InnerConstructElement(): HTMLElement { const e = document.createElement("span") e.innerHTML = this.content - return e; + return e } - - -} \ No newline at end of file +} diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index ecaf09abd..6fcec946f 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -1,31 +1,34 @@ -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" export default class Img extends BaseUIElement { - - private readonly _src: string; - private readonly _rawSvg: boolean; - private readonly _options: { readonly fallbackImage?: string }; + private readonly _src: string + private readonly _rawSvg: boolean + private readonly _options: { readonly fallbackImage?: string } - constructor(src: string, rawSvg = false, options?: { - fallbackImage?: string - }) { - super(); + constructor( + src: string, + rawSvg = false, + options?: { + fallbackImage?: string + } + ) { + super() if (src === undefined || src === "undefined") { throw "Undefined src for image" } - this._src = src; - this._rawSvg = rawSvg; - this._options = options; + this._src = src + this._rawSvg = rawSvg + this._options = options } static AsData(source: string) { if (Utils.runningFromConsole) { - return source; + return source } - try{ - return `data:image/svg+xml;base64,${(btoa(source))}`; - }catch (e){ + try { + return `data:image/svg+xml;base64,${btoa(source)}` + } catch (e) { console.error("Cannot create an image for", source.slice(0, 100)) console.trace("Cannot create an image for the given source string due to ", e) return "" @@ -33,31 +36,31 @@ export default class Img extends BaseUIElement { } static AsImageElement(source: string, css_class: string = "", style = ""): string { - return ``; + return `` } AsMarkdown(): string { if (this._rawSvg === true) { - console.warn("Converting raw svgs to markdown is not supported"); + console.warn("Converting raw svgs to markdown is not supported") return undefined } let src = this._src if (this._src.startsWith("./")) { src = "https://mapcomplete.osm.be/" + src } - return "![](" + src + ")"; + return "![](" + src + ")" } protected InnerConstructElement(): HTMLElement { - const self = this; + const self = this if (this._rawSvg) { const e = document.createElement("div") e.innerHTML = this._src - return e; + return e } const el = document.createElement("img") - el.src = this._src; + el.src = this._src el.onload = () => { el.style.opacity = "1" } @@ -65,12 +68,11 @@ export default class Img extends BaseUIElement { if (self._options?.fallbackImage) { if (el.src === self._options.fallbackImage) { // Sigh... nothing to be done anymore - return; + return } el.src = self._options.fallbackImage } } - return el; + return el } } - diff --git a/UI/Base/Lazy.ts b/UI/Base/Lazy.ts index 1852099f8..c575e5a4e 100644 --- a/UI/Base/Lazy.ts +++ b/UI/Base/Lazy.ts @@ -1,16 +1,15 @@ -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" export default class Lazy extends BaseUIElement { - private readonly _f: () => BaseUIElement; + private readonly _f: () => BaseUIElement constructor(f: () => BaseUIElement) { - super(); - this._f = f; + super() + this._f = f } protected InnerConstructElement(): HTMLElement { // The caching of the BaseUIElement will guarantee that _f will only be called once - return this._f().ConstructElement(); + return this._f().ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Base/LeftIndex.ts b/UI/Base/LeftIndex.ts index 9cd61c6c2..989483ffc 100644 --- a/UI/Base/LeftIndex.ts +++ b/UI/Base/LeftIndex.ts @@ -1,25 +1,25 @@ -import BaseUIElement from "../BaseUIElement"; -import Combine from "./Combine"; -import BackToIndex from "../BigComponents/BackToIndex"; +import BaseUIElement from "../BaseUIElement" +import Combine from "./Combine" +import BackToIndex from "../BigComponents/BackToIndex" export default class LeftIndex extends Combine { - - - constructor(leftContents: BaseUIElement[], mainContent: BaseUIElement, options?: { - hideBackButton: false | boolean - }) { - - let back: BaseUIElement = undefined; + constructor( + leftContents: BaseUIElement[], + mainContent: BaseUIElement, + options?: { + hideBackButton: false | boolean + } + ) { + let back: BaseUIElement = undefined if (options?.hideBackButton ?? true) { back = new BackToIndex() } super([ - new Combine([ - new Combine([back, ...leftContents]).SetClass("sticky top-4"), - ]).SetClass("ml-4 block w-full md:w-2/6 lg:w-1/6"), - mainContent.SetClass("m-8 w-full mb-24") + new Combine([new Combine([back, ...leftContents]).SetClass("sticky top-4")]).SetClass( + "ml-4 block w-full md:w-2/6 lg:w-1/6" + ), + mainContent.SetClass("m-8 w-full mb-24"), ]) this.SetClass("h-full block md:flex") } - -} \ No newline at end of file +} diff --git a/UI/Base/Link.ts b/UI/Base/Link.ts index af2a79b35..76c025dc6 100644 --- a/UI/Base/Link.ts +++ b/UI/Base/Link.ts @@ -1,59 +1,64 @@ -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; - +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" export default class Link extends BaseUIElement { - private readonly _href: string | Store; - private readonly _embeddedShow: BaseUIElement; - private readonly _newTab: boolean; + private readonly _href: string | Store + private readonly _embeddedShow: BaseUIElement + private readonly _newTab: boolean - constructor(embeddedShow: BaseUIElement | string, href: string | Store, newTab: boolean = false) { - super(); - this._embeddedShow = Translations.W(embeddedShow); - this._href = href; - this._newTab = newTab; + constructor( + embeddedShow: BaseUIElement | string, + href: string | Store, + newTab: boolean = false + ) { + super() + this._embeddedShow = Translations.W(embeddedShow) + this._href = href + this._newTab = newTab if (this._embeddedShow === undefined) { throw "Error: got a link where embeddedShow is undefined" } this.onClick(() => {}) - } public static OsmWiki(key: string, value?: string, hideKey = false) { if (value !== undefined) { - let k = ""; + let k = "" if (!hideKey) { k = key + "=" } - return new Link(k + value, `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`, true) + return new Link( + k + value, + `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`, + true + ) } return new Link(key, "https://wiki.openstreetmap.org/wiki/Key:" + key, true) } AsMarkdown(): string { // @ts-ignore - return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})`; + return `[${this._embeddedShow.AsMarkdown()}](${this._href.data ?? this._href})` } protected InnerConstructElement(): HTMLElement { - const embeddedShow = this._embeddedShow?.ConstructElement(); + const embeddedShow = this._embeddedShow?.ConstructElement() if (embeddedShow === undefined) { - return undefined; + return undefined } const el = document.createElement("a") if (typeof this._href === "string") { el.href = this._href } else { - this._href.addCallbackAndRun(href => { - el.href = href; + this._href.addCallbackAndRun((href) => { + el.href = href }) } if (this._newTab) { el.target = "_blank" } el.appendChild(embeddedShow) - return el; + return el } - -} \ No newline at end of file +} diff --git a/UI/Base/LinkToWeblate.ts b/UI/Base/LinkToWeblate.ts index 4b2eb56f5..bb078ed56 100644 --- a/UI/Base/LinkToWeblate.ts +++ b/UI/Base/LinkToWeblate.ts @@ -1,46 +1,63 @@ -import {VariableUiElement} from "./VariableUIElement"; -import Locale from "../i18n/Locale"; -import Link from "./Link"; -import Svg from "../../Svg"; +import { VariableUiElement } from "./VariableUIElement" +import Locale from "../i18n/Locale" +import Link from "./Link" +import Svg from "../../Svg" export default class LinkToWeblate extends VariableUiElement { - private static URI: any; + private static URI: any constructor(context: string, availableTranslations: object) { - super( Locale.language.map(ln => { - if (Locale.showLinkToWeblate.data === false) { - return undefined; - } - if(availableTranslations["*"] !== undefined){ - return undefined - } - if(context === undefined || context.indexOf(":") < 0){ - return undefined - } - const icon = Svg.translate_svg() - .SetClass("rounded-full border border-gray-400 inline-block w-4 h-4 m-1 weblate-link self-center") - if(availableTranslations[ln] === undefined){ - icon.SetClass("bg-red-400") - } - return new Link(icon, - LinkToWeblate.hrefToWeblate(ln, context), true) - } ,[Locale.showLinkToWeblate])); + super( + Locale.language.map( + (ln) => { + if (Locale.showLinkToWeblate.data === false) { + return undefined + } + if (availableTranslations["*"] !== undefined) { + return undefined + } + if (context === undefined || context.indexOf(":") < 0) { + return undefined + } + const icon = Svg.translate_svg().SetClass( + "rounded-full border border-gray-400 inline-block w-4 h-4 m-1 weblate-link self-center" + ) + if (availableTranslations[ln] === undefined) { + icon.SetClass("bg-red-400") + } + return new Link(icon, LinkToWeblate.hrefToWeblate(ln, context), true) + }, + [Locale.showLinkToWeblate] + ) + ) this.SetClass("enable-links hidden-on-mobile") } - - public static hrefToWeblate(language: string, contextKey: string): string{ - if(contextKey === undefined || contextKey.indexOf(":") < 0){ + + public static hrefToWeblate(language: string, contextKey: string): string { + if (contextKey === undefined || contextKey.indexOf(":") < 0) { return undefined } const [category, ...rest] = contextKey.split(":") const key = rest.join(":") - + const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/" return baseUrl + category + "/" + language + "/?offset=1&q=context%3A%3D%22" + key + "%22" } - public static hrefToWeblateZen(language: string, category: "core" | "themes" | "layers" | "shared-questions" | "glossary" | string, searchKey: string): string{ + public static hrefToWeblateZen( + language: string, + category: "core" | "themes" | "layers" | "shared-questions" | "glossary" | string, + searchKey: string + ): string { const baseUrl = "https://hosted.weblate.org/zen/mapcomplete/" // ?offset=1&q=+state%3A%3Ctranslated+context%3Acampersite&sort_by=-priority%2Cposition&checksum= - return baseUrl + category + "/" + language + "?offset=1&q=+state%3A%3Ctranslated+context%3A"+encodeURIComponent(searchKey)+"&sort_by=-priority%2Cposition&checksum=" + return ( + baseUrl + + category + + "/" + + language + + "?offset=1&q=+state%3A%3Ctranslated+context%3A" + + encodeURIComponent(searchKey) + + "&sort_by=-priority%2Cposition&checksum=" + ) } -} \ No newline at end of file +} diff --git a/UI/Base/List.ts b/UI/Base/List.ts index b2e4e8f26..79ca8ee6d 100644 --- a/UI/Base/List.ts +++ b/UI/Base/List.ts @@ -1,24 +1,34 @@ -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import Translations from "../i18n/Translations"; +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import Translations from "../i18n/Translations" export default class List extends BaseUIElement { - private readonly uiElements: BaseUIElement[]; - private readonly _ordered: boolean; + private readonly uiElements: BaseUIElement[] + private readonly _ordered: boolean constructor(uiElements: (string | BaseUIElement)[], ordered = false) { - super(); - this._ordered = ordered; - this.uiElements = Utils.NoNull(uiElements) - .map(s => Translations.W(s)); + super() + this._ordered = ordered + this.uiElements = Utils.NoNull(uiElements).map((s) => Translations.W(s)) } AsMarkdown(): string { if (this._ordered) { - return "\n\n" + this.uiElements.map((el, i) => " " + i + ". " + el.AsMarkdown().replace(/\n/g, ' \n')).join("\n") + "\n" + return ( + "\n\n" + + this.uiElements + .map((el, i) => " " + i + ". " + el.AsMarkdown().replace(/\n/g, " \n")) + .join("\n") + + "\n" + ) } else { - return "\n\n" + this.uiElements.map(el => " - " + el.AsMarkdown().replace(/\n/g, ' \n')).join("\n") + "\n" - + return ( + "\n\n" + + this.uiElements + .map((el) => " - " + el.AsMarkdown().replace(/\n/g, " \n")) + .join("\n") + + "\n" + ) } } @@ -27,7 +37,7 @@ export default class List extends BaseUIElement { for (const subEl of this.uiElements) { if (subEl === undefined || subEl === null) { - continue; + continue } const subHtml = subEl.ConstructElement() if (subHtml !== undefined) { @@ -37,7 +47,6 @@ export default class List extends BaseUIElement { } } - return el; + return el } - -} \ No newline at end of file +} diff --git a/UI/Base/Loading.ts b/UI/Base/Loading.ts index 835670534..b6e0b577a 100644 --- a/UI/Base/Loading.ts +++ b/UI/Base/Loading.ts @@ -1,18 +1,18 @@ -import Combine from "./Combine"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; +import Combine from "./Combine" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" export default class Loading extends Combine { constructor(msg?: BaseUIElement | string) { - const t = Translations.W(msg) ?? Translations.t.general.loading; + const t = Translations.W(msg) ?? Translations.t.general.loading t.SetClass("pl-2") super([ Svg.loading_svg() .SetClass("animate-spin self-center") .SetStyle("width: 1.5rem; height: 1.5rem; min-width: 1.5rem;"), - t + t, ]) this.SetClass("flex p-1") } -} \ No newline at end of file +} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 03480a61c..9ae16a770 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -1,31 +1,30 @@ -import BaseUIElement from "../BaseUIElement"; -import Loc from "../../Models/Loc"; -import BaseLayer from "../../Models/BaseLayer"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {BBox} from "../../Logic/BBox"; +import BaseUIElement from "../BaseUIElement" +import Loc from "../../Models/Loc" +import BaseLayer from "../../Models/BaseLayer" +import { UIEventSource } from "../../Logic/UIEventSource" +import { BBox } from "../../Logic/BBox" export interface MinimapOptions { - background?: UIEventSource, - location?: UIEventSource, - bounds?: UIEventSource, - allowMoving?: boolean, - leafletOptions?: any, - attribution?: BaseUIElement | boolean, - onFullyLoaded?: (leaflet: L.Map) => void, - leafletMap?: UIEventSource, - lastClickLocation?: UIEventSource<{ lat: number, lon: number }>, + background?: UIEventSource + location?: UIEventSource + bounds?: UIEventSource + allowMoving?: boolean + leafletOptions?: any + attribution?: BaseUIElement | boolean + onFullyLoaded?: (leaflet: L.Map) => void + leafletMap?: UIEventSource + lastClickLocation?: UIEventSource<{ lat: number; lon: number }> addLayerControl?: boolean | false } export interface MinimapObj { - readonly leafletMap: UIEventSource, - readonly location: UIEventSource; - readonly bounds: UIEventSource; + readonly leafletMap: UIEventSource + readonly location: UIEventSource + readonly bounds: UIEventSource installBounds(factor: number | BBox, showRange?: boolean): void - TakeScreenshot(): Promise; - + TakeScreenshot(): Promise } export default class Minimap { @@ -34,15 +33,12 @@ export default class Minimap { * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it */ - private constructor() { - } + private constructor() {} /** * Construct a minimap */ - public static createMiniMap: (options?: MinimapOptions) => (BaseUIElement & MinimapObj) = (_) => { + public static createMiniMap: (options?: MinimapOptions) => BaseUIElement & MinimapObj = (_) => { throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" } - - -} \ No newline at end of file +} diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 3972b4d12..602148fab 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -1,67 +1,67 @@ -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Loc from "../../Models/Loc"; -import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import * as L from "leaflet"; -import {Map} from "leaflet"; -import Minimap, {MinimapObj, MinimapOptions} from "./Minimap"; -import {BBox} from "../../Logic/BBox"; -import 'leaflet-polylineoffset' -import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; -import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; -import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"; +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Loc from "../../Models/Loc" +import BaseLayer from "../../Models/BaseLayer" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import * as L from "leaflet" +import { Map } from "leaflet" +import Minimap, { MinimapObj, MinimapOptions } from "./Minimap" +import { BBox } from "../../Logic/BBox" +import "leaflet-polylineoffset" +import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter" +import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" +import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation" export default class MinimapImplementation extends BaseUIElement implements MinimapObj { - private static _nextId = 0; + private static _nextId = 0 public readonly leafletMap: UIEventSource - public readonly location: UIEventSource; - public readonly bounds: UIEventSource | undefined; - private readonly _id: string; - private readonly _background: UIEventSource; - private _isInited = false; - private _allowMoving: boolean; - private readonly _leafletoptions: any; + public readonly location: UIEventSource + public readonly bounds: UIEventSource | undefined + private readonly _id: string + private readonly _background: UIEventSource + private _isInited = false + private _allowMoving: boolean + private readonly _leafletoptions: any private readonly _onFullyLoaded: (leaflet: L.Map) => void - private readonly _attribution: BaseUIElement | boolean; - private readonly _addLayerControl: boolean; - private readonly _options: MinimapOptions; + private readonly _attribution: BaseUIElement | boolean + private readonly _addLayerControl: boolean + private readonly _options: MinimapOptions private constructor(options?: MinimapOptions) { super() options = options ?? {} - this._id = "minimap" + MinimapImplementation._nextId; + this._id = "minimap" + MinimapImplementation._nextId MinimapImplementation._nextId++ this.leafletMap = options.leafletMap ?? new UIEventSource(undefined) - this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) - this.location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) - this.bounds = options?.bounds; - this._allowMoving = options.allowMoving ?? true; + this._background = + options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) + this.location = options?.location ?? new UIEventSource({ lat: 0, lon: 0, zoom: 1 }) + this.bounds = options?.bounds + this._allowMoving = options.allowMoving ?? true this._leafletoptions = options.leafletOptions ?? {} this._onFullyLoaded = options.onFullyLoaded this._attribution = options.attribution this._addLayerControl = options.addLayerControl ?? false this._options = options this.SetClass("relative") - } public static initialize() { AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) - Minimap.createMiniMap = options => new MinimapImplementation(options) - ShowDataLayer.actualContstructor = options => new ShowDataLayerImplementation(options) + Minimap.createMiniMap = (options) => new MinimapImplementation(options) + ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options) } public installBounds(factor: number | BBox, showRange?: boolean) { - this.leafletMap.addCallbackD(leaflet => { - let bounds: { getEast(), getNorth(), getWest(), getSouth() }; + this.leafletMap.addCallbackD((leaflet) => { + let bounds: { getEast(); getNorth(); getWest(); getSouth() } if (typeof factor === "number") { const lbounds = leaflet.getBounds().pad(factor) leaflet.setMaxBounds(lbounds) - bounds = lbounds; + bounds = lbounds } else { // @ts-ignore leaflet.setMaxBounds(factor.toLeaflet()) @@ -71,50 +71,37 @@ export default class MinimapImplementation extends BaseUIElement implements Mini if (showRange) { const data = { type: "FeatureCollection", - features: [{ - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [ - bounds.getEast(), - bounds.getNorth() - ], - [ - bounds.getWest(), - bounds.getNorth() - ], - [ - bounds.getWest(), - bounds.getSouth() - ], + features: [ + { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [bounds.getEast(), bounds.getNorth()], + [bounds.getWest(), bounds.getNorth()], + [bounds.getWest(), bounds.getSouth()], - [ - bounds.getEast(), - bounds.getSouth() + [bounds.getEast(), bounds.getSouth()], + [bounds.getEast(), bounds.getNorth()], ], - [ - bounds.getEast(), - bounds.getNorth() - ] - ] - } - }] + }, + }, + ], } // @ts-ignore L.geoJSON(data, { style: { color: "#f44", weight: 4, - opacity: 0.7 - } + opacity: 0.7, + }, }).addTo(leaflet) } }) } Destroy() { - super.Destroy(); + super.Destroy() console.warn("Decomissioning minimap", this._id) const mp = this.leafletMap.data this.leafletMap.setData(null) @@ -123,14 +110,14 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } public async TakeScreenshot() { - const screenshotter = new SimpleMapScreenshoter(); - screenshotter.addTo(this.leafletMap.data); - return await screenshotter.takeScreen('image') + const screenshotter = new SimpleMapScreenshoter() + screenshotter.addTo(this.leafletMap.data) + return await screenshotter.takeScreen("image") } protected InnerConstructElement(): HTMLElement { const div = document.createElement("div") - div.id = this._id; + div.id = this._id div.style.height = "100%" div.style.width = "100%" div.style.minWidth = "40px" @@ -138,19 +125,21 @@ export default class MinimapImplementation extends BaseUIElement implements Mini div.style.position = "relative" const wrapper = document.createElement("div") wrapper.appendChild(div) - const self = this; + const self = this // @ts-ignore - const resizeObserver = new ResizeObserver(_ => { + const resizeObserver = new ResizeObserver((_) => { if (wrapper.clientHeight === 0 || wrapper.clientWidth === 0) { - return; + return } - if (wrapper.offsetParent === null || window.getComputedStyle(wrapper).display === 'none') { + if ( + wrapper.offsetParent === null || + window.getComputedStyle(wrapper).display === "none" + ) { // Not visible - return; + return } try { - self.InitMap(); - + self.InitMap() } catch (e) { console.warn("Could not construct a minimap:", e) } @@ -160,41 +149,41 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } catch (e) { console.warn("Could not invalidate size of a minimap:", e) } - }); + }) - resizeObserver.observe(div); + resizeObserver.observe(div) if (this._addLayerControl) { - const switcher = new BackgroundMapSwitch({ + const switcher = new BackgroundMapSwitch( + { locationControl: this.location, - backgroundLayer: this._background + backgroundLayer: this._background, }, this._background ).SetClass("top-0 right-0 z-above-map absolute") wrapper.appendChild(switcher.ConstructElement()) } - return wrapper; - + return wrapper } private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet - return; + return } if (document.getElementById(this._id) === null) { // not yet attached, we probably got some other event - return; + return } if (this._isInited) { - return; + return } - this._isInited = true; - const location = this.location; - const self = this; + this._isInited = true + const location = this.location + const self = this let currentLayer = this._background.data.layer() let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0] if (isNaN(latLon[0]) || isNaN(latLon[1])) { @@ -213,22 +202,20 @@ export default class MinimapImplementation extends BaseUIElement implements Mini touchZoom: this._allowMoving, // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, fadeAnimation: this._allowMoving, - maxZoom: 21 + maxZoom: 21, } - Utils.Merge(this._leafletoptions, options) /* - * Somehow, the element gets '_leaflet_id' set on chrome. - * When attempting to init this leaflet map, it'll throw an exception and the map won't show up. - * Simply removing '_leaflet_id' fixes the issue. - * See https://github.com/pietervdvn/MapComplete/issues/726 - * */ + * Somehow, the element gets '_leaflet_id' set on chrome. + * When attempting to init this leaflet map, it'll throw an exception and the map won't show up. + * Simply removing '_leaflet_id' fixes the issue. + * See https://github.com/pietervdvn/MapComplete/issues/726 + * */ delete document.getElementById(this._id)["_leaflet_id"] - const map = L.map(this._id, options); + const map = L.map(this._id, options) if (self._onFullyLoaded !== undefined) { - currentLayer.on("load", () => { console.log("Fully loaded all tiles!") self._onFullyLoaded(map) @@ -239,95 +226,90 @@ export default class MinimapImplementation extends BaseUIElement implements Mini // We give a bit of leeway for people on the edges // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/ - map.setMaxBounds( - [[-100, -200], [100, 200]] - ); + map.setMaxBounds([ + [-100, -200], + [100, 200], + ]) if (this._attribution !== undefined) { if (this._attribution === true) { map.attributionControl.setPrefix(false) } else { - map.attributionControl.setPrefix( - ""); + map.attributionControl.setPrefix("") } } - this._background.addCallbackAndRun(layer => { + this._background.addCallbackAndRun((layer) => { const newLayer = layer.layer() if (currentLayer !== undefined) { - map.removeLayer(currentLayer); + map.removeLayer(currentLayer) } - currentLayer = newLayer; + currentLayer = newLayer if (self._onFullyLoaded !== undefined) { - currentLayer.on("load", () => { console.log("Fully loaded all tiles!") self._onFullyLoaded(map) }) } - map.addLayer(newLayer); + map.addLayer(newLayer) if (self._attribution !== true && self._attribution !== false) { - self._attribution?.AttachTo('leaflet-attribution') + self._attribution?.AttachTo("leaflet-attribution") } - }) - - let isRecursing = false; + let isRecursing = false map.on("moveend", function () { if (isRecursing) { return } - if (map.getZoom() === location.data.zoom && + if ( + map.getZoom() === location.data.zoom && map.getCenter().lat === location.data.lat && - map.getCenter().lng === location.data.lon) { - return; + map.getCenter().lng === location.data.lon + ) { + return } - location.data.zoom = map.getZoom(); - location.data.lat = map.getCenter().lat; - location.data.lon = map.getCenter().lng; - isRecursing = true; - location.ping(); + location.data.zoom = map.getZoom() + location.data.lat = map.getCenter().lat + location.data.lon = map.getCenter().lng + isRecursing = true + location.ping() if (self.bounds !== undefined) { self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) } - - isRecursing = false; // This is ugly, I know + isRecursing = false // This is ugly, I know }) - - location.addCallback(loc => { + location.addCallback((loc) => { const mapLoc = map.getCenter() const dlat = Math.abs(loc.lat - mapLoc[0]) const dlon = Math.abs(loc.lon - mapLoc[1]) if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { - return; + return } map.setView([loc.lat, loc.lon], loc.zoom) }) - if (self.bounds !== undefined) { self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) } - if (this._options.lastClickLocation) { const lastClickLocation = this._options.lastClickLocation map.on("click", function (e) { // @ts-ignore - lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}) - }); + lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng }) + }) map.on("contextmenu", function (e) { // @ts-ignore - lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); - }); + lastClickLocation?.setData({ lat: e.latlng.lat, lon: e.latlng.lng }) + }) } this.leafletMap.setData(map) } -} \ No newline at end of file +} diff --git a/UI/Base/Paragraph.ts b/UI/Base/Paragraph.ts index da5714257..4a8d4546a 100644 --- a/UI/Base/Paragraph.ts +++ b/UI/Base/Paragraph.ts @@ -1,33 +1,30 @@ -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" export class Paragraph extends BaseUIElement { - public readonly content: (string | BaseUIElement); + public readonly content: string | BaseUIElement - constructor(html: (string | BaseUIElement)) { - super(); - this.content = html ?? ""; + constructor(html: string | BaseUIElement) { + super() + this.content = html ?? "" } - AsMarkdown(): string { - let c:string ; - if(typeof this.content !== "string"){ - c = this.content.AsMarkdown() - }else{ - c = this.content - } - return "\n\n"+c+"\n\n" + let c: string + if (typeof this.content !== "string") { + c = this.content.AsMarkdown() + } else { + c = this.content + } + return "\n\n" + c + "\n\n" } protected InnerConstructElement(): HTMLElement { const e = document.createElement("p") - if(typeof this.content !== "string"){ + if (typeof this.content !== "string") { e.appendChild(this.content.ConstructElement()) - }else{ + } else { e.innerHTML = this.content } - return e; + return e } - - -} \ No newline at end of file +} diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 2e15e95e2..24d98922d 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -1,11 +1,11 @@ -import {UIElement} from "../UIElement"; -import Svg from "../../Svg"; -import Combine from "./Combine"; -import {FixedUiElement} from "./FixedUiElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Hash from "../../Logic/Web/Hash"; -import BaseUIElement from "../BaseUIElement"; -import Title from "./Title"; +import { UIElement } from "../UIElement" +import Svg from "../../Svg" +import Combine from "./Combine" +import { FixedUiElement } from "./FixedUiElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Hash from "../../Logic/Web/Hash" +import BaseUIElement from "../BaseUIElement" +import Title from "./Title" /** * @@ -17,99 +17,107 @@ import Title from "./Title"; * */ export default class ScrollableFullScreen extends UIElement { - private static readonly empty = new FixedUiElement(""); - private static _currentlyOpen: ScrollableFullScreen; - public isShown: UIEventSource; - private hashToShow: string; - private _component: BaseUIElement; - private _fullscreencomponent: BaseUIElement; - private _resetScrollSignal: UIEventSource = new UIEventSource(undefined); + private static readonly empty = new FixedUiElement("") + private static _currentlyOpen: ScrollableFullScreen + public isShown: UIEventSource + private hashToShow: string + private _component: BaseUIElement + private _fullscreencomponent: BaseUIElement + private _resetScrollSignal: UIEventSource = new UIEventSource(undefined) - constructor(title: ((options: { mode: string }) => BaseUIElement), - content: ((options: { mode: string, resetScrollSignal: UIEventSource }) => BaseUIElement), - hashToShow: string, - isShown: UIEventSource = new UIEventSource(false), - options?: { - setHash?: true | boolean - } + constructor( + title: (options: { mode: string }) => BaseUIElement, + content: (options: { + mode: string + resetScrollSignal: UIEventSource + }) => BaseUIElement, + hashToShow: string, + isShown: UIEventSource = new UIEventSource(false), + options?: { + setHash?: true | boolean + } ) { - super(); - this.hashToShow = hashToShow; - this.isShown = isShown; + super() + this.hashToShow = hashToShow + this.isShown = isShown if (hashToShow === undefined) { throw "HashToShow should be defined as it is vital for the 'back' key functionality" } - + const desktopOptions = { mode: "desktop", - resetScrollSignal: this._resetScrollSignal + resetScrollSignal: this._resetScrollSignal, } - + const mobileOptions = { mode: "mobile", - resetScrollSignal: this._resetScrollSignal + resetScrollSignal: this._resetScrollSignal, } - this._component = this.BuildComponent(title(desktopOptions), content(desktopOptions)) .SetClass("hidden md:block"); - this._fullscreencomponent = this.BuildComponent(title(mobileOptions), content(mobileOptions).SetClass("pb-20")); + this._component = this.BuildComponent( + title(desktopOptions), + content(desktopOptions) + ).SetClass("hidden md:block") + this._fullscreencomponent = this.BuildComponent( + title(mobileOptions), + content(mobileOptions).SetClass("pb-20") + ) - - const self = this; - const setHash = options?.setHash ?? true; - if(setHash){ - Hash.hash.addCallback(h => { + const self = this + const setHash = options?.setHash ?? true + if (setHash) { + Hash.hash.addCallback((h) => { if (h === undefined) { isShown.setData(false) } }) } - isShown.addCallback(isShown => { + isShown.addCallback((isShown) => { if (isShown) { // We first must set the hash, then activate the panel // If the order is wrong, this will cause the panel to disactivate again - if(setHash){ + if (setHash) { Hash.hash.setData(hashToShow) } - self.Activate(); + self.Activate() } else { // Some cleanup... - const fs = document.getElementById("fullscreen"); + const fs = document.getElementById("fullscreen") if (fs !== null) { ScrollableFullScreen.empty.AttachTo("fullscreen") fs.classList.add("hidden") } - ScrollableFullScreen._currentlyOpen?.isShown?.setData(false); + ScrollableFullScreen._currentlyOpen?.isShown?.setData(false) } }) } InnerRender(): BaseUIElement { - return this._component; + return this._component } Destroy() { - super.Destroy(); + super.Destroy() this._component.Destroy() this._fullscreencomponent.Destroy() } Activate(): void { this.isShown.setData(true) - this._fullscreencomponent.AttachTo("fullscreen"); - const fs = document.getElementById("fullscreen"); - ScrollableFullScreen._currentlyOpen = this; + this._fullscreencomponent.AttachTo("fullscreen") + const fs = document.getElementById("fullscreen") + ScrollableFullScreen._currentlyOpen = this fs.classList.remove("hidden") } - private BuildComponent(title: BaseUIElement, content: BaseUIElement) :BaseUIElement { - const returnToTheMap = - new Combine([ - Svg.back_svg().SetClass("block md:hidden w-12 h-12 p-2 svg-foreground"), - Svg.close_svg() .SetClass("hidden md:block w-12 h-12 p-3 svg-foreground") - ]).SetClass("rounded-full p-0 flex-shrink-0 self-center") + private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement { + const returnToTheMap = new Combine([ + Svg.back_svg().SetClass("block md:hidden w-12 h-12 p-2 svg-foreground"), + Svg.close_svg().SetClass("hidden md:block w-12 h-12 p-3 svg-foreground"), + ]).SetClass("rounded-full p-0 flex-shrink-0 self-center") returnToTheMap.onClick(() => { this.isShown.setData(false) @@ -117,24 +125,28 @@ export default class ScrollableFullScreen extends UIElement { }) title = new Title(title, 2) - title.SetClass("text-l sm:text-xl md:text-2xl w-full p-0 max-h-20vh overflow-y-auto self-center") - - const contentWrapper = new Combine([content]) - .SetClass("block p-2 md:pt-4 w-full h-full overflow-y-auto desktop:max-h-65vh") - - this._resetScrollSignal.addCallback(_ => { - contentWrapper.ScrollToTop(); + title.SetClass( + "text-l sm:text-xl md:text-2xl w-full p-0 max-h-20vh overflow-y-auto self-center" + ) + + const contentWrapper = new Combine([content]).SetClass( + "block p-2 md:pt-4 w-full h-full overflow-y-auto desktop:max-h-65vh" + ) + + this._resetScrollSignal.addCallback((_) => { + contentWrapper.ScrollToTop() }) - - return new Combine([ + + return new Combine([ new Combine([ - new Combine([returnToTheMap, title]) - .SetClass("border-b-1 border-black shadow bg-white flex flex-shrink-0 pt-1 pb-1 md:pt-0 md:pb-0"), - contentWrapper , + new Combine([returnToTheMap, title]).SetClass( + "border-b-1 border-black shadow bg-white flex flex-shrink-0 pt-1 pb-1 md:pt-0 md:pb-0" + ), + contentWrapper, // We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide - ]).SetClass("flex flex-col h-full relative bg-white") - ]).SetClass("fixed top-0 left-0 right-0 h-screen w-screen desktop:max-h-65vh md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden"); - + ]).SetClass("flex flex-col h-full relative bg-white"), + ]).SetClass( + "fixed top-0 left-0 right-0 h-screen w-screen desktop:max-h-65vh md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden" + ) } - -} \ No newline at end of file +} diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 87206e13f..90242fbc9 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -1,88 +1,85 @@ -import Translations from "../i18n/Translations"; -import Combine from "./Combine"; -import BaseUIElement from "../BaseUIElement"; -import Link from "./Link"; -import Img from "./Img"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {UIElement} from "../UIElement"; -import {VariableUiElement} from "./VariableUIElement"; -import Lazy from "./Lazy"; -import Loading from "./Loading"; - +import Translations from "../i18n/Translations" +import Combine from "./Combine" +import BaseUIElement from "../BaseUIElement" +import Link from "./Link" +import Img from "./Img" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { UIElement } from "../UIElement" +import { VariableUiElement } from "./VariableUIElement" +import Lazy from "./Lazy" +import Loading from "./Loading" export class SubtleButton extends UIElement { - private readonly imageUrl: string | BaseUIElement; - private readonly message: string | BaseUIElement; - private readonly options: { url?: string | Store; newTab?: boolean ; imgSize?: string}; + private readonly imageUrl: string | BaseUIElement + private readonly message: string | BaseUIElement + private readonly options: { url?: string | Store; newTab?: boolean; imgSize?: string } - - constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, options: { - url?: string | Store, - newTab?: boolean, - imgSize?: "h-11 w-11" | string - } = undefined) { - super(); - this.imageUrl = imageUrl; - this.message = message; - this.options = options; + constructor( + imageUrl: string | BaseUIElement, + message: string | BaseUIElement, + options: { + url?: string | Store + newTab?: boolean + imgSize?: "h-11 w-11" | string + } = undefined + ) { + super() + this.imageUrl = imageUrl + this.message = message + this.options = options } protected InnerRender(): string | BaseUIElement { - const classes = "block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline"; - const message = Translations.W(this.message)?.SetClass("block text-ellipsis no-images flex-shrink"); - let img; - const imgClasses = "block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11") + const classes = + "block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline" + const message = Translations.W(this.message)?.SetClass( + "block text-ellipsis no-images flex-shrink" + ) + let img + const imgClasses = + "block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11") if ((this.imageUrl ?? "") === "") { - img = undefined; - } else if (typeof (this.imageUrl) === "string") { + img = undefined + } else if (typeof this.imageUrl === "string") { img = new Img(this.imageUrl)?.SetClass(imgClasses) } else { - img = this.imageUrl?.SetClass(imgClasses); + img = this.imageUrl?.SetClass(imgClasses) } - const button = new Combine([ - img, - message - ]).SetClass("flex items-center group w-full") + const button = new Combine([img, message]).SetClass("flex items-center group w-full") - if (this.options?.url == undefined) { this.SetClass(classes) return button } - - return new Link( - button, - this.options.url, - this.options.newTab ?? false - ).SetClass(classes) - + return new Link(button, this.options.url, this.options.newTab ?? false).SetClass(classes) } public OnClickWithLoading( loadingText: BaseUIElement | string, - action: () => Promise ) : BaseUIElement{ + action: () => Promise + ): BaseUIElement { const state = new UIEventSource<"idle" | "running">("idle") - const button = this; - - button.onClick(async() => { + const button = this + + button.onClick(async () => { state.setData("running") - try{ - await action() - }catch(e){ + try { + await action() + } catch (e) { console.error(e) - }finally { + } finally { state.setData("idle") } - }) - const loading = new Lazy(() => new Loading(loadingText) ) - return new VariableUiElement(state.map(st => { - if(st === "idle"){ - return button - } - return loading - })) + const loading = new Lazy(() => new Loading(loadingText)) + return new VariableUiElement( + state.map((st) => { + if (st === "idle") { + return button + } + return loading + }) + ) } - -} \ No newline at end of file +} diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index d931cf475..59e64c5b5 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -1,26 +1,29 @@ -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "./Combine"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "./VariableUIElement"; +import Translations from "../i18n/Translations" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "./Combine" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "./VariableUIElement" export class TabbedComponent extends Combine { - - constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], - openedTab: (UIEventSource | number) = 0, - options?: { - leftOfHeader?: BaseUIElement - styleHeader?: (header: BaseUIElement) => void - }) { - - const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource(0)) + constructor( + elements: { header: BaseUIElement | string; content: BaseUIElement | string }[], + openedTab: UIEventSource | number = 0, + options?: { + leftOfHeader?: BaseUIElement + styleHeader?: (header: BaseUIElement) => void + } + ) { + const openedTabSrc = + typeof openedTab === "number" + ? new UIEventSource(openedTab) + : openedTab ?? new UIEventSource(0) const tabs: BaseUIElement[] = [options?.leftOfHeader] - const contentElements: BaseUIElement[] = []; + const contentElements: BaseUIElement[] = [] for (let i = 0; i < elements.length; i++) { - let element = elements[i]; + let element = elements[i] const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i)) - openedTabSrc.addCallbackAndRun(selected => { + openedTabSrc.addCallbackAndRun((selected) => { if (selected >= elements.length) { selected = 0 } @@ -34,7 +37,7 @@ export class TabbedComponent extends Combine { }) const content = Translations.W(element.content) content.SetClass("relative w-full inline-block") - contentElements.push(content); + contentElements.push(content) const tab = header.SetClass("block tab-single-header") tabs.push(tab) } @@ -44,10 +47,8 @@ export class TabbedComponent extends Combine { options.styleHeader(header) } const actualContent = new VariableUiElement( - openedTabSrc.map(i => contentElements[i]) + openedTabSrc.map((i) => contentElements[i]) ).SetStyle("max-height: inherit; height: inherit") super([header, actualContent]) - } - -} \ No newline at end of file +} diff --git a/UI/Base/Table.ts b/UI/Base/Table.ts index 3d8cf16b7..1fd2d1edc 100644 --- a/UI/Base/Table.ts +++ b/UI/Base/Table.ts @@ -1,34 +1,36 @@ -import BaseUIElement from "../BaseUIElement"; -import {Utils} from "../../Utils"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement" +import { Utils } from "../../Utils" +import Translations from "../i18n/Translations" +import { UIEventSource } from "../../Logic/UIEventSource" export default class Table extends BaseUIElement { + private readonly _header: BaseUIElement[] + private readonly _contents: BaseUIElement[][] + private readonly _contentStyle: string[][] + private readonly _sortable: boolean - private readonly _header: BaseUIElement[]; - private readonly _contents: BaseUIElement[][]; - private readonly _contentStyle: string[][]; - private readonly _sortable: boolean; - - constructor(header: (BaseUIElement | string)[], - contents: (BaseUIElement | string)[][], - options?: { - contentStyle?: string[][], - sortable?: false | boolean - }) { - super(); - this._contentStyle = options?.contentStyle ?? [["min-width: 9rem"]]; - this._header = header?.map(Translations.W); - this._contents = contents.map(row => row.map(Translations.W)); + constructor( + header: (BaseUIElement | string)[], + contents: (BaseUIElement | string)[][], + options?: { + contentStyle?: string[][] + sortable?: false | boolean + } + ) { + super() + this._contentStyle = options?.contentStyle ?? [["min-width: 9rem"]] + this._header = header?.map(Translations.W) + this._contents = contents.map((row) => row.map(Translations.W)) this._sortable = options?.sortable ?? false } AsMarkdown(): string { - - const headerMarkdownParts = this._header.map(hel => hel?.AsMarkdown() ?? " ") - const header = headerMarkdownParts.join(" | "); - const headerSep = headerMarkdownParts.map(part => '-'.repeat(part.length + 2)).join(" | ") - const table = this._contents.map(row => row.map(el => el.AsMarkdown() ?? " ").join(" | ")).join("\n") + const headerMarkdownParts = this._header.map((hel) => hel?.AsMarkdown() ?? " ") + const header = headerMarkdownParts.join(" | ") + const headerSep = headerMarkdownParts.map((part) => "-".repeat(part.length + 2)).join(" | ") + const table = this._contents + .map((row) => row.map((el) => el.AsMarkdown() ?? " ").join(" | ")) + .join("\n") return "\n\n" + [header, headerSep, table, ""].join("\n") } @@ -37,58 +39,61 @@ export default class Table extends BaseUIElement { const table = document.createElement("table") /** - * Sortmode: i: sort column i ascending; + * Sortmode: i: sort column i ascending; * if i is negative : sort column (-i - 1) descending */ - const sortmode = new UIEventSource(undefined); - const self = this; - const headerElems = Utils.NoNull((this._header ?? []).map((elem, i) => { - if(self._sortable){ - elem.onClick(() => { - const current = sortmode.data - if(current == i){ - sortmode.setData(- 1 - i ) - }else{ - sortmode.setData(i) - } - }) - } - return elem.ConstructElement(); - })) + const sortmode = new UIEventSource(undefined) + const self = this + const headerElems = Utils.NoNull( + (this._header ?? []).map((elem, i) => { + if (self._sortable) { + elem.onClick(() => { + const current = sortmode.data + if (current == i) { + sortmode.setData(-1 - i) + } else { + sortmode.setData(i) + } + }) + } + return elem.ConstructElement() + }) + ) if (headerElems.length > 0) { - const thead = document.createElement("thead") - const tr = document.createElement("tr"); - headerElems.forEach(headerElem => { + const tr = document.createElement("tr") + headerElems.forEach((headerElem) => { const td = document.createElement("th") td.appendChild(headerElem) tr.appendChild(td) }) thead.appendChild(tr) table.appendChild(thead) - } for (let i = 0; i < this._contents.length; i++) { - let row = this._contents[i]; + let row = this._contents[i] const tr = document.createElement("tr") for (let j = 0; j < row.length; j++) { try { - - let elem = row[j]; + let elem = row[j] const htmlElem = elem?.ConstructElement() if (htmlElem === undefined) { - continue; + continue } - let style = undefined; - if (this._contentStyle !== undefined && this._contentStyle[i] !== undefined && this._contentStyle[j] !== undefined) { + let style = undefined + if ( + this._contentStyle !== undefined && + this._contentStyle[i] !== undefined && + this._contentStyle[j] !== undefined + ) { style = this._contentStyle[i][j] } const td = document.createElement("td") - td.style.cssText = style; + td.style.cssText = style td.appendChild(htmlElem) tr.appendChild(td) } catch (e) { @@ -97,33 +102,31 @@ export default class Table extends BaseUIElement { } table.appendChild(tr) } - - sortmode.addCallback(sortCol => { - if(sortCol === undefined){ + + sortmode.addCallback((sortCol) => { + if (sortCol === undefined) { return } const descending = sortCol < 0 - const col = descending ? - sortCol - 1: sortCol; + const col = descending ? -sortCol - 1 : sortCol let rows: HTMLTableRowElement[] = Array.from(table.rows) - rows.splice(0,1) // remove header row + rows.splice(0, 1) // remove header row rows = rows.sort((a, b) => { const ac = a.cells[col]?.textContent?.toLowerCase() const bc = b.cells[col]?.textContent?.toLowerCase() - if(ac === bc){ + if (ac === bc) { return 0 } - return( ac < bc !== descending) ? -1 : 1; + return ac < bc !== descending ? -1 : 1 }) - for (let j = rows.length ; j > 1; j--) { + for (let j = rows.length; j > 1; j--) { table.deleteRow(j) } for (const row of rows) { table.appendChild(row) } }) - - return table; + return table } - -} \ No newline at end of file +} diff --git a/UI/Base/TableOfContents.ts b/UI/Base/TableOfContents.ts index d0e083f5b..7c93ca68e 100644 --- a/UI/Base/TableOfContents.ts +++ b/UI/Base/TableOfContents.ts @@ -1,21 +1,23 @@ -import Combine from "./Combine"; -import BaseUIElement from "../BaseUIElement"; -import {Translation} from "../i18n/Translation"; -import {FixedUiElement} from "./FixedUiElement"; -import Title from "./Title"; -import List from "./List"; -import Hash from "../../Logic/Web/Hash"; -import Link from "./Link"; -import {Utils} from "../../Utils"; +import Combine from "./Combine" +import BaseUIElement from "../BaseUIElement" +import { Translation } from "../i18n/Translation" +import { FixedUiElement } from "./FixedUiElement" +import Title from "./Title" +import List from "./List" +import Hash from "../../Logic/Web/Hash" +import Link from "./Link" +import { Utils } from "../../Utils" export default class TableOfContents extends Combine { - private readonly titles: Title[] - constructor(elements: Combine | Title[], options?: { - noTopLevel: false | boolean, - maxDepth?: number - }) { + constructor( + elements: Combine | Title[], + options?: { + noTopLevel: false | boolean + maxDepth?: number + } + ) { let titles: Title[] if (elements instanceof Combine) { titles = TableOfContents.getTitles(elements.getElements()) ?? [] @@ -23,7 +25,7 @@ export default class TableOfContents extends Combine { titles = elements ?? [] } - let els: { level: number, content: BaseUIElement }[] = [] + let els: { level: number; content: BaseUIElement }[] = [] for (const title of titles) { let content: BaseUIElement if (title.title instanceof Translation) { @@ -41,29 +43,27 @@ export default class TableOfContents extends Combine { const vis = new Link(content, "#" + title.id) - Hash.hash.addCallbackAndRun(h => { + Hash.hash.addCallbackAndRun((h) => { if (h === title.id) { vis.SetClass("font-bold") } else { vis.RemoveClass("font-bold") } }) - els.push({level: title.level, content: vis}) - + els.push({ level: title.level, content: vis }) } - const minLevel = Math.min(...els.map(e => e.level)) + const minLevel = Math.min(...els.map((e) => e.level)) if (options?.noTopLevel) { - els = els.filter(e => e.level !== minLevel) + els = els.filter((e) => e.level !== minLevel) } if (options?.maxDepth) { - els = els.filter(e => e.level <= (options.maxDepth + minLevel)) + els = els.filter((e) => e.level <= options.maxDepth + minLevel) } - - super(TableOfContents.mergeLevel(els).map(el => el.SetClass("mt-2"))); + super(TableOfContents.mergeLevel(els).map((el) => el.SetClass("mt-2"))) this.SetClass("flex flex-col") - this.titles = titles; + this.titles = titles } private static getTitles(elements: BaseUIElement[]): Title[] { @@ -78,13 +78,15 @@ export default class TableOfContents extends Combine { return titles } - private static mergeLevel(elements: { level: number, content: BaseUIElement }[]): BaseUIElement[] { - const maxLevel = Math.max(...elements.map(e => e.level)) - const minLevel = Math.min(...elements.map(e => e.level)) + private static mergeLevel( + elements: { level: number; content: BaseUIElement }[] + ): BaseUIElement[] { + const maxLevel = Math.max(...elements.map((e) => e.level)) + const minLevel = Math.min(...elements.map((e) => e.level)) if (maxLevel === minLevel) { - return elements.map(e => e.content) + return elements.map((e) => e.content) } - const result: { level: number, content: BaseUIElement } [] = [] + const result: { level: number; content: BaseUIElement }[] = [] let running: BaseUIElement[] = [] for (const element of elements) { if (element.level === maxLevel) { @@ -94,7 +96,7 @@ export default class TableOfContents extends Combine { if (running.length !== undefined) { result.push({ content: new List(running), - level: maxLevel - 1 + level: maxLevel - 1, }) running = [] } @@ -103,7 +105,7 @@ export default class TableOfContents extends Combine { if (running.length !== undefined) { result.push({ content: new List(running), - level: maxLevel - 1 + level: maxLevel - 1, }) } @@ -112,8 +114,8 @@ export default class TableOfContents extends Combine { AsMarkdown(): string { const depthIcons = ["1.", " -", " +", " *"] - const lines = ["## Table of contents\n"]; - const minLevel = Math.min(...this.titles.map(t => t.level)) + const lines = ["## Table of contents\n"] + const minLevel = Math.min(...this.titles.map((t) => t.level)) for (const title of this.titles) { const prefix = depthIcons[title.level - minLevel] ?? " ~" const text = title.title.AsMarkdown().replace("\n", "") @@ -123,4 +125,4 @@ export default class TableOfContents extends Combine { return lines.join("\n") + "\n\n" } -} \ No newline at end of file +} diff --git a/UI/Base/Title.ts b/UI/Base/Title.ts index 669cbd896..c30ca144c 100644 --- a/UI/Base/Title.ts +++ b/UI/Base/Title.ts @@ -1,11 +1,17 @@ -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "./FixedUiElement"; -import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement" +import { FixedUiElement } from "./FixedUiElement" +import { Utils } from "../../Utils" export default class Title extends BaseUIElement { - private static readonly defaultClassesPerLevel = ["", "text-3xl font-bold", "text-2xl font-bold", "text-xl font-bold", "text-lg font-bold"] - public readonly title: BaseUIElement; - public readonly level: number; + private static readonly defaultClassesPerLevel = [ + "", + "text-3xl font-bold", + "text-2xl font-bold", + "text-xl font-bold", + "text-lg font-bold", + ] + public readonly title: BaseUIElement + public readonly level: number public readonly id: string constructor(embedded: string | BaseUIElement, level: number = 3) { @@ -18,9 +24,9 @@ export default class Title extends BaseUIElement { } else { this.title = embedded } - this.level = level; + this.level = level - let text: string = undefined; + let text: string = undefined if (typeof embedded === "string") { text = embedded } else if (embedded instanceof FixedUiElement) { @@ -31,14 +37,16 @@ export default class Title extends BaseUIElement { } } - this.id = text?.replace(/ /g, '-') - ?.replace(/[?#.;:/]/, "") - ?.toLowerCase() ?? "" + this.id = + text + ?.replace(/ /g, "-") + ?.replace(/[?#.;:/]/, "") + ?.toLowerCase() ?? "" this.SetClass(Title.defaultClassesPerLevel[level] ?? "") } AsMarkdown(): string { - const embedded = " " + this.title.AsMarkdown() + " "; + const embedded = " " + this.title.AsMarkdown() + " " if (this.level == 1) { return "\n\n" + embedded + "\n" + "=".repeat(embedded.length) + "\n\n" @@ -48,17 +56,17 @@ export default class Title extends BaseUIElement { return "\n\n" + embedded + "\n" + "-".repeat(embedded.length) + "\n\n" } - return "\n\n" + "#".repeat(this.level) + embedded + "\n\n"; + return "\n\n" + "#".repeat(this.level) + embedded + "\n\n" } protected InnerConstructElement(): HTMLElement { const el = this.title.ConstructElement() if (el === undefined) { - return undefined; + return undefined } const h = document.createElement("h" + this.level) h.appendChild(el) el.id = this.id - return h; + return h } -} \ No newline at end of file +} diff --git a/UI/Base/Toggleable.ts b/UI/Base/Toggleable.ts index 5b884fa6b..9ec1820cd 100644 --- a/UI/Base/Toggleable.ts +++ b/UI/Base/Toggleable.ts @@ -1,34 +1,36 @@ -import BaseUIElement from "../BaseUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "./Combine"; -import Title from "./Title"; -import Hash from "../../Logic/Web/Hash"; +import BaseUIElement from "../BaseUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "./Combine" +import Title from "./Title" +import Hash from "../../Logic/Web/Hash" export class Accordeon extends Combine { - constructor(toggles: Toggleable[]) { - for (const el of toggles) { - el.isVisible.addCallbackAndRun(isVisible => toggles.forEach(toggle => { - if (toggle !== el && isVisible) { - toggle.isVisible.setData(false) - } - })) + el.isVisible.addCallbackAndRun((isVisible) => + toggles.forEach((toggle) => { + if (toggle !== el && isVisible) { + toggle.isVisible.setData(false) + } + }) + ) } - super(toggles); + super(toggles) } - } - export default class Toggleable extends Combine { public readonly isVisible = new UIEventSource(false) - constructor(title: Title | Combine | BaseUIElement, content: BaseUIElement, options?: { - closeOnClick?: true | boolean, - height?: "100vh" | string - }) { + constructor( + title: Title | Combine | BaseUIElement, + content: BaseUIElement, + options?: { + closeOnClick?: true | boolean + height?: "100vh" | string + } + ) { super([title, content]) content.SetClass("animate-height border-l-4 pl-2 block") title.SetClass("background-subtle rounded-lg") @@ -47,14 +49,14 @@ export default class Toggleable extends Combine { if (title instanceof Combine) { for (const el of title.getElements()) { if (el instanceof Title) { - title = el; - break; + title = el + break } } } if (title instanceof Title) { - Hash.hash.addCallbackAndRun(h => { + Hash.hash.addCallbackAndRun((h) => { if (h === (title).id) { self.isVisible.setData(true) content.RemoveClass("border-gray-300") @@ -64,20 +66,21 @@ export default class Toggleable extends Combine { content.RemoveClass("border-red-300") } }) - this.isVisible.addCallbackAndRun(isVis => { + this.isVisible.addCallbackAndRun((isVis) => { if (isVis) { Hash.hash.setData((<Title>title).id) } }) } - this.isVisible.addCallbackAndRun(isVisible => { + this.isVisible.addCallbackAndRun((isVisible) => { if (isVisible) { contentElement.style.maxHeight = options?.height ?? "100vh" contentElement.style.overflowY = "auto" contentElement.style["-webkit-mask-image"] = "unset" } else { - contentElement.style["-webkit-mask-image"] = "-webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))" + contentElement.style["-webkit-mask-image"] = + "-webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))" contentElement.style.maxHeight = "2rem" } }) @@ -85,7 +88,6 @@ export default class Toggleable extends Combine { public Collapse(): Toggleable { this.isVisible.setData(false) - return this; + return this } - -} \ No newline at end of file +} diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index 5093b85d1..e4cfc4c4f 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -1,24 +1,24 @@ -import {Store} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import Combine from "./Combine"; +import { Store } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import Combine from "./Combine" export class VariableUiElement extends BaseUIElement { - private readonly _contents?: Store<string | BaseUIElement | BaseUIElement[]>; + private readonly _contents?: Store<string | BaseUIElement | BaseUIElement[]> constructor(contents?: Store<string | BaseUIElement | BaseUIElement[]>) { - super(); - this._contents = contents; + super() + this._contents = contents } Destroy() { - super.Destroy(); - this.isDestroyed = true; + super.Destroy() + this.isDestroyed = true } AsMarkdown(): string { - const d = this._contents?.data; + const d = this._contents?.data if (typeof d === "string") { - return d; + return d } if (d instanceof BaseUIElement) { return d.AsMarkdown() @@ -27,36 +27,36 @@ export class VariableUiElement extends BaseUIElement { } protected InnerConstructElement(): HTMLElement { - const el = document.createElement("span"); - const self = this; + const el = document.createElement("span") + const self = this this._contents?.addCallbackAndRun((contents) => { if (self.isDestroyed) { - return true; + return true } - + while (el.firstChild) { - el.removeChild(el.lastChild); + el.removeChild(el.lastChild) } if (contents === undefined) { return } if (typeof contents === "string") { - el.innerHTML = contents; + el.innerHTML = contents } else if (contents instanceof Array) { for (const content of contents) { - const c = content?.ConstructElement(); + const c = content?.ConstructElement() if (c !== undefined && c !== null) { - el.appendChild(c); + el.appendChild(c) } } } else { - const c = contents.ConstructElement(); + const c = contents.ConstructElement() if (c !== undefined && c !== null) { - el.appendChild(c); + el.appendChild(c) } } - }); - return el; + }) + return el } } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index af75841bc..ce720f957 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -4,38 +4,37 @@ * Assumes a read-only configuration, so it has no 'ListenTo' */ export default abstract class BaseUIElement { + protected _constructedHtmlElement: HTMLElement + protected isDestroyed = false + protected readonly clss: Set<string> = new Set<string>() + protected style: string + private _onClick: () => void | Promise<void> - protected _constructedHtmlElement: HTMLElement; - protected isDestroyed = false; - protected readonly clss: Set<string> = new Set<string>(); - protected style: string; - private _onClick: () => void | Promise<void>; - - public onClick(f: (() => void)) { - this._onClick = f; + public onClick(f: () => void) { + this._onClick = f this.SetClass("clickable") if (this._constructedHtmlElement !== undefined) { - this._constructedHtmlElement.onclick = f; + this._constructedHtmlElement.onclick = f } - return this; + return this } AttachTo(divId: string) { - let element = document.getElementById(divId); + let element = document.getElementById(divId) if (element === null) { - throw "SEVERE: could not attach UIElement to " + divId; + throw "SEVERE: could not attach UIElement to " + divId } while (element.firstChild) { //The list is LIVE so it will re-index each call - element.removeChild(element.firstChild); + element.removeChild(element.firstChild) } - const el = this.ConstructElement(); + const el = this.ConstructElement() if (el !== undefined) { element.appendChild(el) } - return this; + return this } public ScrollToTop() { @@ -49,34 +48,34 @@ export default abstract class BaseUIElement { if (clss == undefined) { return this } - const all = clss.split(" ").map(clsName => clsName.trim()); - let recordedChange = false; + const all = clss.split(" ").map((clsName) => clsName.trim()) + let recordedChange = false for (let c of all) { - c = c.trim(); + c = c.trim() if (this.clss.has(clss)) { - continue; + continue } if (c === undefined || c === "") { - continue; + continue } - this.clss.add(c); - recordedChange = true; + this.clss.add(c) + recordedChange = true } if (recordedChange) { - this._constructedHtmlElement?.classList.add(...Array.from(this.clss)); + this._constructedHtmlElement?.classList.add(...Array.from(this.clss)) } - return this; + return this } public RemoveClass(classes: string): BaseUIElement { - const all = classes.split(" ").map(clsName => clsName.trim()); + const all = classes.split(" ").map((clsName) => clsName.trim()) for (let clss of all) { if (this.clss.has(clss)) { - this.clss.delete(clss); + this.clss.delete(clss) this._constructedHtmlElement?.classList.remove(clss) } } - return this; + return this } public HasClass(clss: string): boolean { @@ -84,11 +83,11 @@ export default abstract class BaseUIElement { } public SetStyle(style: string): BaseUIElement { - this.style = style; + this.style = style if (this._constructedHtmlElement !== undefined) { - this._constructedHtmlElement.style.cssText = style; + this._constructedHtmlElement.style.cssText = style } - return this; + return this } /** @@ -96,7 +95,7 @@ export default abstract class BaseUIElement { */ public ConstructElement(): HTMLElement { if (typeof window === undefined) { - return undefined; + return undefined } if (this._constructedHtmlElement !== undefined) { @@ -104,13 +103,13 @@ export default abstract class BaseUIElement { } try { - const el = this.InnerConstructElement(); + const el = this.InnerConstructElement() if (el === undefined) { - return undefined; + return undefined } - this._constructedHtmlElement = el; + this._constructedHtmlElement = el const style = this.style if (style !== undefined && style !== "") { el.style.cssText = style @@ -119,30 +118,35 @@ export default abstract class BaseUIElement { try { el.classList.add(...Array.from(this.clss)) } catch (e) { - console.error("Invalid class name detected in:", Array.from(this.clss).join(" "), "\nErr msg is ", e) + console.error( + "Invalid class name detected in:", + Array.from(this.clss).join(" "), + "\nErr msg is ", + e + ) } } if (this._onClick !== undefined) { - const self = this; + const self = this el.onclick = async (e) => { // @ts-ignore if (e.consumed) { - return; + return } - const v = self._onClick(); - if(typeof v === "object"){ + const v = self._onClick() + if (typeof v === "object") { await v } // @ts-ignore - e.consumed = true; + e.consumed = true } - el.classList.add("pointer-events-none", "cursor-pointer"); + el.classList.add("pointer-events-none", "cursor-pointer") } return el } catch (e) { - const domExc = e as DOMException; + const domExc = e as DOMException if (domExc) { console.log("An exception occured", domExc.code, domExc.message, domExc.name) } @@ -155,8 +159,8 @@ export default abstract class BaseUIElement { } public Destroy() { - this.isDestroyed = true; + this.isDestroyed = true } - protected abstract InnerConstructElement(): HTMLElement; + protected abstract InnerConstructElement(): HTMLElement } diff --git a/UI/BigComponents/AddNewMarker.ts b/UI/BigComponents/AddNewMarker.ts index 112cd3d9e..6c3fe93d6 100644 --- a/UI/BigComponents/AddNewMarker.ts +++ b/UI/BigComponents/AddNewMarker.ts @@ -1,64 +1,75 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import Svg from "../../Svg"; +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import { VariableUiElement } from "../Base/VariableUIElement" +import FilteredLayer from "../../Models/FilteredLayer" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import Svg from "../../Svg" /** * The icon with the 'plus'-sign and the preset icons spinning - * + * */ export default class AddNewMarker extends Combine { - constructor(filteredLayers: UIEventSource<FilteredLayer[]>) { - const icons = new VariableUiElement(filteredLayers.map(filteredLayers => { - const icons = [] - let last = undefined; - for (const filteredLayer of filteredLayers) { - const layer = filteredLayer.layerDef; - if(layer.name === undefined && !filteredLayer.isDisplayed.data){ - continue - } - for (const preset of filteredLayer.layerDef.presets) { - const tags = TagUtils.KVtoProperties(preset.tags) - const icon = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html - .SetClass("block relative") - .SetStyle("width: 42px; height: 42px;"); - icons.push(icon) - if (last === undefined) { - last = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html - .SetClass("block relative") - .SetStyle("width: 42px; height: 42px;"); + const icons = new VariableUiElement( + filteredLayers.map((filteredLayers) => { + const icons = [] + let last = undefined + for (const filteredLayer of filteredLayers) { + const layer = filteredLayer.layerDef + if (layer.name === undefined && !filteredLayer.isDisplayed.data) { + continue + } + for (const preset of filteredLayer.layerDef.presets) { + const tags = TagUtils.KVtoProperties(preset.tags) + const icon = layer.mapRendering[0] + .GenerateLeafletStyle(new UIEventSource<any>(tags), false) + .html.SetClass("block relative") + .SetStyle("width: 42px; height: 42px;") + icons.push(icon) + if (last === undefined) { + last = layer.mapRendering[0] + .GenerateLeafletStyle(new UIEventSource<any>(tags), false) + .html.SetClass("block relative") + .SetStyle("width: 42px; height: 42px;") + } } } - } - if (icons.length === 0) { - return undefined - } - if (icons.length === 1) { - return icons[0] - } - icons.push(last) - const elem = new Combine(icons).SetClass("flex") - elem.SetClass("slide min-w-min").SetStyle("animation: slide " + icons.length + "s linear infinite;") - return elem; - })) - const label = Translations.t.general.add.addNewMapLabel.Clone() - .SetClass("block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap") + if (icons.length === 0) { + return undefined + } + if (icons.length === 1) { + return icons[0] + } + icons.push(last) + const elem = new Combine(icons).SetClass("flex") + elem.SetClass("slide min-w-min").SetStyle( + "animation: slide " + icons.length + "s linear infinite;" + ) + return elem + }) + ) + const label = Translations.t.general.add.addNewMapLabel + .Clone() + .SetClass( + "block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap" + ) .SetStyle("top: 65px; transform: translateX(-50%)") super([ new Combine([ - Svg.add_pin_svg().SetClass("absolute").SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"), + Svg.add_pin_svg() + .SetClass("absolute") + .SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"), new Combine([icons]) .SetStyle("width: 50px") .SetClass("absolute p-1 rounded-full overflow-hidden"), - Svg.addSmall_svg().SetClass("absolute animate-pulse").SetStyle("width: 30px; left: 30px; top: 35px;") + Svg.addSmall_svg() + .SetClass("absolute animate-pulse") + .SetStyle("width: 30px; left: 30px; top: 35px;"), ]).SetClass("absolute"), - new Combine([label]).SetStyle("position: absolute; left: 50%") + new Combine([label]).SetStyle("position: absolute; left: 50%"), ]) - this.SetClass("block relative"); + this.SetClass("block relative") } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/AllDownloads.ts b/UI/BigComponents/AllDownloads.ts index 2d2fc6698..09c2e33df 100644 --- a/UI/BigComponents/AllDownloads.ts +++ b/UI/BigComponents/AllDownloads.ts @@ -1,88 +1,86 @@ -import Combine from "../Base/Combine"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "../Input/Toggle"; -import {DownloadPanel} from "./DownloadPanel"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import ExportPDF from "../ExportPDF"; -import FilteredLayer from "../../Models/FilteredLayer"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {BBox} from "../../Logic/BBox"; -import BaseLayer from "../../Models/BaseLayer"; -import Loc from "../../Models/Loc"; +import Combine from "../Base/Combine" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import Translations from "../i18n/Translations" +import { UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import Toggle from "../Input/Toggle" +import { DownloadPanel } from "./DownloadPanel" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import ExportPDF from "../ExportPDF" +import FilteredLayer from "../../Models/FilteredLayer" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { BBox } from "../../Logic/BBox" +import BaseLayer from "../../Models/BaseLayer" +import Loc from "../../Models/Loc" -interface DownloadState { +interface DownloadState { filteredLayers: UIEventSource<FilteredLayer[]> - featurePipeline: FeaturePipeline, - layoutToUse: LayoutConfig, - currentBounds: UIEventSource<BBox>, - backgroundLayer:UIEventSource<BaseLayer>, - locationControl: UIEventSource<Loc>, - featureSwitchExportAsPdf: UIEventSource<boolean>, - featureSwitchEnableExport: UIEventSource<boolean>, + featurePipeline: FeaturePipeline + layoutToUse: LayoutConfig + currentBounds: UIEventSource<BBox> + backgroundLayer: UIEventSource<BaseLayer> + locationControl: UIEventSource<Loc> + featureSwitchExportAsPdf: UIEventSource<boolean> + featureSwitchEnableExport: UIEventSource<boolean> } - - export default class AllDownloads extends ScrollableFullScreen { - - constructor(isShown: UIEventSource<boolean>,state: { - filteredLayers: UIEventSource<FilteredLayer[]> - featurePipeline: FeaturePipeline, - layoutToUse: LayoutConfig, - currentBounds: UIEventSource<BBox>, - backgroundLayer:UIEventSource<BaseLayer>, - locationControl: UIEventSource<Loc>, - featureSwitchExportAsPdf: UIEventSource<boolean>, - featureSwitchEnableExport: UIEventSource<boolean>, - }) { - super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown); + constructor( + isShown: UIEventSource<boolean>, + state: { + filteredLayers: UIEventSource<FilteredLayer[]> + featurePipeline: FeaturePipeline + layoutToUse: LayoutConfig + currentBounds: UIEventSource<BBox> + backgroundLayer: UIEventSource<BaseLayer> + locationControl: UIEventSource<Loc> + featureSwitchExportAsPdf: UIEventSource<boolean> + featureSwitchEnableExport: UIEventSource<boolean> + } + ) { + super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown) } private static GenTitle(): BaseUIElement { return Translations.t.general.download.title .Clone() - .SetClass("text-2xl break-words font-bold p-2"); + .SetClass("text-2xl break-words font-bold p-2") } private static GeneratePanel(state: DownloadState): BaseUIElement { - const isExporting = new UIEventSource(false, "Pdf-is-exporting") const generatePdf = () => { isExporting.setData(true) - new ExportPDF( - { - freeDivId: "belowmap", - background: state.backgroundLayer, - location: state.locationControl, - features: state.featurePipeline, - layout: state.layoutToUse, - }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning)) + new ExportPDF({ + freeDivId: "belowmap", + background: state.backgroundLayer, + location: state.locationControl, + features: state.featurePipeline, + layout: state.layoutToUse, + }).isRunning.addCallbackAndRun((isRunning) => isExporting.setData(isRunning)) } - const loading = Svg.loading_svg().SetClass("animate-rotate"); + const loading = Svg.loading_svg().SetClass("animate-rotate") const dloadTrans = Translations.t.general.download - const icon = new Toggle(loading, Svg.floppy_ui(), isExporting); + const icon = new Toggle(loading, Svg.floppy_ui(), isExporting) const text = new Toggle( dloadTrans.exporting.Clone(), new Combine([ dloadTrans.downloadAsPdf.Clone().SetClass("font-bold"), - dloadTrans.downloadAsPdfHelper.Clone()] - ).SetClass("flex flex-col") + dloadTrans.downloadAsPdfHelper.Clone(), + ]) + .SetClass("flex flex-col") .onClick(() => { generatePdf() }), - isExporting); + isExporting + ) const pdf = new Toggle( - new SubtleButton( - icon, - text), + new SubtleButton(icon, text), undefined, state.featureSwitchExportAsPdf @@ -94,6 +92,6 @@ export default class AllDownloads extends ScrollableFullScreen { state.featureSwitchEnableExport ) - return new Combine([pdf, exportPanel]).SetClass("flex flex-col"); + return new Combine([pdf, exportPanel]).SetClass("flex flex-col") } } diff --git a/UI/BigComponents/Attribution.ts b/UI/BigComponents/Attribution.ts index e2f6b6562..3b60134f2 100644 --- a/UI/BigComponents/Attribution.ts +++ b/UI/BigComponents/Attribution.ts @@ -1,64 +1,85 @@ -import Link from "../Base/Link"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import UserDetails from "../../Logic/Osm/OsmConnection"; -import Constants from "../../Models/Constants"; -import Loc from "../../Models/Loc"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {BBox} from "../../Logic/BBox"; -import {Utils} from "../../Utils"; +import Link from "../Base/Link" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import { UIEventSource } from "../../Logic/UIEventSource" +import UserDetails from "../../Logic/Osm/OsmConnection" +import Constants from "../../Models/Constants" +import Loc from "../../Models/Loc" +import { VariableUiElement } from "../Base/VariableUIElement" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { BBox } from "../../Logic/BBox" +import { Utils } from "../../Utils" /** * The bottom right attribution panel in the leaflet map */ export default class Attribution extends Combine { + constructor( + location: UIEventSource<Loc>, + userDetails: UIEventSource<UserDetails>, + layoutToUse: LayoutConfig, + currentBounds: UIEventSource<BBox> + ) { + const mapComplete = new Link( + `Mapcomplete ${Constants.vNumber}`, + "https://github.com/pietervdvn/MapComplete", + true + ) + const reportBug = new Link( + Svg.bug_ui().SetClass("small-image"), + "https://github.com/pietervdvn/MapComplete/issues", + true + ) - constructor(location: UIEventSource<Loc>, - userDetails: UIEventSource<UserDetails>, - layoutToUse: LayoutConfig, - currentBounds: UIEventSource<BBox>) { + const layoutId = layoutToUse?.id + const stats = new Link( + Svg.statistics_ui().SetClass("small-image"), + Utils.OsmChaLinkFor(31, layoutId), + true + ) - const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true); - const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true); - - const layoutId = layoutToUse?.id; - const stats = new Link(Svg.statistics_ui().SetClass("small-image"), Utils.OsmChaLinkFor(31, layoutId), true) - - - const idLink = location.map(location => `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`) + const idLink = location.map( + (location) => + `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${ + location?.lat ?? 0 + }/${location?.lon ?? 0}` + ) const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true) - const mapillaryLink = location.map(location => `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`) - const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true); - + const mapillaryLink = location.map( + (location) => + `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${ + location?.lon ?? 0 + }&z=${Math.max((location?.zoom ?? 2) - 1, 1)}` + ) + const mapillary = new Link( + Svg.mapillary_black_ui().SetClass("small-image"), + mapillaryLink, + true + ) let editWithJosm = new VariableUiElement( - userDetails.map(userDetails => { - + userDetails.map( + (userDetails) => { if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { - return undefined; + return undefined } - const bounds: any = currentBounds.data; + const bounds: any = currentBounds.data if (bounds === undefined) { return undefined } - const top = bounds.getNorth(); - const bottom = bounds.getSouth(); - const right = bounds.getEast(); - const left = bounds.getWest(); + const top = bounds.getNorth() + const bottom = bounds.getSouth() + const right = bounds.getEast() + const left = bounds.getWest() const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` - return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true); + return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true) }, [location, currentBounds] ) ) - super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]); + super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]) this.SetClass("flex") - } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/BackToIndex.ts b/UI/BigComponents/BackToIndex.ts index 40f866bc4..dc35f2139 100644 --- a/UI/BigComponents/BackToIndex.ts +++ b/UI/BigComponents/BackToIndex.ts @@ -1,19 +1,16 @@ -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" export default class BackToIndex extends SubtleButton { - constructor(message?: string | BaseUIElement) { super( Svg.back_svg().SetStyle("height: 1.5rem;"), message ?? Translations.t.general.backToMapcomplete, { - url: "index.html" + url: "index.html", } ) } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/BackgroundMapSwitch.ts b/UI/BigComponents/BackgroundMapSwitch.ts index b249f5abb..186d2e44f 100644 --- a/UI/BigComponents/BackgroundMapSwitch.ts +++ b/UI/BigComponents/BackgroundMapSwitch.ts @@ -1,15 +1,14 @@ -import Combine from "../Base/Combine"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Loc from "../../Models/Loc"; -import Svg from "../../Svg"; -import Toggle from "../Input/Toggle"; -import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import BaseUIElement from "../BaseUIElement"; -import {GeoOperations} from "../../Logic/GeoOperations"; +import Combine from "../Base/Combine" +import { UIEventSource } from "../../Logic/UIEventSource" +import Loc from "../../Models/Loc" +import Svg from "../../Svg" +import Toggle from "../Input/Toggle" +import BaseLayer from "../../Models/BaseLayer" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import BaseUIElement from "../BaseUIElement" +import { GeoOperations } from "../../Logic/GeoOperations" class SingleLayerSelectionButton extends Toggle { - public readonly activate: () => void /** @@ -24,38 +23,39 @@ class SingleLayerSelectionButton extends Toggle { constructor( locationControl: UIEventSource<Loc>, options: { - currentBackground: UIEventSource<BaseLayer>, - preferredType: string, - preferredLayer?: BaseLayer, + currentBackground: UIEventSource<BaseLayer> + preferredType: string + preferredLayer?: BaseLayer notAvailable?: () => void - }) { - - + } + ) { const prefered = options.preferredType const previousLayer = new UIEventSource(options.preferredLayer) - const unselected = SingleLayerSelectionButton.getIconFor(prefered) - .SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible") + const unselected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( + "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible" + ) - const selected = SingleLayerSelectionButton.getIconFor(prefered) - .SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch") + const selected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( + "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch" + ) - - const available = AvailableBaseLayers - .SelectBestLayerAccordingTo(locationControl, new UIEventSource<string | string[]>(options.preferredType)) + const available = AvailableBaseLayers.SelectBestLayerAccordingTo( + locationControl, + new UIEventSource<string | string[]>(options.preferredType) + ) let toggle: BaseUIElement = new Toggle( selected, unselected, - options.currentBackground.map(bg => bg.category === options.preferredType) + options.currentBackground.map((bg) => bg.category === options.preferredType) ) - super( toggle, undefined, - available.map(av => av.category === options.preferredType) - ); + available.map((av) => av.category === options.preferredType) + ) /** * Checks that the previous layer is still usable on the current location. @@ -85,27 +85,29 @@ class SingleLayerSelectionButton extends Toggle { options.currentBackground.setData(previousLayer.data) }) - options.currentBackground.addCallbackAndRunD(background => { + options.currentBackground.addCallbackAndRunD((background) => { if (background.category === options.preferredType) { previousLayer.setData(background) } }) - - available.addCallbackD(availableLayer => { + available.addCallbackD((availableLayer) => { // Called whenever a better layer is available if (previousLayer.data === undefined) { // PreviousLayer is unset -> we definitively weren't using this category -> no need to switch - return; + return } if (options.currentBackground.data?.id !== previousLayer.data?.id) { // The previously used layer doesn't match the current layer -> no need to switch - return; + return } // Is the previous layer still valid? If so, we don't bother to switch - if (previousLayer.data.feature === null || GeoOperations.inside(locationControl.data, previousLayer.data.feature)) { + if ( + previousLayer.data.feature === null || + GeoOperations.inside(locationControl.data, previousLayer.data.feature) + ) { return } @@ -134,13 +136,12 @@ class SingleLayerSelectionButton extends Toggle { // Fallback to OSM carto options.currentBackground.setData(AvailableBaseLayers.osmCarto) } - return; + return } previousLayer.setData(previousLayer.data ?? available.data) options.currentBackground.setData(previousLayer.data) } - } private static getIconFor(type: string) { @@ -158,7 +159,6 @@ class SingleLayerSelectionButton extends Toggle { } export default class BackgroundMapSwitch extends Combine { - /** * Three buttons to easily switch map layers between OSM, aerial and some map. * @param state @@ -167,14 +167,13 @@ export default class BackgroundMapSwitch extends Combine { */ constructor( state: { - locationControl: UIEventSource<Loc>, + locationControl: UIEventSource<Loc> backgroundLayer: UIEventSource<BaseLayer> }, currentBackground: UIEventSource<BaseLayer>, - options?:{ - preferredCategory?: string, + options?: { + preferredCategory?: string allowedCategories?: ("osmbasedmap" | "photo" | "map")[] - } ) { const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"] @@ -188,14 +187,12 @@ export default class BackgroundMapSwitch extends Combine { preferredLayer = previousLayer } - const button = new SingleLayerSelectionButton( - state.locationControl, - { - preferredType: category, - preferredLayer: preferredLayer, - currentBackground: currentBackground, - notAvailable: activatePrevious - }) + const button = new SingleLayerSelectionButton(state.locationControl, { + preferredType: category, + preferredLayer: preferredLayer, + currentBackground: currentBackground, + notAvailable: activatePrevious, + }) // Fall back to the first option: OSM activatePrevious = activatePrevious ?? button.activate if (category === options?.preferredCategory) { @@ -209,5 +206,4 @@ export default class BackgroundMapSwitch extends Combine { super(buttons) this.SetClass("flex") } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/BackgroundSelector.ts b/UI/BigComponents/BackgroundSelector.ts index 6d4f6cdf8..035812f68 100644 --- a/UI/BigComponents/BackgroundSelector.ts +++ b/UI/BigComponents/BackgroundSelector.ts @@ -1,41 +1,41 @@ -import {DropDown} from "../Input/DropDown"; -import Translations from "../i18n/Translations"; -import State from "../../State"; -import BaseLayer from "../../Models/BaseLayer"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Store} from "../../Logic/UIEventSource"; +import { DropDown } from "../Input/DropDown" +import Translations from "../i18n/Translations" +import State from "../../State" +import BaseLayer from "../../Models/BaseLayer" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Store } from "../../Logic/UIEventSource" export default class BackgroundSelector extends VariableUiElement { - - constructor(state: {availableBackgroundLayers?:Store<BaseLayer[]>} ) { - const available = state.availableBackgroundLayers?.map(available => { - if(available === undefined){ + constructor(state: { availableBackgroundLayers?: Store<BaseLayer[]> }) { + const available = state.availableBackgroundLayers?.map((available) => { + if (available === undefined) { return undefined } - const baseLayers: { value: BaseLayer, shown: string }[] = []; - for (const i in available) { - if (!available.hasOwnProperty(i)) { - continue; - } - const layer: BaseLayer = available[i]; - baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id}); + const baseLayers: { value: BaseLayer; shown: string }[] = [] + for (const i in available) { + if (!available.hasOwnProperty(i)) { + continue } - return baseLayers + const layer: BaseLayer = available[i] + baseLayers.push({ value: layer, shown: layer.name ?? "id:" + layer.id }) } - ) + return baseLayers + }) super( - available?.map(baseLayers => { - if (baseLayers === undefined || baseLayers.length <= 1) { - return undefined; - } - return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer, { - select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200 w-full' - }) + available?.map((baseLayers) => { + if (baseLayers === undefined || baseLayers.length <= 1) { + return undefined } - ) + return new DropDown( + Translations.t.general.backgroundMap.Clone(), + baseLayers, + State.state.backgroundLayer, + { + select_class: "bg-indigo-100 p-1 rounded hover:bg-indigo-200 w-full", + } + ) + }) ) - } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/CopyrightPanel.ts b/UI/BigComponents/CopyrightPanel.ts index 983c5482c..ee395d4d2 100644 --- a/UI/BigComponents/CopyrightPanel.ts +++ b/UI/BigComponents/CopyrightPanel.ts @@ -1,109 +1,128 @@ -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { FixedUiElement } from "../Base/FixedUiElement" import * as licenses from "../../assets/generated/license_info.json" -import SmallLicense from "../../Models/smallLicense"; -import {Utils} from "../../Utils"; -import Link from "../Base/Link"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import SmallLicense from "../../Models/smallLicense" +import { Utils } from "../../Utils" +import Link from "../Base/Link" +import { VariableUiElement } from "../Base/VariableUIElement" import * as contributors from "../../assets/contributors.json" import * as translators from "../../assets/translators.json" -import BaseUIElement from "../BaseUIElement"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import Title from "../Base/Title"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import {BBox} from "../../Logic/BBox"; -import Loc from "../../Models/Loc"; -import Toggle from "../Input/Toggle"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Constants from "../../Models/Constants"; -import ContributorCount from "../../Logic/ContributorCount"; -import Img from "../Base/Img"; -import {TypedTranslation} from "../i18n/Translation"; -import TranslatorsPanel from "./TranslatorsPanel"; -import {MapillaryLink} from "./MapillaryLink"; -import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"; +import BaseUIElement from "../BaseUIElement" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import Title from "../Base/Title" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import { BBox } from "../../Logic/BBox" +import Loc from "../../Models/Loc" +import Toggle from "../Input/Toggle" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Constants from "../../Models/Constants" +import ContributorCount from "../../Logic/ContributorCount" +import Img from "../Base/Img" +import { TypedTranslation } from "../i18n/Translation" +import TranslatorsPanel from "./TranslatorsPanel" +import { MapillaryLink } from "./MapillaryLink" +import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs" export class OpenIdEditor extends VariableUiElement { - constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) { + constructor( + state: { locationControl: UIEventSource<Loc> }, + iconStyle?: string, + objectId?: string + ) { const t = Translations.t.general.attribution - super(state.locationControl.map(location => { - let elementSelect = ""; - if (objectId !== undefined) { - const parts = objectId.split("/") - const tp = parts[0] - if (parts.length === 2 && !isNaN(Number(parts[1])) && (tp === "node" || tp === "way" || tp === "relation")) { - elementSelect = "&" + tp + "=" + parts[1] + super( + state.locationControl.map((location) => { + let elementSelect = "" + if (objectId !== undefined) { + const parts = objectId.split("/") + const tp = parts[0] + if ( + parts.length === 2 && + !isNaN(Number(parts[1])) && + (tp === "node" || tp === "way" || tp === "relation") + ) { + elementSelect = "&" + tp + "=" + parts[1] + } } - } - const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}` - return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {url: idLink, newTab: true}) - })); + const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ + location?.zoom ?? 0 + }/${location?.lat ?? 0}/${location?.lon ?? 0}` + return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { + url: idLink, + newTab: true, + }) + }) + ) } - } export class OpenJosm extends Combine { - - constructor(state: { osmConnection: OsmConnection, currentBounds: Store<BBox>, }, iconStyle?: string) { + constructor( + state: { osmConnection: OsmConnection; currentBounds: Store<BBox> }, + iconStyle?: string + ) { const t = Translations.t.general.attribution const josmState = new UIEventSource<string>(undefined) // Reset after 15s - josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined)) + josmState.stabilized(15000).addCallbackD((_) => josmState.setData(undefined)) - const stateIndication = new VariableUiElement(josmState.map(state => { - if (state === undefined) { - return undefined - } - state = state.toUpperCase() - if (state === "OK") { - return t.josmOpened.SetClass("thanks") - } - return t.josmNotOpened.SetClass("alert") - })); + const stateIndication = new VariableUiElement( + josmState.map((state) => { + if (state === undefined) { + return undefined + } + state = state.toUpperCase() + if (state === "OK") { + return t.josmOpened.SetClass("thanks") + } + return t.josmNotOpened.SetClass("alert") + }) + ) const toggle = new Toggle( new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { - const bounds: any = state.currentBounds.data; + const bounds: any = state.currentBounds.data if (bounds === undefined) { return undefined } - const top = bounds.getNorth(); - const bottom = bounds.getSouth(); - const right = bounds.getEast(); - const left = bounds.getWest(); + const top = bounds.getNorth() + const bottom = bounds.getSouth() + const right = bounds.getEast() + const left = bounds.getWest() const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` - Utils.download(josmLink).then(answer => josmState.setData(answer.replace(/\n/g, '').trim())).catch(_ => josmState.setData("ERROR")) - }), undefined, state.osmConnection.userDetails.map(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible)) - - super([stateIndication, toggle]); + Utils.download(josmLink) + .then((answer) => josmState.setData(answer.replace(/\n/g, "").trim())) + .catch((_) => josmState.setData("ERROR")) + }), + undefined, + state.osmConnection.userDetails.map( + (ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible + ) + ) + super([stateIndication, toggle]) } - - } - /** * The attribution panel shown on mobile */ export default class CopyrightPanel extends Combine { - - private static LicenseObject = CopyrightPanel.GenerateLicenses(); + private static LicenseObject = CopyrightPanel.GenerateLicenses() constructor(state: { - layoutToUse: LayoutConfig, - featurePipeline: FeaturePipeline, - currentBounds: Store<BBox>, - locationControl: UIEventSource<Loc>, - osmConnection: OsmConnection, + layoutToUse: LayoutConfig + featurePipeline: FeaturePipeline + currentBounds: Store<BBox> + locationControl: UIEventSource<Loc> + osmConnection: OsmConnection isTranslator: Store<boolean> }) { - const t = Translations.t.general.attribution const layoutToUse = state.layoutToUse const imgSize = "h-6 w-6" @@ -112,120 +131,134 @@ export default class CopyrightPanel extends Combine { new SubtleButton(Svg.liberapay_ui(), t.donate, { url: "https://liberapay.com/pietervdvn/", newTab: true, - imgSize + imgSize, }), new SubtleButton(Svg.bug_ui(), t.openIssueTracker, { url: "https://github.com/pietervdvn/MapComplete/issues", newTab: true, - imgSize + imgSize, }), - new SubtleButton(Svg.statistics_ui(), t.openOsmcha.Subs({theme: state.layoutToUse.title}), { - url: Utils.OsmChaLinkFor(31, state.layoutToUse.id), - newTab: true, - imgSize - }), - new SubtleButton(Svg.mastodon_ui(), - new Combine([t.followOnMastodon.SetClass("font-bold"), t.followBridge]).SetClass("flex flex-col"), + new SubtleButton( + Svg.statistics_ui(), + t.openOsmcha.Subs({ theme: state.layoutToUse.title }), { - url:"https://en.osm.town/web/notifications", - newTab: true, - imgSize - }), + url: Utils.OsmChaLinkFor(31, state.layoutToUse.id), + newTab: true, + imgSize, + } + ), + new SubtleButton( + Svg.mastodon_ui(), + new Combine([t.followOnMastodon.SetClass("font-bold"), t.followBridge]).SetClass( + "flex flex-col" + ), + { + url: "https://en.osm.town/web/notifications", + newTab: true, + imgSize, + } + ), new SubtleButton(Svg.twitter_ui(), t.followOnTwitter, { - url:"https://twitter.com/mapcomplete", + url: "https://twitter.com/mapcomplete", newTab: true, - imgSize + imgSize, }), new OpenIdEditor(state, iconStyle), new MapillaryLink(state, iconStyle), new OpenJosm(state, iconStyle), - new TranslatorsPanel(state, iconStyle) - + new TranslatorsPanel(state, iconStyle), ] const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution) let maintainer: BaseUIElement = undefined if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") { - maintainer = t.themeBy.Subs({author: layoutToUse.credits}) + maintainer = t.themeBy.Subs({ author: layoutToUse.credits }) } const contributions = new ContributorCount(state).Contributors - const dataContributors = new VariableUiElement(contributions.map(contributions => { + const dataContributors = new VariableUiElement( + contributions.map((contributions) => { if (contributions === undefined) { return "" } const sorted = Array.from(contributions, ([name, value]) => ({ name, - value - })).filter(x => x.name !== undefined && x.name !== "undefined"); + value, + })).filter((x) => x.name !== undefined && x.name !== "undefined") if (sorted.length === 0) { - return ""; + return "" } - sorted.sort((a, b) => b.value - a.value); - let hiddenCount = 0; + sorted.sort((a, b) => b.value - a.value) + let hiddenCount = 0 if (sorted.length > 10) { hiddenCount = sorted.length - 10 sorted.splice(10, sorted.length - 10) } - const links = sorted.map(kv => `<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>`) + const links = sorted.map( + (kv) => + `<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>` + ) const contribs = links.join(", ") if (hiddenCount <= 0) { return t.mapContributionsBy.Subs({ - contributors: contribs + contributors: contribs, }) } else { return t.mapContributionsByAndHidden.Subs({ contributors: contribs, - hiddenCount: hiddenCount - }); + hiddenCount: hiddenCount, + }) } + }) + ) - - })) - - super([ - new Title(t.attributionTitle), - t.attributionContent, - maintainer, - dataContributors, - CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy), - CopyrightPanel.CodeContributors(translators, t.translatedBy), - new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), - new Combine(actionButtons).SetClass("block w-full link-no-underline"), - new Title(t.iconAttribution.title, 3), - ...iconAttributions - ].map(e => e?.SetClass("mt-4"))); + super( + [ + new Title(t.attributionTitle), + t.attributionContent, + maintainer, + dataContributors, + CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy), + CopyrightPanel.CodeContributors(translators, t.translatedBy), + new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"), + new Combine(actionButtons).SetClass("block w-full link-no-underline"), + new Title(t.iconAttribution.title, 3), + ...iconAttributions, + ].map((e) => e?.SetClass("mt-4")) + ) this.SetClass("flex flex-col link-underline overflow-hidden") this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem") } - private static CodeContributors(contributors, translation: TypedTranslation<{contributors, hiddenCount}>): BaseUIElement { - - const total = contributors.contributors.length; + private static CodeContributors( + contributors, + translation: TypedTranslation<{ contributors; hiddenCount }> + ): BaseUIElement { + const total = contributors.contributors.length let filtered = [...contributors.contributors] - filtered.splice(10, total - 10); + filtered.splice(10, total - 10) - let contribsStr = filtered.map(c => c.contributor).join(", ") + let contribsStr = filtered.map((c) => c.contributor).join(", ") if (contribsStr === "") { // Hmm, something went wrong loading the contributors list. Lets show nothing - return undefined; + return undefined } return translation.Subs({ contributors: contribsStr, - hiddenCount: total - 10 - }); + hiddenCount: total - 10, + }) } private static IconAttribution(iconPath: string): BaseUIElement { if (iconPath.startsWith("http")) { try { - iconPath = "." + new URL(iconPath).pathname; + iconPath = "." + new URL(iconPath).pathname } catch (e) { console.warn(e) } @@ -233,10 +266,10 @@ export default class CopyrightPanel extends Combine { const license: SmallLicense = CopyrightPanel.LicenseObject[iconPath] if (license == undefined) { - return undefined; + return undefined } if (license.license.indexOf("trivial") >= 0) { - return undefined; + return undefined } const sources = Utils.NoNull(Utils.NoEmpty(license.sources)) @@ -246,25 +279,29 @@ export default class CopyrightPanel extends Combine { new Combine([ new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), license.license, - new Combine([...sources.map(lnk => { - let sourceLinkContent = lnk; - try { - sourceLinkContent = new URL(lnk).hostname - } catch { - console.error("Not a valid URL:", lnk) - } - return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2"); - })]).SetClass("flex flex-wrap") - ]).SetClass("flex flex-col").SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;") + new Combine([ + ...sources.map((lnk) => { + let sourceLinkContent = lnk + try { + sourceLinkContent = new URL(lnk).hostname + } catch { + console.error("Not a valid URL:", lnk) + } + return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2") + }), + ]).SetClass("flex flex-wrap"), + ]) + .SetClass("flex flex-col") + .SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;"), ]).SetClass("flex flex-wrap border-b border-gray-300 m-2 border-box") } private static GenerateLicenses() { const allLicenses = {} for (const key in licenses) { - const license: SmallLicense = licenses[key]; + const license: SmallLicense = licenses[key] allLicenses[license.path] = license } - return allLicenses; + return allLicenses } -} \ No newline at end of file +} diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts index 9601def11..82baf3b1d 100644 --- a/UI/BigComponents/DownloadPanel.ts +++ b/UI/BigComponents/DownloadPanel.ts @@ -1,98 +1,105 @@ -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import State from "../../State"; -import {Utils} from "../../Utils"; -import Combine from "../Base/Combine"; -import CheckBoxes from "../Input/Checkboxes"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import Toggle from "../Input/Toggle"; -import Title from "../Base/Title"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {BBox} from "../../Logic/BBox"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import State from "../../State" +import { Utils } from "../../Utils" +import Combine from "../Base/Combine" +import CheckBoxes from "../Input/Checkboxes" +import { GeoOperations } from "../../Logic/GeoOperations" +import Toggle from "../Input/Toggle" +import Title from "../Base/Title" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import { UIEventSource } from "../../Logic/UIEventSource" +import SimpleMetaTagger from "../../Logic/SimpleMetaTagger" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { BBox } from "../../Logic/BBox" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import geojson2svg from "geojson2svg" -import Constants from "../../Models/Constants"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import Constants from "../../Models/Constants" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" export class DownloadPanel extends Toggle { - constructor(state: { filteredLayers: UIEventSource<FilteredLayer[]> - featurePipeline: FeaturePipeline, - layoutToUse: LayoutConfig, - currentBounds: UIEventSource<BBox>, - + featurePipeline: FeaturePipeline + layoutToUse: LayoutConfig + currentBounds: UIEventSource<BBox> }) { - const t = Translations.t.general.download - const name = State.state.layoutToUse.id; + const name = State.state.layoutToUse.id const includeMetaToggle = new CheckBoxes([t.includeMetaData]) - const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) + const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0) + const buttonGeoJson = new SubtleButton( + Svg.floppy_ui(), + new Combine([ + t.downloadGeojson.SetClass("font-bold"), + t.downloadGeoJsonHelper, + ]).SetClass("flex flex-col") + ).OnClickWithLoading(t.exporting, async () => { + const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) + Utils.offerContentsAsDownloadableFile( + JSON.stringify(geojson, null, " "), + `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, + { + mimetype: "application/vnd.geo+json", + } + ) + }) - const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), - new Combine([t.downloadGeojson.SetClass("font-bold"), - t.downloadGeoJsonHelper]).SetClass("flex flex-col")) - .OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, { - mimetype: "application/vnd.geo+json" - }); + const buttonCSV = new SubtleButton( + Svg.floppy_ui(), + new Combine([t.downloadCSV.SetClass("font-bold"), t.downloadCSVHelper]).SetClass( + "flex flex-col" + ) + ).OnClickWithLoading(t.exporting, async () => { + const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) + const csv = GeoOperations.toCSV(geojson.features) + + Utils.offerContentsAsDownloadableFile( + csv, + `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, + { + mimetype: "text/csv", + } + ) + }) + + const buttonSvg = new SubtleButton( + Svg.floppy_ui(), + new Combine([t.downloadAsSvg.SetClass("font-bold"), t.downloadAsSvgHelper]).SetClass( + "flex flex-col" + ) + ).OnClickWithLoading(t.exporting, async () => { + const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data) + const leafletdiv = document.getElementById("leafletDiv") + const csv = DownloadPanel.asSvg(geojson, { + layers: state.filteredLayers.data.map((l) => l.layerDef), + mapExtent: state.currentBounds.data, + width: leafletdiv.offsetWidth, + height: leafletdiv.offsetHeight, }) + Utils.offerContentsAsDownloadableFile( + csv, + `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, + { + mimetype: "image/svg+xml", + } + ) + }) - const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine( - [t.downloadCSV.SetClass("font-bold"), - t.downloadCSVHelper]).SetClass("flex flex-col")) - .OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) - const csv = GeoOperations.toCSV(geojson.features) + const downloadButtons = new Combine([ + new Title(t.title), + buttonGeoJson, + buttonCSV, + buttonSvg, + includeMetaToggle, + t.licenseInfo.SetClass("link-underline"), + ]).SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4") - Utils.offerContentsAsDownloadableFile(csv, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, { - mimetype: "text/csv" - }); - }) - - const buttonSvg = new SubtleButton(Svg.floppy_ui(), new Combine( - [t.downloadAsSvg.SetClass("font-bold"), - t.downloadAsSvgHelper]).SetClass("flex flex-col")) - .OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data) - const leafletdiv = document.getElementById("leafletDiv") - const csv = DownloadPanel.asSvg(geojson, - { - layers: state.filteredLayers.data.map(l => l.layerDef), - mapExtent: state.currentBounds.data, - width: leafletdiv.offsetWidth, - height: leafletdiv.offsetHeight - }) - - Utils.offerContentsAsDownloadableFile(csv, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, { - mimetype: "image/svg+xml" - }); - }) - - const downloadButtons = new Combine( - [new Title(t.title), - buttonGeoJson, - buttonCSV, - buttonSvg, - includeMetaToggle, - t.licenseInfo.SetClass("link-underline")]) - .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4") - - super( - downloadButtons, - t.noDataLoaded, - state.featurePipeline.somethingLoaded) + super(downloadButtons, t.noDataLoaded, state.featurePipeline.somethingLoaded) } /** @@ -112,20 +119,21 @@ export class DownloadPanel extends Toggle { * const perLayer = new Map<string, any[]>([["testlayer", [feature]]]) * DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => `<svg width="1000px" height="1000px" viewBox="0 0 1000 1000"> <g id="testlayer" inkscape:groupmode="layer" inkscape:label="testlayer"> <path d="M0,27.77777777777778 1000,472.22222222222223" style="fill:none;stroke-width:1" stroke="#ff0000"/> </g></svg>` */ - public static asSvg(perLayer: Map<string, any[]>, - options?: - { - layers?: LayerConfig[], - width?: 1000 | number, - height?: 1000 | number, - mapExtent?: BBox - unit?: "px" | "mm" | string - }) { + public static asSvg( + perLayer: Map<string, any[]>, + options?: { + layers?: LayerConfig[] + width?: 1000 | number + height?: 1000 | number + mapExtent?: BBox + unit?: "px" | "mm" | string + } + ) { options = options ?? {} const w = options.width ?? 1000 const h = options.height ?? 1000 const unit = options.unit ?? "px" - const mapExtent = {left: -180, bottom: -90, right: 180, top: 90} + const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 } if (options.mapExtent !== undefined) { const bbox = options.mapExtent mapExtent.left = bbox.minLon @@ -134,51 +142,50 @@ export class DownloadPanel extends Toggle { mapExtent.top = bbox.maxLat } - const elements: string [] = [] + const elements: string[] = [] for (const layer of Array.from(perLayer.keys())) { const features = perLayer.get(layer) - if(features.length === 0){ + if (features.length === 0) { continue } - const layerDef = options?.layers?.find(l => l.id === layer) + const layerDef = options?.layers?.find((l) => l.id === layer) const rendering = layerDef?.lineRendering[0] - const converter = geojson2svg({ - viewportSize: {width: w, height: h}, + const converter = geojson2svg({ + viewportSize: { width: w, height: h }, mapExtent, - output: 'svg', - attributes:[ + output: "svg", + attributes: [ { property: "style", - type:'static', - value: "fill:none;stroke-width:1" + type: "static", + value: "fill:none;stroke-width:1", }, { - property: 'properties.stroke', - type:'dynamic', - key: 'stroke' - } - ] - - }); + property: "properties.stroke", + type: "dynamic", + key: "stroke", + }, + ], + }) for (const feature of features) { - const stroke = rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000" - const color = Utils.colorAsHex( Utils.color(stroke)) + const stroke = + rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000" + const color = Utils.colorAsHex(Utils.color(stroke)) feature.properties.stroke = color } - - - const groupPaths: string[] = converter.convert({type: "FeatureCollection", features}) - const group = ` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` + - groupPaths.map(p => " " + p).join("\n") - + "\n </g>" + + const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features }) + const group = + ` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` + + groupPaths.map((p) => " " + p).join("\n") + + "\n </g>" elements.push(group) } - const header = `<svg width="${w}${unit}" height="${h}${unit}" viewBox="0 0 ${w} ${h}">` return header + "\n" + elements.join("\n") + "\n</svg>" } @@ -189,48 +196,53 @@ export class DownloadPanel extends Toggle { * @param includeMetaData * @private */ - private static getCleanGeoJson(state: { - featurePipeline: FeaturePipeline, - currentBounds: UIEventSource<BBox>, - filteredLayers: UIEventSource<FilteredLayer[]> - }, includeMetaData: boolean) { + private static getCleanGeoJson( + state: { + featurePipeline: FeaturePipeline + currentBounds: UIEventSource<BBox> + filteredLayers: UIEventSource<FilteredLayer[]> + }, + includeMetaData: boolean + ) { const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) const features = [].concat(...Array.from(perLayer.values())) return { type: "FeatureCollection", - features + features, } } - private static getCleanGeoJsonPerLayer(state: { - featurePipeline: FeaturePipeline, - currentBounds: UIEventSource<BBox>, - filteredLayers: UIEventSource<FilteredLayer[]> - }, includeMetaData: boolean): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ { - - const perLayer = new Map<string, any[]>(); - const neededLayers = state.filteredLayers.data.map(l => l.layerDef.id) + private static getCleanGeoJsonPerLayer( + state: { + featurePipeline: FeaturePipeline + currentBounds: UIEventSource<BBox> + filteredLayers: UIEventSource<FilteredLayer[]> + }, + includeMetaData: boolean + ): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ { + const perLayer = new Map<string, any[]>() + const neededLayers = state.filteredLayers.data.map((l) => l.layerDef.id) const bbox = state.currentBounds.data - const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox, new Set(neededLayers)); - outer : for (const tile of featureList) { - - if(Constants.priviliged_layers.indexOf(tile.layer) >= 0){ + const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin( + bbox, + new Set(neededLayers) + ) + outer: for (const tile of featureList) { + if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) { continue } - - const layer = state.filteredLayers.data.find(fl => fl.layerDef.id === tile.layer) + + const layer = state.filteredLayers.data.find((fl) => fl.layerDef.id === tile.layer) if (!perLayer.has(tile.layer)) { perLayer.set(tile.layer, []) } const featureList = perLayer.get(tile.layer) const filters = layer.appliedFilters.data for (const feature of tile.features) { - if (!bbox.overlapsWith(BBox.get(feature))) { continue } - if (filters !== undefined) { for (let key of Array.from(filters.keys())) { const filter: FilterState = filters.get(key) @@ -238,21 +250,21 @@ export class DownloadPanel extends Toggle { continue } if (!filter.currentFilter.matchesProperties(feature.properties)) { - continue outer; + continue outer } } } const cleaned = { type: feature.type, - geometry: {...feature.geometry}, - properties: {...feature.properties} + geometry: { ...feature.geometry }, + properties: { ...feature.properties }, } if (!includeMetaData) { for (const key in cleaned.properties) { if (key === "_lon" || key === "_lat") { - continue; + continue } if (key.startsWith("_")) { delete feature.properties[key] @@ -260,7 +272,11 @@ export class DownloadPanel extends Toggle { } } - const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys)) + const datedKeys = [].concat( + SimpleMetaTagger.metatags + .filter((tagging) => tagging.includesDates) + .map((tagging) => tagging.keys) + ) for (const key of datedKeys) { delete feature.properties[key] } @@ -270,6 +286,5 @@ export class DownloadPanel extends Toggle { } return perLayer - } -} \ No newline at end of file +} diff --git a/UI/BigComponents/ExtraLinkButton.ts b/UI/BigComponents/ExtraLinkButton.ts index 6468874de..92b5621d6 100644 --- a/UI/BigComponents/ExtraLinkButton.ts +++ b/UI/BigComponents/ExtraLinkButton.ts @@ -1,37 +1,44 @@ -import {UIElement} from "../UIElement"; -import BaseUIElement from "../BaseUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"; -import Img from "../Base/Img"; -import {SubtleButton} from "../Base/SubtleButton"; -import Toggle from "../Input/Toggle"; -import Loc from "../../Models/Loc"; -import Locale from "../i18n/Locale"; -import {Utils} from "../../Utils"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import {Translation} from "../i18n/Translation"; +import { UIElement } from "../UIElement" +import BaseUIElement from "../BaseUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig" +import Img from "../Base/Img" +import { SubtleButton } from "../Base/SubtleButton" +import Toggle from "../Input/Toggle" +import Loc from "../../Models/Loc" +import Locale from "../i18n/Locale" +import { Utils } from "../../Utils" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" export default class ExtraLinkButton extends UIElement { - private readonly _config: ExtraLinkConfig; + private readonly _config: ExtraLinkConfig private readonly state: { - layoutToUse: { id: string, title: Translation }; - featureSwitchWelcomeMessage: UIEventSource<boolean>, locationControl: UIEventSource<Loc> - }; + layoutToUse: { id: string; title: Translation } + featureSwitchWelcomeMessage: UIEventSource<boolean> + locationControl: UIEventSource<Loc> + } - constructor(state: { featureSwitchWelcomeMessage: UIEventSource<boolean>, locationControl: UIEventSource<Loc>, layoutToUse: { id: string, title: Translation } }, - config: ExtraLinkConfig) { - super(); - this.state = state; - this._config = config; + constructor( + state: { + featureSwitchWelcomeMessage: UIEventSource<boolean> + locationControl: UIEventSource<Loc> + layoutToUse: { id: string; title: Translation } + }, + config: ExtraLinkConfig + ) { + super() + this.state = state + this._config = config } protected InnerRender(): BaseUIElement { if (this._config === undefined) { - return undefined; + return undefined } - const c = this._config; + const c = this._config const isIframe = window !== window.top @@ -46,17 +53,16 @@ export default class ExtraLinkButton extends UIElement { let link: BaseUIElement const theme = this.state.layoutToUse?.id ?? "" const basepath = window.location.host - const href = this.state.locationControl.map(loc => { + const href = this.state.locationControl.map((loc) => { const subs = { ...loc, theme: theme, basepath, - language: Locale.language.data + language: Locale.language.data, } return Utils.SubstituteKeys(c.href, subs) }) - let img: BaseUIElement = Svg.pop_out_ui() if (c.icon !== undefined) { img = new Img(c.icon).SetClass("h-6") @@ -64,14 +70,16 @@ export default class ExtraLinkButton extends UIElement { let text: Translation if (c.text === undefined) { - text = Translations.t.general.screenToSmall.Subs({theme: this.state.layoutToUse.title}) + text = Translations.t.general.screenToSmall.Subs({ + theme: this.state.layoutToUse.title, + }) } else { text = c.text.Clone() } link = new SubtleButton(img, text, { url: href, - newTab: c.newTab + newTab: c.newTab, }) if (c.requirements.has("no-welcome-message")) { @@ -82,7 +90,6 @@ export default class ExtraLinkButton extends UIElement { link = new Toggle(link, undefined, this.state.featureSwitchWelcomeMessage) } - return link; + return link } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/FeaturedMessage.ts b/UI/BigComponents/FeaturedMessage.ts index aa3e2cf7d..086eca256 100644 --- a/UI/BigComponents/FeaturedMessage.ts +++ b/UI/BigComponents/FeaturedMessage.ts @@ -1,18 +1,16 @@ -import Combine from "../Base/Combine"; +import Combine from "../Base/Combine" import * as welcome_messages from "../../assets/welcome_message.json" -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import MoreScreen from "./MoreScreen"; +import BaseUIElement from "../BaseUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" +import MoreScreen from "./MoreScreen" import * as themeOverview from "../../assets/generated/theme_overview.json" -import Translations from "../i18n/Translations"; -import Title from "../Base/Title"; +import Translations from "../i18n/Translations" +import Title from "../Base/Title" export default class FeaturedMessage extends Combine { - - constructor() { const now = new Date() - let welcome_message = undefined; + let welcome_message = undefined for (const wm of FeaturedMessage.WelcomeMessages()) { if (wm.start_date >= now) { continue @@ -24,19 +22,29 @@ export default class FeaturedMessage extends Combine { if (welcome_message !== undefined) { console.warn("Multiple applicable messages today:", welcome_message.featured_theme) } - welcome_message = wm; + welcome_message = wm } welcome_message = welcome_message ?? undefined - super([FeaturedMessage.CreateFeaturedBox(welcome_message)]); + super([FeaturedMessage.CreateFeaturedBox(welcome_message)]) } - public static WelcomeMessages(): { start_date: Date, end_date: Date, message: string, featured_theme?: string }[] { - const all_messages: { start_date: Date, end_date: Date, message: string, featured_theme?: string }[] = [] + public static WelcomeMessages(): { + start_date: Date + end_date: Date + message: string + featured_theme?: string + }[] { + const all_messages: { + start_date: Date + end_date: Date + message: string + featured_theme?: string + }[] = [] - const themesById = new Map<string, { id: string, title: any, shortDescription: any }>(); + const themesById = new Map<string, { id: string; title: any; shortDescription: any }>() for (const theme of themeOverview["default"]) { - themesById.set(theme.id, theme); + themesById.set(theme.id, theme) } for (const i in welcome_messages) { @@ -62,32 +70,36 @@ export default class FeaturedMessage extends Combine { start_date: new Date(wm.start_date), end_date: new Date(wm.end_date), message: wm.message, - featured_theme: wm.featured_theme + featured_theme: wm.featured_theme, }) - } return all_messages } - public static CreateFeaturedBox(welcome_message: { message: string, featured_theme?: string }): BaseUIElement { + public static CreateFeaturedBox(welcome_message: { + message: string + featured_theme?: string + }): BaseUIElement { const els: BaseUIElement[] = [] if (welcome_message === undefined) { - return undefined; + return undefined } const title = new Title(Translations.t.index.featuredThemeTitle.Clone()) const msg = new FixedUiElement(welcome_message.message).SetClass("link-underline font-lg") els.push(new Combine([title, msg]).SetClass("m-4")) if (welcome_message.featured_theme !== undefined) { + const theme = themeOverview["default"].filter( + (th) => th.id === welcome_message.featured_theme + )[0] - const theme = themeOverview["default"].filter(th => th.id === welcome_message.featured_theme)[0]; - - els.push(MoreScreen.createLinkButton({}, theme) - .SetClass("m-4 self-center md:w-160") - .SetStyle("height: min-content;")) - - + els.push( + MoreScreen.createLinkButton({}, theme) + .SetClass("m-4 self-center md:w-160") + .SetStyle("height: min-content;") + ) } - return new Combine(els).SetClass("border-2 border-grey-400 rounded-xl flex flex-col md:flex-row"); + return new Combine(els).SetClass( + "border-2 border-grey-400 rounded-xl flex flex-col md:flex-row" + ) } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index 83ccda389..eb0abc278 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -1,37 +1,39 @@ -import {Utils} from "../../Utils"; -import {FixedInputElement} from "../Input/FixedInputElement"; -import {RadioButton} from "../Input/RadioButton"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Toggle, {ClickableToggle} from "../Input/Toggle"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import {Translation} from "../i18n/Translation"; -import Svg from "../../Svg"; -import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; -import BackgroundSelector from "./BackgroundSelector"; -import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {QueryParameters} from "../../Logic/Web/QueryParameters"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {InputElement} from "../Input/InputElement"; -import {DropDown} from "../Input/DropDown"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import BaseLayer from "../../Models/BaseLayer"; -import Loc from "../../Models/Loc"; +import { Utils } from "../../Utils" +import { FixedInputElement } from "../Input/FixedInputElement" +import { RadioButton } from "../Input/RadioButton" +import { VariableUiElement } from "../Base/VariableUIElement" +import Toggle, { ClickableToggle } from "../Input/Toggle" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" +import Svg from "../../Svg" +import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import BackgroundSelector from "./BackgroundSelector" +import FilterConfig from "../../Models/ThemeConfig/FilterConfig" +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import ValidatedTextField from "../Input/ValidatedTextField" +import { QueryParameters } from "../../Logic/Web/QueryParameters" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { InputElement } from "../Input/InputElement" +import { DropDown } from "../Input/DropDown" +import { FixedUiElement } from "../Base/FixedUiElement" +import BaseLayer from "../../Models/BaseLayer" +import Loc from "../../Models/Loc" export default class FilterView extends VariableUiElement { - constructor(filteredLayer: Store<FilteredLayer[]>, - tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[], - state: { - availableBackgroundLayers?: Store<BaseLayer[]>, - featureSwitchBackgroundSelection?: UIEventSource<boolean>, - featureSwitchIsDebugging?: UIEventSource<boolean>, - locationControl?: UIEventSource<Loc> - }) { + constructor( + filteredLayer: Store<FilteredLayer[]>, + tileLayers: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[], + state: { + availableBackgroundLayers?: Store<BaseLayer[]> + featureSwitchBackgroundSelection?: UIEventSource<boolean> + featureSwitchIsDebugging?: UIEventSource<boolean> + locationControl?: UIEventSource<Loc> + } + ) { const backgroundSelector = new Toggle( new BackgroundSelector(state), undefined, @@ -39,147 +41,143 @@ export default class FilterView extends VariableUiElement { ) super( filteredLayer.map((filteredLayers) => { - // Create the views which toggle layers (and filters them) ... - let elements = filteredLayers - ?.map(l => FilterView.createOneFilteredLayerElement(l, state)?.SetClass("filter-panel")) - ?.filter(l => l !== undefined) - elements[0].SetClass("first-filter-panel") + // Create the views which toggle layers (and filters them) ... + let elements = filteredLayers + ?.map((l) => + FilterView.createOneFilteredLayerElement(l, state)?.SetClass("filter-panel") + ) + ?.filter((l) => l !== undefined) + elements[0].SetClass("first-filter-panel") - // ... create views for non-interactive layers ... - elements = elements.concat(tileLayers.map(tl => FilterView.createOverlayToggle(state, tl))) - // ... and add the dropdown to select a different background - return elements.concat(backgroundSelector); - } - ) - ); + // ... create views for non-interactive layers ... + elements = elements.concat( + tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl)) + ) + // ... and add the dropdown to select a different background + return elements.concat(backgroundSelector) + }) + ) } - private static createOverlayToggle(state: { locationControl?: UIEventSource<Loc> }, config: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }) { + private static createOverlayToggle( + state: { locationControl?: UIEventSource<Loc> }, + config: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> } + ) { + const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;" - const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;"; + const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle) + const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle) + const name: Translation = config.config.name - const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); - const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( - iconStyle - ); - const name: Translation = config.config.name; + const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") + const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") - const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2"); - const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2"); + const zoomStatus = new Toggle( + undefined, + Translations.t.general.layerSelection.zoomInToSeeThisLayer + .SetClass("alert") + .SetStyle("display: block ruby;width:min-content;"), + state.locationControl?.map((location) => location.zoom >= config.config.minzoom) ?? + new ImmutableStore(false) + ) - const zoomStatus = - new Toggle( - undefined, - Translations.t.general.layerSelection.zoomInToSeeThisLayer - .SetClass("alert") - .SetStyle("display: block ruby;width:min-content;"), - state.locationControl?.map(location => location.zoom >= config.config.minzoom) ?? new ImmutableStore(false) - ) - - - const style = - "display:flex;align-items:center;padding:0.5rem 0;"; + const style = "display:flex;align-items:center;padding:0.5rem 0;" const layerChecked = new Combine([icon, styledNameChecked, zoomStatus]) .SetStyle(style) - .onClick(() => config.isDisplayed.setData(false)); + .onClick(() => config.isDisplayed.setData(false)) const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked]) .SetStyle(style) - .onClick(() => config.isDisplayed.setData(true)); + .onClick(() => config.isDisplayed.setData(true)) - - return new Toggle( - layerChecked, - layerNotChecked, - config.isDisplayed - ); + return new Toggle(layerChecked, layerNotChecked, config.isDisplayed) } - private static createOneFilteredLayerElement(filteredLayer: FilteredLayer, - state: { featureSwitchIsDebugging?: Store<boolean>, locationControl?: Store<Loc> }) { + private static createOneFilteredLayerElement( + filteredLayer: FilteredLayer, + state: { featureSwitchIsDebugging?: Store<boolean>; locationControl?: Store<Loc> } + ) { if (filteredLayer.layerDef.name === undefined) { // Name is not defined: we hide this one return new Toggle( new FixedUiElement(filteredLayer?.layerDef?.id).SetClass("block"), undefined, state?.featureSwitchIsDebugging ?? new ImmutableStore(false) - ); + ) } - const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;"; + const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;" - const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); + const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle) const layer = filteredLayer.layerDef - const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( - iconStyle - ); + const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle) const name: Translation = filteredLayer.layerDef.name.Clone() - const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3"); + const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3") - const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3"); + const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3") - const zoomStatus = - new Toggle( - undefined, - Translations.t.general.layerSelection.zoomInToSeeThisLayer - .SetClass("alert") - .SetStyle("display: block ruby;width:min-content;"), - state?.locationControl?.map(location => location.zoom >= filteredLayer.layerDef.minzoom) ?? new ImmutableStore(false) - ) + const zoomStatus = new Toggle( + undefined, + Translations.t.general.layerSelection.zoomInToSeeThisLayer + .SetClass("alert") + .SetStyle("display: block ruby;width:min-content;"), + state?.locationControl?.map( + (location) => location.zoom >= filteredLayer.layerDef.minzoom + ) ?? new ImmutableStore(false) + ) - - const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0"; + const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0" const layerIcon = layer.defaultIcon()?.SetClass("flex-shrink-0 w-8 h-8 ml-2") - const layerIconUnchecked = layer.defaultIcon()?.SetClass("flex-shrink-0 opacity-50 w-8 h-8 ml-2") + const layerIconUnchecked = layer + .defaultIcon() + ?.SetClass("flex-shrink-0 opacity-50 w-8 h-8 ml-2") const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus]) .SetClass(toggleClasses) - .onClick(() => filteredLayer.isDisplayed.setData(false)); + .onClick(() => filteredLayer.isDisplayed.setData(false)) - const layerNotChecked = new Combine([iconUnselected, layerIconUnchecked, styledNameUnChecked]) + const layerNotChecked = new Combine([ + iconUnselected, + layerIconUnchecked, + styledNameUnChecked, + ]) .SetClass(toggleClasses) - .onClick(() => filteredLayer.isDisplayed.setData(true)); - + .onClick(() => filteredLayer.isDisplayed.setData(true)) const filterPanel: BaseUIElement = new LayerFilterPanel(state, filteredLayer) - return new Toggle( new Combine([layerChecked, filterPanel]), layerNotChecked, filteredLayer.isDisplayed - ); + ) } } export class LayerFilterPanel extends Combine { - public constructor(state: any, flayer: FilteredLayer) { const layer = flayer.layerDef if (layer.filters.length === 0) { super([]) - return undefined; + return undefined } - - const toShow: BaseUIElement [] = [] + const toShow: BaseUIElement[] = [] for (const filter of layer.filters) { - const [ui, actualTags] = LayerFilterPanel.createFilter(state, filter) ui.SetClass("mt-1") toShow.push(ui) - actualTags.addCallbackAndRun(tagsToFilterFor => { + actualTags.addCallbackAndRun((tagsToFilterFor) => { flayer.appliedFilters.data.set(filter.id, tagsToFilterFor) flayer.appliedFilters.ping() }) - flayer.appliedFilters.map(dict => dict.get(filter.id)) - .addCallbackAndRun(filters => actualTags.setData(filters)) - - + flayer.appliedFilters + .map((dict) => dict.get(filter.id)) + .addCallbackAndRun((filters) => actualTags.setData(filters)) } super(toShow) @@ -187,37 +185,53 @@ export class LayerFilterPanel extends Combine { } // Filter which uses one or more textfields - private static createFilterWithFields(state: any, filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { - + private static createFilterWithFields( + state: any, + filterConfig: FilterConfig + ): [BaseUIElement, UIEventSource<FilterState>] { const filter = filterConfig.options[0] const mappings = new Map<string, BaseUIElement>() let allValid: Store<boolean> = new ImmutableStore(true) var allFields: InputElement<string>[] = [] const properties = new UIEventSource<any>({}) - for (const {name, type} of filter.fields) { - const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) - - const field = ValidatedTextField.ForType(type).ConstructInputElement({ - value - }).SetClass("inline-block") + for (const { name, type } of filter.fields) { + const value = QueryParameters.GetQueryParameter( + "filter-" + filterConfig.id + "-" + name, + "", + "Value for filter " + filterConfig.id + ) + + const field = ValidatedTextField.ForType(type) + .ConstructInputElement({ + value, + }) + .SetClass("inline-block") mappings.set(name, field) const stable = value.stabilized(250) - stable.addCallbackAndRunD(v => { - properties.data[name] = v.toLowerCase(); + stable.addCallbackAndRunD((v) => { + properties.data[name] = v.toLowerCase() properties.ping() }) allFields.push(field) - allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable]) + allValid = allValid.map( + (previous) => previous && field.IsValid(stable.data) && stable.data !== "", + [stable] + ) } - const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), state, mappings) - const trigger: Store<FilterState> = allValid.map(isValid => { - if (!isValid) { - return undefined - } - const props = properties.data - // Replace all the field occurences in the tags... - const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, - v => { + const tr = new SubstitutedTranslation( + filter.question, + new UIEventSource<any>({ id: filterConfig.id }), + state, + mappings + ) + const trigger: Store<FilterState> = allValid.map( + (isValid) => { + if (!isValid) { + return undefined + } + const props = properties.data + // Replace all the field occurences in the tags... + const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, (v) => { if (typeof v !== "string") { return v } @@ -227,57 +241,72 @@ export class LayerFilterPanel extends Combine { } return v + }) + const tagsFilter = TagUtils.Tag(tagsSpec) + return { + currentFilter: tagsFilter, + state: JSON.stringify(props), } - ) - const tagsFilter = TagUtils.Tag(tagsSpec) - return { - currentFilter: tagsFilter, - state: JSON.stringify(props) - } - }, [properties]) + }, + [properties] + ) const settableFilter = new UIEventSource<FilterState>(undefined) - trigger.addCallbackAndRun(state => settableFilter.setData(state)) - settableFilter.addCallback(state => { + trigger.addCallbackAndRun((state) => settableFilter.setData(state)) + settableFilter.addCallback((state) => { if (state === undefined) { // still initializing return } if (state.currentFilter === undefined) { - allFields.forEach(f => f.GetValue().setData(undefined)); + allFields.forEach((f) => f.GetValue().setData(undefined)) } }) - return [tr, settableFilter]; + return [tr, settableFilter] } - private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { - let option = filterConfig.options[0]; + private static createCheckboxFilter( + filterConfig: FilterConfig + ): [BaseUIElement, UIEventSource<FilterState>] { + let option = filterConfig.options[0] - const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6"); - const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6"); + const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6") + const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6") const toggle = new ClickableToggle( new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"), - new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex") + new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass( + "flex" + ) ) .ToggleOnClick() .SetClass("block m-1") - return [toggle, toggle.isEnabled.sync(enabled => enabled ? { - currentFilter: option.osmTags, - state: "true" - } : undefined, [], - f => f !== undefined) + return [ + toggle, + toggle.isEnabled.sync( + (enabled) => + enabled + ? { + currentFilter: option.osmTags, + state: "true", + } + : undefined, + [], + (f) => f !== undefined + ), ] } - private static createMultiFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { - - let options = filterConfig.options; + private static createMultiFilter( + filterConfig: FilterConfig + ): [BaseUIElement, UIEventSource<FilterState>] { + let options = filterConfig.options const values: FilterState[] = options.map((f, i) => ({ - currentFilter: f.osmTags, state: i + currentFilter: f.osmTags, + state: i, })) let filterPicker: InputElement<number> @@ -288,36 +317,43 @@ export class LayerFilterPanel extends Combine { new FixedInputElement(option.question.Clone().SetClass("block"), i) ), { - dontStyle: true + dontStyle: true, } - ); + ) } else { - filterPicker = new DropDown("", options.map((option, i) => ({ - value: i, shown: option.question.Clone() - }))) + filterPicker = new DropDown( + "", + options.map((option, i) => ({ + value: i, + shown: option.question.Clone(), + })) + ) } - return [filterPicker, + return [ + filterPicker, filterPicker.GetValue().sync( - i => values[i], + (i) => values[i], [], - selected => { + (selected) => { const v = selected?.state if (v === undefined || typeof v === "string") { return undefined } return v } - )] + ), + ] } - private static createFilter(state: {}, filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { - + private static createFilter( + state: {}, + filterConfig: FilterConfig + ): [BaseUIElement, UIEventSource<FilterState>] { if (filterConfig.options[0].fields.length > 0) { return LayerFilterPanel.createFilterWithFields(state, filterConfig) } - if (filterConfig.options.length === 1) { return LayerFilterPanel.createCheckboxFilter(filterConfig) } diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 6559d83e6..87ea2a053 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -1,43 +1,44 @@ -import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import ShareScreen from "./ShareScreen"; -import MoreScreen from "./MoreScreen"; -import Constants from "../../Models/Constants"; -import Combine from "../Base/Combine"; -import {TabbedComponent} from "../Base/TabbedComponent"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "../Input/Toggle"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Utils} from "../../Utils"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import Loc from "../../Models/Loc"; -import BaseLayer from "../../Models/BaseLayer"; -import FilteredLayer from "../../Models/FilteredLayer"; -import CopyrightPanel from "./CopyrightPanel"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import PrivacyPolicy from "./PrivacyPolicy"; +import ThemeIntroductionPanel from "./ThemeIntroductionPanel" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import ShareScreen from "./ShareScreen" +import MoreScreen from "./MoreScreen" +import Constants from "../../Models/Constants" +import Combine from "../Base/Combine" +import { TabbedComponent } from "../Base/TabbedComponent" +import { UIEventSource } from "../../Logic/UIEventSource" +import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import BaseUIElement from "../BaseUIElement" +import Toggle from "../Input/Toggle" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Utils } from "../../Utils" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import Loc from "../../Models/Loc" +import BaseLayer from "../../Models/BaseLayer" +import FilteredLayer from "../../Models/FilteredLayer" +import CopyrightPanel from "./CopyrightPanel" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import PrivacyPolicy from "./PrivacyPolicy" export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { + public static MoreThemesTabIndex = 1 - public static MoreThemesTabIndex = 1; - - constructor(isShown: UIEventSource<boolean>, - currentTab: UIEventSource<number>, - state: { - layoutToUse: LayoutConfig, - osmConnection: OsmConnection, - featureSwitchShareScreen: UIEventSource<boolean>, - featureSwitchMoreQuests: UIEventSource<boolean>, - locationControl: UIEventSource<Loc>, - featurePipeline: FeaturePipeline, - backgroundLayer: UIEventSource<BaseLayer>, - filteredLayers: UIEventSource<FilteredLayer[]> - } & UserRelatedState) { - const layoutToUse = state.layoutToUse; + constructor( + isShown: UIEventSource<boolean>, + currentTab: UIEventSource<number>, + state: { + layoutToUse: LayoutConfig + osmConnection: OsmConnection + featureSwitchShareScreen: UIEventSource<boolean> + featureSwitchMoreQuests: UIEventSource<boolean> + locationControl: UIEventSource<Loc> + featurePipeline: FeaturePipeline + backgroundLayer: UIEventSource<BaseLayer> + filteredLayers: UIEventSource<FilteredLayer[]> + } & UserRelatedState + ) { + const layoutToUse = state.layoutToUse super( () => layoutToUse.title.Clone(), () => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown), @@ -46,83 +47,99 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { ) } - private static ConstructBaseTabs(state: { layoutToUse: LayoutConfig; osmConnection: OsmConnection; featureSwitchShareScreen: UIEventSource<boolean>; featureSwitchMoreQuests: UIEventSource<boolean>; featurePipeline: FeaturePipeline; locationControl: UIEventSource<Loc>; backgroundLayer: UIEventSource<BaseLayer>; filteredLayers: UIEventSource<FilteredLayer[]> } & UserRelatedState, - isShown: UIEventSource<boolean>, currentTab: UIEventSource<number>): - { header: string | BaseUIElement; content: BaseUIElement }[] { - - const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [ - {header: `<img src='${state.layoutToUse.icon}'>`, content: new ThemeIntroductionPanel(isShown, currentTab, state)}, + private static ConstructBaseTabs( + state: { + layoutToUse: LayoutConfig + osmConnection: OsmConnection + featureSwitchShareScreen: UIEventSource<boolean> + featureSwitchMoreQuests: UIEventSource<boolean> + featurePipeline: FeaturePipeline + locationControl: UIEventSource<Loc> + backgroundLayer: UIEventSource<BaseLayer> + filteredLayers: UIEventSource<FilteredLayer[]> + } & UserRelatedState, + isShown: UIEventSource<boolean>, + currentTab: UIEventSource<number> + ): { header: string | BaseUIElement; content: BaseUIElement }[] { + const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [ + { + header: `<img src='${state.layoutToUse.icon}'>`, + content: new ThemeIntroductionPanel(isShown, currentTab, state), + }, ] - if (state.featureSwitchMoreQuests.data) { tabs.push({ header: Svg.add_img, - content: - new Combine([ - Translations.t.general.morescreen.intro, - new MoreScreen(state) - ]).SetClass("flex flex-col") - }); + content: new Combine([ + Translations.t.general.morescreen.intro, + new MoreScreen(state), + ]).SetClass("flex flex-col"), + }) } - if (state.featureSwitchShareScreen.data) { - tabs.push({header: Svg.share_img, content: new ShareScreen(state)}); + tabs.push({ header: Svg.share_img, content: new ShareScreen(state) }) } const copyright = { header: Svg.copyright_svg(), - content: - new Combine( - [ - Translations.t.general.openStreetMapIntro.SetClass("link-underline"), - new CopyrightPanel(state) - ] - ) + content: new Combine([ + Translations.t.general.openStreetMapIntro.SetClass("link-underline"), + new CopyrightPanel(state), + ]), } tabs.push(copyright) const privacy = { header: Svg.eye_svg(), - content: new PrivacyPolicy() + content: new PrivacyPolicy(), } tabs.push(privacy) - return tabs; + return tabs } - private static GenerateContents(state: { - layoutToUse: LayoutConfig, - osmConnection: OsmConnection, - featureSwitchShareScreen: UIEventSource<boolean>, - featureSwitchMoreQuests: UIEventSource<boolean>, - featurePipeline: FeaturePipeline, - locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> - } & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) { - + private static GenerateContents( + state: { + layoutToUse: LayoutConfig + osmConnection: OsmConnection + featureSwitchShareScreen: UIEventSource<boolean> + featureSwitchMoreQuests: UIEventSource<boolean> + featurePipeline: FeaturePipeline + locationControl: UIEventSource<Loc> + backgroundLayer: UIEventSource<BaseLayer> + filteredLayers: UIEventSource<FilteredLayer[]> + } & UserRelatedState, + currentTab: UIEventSource<number>, + isShown: UIEventSource<boolean> + ) { const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab) - const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab)] - + const tabsWithAboutMc = [ + ...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab), + ] tabsWithAboutMc.push({ - header: Svg.help, - content: new Combine([Translations.t.general.aboutMapcomplete - .Subs({"osmcha_link": Utils.OsmChaLinkFor(7)}), "<br/>Version " + Constants.vNumber]) - .SetClass("link-underline") - } - ); + header: Svg.help, + content: new Combine([ + Translations.t.general.aboutMapcomplete.Subs({ + osmcha_link: Utils.OsmChaLinkFor(7), + }), + "<br/>Version " + Constants.vNumber, + ]).SetClass("link-underline"), + }) - tabs.forEach(c => c.content.SetClass("p-4")) - tabsWithAboutMc.forEach(c => c.content.SetClass("p-4")) + tabs.forEach((c) => c.content.SetClass("p-4")) + tabsWithAboutMc.forEach((c) => c.content.SetClass("p-4")) return new Toggle( new TabbedComponent(tabsWithAboutMc, currentTab), new TabbedComponent(tabs, currentTab), - state.osmConnection.userDetails.map((userdetails: UserDetails) => - userdetails.loggedIn && - userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock) + state.osmConnection.userDetails.map( + (userdetails: UserDetails) => + userdetails.loggedIn && + userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock + ) ) } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/Histogram.ts b/UI/BigComponents/Histogram.ts index 7b9956496..2e8886061 100644 --- a/UI/BigComponents/Histogram.ts +++ b/UI/BigComponents/Histogram.ts @@ -1,14 +1,13 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Table from "../Base/Table"; -import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import Svg from "../../Svg"; +import { VariableUiElement } from "../Base/VariableUIElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Table from "../Base/Table" +import Combine from "../Base/Combine" +import { FixedUiElement } from "../Base/FixedUiElement" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import Svg from "../../Svg" export default class Histogram<T> extends VariableUiElement { - private static defaultPalette = [ "#ff5858", "#ffad48", @@ -16,29 +15,35 @@ export default class Histogram<T> extends VariableUiElement { "#56bd56", "#63a9ff", "#9d62d9", - "#fa61fa" + "#fa61fa", ] - constructor(values: Store<string[]>, - title: string | BaseUIElement, - countTitle: string | BaseUIElement, - options?: { - assignColor?: (t0: string) => string, - sortMode?: "name" | "name-rev" | "count" | "count-rev" - } + constructor( + values: Store<string[]>, + title: string | BaseUIElement, + countTitle: string | BaseUIElement, + options?: { + assignColor?: (t0: string) => string + sortMode?: "name" | "name-rev" | "count" | "count-rev" + } ) { - const sortMode = new UIEventSource<"name" | "name-rev" | "count" | "count-rev">(options?.sortMode ?? "name") - const sortName = new VariableUiElement(sortMode.map(m => { - switch (m) { - case "name": - return Svg.up_svg() - case "name-rev": - return Svg.up_svg().SetStyle("transform: rotate(180deg)") - default: - return Svg.circle_svg() - } - })) - const titleHeader = new Combine([sortName.SetClass("w-4 mr-2"), title]).SetClass("flex") + const sortMode = new UIEventSource<"name" | "name-rev" | "count" | "count-rev">( + options?.sortMode ?? "name" + ) + const sortName = new VariableUiElement( + sortMode.map((m) => { + switch (m) { + case "name": + return Svg.up_svg() + case "name-rev": + return Svg.up_svg().SetStyle("transform: rotate(180deg)") + default: + return Svg.circle_svg() + } + }) + ) + const titleHeader = new Combine([sortName.SetClass("w-4 mr-2"), title]) + .SetClass("flex") .onClick(() => { if (sortMode.data === "name") { sortMode.setData("name-rev") @@ -47,91 +52,103 @@ export default class Histogram<T> extends VariableUiElement { } }) - const sortCount = new VariableUiElement(sortMode.map(m => { - switch (m) { - case "count": - return Svg.up_svg() - case "count-rev": - return Svg.up_svg().SetStyle("transform: rotate(180deg)") - default: - return Svg.circle_svg() - } - })) - - - const countHeader = new Combine([sortCount.SetClass("w-4 mr-2"), countTitle]).SetClass("flex").onClick(() => { - if (sortMode.data === "count-rev") { - sortMode.setData("count") - } else { - sortMode.setData("count-rev") - } - }) - - const header = [ - titleHeader, - countHeader - ] - - super(values.map(values => { - - if (values === undefined) { - return undefined; - } - - values = Utils.NoNull(values) - - const counts = new Map<string, number>() - for (const value of values) { - const c = counts.get(value) ?? 0; - counts.set(value, c + 1); - } - - const keys = Array.from(counts.keys()); - - switch (sortMode.data) { - case "name": - keys.sort() - break; - case "name-rev": - keys.sort().reverse(/*Copy of array, inplace reverse if fine*/) - break; - case "count": - keys.sort((k0, k1) => counts.get(k0) - counts.get(k1)) - break; - case "count-rev": - keys.sort((k0, k1) => counts.get(k1) - counts.get(k0)) - break; - } - - const max = Math.max(...Array.from(counts.values())) - - const fallbackColor = (keyValue: string) => { - const index = keys.indexOf(keyValue) - return Histogram.defaultPalette[index % Histogram.defaultPalette.length] - }; - let actualAssignColor = undefined; - if (options?.assignColor === undefined) { - actualAssignColor = fallbackColor; - } else { - actualAssignColor = (keyValue: string) => { - return options.assignColor(keyValue) ?? fallbackColor(keyValue) + const sortCount = new VariableUiElement( + sortMode.map((m) => { + switch (m) { + case "count": + return Svg.up_svg() + case "count-rev": + return Svg.up_svg().SetStyle("transform: rotate(180deg)") + default: + return Svg.circle_svg() } - } + }) + ) - return new Table( - header, - keys.map(key => [ - key, - new Combine([ - new Combine([new FixedUiElement("" + counts.get(key)).SetClass("font-bold rounded-full block")]) - .SetClass("flex justify-center rounded border border-black") - .SetStyle(`background: ${actualAssignColor(key)}; width: ${100 * counts.get(key) / max}%`) - ]).SetClass("block w-full") - - ]),{ - contentStyle:keys.map(_ => ["width: 20%"]) + const countHeader = new Combine([sortCount.SetClass("w-4 mr-2"), countTitle]) + .SetClass("flex") + .onClick(() => { + if (sortMode.data === "count-rev") { + sortMode.setData("count") + } else { + sortMode.setData("count-rev") } - ).SetClass("w-full zebra-table"); - }, [sortMode])); + }) + + const header = [titleHeader, countHeader] + + super( + values.map( + (values) => { + if (values === undefined) { + return undefined + } + + values = Utils.NoNull(values) + + const counts = new Map<string, number>() + for (const value of values) { + const c = counts.get(value) ?? 0 + counts.set(value, c + 1) + } + + const keys = Array.from(counts.keys()) + + switch (sortMode.data) { + case "name": + keys.sort() + break + case "name-rev": + keys.sort().reverse(/*Copy of array, inplace reverse if fine*/) + break + case "count": + keys.sort((k0, k1) => counts.get(k0) - counts.get(k1)) + break + case "count-rev": + keys.sort((k0, k1) => counts.get(k1) - counts.get(k0)) + break + } + + const max = Math.max(...Array.from(counts.values())) + + const fallbackColor = (keyValue: string) => { + const index = keys.indexOf(keyValue) + return Histogram.defaultPalette[index % Histogram.defaultPalette.length] + } + let actualAssignColor = undefined + if (options?.assignColor === undefined) { + actualAssignColor = fallbackColor + } else { + actualAssignColor = (keyValue: string) => { + return options.assignColor(keyValue) ?? fallbackColor(keyValue) + } + } + + return new Table( + header, + keys.map((key) => [ + key, + new Combine([ + new Combine([ + new FixedUiElement("" + counts.get(key)).SetClass( + "font-bold rounded-full block" + ), + ]) + .SetClass("flex justify-center rounded border border-black") + .SetStyle( + `background: ${actualAssignColor(key)}; width: ${ + (100 * counts.get(key)) / max + }%` + ), + ]).SetClass("block w-full"), + ]), + { + contentStyle: keys.map((_) => ["width: 20%"]), + } + ).SetClass("w-full zebra-table") + }, + [sortMode] + ) + ) } -} \ No newline at end of file +} diff --git a/UI/BigComponents/IndexText.ts b/UI/BigComponents/IndexText.ts index 86e5d8919..371a9e635 100644 --- a/UI/BigComponents/IndexText.ts +++ b/UI/BigComponents/IndexText.ts @@ -1,27 +1,29 @@ -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import { FixedUiElement } from "../Base/FixedUiElement" export default class IndexText extends Combine { constructor() { super([ - new FixedUiElement(`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`) - .SetClass("flex-none m-3"), + new FixedUiElement( + `<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">` + ).SetClass("flex-none m-3"), new Combine([ - Translations.t.index.title - .SetClass("text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline"), + Translations.t.index.title.SetClass( + "text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline" + ), Translations.t.index.intro.SetClass( - "mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"), + "mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0" + ), - Translations.t.index.pickTheme.SetClass("mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0") + Translations.t.index.pickTheme.SetClass( + "mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0" + ), + ]).SetClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4"), + ]) - ]).SetClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4") - ]); - - - this.SetClass("flex flex-row"); + this.SetClass("flex flex-row") } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index a53f51df3..0c4fc6631 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -1,93 +1,102 @@ -import Combine from "../Base/Combine"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; -import Translations from "../i18n/Translations"; -import Toggle from "../Input/Toggle"; -import MapControlButton from "../MapControlButton"; -import Svg from "../../Svg"; -import AllDownloads from "./AllDownloads"; -import FilterView from "./FilterView"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import BackgroundMapSwitch from "./BackgroundMapSwitch"; -import Lazy from "../Base/Lazy"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import FeatureInfoBox from "../Popup/FeatureInfoBox"; -import CopyrightPanel from "./CopyrightPanel"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; +import Combine from "../Base/Combine" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import Translations from "../i18n/Translations" +import Toggle from "../Input/Toggle" +import MapControlButton from "../MapControlButton" +import Svg from "../../Svg" +import AllDownloads from "./AllDownloads" +import FilterView from "./FilterView" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import BackgroundMapSwitch from "./BackgroundMapSwitch" +import Lazy from "../Base/Lazy" +import { VariableUiElement } from "../Base/VariableUIElement" +import FeatureInfoBox from "../Popup/FeatureInfoBox" +import CopyrightPanel from "./CopyrightPanel" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" export default class LeftControls extends Combine { - - constructor(state: FeaturePipelineState, - guiState: { - currentViewControlIsOpened: UIEventSource<boolean>; - downloadControlIsOpened: UIEventSource<boolean>, - filterViewIsOpened: UIEventSource<boolean>, - copyrightViewIsOpened: UIEventSource<boolean> - }) { - - + constructor( + state: FeaturePipelineState, + guiState: { + currentViewControlIsOpened: UIEventSource<boolean> + downloadControlIsOpened: UIEventSource<boolean> + filterViewIsOpened: UIEventSource<boolean> + copyrightViewIsOpened: UIEventSource<boolean> + } + ) { const currentViewFL = state.currentView?.layer const currentViewAction = new Toggle( new Lazy(() => { - const feature: Store<any> = state.currentView.features.map(ffs => ffs[0]?.feature) - const icon = new VariableUiElement(feature.map(feature => { - const defaultIcon = Svg.checkbox_empty_svg() - if (feature === undefined) { - return defaultIcon; - } - const tags = {...feature.properties, button: "yes"} - const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon(new UIEventSource(tags)); - if (elem === undefined) { - return defaultIcon - } - return elem - })).SetClass("inline-block w-full h-full") - - const featureBox = new VariableUiElement(feature.map(feature => { - if (feature === undefined) { - return undefined - } - return new Lazy(() => { - const tagsSource = state.allElements.getEventSourceById(feature.properties.id) - return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, { - hashToShow: "currentview", - isShown: guiState.currentViewControlIsOpened - }) - .SetClass("md:floating-element-width") + const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0]?.feature) + const icon = new VariableUiElement( + feature.map((feature) => { + const defaultIcon = Svg.checkbox_empty_svg() + if (feature === undefined) { + return defaultIcon + } + const tags = { ...feature.properties, button: "yes" } + const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon( + new UIEventSource(tags) + ) + if (elem === undefined) { + return defaultIcon + } + return elem }) - })).SetStyle("width: 40rem").SetClass("block") + ).SetClass("inline-block w-full h-full") + const featureBox = new VariableUiElement( + feature.map((feature) => { + if (feature === undefined) { + return undefined + } + return new Lazy(() => { + const tagsSource = state.allElements.getEventSourceById( + feature.properties.id + ) + return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, { + hashToShow: "currentview", + isShown: guiState.currentViewControlIsOpened, + }).SetClass("md:floating-element-width") + }) + }) + ) + .SetStyle("width: 40rem") + .SetClass("block") return new Toggle( featureBox, new MapControlButton(icon), guiState.currentViewControlIsOpened ) - }).onClick(() => { guiState.currentViewControlIsOpened.setData(true) }), - undefined, - new UIEventSource<boolean>(currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null) + new UIEventSource<boolean>( + currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null + ) ) const toggledDownload = new Toggle( - new AllDownloads( - guiState.downloadControlIsOpened, - state - ).SetClass("block p-1 rounded-full md:floating-element-width"), - new MapControlButton(Svg.download_svg()) - .onClick(() => guiState.downloadControlIsOpened.setData(true)), + new AllDownloads(guiState.downloadControlIsOpened, state).SetClass( + "block p-1 rounded-full md:floating-element-width" + ), + new MapControlButton(Svg.download_svg()).onClick(() => + guiState.downloadControlIsOpened.setData(true) + ), guiState.downloadControlIsOpened ) const downloadButtonn = new Toggle( toggledDownload, undefined, - state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data, - [state.featureSwitchExportAsPdf]) - ); + state.featureSwitchEnableExport.map( + (downloadEnabled) => downloadEnabled || state.featureSwitchExportAsPdf.data, + [state.featureSwitchExportAsPdf] + ) + ) const toggledFilter = new Toggle( new ScrollableFullScreen( @@ -99,16 +108,13 @@ export default class LeftControls extends Combine { "filters", guiState.filterViewIsOpened ).SetClass("rounded-lg md:floating-element-width"), - new MapControlButton(Svg.layers_svg()) - .onClick(() => guiState.filterViewIsOpened.setData(true)), + new MapControlButton(Svg.layers_svg()).onClick(() => + guiState.filterViewIsOpened.setData(true) + ), guiState.filterViewIsOpened ) - const filterButton = new Toggle( - toggledFilter, - undefined, - state.featureSwitchFilter - ); + const filterButton = new Toggle(toggledFilter, undefined, state.featureSwitchFilter) const mapSwitch = new Toggle( new BackgroundMapSwitch(state, state.backgroundLayer), @@ -119,32 +125,26 @@ export default class LeftControls extends Combine { // If the welcomeMessage is disabled, the copyright is hidden (as that is where the copyright is located const copyright = new Toggle( undefined, - new Lazy(() => - new Toggle( - new ScrollableFullScreen( - () => Translations.t.general.attribution.attributionTitle, - () => new CopyrightPanel(state), - "copyright", + new Lazy( + () => + new Toggle( + new ScrollableFullScreen( + () => Translations.t.general.attribution.attributionTitle, + () => new CopyrightPanel(state), + "copyright", + guiState.copyrightViewIsOpened + ), + new MapControlButton(Svg.copyright_svg()).onClick(() => + guiState.copyrightViewIsOpened.setData(true) + ), guiState.copyrightViewIsOpened - ), - new MapControlButton(Svg.copyright_svg()).onClick(() => guiState.copyrightViewIsOpened.setData(true)), - guiState.copyrightViewIsOpened - ) + ) ), state.featureSwitchWelcomeMessage ) - super([ - currentViewAction, - filterButton, - downloadButtonn, - copyright, - mapSwitch - ]) + super([currentViewAction, filterButton, downloadButtonn, copyright, mapSwitch]) this.SetClass("flex flex-col") - } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/LevelSelector.ts b/UI/BigComponents/LevelSelector.ts index fa84fd4b4..789380db9 100644 --- a/UI/BigComponents/LevelSelector.ts +++ b/UI/BigComponents/LevelSelector.ts @@ -1,36 +1,36 @@ -import FloorLevelInputElement from "../Input/FloorLevelInputElement"; -import MapState, {GlobalFilter} from "../../Logic/State/MapState"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import {RegexTag} from "../../Logic/Tags/RegexTag"; -import {Or} from "../../Logic/Tags/Or"; -import {Tag} from "../../Logic/Tags/Tag"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import {OsmFeature} from "../../Models/OsmFeature"; -import {BBox} from "../../Logic/BBox"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import {Store} from "../../Logic/UIEventSource"; +import FloorLevelInputElement from "../Input/FloorLevelInputElement" +import MapState, { GlobalFilter } from "../../Logic/State/MapState" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import { RegexTag } from "../../Logic/Tags/RegexTag" +import { Or } from "../../Logic/Tags/Or" +import { Tag } from "../../Logic/Tags/Tag" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import { OsmFeature } from "../../Models/OsmFeature" +import { BBox } from "../../Logic/BBox" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import { Store } from "../../Logic/UIEventSource" /*** * The element responsible for the level input element and picking the right level, showing and hiding at the right time, ... */ export default class LevelSelector extends Combine { - constructor(state: MapState & { featurePipeline: FeaturePipeline }) { - - const levelsInView : Store< Record<string, number>> = state.currentBounds.map(bbox => { + const levelsInView: Store<Record<string, number>> = state.currentBounds.map((bbox) => { if (bbox === undefined) { return {} } - const allElementsUnfiltered: OsmFeature[] = [].concat(...state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map(ff => ff.features)) - const allElements = allElementsUnfiltered.filter(f => BBox.get(f).overlapsWith(bbox)) - const allLevelsRaw: string[] = allElements.map(f => f.properties["level"]) - - const levels : Record<string, number> = {"0": 0} + const allElementsUnfiltered: OsmFeature[] = [].concat( + ...state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map((ff) => ff.features) + ) + const allElements = allElementsUnfiltered.filter((f) => BBox.get(f).overlapsWith(bbox)) + const allLevelsRaw: string[] = allElements.map((f) => f.properties["level"]) + + const levels: Record<string, number> = { "0": 0 } for (const levelDescription of allLevelsRaw) { - if(levelDescription === undefined){ - levels["0"] ++ + if (levelDescription === undefined) { + levels["0"]++ } for (const level of TagUtils.LevelsParser(levelDescription)) { levels[level] = (levels[level] ?? 0) + 1 @@ -46,61 +46,66 @@ export default class LevelSelector extends Combine { filter: { currentFilter: undefined, state: undefined, - }, id: "level", - onNewPoint: undefined + onNewPoint: undefined, }) - const isShown = levelsInView.map(levelsInView => { + const isShown = levelsInView.map( + (levelsInView) => { if (state.locationControl.data.zoom <= 16) { - return false; + return false } if (Object.keys(levelsInView).length == 1) { - return false; + return false } - - return true; + + return true }, - [state.locationControl]) + [state.locationControl] + ) function setLevelFilter() { - console.log("Updating levels filter to ", levelSelect.GetValue().data, " is shown:", isShown.data) - const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level") + console.log( + "Updating levels filter to ", + levelSelect.GetValue().data, + " is shown:", + isShown.data + ) + const filter: GlobalFilter = state.globalFilters.data.find((gf) => gf.id === "level") if (!isShown.data) { filter.filter = { state: "*", currentFilter: undefined, } filter.onNewPoint = undefined - state.globalFilters.ping(); + state.globalFilters.ping() return } const l = levelSelect.GetValue().data - if(l === undefined){ + if (l === undefined) { return } - let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")); + let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")) if (l === "0") { neededLevel = new Or([neededLevel, new Tag("level", "")]) } filter.filter = { state: l, - currentFilter: neededLevel + currentFilter: neededLevel, } const t = Translations.t.general.levelSelection filter.onNewPoint = { - confirmAddNew: t.confirmLevel.PartialSubs({level: l}), - safetyCheck: t.addNewOnLevel.Subs({level: l}), - tags: [new Tag("level", l)] + confirmAddNew: t.confirmLevel.PartialSubs({ level: l }), + safetyCheck: t.addNewOnLevel.Subs({ level: l }), + tags: [new Tag("level", l)], } - state.globalFilters.ping(); - return; + state.globalFilters.ping() + return } - - isShown.addCallbackAndRun(shown => { + isShown.addCallbackAndRun((shown) => { console.log("Is level selector shown?", shown) setLevelFilter() if (shown) { @@ -110,31 +115,36 @@ export default class LevelSelector extends Combine { } }) - - levelsInView.addCallbackAndRun(levels => { - if(!isShown.data){ + levelsInView.addCallbackAndRun((levels) => { + if (!isShown.data) { return } const value = levelSelect.GetValue() if (!(levels[value.data] === undefined || levels[value.data] === 0)) { - return; + return } // Nothing in view. Lets switch to a different level (the level with the most features) let mostElements = 0 let mostElementsLevel = undefined for (const level in levels) { const count = levels[level] - if(mostElementsLevel === undefined || mostElements < count){ + if (mostElementsLevel === undefined || mostElements < count) { mostElementsLevel = level mostElements = count } } - console.log("Force switching to a different level:", mostElementsLevel,"as it has",mostElements,"elements on that floor",levels,"(old level: "+value.data+")") - value.setData(mostElementsLevel ) - + console.log( + "Force switching to a different level:", + mostElementsLevel, + "as it has", + mostElements, + "elements on that floor", + levels, + "(old level: " + value.data + ")" + ) + value.setData(mostElementsLevel) }) - levelSelect.GetValue().addCallback(_ => setLevelFilter()) + levelSelect.GetValue().addCallback((_) => setLevelFilter()) super([levelSelect]) } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/LicensePicker.ts b/UI/BigComponents/LicensePicker.ts index 619aad54a..d7497b4b4 100644 --- a/UI/BigComponents/LicensePicker.ts +++ b/UI/BigComponents/LicensePicker.ts @@ -1,32 +1,33 @@ -import {DropDown} from "../Input/DropDown"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {Translation} from "../i18n/Translation"; +import { DropDown } from "../Input/DropDown" +import Translations from "../i18n/Translations" +import { UIEventSource } from "../../Logic/UIEventSource" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { Translation } from "../i18n/Translation" export default class LicensePicker extends DropDown<string> { - private static readonly cc0 = "CC0" private static readonly ccbysa = "CC-BY-SA 4.0" private static readonly ccby = "CC-BY 4.0" constructor(state: { osmConnection: OsmConnection }) { - super(Translations.t.image.willBePublished.Clone(), + super( + Translations.t.image.willBePublished.Clone(), [ - {value: LicensePicker.cc0, shown: Translations.t.image.cco.Clone()}, - {value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone()}, - {value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone()} + { value: LicensePicker.cc0, shown: Translations.t.image.cco.Clone() }, + { value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone() }, + { value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone() }, ], - state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource<string>("CC0"), + state?.osmConnection?.GetPreference("pictures-license") ?? + new UIEventSource<string>("CC0"), { - select_class:"w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200" + select_class: "w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200", } ) - this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left"); + this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left") } public static LicenseExplanations(): Map<string, Translation> { - let dict = new Map<string, Translation>(); + let dict = new Map<string, Translation>() dict.set(LicensePicker.cc0, Translations.t.image.ccoExplanation) dict.set(LicensePicker.ccby, Translations.t.image.ccbExplanation) @@ -34,5 +35,4 @@ export default class LicensePicker extends DropDown<string> { return dict } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/MapillaryLink.ts b/UI/BigComponents/MapillaryLink.ts index 84ff11028..96272204e 100644 --- a/UI/BigComponents/MapillaryLink.ts +++ b/UI/BigComponents/MapillaryLink.ts @@ -1,24 +1,29 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Loc from "../../Models/Loc"; -import Translations from "../i18n/Translations"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import Title from "../Base/Title"; +import { VariableUiElement } from "../Base/VariableUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Loc from "../../Models/Loc" +import Translations from "../i18n/Translations" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import Title from "../Base/Title" export class MapillaryLink extends VariableUiElement { constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string) { const t = Translations.t.general.attribution - super(state.locationControl.map(location => { - const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}` - return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), - new Combine([ - t.openMapillary.SetClass("font-bold"), - t.mapillaryHelp]), { - url: mapillaryLink, - newTab: true - }).SetClass("flex flex-col link-no-underline") - })) + super( + state.locationControl.map((location) => { + const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${ + location?.lat ?? 0 + }&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}` + return new SubtleButton( + Svg.mapillary_black_ui().SetStyle(iconStyle), + new Combine([t.openMapillary.SetClass("font-bold"), t.mapillaryHelp]), + { + url: mapillaryLink, + newTab: true, + } + ).SetClass("flex flex-col link-no-underline") + }) + ) } -} \ No newline at end of file +} diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index cc32e738a..4e3de61eb 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -1,82 +1,90 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import {SubtleButton} from "../Base/SubtleButton"; -import Translations from "../i18n/Translations"; +import { VariableUiElement } from "../Base/VariableUIElement" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import { SubtleButton } from "../Base/SubtleButton" +import Translations from "../i18n/Translations" import * as personal from "../../assets/themes/personal/personal.json" -import Constants from "../../Models/Constants"; -import BaseUIElement from "../BaseUIElement"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {ImmutableStore, Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; -import Loc from "../../Models/Loc"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import Toggle from "../Input/Toggle"; -import {Utils} from "../../Utils"; -import Title from "../Base/Title"; +import Constants from "../../Models/Constants" +import BaseUIElement from "../BaseUIElement" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource" +import Loc from "../../Models/Loc" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import Toggle from "../Input/Toggle" +import { Utils } from "../../Utils" +import Title from "../Base/Title" import * as themeOverview from "../../assets/generated/theme_overview.json" -import {Translation} from "../i18n/Translation"; -import {TextField} from "../Input/TextField"; -import FilteredCombine from "../Base/FilteredCombine"; -import Locale from "../i18n/Locale"; - +import { Translation } from "../i18n/Translation" +import { TextField } from "../Input/TextField" +import FilteredCombine from "../Base/FilteredCombine" +import Locale from "../i18n/Locale" export default class MoreScreen extends Combine { - private static readonly officialThemes: { - id: string, - icon: string, - title: any, - shortDescription: any, - definition?: any, - mustHaveLanguage?: boolean, - hideFromOverview: boolean, + id: string + icon: string + title: any + shortDescription: any + definition?: any + mustHaveLanguage?: boolean + hideFromOverview: boolean keywors?: any[] - }[] = themeOverview["default"]; - - constructor(state: UserRelatedState & { - locationControl?: UIEventSource<Loc>, - layoutToUse?: LayoutConfig - }, onMainScreen: boolean = false) { - const tr = Translations.t.general.morescreen; + }[] = themeOverview["default"] + + constructor( + state: UserRelatedState & { + locationControl?: UIEventSource<Loc> + layoutToUse?: LayoutConfig + }, + onMainScreen: boolean = false + ) { + const tr = Translations.t.general.morescreen let themeButtonStyle = "" let themeListStyle = "" if (onMainScreen) { themeButtonStyle = "h-32 min-h-32 max-h-32 text-ellipsis overflow-hidden" - themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" + themeListStyle = + "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" } const search = new TextField({ placeholder: tr.searchForATheme, }) - search.enterPressed.addCallbackD(searchTerm => { + search.enterPressed.addCallbackD((searchTerm) => { searchTerm = searchTerm.toLowerCase() - if(searchTerm === "personal"){ - window.location.href = MoreScreen.createUrlFor({id: "personal"}, false, state).data + if (searchTerm === "personal") { + window.location.href = MoreScreen.createUrlFor( + { id: "personal" }, + false, + state + ).data } - if(searchTerm === "bugs" || searchTerm === "issues") { + if (searchTerm === "bugs" || searchTerm === "issues") { window.location.href = "https://github.com/pietervdvn/MapComplete/issues" } - if(searchTerm === "source") { + if (searchTerm === "source") { window.location.href = "https://github.com/pietervdvn/MapComplete" } - if(searchTerm === "docs") { + if (searchTerm === "docs") { window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs" } - if(searchTerm === "osmcha" || searchTerm === "stats"){ + if (searchTerm === "osmcha" || searchTerm === "stats") { window.location.href = Utils.OsmChaLinkFor(7) } // Enter pressed -> search the first _official_ matchin theme and open it - const publicTheme = MoreScreen.officialThemes.find(th => - th.hideFromOverview == false && - th.id !== "personal" && - MoreScreen.MatchesLayoutFunc(th)(searchTerm)) + const publicTheme = MoreScreen.officialThemes.find( + (th) => + th.hideFromOverview == false && + th.id !== "personal" && + MoreScreen.MatchesLayoutFunc(th)(searchTerm) + ) if (publicTheme !== undefined) { window.location.href = MoreScreen.createUrlFor(publicTheme, false, state).data } - const hiddenTheme = MoreScreen.officialThemes.find(th => - th.id !== "personal" && - MoreScreen.MatchesLayoutFunc(th)(searchTerm)) + const hiddenTheme = MoreScreen.officialThemes.find( + (th) => th.id !== "personal" && MoreScreen.MatchesLayoutFunc(th)(searchTerm) + ) if (hiddenTheme !== undefined) { window.location.href = MoreScreen.createUrlFor(hiddenTheme, false, state).data } @@ -88,53 +96,71 @@ export default class MoreScreen extends Combine { document.addEventListener("keydown", function (event) { if (event.ctrlKey && event.code === "KeyF") { search.focus() - event.preventDefault(); + event.preventDefault() } - }); - - const searchBar = new Combine([Svg.search_svg().SetClass("w-8"), search.SetClass("mr-4 w-full")]) - .SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2") + }) + const searchBar = new Combine([ + Svg.search_svg().SetClass("w-8"), + search.SetClass("mr-4 w-full"), + ]).SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2") super([ new Combine([searchBar]).SetClass("flex justify-center"), - MoreScreen.createOfficialThemesList(state, themeButtonStyle, themeListStyle, search.GetValue()), - MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle, themeListStyle, search.GetValue()), - MoreScreen.createUnofficialThemeList(themeButtonStyle, state, themeListStyle, search.GetValue()), - tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10") - ]); + MoreScreen.createOfficialThemesList( + state, + themeButtonStyle, + themeListStyle, + search.GetValue() + ), + MoreScreen.createPreviouslyVistedHiddenList( + state, + themeButtonStyle, + themeListStyle, + search.GetValue() + ), + MoreScreen.createUnofficialThemeList( + themeButtonStyle, + state, + themeListStyle, + search.GetValue() + ), + tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10"), + ]) } private static NothingFound(search: UIEventSource<string>): BaseUIElement { - const t = Translations.t.general.morescreen; + const t = Translations.t.general.morescreen return new Combine([ new Title(t.noMatchingThemes, 5).SetClass("w-max font-bold"), - new SubtleButton(Svg.search_disable_ui(), t.noSearch, {imgSize: "h-6"}).SetClass("h-12 w-max") - .onClick(() => search.setData("")) + new SubtleButton(Svg.search_disable_ui(), t.noSearch, { imgSize: "h-6" }) + .SetClass("h-12 w-max") + .onClick(() => search.setData("")), ]).SetClass("flex flex-col items-center w-full") } - private static createUrlFor(layout: { id: string, definition?: string }, - isCustom: boolean, - state?: { locationControl?: UIEventSource<{ lat, lon, zoom }>, layoutToUse?: { id } } + private static createUrlFor( + layout: { id: string; definition?: string }, + isCustom: boolean, + state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } } ): Store<string> { if (layout === undefined) { - return undefined; + return undefined } if (layout.id === undefined) { - console.error("ID is undefined for layout", layout); - return undefined; + console.error("ID is undefined for layout", layout) + return undefined } if (layout.id === state?.layoutToUse?.id) { - return undefined; + return undefined } - const currentLocation = state?.locationControl; + const currentLocation = state?.locationControl - let path = window.location.pathname; + let path = window.location.pathname // Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html' - path = path.substr(0, path.lastIndexOf("/")); + path = path.substr(0, path.lastIndexOf("/")) // Path will now contain '/dir/dir', or empty string in case of nothing if (path === "") { path = "." @@ -154,18 +180,19 @@ export default class MoreScreen extends Combine { hash = "#" + btoa(JSON.stringify(layout.definition)) } - return currentLocation?.map(currentLocation => { - const params = [ - ["z", currentLocation?.zoom], - ["lat", currentLocation?.lat], - ["lon", currentLocation?.lon] - ].filter(part => part[1] !== undefined) - .map(part => part[0] + "=" + part[1]) - .join("&") - return `${linkPrefix}${params}${hash}`; - }) ?? new ImmutableStore<string>(`${linkPrefix}`) - - + return ( + currentLocation?.map((currentLocation) => { + const params = [ + ["z", currentLocation?.zoom], + ["lat", currentLocation?.lat], + ["lon", currentLocation?.lon], + ] + .filter((part) => part[1] !== undefined) + .map((part) => part[0] + "=" + part[1]) + .join("&") + return `${linkPrefix}${params}${hash}` + }) ?? new ImmutableStore<string>(`${linkPrefix}`) + ) } /** @@ -174,135 +201,157 @@ export default class MoreScreen extends Combine { */ public static createLinkButton( state: { - locationControl?: UIEventSource<Loc>, + locationControl?: UIEventSource<Loc> layoutToUse?: LayoutConfig - }, layout: { - id: string, - icon: string, - title: any, - shortDescription: any, - definition?: any, + }, + layout: { + id: string + icon: string + title: any + shortDescription: any + definition?: any mustHaveLanguage?: boolean - }, isCustom: boolean = false - ): - BaseUIElement { - + }, + isCustom: boolean = false + ): BaseUIElement { const url = MoreScreen.createUrlFor(layout, isCustom, state) let content = new Combine([ - new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined), + new Translation( + layout.title, + !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined + ), new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", ]).SetClass("overflow-hidden flex flex-col") - - if(state.layoutToUse === undefined){ + + if (state.layoutToUse === undefined) { // Currently on the index screen: we style the buttons equally large content = new Combine([content]).SetClass("flex flex-col justify-center h-24") } - - return new SubtleButton(layout.icon, content, {url, newTab: false}); + + return new SubtleButton(layout.icon, content, { url, newTab: false }) } public static CreateProffessionalSerivesButton() { - const t = Translations.t.professional.indexPage; + const t = Translations.t.professional.indexPage return new Combine([ new Title(t.hook, 4), t.hookMore, - new SubtleButton(undefined, t.button, {url: "./professional.html"}), + new SubtleButton(undefined, t.button, { url: "./professional.html" }), ]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg") } - private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses: string, search: UIEventSource<string>): BaseUIElement { + private static createUnofficialThemeList( + buttonClass: string, + state: UserRelatedState, + themeListClasses: string, + search: UIEventSource<string> + ): BaseUIElement { var currentIds: Store<string[]> = state.installedUserThemes var stableIds = Stores.ListStabilized<string>(currentIds) return new VariableUiElement( - stableIds.map(ids => { - const allThemes: { element: BaseUIElement, predicate?: (s: string) => boolean }[] = [] + stableIds.map((ids) => { + const allThemes: { element: BaseUIElement; predicate?: (s: string) => boolean }[] = + [] for (const id of ids) { const themeInfo = state.GetUnofficialTheme(id) - if(themeInfo === undefined){ + if (themeInfo === undefined) { continue } const link = MoreScreen.createLinkButton(state, themeInfo, true) if (link !== undefined) { allThemes.push({ element: link.SetClass(buttonClass), - predicate: s => id.toLowerCase().indexOf(s) >= 0 + predicate: (s) => id.toLowerCase().indexOf(s) >= 0, }) } } if (allThemes.length <= 0) { - return undefined; + return undefined } return new Combine([ Translations.t.general.customThemeIntro, new FilteredCombine(allThemes, search, { innerClasses: themeListClasses, - onEmpty: MoreScreen.NothingFound(search) - }) - ]); - })); + onEmpty: MoreScreen.NothingFound(search), + }), + ]) + }) + ) } - private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string, search: UIEventSource<string>): BaseUIElement { + private static createPreviouslyVistedHiddenList( + state: UserRelatedState, + buttonClass: string, + themeListStyle: string, + search: UIEventSource<string> + ): BaseUIElement { const t = Translations.t.general.morescreen const prefix = "mapcomplete-hidden-theme-" - const hiddenThemes = themeOverview["default"].filter(layout => layout.hideFromOverview) + const hiddenThemes = themeOverview["default"].filter((layout) => layout.hideFromOverview) const hiddenTotal = hiddenThemes.length return new Toggle( new VariableUiElement( - state.osmConnection.preferencesHandler.preferences.map(allPreferences => { - const knownThemes: Set<string> = new Set(Utils.NoNull(Object.keys(allPreferences) - .filter(key => key.startsWith(prefix)) - .map(key => key.substring(prefix.length, key.length - "-enabled".length)))); + state.osmConnection.preferencesHandler.preferences.map((allPreferences) => { + const knownThemes: Set<string> = new Set( + Utils.NoNull( + Object.keys(allPreferences) + .filter((key) => key.startsWith(prefix)) + .map((key) => + key.substring(prefix.length, key.length - "-enabled".length) + ) + ) + ) if (knownThemes.size === 0) { return undefined } - const knownThemeDescriptions = hiddenThemes.filter(theme => knownThemes.has(theme.id)) - .map(theme => ({ - element: MoreScreen.createLinkButton(state, theme)?.SetClass(buttonClass), - predicate: MoreScreen.MatchesLayoutFunc(theme) - })); + const knownThemeDescriptions = hiddenThemes + .filter((theme) => knownThemes.has(theme.id)) + .map((theme) => ({ + element: MoreScreen.createLinkButton(state, theme)?.SetClass( + buttonClass + ), + predicate: MoreScreen.MatchesLayoutFunc(theme), + })) - const knownLayouts = new FilteredCombine(knownThemeDescriptions, - search, - { - innerClasses: themeListStyle, - onEmpty: MoreScreen.NothingFound(search) - } - ) + const knownLayouts = new FilteredCombine(knownThemeDescriptions, search, { + innerClasses: themeListStyle, + onEmpty: MoreScreen.NothingFound(search), + }) return new Combine([ new Title(t.previouslyHiddenTitle), t.hiddenExplanation.Subs({ hidden_discovered: "" + knownThemes.size, - total_hidden: "" + hiddenTotal + total_hidden: "" + hiddenTotal, }), - knownLayouts + knownLayouts, ]) - }) ).SetClass("flex flex-col"), undefined, state.osmConnection.isLoggedIn ) - - } private static MatchesLayoutFunc(layout: { - id: string, - title: any, - shortDescription: any, + id: string + title: any + shortDescription: any keywords?: any[] - }): ((search: string) => boolean) { + }): (search: string) => boolean { return (search: string) => { search = search.toLocaleLowerCase() if (layout.id.toLowerCase().indexOf(search) >= 0) { - return true; + return true } - const entitiesToSearch = [layout.shortDescription, layout.title, ...(layout.keywords ?? [])] + const entitiesToSearch = [ + layout.shortDescription, + layout.title, + ...(layout.keywords ?? []), + ] for (const entity of entitiesToSearch) { if (entity === undefined) { continue @@ -313,73 +362,73 @@ export default class MoreScreen extends Combine { } } - return false; + return false } } - private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string, themeListStyle: string, search: UIEventSource<string>): BaseUIElement { + private static createOfficialThemesList( + state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }, + buttonClass: string, + themeListStyle: string, + search: UIEventSource<string> + ): BaseUIElement { + let buttons: { element: BaseUIElement; predicate?: (s: string) => boolean }[] = + MoreScreen.officialThemes.map((layout) => { + if (layout === undefined) { + console.trace("Layout is undefined") + return undefined + } + if (layout.hideFromOverview) { + return undefined + } + const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass) + if (layout.id === personal.id) { + const element = new VariableUiElement( + state.osmConnection.userDetails + .map((userdetails) => userdetails.csCount) + .map((csCount) => { + if (csCount < Constants.userJourney.personalLayoutUnlock) { + return undefined + } else { + return button + } + }) + ) + return { element } + } + return { element: button, predicate: MoreScreen.MatchesLayoutFunc(layout) } + }) - - let buttons: { element: BaseUIElement, predicate?: (s: string) => boolean }[] = MoreScreen.officialThemes.map((layout) => { - - if (layout === undefined) { - console.trace("Layout is undefined") - return undefined - } - if (layout.hideFromOverview) { - return undefined; - } - const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass); - if (layout.id === personal.id) { - const element = new VariableUiElement( - state.osmConnection.userDetails.map(userdetails => userdetails.csCount) - .map(csCount => { - if (csCount < Constants.userJourney.personalLayoutUnlock) { - return undefined - } else { - return button - } - }) - ) - return {element} - } - - - return {element: button, predicate: MoreScreen.MatchesLayoutFunc(layout)}; - }) - - const professional = MoreScreen.CreateProffessionalSerivesButton(); + const professional = MoreScreen.CreateProffessionalSerivesButton() const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state) - buttons.splice(0, 0, - {element: customGeneratorLink}, - {element: professional}); + buttons.splice(0, 0, { element: customGeneratorLink }, { element: professional }) return new FilteredCombine(buttons, search, { innerClasses: themeListStyle, - onEmpty: MoreScreen.NothingFound(search) - }); + onEmpty: MoreScreen.NothingFound(search), + }) } /* - * Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets - * */ - private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement { - const tr = Translations.t.general.morescreen; + * Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets + * */ + private static createCustomGeneratorButton(state: { + osmConnection: OsmConnection + }): VariableUiElement { + const tr = Translations.t.general.morescreen return new VariableUiElement( - state.osmConnection.userDetails.map(userDetails => { + state.osmConnection.userDetails.map((userDetails) => { if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) { return new SubtleButton(null, tr.requestATheme.Clone(), { url: "https://github.com/pietervdvn/MapComplete/issues", - newTab: true - }); + newTab: true, + }) } return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), { url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html", - newTab: false - }); + newTab: false, + }) }) ) } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/PlantNetSpeciesSearch.ts b/UI/BigComponents/PlantNetSpeciesSearch.ts index 70e456385..5b133def2 100644 --- a/UI/BigComponents/PlantNetSpeciesSearch.ts +++ b/UI/BigComponents/PlantNetSpeciesSearch.ts @@ -1,17 +1,16 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import PlantNet from "../../Logic/Web/PlantNet"; -import Loading from "../Base/Loading"; -import Wikidata from "../../Logic/Web/Wikidata"; -import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"; -import {Button} from "../Base/Button"; -import Combine from "../Base/Combine"; -import Title from "../Base/Title"; -import WikipediaBox from "../Wikipedia/WikipediaBox"; -import Translations from "../i18n/Translations"; -import List from "../Base/List"; -import Svg from "../../Svg"; - +import { VariableUiElement } from "../Base/VariableUIElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import PlantNet from "../../Logic/Web/PlantNet" +import Loading from "../Base/Loading" +import Wikidata from "../../Logic/Web/Wikidata" +import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox" +import { Button } from "../Base/Button" +import Combine from "../Base/Combine" +import Title from "../Base/Title" +import WikipediaBox from "../Wikipedia/WikipediaBox" +import Translations from "../i18n/Translations" +import List from "../Base/List" +import Svg from "../../Svg" export default class PlantNetSpeciesSearch extends VariableUiElement { /*** @@ -23,99 +22,116 @@ export default class PlantNetSpeciesSearch extends VariableUiElement { const t = Translations.t.plantDetection super( images - .bind(images => { + .bind((images) => { if (images.length === 0) { return null } - return UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0,5))); + return UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0, 5))) }) - .map(result => { + .map((result) => { if (images.data.length === 0) { - return new Combine([t.takeImages, t.howTo.intro, new List( - [ - t.howTo.li0, - t.howTo.li1, - t.howTo.li2, - t.howTo.li3 - ] - )]).SetClass("flex flex-col") + return new Combine([ + t.takeImages, + t.howTo.intro, + new List([t.howTo.li0, t.howTo.li1, t.howTo.li2, t.howTo.li3]), + ]).SetClass("flex flex-col") + } + if (result === undefined) { + return new Loading(t.querying.Subs(images.data)) } - if (result === undefined) { - return new Loading(t.querying.Subs(images.data)) - } - - if (result["error"] !== undefined) { - return t.error.Subs(<any>result).SetClass("alert") - } - console.log(result) - const success = result["success"] - const selectedSpecies = new UIEventSource<string>(undefined) - const speciesInformation = success.results - .filter(species => species.score >= 0.005) - .map(species => { - const wikidata = UIEventSource.FromPromise(Wikidata.Sparql<{ species }>(["?species", "?speciesLabel"], - ["?species wdt:P846 \"" + species.gbif.id + "\""])); + if (result["error"] !== undefined) { + return t.error.Subs(<any>result).SetClass("alert") + } + console.log(result) + const success = result["success"] - const confirmButton = new Button(t.seeInfo, async() => { - await selectedSpecies.setData(wikidata.data[0].species?.value) - }).SetClass("btn") + const selectedSpecies = new UIEventSource<string>(undefined) + const speciesInformation = success.results + .filter((species) => species.score >= 0.005) + .map((species) => { + const wikidata = UIEventSource.FromPromise( + Wikidata.Sparql<{ species }>( + ["?species", "?speciesLabel"], + ['?species wdt:P846 "' + species.gbif.id + '"'] + ) + ) - const match = t.matchPercentage.Subs({match: Math.round(species.score * 100)}).SetClass("font-bold") + const confirmButton = new Button(t.seeInfo, async () => { + await selectedSpecies.setData(wikidata.data[0].species?.value) + }).SetClass("btn") - const extraItems = new Combine([match, confirmButton]).SetClass("flex flex-col") + const match = t.matchPercentage + .Subs({ match: Math.round(species.score * 100) }) + .SetClass("font-bold") - return new WikidataPreviewBox(wikidata.map(wd => wd == undefined ? undefined : wd[0]?.species?.value), - { - whileLoading: new Loading( - t.loadingWikidata.Subs({species: species.species.scientificNameWithoutAuthor})), - extraItems: [new Combine([extraItems])], + const extraItems = new Combine([match, confirmButton]).SetClass( + "flex flex-col" + ) - imageStyle: "max-width: 8rem; width: unset; height: 8rem" + return new WikidataPreviewBox( + wikidata.map((wd) => + wd == undefined ? undefined : wd[0]?.species?.value + ), + { + whileLoading: new Loading( + t.loadingWikidata.Subs({ + species: species.species.scientificNameWithoutAuthor, }) - .SetClass("border-2 border-subtle rounded-xl block mb-2") + ), + extraItems: [new Combine([extraItems])], + + imageStyle: "max-width: 8rem; width: unset; height: 8rem", } - ); - const plantOverview = new Combine([ - new Title(t.overviewTitle), - t.overviewIntro, - t.overviewVerify.SetClass("font-bold"), - ...speciesInformation]).SetClass("flex flex-col") + ).SetClass("border-2 border-subtle rounded-xl block mb-2") + }) + const plantOverview = new Combine([ + new Title(t.overviewTitle), + t.overviewIntro, + t.overviewVerify.SetClass("font-bold"), + ...speciesInformation, + ]).SetClass("flex flex-col") - - return new VariableUiElement(selectedSpecies.map(wikidataSpecies => { + return new VariableUiElement( + selectedSpecies.map((wikidataSpecies) => { if (wikidataSpecies === undefined) { return plantOverview } const buttons = new Combine([ new Button( - new Combine([ - Svg.back_svg().SetClass("w-6 mr-1 bg-white rounded-full p-1"), - t.back]).SetClass("flex"), + new Combine([ + Svg.back_svg().SetClass( + "w-6 mr-1 bg-white rounded-full p-1" + ), + t.back, + ]).SetClass("flex"), () => { - selectedSpecies.setData(undefined) - }).SetClass("btn btn-secondary"), - + selectedSpecies.setData(undefined) + } + ).SetClass("btn btn-secondary"), + new Button( - new Combine([Svg.confirm_svg().SetClass("w-6 mr-1"), t.confirm]).SetClass("flex") - , () => { - onConfirm(wikidataSpecies) - }).SetClass("btn"), - - ]).SetClass("flex justify-between"); + new Combine([ + Svg.confirm_svg().SetClass("w-6 mr-1"), + t.confirm, + ]).SetClass("flex"), + () => { + onConfirm(wikidataSpecies) + } + ).SetClass("btn"), + ]).SetClass("flex justify-between") return new Combine([ new WikipediaBox([wikidataSpecies], { firstParagraphOnly: false, noImages: false, - addHeader: false + addHeader: false, }).SetClass("h-96"), - buttons + buttons, ]).SetClass("flex flex-col self-end") - })) - - } - )) + }) + ) + }) + ) } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/PrivacyPolicy.ts b/UI/BigComponents/PrivacyPolicy.ts index b6955cb36..213d75fb4 100644 --- a/UI/BigComponents/PrivacyPolicy.ts +++ b/UI/BigComponents/PrivacyPolicy.ts @@ -1,6 +1,6 @@ -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import Title from "../Base/Title"; +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import Title from "../Base/Title" export default class PrivacyPolicy extends Combine { constructor() { @@ -19,8 +19,7 @@ export default class PrivacyPolicy extends Combine { t.miscCookies, new Title(t.whileYoureHere), t.surveillance, - - ]); + ]) this.SetClass("link-underline") } -} \ No newline at end of file +} diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index ac95b9a21..c6c4fd7a5 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -1,52 +1,42 @@ -import Combine from "../Base/Combine"; -import Toggle from "../Input/Toggle"; -import MapControlButton from "../MapControlButton"; -import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; -import Svg from "../../Svg"; -import MapState from "../../Logic/State/MapState"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import {Utils} from "../../Utils"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {BBox} from "../../Logic/BBox"; -import {OsmFeature} from "../../Models/OsmFeature"; -import LevelSelector from "./LevelSelector"; +import Combine from "../Base/Combine" +import Toggle from "../Input/Toggle" +import MapControlButton from "../MapControlButton" +import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" +import Svg from "../../Svg" +import MapState from "../../Logic/State/MapState" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import { Utils } from "../../Utils" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { BBox } from "../../Logic/BBox" +import { OsmFeature } from "../../Models/OsmFeature" +import LevelSelector from "./LevelSelector" export default class RightControls extends Combine { - constructor(state: MapState & { featurePipeline: FeaturePipeline }) { - - const geolocatioHandler = new GeoLocationHandler( - state - ) + const geolocatioHandler = new GeoLocationHandler(state) const geolocationButton = new Toggle( - new MapControlButton( - geolocatioHandler - , { - dontStyle: true - } - ), + new MapControlButton(geolocatioHandler, { + dontStyle: true, + }), undefined, state.featureSwitchGeolocation - ); + ) - const plus = new MapControlButton( - Svg.plus_svg() - ).onClick(() => { - state.locationControl.data.zoom++; - state.locationControl.ping(); - }); + const plus = new MapControlButton(Svg.plus_svg()).onClick(() => { + state.locationControl.data.zoom++ + state.locationControl.ping() + }) - const min = new MapControlButton( - Svg.min_svg() - ).onClick(() => { - state.locationControl.data.zoom--; - state.locationControl.ping(); - }); + const min = new MapControlButton(Svg.min_svg()).onClick(() => { + state.locationControl.data.zoom-- + state.locationControl.ping() + }) - const levelSelector = new LevelSelector(state); - super([levelSelector, plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) + const levelSelector = new LevelSelector(state) + super( + [levelSelector, plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1")) + ) this.SetClass("flex flex-col items-center") } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 824302360..8b01330a1 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -1,28 +1,24 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Translation} from "../i18n/Translation"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Svg from "../../Svg"; -import {TextField} from "../Input/TextField"; -import {Geocoding} from "../../Logic/Osm/Geocoding"; -import Translations from "../i18n/Translations"; -import Hash from "../../Logic/Web/Hash"; -import Combine from "../Base/Combine"; -import Locale from "../i18n/Locale"; +import { UIEventSource } from "../../Logic/UIEventSource" +import { Translation } from "../i18n/Translation" +import { VariableUiElement } from "../Base/VariableUIElement" +import Svg from "../../Svg" +import { TextField } from "../Input/TextField" +import { Geocoding } from "../../Logic/Osm/Geocoding" +import Translations from "../i18n/Translations" +import Hash from "../../Logic/Web/Hash" +import Combine from "../Base/Combine" +import Locale from "../i18n/Locale" export default class SearchAndGo extends Combine { - constructor(state: { - leafletMap: UIEventSource<any>, - selectedElement: UIEventSource<any> - }) { - const goButton = Svg.search_ui().SetClass( - "w-8 h-8 full-rounded border-black float-right" - ); + constructor(state: { leafletMap: UIEventSource<any>; selectedElement: UIEventSource<any> }) { + const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right") - const placeholder = new UIEventSource<Translation>( - Translations.t.general.search.search - ); + const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search) const searchField = new TextField({ - placeholder: placeholder.map(tr => tr.textFor(Locale.language.data), [Locale.language]), + placeholder: placeholder.map( + (tr) => tr.textFor(Locale.language.data), + [Locale.language] + ), value: new UIEventSource<string>(""), inputStyle: " background: transparent;\n" + @@ -32,53 +28,52 @@ export default class SearchAndGo extends Combine { " height: 100%;\n" + " box-sizing: border-box;\n" + " color: var(--foreground-color);", - }); + }) - searchField.SetClass("relative float-left mt-0 ml-2"); - searchField.SetStyle("width: calc(100% - 3em);height: 100%"); + searchField.SetClass("relative float-left mt-0 ml-2") + searchField.SetStyle("width: calc(100% - 3em);height: 100%") - super([searchField, goButton]); + super([searchField, goButton]) - this.SetClass("block h-8"); + this.SetClass("block h-8") this.SetStyle( "background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;" - ); + ) // Triggered by 'enter' or onclick async function runSearch() { - const searchString = searchField.GetValue().data; + const searchString = searchField.GetValue().data if (searchString === undefined || searchString === "") { - return; + return } - searchField.GetValue().setData(""); - placeholder.setData(Translations.t.general.search.searching); + searchField.GetValue().setData("") + placeholder.setData(Translations.t.general.search.searching) try { + const result = await Geocoding.Search(searchString) - const result = await Geocoding.Search(searchString); - - console.log("Search result", result); + console.log("Search result", result) if (result.length == 0) { - placeholder.setData(Translations.t.general.search.nothing); - return; + placeholder.setData(Translations.t.general.search.nothing) + return } - const poi = result[0]; - const bb = poi.boundingbox; + const poi = result[0] + const bb = poi.boundingbox const bounds: [[number, number], [number, number]] = [ [bb[0], bb[2]], [bb[1], bb[3]], - ]; - state.selectedElement.setData(undefined); - Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); - state.leafletMap.data.fitBounds(bounds); + ] + state.selectedElement.setData(undefined) + Hash.hash.setData(poi.osm_type + "/" + poi.osm_id) + state.leafletMap.data.fitBounds(bounds) placeholder.setData(Translations.t.general.search.search) - }catch(e){ - searchField.GetValue().setData(""); - placeholder.setData(Translations.t.general.search.error); + } catch (e) { + searchField.GetValue().setData("") + placeholder.setData(Translations.t.general.search.error) } } - searchField.enterPressed.addCallback(runSearch); - goButton.onClick(runSearch); + searchField.enterPressed.addCallback(runSearch) + goButton.onClick(runSearch) } } diff --git a/UI/BigComponents/ShareButton.ts b/UI/BigComponents/ShareButton.ts index a51d54648..a3af0390b 100644 --- a/UI/BigComponents/ShareButton.ts +++ b/UI/BigComponents/ShareButton.ts @@ -1,17 +1,20 @@ -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" export default class ShareButton extends BaseUIElement { - private _embedded: BaseUIElement; - private _shareData: () => { text: string; title: string; url: string }; + private _embedded: BaseUIElement + private _shareData: () => { text: string; title: string; url: string } - constructor(embedded: BaseUIElement, generateShareData: () => { - text: string, - title: string, - url: string - }) { - super(); - this._embedded = embedded; - this._shareData = generateShareData; + constructor( + embedded: BaseUIElement, + generateShareData: () => { + text: string + title: string + url: string + } + ) { + super() + this._embedded = embedded + this._shareData = generateShareData this.SetClass("share-button") } @@ -20,21 +23,21 @@ export default class ShareButton extends BaseUIElement { e.type = "button" e.appendChild(this._embedded.ConstructElement()) - e.addEventListener('click', () => { + e.addEventListener("click", () => { if (navigator.share) { - navigator.share(this._shareData()).then(() => { - console.log('Thanks for sharing!'); - }) - .catch(err => { - console.log(`Couldn't share because of`, err.message); - }); + navigator + .share(this._shareData()) + .then(() => { + console.log("Thanks for sharing!") + }) + .catch((err) => { + console.log(`Couldn't share because of`, err.message) + }) } else { - console.log('web share not supported'); + console.log("web share not supported") } - }); + }) - return e; + return e } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index 091c7996d..2f84ed88b 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -1,116 +1,138 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Translation} from "../i18n/Translation"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import Loc from "../../Models/Loc"; -import BaseLayer from "../../Models/BaseLayer"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {InputElement} from "../Input/InputElement"; -import {CheckBox} from "../Input/Checkboxes"; -import {SubtleButton} from "../Base/SubtleButton"; -import LZString from "lz-string"; +import { VariableUiElement } from "../Base/VariableUIElement" +import { Translation } from "../i18n/Translation" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { Utils } from "../../Utils" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import Loc from "../../Models/Loc" +import BaseLayer from "../../Models/BaseLayer" +import FilteredLayer from "../../Models/FilteredLayer" +import { InputElement } from "../Input/InputElement" +import { CheckBox } from "../Input/Checkboxes" +import { SubtleButton } from "../Base/SubtleButton" +import LZString from "lz-string" export default class ShareScreen extends Combine { - - constructor(state: { layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> }) { - const layout = state?.layoutToUse; - const tr = Translations.t.general.sharescreen; + constructor(state: { + layoutToUse: LayoutConfig + locationControl: UIEventSource<Loc> + backgroundLayer: UIEventSource<BaseLayer> + filteredLayers: UIEventSource<FilteredLayer[]> + }) { + const layout = state?.layoutToUse + const tr = Translations.t.general.sharescreen const optionCheckboxes: InputElement<boolean>[] = [] - const optionParts: (Store<string>)[] = []; + const optionParts: Store<string>[] = [] const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) - optionCheckboxes.push(includeLocation); + optionCheckboxes.push(includeLocation) - const currentLocation = state.locationControl; + const currentLocation = state.locationControl - optionParts.push(includeLocation.GetValue().map((includeL) => { - if (currentLocation === undefined) { - return null; - } - if (includeL) { - return [["z", currentLocation.data?.zoom], ["lat", currentLocation.data?.lat], ["lon", currentLocation.data?.lon]] - .filter(p => p[1] !== undefined) - .map(p => p[0] + "=" + p[1]) - .join("&") - } else { - return null; - } + optionParts.push( + includeLocation.GetValue().map( + (includeL) => { + if (currentLocation === undefined) { + return null + } + if (includeL) { + return [ + ["z", currentLocation.data?.zoom], + ["lat", currentLocation.data?.lat], + ["lon", currentLocation.data?.lon], + ] + .filter((p) => p[1] !== undefined) + .map((p) => p[0] + "=" + p[1]) + .join("&") + } else { + return null + } + }, + [currentLocation] + ) + ) - }, [currentLocation])); - - - function fLayerToParam(flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }) { + function fLayerToParam(flayer: { + isDisplayed: UIEventSource<boolean> + layerDef: LayerConfig + }) { if (flayer.isDisplayed.data) { - return null; // Being displayed is the default + return null // Being displayed is the default } return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data } - - const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = state.backgroundLayer; - const currentBackground = new VariableUiElement(currentLayer.map(layer => { - return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}); - })); + const currentLayer: UIEventSource<{ id: string; name: string; layer: any }> = + state.backgroundLayer + const currentBackground = new VariableUiElement( + currentLayer.map((layer) => { + return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" }) + }) + ) const includeCurrentBackground = new CheckBox(currentBackground, true) - optionCheckboxes.push(includeCurrentBackground); - optionParts.push(includeCurrentBackground.GetValue().map((includeBG) => { - if (includeBG) { - return "background=" + currentLayer.data.id - } else { - return null - } - }, [currentLayer])); - + optionCheckboxes.push(includeCurrentBackground) + optionParts.push( + includeCurrentBackground.GetValue().map( + (includeBG) => { + if (includeBG) { + return "background=" + currentLayer.data.id + } else { + return null + } + }, + [currentLayer] + ) + ) const includeLayerChoices = new CheckBox(tr.fsIncludeCurrentLayers, true) - optionCheckboxes.push(includeLayerChoices); - - optionParts.push(includeLayerChoices.GetValue().map((includeLayerSelection) => { - if (includeLayerSelection) { - return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&") - } else { - return null - } - }, state.filteredLayers.data.map((flayer) => flayer.isDisplayed))); + optionCheckboxes.push(includeLayerChoices) + optionParts.push( + includeLayerChoices.GetValue().map( + (includeLayerSelection) => { + if (includeLayerSelection) { + return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&") + } else { + return null + } + }, + state.filteredLayers.data.map((flayer) => flayer.isDisplayed) + ) + ) const switches = [ - {urlName: "fs-userbadge", human: tr.fsUserbadge}, - {urlName: "fs-search", human: tr.fsSearch}, - {urlName: "fs-welcome-message", human: tr.fsWelcomeMessage}, - {urlName: "fs-layers", human: tr.fsLayers}, - {urlName: "layer-control-toggle", human: tr.fsLayerControlToggle, reverse: true}, - {urlName: "fs-add-new", human: tr.fsAddNew}, - {urlName: "fs-geolocation", human: tr.fsGeolocation}, + { urlName: "fs-userbadge", human: tr.fsUserbadge }, + { urlName: "fs-search", human: tr.fsSearch }, + { urlName: "fs-welcome-message", human: tr.fsWelcomeMessage }, + { urlName: "fs-layers", human: tr.fsLayers }, + { urlName: "layer-control-toggle", human: tr.fsLayerControlToggle, reverse: true }, + { urlName: "fs-add-new", human: tr.fsAddNew }, + { urlName: "fs-geolocation", human: tr.fsGeolocation }, ] - for (const swtch of switches) { - const checkbox = new CheckBox(Translations.W(swtch.human), !swtch.reverse) - optionCheckboxes.push(checkbox); - optionParts.push(checkbox.GetValue().map((isEn) => { - if (isEn) { - if (swtch.reverse) { - return `${swtch.urlName}=true` + optionCheckboxes.push(checkbox) + optionParts.push( + checkbox.GetValue().map((isEn) => { + if (isEn) { + if (swtch.reverse) { + return `${swtch.urlName}=true` + } + return null + } else { + if (swtch.reverse) { + return null + } + return `${swtch.urlName}=false` } - return null; - } else { - if (swtch.reverse) { - return null; - } - return `${swtch.urlName}=false` - } - })) - - + }) + ) } if (layout.definitionRaw !== undefined) { @@ -119,10 +141,9 @@ export default class ShareScreen extends Combine { const options = new Combine(optionCheckboxes).SetClass("flex flex-col") const url = (currentLocation ?? new UIEventSource(undefined)).map(() => { - - const host = window.location.host; - let path = window.location.pathname; - path = path.substr(0, path.lastIndexOf("/")); + const host = window.location.host + let path = window.location.pathname + path = path.substr(0, path.lastIndexOf("/")) let id = layout.id.toLowerCase() if (layout.definitionRaw !== undefined) { id = "theme.html" @@ -133,28 +154,32 @@ export default class ShareScreen extends Combine { if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) { hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw)) } - const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data))); + const parts = Utils.NoEmpty( + Utils.NoNull(optionParts.map((eventSource) => eventSource.data)) + ) if (parts.length === 0) { - return literalText + hash; + return literalText + hash } - return literalText + "?" + parts.join("&") + hash; - }, optionParts); - + return literalText + "?" + parts.join("&") + hash + }, optionParts) const iframeCode = new VariableUiElement( url.map((url) => { return `<span class='literal-code iframe-code-block'> - <iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe> + <iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${ + layout.title?.txt ?? "MapComplete" + } with MapComplete"></iframe> </span>` }) - ); + ) - - const linkStatus = new UIEventSource<string | Translation>(""); + const linkStatus = new UIEventSource<string | Translation>("") const link = new VariableUiElement( - url.map((url) => `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`) + url.map( + (url) => + `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">` + ) ).onClick(async () => { - const shareData = { title: Translations.W(layout.title)?.ConstructElement().textContent ?? "", text: Translations.W(layout.description)?.ConstructElement().textContent ?? "", @@ -162,57 +187,67 @@ export default class ShareScreen extends Combine { } function rejected() { - const copyText = document.getElementById("code-link--copyable"); + const copyText = document.getElementById("code-link--copyable") // @ts-ignore - copyText.select(); + copyText.select() // @ts-ignore - copyText.setSelectionRange(0, 99999); /*For mobile devices*/ + copyText.setSelectionRange(0, 99999) /*For mobile devices*/ - document.execCommand("copy"); - const copied = tr.copiedToClipboard.Clone(); + document.execCommand("copy") + const copied = tr.copiedToClipboard.Clone() copied.SetClass("thanks") - linkStatus.setData(copied); + linkStatus.setData(copied) } try { - navigator.share(shareData) + navigator + .share(shareData) .then(() => { - const thx = tr.thanksForSharing.Clone(); - thx.SetClass("thanks"); - linkStatus.setData(thx); + const thx = tr.thanksForSharing.Clone() + thx.SetClass("thanks") + linkStatus.setData(thx) }, rejected) .catch(rejected) } catch (err) { - rejected(); + rejected() } + }) - }); - - - let downloadThemeConfig: BaseUIElement = undefined; + let downloadThemeConfig: BaseUIElement = undefined if (layout.definitionRaw !== undefined) { - const downloadThemeConfigAsJson = new SubtleButton(Svg.download_svg(), new Combine([ - tr.downloadCustomTheme, - tr.downloadCustomThemeHelp.SetClass("subtle") - ]).onClick(() => { - Utils.offerContentsAsDownloadableFile(layout.definitionRaw, layout.id + ".mapcomplete-theme-definition.json", { - mimetype: "application/json" - }) - }) - .SetClass("flex flex-col")) + const downloadThemeConfigAsJson = new SubtleButton( + Svg.download_svg(), + new Combine([tr.downloadCustomTheme, tr.downloadCustomThemeHelp.SetClass("subtle")]) + .onClick(() => { + Utils.offerContentsAsDownloadableFile( + layout.definitionRaw, + layout.id + ".mapcomplete-theme-definition.json", + { + mimetype: "application/json", + } + ) + }) + .SetClass("flex flex-col") + ) let editThemeConfig: BaseUIElement = undefined if (layout.definedAtUrl === undefined) { const patchedDefinition = JSON.parse(layout.definitionRaw) patchedDefinition["language"] = Object.keys(patchedDefinition.title) - editThemeConfig = new SubtleButton(Svg.pencil_svg(), "Edit this theme on the custom theme generator", + editThemeConfig = new SubtleButton( + Svg.pencil_svg(), + "Edit this theme on the custom theme generator", { - url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa(JSON.stringify(patchedDefinition))}` + url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa( + JSON.stringify(patchedDefinition) + )}`, } ) } - downloadThemeConfig = new Combine([downloadThemeConfigAsJson, editThemeConfig]).SetClass("flex flex-col") - + downloadThemeConfig = new Combine([ + downloadThemeConfigAsJson, + editThemeConfig, + ]).SetClass("flex flex-col") } super([ @@ -226,7 +261,5 @@ export default class ShareScreen extends Combine { iframeCode, ]) this.SetClass("flex flex-col link-underline") - } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 4b19456a5..b69948eae 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -1,147 +1,162 @@ /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient */ -import {UIEventSource} from "../../Logic/UIEventSource"; -import Svg from "../../Svg"; -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import Constants from "../../Models/Constants"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Toggle from "../Input/Toggle"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; -import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; -import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; -import FilteredLayer from "../../Models/FilteredLayer"; -import Loc from "../../Models/Loc"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Changes} from "../../Logic/Osm/Changes"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; -import BaseLayer from "../../Models/BaseLayer"; -import Loading from "../Base/Loading"; -import Hash from "../../Logic/Web/Hash"; -import {GlobalFilter} from "../../Logic/State/MapState"; +import { UIEventSource } from "../../Logic/UIEventSource" +import Svg from "../../Svg" +import { SubtleButton } from "../Base/SubtleButton" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import Constants from "../../Models/Constants" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import Toggle from "../Input/Toggle" +import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" +import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" +import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" +import PresetConfig from "../../Models/ThemeConfig/PresetConfig" +import FilteredLayer from "../../Models/FilteredLayer" +import Loc from "../../Models/Loc" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Changes } from "../../Logic/Osm/Changes" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import { ElementStorage } from "../../Logic/ElementStorage" +import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" +import BaseLayer from "../../Models/BaseLayer" +import Loading from "../Base/Loading" +import Hash from "../../Logic/Web/Hash" +import { GlobalFilter } from "../../Logic/State/MapState" /* -* The SimpleAddUI is a single panel, which can have multiple states: -* - A list of presets which can be added by the user -* - A 'confirm-selection' button (or alternatively: please enable the layer) -* - A 'something is wrong - please soom in further' -* - A 'read your unread messages before adding a point' + * The SimpleAddUI is a single panel, which can have multiple states: + * - A list of presets which can be added by the user + * - A 'confirm-selection' button (or alternatively: please enable the layer) + * - A 'something is wrong - please soom in further' + * - A 'read your unread messages before adding a point' */ export interface PresetInfo extends PresetConfig { - name: string | BaseUIElement, - icon: () => BaseUIElement, - layerToAddTo: FilteredLayer, + name: string | BaseUIElement + icon: () => BaseUIElement + layerToAddTo: FilteredLayer boundsFactor?: 0.25 | number } export default class SimpleAddUI extends Toggle { - /** - * + * * @param isShown * @param resetScrollSignal * @param filterViewIsOpened * @param state * @param takeLocationFrom: defaults to state.lastClickLocation. Take this location to add the new point around */ - constructor(isShown: UIEventSource<boolean>, - resetScrollSignal: UIEventSource<void>, - filterViewIsOpened: UIEventSource<boolean>, - state: { - featureSwitchIsTesting: UIEventSource<boolean>, - layoutToUse: LayoutConfig, - osmConnection: OsmConnection, - changes: Changes, - allElements: ElementStorage, - LastClickLocation: UIEventSource<{ lat: number, lon: number }>, - featurePipeline: FeaturePipeline, - selectedElement: UIEventSource<any>, - locationControl: UIEventSource<Loc>, - filteredLayers: UIEventSource<FilteredLayer[]>, - featureSwitchFilter: UIEventSource<boolean>, - backgroundLayer: UIEventSource<BaseLayer>, - globalFilters: UIEventSource<GlobalFilter[]> - }, - takeLocationFrom?: UIEventSource<{lat: number, lon: number}> + constructor( + isShown: UIEventSource<boolean>, + resetScrollSignal: UIEventSource<void>, + filterViewIsOpened: UIEventSource<boolean>, + state: { + featureSwitchIsTesting: UIEventSource<boolean> + layoutToUse: LayoutConfig + osmConnection: OsmConnection + changes: Changes + allElements: ElementStorage + LastClickLocation: UIEventSource<{ lat: number; lon: number }> + featurePipeline: FeaturePipeline + selectedElement: UIEventSource<any> + locationControl: UIEventSource<Loc> + filteredLayers: UIEventSource<FilteredLayer[]> + featureSwitchFilter: UIEventSource<boolean> + backgroundLayer: UIEventSource<BaseLayer> + globalFilters: UIEventSource<GlobalFilter[]> + }, + takeLocationFrom?: UIEventSource<{ lat: number; lon: number }> ) { - const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone()) - .onClick(() => state.osmConnection.AttemptLogin()); + const loginButton = new SubtleButton( + Svg.osm_logo_ui(), + Translations.t.general.add.pleaseLogin.Clone() + ).onClick(() => state.osmConnection.AttemptLogin()) const readYourMessages = new Combine([ Translations.t.general.readYourMessages.Clone().SetClass("alert"), - new SubtleButton(Svg.envelope_ui(), - Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}) - ]); + new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, { + url: "https://www.openstreetmap.org/messages/inbox", + newTab: false, + }), + ]) - takeLocationFrom = takeLocationFrom ?? state.LastClickLocation - const selectedPreset = new UIEventSource<PresetInfo>(undefined); - selectedPreset.addCallback(_ => { - resetScrollSignal.ping(); + const selectedPreset = new UIEventSource<PresetInfo>(undefined) + selectedPreset.addCallback((_) => { + resetScrollSignal.ping() }) - - - isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened - takeLocationFrom.addCallback(_ => selectedPreset.setData(undefined)) + + isShown.addCallback((_) => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened + takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined)) const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state) - - async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) : Promise<void>{ + async function createNewPoint( + tags: any[], + location: { lat: number; lon: number }, + snapOntoWay?: OsmWay + ): Promise<void> { const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { theme: state.layoutToUse?.id ?? "unkown", changeType: "create", - snapOnto: snapOntoWay + snapOnto: snapOntoWay, }) await state.changes.applyAction(newElementAction) selectedPreset.setData(undefined) isShown.setData(false) - state.selectedElement.setData(state.allElements.ContainingFeatures.get( - newElementAction.newElementId - )) + state.selectedElement.setData( + state.allElements.ContainingFeatures.get(newElementAction.newElementId) + ) Hash.hash.setData(newElementAction.newElementId) } const addUi = new VariableUiElement( - selectedPreset.map(preset => { - if (preset === undefined) { - return presetsOverview - } - - function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId?: string) { - if (snapOntoWayId === undefined) { - createNewPoint(tags, location, undefined) - } else { - OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => { - createNewPoint(tags, location, <OsmWay>way) - return true; - }) - } - } - - function cancel() { - selectedPreset.setData(undefined) - } - - const message = Translations.t.general.add.addNew.Subs({category: preset.name}, preset.name["context"]); - return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset, - message, - takeLocationFrom.data, - confirm, - cancel, - () => { - isShown.setData(false) - }) + selectedPreset.map((preset) => { + if (preset === undefined) { + return presetsOverview } - )) + function confirm( + tags: any[], + location: { lat: number; lon: number }, + snapOntoWayId?: string + ) { + if (snapOntoWayId === undefined) { + createNewPoint(tags, location, undefined) + } else { + OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD((way) => { + createNewPoint(tags, location, <OsmWay>way) + return true + }) + } + } + + function cancel() { + selectedPreset.setData(undefined) + } + + const message = Translations.t.general.add.addNew.Subs( + { category: preset.name }, + preset.name["context"] + ) + return new ConfirmLocationOfPoint( + state, + filterViewIsOpened, + preset, + message, + takeLocationFrom.data, + confirm, + cancel, + () => { + isShown.setData(false) + } + ) + }) + ) super( new Toggle( @@ -152,114 +167,136 @@ export default class SimpleAddUI extends Toggle { state.featurePipeline.runningQuery ), Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), - state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) + state.locationControl.map( + (loc) => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints + ) ), readYourMessages, - state.osmConnection.userDetails.map((userdetails: UserDetails) => - userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock || - userdetails.unreadMessages == 0) + state.osmConnection.userDetails.map( + (userdetails: UserDetails) => + userdetails.csCount >= + Constants.userJourney.addNewPointWithUnreadMessagesUnlock || + userdetails.unreadMessages == 0 + ) ), loginButton, state.osmConnection.isLoggedIn ) } - - public static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) { - const csCount = osmConnection.userDetails.data.csCount; + public static CreateTagInfoFor( + preset: PresetInfo, + osmConnection: OsmConnection, + optionallyLinkToWiki = true + ) { + const csCount = osmConnection.userDetails.data.csCount return new Toggle( - Translations.t.general.add.presetInfo.Subs({ - tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"), - }).SetStyle("word-break: break-all"), + Translations.t.general.add.presetInfo + .Subs({ + tags: preset.tags + .map((t) => + t.asHumanString( + optionallyLinkToWiki && + csCount > Constants.userJourney.tagsVisibleAndWikiLinked, + true + ) + ) + .join("&"), + }) + .SetStyle("word-break: break-all"), undefined, - osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt) - ); + osmConnection.userDetails.map( + (userdetails) => userdetails.csCount >= Constants.userJourney.tagsVisibleAt + ) + ) } - private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>, - state: { - featureSwitchIsTesting: UIEventSource<boolean>; - filteredLayers: UIEventSource<FilteredLayer[]>, - featureSwitchFilter: UIEventSource<boolean>, - osmConnection: OsmConnection - }): BaseUIElement { + private static CreateAllPresetsPanel( + selectedPreset: UIEventSource<PresetInfo>, + state: { + featureSwitchIsTesting: UIEventSource<boolean> + filteredLayers: UIEventSource<FilteredLayer[]> + featureSwitchFilter: UIEventSource<boolean> + osmConnection: OsmConnection + } + ): BaseUIElement { const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) - let intro: BaseUIElement = Translations.t.general.add.intro; + let intro: BaseUIElement = Translations.t.general.add.intro - let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"), + let testMode: BaseUIElement = new Toggle( + Translations.t.general.testing.SetClass("alert"), undefined, - state.featureSwitchIsTesting); + state.featureSwitchIsTesting + ) return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") - } private static CreatePresetSelectButton(preset: PresetInfo) { - - const title = Translations.t.general.add.addNew.Subs({ - category: preset.name - }, preset.name["context"]) + const title = Translations.t.general.add.addNew.Subs( + { + category: preset.name, + }, + preset.name["context"] + ) return new SubtleButton( preset.icon(), new Combine([ title.SetClass("font-bold"), - preset.description?.FirstSentence() + preset.description?.FirstSentence(), ]).SetClass("flex flex-col") ) } /* - * Generates the list with all the buttons.*/ + * Generates the list with all the buttons.*/ private static CreatePresetButtons( state: { - filteredLayers: UIEventSource<FilteredLayer[]>, - featureSwitchFilter: UIEventSource<boolean>, + filteredLayers: UIEventSource<FilteredLayer[]> + featureSwitchFilter: UIEventSource<boolean> osmConnection: OsmConnection }, - selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { - const allButtons = []; + selectedPreset: UIEventSource<PresetInfo> + ): BaseUIElement { + const allButtons = [] for (const layer of state.filteredLayers.data) { - if (layer.isDisplayed.data === false) { // The layer is not displayed... - if(!state.featureSwitchFilter.data){ + if (!state.featureSwitchFilter.data) { // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway - continue; + continue } if (layer.layerDef.name === undefined) { // this layer can never be toggled on in any case, so we skip the presets - continue; + continue } } - - - const presets = layer.layerDef.presets; + const presets = layer.layerDef.presets for (const preset of presets) { - - const tags = TagUtils.KVtoProperties(preset.tags ?? []); - let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html - .SetClass("w-12 h-12 block relative"); + const tags = TagUtils.KVtoProperties(preset.tags ?? []) + let icon: () => BaseUIElement = () => + layer.layerDef.mapRendering[0] + .GenerateLeafletStyle(new UIEventSource<any>(tags), false) + .html.SetClass("w-12 h-12 block relative") const presetInfo: PresetInfo = { layerToAddTo: layer, name: preset.title, title: preset.title, icon: icon, preciseInput: preset.preciseInput, - ...preset + ...preset, } - const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); + const button = SimpleAddUI.CreatePresetSelectButton(presetInfo) button.onClick(() => { selectedPreset.setData(presetInfo) }) - allButtons.push(button); + allButtons.push(button) } } - return new Combine(allButtons).SetClass("flex flex-col"); + return new Combine(allButtons).SetClass("flex flex-col") } - - -} \ No newline at end of file +} diff --git a/UI/BigComponents/StatisticsPanel.ts b/UI/BigComponents/StatisticsPanel.ts index f1e6e98b7..ed8dbab49 100644 --- a/UI/BigComponents/StatisticsPanel.ts +++ b/UI/BigComponents/StatisticsPanel.ts @@ -1,51 +1,68 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import Loading from "../Base/Loading"; -import Title from "../Base/Title"; -import TagRenderingChart from "./TagRenderingChart"; -import Combine from "../Base/Combine"; -import Locale from "../i18n/Locale"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmFeature} from "../../Models/OsmFeature"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import { VariableUiElement } from "../Base/VariableUIElement" +import Loading from "../Base/Loading" +import Title from "../Base/Title" +import TagRenderingChart from "./TagRenderingChart" +import Combine from "../Base/Combine" +import Locale from "../i18n/Locale" +import { UIEventSource } from "../../Logic/UIEventSource" +import { OsmFeature } from "../../Models/OsmFeature" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export default class StatisticsPanel extends VariableUiElement { - constructor(elementsInview: UIEventSource<{ element: OsmFeature, layer: LayerConfig }[]>, state: { - layoutToUse: LayoutConfig - }) { - super(elementsInview.stabilized(1000).map(features => { - if (features === undefined) { - return new Loading("Loading data") - } - if (features.length === 0) { - return "No elements in view" - } - const els = [] - for (const layer of state.layoutToUse.layers) { - if(layer.name === undefined){ - continue - } - const featuresForLayer = features.filter(f => f.layer === layer).map(f => f.element) - if(featuresForLayer.length === 0){ - continue - } - els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) - - const layerStats = [] - for (const tagRendering of (layer?.tagRenderings ?? [])) { - const chart = new TagRenderingChart(featuresForLayer, tagRendering, { - chartclasses: "w-full", - chartstyle: "height: 60rem", - includeTitle: false - }) - const title = new Title(tagRendering.question?.Clone() ?? tagRendering.id, 4).SetClass("mt-8") - if(!chart.HasClass("hidden")){ - layerStats.push(new Combine([title, chart]).SetClass("flex flex-col w-full lg:w-1/3")) + constructor( + elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>, + state: { + layoutToUse: LayoutConfig + } + ) { + super( + elementsInview.stabilized(1000).map( + (features) => { + if (features === undefined) { + return new Loading("Loading data") } - } - els.push(new Combine(layerStats).SetClass("flex flex-wrap")) - } - return new Combine(els) - }, [Locale.language])); + if (features.length === 0) { + return "No elements in view" + } + const els = [] + for (const layer of state.layoutToUse.layers) { + if (layer.name === undefined) { + continue + } + const featuresForLayer = features + .filter((f) => f.layer === layer) + .map((f) => f.element) + if (featuresForLayer.length === 0) { + continue + } + els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) + + const layerStats = [] + for (const tagRendering of layer?.tagRenderings ?? []) { + const chart = new TagRenderingChart(featuresForLayer, tagRendering, { + chartclasses: "w-full", + chartstyle: "height: 60rem", + includeTitle: false, + }) + const title = new Title( + tagRendering.question?.Clone() ?? tagRendering.id, + 4 + ).SetClass("mt-8") + if (!chart.HasClass("hidden")) { + layerStats.push( + new Combine([title, chart]).SetClass( + "flex flex-col w-full lg:w-1/3" + ) + ) + } + } + els.push(new Combine(layerStats).SetClass("flex flex-wrap")) + } + return new Combine(els) + }, + [Locale.language] + ) + ) } -} \ No newline at end of file +} diff --git a/UI/BigComponents/TagRenderingChart.ts b/UI/BigComponents/TagRenderingChart.ts index 7fff77383..32a7371a1 100644 --- a/UI/BigComponents/TagRenderingChart.ts +++ b/UI/BigComponents/TagRenderingChart.ts @@ -1,62 +1,66 @@ -import ChartJs from "../Base/ChartJs"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import {ChartConfiguration} from 'chart.js'; -import Combine from "../Base/Combine"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {Utils} from "../../Utils"; -import {OsmFeature} from "../../Models/OsmFeature"; +import ChartJs from "../Base/ChartJs" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import { ChartConfiguration } from "chart.js" +import Combine from "../Base/Combine" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { Utils } from "../../Utils" +import { OsmFeature } from "../../Models/OsmFeature" export interface TagRenderingChartOptions { - - groupToOtherCutoff?: 3 | number, + groupToOtherCutoff?: 3 | number sort?: boolean } export class StackedRenderingChart extends ChartJs { - constructor(tr: TagRenderingConfig, features: (OsmFeature & { properties: { date: string } })[], options?: { - period: "day" | "month", - groupToOtherCutoff?: 3 | number - }) { - const {labels, data} = TagRenderingChart.extractDataAndLabels(tr, features, { + constructor( + tr: TagRenderingConfig, + features: (OsmFeature & { properties: { date: string } })[], + options?: { + period: "day" | "month" + groupToOtherCutoff?: 3 | number + } + ) { + const { labels, data } = TagRenderingChart.extractDataAndLabels(tr, features, { sort: true, - groupToOtherCutoff: options?.groupToOtherCutoff + groupToOtherCutoff: options?.groupToOtherCutoff, }) if (labels === undefined || data === undefined) { console.error("Could not extract data and labels for ", tr, " with features", features) - throw ("No labels or data given...") + throw "No labels or data given..." } // labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ] - for (let i = labels.length; i >= 0; i--) { if (data[i]?.length != 0) { continue } data.splice(i, 1) labels.splice(i, 1) - } - const datasets: { label: string /*themename*/, data: number[]/*counts per day*/, backgroundColor: string }[] = [] + const datasets: { + label: string /*themename*/ + data: number[] /*counts per day*/ + backgroundColor: string + }[] = [] const allDays = StackedRenderingChart.getAllDays(features) - let trimmedDays = allDays.map(d => d.substr(0, 10)) + let trimmedDays = allDays.map((d) => d.substr(0, 10)) if (options?.period === "month") { - trimmedDays = trimmedDays.map(d => d.substr(0, 7)) + trimmedDays = trimmedDays.map((d) => d.substr(0, 7)) } trimmedDays = Utils.Dedup(trimmedDays) - for (let i = 0; i < labels.length; i++) { - const label = labels[i]; + const label = labels[i] const changesetsForTheme = data[i] const perDay: Record<string, OsmFeature[]> = {} for (const changeset of changesetsForTheme) { const csDate = new Date(changeset.properties.date) Utils.SetMidnight(csDate) - let str = csDate.toISOString(); + let str = csDate.toISOString() str = str.substr(0, 10) if (options?.period === "month") { - str = str.substr(0, 7); + str = str.substr(0, 7) } if (perDay[str] === undefined) { perDay[str] = [changeset] @@ -67,10 +71,11 @@ export class StackedRenderingChart extends ChartJs { const countsPerDay: number[] = [] for (let i = 0; i < trimmedDays.length; i++) { - const day = trimmedDays[i]; + const day = trimmedDays[i] countsPerDay[i] = perDay[day]?.length ?? 0 } - let backgroundColor = TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length] + let backgroundColor = + TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length] if (label === "Unknown") { backgroundColor = TagRenderingChart.unkownBorderColor } @@ -80,47 +85,44 @@ export class StackedRenderingChart extends ChartJs { datasets.push({ data: countsPerDay, backgroundColor, - label + label, }) } - - - const perDayData = { labels: trimmedDays, - datasets + datasets, } const config = <ChartConfiguration>{ - type: 'bar', + type: "bar", data: perDayData, options: { responsive: true, legend: { - display: false + display: false, }, scales: { x: { stacked: true, }, y: { - stacked: true - } - } - } + stacked: true, + }, + }, + }, } super(config) - - } - public static getAllDays(features: (OsmFeature & { properties: { date: string } })[]): string[] { + public static getAllDays( + features: (OsmFeature & { properties: { date: string } })[] + ): string[] { let earliest: Date = undefined - let latest: Date = undefined; - let allDates = new Set<string>(); + let latest: Date = undefined + let allDates = new Set<string>() features.forEach((value, key) => { - const d = new Date(value.properties.date); + const d = new Date(value.properties.date) Utils.SetMidnight(d) if (earliest === undefined) { @@ -147,60 +149,72 @@ export class StackedRenderingChart extends ChartJs { } export default class TagRenderingChart extends Combine { + public static readonly unkownColor = "rgba(128, 128, 128, 0.2)" + public static readonly unkownBorderColor = "rgba(128, 128, 128, 0.2)" - public static readonly unkownColor = 'rgba(128, 128, 128, 0.2)' - public static readonly unkownBorderColor = 'rgba(128, 128, 128, 0.2)' - - public static readonly otherColor = 'rgba(128, 128, 128, 0.2)' - public static readonly otherBorderColor = 'rgba(128, 128, 255)' - public static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)' - public static readonly notApplicableBorderColor = 'rgba(255, 0, 0)' - + public static readonly otherColor = "rgba(128, 128, 128, 0.2)" + public static readonly otherBorderColor = "rgba(128, 128, 255)" + public static readonly notApplicableColor = "rgba(128, 128, 128, 0.2)" + public static readonly notApplicableBorderColor = "rgba(255, 0, 0)" public static readonly backgroundColors = [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' + "rgba(255, 99, 132, 0.2)", + "rgba(54, 162, 235, 0.2)", + "rgba(255, 206, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(153, 102, 255, 0.2)", + "rgba(255, 159, 64, 0.2)", ] public static readonly borderColors = [ - 'rgba(255, 99, 132, 1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' + "rgba(255, 99, 132, 1)", + "rgba(54, 162, 235, 1)", + "rgba(255, 206, 86, 1)", + "rgba(75, 192, 192, 1)", + "rgba(153, 102, 255, 1)", + "rgba(255, 159, 64, 1)", ] /** * Creates a chart about this tagRendering for the given data */ - constructor(features: { properties: Record<string, string> }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & { - chartclasses?: string, - chartstyle?: string, - includeTitle?: boolean, - chartType?: "pie" | "bar" | "doughnut" - }) { + constructor( + features: { properties: Record<string, string> }[], + tagRendering: TagRenderingConfig, + options?: TagRenderingChartOptions & { + chartclasses?: string + chartstyle?: string + includeTitle?: boolean + chartType?: "pie" | "bar" | "doughnut" + } + ) { if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) { super([]) this.SetClass("hidden") - return; + return } - const {labels, data} = TagRenderingChart.extractDataAndLabels(tagRendering, features, options) + const { labels, data } = TagRenderingChart.extractDataAndLabels( + tagRendering, + features, + options + ) if (labels === undefined || data === undefined) { super([]) this.SetClass("hidden") return } - - const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor] - const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor] - + const borderColor = [ + TagRenderingChart.unkownBorderColor, + TagRenderingChart.otherBorderColor, + TagRenderingChart.notApplicableBorderColor, + ] + const backgroundColor = [ + TagRenderingChart.unkownColor, + TagRenderingChart.otherColor, + TagRenderingChart.notApplicableColor, + ] while (borderColor.length < data.length) { borderColor.push(...TagRenderingChart.borderColors) @@ -216,80 +230,87 @@ export default class TagRenderingChart extends Combine { } } - - let barchartMode = tagRendering.multiAnswer; + let barchartMode = tagRendering.multiAnswer if (labels.length > 9) { - barchartMode = true; + barchartMode = true } const config = <ChartConfiguration>{ - type: options.chartType ?? (barchartMode ? 'bar' : 'doughnut'), + type: options.chartType ?? (barchartMode ? "bar" : "doughnut"), data: { labels, - datasets: [{ - data: data.map(l => l.length), - backgroundColor, - borderColor, - borderWidth: 1, - label: undefined - }] + datasets: [ + { + data: data.map((l) => l.length), + backgroundColor, + borderColor, + borderWidth: 1, + label: undefined, + }, + ], }, options: { plugins: { legend: { - display: !barchartMode - } - } - } + display: !barchartMode, + }, + }, + }, } - const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32"); + const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32") if (options.chartstyle !== undefined) { chart.SetStyle(options.chartstyle) } - super([ - options?.includeTitle ? (tagRendering.question.Clone() ?? tagRendering.id) : undefined, - chart]) + options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined, + chart, + ]) this.SetClass("block") } - - public static extractDataAndLabels<T extends { properties: Record<string, string> }>(tagRendering: TagRenderingConfig, features: T[], options?: TagRenderingChartOptions): { labels: string[], data: T[][] } { + public static extractDataAndLabels<T extends { properties: Record<string, string> }>( + tagRendering: TagRenderingConfig, + features: T[], + options?: TagRenderingChartOptions + ): { labels: string[]; data: T[][] } { const mappings = tagRendering.mappings ?? [] options = options ?? {} - let unknownCount: T[] = []; - const categoryCounts: T[][] = mappings.map(_ => []) + let unknownCount: T[] = [] + const categoryCounts: T[][] = mappings.map((_) => []) const otherCounts: Record<string, T[]> = {} - let notApplicable: T[] = []; + let notApplicable: T[] = [] for (const feature of features) { const props = feature.properties - if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) { - notApplicable.push(feature); - continue; + if ( + tagRendering.condition !== undefined && + !tagRendering.condition.matchesProperties(props) + ) { + notApplicable.push(feature) + continue } if (!tagRendering.IsKnown(props)) { - unknownCount.push(feature); - continue; + unknownCount.push(feature) + continue } - let foundMatchingMapping = false; + let foundMatchingMapping = false if (!tagRendering.multiAnswer) { for (let i = 0; i < mappings.length; i++) { - const mapping = mappings[i]; + const mapping = mappings[i] if (mapping.if.matchesProperties(props)) { categoryCounts[i].push(feature) foundMatchingMapping = true - break; + break } } } else { for (let i = 0; i < mappings.length; i++) { - const mapping = mappings[i]; + const mapping = mappings[i] if (TagUtils.MatchesMultiAnswer(mapping.if, props)) { categoryCounts[i].push(feature) foundMatchingMapping = true @@ -297,9 +318,12 @@ export default class TagRenderingChart extends Combine { } } if (!foundMatchingMapping) { - if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) { + if ( + tagRendering.freeform?.key !== undefined && + props[tagRendering.freeform.key] !== undefined + ) { const otherValue = props[tagRendering.freeform.key] - otherCounts[otherValue] = (otherCounts[otherValue] ?? []) + otherCounts[otherValue] = otherCounts[otherValue] ?? [] otherCounts[otherValue].push(feature) } else { unknownCount.push(feature) @@ -309,15 +333,15 @@ export default class TagRenderingChart extends Combine { if (unknownCount.length + notApplicable.length === features.length) { console.log("Returning no label nor data: all features are unkown or notApplicable") - return {labels: undefined, data: undefined} + return { labels: undefined, data: undefined } } - let otherGrouped: T[] = []; + let otherGrouped: T[] = [] const otherLabels: string[] = [] const otherData: T[][] = [] const sortedOtherCounts: [string, T[]][] = [] for (const v in otherCounts) { - sortedOtherCounts.push([v, otherCounts[v]]); + sortedOtherCounts.push([v, otherCounts[v]]) } if (options?.sort) { sortedOtherCounts.sort((a, b) => b[1].length - a[1].length) @@ -327,15 +351,25 @@ export default class TagRenderingChart extends Combine { otherLabels.push(v) otherData.push(otherCounts[v]) } else { - otherGrouped.push(...count); + otherGrouped.push(...count) } } + const labels = [ + "Unknown", + "Other", + "Not applicable", + ...(mappings?.map((m) => m.then.txt) ?? []), + ...otherLabels, + ] + const data: T[][] = [ + unknownCount, + otherGrouped, + notApplicable, + ...categoryCounts, + ...otherData, + ] - const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels] - const data: T[][] = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ...otherData] - - return {labels, data} + return { labels, data } } - -} \ No newline at end of file +} diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts index 7153692f2..75b4fd1e2 100644 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ b/UI/BigComponents/ThemeIntroductionPanel.ts @@ -1,18 +1,27 @@ -import Combine from "../Base/Combine"; -import LanguagePicker from "../LanguagePicker"; -import Translations from "../i18n/Translations"; -import Toggle from "../Input/Toggle"; -import {SubtleButton} from "../Base/SubtleButton"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LoginToggle} from "../Popup/LoginButton"; -import Svg from "../../Svg"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"; +import Combine from "../Base/Combine" +import LanguagePicker from "../LanguagePicker" +import Translations from "../i18n/Translations" +import Toggle from "../Input/Toggle" +import { SubtleButton } from "../Base/SubtleButton" +import { UIEventSource } from "../../Logic/UIEventSource" +import { LoginToggle } from "../Popup/LoginButton" +import Svg from "../../Svg" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs" export default class ThemeIntroductionPanel extends Combine { - - constructor(isShown: UIEventSource<boolean>, currentTab: UIEventSource<number>, state: { featureSwitchMoreQuests: UIEventSource<boolean>; featureSwitchAddNew: UIEventSource<boolean>; featureSwitchUserbadge: UIEventSource<boolean>; layoutToUse: LayoutConfig; osmConnection: OsmConnection }) { + constructor( + isShown: UIEventSource<boolean>, + currentTab: UIEventSource<number>, + state: { + featureSwitchMoreQuests: UIEventSource<boolean> + featureSwitchAddNew: UIEventSource<boolean> + featureSwitchUserbadge: UIEventSource<boolean> + layoutToUse: LayoutConfig + osmConnection: OsmConnection + } + ) { const t = Translations.t.general const layout = state.layoutToUse @@ -21,48 +30,56 @@ export default class ThemeIntroductionPanel extends Combine { const toTheMap = new SubtleButton( undefined, t.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center") - ).onClick(() => { - isShown.setData(false) - }).SetClass("only-on-mobile") + ) + .onClick(() => { + isShown.setData(false) + }) + .SetClass("only-on-mobile") - - const loginStatus = - new Toggle( - new LoginToggle( - undefined, - new Combine([Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"), - Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold")] - ).SetClass("flex flex-col"), - state - ), + const loginStatus = new Toggle( + new LoginToggle( undefined, - state.featureSwitchUserbadge - ) + new Combine([ + Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"), + Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"), + ]).SetClass("flex flex-col"), + state + ), + undefined, + state.featureSwitchUserbadge + ) - const hasPresets = layout.layers.some(l => l.presets?.length > 0) + const hasPresets = layout.layers.some((l) => l.presets?.length > 0) super([ layout.description.Clone().SetClass("blcok mb-4"), new Combine([ t.welcomeExplanation.general, - hasPresets ? Toggle.If( state.featureSwitchAddNew, () => t.welcomeExplanation.addNew) : undefined, + hasPresets + ? Toggle.If(state.featureSwitchAddNew, () => t.welcomeExplanation.addNew) + : undefined, ]).SetClass("flex flex-col mt-2"), - + toTheMap, loginStatus.SetClass("block"), layout.descriptionTail?.Clone().SetClass("block mt-4"), - + languagePicker?.SetClass("block mt-4"), - - Toggle.If(state.featureSwitchMoreQuests, - () => new Combine([ + + Toggle.If(state.featureSwitchMoreQuests, () => + new Combine([ t.welcomeExplanation.browseOtherThemesIntro, - new SubtleButton(Svg.add_ui().SetClass("h-6"),t.welcomeExplanation.browseMoreMaps ) - .onClick(() => currentTab.setData(FullWelcomePaneWithTabs.MoreThemesTabIndex)) - .SetClass("h-12") - - ]).SetClass("flex flex-col mt-6")), - - ...layout.CustomCodeSnippets() + new SubtleButton( + Svg.add_ui().SetClass("h-6"), + t.welcomeExplanation.browseMoreMaps + ) + .onClick(() => + currentTab.setData(FullWelcomePaneWithTabs.MoreThemesTabIndex) + ) + .SetClass("h-12"), + ]).SetClass("flex flex-col mt-6") + ), + + ...layout.CustomCodeSnippets(), ]) this.SetClass("link-underline") diff --git a/UI/BigComponents/TranslatorsPanel.ts b/UI/BigComponents/TranslatorsPanel.ts index 88b1eb1f6..c6b1faaf3 100644 --- a/UI/BigComponents/TranslatorsPanel.ts +++ b/UI/BigComponents/TranslatorsPanel.ts @@ -1,28 +1,29 @@ -import Toggle from "../Input/Toggle"; -import Lazy from "../Base/Lazy"; -import {Utils} from "../../Utils"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import Locale from "../i18n/Locale"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Translation} from "../i18n/Translation"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Link from "../Base/Link"; -import LinkToWeblate from "../Base/LinkToWeblate"; -import Toggleable from "../Base/Toggleable"; -import Title from "../Base/Title"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; +import Toggle from "../Input/Toggle" +import Lazy from "../Base/Lazy" +import { Utils } from "../../Utils" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import Locale from "../i18n/Locale" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Translation } from "../i18n/Translation" +import { VariableUiElement } from "../Base/VariableUIElement" +import Link from "../Base/Link" +import LinkToWeblate from "../Base/LinkToWeblate" +import Toggleable from "../Base/Toggleable" +import Title from "../Base/Title" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" import * as native_languages from "../../assets/language_native.json" import * as used_languages from "../../assets/generated/used_languages.json" -import BaseUIElement from "../BaseUIElement"; +import BaseUIElement from "../BaseUIElement" class TranslatorsPanelContent extends Combine { constructor(layout: LayoutConfig, isTranslator: Store<boolean>) { const t = Translations.t.translations - const {completeness, untranslated, total} = TranslatorsPanel.MissingTranslationsFor(layout) + const { completeness, untranslated, total } = + TranslatorsPanel.MissingTranslationsFor(layout) const seed = t.completeness for (const ln of Array.from(completeness.keys())) { @@ -36,127 +37,164 @@ class TranslatorsPanelContent extends Combine { const completenessTr = {} const completenessPercentage = {} - seed.SupportedLanguages().forEach(ln => { + seed.SupportedLanguages().forEach((ln) => { completenessTr[ln] = "" + (completeness.get(ln) ?? 0) - completenessPercentage[ln] = "" + Math.round(100 * (completeness.get(ln) ?? 0) / total) + completenessPercentage[ln] = + "" + Math.round((100 * (completeness.get(ln) ?? 0)) / total) }) function missingTranslationsFor(language: string): BaseUIElement[] { // e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description const missingKeys = Utils.NoNull(untranslated.get(language) ?? []) - .filter(ctx => ctx.indexOf(":") >= 0) - .map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import")) + .filter((ctx) => ctx.indexOf(":") >= 0) + .map((ctx) => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import")) - const hasMissingTheme = missingKeys.some(k => k.startsWith("themes:")) - const missingLayers = Utils.Dedup( missingKeys.filter(k => k.startsWith("layers:")) - .map(k => k.slice("layers:".length).split(".")[0])) + const hasMissingTheme = missingKeys.some((k) => k.startsWith("themes:")) + const missingLayers = Utils.Dedup( + missingKeys + .filter((k) => k.startsWith("layers:")) + .map((k) => k.slice("layers:".length).split(".")[0]) + ) - console.log("Getting untranslated string for",language,"raw:",missingKeys,"hasMissingTheme:",hasMissingTheme,"missingLayers:",missingLayers) + console.log( + "Getting untranslated string for", + language, + "raw:", + missingKeys, + "hasMissingTheme:", + hasMissingTheme, + "missingLayers:", + missingLayers + ) return [ - hasMissingTheme ? new Link("themes:" + layout.id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id), true) : undefined, - ...missingLayers.map(id => new Link("layer:" + id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "layers", id), true)), - ...missingKeys.map(context => new Link(context, LinkToWeblate.hrefToWeblate(language, context), true)) + hasMissingTheme + ? new Link( + "themes:" + layout.id + ".* (zen mode)", + LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id), + true + ) + : undefined, + ...missingLayers.map( + (id) => + new Link( + "layer:" + id + ".* (zen mode)", + LinkToWeblate.hrefToWeblateZen(language, "layers", id), + true + ) + ), + ...missingKeys.map( + (context) => + new Link(context, LinkToWeblate.hrefToWeblate(language, context), true) + ), ] } - // - // + // // "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}", const translated = seed.Subs({ - total, theme: layout.title, + total, + theme: layout.title, percentage: new Translation(completenessPercentage), translated: new Translation(completenessTr), - language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng) + language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng), }) super([ - new Title( - Translations.t.translations.activateButton, - ), + new Title(Translations.t.translations.activateButton), new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator), t.help, translated, /*Disable button:*/ - new SubtleButton(undefined, t.deactivate) - .onClick(() => { - Locale.showLinkToWeblate.setData(false) - }), + new SubtleButton(undefined, t.deactivate).onClick(() => { + Locale.showLinkToWeblate.setData(false) + }), - new VariableUiElement(Locale.language.map(ln => { - const missing = missingTranslationsFor(ln) - if (missing.length === 0) { - return undefined - } - let title = Translations.t.translations.allMissing; - if(untranslated.get(ln) !== undefined){ - title = Translations.t.translations.missing.Subs({count: untranslated.get(ln).length}) - } - return new Toggleable( - new Title(title), - new Combine(missing).SetClass("flex flex-col") - ) - })) + new VariableUiElement( + Locale.language.map((ln) => { + const missing = missingTranslationsFor(ln) + if (missing.length === 0) { + return undefined + } + let title = Translations.t.translations.allMissing + if (untranslated.get(ln) !== undefined) { + title = Translations.t.translations.missing.Subs({ + count: untranslated.get(ln).length, + }) + } + return new Toggleable( + new Title(title), + new Combine(missing).SetClass("flex flex-col") + ) + }) + ), ]) - } - } export default class TranslatorsPanel extends Toggle { - - - constructor(state: { layoutToUse: LayoutConfig, isTranslator: Store<boolean> }, iconStyle?: string) { + constructor( + state: { layoutToUse: LayoutConfig; isTranslator: Store<boolean> }, + iconStyle?: string + ) { const t = Translations.t.translations super( - new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator) + new Lazy( + () => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator) ).SetClass("flex flex-col"), - new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => Locale.showLinkToWeblate.setData(true)), + new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => + Locale.showLinkToWeblate.setData(true) + ), Locale.showLinkToWeblate ) this.SetClass("hidden-on-mobile") - } - - public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } { + public static MissingTranslationsFor(layout: LayoutConfig): { + completeness: Map<string, number> + untranslated: Map<string, string[]> + total: number + } { let total = 0 const completeness = new Map<string, number>() const untranslated = new Map<string, string[]>() - Utils.WalkObject(layout, (o) => { - const translation = <Translation><any>o; - if (translation.translations["*"] !== undefined) { - return - } - if (translation.context === undefined || translation.context.indexOf(":") < 0) { - // no source given - lets ignore - return - } - - total ++ - used_languages.languages.forEach(ln => { - const trans = translation.translations - if (trans["*"] !== undefined) { - return; + Utils.WalkObject( + layout, + (o) => { + const translation = <Translation>(<any>o) + if (translation.translations["*"] !== undefined) { + return } - if (trans[ln] === undefined) { - if (!untranslated.has(ln)) { - untranslated.set(ln, []) - } - untranslated.get(ln).push(translation.context) - }else{ - completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) + if (translation.context === undefined || translation.context.indexOf(":") < 0) { + // no source given - lets ignore + return } - }) - - }, o => { - if (o === undefined || o === null) { - return false; - } - return o instanceof Translation; - }) - return {completeness, untranslated, total} + total++ + used_languages.languages.forEach((ln) => { + const trans = translation.translations + if (trans["*"] !== undefined) { + return + } + if (trans[ln] === undefined) { + if (!untranslated.has(ln)) { + untranslated.set(ln, []) + } + untranslated.get(ln).push(translation.context) + } else { + completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) + } + }) + }, + (o) => { + if (o === undefined || o === null) { + return false + } + return o instanceof Translation + } + ) + + return { completeness, untranslated, total } } } diff --git a/UI/BigComponents/UserBadge.ts b/UI/BigComponents/UserBadge.ts index 39bfe2251..d036b6ec3 100644 --- a/UI/BigComponents/UserBadge.ts +++ b/UI/BigComponents/UserBadge.ts @@ -1,104 +1,102 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import LanguagePicker from "../LanguagePicker"; -import Translations from "../i18n/Translations"; -import Link from "../Base/Link"; -import Toggle from "../Input/Toggle"; -import Img from "../Base/Img"; -import MapState from "../../Logic/State/MapState"; -import {LoginToggle} from "../Popup/LoginButton"; +import { VariableUiElement } from "../Base/VariableUIElement" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import { FixedUiElement } from "../Base/FixedUiElement" +import LanguagePicker from "../LanguagePicker" +import Translations from "../i18n/Translations" +import Link from "../Base/Link" +import Toggle from "../Input/Toggle" +import Img from "../Base/Img" +import MapState from "../../Logic/State/MapState" +import { LoginToggle } from "../Popup/LoginButton" export default class UserBadge extends LoginToggle { - constructor(state: MapState) { - const userDetails = state.osmConnection.userDetails; - const logout = - Svg.logout_svg() - .onClick(() => { - state.osmConnection.LogOut(); - }); + const userDetails = state.osmConnection.userDetails + const logout = Svg.logout_svg().onClick(() => { + state.osmConnection.LogOut() + }) - - const userBadge = new VariableUiElement(userDetails.map(user => { - { - const homeButton = new VariableUiElement( - userDetails.map((userinfo) => { - if (userinfo.home) { - return Svg.home_svg(); + const userBadge = new VariableUiElement( + userDetails.map((user) => { + { + const homeButton = new VariableUiElement( + userDetails.map((userinfo) => { + if (userinfo.home) { + return Svg.home_svg() + } + return " " + }) + ).onClick(() => { + const home = state.osmConnection.userDetails.data?.home + if (home === undefined) { + return } - return " "; + state.leafletMap.data?.setView([home.lat, home.lon], 16) }) - ).onClick(() => { - const home = state.osmConnection.userDetails.data?.home; - if (home === undefined) { - return; - } - state.leafletMap.data?.setView([home.lat, home.lon], 16); - }); - const linkStyle = "flex items-baseline" - const languagePicker = (new LanguagePicker(state.layoutToUse.language, "") ?? new FixedUiElement("")) - .SetStyle("width:min-content;"); + const linkStyle = "flex items-baseline" + const languagePicker = ( + new LanguagePicker(state.layoutToUse.language, "") ?? new FixedUiElement("") + ).SetStyle("width:min-content;") - let messageSpan = - new Link( + let messageSpan = new Link( new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), `${user.backend}/messages/inbox`, true ) - - const csCount = - new Link( + const csCount = new Link( new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), `${user.backend}/user/${user.name}/history`, - true); - - - if (user.unreadMessages > 0) { - messageSpan = new Link( - new Combine([Svg.envelope, "" + user.unreadMessages]), - `${user.backend}/messages/inbox`, true - ).SetClass("alert") - } + ) - let dryrun = new Toggle( - new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"), - undefined, - state.featureSwitchIsTesting - ) + if (user.unreadMessages > 0) { + messageSpan = new Link( + new Combine([Svg.envelope, "" + user.unreadMessages]), + `${user.backend}/messages/inbox`, + true + ).SetClass("alert") + } - const settings = - new Link(Svg.gear, + let dryrun = new Toggle( + new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"), + undefined, + state.featureSwitchIsTesting + ) + + const settings = new Link( + Svg.gear, `${user.backend}/user/${encodeURIComponent(user.name)}/account`, - true) + true + ) + const userName = new Link( + new FixedUiElement(user.name), + `${user.backend}/user/${user.name}`, + true + ) - const userName = new Link( - new FixedUiElement(user.name), - `${user.backend}/user/${user.name}`, - true); + const userStats = new Combine([ + homeButton, + settings, + messageSpan, + csCount, + languagePicker, + logout, + ]).SetClass("userstats") - - const userStats = new Combine([ - homeButton, - settings, - messageSpan, - csCount, - languagePicker, - logout - ]) - .SetClass("userstats") - - const usertext = new Combine([ - new Combine([userName, dryrun]).SetClass("flex justify-end w-full"), - userStats - ]).SetClass("flex flex-col sm:w-auto sm:pl-2 overflow-hidden w-0") - const userIcon = - (user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img)).SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 min-width-16 h16 float-left") + const usertext = new Combine([ + new Combine([userName, dryrun]).SetClass("flex justify-end w-full"), + userStats, + ]).SetClass("flex flex-col sm:w-auto sm:pl-2 overflow-hidden w-0") + const userIcon = ( + user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img) + ) + .SetClass( + "rounded-full opacity-0 m-0 p-0 duration-500 w-16 min-width-16 h16 float-left" + ) .onClick(() => { if (usertext.HasClass("w-0")) { usertext.RemoveClass("w-0") @@ -110,23 +108,17 @@ export default class UserBadge extends LoginToggle { } }) - return new Combine([ - usertext, - userIcon, - ]).SetClass("h-16 flex bg-white") + return new Combine([usertext, userIcon]).SetClass("h-16 flex bg-white") + } + }) + ) - } - })); - - super( - new Combine([userBadge.SetClass("inline-block m-0 w-full").SetStyle("pointer-events: all")]) - .SetClass("shadow rounded-full h-min overflow-hidden block w-full md:w-max"), + new Combine([ + userBadge.SetClass("inline-block m-0 w-full").SetStyle("pointer-events: all"), + ]).SetClass("shadow rounded-full h-min overflow-hidden block w-full md:w-max"), Translations.t.general.loginWithOpenStreetMap, state ) - } - - } diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index a05b091a5..ef6274289 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -1,48 +1,43 @@ -import Translations from "./i18n/Translations"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; -import Loading from "./Base/Loading"; +import Translations from "./i18n/Translations" +import { VariableUiElement } from "./Base/VariableUIElement" +import FeaturePipelineState from "../Logic/State/FeaturePipelineState" +import Loading from "./Base/Loading" export default class CenterMessageBox extends VariableUiElement { - constructor(state: FeaturePipelineState) { - const updater = state.featurePipeline; - const t = Translations.t.centerMessage; + const updater = state.featurePipeline + const t = Translations.t.centerMessage const message = updater.runningQuery.map( - isRunning => { + (isRunning) => { if (isRunning) { - return {el: new Loading(t.loadingData)}; + return { el: new Loading(t.loadingData) } } if (!updater.sufficientlyZoomed.data) { - return {el: t.zoomIn} + return { el: t.zoomIn } } if (updater.timeout.data > 0) { - return {el: t.retrying.Subs({count: "" + updater.timeout.data})} + return { el: t.retrying.Subs({ count: "" + updater.timeout.data }) } } - return {el: t.ready, isDone: true} - + return { el: t.ready, isDone: true } }, [updater.timeout, updater.sufficientlyZoomed, state.locationControl] ) - super(message.map(toShow => toShow.el)) + super(message.map((toShow) => toShow.el)) - this.SetClass("flex justify-center " + - "rounded-3xl bg-white text-xl font-bold pointer-events-none p-4") + this.SetClass( + "flex justify-center " + + "rounded-3xl bg-white text-xl font-bold pointer-events-none p-4" + ) this.SetStyle("transition: opacity 750ms linear") - message.addCallbackAndRun(toShow => { - const isDone = toShow.isDone ?? false; + message.addCallbackAndRun((toShow) => { + const isDone = toShow.isDone ?? false if (isDone) { this.SetStyle("transition: opacity 750ms linear; opacity: 0") } else { this.SetStyle("transition: opacity 750ms linear; opacity: 0.75") - } }) - } - } - - diff --git a/UI/DashboardGui.ts b/UI/DashboardGui.ts index 266747954..8587cd66a 100644 --- a/UI/DashboardGui.ts +++ b/UI/DashboardGui.ts @@ -1,55 +1,61 @@ -import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; -import {DefaultGuiState} from "./DefaultGuiState"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import {Utils} from "../Utils"; -import Combine from "./Base/Combine"; -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import * as home_location_json from "../assets/layers/home_location/home_location.json"; -import State from "../State"; -import Title from "./Base/Title"; -import {MinimapObj} from "./Base/Minimap"; -import BaseUIElement from "./BaseUIElement"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import {GeoOperations} from "../Logic/GeoOperations"; -import {OsmFeature} from "../Models/OsmFeature"; -import SearchAndGo from "./BigComponents/SearchAndGo"; -import FeatureInfoBox from "./Popup/FeatureInfoBox"; -import {UIEventSource} from "../Logic/UIEventSource"; -import LanguagePicker from "./LanguagePicker"; -import Lazy from "./Base/Lazy"; -import TagRenderingAnswer from "./Popup/TagRenderingAnswer"; -import Hash from "../Logic/Web/Hash"; -import FilterView from "./BigComponents/FilterView"; -import Translations from "./i18n/Translations"; -import Constants from "../Models/Constants"; -import SimpleAddUI from "./BigComponents/SimpleAddUI"; -import BackToIndex from "./BigComponents/BackToIndex"; -import StatisticsPanel from "./BigComponents/StatisticsPanel"; - +import FeaturePipelineState from "../Logic/State/FeaturePipelineState" +import { DefaultGuiState } from "./DefaultGuiState" +import { FixedUiElement } from "./Base/FixedUiElement" +import { Utils } from "../Utils" +import Combine from "./Base/Combine" +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import * as home_location_json from "../assets/layers/home_location/home_location.json" +import State from "../State" +import Title from "./Base/Title" +import { MinimapObj } from "./Base/Minimap" +import BaseUIElement from "./BaseUIElement" +import { VariableUiElement } from "./Base/VariableUIElement" +import { GeoOperations } from "../Logic/GeoOperations" +import { OsmFeature } from "../Models/OsmFeature" +import SearchAndGo from "./BigComponents/SearchAndGo" +import FeatureInfoBox from "./Popup/FeatureInfoBox" +import { UIEventSource } from "../Logic/UIEventSource" +import LanguagePicker from "./LanguagePicker" +import Lazy from "./Base/Lazy" +import TagRenderingAnswer from "./Popup/TagRenderingAnswer" +import Hash from "../Logic/Web/Hash" +import FilterView from "./BigComponents/FilterView" +import Translations from "./i18n/Translations" +import Constants from "../Models/Constants" +import SimpleAddUI from "./BigComponents/SimpleAddUI" +import BackToIndex from "./BigComponents/BackToIndex" +import StatisticsPanel from "./BigComponents/StatisticsPanel" export default class DashboardGui { - private readonly state: FeaturePipelineState; - private readonly currentView: UIEventSource<{ title: string | BaseUIElement, contents: string | BaseUIElement }> = new UIEventSource(undefined) - + private readonly state: FeaturePipelineState + private readonly currentView: UIEventSource<{ + title: string | BaseUIElement + contents: string | BaseUIElement + }> = new UIEventSource(undefined) constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { - this.state = state; + this.state = state } - private viewSelector(shown: BaseUIElement, title: string | BaseUIElement, contents: string | BaseUIElement, hash?: string): BaseUIElement { + private viewSelector( + shown: BaseUIElement, + title: string | BaseUIElement, + contents: string | BaseUIElement, + hash?: string + ): BaseUIElement { const currentView = this.currentView - const v = {title, contents} + const v = { title, contents } shown.SetClass("pl-1 pr-1 rounded-md") shown.onClick(() => { currentView.setData(v) }) - Hash.hash.addCallbackAndRunD(h => { + Hash.hash.addCallbackAndRunD((h) => { if (h === hash) { currentView.setData(v) } }) - currentView.addCallbackAndRunD(cv => { + currentView.addCallbackAndRunD((cv) => { if (cv == v) { shown.SetClass("bg-unsubtle") Hash.hash.setData(hash) @@ -57,29 +63,38 @@ export default class DashboardGui { shown.RemoveClass("bg-unsubtle") } }) - return shown; + return shown } private singleElementCache: Record<string, BaseUIElement> = {} - private singleElementView(element: OsmFeature, layer: LayerConfig, distance: number): BaseUIElement { + private singleElementView( + element: OsmFeature, + layer: LayerConfig, + distance: number + ): BaseUIElement { if (this.singleElementCache[element.properties.id] !== undefined) { return this.singleElementCache[element.properties.id] } const tags = this.state.allElements.getEventSourceById(element.properties.id) - const title = new Combine([new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4), - distance < 900 ? Math.floor(distance) + "m away" : - Utils.Round(distance / 1000) + "km away" - ]).SetClass("flex justify-between"); + const title = new Combine([ + new Title(new TagRenderingAnswer(tags, layer.title, this.state), 4), + distance < 900 + ? Math.floor(distance) + "m away" + : Utils.Round(distance / 1000) + "km away", + ]).SetClass("flex justify-between") - return this.singleElementCache[element.properties.id] = this.viewSelector(title, + return (this.singleElementCache[element.properties.id] = this.viewSelector( + title, new Lazy(() => FeatureInfoBox.GenerateTitleBar(tags, layer, this.state)), - new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state)), + new Lazy(() => FeatureInfoBox.GenerateContent(tags, layer, this.state)) // element.properties.id - ); + )) } - private mainElementsView(elements: { element: OsmFeature, layer: LayerConfig, distance: number }[]): BaseUIElement { + private mainElementsView( + elements: { element: OsmFeature; layer: LayerConfig; distance: number }[] + ): BaseUIElement { const self = this if (elements === undefined) { return new FixedUiElement("Initializing") @@ -87,64 +102,84 @@ export default class DashboardGui { if (elements.length == 0) { return new FixedUiElement("No elements in view") } - return new Combine(elements.map(e => self.singleElementView(e.element, e.layer, e.distance))) + return new Combine( + elements.map((e) => self.singleElementView(e.element, e.layer, e.distance)) + ) } - private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement { - return this.viewSelector(Translations.W(layerConfig.name?.Clone() ?? layerConfig.id), new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]), + private documentationButtonFor(layerConfig: LayerConfig): BaseUIElement { + return this.viewSelector( + Translations.W(layerConfig.name?.Clone() ?? layerConfig.id), + new Combine(["Documentation about ", layerConfig.name?.Clone() ?? layerConfig.id]), layerConfig.GenerateDocumentation([]), - "documentation-" + layerConfig.id) + "documentation-" + layerConfig.id + ) } private allDocumentationButtons(): BaseUIElement { - const layers = this.state.layoutToUse.layers.filter(l => Constants.priviliged_layers.indexOf(l.id) < 0) - .filter(l => !l.id.startsWith("note_import_")); + const layers = this.state.layoutToUse.layers + .filter((l) => Constants.priviliged_layers.indexOf(l.id) < 0) + .filter((l) => !l.id.startsWith("note_import_")) if (layers.length === 1) { return this.documentationButtonFor(layers[0]) } - return this.viewSelector(new FixedUiElement("Documentation"), "Documentation", - new Combine(layers.map(l => this.documentationButtonFor(l).SetClass("flex flex-col")))) + return this.viewSelector( + new FixedUiElement("Documentation"), + "Documentation", + new Combine(layers.map((l) => this.documentationButtonFor(l).SetClass("flex flex-col"))) + ) } - public setup(): void { - - const state = this.state; + const state = this.state if (this.state.layoutToUse.customCss !== undefined) { if (window.location.pathname.indexOf("index") >= 0) { Utils.LoadCustomCss(this.state.layoutToUse.customCss) } } - const map = this.SetupMap(); + const map = this.SetupMap() - Utils.downloadJson("./service-worker-version").then(data => console.log("Service worker", data)).catch(_ => console.log("Service worker not active")) + Utils.downloadJson("./service-worker-version") + .then((data) => console.log("Service worker", data)) + .catch((_) => console.log("Service worker not active")) document.getElementById("centermessage").classList.add("hidden") const layers: Record<string, LayerConfig> = {} for (const layer of state.layoutToUse.layers) { - layers[layer.id] = layer; + layers[layer.id] = layer } - const self = this; - const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]); + const self = this + const elementsInview = new UIEventSource< + { + distance: number + center: [number, number] + element: OsmFeature + layer: LayerConfig + }[] + >([]) function update() { - const mapCenter = <[number,number]> [self.state.locationControl.data.lon, self.state.locationControl.data.lon] - const elements = self.state.featurePipeline.getAllVisibleElementsWithmeta(self.state.currentBounds.data).map(el => { - const distance = GeoOperations.distanceBetween(el.center, mapCenter) - return {...el, distance } - }) + const mapCenter = <[number, number]>[ + self.state.locationControl.data.lon, + self.state.locationControl.data.lon, + ] + const elements = self.state.featurePipeline + .getAllVisibleElementsWithmeta(self.state.currentBounds.data) + .map((el) => { + const distance = GeoOperations.distanceBetween(el.center, mapCenter) + return { ...el, distance } + }) elements.sort((e0, e1) => e0.distance - e1.distance) elementsInview.setData(elements) - } map.bounds.addCallbackAndRun(update) - state.featurePipeline.newDataLoadedSignal.addCallback(update); - state.filteredLayers.addCallbackAndRun(fls => { + state.featurePipeline.newDataLoadedSignal.addCallback(update) + state.filteredLayers.addCallbackAndRun((fls) => { for (const fl of fls) { fl.isDisplayed.addCallback(update) fl.appliedFilters.addCallback(update) @@ -153,28 +188,33 @@ export default class DashboardGui { const filterView = new Lazy(() => { return new FilterView(state.filteredLayers, state.overlayToggles, state) - }); - const welcome = new Combine([state.layoutToUse.description, state.layoutToUse.descriptionTail]) - self.currentView.setData({title: state.layoutToUse.title, contents: welcome}) + }) + const welcome = new Combine([ + state.layoutToUse.description, + state.layoutToUse.descriptionTail, + ]) + self.currentView.setData({ title: state.layoutToUse.title, contents: welcome }) const filterViewIsOpened = new UIEventSource(false) - filterViewIsOpened.addCallback(_ => self.currentView.setData({title: "filters", contents: filterView})) + filterViewIsOpened.addCallback((_) => + self.currentView.setData({ title: "filters", contents: filterView }) + ) - const newPointIsShown = new UIEventSource(false); + const newPointIsShown = new UIEventSource(false) const addNewPoint = new SimpleAddUI( new UIEventSource(true), new UIEventSource(undefined), filterViewIsOpened, state, state.locationControl - ); + ) const addNewPointTitle = "Add a missing point" - this.currentView.addCallbackAndRunD(cv => { + this.currentView.addCallbackAndRunD((cv) => { newPointIsShown.setData(cv.contents === addNewPoint) }) - newPointIsShown.addCallbackAndRun(isShown => { + newPointIsShown.addCallbackAndRun((isShown) => { if (isShown) { if (self.currentView.data.contents !== addNewPoint) { - self.currentView.setData({title: addNewPointTitle, contents: addNewPoint}) + self.currentView.setData({ title: addNewPointTitle, contents: addNewPoint }) } } else { if (self.currentView.data.contents === addNewPoint) { @@ -183,60 +223,83 @@ export default class DashboardGui { } }) - new Combine([ new Combine([ - this.viewSelector(new Title(state.layoutToUse.title.Clone(), 2), state.layoutToUse.title.Clone(), welcome, "welcome"), + this.viewSelector( + new Title(state.layoutToUse.title.Clone(), 2), + state.layoutToUse.title.Clone(), + welcome, + "welcome" + ), map.SetClass("w-full h-64 shrink-0 rounded-lg"), new SearchAndGo(state), - this.viewSelector(new Title( - new VariableUiElement(elementsInview.map(elements => "There are " + elements?.length + " elements in view"))), + this.viewSelector( + new Title( + new VariableUiElement( + elementsInview.map( + (elements) => "There are " + elements?.length + " elements in view" + ) + ) + ), "Statistics", - new StatisticsPanel(elementsInview, this.state), "statistics"), + new StatisticsPanel(elementsInview, this.state), + "statistics" + ), - this.viewSelector(new FixedUiElement("Filter"), - "Filters", filterView, "filters"), - this.viewSelector(new Combine(["Add a missing point"]), addNewPointTitle, + this.viewSelector(new FixedUiElement("Filter"), "Filters", filterView, "filters"), + this.viewSelector( + new Combine(["Add a missing point"]), + addNewPointTitle, addNewPoint ), - new VariableUiElement(elementsInview.map(elements => this.mainElementsView(elements).SetClass("block m-2"))) - .SetClass("block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg"), + new VariableUiElement( + elementsInview.map((elements) => + this.mainElementsView(elements).SetClass("block m-2") + ) + ).SetClass( + "block shrink-2 overflow-x-auto h-full border-2 border-subtle rounded-lg" + ), this.allDocumentationButtons(), - new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass("mt-2"), - new BackToIndex() + new LanguagePicker(Object.keys(state.layoutToUse.title.translations)).SetClass( + "mt-2" + ), + new BackToIndex(), ]).SetClass("w-1/2 lg:w-1/4 m-4 flex flex-col shrink-0 grow-0"), - new VariableUiElement(this.currentView.map(({title, contents}) => { - return new Combine([ - new Title(Translations.W(title), 2).SetClass("shrink-0 border-b-4 border-subtle"), - Translations.W(contents).SetClass("shrink-2 overflow-y-auto block") - ]).SetClass("flex flex-col h-full") - })).SetClass("w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0"), - - ]).SetClass("flex h-full") + new VariableUiElement( + this.currentView.map(({ title, contents }) => { + return new Combine([ + new Title(Translations.W(title), 2).SetClass( + "shrink-0 border-b-4 border-subtle" + ), + Translations.W(contents).SetClass("shrink-2 overflow-y-auto block"), + ]).SetClass("flex flex-col h-full") + }) + ).SetClass( + "w-1/2 lg:w-3/4 m-4 p-2 border-2 border-subtle rounded-xl m-4 ml-0 mr-8 shrink-0 grow-0" + ), + ]) + .SetClass("flex h-full") .AttachTo("leafletDiv") - } private SetupMap(): MinimapObj & BaseUIElement { - const state = this.state; + const state = this.state new ShowDataLayer({ leafletMap: state.leafletMap, layerToShow: new LayerConfig(home_location_json, "home_location", true), features: state.homeLocation, - state + state, }) - state.leafletMap.addCallbackAndRunD(_ => { + state.leafletMap.addCallbackAndRunD((_) => { // Lets assume that all showDataLayers are initialized at this point state.selectedElement.ping() - State.state.locationControl.ping(); - return true; + State.state.locationControl.ping() + return true }) return state.mainMapObject - } - -} \ No newline at end of file +} diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 9252faed3..019589b49 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -1,31 +1,30 @@ -import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; -import State from "../State"; -import {Utils} from "../Utils"; -import {UIEventSource} from "../Logic/UIEventSource"; -import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"; -import MapControlButton from "./MapControlButton"; -import Svg from "../Svg"; -import Toggle from "./Input/Toggle"; -import UserBadge from "./BigComponents/UserBadge"; -import SearchAndGo from "./BigComponents/SearchAndGo"; -import BaseUIElement from "./BaseUIElement"; -import LeftControls from "./BigComponents/LeftControls"; -import RightControls from "./BigComponents/RightControls"; -import CenterMessageBox from "./CenterMessageBox"; -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; -import ScrollableFullScreen from "./Base/ScrollableFullScreen"; -import Translations from "./i18n/Translations"; -import SimpleAddUI from "./BigComponents/SimpleAddUI"; -import StrayClickHandler from "../Logic/Actors/StrayClickHandler"; -import {DefaultGuiState} from "./DefaultGuiState"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import * as home_location_json from "../assets/layers/home_location/home_location.json"; -import NewNoteUi from "./Popup/NewNoteUi"; -import Combine from "./Base/Combine"; -import AddNewMarker from "./BigComponents/AddNewMarker"; -import FilteredLayer from "../Models/FilteredLayer"; -import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; - +import FeaturePipelineState from "../Logic/State/FeaturePipelineState" +import State from "../State" +import { Utils } from "../Utils" +import { UIEventSource } from "../Logic/UIEventSource" +import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" +import MapControlButton from "./MapControlButton" +import Svg from "../Svg" +import Toggle from "./Input/Toggle" +import UserBadge from "./BigComponents/UserBadge" +import SearchAndGo from "./BigComponents/SearchAndGo" +import BaseUIElement from "./BaseUIElement" +import LeftControls from "./BigComponents/LeftControls" +import RightControls from "./BigComponents/RightControls" +import CenterMessageBox from "./CenterMessageBox" +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" +import ScrollableFullScreen from "./Base/ScrollableFullScreen" +import Translations from "./i18n/Translations" +import SimpleAddUI from "./BigComponents/SimpleAddUI" +import StrayClickHandler from "../Logic/Actors/StrayClickHandler" +import { DefaultGuiState } from "./DefaultGuiState" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import * as home_location_json from "../assets/layers/home_location/home_location.json" +import NewNoteUi from "./Popup/NewNoteUi" +import Combine from "./Base/Combine" +import AddNewMarker from "./BigComponents/AddNewMarker" +import FilteredLayer from "../Models/FilteredLayer" +import ExtraLinkButton from "./BigComponents/ExtraLinkButton" /** * The default MapComplete GUI initializor @@ -33,49 +32,64 @@ import ExtraLinkButton from "./BigComponents/ExtraLinkButton"; * Adds a welcome pane, contorl buttons, ... etc to index.html */ export default class DefaultGUI { - private readonly guiState: DefaultGuiState; - private readonly state: FeaturePipelineState; - + private readonly guiState: DefaultGuiState + private readonly state: FeaturePipelineState constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { - this.state = state; - this.guiState = guiState; + this.state = state + this.guiState = guiState + } - } - - public setup(){ - this.SetupUIElements(); + public setup() { + this.SetupUIElements() this.SetupMap() - if (this.state.layoutToUse.customCss !== undefined && window.location.pathname.indexOf("index") >= 0) { + if ( + this.state.layoutToUse.customCss !== undefined && + window.location.pathname.indexOf("index") >= 0 + ) { Utils.LoadCustomCss(this.state.layoutToUse.customCss) } - - Utils.downloadJson("./service-worker-version").then(data => console.log("Service worker", data)).catch(e => console.log("Service worker not active")) - } - - public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) { - const hasPresets = state.layoutToUse.layers.some(layer => layer.presets.length > 0); - const noteLayer: FilteredLayer = state.filteredLayers.data.filter(l => l.layerDef.id === "note")[0] - let addNewNoteDialog: (isShown: UIEventSource<boolean>) => BaseUIElement = undefined; + Utils.downloadJson("./service-worker-version") + .then((data) => console.log("Service worker", data)) + .catch((e) => console.log("Service worker not active")) + } + + public setupClickDialogOnMap( + filterViewIsOpened: UIEventSource<boolean>, + state: FeaturePipelineState + ) { + const hasPresets = state.layoutToUse.layers.some((layer) => layer.presets.length > 0) + const noteLayer: FilteredLayer = state.filteredLayers.data.filter( + (l) => l.layerDef.id === "note" + )[0] + let addNewNoteDialog: (isShown: UIEventSource<boolean>) => BaseUIElement = undefined if (noteLayer !== undefined) { addNewNoteDialog = (isShown) => new NewNoteUi(noteLayer, isShown, state) } function setup() { if (!hasPresets && addNewNoteDialog === undefined) { - return; // nothing to do + return // nothing to do } - const newPointDialogIsShown = new UIEventSource<boolean>(false); + const newPointDialogIsShown = new UIEventSource<boolean>(false) const addNewPoint = new ScrollableFullScreen( - () => hasPresets ? Translations.t.general.add.title : Translations.t.notes.createNoteTitle, - ({resetScrollSignal}) => { - let addNew = undefined; + () => + hasPresets + ? Translations.t.general.add.title + : Translations.t.notes.createNoteTitle, + ({ resetScrollSignal }) => { + let addNew = undefined if (hasPresets) { - addNew = new SimpleAddUI(newPointDialogIsShown, resetScrollSignal, filterViewIsOpened, state); + addNew = new SimpleAddUI( + newPointDialogIsShown, + resetScrollSignal, + filterViewIsOpened, + state + ) } - let addNote = undefined; + let addNote = undefined if (noteLayer !== undefined) { addNote = addNewNoteDialog(newPointDialogIsShown) } @@ -83,22 +97,23 @@ export default class DefaultGUI { }, "new", newPointDialogIsShown - ); + ) addNewPoint.isShown.addCallback((isShown) => { if (!isShown) { // Clear the 'last-click'-location when the dialog is closed - this causes the popup and the marker to be removed - state.LastClickLocation.setData(undefined); + state.LastClickLocation.setData(undefined) } - }); + }) - let noteMarker = undefined; + let noteMarker = undefined if (!hasPresets && addNewNoteDialog !== undefined) { - noteMarker = new Combine( - [Svg.note_svg().SetClass("absolute bottom-0").SetStyle("height: 40px"), - Svg.addSmall_svg().SetClass("absolute w-6 animate-pulse") - .SetStyle("right: 10px; bottom: -8px;") - ]) + noteMarker = new Combine([ + Svg.note_svg().SetClass("absolute bottom-0").SetStyle("height: 40px"), + Svg.addSmall_svg() + .SetClass("absolute w-6 animate-pulse") + .SetStyle("right: 10px; bottom: -8px;"), + ]) .SetClass("block relative h-full") .SetStyle("left: calc( 50% - 15px )") // This is a bit hacky, yes I know! } @@ -107,91 +122,83 @@ export default class DefaultGUI { state, addNewPoint, hasPresets ? new AddNewMarker(state.filteredLayers) : noteMarker - ); + ) } if (noteLayer !== undefined) { setup() } else { - state.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => { + state.featureSwitchAddNew.addCallbackAndRunD((addNewAllowed) => { if (addNewAllowed) { setup() - return true; + return true } }) } - } private SetupMap() { - const state = this.state; - const guiState = this.guiState; + const state = this.state + const guiState = this.guiState // Attach the map - state.mainMapObject.SetClass("w-full h-full") - .AttachTo("leafletDiv") - - this.setupClickDialogOnMap( - guiState.filterViewIsOpened, - state - ) + state.mainMapObject.SetClass("w-full h-full").AttachTo("leafletDiv") + this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) new ShowDataLayer({ leafletMap: state.leafletMap, layerToShow: new LayerConfig(home_location_json, "home_location", true), features: state.homeLocation, - state + state, }) - state.leafletMap.addCallbackAndRunD(_ => { + state.leafletMap.addCallbackAndRunD((_) => { // Lets assume that all showDataLayers are initialized at this point state.selectedElement.ping() - State.state.locationControl.ping(); - return true; + State.state.locationControl.ping() + return true }) - } private SetupUIElements() { - const state = this.state; - const guiState = this.guiState; + const state = this.state + const guiState = this.guiState const self = this new Combine([ - Toggle.If(state.featureSwitchUserbadge, - () => new UserBadge(state) - ), - Toggle.If(state.featureSwitchExtraLinkEnabled, + Toggle.If(state.featureSwitchUserbadge, () => new UserBadge(state)), + Toggle.If( + state.featureSwitchExtraLinkEnabled, () => new ExtraLinkButton(state, state.layoutToUse.extraLink) - ) - ]).SetClass("flex flex-col") + ), + ]) + .SetClass("flex flex-col") .AttachTo("userbadge") new Combine([ - new ExtraLinkButton(state, {...state.layoutToUse.extraLink, newTab: true, requirements: new Set<"iframe" | "no-iframe" | "welcome-message" | "no-welcome-message">( ) }) - ]).SetClass("flex items-center justify-center normal-background h-full") + new ExtraLinkButton(state, { + ...state.layoutToUse.extraLink, + newTab: true, + requirements: new Set< + "iframe" | "no-iframe" | "welcome-message" | "no-welcome-message" + >(), + }), + ]) + .SetClass("flex items-center justify-center normal-background h-full") .AttachTo("on-small-screen") - Toggle.If(state.featureSwitchSearch, - () => new SearchAndGo(state)) - .AttachTo("searchbox"); + Toggle.If(state.featureSwitchSearch, () => new SearchAndGo(state)).AttachTo("searchbox") - - Toggle.If( - state.featureSwitchWelcomeMessage, - () => self.InitWelcomeMessage() + Toggle.If(state.featureSwitchWelcomeMessage, () => self.InitWelcomeMessage()).AttachTo( + "messagesbox" ) - .AttachTo("messagesbox"); + new LeftControls(state, guiState).AttachTo("bottom-left") + new RightControls(state).AttachTo("bottom-right") - new LeftControls(state, guiState).AttachTo("bottom-left"); - new RightControls(state).AttachTo("bottom-right"); - - new CenterMessageBox(state).AttachTo("centermessage"); - document - .getElementById("centermessage") - .classList.add("pointer-events-none"); + new CenterMessageBox(state).AttachTo("centermessage") + document.getElementById("centermessage").classList.add("pointer-events-none") // We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed for (const state of guiState.allFullScreenStates) { @@ -205,39 +212,40 @@ export default class DefaultGUI { */ state.selectedElement.addCallbackAndRunD((_) => { - guiState.allFullScreenStates.forEach(s => s.setData(false)) - }); + guiState.allFullScreenStates.forEach((s) => s.setData(false)) + }) } private InitWelcomeMessage(): BaseUIElement { const isOpened = this.guiState.welcomeMessageIsOpened - const fullOptions = new FullWelcomePaneWithTabs(isOpened, this.guiState.welcomeMessageOpenedTab, this.state); + const fullOptions = new FullWelcomePaneWithTabs( + isOpened, + this.guiState.welcomeMessageOpenedTab, + this.state + ) // ?-Button on Desktop, opens panel with close-X. - const help = new MapControlButton(Svg.help_svg()); - help.onClick(() => isOpened.setData(true)); + const help = new MapControlButton(Svg.help_svg()) + help.onClick(() => isOpened.setData(true)) - - const openedTime = new Date().getTime(); + const openedTime = new Date().getTime() this.state.locationControl.addCallback(() => { if (new Date().getTime() - openedTime < 15 * 1000) { // Don't autoclose the first 15 secs when the map is moving - return; + return } - isOpened.setData(false); - return true; // Unregister this caller - we only autoclose once - }); + isOpened.setData(false) + return true // Unregister this caller - we only autoclose once + }) this.state.selectedElement.addCallbackAndRunD((_) => { - isOpened.setData(false); - }); + isOpened.setData(false) + }) return new Toggle( fullOptions.SetClass("welcomeMessage pointer-events-auto"), help.SetClass("pointer-events-auto"), isOpened ) - } - -} \ No newline at end of file +} diff --git a/UI/DefaultGuiState.ts b/UI/DefaultGuiState.ts index b67723c10..ccdfe323e 100644 --- a/UI/DefaultGuiState.ts +++ b/UI/DefaultGuiState.ts @@ -1,25 +1,25 @@ -import {UIEventSource} from "../Logic/UIEventSource"; -import {QueryParameters} from "../Logic/Web/QueryParameters"; -import Hash from "../Logic/Web/Hash"; +import { UIEventSource } from "../Logic/UIEventSource" +import { QueryParameters } from "../Logic/Web/QueryParameters" +import Hash from "../Logic/Web/Hash" export class DefaultGuiState { - static state: DefaultGuiState; - public readonly welcomeMessageIsOpened: UIEventSource<boolean>; - public readonly downloadControlIsOpened: UIEventSource<boolean>; - public readonly filterViewIsOpened: UIEventSource<boolean>; - public readonly copyrightViewIsOpened: UIEventSource<boolean>; - public readonly currentViewControlIsOpened: UIEventSource<boolean>; + static state: DefaultGuiState + public readonly welcomeMessageIsOpened: UIEventSource<boolean> + public readonly downloadControlIsOpened: UIEventSource<boolean> + public readonly filterViewIsOpened: UIEventSource<boolean> + public readonly copyrightViewIsOpened: UIEventSource<boolean> + public readonly currentViewControlIsOpened: UIEventSource<boolean> public readonly welcomeMessageOpenedTab: UIEventSource<number> public readonly allFullScreenStates: UIEventSource<boolean>[] = [] constructor() { - - - this.welcomeMessageOpenedTab = UIEventSource.asFloat(QueryParameters.GetQueryParameter( - "tab", - "0", - `The tab that is shown in the welcome-message.` - )); + this.welcomeMessageOpenedTab = UIEventSource.asFloat( + QueryParameters.GetQueryParameter( + "tab", + "0", + `The tab that is shown in the welcome-message.` + ) + ) this.welcomeMessageIsOpened = QueryParameters.GetBooleanQueryParameter( "welcome-control-toggle", false, @@ -50,9 +50,9 @@ export class DefaultGuiState { filters: this.filterViewIsOpened, copyright: this.copyrightViewIsOpened, currentview: this.currentViewControlIsOpened, - welcome: this.welcomeMessageIsOpened + welcome: this.welcomeMessageIsOpened, } - Hash.hash.addCallbackAndRunD(hash => { + Hash.hash.addCallbackAndRunD((hash) => { hash = hash.toLowerCase() states[hash]?.setData(true) }) @@ -61,22 +61,27 @@ export class DefaultGuiState { this.welcomeMessageIsOpened.setData(true) } - this.allFullScreenStates.push(this.downloadControlIsOpened, this.filterViewIsOpened, this.copyrightViewIsOpened, this.welcomeMessageIsOpened, this.currentViewControlIsOpened) + this.allFullScreenStates.push( + this.downloadControlIsOpened, + this.filterViewIsOpened, + this.copyrightViewIsOpened, + this.welcomeMessageIsOpened, + this.currentViewControlIsOpened + ) for (let i = 0; i < this.allFullScreenStates.length; i++) { - const fullScreenState = this.allFullScreenStates[i]; + const fullScreenState = this.allFullScreenStates[i] for (let j = 0; j < this.allFullScreenStates.length; j++) { if (i == j) { continue } - const otherState = this.allFullScreenStates[j]; - fullScreenState.addCallbackAndRunD(isOpened => { + const otherState = this.allFullScreenStates[j] + fullScreenState.addCallbackAndRunD((isOpened) => { if (isOpened) { otherState.setData(false) } }) } } - } -} \ No newline at end of file +} diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts index 52be72d4d..0ff42c860 100644 --- a/UI/ExportPDF.ts +++ b/UI/ExportPDF.ts @@ -1,16 +1,16 @@ -import jsPDF from "jspdf"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Minimap, {MinimapObj} from "./Base/Minimap"; -import Loc from "../Models/Loc"; -import BaseLayer from "../Models/BaseLayer"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import Translations from "./i18n/Translations"; -import State from "../State"; -import Constants from "../Models/Constants"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; -import {BBox} from "../Logic/BBox"; +import jsPDF from "jspdf" +import { UIEventSource } from "../Logic/UIEventSource" +import Minimap, { MinimapObj } from "./Base/Minimap" +import Loc from "../Models/Loc" +import BaseLayer from "../Models/BaseLayer" +import { FixedUiElement } from "./Base/FixedUiElement" +import Translations from "./i18n/Translations" +import State from "../State" +import Constants from "../Models/Constants" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline" +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" +import { BBox } from "../Logic/BBox" /** * Creates screenshoter to take png screenshot @@ -26,81 +26,77 @@ export default class ExportPDF { // dimensions of the map in milimeter public isRunning = new UIEventSource(true) // A4: 297 * 210mm - private readonly mapW = 297; - private readonly mapH = 210; + private readonly mapW = 297 + private readonly mapH = 210 private readonly scaling = 2 - private readonly freeDivId: string; - private readonly _layout: LayoutConfig; - private _screenhotTaken = false; + private readonly freeDivId: string + private readonly _layout: LayoutConfig + private _screenhotTaken = false - constructor( - options: { - freeDivId: string, - location: UIEventSource<Loc>, - background?: UIEventSource<BaseLayer> - features: FeaturePipeline, - layout: LayoutConfig - } - ) { - - this.freeDivId = options.freeDivId; - this._layout = options.layout; - const self = this; + constructor(options: { + freeDivId: string + location: UIEventSource<Loc> + background?: UIEventSource<BaseLayer> + features: FeaturePipeline + layout: LayoutConfig + }) { + this.freeDivId = options.freeDivId + this._layout = options.layout + const self = this // We create a minimap at the given location and attach it to the given 'hidden' element - const l = options.location.data; + const l = options.location.data const loc = { lat: l.lat, lon: l.lon, - zoom: l.zoom + 1 + zoom: l.zoom + 1, } const minimap = Minimap.createMiniMap({ location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot background: options.background, allowMoving: false, - onFullyLoaded: _ => window.setTimeout(() => { - if (self._screenhotTaken) { - return; - } - try { - self.CreatePdf(minimap) - .then(() => self.cleanup()) - .catch(() => self.cleanup()) - } catch (e) { - console.error(e) - self.cleanup() - } - - }, 500) + onFullyLoaded: (_) => + window.setTimeout(() => { + if (self._screenhotTaken) { + return + } + try { + self.CreatePdf(minimap) + .then(() => self.cleanup()) + .catch(() => self.cleanup()) + } catch (e) { + console.error(e) + self.cleanup() + } + }, 500), }) - minimap.SetStyle(`width: ${this.mapW * this.scaling}mm; height: ${this.mapH * this.scaling}mm;`) + minimap.SetStyle( + `width: ${this.mapW * this.scaling}mm; height: ${this.mapH * this.scaling}mm;` + ) minimap.AttachTo(options.freeDivId) // Next: we prepare the features. Only fully contained features are shown - minimap.leafletMap.addCallbackAndRunD(leaflet => { + minimap.leafletMap.addCallbackAndRunD((leaflet) => { const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2)) - options.features.GetTilesPerLayerWithin(bounds, tile => { + options.features.GetTilesPerLayerWithin(bounds, (tile) => { if (tile.layer.layerDef.minzoom > l.zoom) { return } - if(tile.layer.layerDef.id.startsWith("note_import")){ + if (tile.layer.layerDef.id.startsWith("note_import")) { // Don't export notes to import - return; + return } - new ShowDataLayer( - { - features: tile, - leafletMap: minimap.leafletMap, - layerToShow: tile.layer.layerDef, - doShowLayer: tile.layer.isDisplayed, - state: undefined - } - ) + new ShowDataLayer({ + features: tile, + leafletMap: minimap.leafletMap, + layerToShow: tile.layer.layerDef, + doShowLayer: tile.layer.isDisplayed, + state: undefined, + }) }) - }) State.state.AddAllOverlaysToMap(minimap.leafletMap) @@ -108,85 +104,92 @@ export default class ExportPDF { private cleanup() { new FixedUiElement("Screenshot taken!").AttachTo(this.freeDivId) - this._screenhotTaken = true; + this._screenhotTaken = true } private async CreatePdf(minimap: MinimapObj) { - - console.log("PDF creation started") - const t = Translations.t.general.pdf; + const t = Translations.t.general.pdf const layout = this._layout - let doc = new jsPDF('landscape'); + let doc = new jsPDF("landscape") const image = await minimap.TakeScreenshot() // @ts-ignore - doc.addImage(image, 'PNG', 0, 0, this.mapW, this.mapH); - + doc.addImage(image, "PNG", 0, 0, this.mapW, this.mapH) doc.setDrawColor(255, 255, 255) doc.setFillColor(255, 255, 255) - doc.roundedRect(12, 10, 145, 25, 5, 5, 'FD') + doc.roundedRect(12, 10, 145, 25, 5, 5, "FD") doc.setFontSize(20) doc.textWithLink(layout.title.txt, 40, 18.5, { maxWidth: 125, - url: window.location.href + url: window.location.href, }) doc.setFontSize(10) doc.text(t.generatedWith.txt, 40, 23, { - maxWidth: 125 + maxWidth: 125, }) const backgroundLayer: BaseLayer = State.state.backgroundLayer.data - const attribution = new FixedUiElement(backgroundLayer.layer().getAttribution() ?? backgroundLayer.name).ConstructElement().textContent + const attribution = new FixedUiElement( + backgroundLayer.layer().getAttribution() ?? backgroundLayer.name + ).ConstructElement().textContent doc.textWithLink(t.attr.txt, 40, 26.5, { maxWidth: 125, - url: "https://www.openstreetmap.org/copyright" + url: "https://www.openstreetmap.org/copyright", }) - doc.text(t.attrBackground.Subs({ - background: attribution - }).txt, 40, 30) + doc.text( + t.attrBackground.Subs({ + background: attribution, + }).txt, + 40, + 30 + ) let date = new Date().toISOString().substr(0, 16) doc.setFontSize(7) - doc.text(t.versionInfo.Subs({ - version: Constants.vNumber, - date: date - }).txt, 40, 34, { - maxWidth: 125 - }) + doc.text( + t.versionInfo.Subs({ + version: Constants.vNumber, + date: date, + }).txt, + 40, + 34, + { + maxWidth: 125, + } + ) // Add the logo of the layout - let img = document.createElement('img'); + let img = document.createElement("img") const imgSource = layout.icon - const imgType = imgSource.substr(imgSource.lastIndexOf(".") + 1); + const imgType = imgSource.substr(imgSource.lastIndexOf(".") + 1) img.src = imgSource if (imgType.toLowerCase() === "svg") { new FixedUiElement("").AttachTo(this.freeDivId) // This is an svg image, we use the canvas to convert it to a png - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d'); + const canvas = document.createElement("canvas") + const ctx = canvas.getContext("2d") canvas.width = 500 canvas.height = 500 img.style.width = "100%" img.style.height = "100%" - ctx.drawImage(img, 0, 0, 500, 500); + ctx.drawImage(img, 0, 0, 500, 500) const base64img = canvas.toDataURL("image/png") - doc.addImage(base64img, 'png', 15, 12, 20, 20); - + doc.addImage(base64img, "png", 15, 12, 20, 20) } else { try { - doc.addImage(img, imgType, 15, 12, 20, 20); + doc.addImage(img, imgType, 15, 12, 20, 20) } catch (e) { console.error(e) } } - doc.save(`MapComplete_${layout.title.txt}_${date}.pdf`); + doc.save(`MapComplete_${layout.title.txt}_${date}.pdf`) this.isRunning.setData(false) } diff --git a/UI/Image/AttributedImage.ts b/UI/Image/AttributedImage.ts index bb6ab806d..2c4820d2e 100644 --- a/UI/Image/AttributedImage.ts +++ b/UI/Image/AttributedImage.ts @@ -1,37 +1,29 @@ -import Combine from "../Base/Combine"; -import Attribution from "./Attribution"; -import Img from "../Base/Img"; -import ImageProvider from "../../Logic/ImageProviders/ImageProvider"; -import BaseUIElement from "../BaseUIElement"; -import {Mapillary} from "../../Logic/ImageProviders/Mapillary"; -import {UIEventSource} from "../../Logic/UIEventSource"; - +import Combine from "../Base/Combine" +import Attribution from "./Attribution" +import Img from "../Base/Img" +import ImageProvider from "../../Logic/ImageProviders/ImageProvider" +import BaseUIElement from "../BaseUIElement" +import { Mapillary } from "../../Logic/ImageProviders/Mapillary" +import { UIEventSource } from "../../Logic/UIEventSource" export class AttributedImage extends Combine { - - constructor(imageInfo: { - url: string, - provider?: ImageProvider, - date?: Date - } - ) { - let img: BaseUIElement; + constructor(imageInfo: { url: string; provider?: ImageProvider; date?: Date }) { + let img: BaseUIElement img = new Img(imageInfo.url, false, { - fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined - }); - + fallbackImage: + imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined, + }) + let attr: BaseUIElement = undefined - if(imageInfo.provider !== undefined){ - attr = new Attribution(UIEventSource.FromPromise( imageInfo.provider?.DownloadAttribution(imageInfo.url)), + if (imageInfo.provider !== undefined) { + attr = new Attribution( + UIEventSource.FromPromise(imageInfo.provider?.DownloadAttribution(imageInfo.url)), imageInfo.provider?.SourceIcon(), imageInfo.date ) } - - super([img, attr]); - this.SetClass('block relative h-full'); + super([img, attr]) + this.SetClass("block relative h-full") } - - -} \ No newline at end of file +} diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index adf70d39b..a44189a1e 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -1,17 +1,16 @@ -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Store} from "../../Logic/UIEventSource"; -import {LicenseInfo} from "../../Logic/ImageProviders/LicenseInfo"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Link from "../Base/Link"; +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Store } from "../../Logic/UIEventSource" +import { LicenseInfo } from "../../Logic/ImageProviders/LicenseInfo" +import { FixedUiElement } from "../Base/FixedUiElement" +import Link from "../Base/Link" /** * Small box in the bottom left of an image, e.g. the image in a popup */ export default class Attribution extends VariableUiElement { - constructor(license: Store<LicenseInfo>, icon: BaseUIElement, date?: Date) { if (license === undefined) { throw "No license source given in the attribution element" @@ -22,7 +21,7 @@ export default class Attribution extends VariableUiElement { return undefined } - let title = undefined; + let title = undefined if (license?.title) { title = Translations.W(license?.title).SetClass("block") if (license.informationLocation) { @@ -30,17 +29,22 @@ export default class Attribution extends VariableUiElement { } } return new Combine([ - icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), + icon + ?.SetClass("block left") + .SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), new Combine([ title, Translations.W(license?.artist ?? "").SetClass("block font-bold"), Translations.W(license?.license ?? license?.licenseShortName), - date === undefined ? undefined : new FixedUiElement(date.toLocaleDateString()) - ]).SetClass("flex flex-col") - ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images") - - })); + date === undefined + ? undefined + : new FixedUiElement(date.toLocaleDateString()), + ]).SetClass("flex flex-col"), + ]).SetClass( + "flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images" + ) + }) + ) } - -} \ No newline at end of file +} diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 183762d20..636750607 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -1,47 +1,56 @@ -import {Store} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import Toggle, {ClickableToggle} from "../Input/Toggle"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import {Tag} from "../../Logic/Tags/Tag"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import {Changes} from "../../Logic/Osm/Changes"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import { Store } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import Toggle, { ClickableToggle } from "../Input/Toggle" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import { Tag } from "../../Logic/Tags/Tag" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import { Changes } from "../../Logic/Osm/Changes" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export default class DeleteImage extends Toggle { - - constructor(key: string, tags: Store<any>, state: { layoutToUse: LayoutConfig, changes?: Changes, osmConnection?: OsmConnection }) { + constructor( + key: string, + tags: Store<any>, + state: { layoutToUse: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } + ) { const oldValue = tags.data[key] - const isDeletedBadge = Translations.t.image.isDeleted.Clone() + const isDeletedBadge = Translations.t.image.isDeleted + .Clone() .SetClass("rounded-full p-1") .SetStyle("color:white;background:#ff8c8c") .onClick(async () => { - await state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { - changeType: "delete-image", - theme: state.layoutToUse.id - })) - }); + await state?.changes?.applyAction( + new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { + changeType: "delete-image", + theme: state.layoutToUse.id, + }) + ) + }) - const deleteButton = Translations.t.image.doDelete.Clone() + const deleteButton = Translations.t.image.doDelete + .Clone() .SetClass("block w-full pl-4 pr-4") - .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") + .SetStyle( + "color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;" + ) .onClick(async () => { await state?.changes?.applyAction( new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { changeType: "answer", - theme: state.layoutToUse.id + theme: state.layoutToUse.id, }) ) - }); + }) - const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); + const cancelButton = Translations.t.general.cancel + .Clone() + .SetClass("bg-white pl-4 pr-4") + .SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;") const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;") const deleteDialog = new ClickableToggle( - new Combine([ - deleteButton, - cancelButton - ]).SetClass("flex flex-col background-black"), + new Combine([deleteButton, cancelButton]).SetClass("flex flex-col background-black"), openDelete ) @@ -52,12 +61,11 @@ export default class DeleteImage extends Toggle { new Toggle( deleteDialog, isDeletedBadge, - tags.map(tags => (tags[key] ?? "") !== "") + tags.map((tags) => (tags[key] ?? "") !== "") ), undefined /*Login (and thus editing) is disabled*/, state?.osmConnection?.isLoggedIn ) this.SetClass("cursor-pointer") } - -} \ No newline at end of file +} diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 5c1d6e829..c9ca57f3c 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -1,50 +1,53 @@ -import {SlideShow} from "./SlideShow"; -import {Store} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import DeleteImage from "./DeleteImage"; -import {AttributedImage} from "./AttributedImage"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "../Input/Toggle"; -import ImageProvider from "../../Logic/ImageProviders/ImageProvider"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {Changes} from "../../Logic/Osm/Changes"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import { SlideShow } from "./SlideShow" +import { Store } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import DeleteImage from "./DeleteImage" +import { AttributedImage } from "./AttributedImage" +import BaseUIElement from "../BaseUIElement" +import Toggle from "../Input/Toggle" +import ImageProvider from "../../Logic/ImageProviders/ImageProvider" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { Changes } from "../../Logic/Osm/Changes" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export class ImageCarousel extends Toggle { + constructor( + images: Store<{ key: string; url: string; provider: ImageProvider }[]>, + tags: Store<any>, + state: { osmConnection?: OsmConnection; changes?: Changes; layoutToUse: LayoutConfig } + ) { + const uiElements = images.map( + (imageURLS: { key: string; url: string; provider: ImageProvider }[]) => { + const uiElements: BaseUIElement[] = [] + for (const url of imageURLS) { + try { + let image = new AttributedImage(url) - constructor(images: Store<{ key: string, url: string, provider: ImageProvider }[]>, - tags: Store<any>, - state: { osmConnection?: OsmConnection, changes?: Changes, layoutToUse: LayoutConfig }) { - const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => { - const uiElements: BaseUIElement[] = []; - for (const url of imageURLS) { - try { - let image = new AttributedImage(url) - - if (url.key !== undefined) { - image = new Combine([ - image, - new DeleteImage(url.key, tags, state).SetClass("delete-image-marker absolute top-0 left-0 pl-3") - ]).SetClass("relative"); + if (url.key !== undefined) { + image = new Combine([ + image, + new DeleteImage(url.key, tags, state).SetClass( + "delete-image-marker absolute top-0 left-0 pl-3" + ), + ]).SetClass("relative") + } + image + .SetClass("w-full block") + .SetStyle("min-width: 50px; background: grey;") + uiElements.push(image) + } catch (e) { + console.error("Could not generate image element for", url.url, "due to", e) } - image - .SetClass("w-full block") - .SetStyle("min-width: 50px; background: grey;") - uiElements.push(image); - } catch (e) { - console.error("Could not generate image element for", url.url, "due to", e) } - - + return uiElements } - return uiElements; - }); + ) super( new SlideShow(uiElements).SetClass("w-full"), undefined, - uiElements.map(els => els.length > 0) + uiElements.map((els) => els.length > 0) ) - this.SetClass("block w-full"); + this.SetClass("block w-full") } -} \ No newline at end of file +} diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index b459a3341..0f7dcc7df 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -1,168 +1,189 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import Svg from "../../Svg"; -import {Tag} from "../../Logic/Tags/Tag"; -import BaseUIElement from "../BaseUIElement"; -import LicensePicker from "../BigComponents/LicensePicker"; -import Toggle from "../Input/Toggle"; -import FileSelectorButton from "../Input/FileSelectorButton"; -import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {Changes} from "../../Logic/Osm/Changes"; -import Loading from "../Base/Loading"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import Svg from "../../Svg" +import { Tag } from "../../Logic/Tags/Tag" +import BaseUIElement from "../BaseUIElement" +import LicensePicker from "../BigComponents/LicensePicker" +import Toggle from "../Input/Toggle" +import FileSelectorButton from "../Input/FileSelectorButton" +import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { FixedUiElement } from "../Base/FixedUiElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { Changes } from "../../Logic/Osm/Changes" +import Loading from "../Base/Loading" export class ImageUploadFlow extends Toggle { - - private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() - constructor(tagsSource: Store<any>, - state: { - osmConnection: OsmConnection; - layoutToUse: LayoutConfig; - changes: Changes, - featureSwitchUserbadge: Store<boolean>; - }, - imagePrefix: string = "image", text: string = undefined) { + constructor( + tagsSource: Store<any>, + state: { + osmConnection: OsmConnection + layoutToUse: LayoutConfig + changes: Changes + featureSwitchUserbadge: Store<boolean> + }, + imagePrefix: string = "image", + text: string = undefined + ) { const perId = ImageUploadFlow.uploadCountsPerId const id = tagsSource.data.id if (!perId.has(id)) { perId.set(id, new UIEventSource<number>(0)) } const uploadedCount = perId.get(id) - const uploader = new ImgurUploader(async url => { + const uploader = new ImgurUploader(async (url) => { // A file was uploaded - we add it to the tags of the object const tags = tagsSource.data let key = imagePrefix if (tags[imagePrefix] !== undefined) { - let freeIndex = 0; + let freeIndex = 0 while (tags[imagePrefix + ":" + freeIndex] !== undefined) { - freeIndex++; + freeIndex++ } - key = imagePrefix + ":" + freeIndex; + key = imagePrefix + ":" + freeIndex } - - await state.changes - .applyAction(new ChangeTagAction( - tags.id, new Tag(key, url), tagsSource.data, - { - changeType: "add-image", - theme: state.layoutToUse.id - } - )) - console.log("Adding image:" + key, url); + + await state.changes.applyAction( + new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { + changeType: "add-image", + theme: state.layoutToUse.id, + }) + ) + console.log("Adding image:" + key, url) uploadedCount.data++ uploadedCount.ping() }) const licensePicker = new LicensePicker(state) const explanations = LicensePicker.LicenseExplanations() - const chosenLicense = new VariableUiElement(licensePicker.GetValue().map(license => explanations.get(license))) + const chosenLicense = new VariableUiElement( + licensePicker.GetValue().map((license) => explanations.get(license)) + ) - const t = Translations.t.image; + const t = Translations.t.image let labelContent: BaseUIElement if (text === undefined) { - labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ") + labelContent = Translations.t.image.addPicture + .Clone() + .SetClass("block align-middle mt-1 ml-3 text-4xl ") } else { - labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ") + labelContent = new FixedUiElement(text).SetClass( + "block align-middle mt-1 ml-3 text-2xl " + ) } const label = new Combine([ Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "), - labelContent - ]).SetClass("p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center") + labelContent, + ]).SetClass( + "p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center" + ) const fileSelector = new FileSelectorButton(label) - fileSelector.GetValue().addCallback(filelist => { + fileSelector.GetValue().addCallback((filelist) => { if (filelist === undefined || filelist.length === 0) { - return; + return } - for (var i = 0; i < filelist.length; i++) { const sizeInBytes = filelist[i].size - console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes"); + console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes") if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) { - alert(Translations.t.image.toBig.Subs({ - actual_size: (Math.floor(sizeInBytes / 1000000)) + "MB", - max_size: uploader.maxFileSizeInMegabytes + "MB" - }).txt) - return; + alert( + Translations.t.image.toBig.Subs({ + actual_size: Math.floor(sizeInBytes / 1000000) + "MB", + max_size: uploader.maxFileSizeInMegabytes + "MB", + }).txt + ) + return } } console.log("Received images from the user, starting upload") const license = licensePicker.GetValue()?.data ?? "CC0" - const tags = tagsSource.data; + const tags = tagsSource.data const layout = state?.layoutToUse let matchingLayer: LayerConfig = undefined for (const layer of layout?.layers ?? []) { if (layer.source.osmTags.matchesProperties(tags)) { - matchingLayer = layer; - break; + matchingLayer = layer + break } } - - const title = matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.ConstructElement()?.textContent ?? tags.name ?? "https//osm.org/"+tags.id; + const title = + matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.ConstructElement() + ?.textContent ?? + tags.name ?? + "https//osm.org/" + tags.id const description = [ "author:" + state.osmConnection.userDetails.data.name, "license:" + license, "osmid:" + tags.id, - ].join("\n"); + ].join("\n") uploader.uploadMany(title, description, filelist) - }) - const uploadFlow: BaseUIElement = new Combine([ - new VariableUiElement(uploader.queue.map(q => q.length).map(l => { - if (l == 0) { - return undefined; - } - if (l == 1) { - return new Loading( t.uploadingPicture).SetClass("alert") - } else { - return new Loading(t.uploadingMultiple.Subs({count: "" + l})).SetClass("alert") - } - })), - new VariableUiElement(uploader.failed.map(q => q.length).map(l => { - if (l == 0) { - return undefined - } - console.log(l) - return t.uploadFailed.SetClass("block alert"); - })), - new VariableUiElement(uploadedCount.map(l => { - if (l == 0) { - return undefined; - } - if (l == 1) { - return t.uploadDone.Clone().SetClass("thanks block"); - } - return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks block") - })), + new VariableUiElement( + uploader.queue + .map((q) => q.length) + .map((l) => { + if (l == 0) { + return undefined + } + if (l == 1) { + return new Loading(t.uploadingPicture).SetClass("alert") + } else { + return new Loading( + t.uploadingMultiple.Subs({ count: "" + l }) + ).SetClass("alert") + } + }) + ), + new VariableUiElement( + uploader.failed + .map((q) => q.length) + .map((l) => { + if (l == 0) { + return undefined + } + console.log(l) + return t.uploadFailed.SetClass("block alert") + }) + ), + new VariableUiElement( + uploadedCount.map((l) => { + if (l == 0) { + return undefined + } + if (l == 1) { + return t.uploadDone.Clone().SetClass("thanks block") + } + return t.uploadMultipleDone.Subs({ count: l }).SetClass("thanks block") + }) + ), fileSelector, Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"), licensePicker, - chosenLicense.SetClass("subtle text-sm") + chosenLicense.SetClass("subtle text-sm"), ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center") - - const pleaseLoginButton = t.pleaseLogin.Clone() + const pleaseLoginButton = t.pleaseLogin + .Clone() .onClick(() => state.osmConnection.AttemptLogin()) - .SetClass("login-button-friendly"); + .SetClass("login-button-friendly") super( new Toggle( /*We can show the actual upload button!*/ @@ -173,8 +194,5 @@ export class ImageUploadFlow extends Toggle { undefined /* Nothing as the user badge is disabled*/, state?.featureSwitchUserbadge ) - } - - -} \ No newline at end of file +} diff --git a/UI/Image/SlideShow.ts b/UI/Image/SlideShow.ts index aa19056c1..51bc6edb6 100644 --- a/UI/Image/SlideShow.ts +++ b/UI/Image/SlideShow.ts @@ -1,16 +1,14 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import {Utils} from "../../Utils"; -import Combine from "../Base/Combine"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import { Utils } from "../../Utils" +import Combine from "../Base/Combine" export class SlideShow extends BaseUIElement { - - - private readonly embeddedElements: Store<BaseUIElement[]>; + private readonly embeddedElements: Store<BaseUIElement[]> constructor(embeddedElements: Store<BaseUIElement[]>) { super() - this.embeddedElements = embeddedElements; + this.embeddedElements = embeddedElements this.SetStyle("scroll-snap-type: x mandatory; overflow-x: auto") } @@ -19,8 +17,7 @@ export class SlideShow extends BaseUIElement { el.style.minWidth = "min-content" el.style.display = "flex" el.style.justifyContent = "center" - this.embeddedElements.addCallbackAndRun(elements => { - + this.embeddedElements.addCallbackAndRun((elements) => { if (elements.length > 1) { el.style.justifyContent = "unset" } @@ -29,21 +26,23 @@ export class SlideShow extends BaseUIElement { el.removeChild(el.lastChild) } - elements = Utils.NoNull(elements).map(el => new Combine([el]) - .SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item") - .SetStyle("min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;") + elements = Utils.NoNull(elements).map((el) => + new Combine([el]) + .SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item") + .SetStyle( + "min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;" + ) ) for (const element of elements ?? []) { el.appendChild(element.ConstructElement()) } - }); + }) const wrapper = document.createElement("div") wrapper.style.maxWidth = "100%" wrapper.style.overflowX = "auto" wrapper.appendChild(el) - return wrapper; + return wrapper } - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/AskMetadata.ts b/UI/ImportFlow/AskMetadata.ts index 6584fc065..0784ccb1b 100644 --- a/UI/ImportFlow/AskMetadata.ts +++ b/UI/ImportFlow/AskMetadata.ts @@ -1,117 +1,128 @@ -import Combine from "../Base/Combine"; -import {FlowStep} from "./FlowStep"; -import {Store} from "../../Logic/UIEventSource"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; -import Title from "../Base/Title"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Translations from "../i18n/Translations"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; - -export class AskMetadata extends Combine implements FlowStep<{ - features: any[], - wikilink: string, - intro: string, - source: string, - theme: string -}> { +import Combine from "../Base/Combine" +import { FlowStep } from "./FlowStep" +import { Store } from "../../Logic/UIEventSource" +import ValidatedTextField from "../Input/ValidatedTextField" +import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" +import Title from "../Base/Title" +import { VariableUiElement } from "../Base/VariableUIElement" +import Translations from "../i18n/Translations" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import { Utils } from "../../Utils" +export class AskMetadata + extends Combine + implements + FlowStep<{ + features: any[] + wikilink: string + intro: string + source: string + theme: string + }> +{ public readonly Value: Store<{ - features: any[], - wikilink: string, - intro: string, - source: string, + features: any[] + wikilink: string + intro: string + source: string theme: string - }>; - public readonly IsValid: Store<boolean>; + }> + public readonly IsValid: Store<boolean> - constructor(params: ({ features: any[], theme: string })) { + constructor(params: { features: any[]; theme: string }) { const t = Translations.t.importHelper.askMetadata const introduction = ValidatedTextField.ForType("text").ConstructInputElement({ value: LocalStorageSource.Get("import-helper-introduction-text"), - inputStyle: "width: 100%" + inputStyle: "width: 100%", }) const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({ value: LocalStorageSource.Get("import-helper-wikilink-text"), - inputStyle: "width: 100%" + inputStyle: "width: 100%", }) const source = ValidatedTextField.ForType("string").ConstructInputElement({ value: LocalStorageSource.Get("import-helper-source-text"), - inputStyle: "width: 100%" + inputStyle: "width: 100%", }) super([ new Title(t.title), - t.intro.Subs({count: params.features.length}), - t.giveDescription, + t.intro.Subs({ count: params.features.length }), + t.giveDescription, introduction.SetClass("w-full border border-black"), - t.giveSource, - source.SetClass("w-full border border-black"), - t.giveWikilink , + t.giveSource, + source.SetClass("w-full border border-black"), + t.giveWikilink, wikilink.SetClass("w-full border border-black"), - new VariableUiElement(wikilink.GetValue().map(wikilink => { - try{ - const url = new URL(wikilink) - if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){ - return t.shouldBeOsmWikilink.SetClass("alert"); - } + new VariableUiElement( + wikilink.GetValue().map((wikilink) => { + try { + const url = new URL(wikilink) + if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") { + return t.shouldBeOsmWikilink.SetClass("alert") + } - if(url.pathname.toLowerCase() === "/wiki/main_page"){ - return t.shouldNotBeHomepage.SetClass("alert"); + if (url.pathname.toLowerCase() === "/wiki/main_page") { + return t.shouldNotBeHomepage.SetClass("alert") + } + } catch (e) { + return t.shouldBeUrl.SetClass("alert") } - }catch(e){ - return t.shouldBeUrl.SetClass("alert") - } - })), - t.orDownload, - new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading("Preparing your download", - async ( ) => { - const geojson = { - type:"FeatureCollection", - features: params.features - } - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+params.theme+".geojson",{ - mimetype: "application/vnd.geo+json" - }) }) - ]); + ), + t.orDownload, + new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading( + "Preparing your download", + async () => { + const geojson = { + type: "FeatureCollection", + features: params.features, + } + Utils.offerContentsAsDownloadableFile( + JSON.stringify(geojson), + "prepared_import_" + params.theme + ".geojson", + { + mimetype: "application/vnd.geo+json", + } + ) + } + ), + ]) this.SetClass("flex flex-col") - this.Value = introduction.GetValue().map(intro => { - return { - features: params.features, - wikilink: wikilink.GetValue().data, - intro, - source: source.GetValue().data, - theme: params.theme - } - }, [wikilink.GetValue(), source.GetValue()]) - - this.IsValid = this.Value.map(obj => { - if (obj === undefined) { - return false; - } - if ([ obj.features, obj.intro, obj.wikilink, obj.source].some(v => v === undefined)){ - return false; - } - - try{ - const url = new URL(obj.wikilink) - if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){ - return false; + this.Value = introduction.GetValue().map( + (intro) => { + return { + features: params.features, + wikilink: wikilink.GetValue().data, + intro, + source: source.GetValue().data, + theme: params.theme, } - }catch(e){ + }, + [wikilink.GetValue(), source.GetValue()] + ) + + this.IsValid = this.Value.map((obj) => { + if (obj === undefined) { return false } - - return true; - + if ([obj.features, obj.intro, obj.wikilink, obj.source].some((v) => v === undefined)) { + return false + } + + try { + const url = new URL(obj.wikilink) + if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") { + return false + } + } catch (e) { + return false + } + + return true }) } - - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts index 8cf334660..128885dab 100644 --- a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts +++ b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts @@ -1,74 +1,84 @@ -import Combine from "../Base/Combine"; -import {FlowStep} from "./FlowStep"; -import {BBox} from "../../Logic/BBox"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; -import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource"; -import MetaTagging from "../../Logic/MetaTagging"; -import RelationsTracker from "../../Logic/Osm/RelationsTracker"; -import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"; -import Minimap from "../Base/Minimap"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import FeatureInfoBox from "../Popup/FeatureInfoBox"; -import {ImportUtils} from "./ImportUtils"; -import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import Title from "../Base/Title"; -import Loading from "../Base/Loading"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import Combine from "../Base/Combine" +import { FlowStep } from "./FlowStep" +import { BBox } from "../../Logic/BBox" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource" +import MetaTagging from "../../Logic/MetaTagging" +import RelationsTracker from "../../Logic/Osm/RelationsTracker" +import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" +import Minimap from "../Base/Minimap" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import FeatureInfoBox from "../Popup/FeatureInfoBox" +import { ImportUtils } from "./ImportUtils" +import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import Title from "../Base/Title" +import Loading from "../Base/Loading" +import { VariableUiElement } from "../Base/VariableUIElement" import * as known_layers from "../../assets/generated/known_layers.json" -import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"; -import Translations from "../i18n/Translations"; +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" +import Translations from "../i18n/Translations" /** * Filters out points for which the import-note already exists, to prevent duplicates */ -export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> { - +export class CompareToAlreadyExistingNotes + extends Combine + implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }> +{ public IsValid: Store<boolean> - public Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> + public Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }> - - constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) { + constructor(state, params: { bbox: BBox; layer: LayerConfig; features: any[]; theme: string }) { const t = Translations.t.importHelper.compareToAlreadyExistingNotes - const layerConfig = known_layers.layers.filter(l => l.id === params.layer.id)[0] + const layerConfig = known_layers.layers.filter((l) => l.id === params.layer.id)[0] if (layerConfig === undefined) { console.error("WEIRD: layer not found in the builtin layer overview") } - const importLayerJson = new CreateNoteImportLayer(150).convertStrict(<LayerConfigJson>layerConfig, "CompareToAlreadyExistingNotes") + const importLayerJson = new CreateNoteImportLayer(150).convertStrict( + <LayerConfigJson>layerConfig, + "CompareToAlreadyExistingNotes" + ) const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic") const flayer: FilteredLayer = { - appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>()), + appliedFilters: new UIEventSource<Map<string, FilterState>>( + new Map<string, FilterState>() + ), isDisplayed: new UIEventSource<boolean>(true), - layerDef: importLayer + layerDef: importLayer, } const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001)) - allNotesWithinBbox.features.map(f => MetaTagging.addMetatags( + allNotesWithinBbox.features.map((f) => + MetaTagging.addMetatags( f, { memberships: new RelationsTracker(), getFeaturesWithin: () => [], - getFeatureById: () => undefined + getFeatureById: () => undefined, }, importLayer, state, { includeDates: true, // We assume that the non-dated metatags are already set by the cache generator - includeNonDates: true + includeNonDates: true, } ) ) - const alreadyOpenImportNotes = new FilteringFeatureSource(state, undefined, allNotesWithinBbox) + const alreadyOpenImportNotes = new FilteringFeatureSource( + state, + undefined, + allNotesWithinBbox + ) const map = Minimap.createMiniMap() map.SetClass("w-full").SetStyle("height: 500px") const comparisonMap = Minimap.createMiniMap({ location: map.location, - }) comparisonMap.SetClass("w-full").SetStyle("height: 500px") @@ -78,94 +88,109 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ zoomToFeatures: true, leafletMap: map.leafletMap, features: alreadyOpenImportNotes, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state) + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), }) - const maxDistance = new UIEventSource<number>(10) - const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(params, alreadyOpenImportNotes.features - .map(ff => ({features: ff.map(ff => ff.feature)})), maxDistance) - + const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby( + params, + alreadyOpenImportNotes.features.map((ff) => ({ features: ff.map((ff) => ff.feature) })), + maxDistance + ) new ShowDataLayer({ layerToShow: new LayerConfig(import_candidate), state, zoomToFeatures: true, leafletMap: comparisonMap.leafletMap, - features: StaticFeatureSource.fromGeojsonStore(partitionedImportPoints.map(p => p.hasNearby)), - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state) + features: StaticFeatureSource.fromGeojsonStore( + partitionedImportPoints.map((p) => p.hasNearby) + ), + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), }) super([ new Title(t.titleLong), new VariableUiElement( - alreadyOpenImportNotes.features.map(notesWithImport => { - if (allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined) { - const error = allNotesWithinBbox.state.data["error"] - t.loadingFailed.Subs({error}) - } - if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) { - return new Loading(t.loading) - } - if (notesWithImport.length === 0) { - return t.noPreviousNotesFound.SetClass("thanks") - } - return new Combine([ - t.mapExplanation.Subs(params.features), - map, + alreadyOpenImportNotes.features.map( + (notesWithImport) => { + if ( + allNotesWithinBbox.state.data !== undefined && + allNotesWithinBbox.state.data["error"] !== undefined + ) { + const error = allNotesWithinBbox.state.data["error"] + t.loadingFailed.Subs({ error }) + } + if ( + allNotesWithinBbox.features.data === undefined || + allNotesWithinBbox.features.data.length === 0 + ) { + return new Loading(t.loading) + } + if (notesWithImport.length === 0) { + return t.noPreviousNotesFound.SetClass("thanks") + } + return new Combine([ + t.mapExplanation.Subs(params.features), + map, - new VariableUiElement(partitionedImportPoints.map(({noNearby, hasNearby}) => { + new VariableUiElement( + partitionedImportPoints.map(({ noNearby, hasNearby }) => { + if (noNearby.length === 0) { + // Nothing can be imported + return t.completelyImported + .SetClass("alert w-full block") + .SetStyle("padding: 0.5rem") + } - if (noNearby.length === 0) { - // Nothing can be imported - return t.completelyImported.SetClass("alert w-full block").SetStyle("padding: 0.5rem") - } + if (hasNearby.length === 0) { + // All points can be imported + return t.nothingNearby + .SetClass("thanks w-full block") + .SetStyle("padding: 0.5rem") + } - if (hasNearby.length === 0) { - // All points can be imported - return t.nothingNearby.SetClass("thanks w-full block").SetStyle("padding: 0.5rem") - - } - - return new Combine([ - t.someNearby.Subs({ - hasNearby: hasNearby.length, - distance: maxDistance.data - }).SetClass("alert"), - t.wontBeImported, - comparisonMap.SetClass("w-full") - ]).SetClass("w-full") - })) - - - ]).SetClass("flex flex-col") - - }, [allNotesWithinBbox.features, allNotesWithinBbox.state]) + return new Combine([ + t.someNearby + .Subs({ + hasNearby: hasNearby.length, + distance: maxDistance.data, + }) + .SetClass("alert"), + t.wontBeImported, + comparisonMap.SetClass("w-full"), + ]).SetClass("w-full") + }) + ), + ]).SetClass("flex flex-col") + }, + [allNotesWithinBbox.features, allNotesWithinBbox.state] + ) ), - - - ]); + ]) this.SetClass("flex flex-col") - this.Value = partitionedImportPoints.map(({noNearby}) => ({ + this.Value = partitionedImportPoints.map(({ noNearby }) => ({ features: noNearby, bbox: params.bbox, layer: params.layer, - theme: params.theme + theme: params.theme, })) - this.IsValid = alreadyOpenImportNotes.features.map(ff => { - if (allNotesWithinBbox.features.data.length === 0) { - // Not yet loaded - return false - } - if (ff.length == 0) { - // No import notes at all - return true; - } + this.IsValid = alreadyOpenImportNotes.features.map( + (ff) => { + if (allNotesWithinBbox.features.data.length === 0) { + // Not yet loaded + return false + } + if (ff.length == 0) { + // No import notes at all + return true + } - return partitionedImportPoints.data.noNearby.length > 0; // at least _something_ can be imported - }, [partitionedImportPoints, allNotesWithinBbox.features]) + return partitionedImportPoints.data.noNearby.length > 0 // at least _something_ can be imported + }, + [partitionedImportPoints, allNotesWithinBbox.features] + ) } - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/ConfirmProcess.ts b/UI/ImportFlow/ConfirmProcess.ts index 580ef7ed0..b91ab5368 100644 --- a/UI/ImportFlow/ConfirmProcess.ts +++ b/UI/ImportFlow/ConfirmProcess.ts @@ -1,32 +1,35 @@ -import Combine from "../Base/Combine"; -import {FlowStep} from "./FlowStep"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Link from "../Base/Link"; -import CheckBoxes from "../Input/Checkboxes"; -import Title from "../Base/Title"; -import Translations from "../i18n/Translations"; - -export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> { +import Combine from "../Base/Combine" +import { FlowStep } from "./FlowStep" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Link from "../Base/Link" +import CheckBoxes from "../Input/Checkboxes" +import Title from "../Base/Title" +import Translations from "../i18n/Translations" +export class ConfirmProcess + extends Combine + implements FlowStep<{ features: any[]; theme: string }> +{ public IsValid: Store<boolean> - public Value: Store<{ features: any[], theme: string }> + public Value: Store<{ features: any[]; theme: string }> - constructor(v: { features: any[], theme: string }) { - const t = Translations.t.importHelper.confirmProcess; + constructor(v: { features: any[]; theme: string }) { + const t = Translations.t.importHelper.confirmProcess const elements = [ - new Link(t.readImportGuidelines, "https://wiki.openstreetmap.org/wiki/Import_guidelines", true), + new Link( + t.readImportGuidelines, + "https://wiki.openstreetmap.org/wiki/Import_guidelines", + true + ), t.contactedCommunity, t.licenseIsCompatible, - t.wikipageIsMade + t.wikipageIsMade, ] - const toConfirm = new CheckBoxes(elements); + const toConfirm = new CheckBoxes(elements) - super([ - new Title(t.titleLong), - toConfirm, - ]); + super([new Title(t.titleLong), toConfirm]) this.SetClass("link-underline") - this.IsValid = toConfirm.GetValue().map(selected => elements.length == selected.length) - this.Value = new UIEventSource<{ features: any[], theme: string }>(v) + this.IsValid = toConfirm.GetValue().map((selected) => elements.length == selected.length) + this.Value = new UIEventSource<{ features: any[]; theme: string }>(v) } -} \ No newline at end of file +} diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts index e64a75342..55ec9c0d9 100644 --- a/UI/ImportFlow/ConflationChecker.ts +++ b/UI/ImportFlow/ConflationChecker.ts @@ -1,120 +1,134 @@ -import {BBox} from "../../Logic/BBox"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import Combine from "../Base/Combine"; -import Title from "../Base/Title"; -import {Overpass} from "../../Logic/Osm/Overpass"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Constants from "../../Models/Constants"; -import RelationsTracker from "../../Logic/Osm/RelationsTracker"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {FlowStep} from "./FlowStep"; -import Loading from "../Base/Loading"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; -import {IdbLocalStorage} from "../../Logic/Web/IdbLocalStorage"; -import Minimap from "../Base/Minimap"; -import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import Loc from "../../Models/Loc"; -import Attribution from "../BigComponents/Attribution"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; +import { BBox } from "../../Logic/BBox" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import Combine from "../Base/Combine" +import Title from "../Base/Title" +import { Overpass } from "../../Logic/Osm/Overpass" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Constants from "../../Models/Constants" +import RelationsTracker from "../../Logic/Osm/RelationsTracker" +import { VariableUiElement } from "../Base/VariableUIElement" +import { FlowStep } from "./FlowStep" +import Loading from "../Base/Loading" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import { Utils } from "../../Utils" +import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage" +import Minimap from "../Base/Minimap" +import BaseLayer from "../../Models/BaseLayer" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import Loc from "../../Models/Loc" +import Attribution from "../BigComponents/Attribution" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import ValidatedTextField from "../Input/ValidatedTextField" +import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" -import {GeoOperations} from "../../Logic/GeoOperations"; -import FeatureInfoBox from "../Popup/FeatureInfoBox"; -import {ImportUtils} from "./ImportUtils"; -import Translations from "../i18n/Translations"; -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; -import {Feature, FeatureCollection} from "@turf/turf"; +import { GeoOperations } from "../../Logic/GeoOperations" +import FeatureInfoBox from "../Popup/FeatureInfoBox" +import { ImportUtils } from "./ImportUtils" +import Translations from "../i18n/Translations" +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import { Feature, FeatureCollection } from "@turf/turf" import * as currentview from "../../assets/layers/current_view/current_view.json" -import {CheckBox} from "../Input/Checkboxes"; -import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; +import { CheckBox } from "../Input/Checkboxes" +import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" /** * Given the data to import, the bbox and the layer, will query overpass for similar items */ -export default class ConflationChecker extends Combine implements FlowStep<{ features: any[], theme: string }> { - +export default class ConflationChecker + extends Combine + implements FlowStep<{ features: any[]; theme: string }> +{ public readonly IsValid - public readonly Value: Store<{ features: any[], theme: string }> - - constructor( - state, - params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) { + public readonly Value: Store<{ features: any[]; theme: string }> + constructor(state, params: { bbox: BBox; layer: LayerConfig; theme: string; features: any[] }) { const t = Translations.t.importHelper.conflationChecker const bbox = params.bbox.padAbsolute(0.0001) - const layer = params.layer; - - const toImport: { features: any[] } = params; - let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle") + const layer = params.layer + const toImport: { features: any[] } = params + let overpassStatus = new UIEventSource< + { error: string } | "running" | "success" | "idle" | "cached" + >("idle") function loadDataFromOverpass() { // Load the data! const url = Constants.defaultOverpassUrls[1] const relationTracker = new RelationsTracker() - const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource<number>(180), relationTracker, true) + const overpass = new Overpass( + params.layer.source.osmTags, + [], + url, + new UIEventSource<number>(180), + relationTracker, + true + ) console.log("Loading from overpass!") overpassStatus.setData("running") overpass.queryGeoJson(bbox).then( ([data, date]) => { - console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date); + console.log( + "Received overpass-data: ", + data.features.length, + "features are loaded at ", + date + ) overpassStatus.setData("success") fromLocalStorage.setData([data, date]) }, (error) => { - overpassStatus.setData({error}) - }) + overpassStatus.setData({ error }) + } + ) } - - const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { - - whenLoaded: (v) => { - if (v !== undefined && v !== null) { - console.log("Loaded from local storage:", v) - overpassStatus.setData("cached") - }else{ - loadDataFromOverpass() - } + const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>( + "importer-overpass-cache-" + layer.id, + { + whenLoaded: (v) => { + if (v !== undefined && v !== null) { + console.log("Loaded from local storage:", v) + overpassStatus.setData("cached") + } else { + loadDataFromOverpass() + } + }, } - }); + ) - const cacheAge = fromLocalStorage.map(d => { - if(d === undefined || d[1] === undefined){ + const cacheAge = fromLocalStorage.map((d) => { + if (d === undefined || d[1] === undefined) { return undefined } const [_, loadedDate] = d - return (new Date().getTime() - loadedDate.getTime()) / 1000; + return (new Date().getTime() - loadedDate.getTime()) / 1000 }) - cacheAge.addCallbackD(timeDiff => { + cacheAge.addCallbackD((timeDiff) => { if (timeDiff < 24 * 60 * 60) { - // Recently cached! + // Recently cached! overpassStatus.setData("cached") - return; + return } else { loadDataFromOverpass() } }) - const geojson: Store<FeatureCollection> = fromLocalStorage.map(d => { + const geojson: Store<FeatureCollection> = fromLocalStorage.map((d) => { if (d === undefined) { return undefined } return d[0] }) - + const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) - const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) + const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 }) const currentBounds = new UIEventSource<BBox>(undefined) const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({ - value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0") + value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0"), }) zoomLevel.SetClass("ml-1 border border-black") const osmLiveData = Minimap.createMiniMap({ @@ -122,26 +136,34 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea location, background, bounds: currentBounds, - attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) + attribution: new Attribution( + location, + state.osmConnection.userDetails, + undefined, + currentBounds + ), }) osmLiveData.SetClass("w-full").SetStyle("height: 500px") - - const geojsonFeatures : Store<Feature[]> = geojson.map(geojson => { - if (geojson?.features === undefined) { - return [] - } - const currentZoom = zoomLevel.GetValue().data - const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom) - if (currentZoom !== undefined && !zoomedEnough) { - return [] - } - const bounds = osmLiveData.bounds.data - if(bounds === undefined){ - return geojson.features; - } - return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds)) - }, [osmLiveData.bounds, zoomLevel.GetValue()]) - + + const geojsonFeatures: Store<Feature[]> = geojson.map( + (geojson) => { + if (geojson?.features === undefined) { + return [] + } + const currentZoom = zoomLevel.GetValue().data + const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom) + if (currentZoom !== undefined && !zoomedEnough) { + return [] + } + const bounds = osmLiveData.bounds.data + if (bounds === undefined) { + return geojson.features + } + return geojson.features.filter((f) => BBox.get(f).overlapsWith(bounds)) + }, + [osmLiveData.bounds, zoomLevel.GetValue()] + ) + const preview = StaticFeatureSource.fromGeojsonStore(geojsonFeatures) new ShowDataLayer({ @@ -150,32 +172,32 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea leafletMap: osmLiveData.leafletMap, popup: undefined, zoomToFeatures: true, - features: StaticFeatureSource.fromGeojson([ - bbox.asGeoJson({}) - ]) + features: StaticFeatureSource.fromGeojson([bbox.asGeoJson({})]), }) new ShowDataMultiLayer({ //layerToShow: layer, - layers: new UIEventSource<FilteredLayer[]>([{ - layerDef: layer, - isDisplayed: new UIEventSource<boolean>(true), - appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) - }]), + layers: new UIEventSource<FilteredLayer[]>([ + { + layerDef: layer, + isDisplayed: new UIEventSource<boolean>(true), + appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined), + }, + ]), state, leafletMap: osmLiveData.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, - features: preview + features: preview, }) new ShowDataLayer({ layerToShow: new LayerConfig(import_candidate), state, leafletMap: osmLiveData.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, - features: StaticFeatureSource.fromGeojson(toImport.features) + features: StaticFeatureSource.fromGeojson(toImport.features), }) const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement() @@ -184,138 +206,172 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea const matchedFeaturesMap = Minimap.createMiniMap({ allowMoving: true, - background + background, }) matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px") - // Featuresource showing OSM-features which are nearby a toImport-feature - const geojsonMapped: Store<Feature[]> = geojson.map(osmData => { - if (osmData?.features === undefined) { - return [] - } - const maxDist = Number(nearbyCutoff.GetValue().data) - return osmData.features.filter(f => - toImport.features.some(imp => - maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))) - }, [nearbyCutoff.GetValue().stabilized(500)]) - const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped); - const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number)); + // Featuresource showing OSM-features which are nearby a toImport-feature + const geojsonMapped: Store<Feature[]> = geojson.map( + (osmData) => { + if (osmData?.features === undefined) { + return [] + } + const maxDist = Number(nearbyCutoff.GetValue().data) + return osmData.features.filter((f) => + toImport.features.some( + (imp) => + maxDist >= + GeoOperations.distanceBetween( + imp.geometry.coordinates, + GeoOperations.centerpointCoordinates(f) + ) + ) + ) + }, + [nearbyCutoff.GetValue().stabilized(500)] + ) + const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped) + const paritionedImport = ImportUtils.partitionFeaturesIfNearby( + toImport, + geojson, + nearbyCutoff.GetValue().map(Number) + ) - // Featuresource showing OSM-features which are nearby a toImport-feature - const toImportWithNearby = StaticFeatureSource.fromGeojsonStore(paritionedImport.map(els => els?.hasNearby ?? [])); - toImportWithNearby.features.addCallback(nearby => console.log("The following features are near an already existing object:", nearby)) + // Featuresource showing OSM-features which are nearby a toImport-feature + const toImportWithNearby = StaticFeatureSource.fromGeojsonStore( + paritionedImport.map((els) => els?.hasNearby ?? []) + ) + toImportWithNearby.features.addCallback((nearby) => + console.log("The following features are near an already existing object:", nearby) + ) new ShowDataLayer({ layerToShow: new LayerConfig(import_candidate), state, leafletMap: matchedFeaturesMap.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: false, - features: toImportWithNearby + features: toImportWithNearby, }) const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true) new ShowDataLayer({ layerToShow: layer, state, leafletMap: matchedFeaturesMap.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), zoomToFeatures: true, features: nearbyFeatures, - doShowLayer: showOsmLayer.GetValue() + doShowLayer: showOsmLayer.GetValue(), }) - - - - const conflationMaps = new Combine([ new VariableUiElement( - geojson.map(geojson => { + geojson.map((geojson) => { if (geojson === undefined) { - return undefined; + return undefined } - return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(() => { - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", { - mimetype: "application/json+geo" - }) - }); - })), - new VariableUiElement(cacheAge.map(age => { - if (age === undefined) { - return undefined; - } - if (age < 0) { - return t.cacheExpired - } - return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}), - new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) - .onClick(loadDataFromOverpass) - .SetClass("h-12") - ]) - })), + return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick( + () => { + Utils.offerContentsAsDownloadableFile( + JSON.stringify(geojson, null, " "), + "mapcomplete-" + layer.id + ".geojson", + { + mimetype: "application/json+geo", + } + ) + } + ) + }) + ), + new VariableUiElement( + cacheAge.map((age) => { + if (age === undefined) { + return undefined + } + if (age < 0) { + return t.cacheExpired + } + return new Combine([ + t.loadedDataAge.Subs({ age: Utils.toHumanTime(age) }), + new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) + .onClick(loadDataFromOverpass) + .SetClass("h-12"), + ]) + }) + ), new Title(t.titleLive), - t.importCandidatesCount.Subs({count: toImport.features.length}), - new VariableUiElement(geojson.map(geojson => { - if (geojson?.features?.length === undefined || geojson?.features?.length === 0) { - return t.nothingLoaded.Subs(layer).SetClass("alert") - } - return new Combine([ - t.osmLoaded.Subs({count: geojson.features.length, name: layer.name}), - - ]) - })), + t.importCandidatesCount.Subs({ count: toImport.features.length }), + new VariableUiElement( + geojson.map((geojson) => { + if ( + geojson?.features?.length === undefined || + geojson?.features?.length === 0 + ) { + return t.nothingLoaded.Subs(layer).SetClass("alert") + } + return new Combine([ + t.osmLoaded.Subs({ count: geojson.features.length, name: layer.name }), + ]) + }) + ), osmLiveData, new Combine([ t.zoomLevelSelection, zoomLevel, - new VariableUiElement(osmLiveData.location.map(location => { - return t.zoomIn.Subs(<any>{current: location.zoom}) - })), + new VariableUiElement( + osmLiveData.location.map((location) => { + return t.zoomIn.Subs(<any>{ current: location.zoom }) + }) + ), ]).SetClass("flex"), new Title(t.titleNearby), new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"), - new VariableUiElement(toImportWithNearby.features.map(feats => - t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))), + new VariableUiElement( + toImportWithNearby.features.map((feats) => + t.nearbyWarn.Subs({ count: feats.length }).SetClass("alert") + ) + ), t.setRangeToZero, matchedFeaturesMap, new Combine([ - new BackgroundMapSwitch({backgroundLayer: background, locationControl: matchedFeaturesMap.location}, background), - showOsmLayer, - - ]).SetClass("flex") - + new BackgroundMapSwitch( + { backgroundLayer: background, locationControl: matchedFeaturesMap.location }, + background + ), + showOsmLayer, + ]).SetClass("flex"), ]).SetClass("flex flex-col") super([ new Title(t.title), - new VariableUiElement(overpassStatus.map(d => { - if (d === "idle") { - return new Loading(t.states.idle) - } - if (d === "running") { - return new Loading(t.states.running) - } - if (d["error"] !== undefined) { - return t.states.error.Subs({error: d["error"]}).SetClass("alert") - } - - if (d === "cached") { - return conflationMaps - } - if (d === "success") { - return conflationMaps - } - return t.states.unexpected.Subs({state: d}).SetClass("alert") - })) + new VariableUiElement( + overpassStatus.map((d) => { + if (d === "idle") { + return new Loading(t.states.idle) + } + if (d === "running") { + return new Loading(t.states.running) + } + if (d["error"] !== undefined) { + return t.states.error.Subs({ error: d["error"] }).SetClass("alert") + } + if (d === "cached") { + return conflationMaps + } + if (d === "success") { + return conflationMaps + } + return t.states.unexpected.Subs({ state: d }).SetClass("alert") + }) + ), ]) - this.Value = paritionedImport.map(feats => ({ + this.Value = paritionedImport.map((feats) => ({ theme: params.theme, features: feats?.noNearby, - layer: params.layer + layer: params.layer, })) - this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0) + this.IsValid = this.Value.map((v) => v?.features !== undefined && v.features.length > 0) } - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/CreateNotes.ts b/UI/ImportFlow/CreateNotes.ts index 249be836e..81713c713 100644 --- a/UI/ImportFlow/CreateNotes.ts +++ b/UI/ImportFlow/CreateNotes.ts @@ -1,22 +1,21 @@ -import Combine from "../Base/Combine"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Title from "../Base/Title"; -import Toggle from "../Input/Toggle"; -import Loading from "../Base/Loading"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import {Translation} from "../i18n/Translation"; +import Combine from "../Base/Combine" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { UIEventSource } from "../../Logic/UIEventSource" +import Title from "../Base/Title" +import Toggle from "../Input/Toggle" +import Loading from "../Base/Loading" +import { VariableUiElement } from "../Base/VariableUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" export class CreateNotes extends Combine { - - - public static createNoteContentsUi(feature: {properties: any, geometry: {coordinates: [number,number]}}, - options: {wikilink: string; intro: string; source: string, theme: string } - ): (Translation | string)[]{ + public static createNoteContentsUi( + feature: { properties: any; geometry: { coordinates: [number, number] } }, + options: { wikilink: string; intro: string; source: string; theme: string } + ): (Translation | string)[] { const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source delete feature.properties["source"] delete feature.properties["src"] @@ -26,7 +25,7 @@ export class CreateNotes extends Combine { delete feature.properties["note"] } - const tags: string [] = [] + const tags: string[] = [] for (const key in feature.properties) { if (feature.properties[key] === null || feature.properties[key] === undefined) { console.warn("Null or undefined key for ", feature.properties) @@ -35,7 +34,14 @@ export class CreateNotes extends Combine { if (feature.properties[key] === "") { continue } - tags.push(key + "=" + (feature.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n")) + tags.push( + key + + "=" + + (feature.properties[key] + "") + .replace(/=/, "\\=") + .replace(/;/g, "\\;") + .replace(/\n/g, "\\n") + ) } const lat = feature.geometry.coordinates[1] const lon = feature.geometry.coordinates[0] @@ -43,82 +49,88 @@ export class CreateNotes extends Combine { return [ options.intro, extraNote, - note.datasource.Subs({source: src}), + note.datasource.Subs({ source: src }), note.wikilink.Subs(options), - '', + "", note.importEasily, `https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`, - ...tags] + ...tags, + ] } - public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}}, - options: {wikilink: string; intro: string; source: string, theme: string } - ): string[]{ - return CreateNotes.createNoteContentsUi(feature, options).map(trOrStr => { - if(typeof trOrStr === "string"){ + public static createNoteContents( + feature: { properties: any; geometry: { coordinates: [number, number] } }, + options: { wikilink: string; intro: string; source: string; theme: string } + ): string[] { + return CreateNotes.createNoteContentsUi(feature, options).map((trOrStr) => { + if (typeof trOrStr === "string") { return trOrStr } return trOrStr.txt }) } - - constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) { - const t = Translations.t.importHelper.createNotes; + constructor( + state: { osmConnection: OsmConnection }, + v: { features: any[]; wikilink: string; intro: string; source: string; theme: string } + ) { + const t = Translations.t.importHelper.createNotes const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([]) const failed = new UIEventSource<string[]>([]) - const currentNote = createdNotes.map(n => n.length) + const currentNote = createdNotes.map((n) => n.length) for (const f of v.features) { - const lat = f.geometry.coordinates[1] const lon = f.geometry.coordinates[0] const text = CreateNotes.createNoteContents(f, v).join("\n") - state.osmConnection.openNote( - lat, lon, text) - .then(({id}) => { + state.osmConnection.openNote(lat, lon, text).then( + ({ id }) => { createdNotes.data.push(id) createdNotes.ping() - }, err => { + }, + (err) => { failed.data.push(err) failed.ping() - }) + } + ) } super([ new Title(t.title), - t.loading , + t.loading, new Toggle( - new Loading(new VariableUiElement(currentNote.map(count => t.creating.Subs({ - count, total: v.features.length - } - - )))), + new Loading( + new VariableUiElement( + currentNote.map((count) => + t.creating.Subs({ + count, + total: v.features.length, + }) + ) + ) + ), new Combine([ Svg.party_svg().SetClass("w-24"), - t.done.Subs({count: v.features.length}).SetClass("thanks"), - new SubtleButton(Svg.note_svg(), - t.openImportViewer , { - url: "import_viewer.html" - }) - ] - ), - currentNote.map(count => count < v.features.length) + t.done.Subs({ count: v.features.length }).SetClass("thanks"), + new SubtleButton(Svg.note_svg(), t.openImportViewer, { + url: "import_viewer.html", + }), + ]), + currentNote.map((count) => count < v.features.length) + ), + new VariableUiElement( + failed.map((failed) => { + if (failed.length === 0) { + return undefined + } + return new Combine([ + new FixedUiElement("Some entries failed").SetClass("alert"), + ...failed, + ]).SetClass("flex flex-col") + }) ), - new VariableUiElement(failed.map(failed => { - - if (failed.length === 0) { - return undefined - } - return new Combine([ - new FixedUiElement("Some entries failed").SetClass("alert"), - ...failed - ]).SetClass("flex flex-col") - - })) ]) - this.SetClass("flex flex-col"); + this.SetClass("flex flex-col") } - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/FlowStep.ts b/UI/ImportFlow/FlowStep.ts index fc9b4b398..b99b2829e 100644 --- a/UI/ImportFlow/FlowStep.ts +++ b/UI/ImportFlow/FlowStep.ts @@ -1,13 +1,13 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import BaseUIElement from "../BaseUIElement"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Toggle from "../Input/Toggle"; -import {UIElement} from "../UIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import BaseUIElement from "../BaseUIElement" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Translations from "../i18n/Translations" +import { VariableUiElement } from "../Base/VariableUIElement" +import Toggle from "../Input/Toggle" +import { UIElement } from "../UIElement" +import { FixedUiElement } from "../Base/FixedUiElement" export interface FlowStep<T> extends BaseUIElement { readonly IsValid: Store<boolean> @@ -15,21 +15,31 @@ export interface FlowStep<T> extends BaseUIElement { } export class FlowPanelFactory<T> { - private _initial: FlowStep<any>; - private _steps: ((x: any) => FlowStep<any>)[]; - private _stepNames: (string | BaseUIElement)[]; + private _initial: FlowStep<any> + private _steps: ((x: any) => FlowStep<any>)[] + private _stepNames: (string | BaseUIElement)[] - private constructor(initial: FlowStep<any>, steps: ((x: any) => FlowStep<any>)[], stepNames: (string | BaseUIElement)[]) { - this._initial = initial; - this._steps = steps; - this._stepNames = stepNames; + private constructor( + initial: FlowStep<any>, + steps: ((x: any) => FlowStep<any>)[], + stepNames: (string | BaseUIElement)[] + ) { + this._initial = initial + this._steps = steps + this._stepNames = stepNames } - public static start<TOut>(name:{title: BaseUIElement}, step: FlowStep<TOut>): FlowPanelFactory<TOut> { + public static start<TOut>( + name: { title: BaseUIElement }, + step: FlowStep<TOut> + ): FlowPanelFactory<TOut> { return new FlowPanelFactory(step, [], [name.title]) } - public then<TOut>(name: string | {title: BaseUIElement}, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> { + public then<TOut>( + name: string | { title: BaseUIElement }, + construct: (t: T) => FlowStep<TOut> + ): FlowPanelFactory<TOut> { return new FlowPanelFactory<TOut>( this._initial, this._steps.concat([construct]), @@ -37,25 +47,30 @@ export class FlowPanelFactory<T> { ) } - public finish(name: string | BaseUIElement, construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)): { - flow: BaseUIElement, - furthestStep: UIEventSource<number>, + public finish( + name: string | BaseUIElement, + construct: (t: T, backButton?: BaseUIElement) => BaseUIElement + ): { + flow: BaseUIElement + furthestStep: UIEventSource<number> titles: (string | BaseUIElement)[] } { const furthestStep = new UIEventSource(0) // Construct all the flowpanels step by step (in reverse order) - const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined) + const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map( + (_) => undefined + ) nextConstr.push(construct) for (let i = this._steps.length - 1; i >= 0; i--) { - const createFlowStep: (value) => FlowStep<any> = this._steps[i]; - const isConfirm = i == this._steps.length - 1; + const createFlowStep: (value) => FlowStep<any> = this._steps[i] + const isConfirm = i == this._steps.length - 1 nextConstr[i] = (value, backButton) => { const flowStep = createFlowStep(value) - furthestStep.setData(i + 1); - const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm); - panel.isActive.addCallbackAndRun(active => { + furthestStep.setData(i + 1) + const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm) + panel.isActive.addCallbackAndRun((active) => { if (active) { - furthestStep.setData(i + 1); + furthestStep.setData(i + 1) } }) return panel @@ -63,32 +78,31 @@ export class FlowPanelFactory<T> { } const flow = new FlowPanel(this._initial, nextConstr[0]) - flow.isActive.addCallbackAndRun(active => { + flow.isActive.addCallbackAndRun((active) => { if (active) { - furthestStep.setData(0); + furthestStep.setData(0) } }) return { flow, furthestStep, - titles: this._stepNames + titles: this._stepNames, } } - } export class FlowPanel<T> extends Toggle { public isActive: UIEventSource<boolean> constructor( - initial: (FlowStep<T>), - constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), + initial: FlowStep<T>, + constructNextstep: (input: T, backButton: BaseUIElement) => BaseUIElement, backbutton?: BaseUIElement, isConfirm = false ) { - const t = Translations.t.general; + const t = Translations.t.general - const currentStepActive = new UIEventSource(true); + const currentStepActive = new UIEventSource(true) let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined) const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => { @@ -106,13 +120,13 @@ export class FlowPanel<T> extends Toggle { backbutton, new Toggle( new SubtleButton( - isConfirm ? Svg.checkmark_svg() : - Svg.back_svg().SetStyle("transform: rotate(180deg);"), + isConfirm + ? Svg.checkmark_svg() + : Svg.back_svg().SetStyle("transform: rotate(180deg);"), isConfirm ? t.confirm : t.next ).onClick(() => { try { - - const v = initial.Value.data; + const v = initial.Value.data nextStep.setData(constructNextstep(v, backButtonForNextStep)) currentStepActive.setData(false) } catch (e) { @@ -123,24 +137,16 @@ export class FlowPanel<T> extends Toggle { new SubtleButton(Svg.invalid_svg(), t.notValid), initial.IsValid ), - new Toggle( - t.error.SetClass("alert"), - undefined, - isError), + new Toggle(t.error.SetClass("alert"), undefined, isError), ]).SetClass("flex w-full justify-end space-x-2"), - - ] } - super( new Combine(elements).SetClass("h-full flex flex-col justify-between"), new VariableUiElement(nextStep), currentStepActive - ); + ) this.isActive = currentStepActive } - - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/ImportHelperGui.ts b/UI/ImportFlow/ImportHelperGui.ts index 657b5a755..62f1ea064 100644 --- a/UI/ImportFlow/ImportHelperGui.ts +++ b/UI/ImportFlow/ImportHelperGui.ts @@ -1,77 +1,84 @@ -import Combine from "../Base/Combine"; -import Toggle from "../Input/Toggle"; -import LanguagePicker from "../LanguagePicker"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import BaseUIElement from "../BaseUIElement"; -import MinimapImplementation from "../Base/MinimapImplementation"; -import Translations from "../i18n/Translations"; -import {FlowPanelFactory} from "./FlowStep"; -import {RequestFile} from "./RequestFile"; -import {PreviewAttributesPanel} from "./PreviewPanel"; -import ConflationChecker from "./ConflationChecker"; -import {AskMetadata} from "./AskMetadata"; -import {ConfirmProcess} from "./ConfirmProcess"; -import {CreateNotes} from "./CreateNotes"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import List from "../Base/List"; -import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes"; -import Introdution from "./Introdution"; -import LoginToImport from "./LoginToImport"; -import {MapPreview} from "./MapPreview"; -import LeftIndex from "../Base/LeftIndex"; -import {SubtleButton} from "../Base/SubtleButton"; -import SelectTheme from "./SelectTheme"; +import Combine from "../Base/Combine" +import Toggle from "../Input/Toggle" +import LanguagePicker from "../LanguagePicker" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import BaseUIElement from "../BaseUIElement" +import MinimapImplementation from "../Base/MinimapImplementation" +import Translations from "../i18n/Translations" +import { FlowPanelFactory } from "./FlowStep" +import { RequestFile } from "./RequestFile" +import { PreviewAttributesPanel } from "./PreviewPanel" +import ConflationChecker from "./ConflationChecker" +import { AskMetadata } from "./AskMetadata" +import { ConfirmProcess } from "./ConfirmProcess" +import { CreateNotes } from "./CreateNotes" +import { VariableUiElement } from "../Base/VariableUIElement" +import List from "../Base/List" +import { CompareToAlreadyExistingNotes } from "./CompareToAlreadyExistingNotes" +import Introdution from "./Introdution" +import LoginToImport from "./LoginToImport" +import { MapPreview } from "./MapPreview" +import LeftIndex from "../Base/LeftIndex" +import { SubtleButton } from "../Base/SubtleButton" +import SelectTheme from "./SelectTheme" export default class ImportHelperGui extends LeftIndex { constructor() { const state = new UserRelatedState(undefined) - const t = Translations.t.importHelper; - const {flow, furthestStep, titles} = - FlowPanelFactory - .start(t.introduction, new Introdution()) - .then(t.login, _ => new LoginToImport(state)) - .then(t.selectFile, _ => new RequestFile()) - .then(t.previewAttributes, geojson => new PreviewAttributesPanel(state, geojson)) - .then(t.mapPreview, geojson => new MapPreview(state, geojson)) - .then(t.selectTheme, v => new SelectTheme(v)) - .then(t.compareToAlreadyExistingNotes, v => new CompareToAlreadyExistingNotes(state, v)) - .then(t.conflationChecker, v => new ConflationChecker(state, v)) - .then(t.confirmProcess, v => new ConfirmProcess(v)) - .then(t.askMetadata, (v) => new AskMetadata(v)) - .finish(t.createNotes.title, v => new CreateNotes(state, v)); + const t = Translations.t.importHelper + const { flow, furthestStep, titles } = FlowPanelFactory.start( + t.introduction, + new Introdution() + ) + .then(t.login, (_) => new LoginToImport(state)) + .then(t.selectFile, (_) => new RequestFile()) + .then(t.previewAttributes, (geojson) => new PreviewAttributesPanel(state, geojson)) + .then(t.mapPreview, (geojson) => new MapPreview(state, geojson)) + .then(t.selectTheme, (v) => new SelectTheme(v)) + .then( + t.compareToAlreadyExistingNotes, + (v) => new CompareToAlreadyExistingNotes(state, v) + ) + .then(t.conflationChecker, (v) => new ConflationChecker(state, v)) + .then(t.confirmProcess, (v) => new ConfirmProcess(v)) + .then(t.askMetadata, (v) => new AskMetadata(v)) + .finish(t.createNotes.title, (v) => new CreateNotes(state, v)) const toc = new List( - titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => { - if (i > currentStep) { - return new Combine([title]).SetClass("subtle"); - } - if (i == currentStep) { - return new Combine([title]).SetClass("font-bold"); - } - if (i < currentStep) { - return title - } - - - }))) - , true) + titles.map( + (title, i) => + new VariableUiElement( + furthestStep.map((currentStep) => { + if (i > currentStep) { + return new Combine([title]).SetClass("subtle") + } + if (i == currentStep) { + return new Combine([title]).SetClass("font-bold") + } + if (i < currentStep) { + return title + } + }) + ) + ), + true + ) const leftContents: BaseUIElement[] = [ new SubtleButton(undefined, t.gotoImportViewer, { - url: "import_viewer.html" + url: "import_viewer.html", }), toc, new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting), - new LanguagePicker(Translations.t.importHelper.title.SupportedLanguages(), "")?.SetClass("mt-4 self-end flex-col"), - ].map(el => el?.SetClass("pl-4")) - - super( - leftContents, - flow) + new LanguagePicker( + Translations.t.importHelper.title.SupportedLanguages(), + "" + )?.SetClass("mt-4 self-end flex-col"), + ].map((el) => el?.SetClass("pl-4")) + super(leftContents, flow) } - } MinimapImplementation.initialize() -new ImportHelperGui().AttachTo("main") \ No newline at end of file +new ImportHelperGui().AttachTo("main") diff --git a/UI/ImportFlow/ImportUtils.ts b/UI/ImportFlow/ImportUtils.ts index 850cbacfc..1622672be 100644 --- a/UI/ImportFlow/ImportUtils.ts +++ b/UI/ImportFlow/ImportUtils.ts @@ -1,35 +1,44 @@ -import {Store} from "../../Logic/UIEventSource"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {Feature, Geometry} from "@turf/turf"; +import { Store } from "../../Logic/UIEventSource" +import { GeoOperations } from "../../Logic/GeoOperations" +import { Feature, Geometry } from "@turf/turf" export class ImportUtils { public static partitionFeaturesIfNearby( - toPartitionFeatureCollection: ({ features: Feature<Geometry>[] }), + toPartitionFeatureCollection: { features: Feature<Geometry>[] }, compareWith: Store<{ features: Feature[] }>, - cutoffDistanceInMeters: Store<number>) - : Store<{ hasNearby: Feature[], noNearby: Feature[] }> { - return compareWith.map(osmData => { - if (osmData?.features === undefined) { - return undefined - } - if (osmData.features.length === 0) { - return {noNearby: toPartitionFeatureCollection.features, hasNearby: []} - } - const maxDist = cutoffDistanceInMeters.data - - const hasNearby = [] - const noNearby = [] - for (const toImportElement of toPartitionFeatureCollection.features) { - const hasNearbyFeature = osmData.features.some(f => - maxDist >= GeoOperations.distanceBetween(<any> toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) - if (hasNearbyFeature) { - hasNearby.push(toImportElement) - } else { - noNearby.push(toImportElement) + cutoffDistanceInMeters: Store<number> + ): Store<{ hasNearby: Feature[]; noNearby: Feature[] }> { + return compareWith.map( + (osmData) => { + if (osmData?.features === undefined) { + return undefined } - } + if (osmData.features.length === 0) { + return { noNearby: toPartitionFeatureCollection.features, hasNearby: [] } + } + const maxDist = cutoffDistanceInMeters.data - return {hasNearby, noNearby} - }, [cutoffDistanceInMeters]); + const hasNearby = [] + const noNearby = [] + for (const toImportElement of toPartitionFeatureCollection.features) { + const hasNearbyFeature = osmData.features.some( + (f) => + maxDist >= + GeoOperations.distanceBetween( + <any>toImportElement.geometry.coordinates, + GeoOperations.centerpointCoordinates(f) + ) + ) + if (hasNearbyFeature) { + hasNearby.push(toImportElement) + } else { + noNearby.push(toImportElement) + } + } + + return { hasNearby, noNearby } + }, + [cutoffDistanceInMeters] + ) } -} \ No newline at end of file +} diff --git a/UI/ImportFlow/ImportViewerGui.ts b/UI/ImportFlow/ImportViewerGui.ts index 7dc990c26..e27f25e5b 100644 --- a/UI/ImportFlow/ImportViewerGui.ts +++ b/UI/ImportFlow/ImportViewerGui.ts @@ -1,56 +1,62 @@ -import Combine from "../Base/Combine"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Utils} from "../../Utils"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Title from "../Base/Title"; -import Translations from "../i18n/Translations"; -import Loading from "../Base/Loading"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Link from "../Base/Link"; -import {DropDown} from "../Input/DropDown"; -import BaseUIElement from "../BaseUIElement"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Toggle, {ClickableToggle} from "../Input/Toggle"; -import Table from "../Base/Table"; -import LeftIndex from "../Base/LeftIndex"; -import Toggleable, {Accordeon} from "../Base/Toggleable"; -import TableOfContents from "../Base/TableOfContents"; -import {LoginToggle} from "../Popup/LoginButton"; -import {QueryParameters} from "../../Logic/Web/QueryParameters"; -import Lazy from "../Base/Lazy"; -import {Button} from "../Base/Button"; +import Combine from "../Base/Combine" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Utils } from "../../Utils" +import { UIEventSource } from "../../Logic/UIEventSource" +import Title from "../Base/Title" +import Translations from "../i18n/Translations" +import Loading from "../Base/Loading" +import { FixedUiElement } from "../Base/FixedUiElement" +import Link from "../Base/Link" +import { DropDown } from "../Input/DropDown" +import BaseUIElement from "../BaseUIElement" +import ValidatedTextField from "../Input/ValidatedTextField" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import Toggle, { ClickableToggle } from "../Input/Toggle" +import Table from "../Base/Table" +import LeftIndex from "../Base/LeftIndex" +import Toggleable, { Accordeon } from "../Base/Toggleable" +import TableOfContents from "../Base/TableOfContents" +import { LoginToggle } from "../Popup/LoginButton" +import { QueryParameters } from "../../Logic/Web/QueryParameters" +import Lazy from "../Base/Lazy" +import { Button } from "../Base/Button" interface NoteProperties { - "id": number, - "url": string, - "date_created": string, - closed_at?: string, - "status": "open" | "closed", - "comments": { - date: string, - uid: number, - user: string, - text: string, + id: number + url: string + date_created: string + closed_at?: string + status: "open" | "closed" + comments: { + date: string + uid: number + user: string + text: string html: string }[] } interface NoteState { - props: NoteProperties, - theme: string, - intro: string, - dateStr: string, - status: "imported" | "already_mapped" | "invalid" | "closed" | "not_found" | "open" | "has_comments" + props: NoteProperties + theme: string + intro: string + dateStr: string + status: + | "imported" + | "already_mapped" + | "invalid" + | "closed" + | "not_found" + | "open" + | "has_comments" } class DownloadStatisticsButton extends SubtleButton { constructor(states: NoteState[][]) { - super(Svg.statistics_svg(), "Download statistics"); + super(Svg.statistics_svg(), "Download statistics") this.onClick(() => { - const st: NoteState[] = [].concat(...states) const fields = [ @@ -61,26 +67,27 @@ class DownloadStatisticsButton extends SubtleButton { "date_closed", "days_open", "intro", - "...comments" + "...comments", ] - const values: string[][] = st.map(note => { - - - return [note.props.id + "", + const values: string[][] = st.map((note) => { + return [ + note.props.id + "", note.status, note.theme, note.props.date_created?.substr(0, note.props.date_created.length - 3), note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "", JSON.stringify(note.intro), - ...note.props.comments.map(c => JSON.stringify(c.user) + ": " + JSON.stringify(c.text)) + ...note.props.comments.map( + (c) => JSON.stringify(c.user) + ": " + JSON.stringify(c.text) + ), ] }) Utils.offerContentsAsDownloadableFile( - [fields, ...values].map(c => c.join(", ")).join("\n"), + [fields, ...values].map((c) => c.join(", ")).join("\n"), "mapcomplete_import_notes_overview.csv", { - mimetype: "text/csv" + mimetype: "text/csv", } ) }) @@ -92,32 +99,32 @@ class MassAction extends Combine { const textField = ValidatedTextField.ForType("text").ConstructInputElement() const actions = new DropDown<{ - predicate: (p: NoteProperties) => boolean, + predicate: (p: NoteProperties) => boolean action: (p: NoteProperties) => Promise<void> }>("On which notes should an action be performed?", [ { value: undefined, - shown: <string | BaseUIElement>"Pick an option..." + shown: <string | BaseUIElement>"Pick an option...", }, { value: { - predicate: p => p.status === "open", - action: async p => { + predicate: (p) => p.status === "open", + action: async (p) => { const txt = textField.GetValue().data state.osmConnection.closeNote(p.id, txt) - } + }, }, - shown: "Add comment to every open note and close all notes" + shown: "Add comment to every open note and close all notes", }, { value: { - predicate: p => p.status === "open", - action: async p => { + predicate: (p) => p.status === "open", + action: async (p) => { const txt = textField.GetValue().data state.osmConnection.addCommentToNote(p.id, txt) - } + }, }, - shown: "Add comment to every open note" + shown: "Add comment to every open note", }, /* { @@ -131,25 +138,22 @@ class MassAction extends Combine { }, shown:"On every open note, read the 'note='-tag and and this note as comment. (This action ignores the textfield)" },//*/ - ]) const handledNotesCounter = new UIEventSource<number>(undefined) - const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action") - .onClick(async () => { - const {predicate, action} = actions.GetValue().data - for (let i = 0; i < props.length; i++) { - handledNotesCounter.setData(i) - const prop = props[i] - if (!predicate(prop)) { - continue - } - await action(prop) + const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action").onClick(async () => { + const { predicate, action } = actions.GetValue().data + for (let i = 0; i < props.length; i++) { + handledNotesCounter.setData(i) + const prop = props[i] + if (!predicate(prop)) { + continue } - handledNotesCounter.setData(props.length) - }) + await action(prop) + } + handledNotesCounter.setData(props.length) + }) super([ - actions, textField.SetClass("w-full border border-black"), new Toggle( @@ -157,37 +161,57 @@ class MassAction extends Combine { apply, new Toggle( - new Loading(new VariableUiElement(handledNotesCounter.map(state => { - if (state === props.length) { - return "All done!" - } - return "Handling note " + (state + 1) + " out of " + props.length; - }))), - new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass("thanks flex p-4"), - handledNotesCounter.map(s => s < props.length) + new Loading( + new VariableUiElement( + handledNotesCounter.map((state) => { + if (state === props.length) { + return "All done!" + } + return ( + "Handling note " + (state + 1) + " out of " + props.length + ) + }) + ) + ), + new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass( + "thanks flex p-4" + ), + handledNotesCounter.map((s) => s < props.length) ), - handledNotesCounter.map(s => s === undefined) - ) + handledNotesCounter.map((s) => s === undefined) + ), - , new VariableUiElement(textField.GetValue().map(txt => "Type a text of at least 15 characters to apply the action. Currently, there are " + (txt?.length ?? 0) + " characters")).SetClass("alert"), - actions.GetValue().map(v => v !== undefined && textField.GetValue()?.data?.length > 15, [textField.GetValue()]) + new VariableUiElement( + textField + .GetValue() + .map( + (txt) => + "Type a text of at least 15 characters to apply the action. Currently, there are " + + (txt?.length ?? 0) + + " characters" + ) + ).SetClass("alert"), + actions + .GetValue() + .map( + (v) => v !== undefined && textField.GetValue()?.data?.length > 15, + [textField.GetValue()] + ) ), new Toggle( - new FixedUiElement("Testmode enable").SetClass("alert"), undefined, + new FixedUiElement("Testmode enable").SetClass("alert"), + undefined, state.featureSwitchIsTesting - ) - ]); + ), + ]) } - } - class NoteTable extends Combine { - private static individualActions: [() => BaseUIElement, string][] = [ [Svg.not_found_svg, "This feature does not exist"], [Svg.addSmall_svg, "imported"], - [Svg.duplicate_svg, "Already mapped"] + [Svg.duplicate_svg, "Already mapped"], ] constructor(noteStates: NoteState[], state?: UserRelatedState) { @@ -195,18 +219,21 @@ class NoteTable extends Combine { const table = new Table( ["id", "status", "last comment", "last modified by", "actions"], - noteStates.map(ns => NoteTable.noteField(ns, state)), - {sortable: true} - ).SetClass("zebra-table link-underline"); - + noteStates.map((ns) => NoteTable.noteField(ns, state)), + { sortable: true } + ).SetClass("zebra-table link-underline") super([ new Title("Mass apply an action on " + noteStates.length + " notes below"), - state !== undefined ? new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block") : undefined, + state !== undefined + ? new MassAction( + state, + noteStates.map((ns) => ns.props) + ).SetClass("block") + : undefined, table, new Title("Example note", 4), new FixedUiElement(typicalComment).SetClass("literal-code link-underline"), - ]) this.SetClass("flex flex-col") } @@ -214,9 +241,10 @@ class NoteTable extends Combine { private static noteField(ns: NoteState, state: UserRelatedState) { const link = new Link( "" + ns.props.id, - "https://openstreetmap.org/note/" + ns.props.id, true + "https://openstreetmap.org/note/" + ns.props.id, + true ) - let last_comment = ""; + let last_comment = "" const last_comment_props = ns.props.comments[ns.props.comments.length - 1] const before_last_comment = ns.props.comments[ns.props.comments.length - 2] if (ns.props.comments.length > 1) { @@ -226,41 +254,56 @@ class NoteTable extends Combine { } } const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0") - const togglestate = new UIEventSource(false); - const changed = new UIEventSource<string>(undefined); - - const lazyButtons = new Lazy(( ) => new Combine( - this.individualActions.map(([img, text]) => - img().onClick(async () => { - if (ns.props.status === "closed") { - await state.osmConnection.reopenNote(ns.props.id) - } - await state.osmConnection.closeNote(ns.props.id, text) - changed.setData(text) - }).SetClass("h-8 w-8")) - ).SetClass("flex")); - - const appliedButtons = new VariableUiElement(changed.map(currentState => currentState === undefined ? lazyButtons : currentState)); - - const buttons = Toggle.If(state?.osmConnection?.isLoggedIn, - () => new ClickableToggle( - appliedButtons, - new Button("edit...", () => { - console.log("Enabling...") - togglestate.setData(true); - }), - togglestate - )); - return [link, new Combine([statusIcon, ns.status]).SetClass("flex"), last_comment, - new Link(last_comment_props.user, "https://www.openstreetmap.org/user/" + last_comment_props.user, true), - buttons + const togglestate = new UIEventSource(false) + const changed = new UIEventSource<string>(undefined) + + const lazyButtons = new Lazy(() => + new Combine( + this.individualActions.map(([img, text]) => + img() + .onClick(async () => { + if (ns.props.status === "closed") { + await state.osmConnection.reopenNote(ns.props.id) + } + await state.osmConnection.closeNote(ns.props.id, text) + changed.setData(text) + }) + .SetClass("h-8 w-8") + ) + ).SetClass("flex") + ) + + const appliedButtons = new VariableUiElement( + changed.map((currentState) => (currentState === undefined ? lazyButtons : currentState)) + ) + + const buttons = Toggle.If( + state?.osmConnection?.isLoggedIn, + () => + new ClickableToggle( + appliedButtons, + new Button("edit...", () => { + console.log("Enabling...") + togglestate.setData(true) + }), + togglestate + ) + ) + return [ + link, + new Combine([statusIcon, ns.status]).SetClass("flex"), + last_comment, + new Link( + last_comment_props.user, + "https://www.openstreetmap.org/user/" + last_comment_props.user, + true + ), + buttons, ] } - } class BatchView extends Toggleable { - public static icons = { open: Svg.compass_svg, has_comments: Svg.speech_bubble_svg, @@ -272,10 +315,9 @@ class BatchView extends Toggleable { } constructor(noteStates: NoteState[], state?: UserRelatedState) { - noteStates.sort((a, b) => a.props.id - b.props.id) - const {theme, intro, dateStr} = noteStates[0] + const { theme, intro, dateStr } = noteStates[0] const statusHist = new Map<string, number>() for (const noteState of noteStates) { @@ -284,109 +326,151 @@ class BatchView extends Toggleable { statusHist.set(st, c + 1) } - const unresolvedTotal = (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0) - const badges: (BaseUIElement)[] = [ + const unresolvedTotal = + (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0) + const badges: BaseUIElement[] = [ new FixedUiElement(dateStr).SetClass("literal-code rounded-full"), - new FixedUiElement(noteStates.length + " total").SetClass("literal-code rounded-full ml-1 border-4 border-gray") + new FixedUiElement(noteStates.length + " total") + .SetClass("literal-code rounded-full ml-1 border-4 border-gray") .onClick(() => filterOn.setData(undefined)), - unresolvedTotal === 0 ? - new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]) - .SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") : - new FixedUiElement(Math.round(100 - 100 * unresolvedTotal / noteStates.length) + "%").SetClass("literal-code rounded-full ml-1") + unresolvedTotal === 0 + ? new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]).SetClass( + "flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black" + ) + : new FixedUiElement( + Math.round(100 - (100 * unresolvedTotal) / noteStates.length) + "%" + ).SetClass("literal-code rounded-full ml-1"), ] const filterOn = new UIEventSource<string>(undefined) - Object.keys(BatchView.icons).forEach(status => { + Object.keys(BatchView.icons).forEach((status) => { const count = statusHist.get(status) if (count === undefined) { return undefined } - const normal = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status]) - .SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") - const selected = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status]) - .SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse") + const normal = new Combine([ + BatchView.icons[status]().SetClass("h-6 m-1"), + count + " " + status, + ]).SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") + const selected = new Combine([ + BatchView.icons[status]().SetClass("h-6 m-1"), + count + " " + status, + ]).SetClass( + "flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse" + ) - const toggle = new ClickableToggle(selected, normal, filterOn.sync(f => f === status, [], (selected, previous) => { - if (selected) { - return status; - } - if (previous === status) { - return undefined - } - return previous - })).ToggleOnClick() + const toggle = new ClickableToggle( + selected, + normal, + filterOn.sync( + (f) => f === status, + [], + (selected, previous) => { + if (selected) { + return status + } + if (previous === status) { + return undefined + } + return previous + } + ) + ).ToggleOnClick() badges.push(toggle) }) - - const fullTable = new NoteTable(noteStates, state); - + const fullTable = new NoteTable(noteStates, state) super( new Combine([ new Title(theme + ": " + intro, 2), new Combine(badges).SetClass("flex flex-wrap"), ]), - new VariableUiElement(filterOn.map(filter => { - if (filter === undefined) { - return fullTable - } - return new NoteTable(noteStates.filter(ns => ns.status === filter), state) - })), + new VariableUiElement( + filterOn.map((filter) => { + if (filter === undefined) { + return fullTable + } + return new NoteTable( + noteStates.filter((ns) => ns.status === filter), + state + ) + }) + ), { - closeOnClick: false - }) - + closeOnClick: false, + } + ) } } class ImportInspector extends VariableUiElement { - - constructor(userDetails: { uid: number } | { display_name: string, search?: string }, state: UserRelatedState) { - let url; + constructor( + userDetails: { uid: number } | { display_name: string; search?: string }, + state: UserRelatedState + ) { + let url if (userDetails["uid"] !== undefined) { - url = "https://api.openstreetmap.org/api/0.6/notes/search.json?user=" + userDetails["uid"] + "&closed=730&limit=10000&sort=created_at&q=%23import" + url = + "https://api.openstreetmap.org/api/0.6/notes/search.json?user=" + + userDetails["uid"] + + "&closed=730&limit=10000&sort=created_at&q=%23import" } else { - url = "https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" + - encodeURIComponent(userDetails["display_name"]) + "&limit=10000&closed=730&sort=created_at&q=" + encodeURIComponent(userDetails["search"] ?? "#import") + url = + "https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" + + encodeURIComponent(userDetails["display_name"]) + + "&limit=10000&closed=730&sort=created_at&q=" + + encodeURIComponent(userDetails["search"] ?? "#import") } + const notes: UIEventSource< + { error: string } | { success: { features: { properties: NoteProperties }[] } } + > = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url)) + super( + notes.map((notes) => { + if (notes === undefined) { + return new Loading("Loading notes which mention '#import'") + } + if (notes["error"] !== undefined) { + return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass( + "alert" + ) + } + // We only care about the properties here + const props: NoteProperties[] = notes["success"].features.map((f) => f.properties) + const perBatch: NoteState[][] = Array.from( + ImportInspector.SplitNotesIntoBatches(props).values() + ) + const els: Toggleable[] = perBatch.map( + (noteStates) => new BatchView(noteStates, state) + ) - const notes: UIEventSource<{ error: string } | { success: { features: { properties: NoteProperties }[] } }> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url)) - super(notes.map(notes => { - - if (notes === undefined) { - return new Loading("Loading notes which mention '#import'") - } - if (notes["error"] !== undefined) { - return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass("alert") - } - // We only care about the properties here - const props: NoteProperties[] = notes["success"].features.map(f => f.properties) - const perBatch: NoteState[][] = Array.from(ImportInspector.SplitNotesIntoBatches(props).values()); - const els: Toggleable[] = perBatch.map(noteStates => new BatchView(noteStates, state)) - - const accordeon = new Accordeon(els) - let contents = []; - if (state?.osmConnection?.isLoggedIn?.data) { - contents = - [ + const accordeon = new Accordeon(els) + let contents = [] + if (state?.osmConnection?.isLoggedIn?.data) { + contents = [ new Title(Translations.t.importInspector.title, 1), - new SubtleButton(undefined, "Create a new batch of imports", {url: 'import_helper.html'})] - } - contents.push(accordeon) - const content = new Combine(contents) - return new LeftIndex( - [new TableOfContents(content, {noTopLevel: true, maxDepth: 1}).SetClass("subtle"), - new DownloadStatisticsButton(perBatch) - ], - content - ) - - })); + new SubtleButton(undefined, "Create a new batch of imports", { + url: "import_helper.html", + }), + ] + } + contents.push(accordeon) + const content = new Combine(contents) + return new LeftIndex( + [ + new TableOfContents(content, { noTopLevel: true, maxDepth: 1 }).SetClass( + "subtle" + ), + new DownloadStatisticsButton(perBatch), + ], + content + ) + }) + ) } /** @@ -397,7 +481,7 @@ class ImportInspector extends VariableUiElement { const prefix = "https://mapcomplete.osm.be/" for (const prop of props) { const lines = prop.comments[0].text.split("\n") - const trigger = lines.findIndex(l => l.startsWith(prefix) && l.endsWith("#import")) + const trigger = lines.findIndex((l) => l.startsWith(prefix) && l.endsWith("#import")) if (trigger < 0) { continue } @@ -409,16 +493,30 @@ class ImportInspector extends VariableUiElement { if (!perBatch.has(key)) { perBatch.set(key, []) } - let status: "open" | "closed" | "imported" | "invalid" | "already_mapped" | "not_found" | "has_comments" = "open" + let status: + | "open" + | "closed" + | "imported" + | "invalid" + | "already_mapped" + | "not_found" + | "has_comments" = "open" if (prop.closed_at !== undefined) { const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase() if (lastComment.indexOf("does not exist") >= 0) { status = "not_found" } else if (lastComment.indexOf("already mapped") >= 0) { status = "already_mapped" - } else if (lastComment.indexOf("invalid") >= 0 || lastComment.indexOf("incorrecto") >= 0) { + } else if ( + lastComment.indexOf("invalid") >= 0 || + lastComment.indexOf("incorrecto") >= 0 + ) { status = "invalid" - } else if (["imported", "erbij", "toegevoegd", "added"].some(keyword => lastComment.toLowerCase().indexOf(keyword) >= 0)) { + } else if ( + ["imported", "erbij", "toegevoegd", "added"].some( + (keyword) => lastComment.toLowerCase().indexOf(keyword) >= 0 + ) + ) { status = "imported" } else { status = "closed" @@ -435,28 +533,41 @@ class ImportInspector extends VariableUiElement { status, }) } - return perBatch; + return perBatch } } class ImportViewerGui extends LoginToggle { - constructor() { const state = new UserRelatedState(undefined) - const displayNameParam = QueryParameters.GetQueryParameter("user", "", "The username of the person whom you want to see the notes for"); - const searchParam = QueryParameters.GetQueryParameter("search", "", "A text that should be included in the first comment of the note to be shown") + const displayNameParam = QueryParameters.GetQueryParameter( + "user", + "", + "The username of the person whom you want to see the notes for" + ) + const searchParam = QueryParameters.GetQueryParameter( + "search", + "", + "A text that should be included in the first comment of the note to be shown" + ) super( - new VariableUiElement(state.osmConnection.userDetails.map(ud => { - const display_name = displayNameParam.data; - const search = searchParam.data; - if (display_name !== "" && search !== "") { - return new ImportInspector({display_name, search}, undefined); - } - return new ImportInspector(ud, state); - }, [displayNameParam, searchParam])), - "Login to inspect your import flows", state + new VariableUiElement( + state.osmConnection.userDetails.map( + (ud) => { + const display_name = displayNameParam.data + const search = searchParam.data + if (display_name !== "" && search !== "") { + return new ImportInspector({ display_name, search }, undefined) + } + return new ImportInspector(ud, state) + }, + [displayNameParam, searchParam] + ) + ), + "Login to inspect your import flows", + state ) } } -new ImportViewerGui().AttachTo("main") \ No newline at end of file +new ImportViewerGui().AttachTo("main") diff --git a/UI/ImportFlow/Introdution.ts b/UI/ImportFlow/Introdution.ts index d80090196..e5f14f0af 100644 --- a/UI/ImportFlow/Introdution.ts +++ b/UI/ImportFlow/Introdution.ts @@ -1,45 +1,43 @@ -import Combine from "../Base/Combine"; -import {FlowStep} from "./FlowStep"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import Title from "../Base/Title"; -import {CreateNotes} from "./CreateNotes"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import Combine from "../Base/Combine" +import { FlowStep } from "./FlowStep" +import { UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import Title from "../Base/Title" +import { CreateNotes } from "./CreateNotes" +import { FixedUiElement } from "../Base/FixedUiElement" export default class Introdution extends Combine implements FlowStep<void> { - readonly IsValid: UIEventSource<boolean>; - readonly Value: UIEventSource<void>; + readonly IsValid: UIEventSource<boolean> + readonly Value: UIEventSource<void> constructor() { - const example = CreateNotes.createNoteContentsUi({ - properties:{ - "some_key":"some_value", - "note":"a note in the original dataset" + const example = CreateNotes.createNoteContentsUi( + { + properties: { + some_key: "some_value", + note: "a note in the original dataset", + }, + geometry: { + coordinates: [3.4, 51.2], + }, }, - geometry:{ - coordinates: [3.4,51.2] + { + wikilink: + "https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>", + intro: "There might be an XYZ here", + theme: "theme", + source: "source of the data", } - }, { - wikilink: "https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>", - intro: "There might be an XYZ here", - theme: "theme", - source: "source of the data" - }).map(el => el === "" ? new FixedUiElement("").SetClass("block") : el) - + ).map((el) => (el === "" ? new FixedUiElement("").SetClass("block") : el)) + super([ new Title(Translations.t.importHelper.introduction.title), Translations.t.importHelper.introduction.description, Translations.t.importHelper.introduction.importFormat, - new Combine( - [new Combine( - example - ).SetClass("flex flex-col") - ] ).SetClass("literal-code") - ]); + new Combine([new Combine(example).SetClass("flex flex-col")]).SetClass("literal-code"), + ]) this.SetClass("flex flex-col") - this. IsValid= new UIEventSource<boolean>(true); - this. Value = new UIEventSource<void>(undefined); - + this.IsValid = new UIEventSource<boolean>(true) + this.Value = new UIEventSource<void>(undefined) } - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/LoginToImport.ts b/UI/ImportFlow/LoginToImport.ts index 08db0e303..9e5a6696c 100644 --- a/UI/ImportFlow/LoginToImport.ts +++ b/UI/ImportFlow/LoginToImport.ts @@ -1,55 +1,74 @@ -import Combine from "../Base/Combine"; -import {FlowStep} from "./FlowStep"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import Title from "../Base/Title"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {LoginToggle} from "../Popup/LoginButton"; -import Img from "../Base/Img"; -import Constants from "../../Models/Constants"; -import Toggle from "../Input/Toggle"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import MoreScreen from "../BigComponents/MoreScreen"; -import CheckBoxes from "../Input/Checkboxes"; +import Combine from "../Base/Combine" +import { FlowStep } from "./FlowStep" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import Title from "../Base/Title" +import { VariableUiElement } from "../Base/VariableUIElement" +import { LoginToggle } from "../Popup/LoginButton" +import Img from "../Base/Img" +import Constants from "../../Models/Constants" +import Toggle from "../Input/Toggle" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import MoreScreen from "../BigComponents/MoreScreen" +import CheckBoxes from "../Input/Checkboxes" export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> { - readonly IsValid: Store<boolean>; - readonly Value: Store<UserRelatedState>; + readonly IsValid: Store<boolean> + readonly Value: Store<UserRelatedState> + + private static readonly whitelist = [15015689] - private static readonly whitelist = [15015689]; - constructor(state: UserRelatedState) { const t = Translations.t.importHelper.login - const check = new CheckBoxes([new VariableUiElement(state.osmConnection.userDetails.map(ud => t.loginIsCorrect.Subs(ud)))]) - const isValid = state.osmConnection.userDetails.map(ud => - LoginToImport.whitelist.indexOf(ud.uid) >= 0 || ud.csCount >= Constants.userJourney.importHelperUnlock) + const check = new CheckBoxes([ + new VariableUiElement( + state.osmConnection.userDetails.map((ud) => t.loginIsCorrect.Subs(ud)) + ), + ]) + const isValid = state.osmConnection.userDetails.map( + (ud) => + LoginToImport.whitelist.indexOf(ud.uid) >= 0 || + ud.csCount >= Constants.userJourney.importHelperUnlock + ) super([ new Title(t.userAccountTitle), new LoginToggle( - new VariableUiElement(state.osmConnection.userDetails.map(ud => { - if (ud === undefined) { - return undefined - } - return new Combine([ - new Img(ud.img ?? "./assets/svgs/help.svg").SetClass("w-16 h-16 rounded-full"), - t.loggedInWith.Subs(ud), - new SubtleButton(Svg.logout_svg().SetClass("h-8"), Translations.t.general.logout) - .onClick(() => state.osmConnection.LogOut()), - check - ]); - })), + new VariableUiElement( + state.osmConnection.userDetails.map((ud) => { + if (ud === undefined) { + return undefined + } + return new Combine([ + new Img(ud.img ?? "./assets/svgs/help.svg").SetClass( + "w-16 h-16 rounded-full" + ), + t.loggedInWith.Subs(ud), + new SubtleButton( + Svg.logout_svg().SetClass("h-8"), + Translations.t.general.logout + ).onClick(() => state.osmConnection.LogOut()), + check, + ]) + }) + ), t.loginRequired, state ), - new Toggle(undefined, - new Combine( - [t.lockNotice.Subs(Constants.userJourney).SetClass("alert"), - MoreScreen.CreateProffessionalSerivesButton()]) - , isValid) + new Toggle( + undefined, + new Combine([ + t.lockNotice.Subs(Constants.userJourney).SetClass("alert"), + MoreScreen.CreateProffessionalSerivesButton(), + ]), + isValid + ), ]) this.Value = new UIEventSource<UserRelatedState>(state) - this.IsValid = isValid.map(isValid => isValid && check.GetValue().data.length > 0, [check.GetValue()]); + this.IsValid = isValid.map( + (isValid) => isValid && check.GetValue().data.length > 0, + [check.GetValue()] + ) } -} \ No newline at end of file +} diff --git a/UI/ImportFlow/MapPreview.ts b/UI/ImportFlow/MapPreview.ts index cb1a1049f..ab1881327 100644 --- a/UI/ImportFlow/MapPreview.ts +++ b/UI/ImportFlow/MapPreview.ts @@ -1,82 +1,86 @@ -import Combine from "../Base/Combine"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {BBox} from "../../Logic/BBox"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import Translations from "../i18n/Translations"; -import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; -import Constants from "../../Models/Constants"; -import {DropDown} from "../Input/DropDown"; -import {Utils} from "../../Utils"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import Loc from "../../Models/Loc"; -import Minimap from "../Base/Minimap"; -import Attribution from "../BigComponents/Attribution"; -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; -import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import Toggle from "../Input/Toggle"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {FlowStep} from "./FlowStep"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; -import Title from "../Base/Title"; -import CheckBoxes from "../Input/Checkboxes"; -import {AllTagsPanel} from "../AllTagsPanel"; -import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; +import Combine from "../Base/Combine" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { BBox } from "../../Logic/BBox" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import Translations from "../i18n/Translations" +import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" +import Constants from "../../Models/Constants" +import { DropDown } from "../Input/DropDown" +import { Utils } from "../../Utils" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import BaseLayer from "../../Models/BaseLayer" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import Loc from "../../Models/Loc" +import Minimap from "../Base/Minimap" +import Attribution from "../BigComponents/Attribution" +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" +import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import Toggle from "../Input/Toggle" +import { VariableUiElement } from "../Base/VariableUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" +import { FlowStep } from "./FlowStep" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import Title from "../Base/Title" +import CheckBoxes from "../Input/Checkboxes" +import { AllTagsPanel } from "../AllTagsPanel" +import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" class PreviewPanel extends ScrollableFullScreen { - constructor(tags: UIEventSource<any>) { super( - _ => new FixedUiElement("Element to import"), - _ => new Combine(["The tags are:", - new AllTagsPanel(tags) - ]).SetClass("flex flex-col"), + (_) => new FixedUiElement("Element to import"), + (_) => new Combine(["The tags are:", new AllTagsPanel(tags)]).SetClass("flex flex-col"), "element" - ); + ) } - } /** * Shows the data to import on a map, asks for the correct layer to be selected */ -export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[] }> { - public readonly IsValid: Store<boolean>; - public readonly Value: Store<{ bbox: BBox, layer: LayerConfig, features: any[] }> +export class MapPreview + extends Combine + implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[] }> +{ + public readonly IsValid: Store<boolean> + public readonly Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[] }> constructor( state: UserRelatedState, - geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) { - const t = Translations.t.importHelper.mapPreview; + geojson: { features: { properties: any; geometry: { coordinates: [number, number] } }[] } + ) { + const t = Translations.t.importHelper.mapPreview const propertyKeys = new Set<string>() for (const f of geojson.features) { - Object.keys(f.properties).forEach(key => propertyKeys.add(key)) + Object.keys(f.properties).forEach((key) => propertyKeys.add(key)) } - - const availableLayers = AllKnownLayouts.AllPublicLayers().filter(l => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0) - const layerPicker = new DropDown(t.selectLayer, - [{shown: t.selectLayer, value: undefined}].concat(availableLayers.map(l => ({ - shown: l.name, - value: l - }))) + const availableLayers = AllKnownLayouts.AllPublicLayers().filter( + (l) => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0 + ) + const layerPicker = new DropDown( + t.selectLayer, + [{ shown: t.selectLayer, value: undefined }].concat( + availableLayers.map((l) => ({ + shown: l.name, + value: l, + })) + ) ) let autodetected = new UIEventSource(false) for (const layer of availableLayers) { - const mismatched = geojson.features.some(f => - !layer.source.osmTags.matchesProperties(f.properties) + const mismatched = geojson.features.some( + (f) => !layer.source.osmTags.matchesProperties(f.properties) ) if (!mismatched) { console.log("Autodected layer", layer.id) - layerPicker.GetValue().setData(layer); - layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) + layerPicker.GetValue().setData(layer) + layerPicker.GetValue().addCallback((_) => autodetected.setData(false)) autodetected.setData(true) - break; + break } } @@ -86,65 +90,84 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: return copy }) - const matching: Store<{ properties: any, geometry: { coordinates: [number, number] } }[]> = layerPicker.GetValue().map((layer: LayerConfig) => { - if (layer === undefined) { - return []; - } - const matching: { properties: any, geometry: { coordinates: [number, number] } }[] = [] - - for (const feature of withId) { - if (layer.source.osmTags.matchesProperties(feature.properties)) { - matching.push(feature) + const matching: Store<{ properties: any; geometry: { coordinates: [number, number] } }[]> = + layerPicker.GetValue().map((layer: LayerConfig) => { + if (layer === undefined) { + return [] } - } + const matching: { properties: any; geometry: { coordinates: [number, number] } }[] = + [] - return matching - }) + for (const feature of withId) { + if (layer.source.osmTags.matchesProperties(feature.properties)) { + matching.push(feature) + } + } + + return matching + }) const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) - const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) + const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 }) const currentBounds = new UIEventSource<BBox>(undefined) const map = Minimap.createMiniMap({ allowMoving: true, location, background, bounds: currentBounds, - attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) + attribution: new Attribution( + location, + state.osmConnection.userDetails, + undefined, + currentBounds + ), }) - const layerControl = new BackgroundMapSwitch( { - backgroundLayer: background, - locationControl: location - },background) + const layerControl = new BackgroundMapSwitch( + { + backgroundLayer: background, + locationControl: location, + }, + background + ) map.SetClass("w-full").SetStyle("height: 500px") new ShowDataMultiLayer({ - layers: new UIEventSource<FilteredLayer[]>(AllKnownLayouts.AllPublicLayers() - .filter(l => l.source.geojsonSource === undefined) - .map(l => ({ - layerDef: l, - isDisplayed: new UIEventSource<boolean>(true), - appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) - }))), + layers: new UIEventSource<FilteredLayer[]>( + AllKnownLayouts.AllPublicLayers() + .filter((l) => l.source.geojsonSource === undefined) + .map((l) => ({ + layerDef: l, + isDisplayed: new UIEventSource<boolean>(true), + appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined), + })) + ), zoomToFeatures: true, - features: StaticFeatureSource.fromDateless(matching.map(features => features.map(feature => ({feature})))), + features: StaticFeatureSource.fromDateless( + matching.map((features) => features.map((feature) => ({ feature }))) + ), leafletMap: map.leafletMap, - popup: (tag) => new PreviewPanel(tag).SetClass("font-lg") + popup: (tag) => new PreviewPanel(tag).SetClass("font-lg"), }) - var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) + var bbox = matching.map((feats) => + BBox.bboxAroundAll(feats.map((f) => new BBox([f.geometry.coordinates]))) + ) + const mismatchIndicator = new VariableUiElement( + matching.map((matching) => { + if (matching === undefined) { + return undefined + } + const diff = geojson.features.length - matching.length + if (diff === 0) { + return undefined + } + const obligatory = layerPicker + .GetValue() + .data?.source?.osmTags?.asHumanString(false, false, {}) + return t.mismatch.Subs({ count: diff, tags: obligatory }).SetClass("alert") + }) + ) - const mismatchIndicator = new VariableUiElement(matching.map(matching => { - if (matching === undefined) { - return undefined - } - const diff = geojson.features.length - matching.length; - if (diff === 0) { - return undefined - } - const obligatory = layerPicker.GetValue().data?.source?.osmTags?.asHumanString(false, false, {}); - return t.mismatch.Subs({count: diff, tags: obligatory}).SetClass("alert") - })) - - const confirm = new CheckBoxes([t.confirm]); + const confirm = new CheckBoxes([t.confirm]) super([ new Title(t.title, 1), layerPicker, @@ -153,26 +176,30 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: mismatchIndicator, map, layerControl, - confirm - ]); + confirm, + ]) - this.Value = bbox.map(bbox => - ({ + this.Value = bbox.map( + (bbox) => ({ bbox, features: geojson.features, - layer: layerPicker.GetValue().data - }), [layerPicker.GetValue()]) - - this.IsValid = matching.map(matching => { - if (matching === undefined) { - return false - } - if (confirm.GetValue().data.length !== 1) { - return false - } - const diff = geojson.features.length - matching.length; - return diff === 0; - }, [confirm.GetValue()]) + layer: layerPicker.GetValue().data, + }), + [layerPicker.GetValue()] + ) + this.IsValid = matching.map( + (matching) => { + if (matching === undefined) { + return false + } + if (confirm.GetValue().data.length !== 1) { + return false + } + const diff = geojson.features.length - matching.length + return diff === 0 + }, + [confirm.GetValue()] + ) } -} \ No newline at end of file +} diff --git a/UI/ImportFlow/PreviewPanel.ts b/UI/ImportFlow/PreviewPanel.ts index 69e18bd05..7bfb28533 100644 --- a/UI/ImportFlow/PreviewPanel.ts +++ b/UI/ImportFlow/PreviewPanel.ts @@ -1,47 +1,53 @@ -import Combine from "../Base/Combine"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import UserRelatedState from "../../Logic/State/UserRelatedState"; -import Translations from "../i18n/Translations"; -import {Utils} from "../../Utils"; -import {FlowStep} from "./FlowStep"; -import Title from "../Base/Title"; -import BaseUIElement from "../BaseUIElement"; -import Histogram from "../BigComponents/Histogram"; -import Toggleable from "../Base/Toggleable"; -import List from "../Base/List"; -import CheckBoxes from "../Input/Checkboxes"; +import Combine from "../Base/Combine" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import UserRelatedState from "../../Logic/State/UserRelatedState" +import Translations from "../i18n/Translations" +import { Utils } from "../../Utils" +import { FlowStep } from "./FlowStep" +import Title from "../Base/Title" +import BaseUIElement from "../BaseUIElement" +import Histogram from "../BigComponents/Histogram" +import Toggleable from "../Base/Toggleable" +import List from "../Base/List" +import CheckBoxes from "../Input/Checkboxes" /** * Shows the attributes by value, requests to check them of */ -export class PreviewAttributesPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> { - public readonly IsValid: Store<boolean>; - public readonly Value: Store<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> +export class PreviewAttributesPanel + extends Combine + implements + FlowStep<{ features: { properties: any; geometry: { coordinates: [number, number] } }[] }> +{ + public readonly IsValid: Store<boolean> + public readonly Value: Store<{ + features: { properties: any; geometry: { coordinates: [number, number] } }[] + }> constructor( state: UserRelatedState, - geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) { - const t = Translations.t.importHelper.previewAttributes; - + geojson: { features: { properties: any; geometry: { coordinates: [number, number] } }[] } + ) { + const t = Translations.t.importHelper.previewAttributes + const propertyKeys = new Set<string>() for (const f of geojson.features) { - Object.keys(f.properties).forEach(key => propertyKeys.add(key)) + Object.keys(f.properties).forEach((key) => propertyKeys.add(key)) } const attributeOverview: BaseUIElement[] = [] - const n = geojson.features.length; + const n = geojson.features.length for (const key of Array.from(propertyKeys)) { - - const values = Utils.NoNull(geojson.features.map(f => f.properties[key])) - const allSame = !values.some(v => v !== values[0]) + const values = Utils.NoNull(geojson.features.map((f) => f.properties[key])) + const allSame = !values.some((v) => v !== values[0]) let countSummary: BaseUIElement if (values.length === n) { countSummary = t.allAttributesSame } else { countSummary = t.someHaveSame.Subs({ count: values.length, - percentage: Math.floor(100 * values.length / n) + percentage: Math.floor((100 * values.length) / n), }) } if (allSame) { @@ -54,25 +60,16 @@ export class PreviewAttributesPanel extends Combine implements FlowStep<{ featur if (uniqueCount !== values.length && uniqueCount < 15) { attributeOverview.push() // There are some overlapping values: histogram time! - let hist: BaseUIElement = - new Combine([ - countSummary, - new Histogram( - new UIEventSource<string[]>(values), - "Value", - "Occurence", - { - sortMode: "count-rev" - }) - ]).SetClass("flex flex-col") - + let hist: BaseUIElement = new Combine([ + countSummary, + new Histogram(new UIEventSource<string[]>(values), "Value", "Occurence", { + sortMode: "count-rev", + }), + ]).SetClass("flex flex-col") const title = new Title(key + "=*") if (uniqueCount > 15) { - hist = new Toggleable(title, - hist.SetClass("block") - ).Collapse() - + hist = new Toggleable(title, hist.SetClass("block")).Collapse() } else { attributeOverview.push(title) } @@ -82,27 +79,23 @@ export class PreviewAttributesPanel extends Combine implements FlowStep<{ featur } // All values are different or too much unique values, we add a boring (but collapsable) list - attributeOverview.push(new Toggleable( - new Title(key + "=*"), - new Combine([ - countSummary, - new List(values) - ]) - )) - + attributeOverview.push( + new Toggleable(new Title(key + "=*"), new Combine([countSummary, new List(values)])) + ) } const confirm = new CheckBoxes([t.inspectLooksCorrect]) super([ - new Title(t.inspectDataTitle.Subs({count: geojson.features.length})), + new Title(t.inspectDataTitle.Subs({ count: geojson.features.length })), "Extra remark: An attribute with 'source' or 'src' will be added as 'source' into the map pin; an attribute 'note' will be added into the map pin as well. These values won't be imported", ...attributeOverview, - confirm - ]); - - this.Value = new UIEventSource<{ features: { properties: any; geometry: { coordinates: [number, number] } }[] }>(geojson) - this.IsValid = confirm.GetValue().map(selected => selected.length == 1) + confirm, + ]) + this.Value = new UIEventSource<{ + features: { properties: any; geometry: { coordinates: [number, number] } }[] + }>(geojson) + this.IsValid = confirm.GetValue().map((selected) => selected.length == 1) } -} \ No newline at end of file +} diff --git a/UI/ImportFlow/RequestFile.ts b/UI/ImportFlow/RequestFile.ts index ee1ae621b..b8231a330 100644 --- a/UI/ImportFlow/RequestFile.ts +++ b/UI/ImportFlow/RequestFile.ts @@ -1,33 +1,33 @@ -import Combine from "../Base/Combine"; -import {Store, Stores} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import {SubtleButton} from "../Base/SubtleButton"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Title from "../Base/Title"; -import InputElementMap from "../Input/InputElementMap"; -import BaseUIElement from "../BaseUIElement"; -import FileSelectorButton from "../Input/FileSelectorButton"; -import {FlowStep} from "./FlowStep"; -import {parse} from "papaparse"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; +import Combine from "../Base/Combine" +import { Store, Stores } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import { SubtleButton } from "../Base/SubtleButton" +import { VariableUiElement } from "../Base/VariableUIElement" +import Title from "../Base/Title" +import InputElementMap from "../Input/InputElementMap" +import BaseUIElement from "../BaseUIElement" +import FileSelectorButton from "../Input/FileSelectorButton" +import { FlowStep } from "./FlowStep" +import { parse } from "papaparse" +import { FixedUiElement } from "../Base/FixedUiElement" +import { TagUtils } from "../../Logic/Tags/TagUtils" -class FileSelector extends InputElementMap<FileList, { name: string, contents: Promise<string> }> { +class FileSelector extends InputElementMap<FileList, { name: string; contents: Promise<string> }> { constructor(label: BaseUIElement) { super( - new FileSelectorButton(label, {allowMultiple: false, acceptType: "*"}), + new FileSelectorButton(label, { allowMultiple: false, acceptType: "*" }), (x0, x1) => { // Total hack: x1 is undefined is the backvalue - we effectively make this a one-way-story - return x1 === undefined || x0 === x1; + return x1 === undefined || x0 === x1 }, - filelist => { + (filelist) => { if (filelist === undefined) { return undefined } const file = filelist.item(0) - return {name: file.name, contents: file.text()} + return { name: file.name, contents: file.text() } }, - _ => undefined + (_) => undefined ) } } @@ -35,149 +35,153 @@ class FileSelector extends InputElementMap<FileList, { name: string, contents: P /** * The first step in the import flow: load a file and validate that it is a correct geojson or CSV file */ -export class RequestFile extends Combine implements FlowStep<{features: any[]}> { - +export class RequestFile extends Combine implements FlowStep<{ features: any[] }> { public readonly IsValid: Store<boolean> /** * The loaded GeoJSON */ - public readonly Value: Store<{features: any[]}> + public readonly Value: Store<{ features: any[] }> constructor() { - const t = Translations.t.importHelper.selectFile; + const t = Translations.t.importHelper.selectFile const csvSelector = new FileSelector(new SubtleButton(undefined, t.description)) - const loadedFiles = new VariableUiElement(csvSelector.GetValue().map(file => { - if (file === undefined) { - return t.noFilesLoaded.SetClass("alert") - } - return t.loadedFilesAre.Subs({file: file.name}).SetClass("thanks") - })) + const loadedFiles = new VariableUiElement( + csvSelector.GetValue().map((file) => { + if (file === undefined) { + return t.noFilesLoaded.SetClass("alert") + } + return t.loadedFilesAre.Subs({ file: file.name }).SetClass("thanks") + }) + ) const text = Stores.flatten( - csvSelector.GetValue().map(v => { + csvSelector.GetValue().map((v) => { if (v === undefined) { return undefined } return Stores.FromPromise(v.contents) - })) + }) + ) - const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map((src: string) => { - if (src === undefined) { - return undefined - } - try { - const parsed = JSON.parse(src) - if (parsed["type"] !== "FeatureCollection") { - return {error: t.errNotFeatureCollection} + const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map( + (src: string) => { + if (src === undefined) { + return undefined } - if (parsed.features.some(f => f.geometry.type != "Point")) { - return {error: t.errPointsOnly} - } - parsed.features.forEach(f => { - const props = f.properties - for (const key in props) { - if(props[key] === undefined || props[key] === null || props[key] === ""){ - delete props[key] - } - if(!TagUtils.isValidKey(key)){ - return {error: "Probably an invalid key: "+key} + try { + const parsed = JSON.parse(src) + if (parsed["type"] !== "FeatureCollection") { + return { error: t.errNotFeatureCollection } } + if (parsed.features.some((f) => f.geometry.type != "Point")) { + return { error: t.errPointsOnly } } - }) - return parsed; - - } catch (e) { - // Loading as CSV - var lines: string[][] = <any>parse(src).data; - const header = lines[0] - lines.splice(0, 1) - if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) { - return {error: t.errNoLatOrLon} - } - - if (header.some(h => h.trim() == "")) { - return {error: t.errNoName} - } - - - if (new Set(header).size !== header.length) { - return {error: t.errDuplicate} - } - - - const features = [] - for (let i = 0; i < lines.length; i++) { - const attrs = lines[i]; - if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) { - // empty line - continue + parsed.features.forEach((f) => { + const props = f.properties + for (const key in props) { + if ( + props[key] === undefined || + props[key] === null || + props[key] === "" + ) { + delete props[key] + } + if (!TagUtils.isValidKey(key)) { + return { error: "Probably an invalid key: " + key } + } + } + }) + return parsed + } catch (e) { + // Loading as CSV + var lines: string[][] = <any>parse(src).data + const header = lines[0] + lines.splice(0, 1) + if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) { + return { error: t.errNoLatOrLon } } - const properties = {} - for (let i = 0; i < header.length; i++) { - const v = attrs[i] - if (v === undefined || v === "") { + + if (header.some((h) => h.trim() == "")) { + return { error: t.errNoName } + } + + if (new Set(header).size !== header.length) { + return { error: t.errDuplicate } + } + + const features = [] + for (let i = 0; i < lines.length; i++) { + const attrs = lines[i] + if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) { + // empty line continue } - properties[header[i]] = v; - } - const coordinates = [Number(properties["lon"]), Number(properties["lat"])] - delete properties["lat"] - delete properties["lon"] - if (coordinates.some(isNaN)) { - return {error: "A coordinate could not be parsed for line " + (i + 2)} - } - const f = { - type: "Feature", - properties, - geometry: { - type: "Point", - coordinates + const properties = {} + for (let i = 0; i < header.length; i++) { + const v = attrs[i] + if (v === undefined || v === "") { + continue + } + properties[header[i]] = v } - }; - features.push(f) - } + const coordinates = [Number(properties["lon"]), Number(properties["lat"])] + delete properties["lat"] + delete properties["lon"] + if (coordinates.some(isNaN)) { + return { error: "A coordinate could not be parsed for line " + (i + 2) } + } + const f = { + type: "Feature", + properties, + geometry: { + type: "Point", + coordinates, + }, + } + features.push(f) + } - return { - type: "FeatureCollection", - features + return { + type: "FeatureCollection", + features, + } } } - }) + ) - - const errorIndicator = new VariableUiElement(asGeoJson.map(v => { - if (v === undefined) { - return undefined; - } - if (v?.error === undefined) { - return undefined; - } - let err: BaseUIElement; - if(typeof v.error === "string"){ - err = new FixedUiElement(v.error) - }else if(v.error.Clone !== undefined){ - err = v.error.Clone() - }else{ - err = v.error - } - return err.SetClass("alert"); - })) + const errorIndicator = new VariableUiElement( + asGeoJson.map((v) => { + if (v === undefined) { + return undefined + } + if (v?.error === undefined) { + return undefined + } + let err: BaseUIElement + if (typeof v.error === "string") { + err = new FixedUiElement(v.error) + } else if (v.error.Clone !== undefined) { + err = v.error.Clone() + } else { + err = v.error + } + return err.SetClass("alert") + }) + ) super([ - new Title(t.title, 1), t.fileFormatDescription, t.fileFormatDescriptionCsv, t.fileFormatDescriptionGeoJson, csvSelector, loadedFiles, - errorIndicator - - ]); + errorIndicator, + ]) this.SetClass("flex flex-col wi") - this.IsValid = asGeoJson.map(geojson => geojson !== undefined && geojson["error"] === undefined) + this.IsValid = asGeoJson.map( + (geojson) => geojson !== undefined && geojson["error"] === undefined + ) this.Value = asGeoJson } - - -} \ No newline at end of file +} diff --git a/UI/ImportFlow/SelectTheme.ts b/UI/ImportFlow/SelectTheme.ts index b443273d6..766a23688 100644 --- a/UI/ImportFlow/SelectTheme.ts +++ b/UI/ImportFlow/SelectTheme.ts @@ -1,156 +1,183 @@ -import {FlowStep} from "./FlowStep"; -import Combine from "../Base/Combine"; -import {Store} from "../../Logic/UIEventSource"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {InputElement} from "../Input/InputElement"; -import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; -import {FixedInputElement} from "../Input/FixedInputElement"; -import Img from "../Base/Img"; -import Title from "../Base/Title"; -import {RadioButton} from "../Input/RadioButton"; -import {And} from "../../Logic/Tags/And"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Toggleable from "../Base/Toggleable"; -import {BBox} from "../../Logic/BBox"; -import BaseUIElement from "../BaseUIElement"; -import PresetConfig from "../../Models/ThemeConfig/PresetConfig"; -import List from "../Base/List"; -import Translations from "../i18n/Translations"; - -export default class SelectTheme extends Combine implements FlowStep<{ - features: any[], - theme: string, - layer: LayerConfig, - bbox: BBox, -}> { +import { FlowStep } from "./FlowStep" +import Combine from "../Base/Combine" +import { Store } from "../../Logic/UIEventSource" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { InputElement } from "../Input/InputElement" +import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" +import { FixedInputElement } from "../Input/FixedInputElement" +import Img from "../Base/Img" +import Title from "../Base/Title" +import { RadioButton } from "../Input/RadioButton" +import { And } from "../../Logic/Tags/And" +import { VariableUiElement } from "../Base/VariableUIElement" +import Toggleable from "../Base/Toggleable" +import { BBox } from "../../Logic/BBox" +import BaseUIElement from "../BaseUIElement" +import PresetConfig from "../../Models/ThemeConfig/PresetConfig" +import List from "../Base/List" +import Translations from "../i18n/Translations" +export default class SelectTheme + extends Combine + implements + FlowStep<{ + features: any[] + theme: string + layer: LayerConfig + bbox: BBox + }> +{ public readonly Value: Store<{ - features: any[], - theme: string, - layer: LayerConfig, - bbox: BBox, - }>; - public readonly IsValid: Store<boolean>; + features: any[] + theme: string + layer: LayerConfig + bbox: BBox + }> + public readonly IsValid: Store<boolean> - constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) { + constructor(params: { features: any[]; layer: LayerConfig; bbox: BBox }) { const t = Translations.t.importHelper.selectTheme let options: InputElement<string>[] = AllKnownLayouts.layoutsList - .filter(th => th.layers.some(l => l.id === params.layer.id)) - .filter(th => th.id !== "personal") - .map(th => new FixedInputElement<string>( - new Combine([ - new Img(th.icon).SetClass("block h-12 w-12 br-4"), - new Title(th.title) - ]).SetClass("flex items-center"), - th.id)) - + .filter((th) => th.layers.some((l) => l.id === params.layer.id)) + .filter((th) => th.id !== "personal") + .map( + (th) => + new FixedInputElement<string>( + new Combine([ + new Img(th.icon).SetClass("block h-12 w-12 br-4"), + new Title(th.title), + ]).SetClass("flex items-center"), + th.id + ) + ) const themeRadios = new RadioButton<string>(options, { - selectFirstAsDefault: false + selectFirstAsDefault: false, }) - - const applicablePresets = themeRadios.GetValue().map(theme => { + const applicablePresets = themeRadios.GetValue().map((theme) => { if (theme === undefined) { return [] } // we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides - const themeConfig = AllKnownLayouts.layoutsList.find(th => th.id === theme) - const layer = themeConfig.layers.find(l => l.id === params.layer.id) + const themeConfig = AllKnownLayouts.layoutsList.find((th) => th.id === theme) + const layer = themeConfig.layers.find((l) => l.id === params.layer.id) return layer.presets }) - - const nonMatchedElements = applicablePresets.map(presets => { + const nonMatchedElements = applicablePresets.map((presets) => { if (presets === undefined || presets.length === 0) { return undefined } - return params.features.filter(feat => !presets.some(preset => new And(preset.tags).matchesProperties(feat.properties))) + return params.features.filter( + (feat) => + !presets.some((preset) => + new And(preset.tags).matchesProperties(feat.properties) + ) + ) }) super([ new Title(t.title), - t.intro, + t.intro, themeRadios, - new VariableUiElement(applicablePresets.map(applicablePresets => { - if (themeRadios.GetValue().data === undefined) { - return undefined - } - if (applicablePresets === undefined || applicablePresets.length === 0) { - return t.noMatchingPresets.SetClass("alert") - } - }, [themeRadios.GetValue()])), + new VariableUiElement( + applicablePresets.map( + (applicablePresets) => { + if (themeRadios.GetValue().data === undefined) { + return undefined + } + if (applicablePresets === undefined || applicablePresets.length === 0) { + return t.noMatchingPresets.SetClass("alert") + } + }, + [themeRadios.GetValue()] + ) + ), - new VariableUiElement(nonMatchedElements.map(unmatched => SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data), [applicablePresets])) - ]); + new VariableUiElement( + nonMatchedElements.map( + (unmatched) => + SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data), + [applicablePresets] + ) + ), + ]) this.SetClass("flex flex-col") - this.Value = themeRadios.GetValue().map(theme => ({ + this.Value = themeRadios.GetValue().map((theme) => ({ features: params.features, layer: params.layer, bbox: params.bbox, - theme + theme, })) - this.IsValid = this.Value.map(obj => { - if (obj === undefined) { - return false; - } - if ([obj.theme, obj.features].some(v => v === undefined)) { - return false; - } - if (applicablePresets.data === undefined || applicablePresets.data.length === 0) { - return false - } - if ((nonMatchedElements.data?.length ?? 0) > 0) { - return false; - } + this.IsValid = this.Value.map( + (obj) => { + if (obj === undefined) { + return false + } + if ([obj.theme, obj.features].some((v) => v === undefined)) { + return false + } + if (applicablePresets.data === undefined || applicablePresets.data.length === 0) { + return false + } + if ((nonMatchedElements.data?.length ?? 0) > 0) { + return false + } - return true; - - }, [applicablePresets]) + return true + }, + [applicablePresets] + ) } - private static nonMatchedElementsPanel(unmatched: any[], applicablePresets: PresetConfig[]): BaseUIElement { + private static nonMatchedElementsPanel( + unmatched: any[], + applicablePresets: PresetConfig[] + ): BaseUIElement { if (unmatched === undefined || unmatched.length === 0) { return } - const t = Translations.t.importHelper.selectTheme - - const applicablePresetsOverview = applicablePresets.map(preset => - t.needsTags.Subs( - {title: preset.title, - tags:preset.tags.map(t => t.asHumanString()).join(" & ") }) + const t = Translations.t.importHelper.selectTheme + + const applicablePresetsOverview = applicablePresets.map((preset) => + t.needsTags + .Subs({ + title: preset.title, + tags: preset.tags.map((t) => t.asHumanString()).join(" & "), + }) .SetClass("thanks") - ); + ) const unmatchedPanels: BaseUIElement[] = [] for (const feat of unmatched) { const parts: BaseUIElement[] = [] - parts.push(new Combine(Object.keys(feat.properties).map(k => - k+"="+feat.properties[k] - )).SetClass("flex flex-col")) + parts.push( + new Combine( + Object.keys(feat.properties).map((k) => k + "=" + feat.properties[k]) + ).SetClass("flex flex-col") + ) for (const preset of applicablePresets) { const tags = new And(preset.tags).asChange({}) const missing = [] - for (const {k, v} of tags) { + for (const { k, v } of tags) { if (preset[k] === undefined) { - missing.push(t.missing.Subs({k,v})) + missing.push(t.missing.Subs({ k, v })) } else if (feat.properties[k] !== v) { - missing.push(t.misMatch.Subs({k, v, properties: feat.properties})) + missing.push(t.misMatch.Subs({ k, v, properties: feat.properties })) } } if (missing.length > 0) { parts.push( - new Combine([ - t.notApplicable.Subs(preset), - new List(missing) - ]).SetClass("flex flex-col alert") + new Combine([t.notApplicable.Subs(preset), new List(missing)]).SetClass( + "flex flex-col alert" + ) ) } - } unmatchedPanels.push(new Combine(parts).SetClass("flex flex-col")) @@ -159,11 +186,7 @@ export default class SelectTheme extends Combine implements FlowStep<{ return new Combine([ t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"), ...applicablePresetsOverview, - new Toggleable(new Title(t.unmatchedTitle), - new Combine(unmatchedPanels)) + new Toggleable(new Title(t.unmatchedTitle), new Combine(unmatchedPanels)), ]).SetClass("flex flex-col") - } - - -} \ No newline at end of file +} diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 236e7b831..108130d6c 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -1,18 +1,18 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import InputElementMap from "./InputElementMap"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import InputElementMap from "./InputElementMap" export class CheckBox extends InputElementMap<number[], boolean> { - constructor(el: BaseUIElement , defaultValue?: boolean) { + constructor(el: BaseUIElement, defaultValue?: boolean) { super( new CheckBoxes([el]), (x0, x1) => x0 === x1, - t => t.length > 0, - x => x ? [0] : [], - ); - if(defaultValue !== undefined){ + (t) => t.length > 0, + (x) => (x ? [0] : []) + ) + if (defaultValue !== undefined) { this.GetValue().setData(defaultValue) } } @@ -23,94 +23,78 @@ export class CheckBox extends InputElementMap<number[], boolean> { * The value will contain the indexes of the selected checkboxes */ export default class CheckBoxes extends InputElement<number[]> { - private static _nextId = 0; - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private readonly value: UIEventSource<number[]>; - private readonly _elements: BaseUIElement[]; + private static _nextId = 0 + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) + private readonly value: UIEventSource<number[]> + private readonly _elements: BaseUIElement[] - constructor( - elements: BaseUIElement[], - value = new UIEventSource<number[]>([]) - ) { - super(); - this.value = value; - this._elements = Utils.NoNull(elements); - this.SetClass("flex flex-col"); + constructor(elements: BaseUIElement[], value = new UIEventSource<number[]>([])) { + super() + this.value = value + this._elements = Utils.NoNull(elements) + this.SetClass("flex flex-col") } IsValid(ts: number[]): boolean { - return ts !== undefined; + return ts !== undefined } GetValue(): UIEventSource<number[]> { - return this.value; + return this.value } protected InnerConstructElement(): HTMLElement { - const formTag = document.createElement("form"); + const formTag = document.createElement("form") - const value = this.value; - const elements = this._elements; + const value = this.value + const elements = this._elements for (let i = 0; i < elements.length; i++) { - let inputI = elements[i]; - const input = document.createElement("input"); - const id = CheckBoxes._nextId; - CheckBoxes._nextId++; - input.id = "checkbox" + id; + let inputI = elements[i] + const input = document.createElement("input") + const id = CheckBoxes._nextId + CheckBoxes._nextId++ + input.id = "checkbox" + id - input.type = "checkbox"; - input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0"); + input.type = "checkbox" + input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0") - const label = document.createElement("label"); - label.htmlFor = input.id; - label.appendChild(inputI.ConstructElement()); - label.classList.add( - "block", - "w-full", - "p-2", - "cursor-pointer", - "bg-red" - ); + const label = document.createElement("label") + label.htmlFor = input.id + label.appendChild(inputI.ConstructElement()) + label.classList.add("block", "w-full", "p-2", "cursor-pointer", "bg-red") - const wrapper = document.createElement("div"); - wrapper.classList.add( - "wrapper", - "flex", - "w-full", - "border", - "border-gray-400", - "mb-1" - ); - wrapper.appendChild(input); - wrapper.appendChild(label); - formTag.appendChild(wrapper); + const wrapper = document.createElement("div") + wrapper.classList.add("wrapper", "flex", "w-full", "border", "border-gray-400", "mb-1") + wrapper.appendChild(input) + wrapper.appendChild(label) + formTag.appendChild(wrapper) value.addCallbackAndRunD((selectedValues) => { - input.checked = selectedValues.indexOf(i) >= 0; + input.checked = selectedValues.indexOf(i) >= 0 if (input.checked) { - wrapper.classList.remove("border-gray-400"); - wrapper.classList.add("border-black"); + wrapper.classList.remove("border-gray-400") + wrapper.classList.add("border-black") } else { - wrapper.classList.add("border-gray-400"); - wrapper.classList.remove("border-black"); + wrapper.classList.add("border-gray-400") + wrapper.classList.remove("border-black") } - }); + }) input.onchange = () => { // Index = index in the list of already checked items - const index = value.data.indexOf(i); + const index = value.data.indexOf(i) if (input.checked && index < 0) { - value.data.push(i); - value.ping(); + value.data.push(i) + value.ping() } else if (index >= 0) { - value.data.splice(index, 1); - value.ping(); + value.data.splice(index, 1) + value.ping() } - }; + } } - return formTag; + return formTag } } diff --git a/UI/Input/ColorPicker.ts b/UI/Input/ColorPicker.ts index 8302df986..3960c6ccb 100644 --- a/UI/Input/ColorPicker.ts +++ b/UI/Input/ColorPicker.ts @@ -1,43 +1,39 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" export default class ColorPicker extends InputElement<string> { - - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) private readonly value: UIEventSource<string> private readonly _element: HTMLElement - constructor( - value: UIEventSource<string> = new UIEventSource<string>(undefined) - ) { - super(); - this.value = value; + constructor(value: UIEventSource<string> = new UIEventSource<string>(undefined)) { + super() + this.value = value const el = document.createElement("input") - this._element = el; + this._element = el el.type = "color" - this.value.addCallbackAndRunD(v => { + this.value.addCallbackAndRunD((v) => { el.value = v - }); + }) el.oninput = () => { - const hex = el.value; - value.setData(hex); + const hex = el.value + value.setData(hex) } } GetValue(): UIEventSource<string> { - return this.value; + return this.value } IsValid(t: string): boolean { - return false; + return false } protected InnerConstructElement(): HTMLElement { - return this._element; + return this._element } - -} \ No newline at end of file +} diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts index adb86603a..2af732833 100644 --- a/UI/Input/CombinedInputElement.ts +++ b/UI/Input/CombinedInputElement.ts @@ -1,28 +1,30 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import BaseUIElement from "../BaseUIElement"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import BaseUIElement from "../BaseUIElement" export default class CombinedInputElement<T, J, X> extends InputElement<X> { - - private readonly _a: InputElement<T>; - private readonly _b: InputElement<J>; - private readonly _combined: BaseUIElement; + private readonly _a: InputElement<T> + private readonly _b: InputElement<J> + private readonly _combined: BaseUIElement private readonly _value: UIEventSource<X> - private readonly _split: (x: X) => [T, J]; + private readonly _split: (x: X) => [T, J] - constructor(a: InputElement<T>, b: InputElement<J>, - combine: (t: T, j: J) => X, - split: (x: X) => [T, J]) { - super(); - this._a = a; - this._b = b; - this._split = split; - this._combined = new Combine([this._a, this._b]); + constructor( + a: InputElement<T>, + b: InputElement<J>, + combine: (t: T, j: J) => X, + split: (x: X) => [T, J] + ) { + super() + this._a = a + this._b = b + this._split = split + this._combined = new Combine([this._a, this._b]) this._value = this._a.GetValue().sync( - t => combine(t, this._b?.GetValue()?.data), + (t) => combine(t, this._b?.GetValue()?.data), [this._b.GetValue()], - x => { + (x) => { const [t, j] = split(x) this._b.GetValue()?.setData(j) return t @@ -31,16 +33,15 @@ export default class CombinedInputElement<T, J, X> extends InputElement<X> { } GetValue(): UIEventSource<X> { - return this._value; + return this._value } IsValid(x: X): boolean { const [t, j] = this._split(x) - return this._a.IsValid(t) && this._b.IsValid(j); + return this._a.IsValid(t) && this._b.IsValid(j) } protected InnerConstructElement(): HTMLElement { - return this._combined.ConstructElement(); + return this._combined.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 108065b68..2df6adf23 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -1,120 +1,118 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {Utils} from "../../Utils"; -import Loc from "../../Models/Loc"; -import Minimap from "../Base/Minimap"; - +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import BaseUIElement from "../BaseUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" +import { Utils } from "../../Utils" +import Loc from "../../Models/Loc" +import Minimap from "../Base/Minimap" /** * Selects a direction in degrees */ export default class DirectionInput extends InputElement<string> { - public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private readonly _location: UIEventSource<Loc>; - private readonly value: UIEventSource<string>; - private background; + public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) + private readonly _location: UIEventSource<Loc> + private readonly value: UIEventSource<string> + private background - constructor(mapBackground: UIEventSource<any>, - location: UIEventSource<Loc>, - value?: UIEventSource<string>) { - super(); - this._location = location; - this.value = value ?? new UIEventSource<string>(undefined); - this.background = mapBackground; + constructor( + mapBackground: UIEventSource<any>, + location: UIEventSource<Loc>, + value?: UIEventSource<string> + ) { + super() + this._location = location + this.value = value ?? new UIEventSource<string>(undefined) + this.background = mapBackground } GetValue(): UIEventSource<string> { - return this.value; + return this.value } IsValid(str: string): boolean { - const t = Number(str); - return !isNaN(t) && t >= 0 && t <= 360; + const t = Number(str) + return !isNaN(t) && t >= 0 && t <= 360 } protected InnerConstructElement(): HTMLElement { - let map: BaseUIElement = new FixedUiElement("") if (!Utils.runningFromConsole) { map = Minimap.createMiniMap({ background: this.background, allowMoving: false, - location: this._location + location: this._location, }) } const element = new Combine([ - Svg.direction_stroke_svg().SetStyle( - `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) + Svg.direction_stroke_svg() + .SetStyle( + `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${ + this.value.data ?? 0 + }deg);` + ) .SetClass("direction-svg relative") .SetStyle("z-index: 1000"), - map.SetStyle( - `position: absolute;top: 0;left: 0;width: 100%;height: 100%;`) - + map.SetStyle(`position: absolute;top: 0;left: 0;width: 100%;height: 100%;`), ]) .SetStyle("width: min(100%, 25em); height: 0; padding-bottom: 100%") // A bit a weird CSS , see https://stackoverflow.com/questions/13851940/pure-css-solution-square-elements#19448481 .SetClass("relative block bg-white border border-black overflow-hidden rounded-full") .ConstructElement() - - this.value.addCallbackAndRunD(rotation => { + this.value.addCallbackAndRunD((rotation) => { const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement - cone.style.transform = `rotate(${rotation}deg)`; - + cone.style.transform = `rotate(${rotation}deg)` }) this.RegisterTriggers(element) element.style.overflow = "hidden" element.style.display = "block" - return element; + return element } private RegisterTriggers(htmlElement: HTMLElement) { - const self = this; + const self = this function onPosChange(x: number, y: number) { - const rect = htmlElement.getBoundingClientRect(); - const dx = -(rect.left + rect.right) / 2 + x; - const dy = (rect.top + rect.bottom) / 2 - y; - const angle = 180 * Math.atan2(dy, dx) / Math.PI; - const angleGeo = Math.floor((450 - angle) % 360); + const rect = htmlElement.getBoundingClientRect() + const dx = -(rect.left + rect.right) / 2 + x + const dy = (rect.top + rect.bottom) / 2 - y + const angle = (180 * Math.atan2(dy, dx)) / Math.PI + const angleGeo = Math.floor((450 - angle) % 360) self.value.setData("" + angleGeo) } - htmlElement.ontouchmove = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); - ev.preventDefault(); + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY) + ev.preventDefault() } htmlElement.ontouchstart = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY) } - let isDown = false; + let isDown = false htmlElement.onmousedown = (ev: MouseEvent) => { - isDown = true; - onPosChange(ev.clientX, ev.clientY); - ev.preventDefault(); + isDown = true + onPosChange(ev.clientX, ev.clientY) + ev.preventDefault() } htmlElement.onmouseup = (ev) => { - isDown = false; - ev.preventDefault(); + isDown = false + ev.preventDefault() } htmlElement.onmousemove = (ev: MouseEvent) => { if (isDown) { - onPosChange(ev.clientX, ev.clientY); + onPosChange(ev.clientX, ev.clientY) } - ev.preventDefault(); + ev.preventDefault() } } - -} \ No newline at end of file +} diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 493c9bec7..e75467654 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -1,59 +1,58 @@ -import {InputElement} from "./InputElement"; -import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; +import { InputElement } from "./InputElement" +import Translations from "../i18n/Translations" +import { UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" export class DropDown<T> extends InputElement<T> { + private static _nextDropdownId = 0 + public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) - private static _nextDropdownId = 0; - public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); + private readonly _element: HTMLElement - private readonly _element: HTMLElement; - - private readonly _value: UIEventSource<T>; - private readonly _values: { value: T; shown: string | BaseUIElement }[]; + private readonly _value: UIEventSource<T> + private readonly _values: { value: T; shown: string | BaseUIElement }[] /** - * + * * const dropdown = new DropDown<number>("test",[{value: 42, shown: "the answer"}]) * dropdown.GetValue().data // => 42 */ - constructor(label: string | BaseUIElement, - values: { value: T, shown: string | BaseUIElement }[], - value: UIEventSource<T> = undefined, - options?: { - select_class?: string - } + constructor( + label: string | BaseUIElement, + values: { value: T; shown: string | BaseUIElement }[], + value: UIEventSource<T> = undefined, + options?: { + select_class?: string + } ) { - super(); + super() value = value ?? new UIEventSource<T>(values[0].value) this._value = value - this._values = values; + this._values = values if (values.length <= 1) { - return; + return } - const id = DropDown._nextDropdownId; - DropDown._nextDropdownId++; - + const id = DropDown._nextDropdownId + DropDown._nextDropdownId++ const el = document.createElement("form") - this._element = el; - el.id = "dropdown" + id; + this._element = el + el.id = "dropdown" + id { const labelEl = Translations.W(label)?.ConstructElement() if (labelEl !== undefined) { const labelHtml = document.createElement("label") labelHtml.appendChild(labelEl) - labelHtml.htmlFor = el.id; + labelHtml.htmlFor = el.id el.appendChild(labelHtml) } } options = options ?? {} - options.select_class = options.select_class ?? 'w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200' - + options.select_class = + options.select_class ?? "w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200" { const select = document.createElement("select") @@ -66,42 +65,38 @@ export class DropDown<T> extends InputElement<T> { } el.appendChild(select) + select.onchange = () => { + const index = select.selectedIndex + value.setData(values[index].value) + } - select.onchange = (() => { - const index = select.selectedIndex; - value.setData(values[index].value); - }); - - value.addCallbackAndRun(selected => { + value.addCallbackAndRun((selected) => { for (let i = 0; i < values.length; i++) { - const value = values[i].value; + const value = values[i].value if (value === selected) { - select.selectedIndex = i; + select.selectedIndex = i } } }) } - - this.onClick(() => { - }) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes + this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes } GetValue(): UIEventSource<T> { - return this._value; + return this._value } IsValid(t: T): boolean { for (const value of this._values) { if (value.value === t) { - return true; + return true } } return false } protected InnerConstructElement(): HTMLElement { - return this._element; + return this._element } - -} \ No newline at end of file +} diff --git a/UI/Input/FileSelectorButton.ts b/UI/Input/FileSelectorButton.ts index 1c03875af..ae0b0ba42 100644 --- a/UI/Input/FileSelectorButton.ts +++ b/UI/Input/FileSelectorButton.ts @@ -1,54 +1,55 @@ -import BaseUIElement from "../BaseUIElement"; -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement" +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" export default class FileSelectorButton extends InputElement<FileList> { + private static _nextid + IsSelected: UIEventSource<boolean> + private readonly _value = new UIEventSource<FileList>(undefined) + private readonly _label: BaseUIElement + private readonly _acceptType: string + private readonly allowMultiple: boolean - private static _nextid; - IsSelected: UIEventSource<boolean>; - private readonly _value = new UIEventSource<FileList>(undefined); - private readonly _label: BaseUIElement; - private readonly _acceptType: string; - private readonly allowMultiple: boolean; - - constructor(label: BaseUIElement, options?: - { - acceptType: "image/*" | string, + constructor( + label: BaseUIElement, + options?: { + acceptType: "image/*" | string allowMultiple: true | boolean - }) { - super(); - this._label = label; - this._acceptType = options?.acceptType ?? "image/*"; + } + ) { + super() + this._label = label + this._acceptType = options?.acceptType ?? "image/*" this.SetClass("block cursor-pointer") label.SetClass("cursor-pointer") this.allowMultiple = options?.allowMultiple ?? true } GetValue(): UIEventSource<FileList> { - return this._value; + return this._value } IsValid(t: FileList): boolean { - return true; + return true } protected InnerConstructElement(): HTMLElement { - const self = this; + const self = this const el = document.createElement("form") const label = document.createElement("label") label.appendChild(this._label.ConstructElement()) el.appendChild(label) - const actualInputElement = document.createElement("input"); - actualInputElement.style.cssText = "display:none"; - actualInputElement.type = "file"; - actualInputElement.accept = this._acceptType; - actualInputElement.name = "picField"; - actualInputElement.multiple = this.allowMultiple; - actualInputElement.id = "fileselector" + FileSelectorButton._nextid; - FileSelectorButton._nextid++; + const actualInputElement = document.createElement("input") + actualInputElement.style.cssText = "display:none" + actualInputElement.type = "file" + actualInputElement.accept = this._acceptType + actualInputElement.name = "picField" + actualInputElement.multiple = this.allowMultiple + actualInputElement.id = "fileselector" + FileSelectorButton._nextid + FileSelectorButton._nextid++ - label.htmlFor = actualInputElement.id; + label.htmlFor = actualInputElement.id actualInputElement.onchange = () => { if (actualInputElement.files !== null) { @@ -56,7 +57,7 @@ export default class FileSelectorButton extends InputElement<FileList> { } } - el.addEventListener('submit', e => { + el.addEventListener("submit", (e) => { if (actualInputElement.files !== null) { self._value.setData(actualInputElement.files) } @@ -65,22 +66,20 @@ export default class FileSelectorButton extends InputElement<FileList> { el.appendChild(actualInputElement) - el.addEventListener('dragover', (event) => { - event.stopPropagation(); - event.preventDefault(); + el.addEventListener("dragover", (event) => { + event.stopPropagation() + event.preventDefault() // Style the drag-and-drop as a "copy file" operation. - event.dataTransfer.dropEffect = 'copy'; - }); + event.dataTransfer.dropEffect = "copy" + }) - el.addEventListener('drop', (event) => { - event.stopPropagation(); - event.preventDefault(); - const fileList = event.dataTransfer.files; + el.addEventListener("drop", (event) => { + event.stopPropagation() + event.preventDefault() + const fileList = event.dataTransfer.files this._value.setData(fileList) - }); + }) - return el; + return el } - - -} \ No newline at end of file +} diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts index 6dd39fc0d..b8c272fae 100644 --- a/UI/Input/FixedInputElement.ts +++ b/UI/Input/FixedInputElement.ts @@ -1,23 +1,25 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" export class FixedInputElement<T> extends InputElement<T> { - private readonly value: UIEventSource<T>; - private readonly _comparator: (t0: T, t1: T) => boolean; + private readonly value: UIEventSource<T> + private readonly _comparator: (t0: T, t1: T) => boolean - private readonly _el: HTMLElement; + private readonly _el: HTMLElement - constructor(rendering: BaseUIElement | string, - value: T | UIEventSource<T>, - comparator: ((t0: T, t1: T) => boolean) = undefined) { - super(); - this._comparator = comparator ?? ((t0, t1) => t0 == t1); + constructor( + rendering: BaseUIElement | string, + value: T | UIEventSource<T>, + comparator: (t0: T, t1: T) => boolean = undefined + ) { + super() + this._comparator = comparator ?? ((t0, t1) => t0 == t1) if (value instanceof UIEventSource) { this.value = value } else { - this.value = new UIEventSource<T>(value); + this.value = new UIEventSource<T>(value) } this._el = document.createElement("span") @@ -25,18 +27,17 @@ export class FixedInputElement<T> extends InputElement<T> { if (e) { this._el.appendChild(e) } - } GetValue(): UIEventSource<T> { - return this.value; + return this.value } IsValid(t: T): boolean { - return this._comparator(t, this.value.data); + return this._comparator(t, this.value.data) } protected InnerConstructElement(): HTMLElement { - return this._el; + return this._el } } diff --git a/UI/Input/FloorLevelInputElement.ts b/UI/Input/FloorLevelInputElement.ts index 844f05081..8b01b7783 100644 --- a/UI/Input/FloorLevelInputElement.ts +++ b/UI/Input/FloorLevelInputElement.ts @@ -1,89 +1,94 @@ -import {InputElement} from "./InputElement"; -import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Slider from "./Slider"; -import {ClickableToggle} from "./Toggle"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import BaseUIElement from "../BaseUIElement"; - -export default class FloorLevelInputElement extends VariableUiElement implements InputElement<string> { - - private readonly _value: UIEventSource<string>; - - constructor(currentLevels: Store<Record<string, number>>, options?: { - value?: UIEventSource<string> - }) { +import { InputElement } from "./InputElement" +import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import Slider from "./Slider" +import { ClickableToggle } from "./Toggle" +import { FixedUiElement } from "../Base/FixedUiElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import BaseUIElement from "../BaseUIElement" +export default class FloorLevelInputElement + extends VariableUiElement + implements InputElement<string> +{ + private readonly _value: UIEventSource<string> + constructor( + currentLevels: Store<Record<string, number>>, + options?: { + value?: UIEventSource<string> + } + ) { const value = options?.value ?? new UIEventSource<string>("0") - super(currentLevels.map(levels => { + super( + currentLevels.map((levels) => { const allLevels = Object.keys(levels) allLevels.sort((a, b) => { const an = Number(a) const bn = Number(b) if (isNaN(an) || isNaN(bn)) { - return a < b ? -1 : 1; + return a < b ? -1 : 1 } - return an - bn; + return an - bn }) return FloorLevelInputElement.constructPicker(allLevels, value) - } - )) - + }) + ) this._value = value - } private static constructPicker(levels: string[], value: UIEventSource<string>): BaseUIElement { - let slider = new Slider(0, levels.length - 1, {vertical: true}); - const toggleClass = "flex border-2 border-blue-500 w-10 h-10 place-content-center items-center border-box" - slider.SetClass("flex elevator w-10").SetStyle(`height: ${2.5 * levels.length}rem; background: #00000000`) + let slider = new Slider(0, levels.length - 1, { vertical: true }) + const toggleClass = + "flex border-2 border-blue-500 w-10 h-10 place-content-center items-center border-box" + slider + .SetClass("flex elevator w-10") + .SetStyle(`height: ${2.5 * levels.length}rem; background: #00000000`) - const values = levels.map((data, i) => new ClickableToggle( - new FixedUiElement(data).SetClass("font-bold active bg-subtle " + toggleClass), - new FixedUiElement(data).SetClass("normal-background " + toggleClass), - slider.GetValue().sync( - (sliderVal) => { - return sliderVal === i - }, - [], - (isSelected) => { - return isSelected ? i : slider.GetValue().data - } - )) - .ToggleOnClick() - .SetClass("flex w-10 h-10")) + const values = levels.map((data, i) => + new ClickableToggle( + new FixedUiElement(data).SetClass("font-bold active bg-subtle " + toggleClass), + new FixedUiElement(data).SetClass("normal-background " + toggleClass), + slider.GetValue().sync( + (sliderVal) => { + return sliderVal === i + }, + [], + (isSelected) => { + return isSelected ? i : slider.GetValue().data + } + ) + ) + .ToggleOnClick() + .SetClass("flex w-10 h-10") + ) values.reverse(/* This is a new list, no side-effects */) const combine = new Combine([new Combine(values), slider]) - combine.SetClass("flex flex-row overflow-hidden"); + combine.SetClass("flex flex-row overflow-hidden") - - slider.GetValue().addCallbackD(i => { + slider.GetValue().addCallbackD((i) => { if (levels === undefined) { return } - if(levels[i] == undefined){ + if (levels[i] == undefined) { return } - value.setData(levels[i]); + value.setData(levels[i]) }) - value.addCallbackAndRunD(level => { - const i = levels.findIndex(l => l === level) + value.addCallbackAndRunD((level) => { + const i = levels.findIndex((l) => l === level) slider.GetValue().setData(i) }) return combine } GetValue(): UIEventSource<string> { - return this._value; + return this._value } IsValid(t: string): boolean { - return false; + return false } - - -} \ No newline at end of file +} diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index 46ec456f3..cbb3a1aa2 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -1,13 +1,12 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" -export interface ReadonlyInputElement<T> extends BaseUIElement{ - GetValue(): Store<T>; - IsValid(t: T): boolean; +export interface ReadonlyInputElement<T> extends BaseUIElement { + GetValue(): Store<T> + IsValid(t: T): boolean } - -export abstract class InputElement<T> extends BaseUIElement implements ReadonlyInputElement<any>{ - abstract GetValue(): UIEventSource<T>; - abstract IsValid(t: T): boolean; +export abstract class InputElement<T> extends BaseUIElement implements ReadonlyInputElement<any> { + abstract GetValue(): UIEventSource<T> + abstract IsValid(t: T): boolean } diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index 0f0d74d0c..5ca998baf 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -1,56 +1,58 @@ -import {InputElement} from "./InputElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; - +import { InputElement } from "./InputElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" export default class InputElementMap<T, X> extends InputElement<X> { - private readonly _inputElement: InputElement<T>; - private isSame: (x0: X, x1: X) => boolean; - private readonly fromX: (x: X) => T; - private readonly toX: (t: T) => X; - private readonly _value: UIEventSource<X>; + private readonly _inputElement: InputElement<T> + private isSame: (x0: X, x1: X) => boolean + private readonly fromX: (x: X) => T + private readonly toX: (t: T) => X + private readonly _value: UIEventSource<X> - constructor(inputElement: InputElement<T>, - isSame: (x0: X, x1: X) => boolean, - toX: (t: T) => X, - fromX: (x: X) => T, - extraSources: Store<any>[] = [] + constructor( + inputElement: InputElement<T>, + isSame: (x0: X, x1: X) => boolean, + toX: (t: T) => X, + fromX: (x: X) => T, + extraSources: Store<any>[] = [] ) { - super(); - this.isSame = isSame; - this.fromX = fromX; - this.toX = toX; - this._inputElement = inputElement; - const self = this; + super() + this.isSame = isSame + this.fromX = fromX + this.toX = toX + this._inputElement = inputElement + const self = this this._value = inputElement.GetValue().sync( - (t => { - const newX = toX(t); - const currentX = self.GetValue()?.data; + (t) => { + const newX = toX(t) + const currentX = self.GetValue()?.data if (isSame(currentX, newX)) { - return currentX; + return currentX } - return newX; - }), extraSources, x => { - return fromX(x); - }); + return newX + }, + extraSources, + (x) => { + return fromX(x) + } + ) } GetValue(): UIEventSource<X> { - return this._value; + return this._value } IsValid(x: X): boolean { if (x === undefined) { - return false; + return false } - const t = this.fromX(x); + const t = this.fromX(x) if (t === undefined) { - return false; + return false } - return this._inputElement.IsValid(t); + return this._inputElement.IsValid(t) } protected InnerConstructElement(): HTMLElement { - return this._inputElement.ConstructElement(); + return this._inputElement.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Input/InputElementWrapper.ts b/UI/Input/InputElementWrapper.ts index 623fc18d5..99fa3309e 100644 --- a/UI/Input/InputElementWrapper.ts +++ b/UI/Input/InputElementWrapper.ts @@ -1,37 +1,43 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import {Translation} from "../i18n/Translation"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import { Translation } from "../i18n/Translation" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" export default class InputElementWrapper<T> extends InputElement<T> { - private readonly _inputElement: InputElement<T>; + private readonly _inputElement: InputElement<T> private readonly _renderElement: BaseUIElement - constructor(inputElement: InputElement<T>, translation: Translation, key: string, - tags: UIEventSource<any>, state: FeaturePipelineState) { + constructor( + inputElement: InputElement<T>, + translation: Translation, + key: string, + tags: UIEventSource<any>, + state: FeaturePipelineState + ) { super() - this._inputElement = inputElement; + this._inputElement = inputElement const mapping = new Map<string, BaseUIElement>() mapping.set(key, inputElement) // Bit of a hack: the SubstitutedTranslation expects a special rendering, but those are formatted '{key()}' instead of '{key}', so we substitute it first - translation = translation.OnEveryLanguage((txt) => txt.replace("{" + key + "}", "{" + key + "()}")) + translation = translation.OnEveryLanguage((txt) => + txt.replace("{" + key + "}", "{" + key + "()}") + ) this._renderElement = new SubstitutedTranslation(translation, tags, state, mapping) } GetValue(): UIEventSource<T> { - return this._inputElement.GetValue(); + return this._inputElement.GetValue() } IsValid(t: T): boolean { - return this._inputElement.IsValid(t); + return this._inputElement.IsValid(t) } protected InnerConstructElement(): HTMLElement { - return this._renderElement.ConstructElement(); + return this._renderElement.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index ad8b07180..21d813681 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -1,47 +1,46 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; -import Loc from "../../Models/Loc"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import Minimap, {MinimapObj} from "../Base/Minimap"; -import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; -import BaseUIElement from "../BaseUIElement"; - +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import { Utils } from "../../Utils" +import Loc from "../../Models/Loc" +import { GeoOperations } from "../../Logic/GeoOperations" +import Minimap, { MinimapObj } from "../Base/Minimap" +import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" +import BaseUIElement from "../BaseUIElement" /** * Selects a length after clicking on the minimap, in meters */ export default class LengthInput extends InputElement<string> { - - private readonly _location: UIEventSource<Loc>; - private readonly value: UIEventSource<string>; - private readonly background: UIEventSource<any>; + private readonly _location: UIEventSource<Loc> + private readonly value: UIEventSource<string> + private readonly background: UIEventSource<any> - constructor(mapBackground: UIEventSource<any>, - location: UIEventSource<Loc>, - value?: UIEventSource<string>) { - super(); - this._location = location; - this.value = value ?? new UIEventSource<string>(undefined); - this.background = mapBackground; + constructor( + mapBackground: UIEventSource<any>, + location: UIEventSource<Loc>, + value?: UIEventSource<string> + ) { + super() + this._location = location + this.value = value ?? new UIEventSource<string>(undefined) + this.background = mapBackground this.SetClass("block") - } GetValue(): UIEventSource<string> { - return this.value; + return this.value } IsValid(str: string): boolean { const t = Number(str) - return !isNaN(t) && t >= 0; + return !isNaN(t) && t >= 0 } protected InnerConstructElement(): HTMLElement { - let map : (BaseUIElement & MinimapObj) = undefined - let layerControl : BaseUIElement = undefined + let map: BaseUIElement & MinimapObj = undefined + let layerControl: BaseUIElement = undefined if (!Utils.runningFromConsole) { map = Minimap.createMiniMap({ background: this.background, @@ -49,139 +48,157 @@ export default class LengthInput extends InputElement<string> { location: this._location, attribution: true, leafletOptions: { - tap: true + tap: true, + }, + }) + + layerControl = new BackgroundMapSwitch( + { + locationControl: this._location, + backgroundLayer: this.background, + }, + this.background, + { + allowedCategories: ["map", "photo"], } - }) - - layerControl = new BackgroundMapSwitch({ - locationControl: this._location, - backgroundLayer: this.background, - }, this.background,{ - allowedCategories: ["map","photo"] - }) - + ) } - const crosshair = new Combine([Svg.length_crosshair_svg().SetStyle( - `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) - ]) .SetClass("block length-crosshair-svg relative pointer-events-none") + const crosshair = new Combine([ + Svg.length_crosshair_svg().SetStyle( + `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` + ), + ]) + .SetClass("block length-crosshair-svg relative pointer-events-none") .SetStyle("z-index: 1000; visibility: hidden") - + const element = new Combine([ crosshair, - layerControl?.SetStyle("position: absolute; bottom: 0.25rem; left: 0.25rem; z-index: 1000"), + layerControl?.SetStyle( + "position: absolute; bottom: 0.25rem; left: 0.25rem; z-index: 1000" + ), map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), ]) .SetClass("relative block bg-white border border-black rounded-xl overflow-hidden") .ConstructElement() - - this.RegisterTriggers(map?.ConstructElement(), map?.leafletMap, crosshair.ConstructElement()) + this.RegisterTriggers( + map?.ConstructElement(), + map?.leafletMap, + crosshair.ConstructElement() + ) element.style.overflow = "hidden" element.style.display = "block" return element } - private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource<L.Map>, measurementCrosshair: HTMLElement) { - + private RegisterTriggers( + htmlElement: HTMLElement, + leafletMap: UIEventSource<L.Map>, + measurementCrosshair: HTMLElement + ) { let firstClickXY: [number, number] = undefined let lastClickXY: [number, number] = undefined - const self = this; - + const self = this function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) { if (x === undefined || y === undefined) { // Touch end - firstClickXY = undefined; - lastClickXY = undefined; - return; + firstClickXY = undefined + lastClickXY = undefined + return } - const rect = htmlElement.getBoundingClientRect(); + const rect = htmlElement.getBoundingClientRect() // From the central part of location - const dx = x - rect.left; - const dy = y - rect.top; + const dx = x - rect.left + const dy = y - rect.top if (isDown) { if (lastClickXY === undefined && firstClickXY === undefined) { - firstClickXY = [dx, dy]; + firstClickXY = [dx, dy] } else if (firstClickXY !== undefined && lastClickXY === undefined) { lastClickXY = [dx, dy] } else if (firstClickXY !== undefined && lastClickXY !== undefined) { // we measure again firstClickXY = [dx, dy] - lastClickXY = undefined; + lastClickXY = undefined } } if (firstClickXY === undefined) { measurementCrosshair.style.visibility = "hidden" - return; + return } - - - const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) + const distance = Math.sqrt( + (dy - firstClickXY[1]) * (dy - firstClickXY[1]) + + (dx - firstClickXY[0]) * (dx - firstClickXY[0]) + ) if (isUp) { if (distance > 15) { lastClickXY = [dx, dy] } } else if (lastClickXY !== undefined) { - return; + return } measurementCrosshair.style.visibility = "unset" - measurementCrosshair.style.left = firstClickXY[0] + "px"; + measurementCrosshair.style.left = firstClickXY[0] + "px" measurementCrosshair.style.top = firstClickXY[1] + "px" - const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI; + const angle = (180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx)) / Math.PI const angleGeo = (angle + 270) % 360 - const measurementCrosshairInner: HTMLElement = <HTMLElement>measurementCrosshair.firstChild - measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`; + const measurementCrosshairInner: HTMLElement = <HTMLElement>( + measurementCrosshair.firstChild + ) + measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)` - measurementCrosshairInner.style.width = (distance * 2) + "px" + measurementCrosshairInner.style.width = distance * 2 + "px" measurementCrosshairInner.style.marginLeft = -distance + "px" measurementCrosshairInner.style.marginTop = -distance + "px" - const leaflet = leafletMap?.data if (leaflet) { const first = leaflet.layerPointToLatLng(firstClickXY) const last = leaflet.layerPointToLatLng([dx, dy]) - const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 10) / 10 + const geoDist = + Math.floor( + GeoOperations.distanceBetween( + [first.lng, first.lat], + [last.lng, last.lat] + ) * 10 + ) / 10 self.value.setData("" + geoDist) } - } - htmlElement.ontouchstart = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); - ev.preventDefault(); + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true) + ev.preventDefault() } htmlElement.ontouchmove = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); - ev.preventDefault(); + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false) + ev.preventDefault() } htmlElement.ontouchend = (ev: TouchEvent) => { - onPosChange(undefined, undefined, false, true); - ev.preventDefault(); + onPosChange(undefined, undefined, false, true) + ev.preventDefault() } htmlElement.onmousedown = (ev: MouseEvent) => { - onPosChange(ev.clientX, ev.clientY, true); - ev.preventDefault(); + onPosChange(ev.clientX, ev.clientY, true) + ev.preventDefault() } htmlElement.onmouseup = (ev) => { - onPosChange(ev.clientX, ev.clientY, false, true); - ev.preventDefault(); + onPosChange(ev.clientX, ev.clientY, false, true) + ev.preventDefault() } htmlElement.onmousemove = (ev: MouseEvent) => { - onPosChange(ev.clientX, ev.clientY, false); - ev.preventDefault(); + onPosChange(ev.clientX, ev.clientY, false) + ev.preventDefault() } } - -} \ No newline at end of file +} diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 60a03e677..1d7025053 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -1,33 +1,39 @@ -import {ReadonlyInputElement} from "./InputElement"; -import Loc from "../../Models/Loc"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Minimap, {MinimapObj} from "../Base/Minimap"; -import BaseLayer from "../../Models/BaseLayer"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import State from "../../State"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {BBox} from "../../Logic/BBox"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "./Toggle"; +import { ReadonlyInputElement } from "./InputElement" +import Loc from "../../Models/Loc" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Minimap, { MinimapObj } from "../Base/Minimap" +import BaseLayer from "../../Models/BaseLayer" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import State from "../../State" +import { GeoOperations } from "../../Logic/GeoOperations" +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { BBox } from "../../Logic/BBox" +import { FixedUiElement } from "../Base/FixedUiElement" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import BaseUIElement from "../BaseUIElement" +import Toggle from "./Toggle" import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" -export default class LocationInput extends BaseUIElement implements ReadonlyInputElement<Loc>, MinimapObj { - - private static readonly matchLayer = new LayerConfig(matchpoint, "LocationInput.matchpoint", true) +export default class LocationInput + extends BaseUIElement + implements ReadonlyInputElement<Loc>, MinimapObj +{ + private static readonly matchLayer = new LayerConfig( + matchpoint, + "LocationInput.matchpoint", + true + ) public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined) - public readonly _matching_layer: LayerConfig; + public readonly _matching_layer: LayerConfig public readonly leafletMap: UIEventSource<any> - public readonly bounds; - public readonly location; - private _centerLocation: UIEventSource<Loc>; - private readonly mapBackground: UIEventSource<BaseLayer>; + public readonly bounds + public readonly location + private _centerLocation: UIEventSource<Loc> + private readonly mapBackground: UIEventSource<BaseLayer> /** * The features to which the input should be snapped * @private @@ -36,33 +42,35 @@ export default class LocationInput extends BaseUIElement implements ReadonlyInpu private readonly _value: Store<Loc> private readonly _snappedPoint: Store<any> private readonly _maxSnapDistance: number - private readonly _snappedPointTags: any; - private readonly _bounds: UIEventSource<BBox>; - private readonly map: BaseUIElement & MinimapObj; - private readonly clickLocation: UIEventSource<Loc>; - private readonly _minZoom: number; + private readonly _snappedPointTags: any + private readonly _bounds: UIEventSource<BBox> + private readonly map: BaseUIElement & MinimapObj + private readonly clickLocation: UIEventSource<Loc> + private readonly _minZoom: number constructor(options: { - minZoom?: number, - mapBackground?: UIEventSource<BaseLayer>, - snapTo?: UIEventSource<{ feature: any }[]>, - maxSnapDistance?: number, - snappedPointTags?: any, - requiresSnapping?: boolean, - centerLocation: UIEventSource<Loc>, + minZoom?: number + mapBackground?: UIEventSource<BaseLayer> + snapTo?: UIEventSource<{ feature: any }[]> + maxSnapDistance?: number + snappedPointTags?: any + requiresSnapping?: boolean + centerLocation: UIEventSource<Loc> bounds?: UIEventSource<BBox> }) { - super(); - this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point")) + super() + this._snapTo = options.snapTo?.map((features) => + features?.filter((feat) => feat.feature.geometry.type !== "Point") + ) this._maxSnapDistance = options.maxSnapDistance - this._centerLocation = options.centerLocation; + this._centerLocation = options.centerLocation this._snappedPointTags = options.snappedPointTags - this._bounds = options.bounds; + this._bounds = options.bounds this._minZoom = options.minZoom if (this._snapTo === undefined) { - this._value = this._centerLocation; + this._value = this._centerLocation } else { - const self = this; + const self = this if (self._snappedPointTags !== undefined) { const layout = State.state.layoutToUse @@ -73,89 +81,96 @@ export default class LocationInput extends BaseUIElement implements ReadonlyInpu matchingLayer = layer } } - this._matching_layer = matchingLayer; + this._matching_layer = matchingLayer } else { this._matching_layer = LocationInput.matchLayer } - this._snappedPoint = options.centerLocation.map(loc => { - if (loc === undefined) { - return undefined; - } - - // We reproject the location onto every 'snap-to-feature' and select the closest - - let min = undefined; - let matchedWay = undefined; - for (const feature of self._snapTo.data ?? []) { - try { - - const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat]) - if (min === undefined) { - min = nearestPointOnLine - matchedWay = feature.feature; - continue; - } - - if (min.properties.dist > nearestPointOnLine.properties.dist) { - min = nearestPointOnLine - matchedWay = feature.feature; - - } - } catch (e) { - console.log("Snapping to a nearest point failed for ", feature.feature, "due to ", e) - } - } - - if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { - if (options.requiresSnapping) { + this._snappedPoint = options.centerLocation.map( + (loc) => { + if (loc === undefined) { return undefined - } else { - return { - "type": "Feature", - "properties": options.snappedPointTags ?? min.properties, - "geometry": {"type": "Point", "coordinates": [loc.lon, loc.lat]} + } + + // We reproject the location onto every 'snap-to-feature' and select the closest + + let min = undefined + let matchedWay = undefined + for (const feature of self._snapTo.data ?? []) { + try { + const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [ + loc.lon, + loc.lat, + ]) + if (min === undefined) { + min = nearestPointOnLine + matchedWay = feature.feature + continue + } + + if (min.properties.dist > nearestPointOnLine.properties.dist) { + min = nearestPointOnLine + matchedWay = feature.feature + } + } catch (e) { + console.log( + "Snapping to a nearest point failed for ", + feature.feature, + "due to ", + e + ) } } - } - min.properties = options.snappedPointTags ?? min.properties - self.snappedOnto.setData(matchedWay) - return min - }, [this._snapTo]) - this._value = this._snappedPoint.map(f => { - const [lon, lat] = f.geometry.coordinates; + if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { + if (options.requiresSnapping) { + return undefined + } else { + return { + type: "Feature", + properties: options.snappedPointTags ?? min.properties, + geometry: { type: "Point", coordinates: [loc.lon, loc.lat] }, + } + } + } + min.properties = options.snappedPointTags ?? min.properties + self.snappedOnto.setData(matchedWay) + return min + }, + [this._snapTo] + ) + + this._value = this._snappedPoint.map((f) => { + const [lon, lat] = f.geometry.coordinates return { - lon: lon, lat: lat, zoom: undefined + lon: lon, + lat: lat, + zoom: undefined, } }) - } this.mapBackground = options.mapBackground ?? State.state?.backgroundLayer this.SetClass("block h-full") - - this.clickLocation = new UIEventSource<Loc>(undefined); - this.map = Minimap.createMiniMap( - { - location: this._centerLocation, - background: this.mapBackground, - attribution: this.mapBackground !== State.state?.backgroundLayer, - lastClickLocation: this.clickLocation, - bounds: this._bounds, - addLayerControl: true - } - ) + this.clickLocation = new UIEventSource<Loc>(undefined) + this.map = Minimap.createMiniMap({ + location: this._centerLocation, + background: this.mapBackground, + attribution: this.mapBackground !== State.state?.backgroundLayer, + lastClickLocation: this.clickLocation, + bounds: this._bounds, + addLayerControl: true, + }) this.leafletMap = this.map.leafletMap - this.location = this.map.location; + this.location = this.map.location } GetValue(): Store<Loc> { - return this._value; + return this._value } IsValid(t: Loc): boolean { - return t !== undefined; + return t !== undefined } installBounds(factor: number | BBox, showRange?: boolean): void { @@ -168,37 +183,39 @@ export default class LocationInput extends BaseUIElement implements ReadonlyInpu protected InnerConstructElement(): HTMLElement { try { - const self = this; + const self = this const hasMoved = new UIEventSource(false) - const startLocation = {...this._centerLocation.data} - this._centerLocation.addCallbackD(newLocation => { + const startLocation = { ...this._centerLocation.data } + this._centerLocation.addCallbackD((newLocation) => { const f = 100000 console.log(newLocation.lon, startLocation.lon) - const diff = (Math.abs(newLocation.lon * f - startLocation.lon * f) + Math.abs(newLocation.lat * f - startLocation.lat * f)) + const diff = + Math.abs(newLocation.lon * f - startLocation.lon * f) + + Math.abs(newLocation.lat * f - startLocation.lat * f) if (diff < 1) { - return; + return } hasMoved.setData(true) - return true; + return true }) - this.clickLocation.addCallbackAndRunD(location => this._centerLocation.setData(location)) + this.clickLocation.addCallbackAndRunD((location) => + this._centerLocation.setData(location) + ) if (this._snapTo !== undefined) { - // Show the lines to snap to console.log("Constructing the snap-to layer", this._snapTo) new ShowDataMultiLayer({ - features: StaticFeatureSource.fromDateless(this._snapTo), - zoomToFeatures: false, - leafletMap: this.map.leafletMap, - layers: State.state.filteredLayers - } - ) + features: StaticFeatureSource.fromDateless(this._snapTo), + zoomToFeatures: false, + leafletMap: this.map.leafletMap, + layers: State.state.filteredLayers, + }) // Show the central point - const matchPoint = this._snappedPoint.map(loc => { + const matchPoint = this._snappedPoint.map((loc) => { if (loc === undefined) { return [] } - return [{feature: loc}]; + return [{ feature: loc }] }) console.log("Constructing the match layer", matchPoint) @@ -208,21 +225,22 @@ export default class LocationInput extends BaseUIElement implements ReadonlyInpu leafletMap: this.map.leafletMap, layerToShow: this._matching_layer, state: State.state, - selectedElement: State.state.selectedElement + selectedElement: State.state.selectedElement, }) - } - this.mapBackground.map(layer => { - const leaflet = this.map.leafletMap.data - if (leaflet === undefined || layer === undefined) { - return; - } + this.mapBackground.map( + (layer) => { + const leaflet = this.map.leafletMap.data + if (leaflet === undefined || layer === undefined) { + return + } - leaflet.setMaxZoom(layer.max_zoom) - leaflet.setMinZoom(self._minZoom ?? layer.max_zoom - 2) - leaflet.setZoom(layer.max_zoom - 1) - - }, [this.map.leafletMap]) + leaflet.setMaxZoom(layer.max_zoom) + leaflet.setMinZoom(self._minZoom ?? layer.max_zoom - 2) + leaflet.setZoom(layer.max_zoom - 1) + }, + [this.map.leafletMap] + ) const animatedHand = Svg.hand_ui() .SetStyle("width: 2rem; height: unset;") @@ -232,23 +250,24 @@ export default class LocationInput extends BaseUIElement implements ReadonlyInpu new Combine([ Svg.move_arrows_ui() .SetClass("block relative pointer-events-none") - .SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem") - ]).SetClass("block w-0 h-0 z-10 relative") - .SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5"), + .SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem"), + ]) + .SetClass("block w-0 h-0 z-10 relative") + .SetStyle( + "background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5" + ), - new Toggle(undefined, - animatedHand, hasMoved) + new Toggle(undefined, animatedHand, hasMoved) .SetClass("block w-0 h-0 z-10 relative") .SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"), - this.map - .SetClass("z-0 relative block w-full h-full bg-gray-100") - - ]).ConstructElement(); + this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"), + ]).ConstructElement() } catch (e) { console.error("Could not generate LocationInputElement:", e) - return new FixedUiElement("Constructing a locationInput failed due to" + e).SetClass("alert").ConstructElement(); + return new FixedUiElement("Constructing a locationInput failed due to" + e) + .SetClass("alert") + .ConstructElement() } } - -} \ No newline at end of file +} diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 9c2145101..03adaad69 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -1,178 +1,157 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" +import { Utils } from "../../Utils" export class RadioButton<T> extends InputElement<T> { - private static _nextId = 0; - - private readonly value: UIEventSource<T>; - private _elements: InputElement<T>[]; - private _selectFirstAsDefault: boolean; + private static _nextId = 0 + + private readonly value: UIEventSource<T> + private _elements: InputElement<T>[] + private _selectFirstAsDefault: boolean private _dontStyle: boolean constructor( elements: InputElement<T>[], options?: { - selectFirstAsDefault?: true | boolean, + selectFirstAsDefault?: true | boolean dontStyle?: boolean } ) { - super(); + super() options = options ?? {} - this._selectFirstAsDefault = options.selectFirstAsDefault ?? true; - this._elements = Utils.NoNull(elements); - this.value = new UIEventSource<T>(undefined); + this._selectFirstAsDefault = options.selectFirstAsDefault ?? true + this._elements = Utils.NoNull(elements) + this.value = new UIEventSource<T>(undefined) this._dontStyle = options.dontStyle ?? false } IsValid(t: T): boolean { for (const inputElement of this._elements) { if (inputElement.IsValid(t)) { - return true; + return true } } - return false; + return false } GetValue(): UIEventSource<T> { - return this.value; + return this.value } protected InnerConstructElement(): HTMLElement { - const elements = this._elements; - const selectFirstAsDefault = this._selectFirstAsDefault; + const elements = this._elements + const selectFirstAsDefault = this._selectFirstAsDefault - const selectedElementIndex: UIEventSource<number> = - new UIEventSource<number>(null); + const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(null) const value = UIEventSource.flatten( selectedElementIndex.map((selectedIndex) => { if (selectedIndex !== undefined && selectedIndex !== null) { - return elements[selectedIndex].GetValue(); + return elements[selectedIndex].GetValue() } }), elements.map((e) => e?.GetValue()) - ); - value.syncWith(this.value); + ) + value.syncWith(this.value) if (selectFirstAsDefault) { value.addCallbackAndRun((selected) => { if (selected === undefined) { for (const element of elements) { - const v = element.GetValue().data; + const v = element.GetValue().data if (v !== undefined) { - value.setData(v); - break; + value.setData(v) + break } } } - }); + }) } for (let i = 0; i < elements.length; i++) { // If an element is clicked, the radio button corresponding with it should be selected as well elements[i]?.onClick(() => { - selectedElementIndex.setData(i); - }); + selectedElementIndex.setData(i) + }) elements[i].GetValue().addCallback(() => { - selectedElementIndex.setData(i); - }); + selectedElementIndex.setData(i) + }) } - const groupId = "radiogroup" + RadioButton._nextId; - RadioButton._nextId++; + const groupId = "radiogroup" + RadioButton._nextId + RadioButton._nextId++ - const form = document.createElement("form"); + const form = document.createElement("form") - const inputs = []; - const wrappers: HTMLElement[] = []; + const inputs = [] + const wrappers: HTMLElement[] = [] for (let i1 = 0; i1 < elements.length; i1++) { - let element = elements[i1]; - const labelHtml = element.ConstructElement(); + let element = elements[i1] + const labelHtml = element.ConstructElement() if (labelHtml === undefined) { - continue; + continue } - const input = document.createElement("input"); - input.id = "radio" + groupId + "-" + i1; - input.name = groupId; - input.type = "radio"; - input.classList.add( - "cursor-pointer", - "p-1", - "mr-2" - ); - + const input = document.createElement("input") + input.id = "radio" + groupId + "-" + i1 + input.name = groupId + input.type = "radio" + input.classList.add("cursor-pointer", "p-1", "mr-2") if (!this._dontStyle) { - input.classList.add( - "p-1", - "ml-2", - "pl-2", - "pr-0", - "m-3", - "mr-0" - ); + input.classList.add("p-1", "ml-2", "pl-2", "pr-0", "m-3", "mr-0") } input.onchange = () => { if (input.checked) { - selectedElementIndex.setData(i1); + selectedElementIndex.setData(i1) } - }; + } - inputs.push(input); + inputs.push(input) - const label = document.createElement("label"); - label.appendChild(labelHtml); - label.htmlFor = input.id; - label.classList.add("flex", "w-full", "cursor-pointer", "bg-red"); + const label = document.createElement("label") + label.appendChild(labelHtml) + label.htmlFor = input.id + label.classList.add("flex", "w-full", "cursor-pointer", "bg-red") if (!this._dontStyle) { labelHtml.classList.add("p-2") } - const block = document.createElement("div"); - block.appendChild(input); - block.appendChild(label); - block.classList.add( - "flex", - "w-full", - ); + const block = document.createElement("div") + block.appendChild(input) + block.appendChild(label) + block.classList.add("flex", "w-full") if (!this._dontStyle) { - block.classList.add( - "m-1", - "border", - "border-gray-400" - ) + block.classList.add("m-1", "border", "border-gray-400") } block.style.borderRadius = "1.5rem" - wrappers.push(block); + wrappers.push(block) - form.appendChild(block); + form.appendChild(block) } - value.addCallbackAndRun((selected:T) => { - let somethingChecked = false; + value.addCallbackAndRun((selected: T) => { + let somethingChecked = false for (let i = 0; i < inputs.length; i++) { - let input = inputs[i]; - input.checked = !somethingChecked && elements[i].IsValid(selected); - somethingChecked = somethingChecked || input.checked; + let input = inputs[i] + input.checked = !somethingChecked && elements[i].IsValid(selected) + somethingChecked = somethingChecked || input.checked if (input.checked) { - wrappers[i].classList.remove("border-gray-400"); - wrappers[i].classList.add("border-attention"); + wrappers[i].classList.remove("border-gray-400") + wrappers[i].classList.add("border-attention") } else { - wrappers[i].classList.add("border-gray-400"); - wrappers[i].classList.remove("border-attention"); + wrappers[i].classList.add("border-gray-400") + wrappers[i].classList.remove("border-attention") } } - }); + }) - this.SetClass("flex flex-col"); + this.SetClass("flex flex-col") - return form; + return form } - } diff --git a/UI/Input/SearchableMappingsSelector.ts b/UI/Input/SearchableMappingsSelector.ts index e1f639203..e10fe42ab 100644 --- a/UI/Input/SearchableMappingsSelector.ts +++ b/UI/Input/SearchableMappingsSelector.ts @@ -1,39 +1,38 @@ -import {UIElement} from "../UIElement"; -import {InputElement} from "./InputElement"; -import BaseUIElement from "../BaseUIElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import Locale from "../i18n/Locale"; -import Combine from "../Base/Combine"; -import {TextField} from "./TextField"; -import Svg from "../../Svg"; -import {VariableUiElement} from "../Base/VariableUIElement"; - +import { UIElement } from "../UIElement" +import { InputElement } from "./InputElement" +import BaseUIElement from "../BaseUIElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import Locale from "../i18n/Locale" +import Combine from "../Base/Combine" +import { TextField } from "./TextField" +import Svg from "../../Svg" +import { VariableUiElement } from "../Base/VariableUIElement" /** * A single 'pill' which can hide itself if the search criteria is not met */ class SelfHidingToggle extends UIElement implements InputElement<boolean> { - private readonly _shown: BaseUIElement; + private readonly _shown: BaseUIElement public readonly _selected: UIEventSource<boolean> - public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true); + public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true) public readonly forceSelected: UIEventSource<boolean> - private readonly _squared: boolean; + private readonly _squared: boolean public constructor( shown: string | BaseUIElement, mainTerm: Record<string, string>, search: Store<string>, options?: { - searchTerms?: Record<string, string[]>, - selected?: UIEventSource<boolean>, - forceSelected?: UIEventSource<boolean>, + searchTerms?: Record<string, string[]> + selected?: UIEventSource<boolean> + forceSelected?: UIEventSource<boolean> squared?: boolean } ) { - super(); - this._shown = Translations.W(shown); - this._squared = options?.squared ?? false; - const searchTerms: Record<string, string[]> = {}; + super() + this._shown = Translations.W(shown) + this._squared = options?.squared ?? false + const searchTerms: Record<string, string[]> = {} for (const lng in options?.searchTerms ?? []) { if (lng === "_context") { continue @@ -44,30 +43,34 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> { if (lng === "_context") { continue } - const main = SelfHidingToggle.clean( mainTerm[lng]) + const main = SelfHidingToggle.clean(mainTerm[lng]) searchTerms[lng] = [main].concat(searchTerms[lng] ?? []) } - const selected = this._selected = options?.selected ?? new UIEventSource<boolean>(false); - const forceSelected = this.forceSelected = options?.forceSelected ?? new UIEventSource<boolean>(false) - this.isShown = search.map(s => { - if (s === undefined || s.length === 0) { - return true; - } - if (selected.data && !forceSelected.data) { - return true - } - s = s?.trim()?.toLowerCase() - if(searchTerms[Locale.language.data]?.some(t => t.indexOf(s) >= 0)){ - return true - } - if(searchTerms["*"]?.some(t => t.indexOf(s) >= 0)){ - return true - } - return false; - }, [selected, Locale.language]) + const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false)) + const forceSelected = (this.forceSelected = + options?.forceSelected ?? new UIEventSource<boolean>(false)) + this.isShown = search.map( + (s) => { + if (s === undefined || s.length === 0) { + return true + } + if (selected.data && !forceSelected.data) { + return true + } + s = s?.trim()?.toLowerCase() + if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) { + return true + } + if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) { + return true + } + return false + }, + [selected, Locale.language] + ) - const self = this; - this.isShown.addCallbackAndRun(shown => { + const self = this + this.isShown.addCallbackAndRun((shown) => { if (shown) { self.RemoveClass("hidden") } else { @@ -75,25 +78,24 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> { } }) } - - private static clean(s: string) : string{ + + private static clean(s: string): string { return s?.trim()?.toLowerCase()?.replace(/[-]/, "") } - GetValue(): UIEventSource<boolean> { return this._selected } IsValid(t: boolean): boolean { - return true; + return true } protected InnerRender(): string | BaseUIElement { - let el: BaseUIElement = this._shown; - const selected = this._selected; + let el: BaseUIElement = this._shown + const selected = this._selected - selected.addCallbackAndRun(selected => { + selected.addCallbackAndRun((selected) => { if (selected) { el.SetClass("border-4") el.RemoveClass("border") @@ -107,77 +109,88 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> { const forcedSelection = this.forceSelected el.onClick(() => { - if(forcedSelection.data){ + if (forcedSelection.data) { selected.setData(true) - }else{ - selected.setData(!selected.data); + } else { + selected.setData(!selected.data) } }) - if(!this._squared){ + if (!this._squared) { el.SetClass("rounded-full") } return el.SetClass("border border-black p-1 px-4") } } - /** * The searchable mappings selector is a selector which shows various pills from which one (or more) options can be chosen. * A searchfield can be used to filter the values */ export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> { - private readonly selectedElements: UIEventSource<T[]>; + private readonly selectedElements: UIEventSource<T[]> - public readonly someMatchFound: Store<boolean>; + public readonly someMatchFound: Store<boolean> /** - * + * * @param values * @param options */ constructor( - values: { show: BaseUIElement, value: T, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]> }[], + values: { + show: BaseUIElement + value: T + mainTerm: Record<string, string> + searchTerms?: Record<string, string[]> + }[], options?: { - mode?: "select-one" | "select-many", - selectedElements?: UIEventSource<T[]>, - searchValue?: UIEventSource<string>, - onNoMatches?: BaseUIElement, - onNoSearchMade?: BaseUIElement, + mode?: "select-one" | "select-many" + selectedElements?: UIEventSource<T[]> + searchValue?: UIEventSource<string> + onNoMatches?: BaseUIElement + onNoSearchMade?: BaseUIElement /** * Shows this if there are many (>200) possible mappings */ - onManyElements?: BaseUIElement, - onManyElementsValue?: UIEventSource<T[]>, - selectIfSingle?: false | boolean, - searchAreaClass?: string, + onManyElements?: BaseUIElement + onManyElementsValue?: UIEventSource<T[]> + selectIfSingle?: false | boolean + searchAreaClass?: string hideSearchBar?: false | boolean - }) { + } + ) { + const search = new TextField({ value: options?.searchValue }) - const search = new TextField({value: options?.searchValue}) + const searchBar = options?.hideSearchBar + ? undefined + : new Combine([ + Svg.search_svg().SetClass("w-8 normal-background"), + search.SetClass("w-full"), + ]).SetClass("flex items-center border-2 border-black m-2") - const searchBar = options?.hideSearchBar ? undefined : new Combine([Svg.search_svg().SetClass("w-8 normal-background"), search.SetClass("w-full")]) - .SetClass("flex items-center border-2 border-black m-2") - - const searchValue = search.GetValue().map(s => s?.trim()?.toLowerCase()) - const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([]); - const mode = options?.mode ?? "select-one"; + const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase()) + const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([]) + const mode = options?.mode ?? "select-one" const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping - const mappedValues: { show: SelfHidingToggle, mainTerm: Record<string, string>, value: T }[] = values.map(v => { + const mappedValues: { + show: SelfHidingToggle + mainTerm: Record<string, string> + value: T + }[] = values.map((v) => { + const vIsSelected = new UIEventSource(false) - const vIsSelected = new UIEventSource(false); - - selectedElements.addCallbackAndRunD(selectedElements => { - vIsSelected.setData(selectedElements.some(t => t === v.value)) + selectedElements.addCallbackAndRunD((selectedElements) => { + vIsSelected.setData(selectedElements.some((t) => t === v.value)) }) - vIsSelected.addCallback(selected => { + vIsSelected.addCallback((selected) => { if (selected) { if (mode === "select-one") { selectedElements.setData([v.value]) - } else if (!selectedElements.data.some(t => t === v.value)) { - selectedElements.data.push(v.value); + } else if (!selectedElements.data.some((t) => t === v.value)) { + selectedElements.data.push(v.value) selectedElements.ping() } } else { @@ -186,7 +199,7 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement< if (t == v.value) { selectedElements.data.splice(i, 1) selectedElements.ping() - break; + break } } } @@ -195,89 +208,99 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement< const toggle = new SelfHidingToggle(v.show, v.mainTerm, searchValue, { searchTerms: v.searchTerms, selected: vIsSelected, - squared: mode === "select-many" + squared: mode === "select-many", }) - return { ...v, - show: toggle - }; + show: toggle, + } }) let totalShown: Store<number> if (options.selectIfSingle) { - let forcedSelection : { value: T, show: SelfHidingToggle } = undefined - totalShown = searchValue.map(_ => { - let totalShown = 0; - let lastShownValue: { value: T, show: SelfHidingToggle } - for (const mv of mappedValues) { - const valueIsShown = mv.show.isShown.data - if (valueIsShown) { - totalShown++; - lastShownValue = mv + let forcedSelection: { value: T; show: SelfHidingToggle } = undefined + totalShown = searchValue.map( + (_) => { + let totalShown = 0 + let lastShownValue: { value: T; show: SelfHidingToggle } + for (const mv of mappedValues) { + const valueIsShown = mv.show.isShown.data + if (valueIsShown) { + totalShown++ + lastShownValue = mv + } } - } - if (totalShown == 1) { - if (selectedElements.data?.indexOf(lastShownValue.value) < 0) { - selectedElements.setData([lastShownValue.value]) - lastShownValue.show.forceSelected.setData(true) - forcedSelection = lastShownValue + if (totalShown == 1) { + if (selectedElements.data?.indexOf(lastShownValue.value) < 0) { + selectedElements.setData([lastShownValue.value]) + lastShownValue.show.forceSelected.setData(true) + forcedSelection = lastShownValue + } + } else if (forcedSelection != undefined) { + forcedSelection?.show?.forceSelected?.setData(false) + forcedSelection = undefined + selectedElements.setData([]) } - } else if (forcedSelection != undefined) { - forcedSelection?.show?.forceSelected?.setData(false) - forcedSelection = undefined; - selectedElements.setData([]) - } - return totalShown - }, mappedValues.map(mv => mv.show.GetValue())) + return totalShown + }, + mappedValues.map((mv) => mv.show.GetValue()) + ) } else { - totalShown = searchValue.map(_ => mappedValues.filter(mv => mv.show.isShown.data).length, mappedValues.map(mv => mv.show.GetValue())) - + totalShown = searchValue.map( + (_) => mappedValues.filter((mv) => mv.show.isShown.data).length, + mappedValues.map((mv) => mv.show.GetValue()) + ) } - const tooMuchElementsCutoff = 200; - options?.onManyElementsValue?.map(value => { - console.log("Installing toMuchElementsValue", value) - if(tooMuchElementsCutoff <= totalShown.data){ - selectedElements.setData(value) - selectedElements.ping() - } - }, [totalShown]) + const tooMuchElementsCutoff = 200 + options?.onManyElementsValue?.map( + (value) => { + console.log("Installing toMuchElementsValue", value) + if (tooMuchElementsCutoff <= totalShown.data) { + selectedElements.setData(value) + selectedElements.ping() + } + }, + [totalShown] + ) super([ searchBar, - new VariableUiElement(Locale.language.map(lng => { - if(totalShown.data >= 200){ - return options?.onManyElements ?? Translations.t.general.useSearch; - } - if (options?.onNoSearchMade !== undefined && (searchValue.data === undefined || searchValue.data.length === 0)) { - return options?.onNoSearchMade - } - if (totalShown.data == 0) { - return onEmpty - } - - mappedValues.sort((a, b) => a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1) - return new Combine(mappedValues.map(e => e.show)) - .SetClass("flex flex-wrap w-full content-start") - .SetClass(options?.searchAreaClass ?? "") - }, [totalShown, searchValue])) + new VariableUiElement( + Locale.language.map( + (lng) => { + if (totalShown.data >= 200) { + return options?.onManyElements ?? Translations.t.general.useSearch + } + if ( + options?.onNoSearchMade !== undefined && + (searchValue.data === undefined || searchValue.data.length === 0) + ) { + return options?.onNoSearchMade + } + if (totalShown.data == 0) { + return onEmpty + } + mappedValues.sort((a, b) => (a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1)) + return new Combine(mappedValues.map((e) => e.show)) + .SetClass("flex flex-wrap w-full content-start") + .SetClass(options?.searchAreaClass ?? "") + }, + [totalShown, searchValue] + ) + ), ]) - this.selectedElements = selectedElements; - this.someMatchFound = totalShown.map(t => t > 0); - + this.selectedElements = selectedElements + this.someMatchFound = totalShown.map((t) => t > 0) } public GetValue(): UIEventSource<T[]> { - return this.selectedElements; + return this.selectedElements } IsValid(t: T[]): boolean { - return true; + return true } - - } - diff --git a/UI/Input/SimpleDatePicker.ts b/UI/Input/SimpleDatePicker.ts index a131e456e..2c40242b9 100644 --- a/UI/Input/SimpleDatePicker.ts +++ b/UI/Input/SimpleDatePicker.ts @@ -1,45 +1,38 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" export default class SimpleDatePicker extends InputElement<string> { - - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) private readonly value: UIEventSource<string> - private readonly _element: HTMLElement; + private readonly _element: HTMLElement - constructor( - value?: UIEventSource<string> - ) { - super(); - this.value = value ?? new UIEventSource<string>(undefined); - const self = this; + constructor(value?: UIEventSource<string>) { + super() + this.value = value ?? new UIEventSource<string>(undefined) + const self = this const el = document.createElement("input") - this._element = el; + this._element = el el.type = "date" el.oninput = () => { - // Already in YYYY-MM-DD value! - self.value.setData(el.value); + // Already in YYYY-MM-DD value! + self.value.setData(el.value) } - - this.value.addCallbackAndRunD(v => { - el.value = v; - }); - - + this.value.addCallbackAndRunD((v) => { + el.value = v + }) } GetValue(): UIEventSource<string> { - return this.value; + return this.value } IsValid(t: string): boolean { - return !isNaN(new Date(t).getTime()); + return !isNaN(new Date(t).getTime()) } protected InnerConstructElement(): HTMLElement { return this._element } - -} \ No newline at end of file +} diff --git a/UI/Input/Slider.ts b/UI/Input/Slider.ts index e6e2f4a55..272a0fa41 100644 --- a/UI/Input/Slider.ts +++ b/UI/Input/Slider.ts @@ -1,13 +1,12 @@ -import {InputElement} from "./InputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import { InputElement } from "./InputElement" +import { UIEventSource } from "../../Logic/UIEventSource" export default class Slider extends InputElement<number> { - private readonly _value: UIEventSource<number> - private readonly min: number; - private readonly max: number; - private readonly step: number; - private readonly vertical: boolean; + private readonly min: number + private readonly max: number + private readonly step: number + private readonly vertical: boolean /** * Constructs a slider input element for natural numbers @@ -15,21 +14,25 @@ export default class Slider extends InputElement<number> { * @param max: the max value that is allowed, inclusive * @param options: value: injectable value; step: the step size of the slider */ - constructor(min: number, max: number, options?: { - value?: UIEventSource<number>, - step?: 1 | number, - vertical?: false | boolean - }) { - super(); - this.max = max; - this.min = min; + constructor( + min: number, + max: number, + options?: { + value?: UIEventSource<number> + step?: 1 | number + vertical?: false | boolean + } + ) { + super() + this.max = max + this.min = min this._value = options?.value ?? new UIEventSource<number>(min) - this.step = options?.step ?? 1; - this.vertical = options?.vertical ?? false; + this.step = options?.step ?? 1 + this.vertical = options?.vertical ?? false } GetValue(): UIEventSource<number> { - return this._value; + return this._value } protected InnerConstructElement(): HTMLElement { @@ -42,16 +45,15 @@ export default class Slider extends InputElement<number> { el.oninput = () => { valuestore.setData(Number(el.value)) } - if(this.vertical){ + if (this.vertical) { el.classList.add("vertical") - el.setAttribute('orient','vertical'); // firefox only workaround... + el.setAttribute("orient", "vertical") // firefox only workaround... } - valuestore.addCallbackAndRunD(v => el.value = ""+valuestore.data) - return el; + valuestore.addCallbackAndRunD((v) => (el.value = "" + valuestore.data)) + return el } IsValid(t: number): boolean { - return Math.round(t) == t && t >= this.min && t <= this.max; + return Math.round(t) == t && t >= this.min && t <= this.max } - -} \ No newline at end of file +} diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 29ee2894c..f4e936612 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -1,82 +1,79 @@ -import {InputElement} from "./InputElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import {Translation} from "../i18n/Translation"; -import Locale from "../i18n/Locale"; - +import { InputElement } from "./InputElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import { Translation } from "../i18n/Translation" +import Locale from "../i18n/Locale" interface TextFieldOptions { - placeholder?: string | Store<string> | Translation, - value?: UIEventSource<string>, - htmlType?: "area" | "text" | "time" | string, - inputMode?: string, - label?: BaseUIElement, - textAreaRows?: number, - inputStyle?: string, + placeholder?: string | Store<string> | Translation + value?: UIEventSource<string> + htmlType?: "area" | "text" | "time" | string + inputMode?: string + label?: BaseUIElement + textAreaRows?: number + inputStyle?: string isValid?: (s: string) => boolean } export class TextField extends InputElement<string> { - public readonly enterPressed = new UIEventSource<string>(undefined); - private readonly value: UIEventSource<string>; - private _actualField : HTMLElement - private readonly _isValid: (s: string) => boolean; - private readonly _rawValue: UIEventSource<string> - private _isFocused = false; - private readonly _options : TextFieldOptions; - + public readonly enterPressed = new UIEventSource<string>(undefined) + private readonly value: UIEventSource<string> + private _actualField: HTMLElement + private readonly _isValid: (s: string) => boolean + private readonly _rawValue: UIEventSource<string> + private _isFocused = false + private readonly _options: TextFieldOptions + constructor(options?: TextFieldOptions) { - super(); + super() this._options = options ?? {} - options = options ?? {}; - this.value = options?.value ?? new UIEventSource<string>(undefined); + options = options ?? {} + this.value = options?.value ?? new UIEventSource<string>(undefined) this._rawValue = new UIEventSource<string>("") - this._isValid = options.isValid ?? (_ => true); + this._isValid = options.isValid ?? ((_) => true) } private static SetCursorPosition(textfield: HTMLElement, i: number) { if (textfield === undefined || textfield === null) { - return; + return } if (i === -1) { // @ts-ignore - i = textfield.value.length; + i = textfield.value.length } - textfield.focus(); + textfield.focus() // @ts-ignore - textfield.setSelectionRange(i, i); - + textfield.setSelectionRange(i, i) } GetValue(): UIEventSource<string> { - return this.value; + return this.value } - - GetRawValue(): UIEventSource<string>{ + + GetRawValue(): UIEventSource<string> { return this._rawValue } - + IsValid(t: string): boolean { if (t === undefined || t === null) { return false } - return this._isValid(t); + return this._isValid(t) } - private static test(){ + private static test() { const placeholder = new UIEventSource<string>("placeholder") const tf = new TextField({ - placeholder + placeholder, }) - const html = <HTMLInputElement> tf.InnerConstructElement().children[0]; + const html = <HTMLInputElement>tf.InnerConstructElement().children[0] html.placeholder // => 'placeholder' placeholder.setData("another piece of text") - html.placeholder// => "another piece of text" + html.placeholder // => "another piece of text" } - /** - * + * * // should update placeholders dynamically * const placeholder = new UIEventSource<string>("placeholder") * const tf = new TextField({ @@ -86,7 +83,7 @@ export class TextField extends InputElement<string> { * html.placeholder // => 'placeholder' * placeholder.setData("another piece of text") * html.placeholder// => "another piece of text" - * + * * // should update translated placeholders dynamically * const placeholder = new Translation({nl: "Nederlands", en: "English"}) * Locale.language.setData("nl"); @@ -99,26 +96,32 @@ export class TextField extends InputElement<string> { * html.placeholder // => 'English' */ protected InnerConstructElement(): HTMLElement { - const options = this._options; - const self = this; + const options = this._options + const self = this let placeholderStore: Store<string> - let placeholder : string = ""; - if(options.placeholder){ - if(typeof options.placeholder === "string"){ - placeholder = options.placeholder; - placeholderStore = undefined; - }else { - if ((options.placeholder instanceof Store) && options.placeholder["data"] !== undefined) { - placeholderStore = options.placeholder; - } else if ((options.placeholder instanceof Translation) && options.placeholder["translations"] !== undefined) { - placeholderStore = <Store<string>>Locale.language.map(l => (<Translation>options.placeholder).textFor(l)) + let placeholder: string = "" + if (options.placeholder) { + if (typeof options.placeholder === "string") { + placeholder = options.placeholder + placeholderStore = undefined + } else { + if ( + options.placeholder instanceof Store && + options.placeholder["data"] !== undefined + ) { + placeholderStore = options.placeholder + } else if ( + options.placeholder instanceof Translation && + options.placeholder["translations"] !== undefined + ) { + placeholderStore = <Store<string>>( + Locale.language.map((l) => (<Translation>options.placeholder).textFor(l)) + ) } - placeholder = placeholderStore?.data ?? placeholder ?? ""; + placeholder = placeholderStore?.data ?? placeholder ?? "" } } - - this.SetClass("form-text-field") let inputEl: HTMLElement if (options.htmlType === "area") { @@ -128,9 +131,9 @@ export class TextField extends InputElement<string> { el.rows = options.textAreaRows el.cols = 50 el.style.width = "100%" - inputEl = el; - if(placeholderStore){ - placeholderStore.addCallbackAndRunD(placeholder => el.placeholder = placeholder) + inputEl = el + if (placeholderStore) { + placeholderStore.addCallbackAndRunD((placeholder) => (el.placeholder = placeholder)) } } else { const el = document.createElement("input") @@ -139,86 +142,81 @@ export class TextField extends InputElement<string> { el.placeholder = placeholder el.style.cssText = options.inputStyle ?? "width: 100%;" inputEl = el - if(placeholderStore){ - placeholderStore.addCallbackAndRunD(placeholder => el.placeholder = placeholder) + if (placeholderStore) { + placeholderStore.addCallbackAndRunD((placeholder) => (el.placeholder = placeholder)) } } - - const form = document.createElement("form") form.appendChild(inputEl) - form.onsubmit = () => false; + form.onsubmit = () => false if (options.label) { form.appendChild(options.label.ConstructElement()) } + const field = inputEl - const field = inputEl; - - this.value.addCallbackAndRunD(value => { + this.value.addCallbackAndRunD((value) => { // We leave the textfield as is in the case of undefined or null (handled by addCallbackAndRunD) - make sure we do not erase it! - field["value"] = value; + field["value"] = value if (self.IsValid(value)) { self.RemoveClass("invalid") } else { self.SetClass("invalid") } - }) field.oninput = () => { - // How much characters are on the right, not including spaces? // @ts-ignore - const endDistance = field.value.substring(field.selectionEnd).replace(/ /g, '').length; + const endDistance = field.value.substring(field.selectionEnd).replace(/ /g, "").length // @ts-ignore - let val: string = field.value; + let val: string = field.value self._rawValue.setData(val) if (!self.IsValid(val)) { - self.value.setData(undefined); + self.value.setData(undefined) } else { - self.value.setData(val); + self.value.setData(val) } // Setting the value might cause the value to be set again. We keep the distance _to the end_ stable, as phone number formatting might cause the start to change // See https://github.com/pietervdvn/MapComplete/issues/103 // We reread the field value - it might have changed! // @ts-ignore - val = field.value; - let newCursorPos = val.length - endDistance; - while (newCursorPos >= 0 && + val = field.value + let newCursorPos = val.length - endDistance + while ( + newCursorPos >= 0 && // We count the number of _actual_ characters (non-space characters) on the right of the new value // This count should become bigger then the end distance - val.substr(newCursorPos).replace(/ /g, '').length < endDistance - ) { - newCursorPos--; + val.substr(newCursorPos).replace(/ /g, "").length < endDistance + ) { + newCursorPos-- } - TextField.SetCursorPosition(field, newCursorPos); - }; - + TextField.SetCursorPosition(field, newCursorPos) + } field.addEventListener("keyup", function (event) { if (event.key === "Enter") { // @ts-ignore - self.enterPressed.setData(field.value); + self.enterPressed.setData(field.value) } - }); + }) - if(this._isFocused){ + if (this._isFocused) { field.focus() } - this._actualField = field; - return form; + this._actualField = field + return form } public focus() { - if(this._actualField === undefined){ + if (this._actualField === undefined) { this._isFocused = true - }else{ + } else { this._actualField.focus() } } -} \ No newline at end of file +} diff --git a/UI/Input/Toggle.ts b/UI/Input/Toggle.ts index cfefec351..154d7af8b 100644 --- a/UI/Input/Toggle.ts +++ b/UI/Input/Toggle.ts @@ -1,20 +1,21 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Lazy from "../Base/Lazy"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import Lazy from "../Base/Lazy" /** * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. * It can be used to implement e.g. checkboxes or collapsible elements */ export default class Toggle extends VariableUiElement { + public readonly isEnabled: Store<boolean> - public readonly isEnabled: Store<boolean>; - - constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: Store<boolean>) { - super( - isEnabled?.map(isEnabled => isEnabled ? showEnabled : showDisabled) - ); + constructor( + showEnabled: string | BaseUIElement, + showDisabled: string | BaseUIElement, + isEnabled: Store<boolean> + ) { + super(isEnabled?.map((isEnabled) => (isEnabled ? showEnabled : showDisabled))) this.isEnabled = isEnabled } @@ -22,35 +23,30 @@ export default class Toggle extends VariableUiElement { if (constructor === undefined) { return undefined } - return new Toggle( - new Lazy(constructor), - undefined, - condition - ) - + return new Toggle(new Lazy(constructor), undefined, condition) } - } /** * Same as `Toggle`, but will swap on click */ export class ClickableToggle extends Toggle { + public readonly isEnabled: UIEventSource<boolean> - public readonly isEnabled: UIEventSource<boolean>; - - constructor(showEnabled: string | BaseUIElement, showDisabled: string | BaseUIElement, isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false)) { - super( - showEnabled, showDisabled, isEnabled - ); + constructor( + showEnabled: string | BaseUIElement, + showDisabled: string | BaseUIElement, + isEnabled: UIEventSource<boolean> = new UIEventSource<boolean>(false) + ) { + super(showEnabled, showDisabled, isEnabled) this.isEnabled = isEnabled } - + public ToggleOnClick(): ClickableToggle { - const self = this; + const self = this this.onClick(() => { - self.isEnabled.setData(!self.isEnabled.data); + self.isEnabled.setData(!self.isEnabled.data) }) - return this; + return this } -} \ No newline at end of file +} diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 5952e5551..e9d307dc8 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -1,46 +1,44 @@ -import {DropDown} from "./DropDown"; -import * as EmailValidator from "email-validator"; -import {parsePhoneNumberFromString} from "libphonenumber-js"; -import {InputElement} from "./InputElement"; -import {TextField} from "./TextField"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import CombinedInputElement from "./CombinedInputElement"; -import SimpleDatePicker from "./SimpleDatePicker"; -import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"; -import DirectionInput from "./DirectionInput"; -import ColorPicker from "./ColorPicker"; -import {Utils} from "../../Utils"; -import Loc from "../../Models/Loc"; -import BaseUIElement from "../BaseUIElement"; -import LengthInput from "./LengthInput"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {Unit} from "../../Models/Unit"; -import {FixedInputElement} from "./FixedInputElement"; -import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"; -import Wikidata from "../../Logic/Web/Wikidata"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import Table from "../Base/Table"; -import Combine from "../Base/Combine"; -import Title from "../Base/Title"; -import InputElementMap from "./InputElementMap"; -import Translations from "../i18n/Translations"; -import {Translation} from "../i18n/Translation"; -import BaseLayer from "../../Models/BaseLayer"; -import Locale from "../i18n/Locale"; +import { DropDown } from "./DropDown" +import * as EmailValidator from "email-validator" +import { parsePhoneNumberFromString } from "libphonenumber-js" +import { InputElement } from "./InputElement" +import { TextField } from "./TextField" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import CombinedInputElement from "./CombinedInputElement" +import SimpleDatePicker from "./SimpleDatePicker" +import OpeningHoursInput from "../OpeningHours/OpeningHoursInput" +import DirectionInput from "./DirectionInput" +import ColorPicker from "./ColorPicker" +import { Utils } from "../../Utils" +import Loc from "../../Models/Loc" +import BaseUIElement from "../BaseUIElement" +import LengthInput from "./LengthInput" +import { GeoOperations } from "../../Logic/GeoOperations" +import { Unit } from "../../Models/Unit" +import { FixedInputElement } from "./FixedInputElement" +import WikidataSearchBox from "../Wikipedia/WikidataSearchBox" +import Wikidata from "../../Logic/Web/Wikidata" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import Table from "../Base/Table" +import Combine from "../Base/Combine" +import Title from "../Base/Title" +import InputElementMap from "./InputElementMap" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" +import BaseLayer from "../../Models/BaseLayer" +import Locale from "../i18n/Locale" export class TextFieldDef { - - public readonly name: string; + public readonly name: string /* - * An explanation for the theme builder. - * This can indicate which special input element is used, ... - * */ - public readonly explanation: string; + * An explanation for the theme builder. + * This can indicate which special input element is used, ... + * */ + public readonly explanation: string protected inputmode?: string = undefined - constructor(name: string, - explanation: string | BaseUIElement) { - this.name = name; + constructor(name: string, explanation: string | BaseUIElement) { + this.name = name if (this.name.endsWith("textfield")) { this.name = this.name.substr(0, this.name.length - "TextField".length) } @@ -48,88 +46,85 @@ export class TextFieldDef { this.name = this.name.substr(0, this.name.length - "TextFieldDef".length) } if (typeof explanation === "string") { - this.explanation = explanation } else { - this.explanation = explanation.AsMarkdown(); + this.explanation = explanation.AsMarkdown() } } protectedisValid(s: string, _: (() => string) | undefined): boolean { - return true; + return true } public getFeedback(s: string): Translation { const tr = Translations.t.validation[this.name] - if(tr !== undefined){ + if (tr !== undefined) { return tr["feedback"] } } - public ConstructInputElement(options: { - value?: UIEventSource<string>, - inputStyle?: string, - feedback?: UIEventSource<Translation> - placeholder?: string | Translation | UIEventSource<string>, - country?: () => string, - location?: [number /*lat*/, number /*lon*/], - mapBackgroundLayer?: UIEventSource</*BaseLayer*/ any>, - unit?: Unit, - args?: (string | number | boolean | any)[] // Extra arguments for the inputHelper, - feature?: any, - } = {}): InputElement<string> { - + public ConstructInputElement( + options: { + value?: UIEventSource<string> + inputStyle?: string + feedback?: UIEventSource<Translation> + placeholder?: string | Translation | UIEventSource<string> + country?: () => string + location?: [number /*lat*/, number /*lon*/] + mapBackgroundLayer?: UIEventSource</*BaseLayer*/ any> + unit?: Unit + args?: (string | number | boolean | any)[] // Extra arguments for the inputHelper, + feature?: any + } = {} + ): InputElement<string> { if (options.placeholder === undefined) { options.placeholder = Translations.t.validation[this.name]?.description ?? this.name } - options["textArea"] = this.name === "text"; + options["textArea"] = this.name === "text" - const self = this; + const self = this if (options.unit !== undefined) { // Reformatting is handled by the unit in this case - options["isValid"] = str => { - const denom = options.unit.findDenomination(str, options?.country); + options["isValid"] = (str) => { + const denom = options.unit.findDenomination(str, options?.country) if (denom === undefined) { - return false; + return false } const stripped = denom[0] return self.isValid(stripped, options.country) } } else { - options["isValid"] = str => self.isValid(str, options.country); + options["isValid"] = (str) => self.isValid(str, options.country) } options["cssText"] = "width: 100%;" - - options["inputMode"] = this.inputmode; + options["inputMode"] = this.inputmode if (this.inputmode === "text") { options["htmlType"] = "area" options["textAreaRows"] = 4 } - - const textfield = new TextField(options); + const textfield = new TextField(options) let input: InputElement<string> = textfield if (options.feedback) { - textfield.GetRawValue().addCallback(v => { - if(self.isValid(v, options.country)){ + textfield.GetRawValue().addCallback((v) => { + if (self.isValid(v, options.country)) { options.feedback.setData(undefined) - }else{ - options.feedback.setData(self.getFeedback(v)) + } else { + options.feedback.setData(self.getFeedback(v)) } }) } - if (this.reformat && options.unit === undefined) { - input.GetValue().addCallbackAndRun(str => { + input.GetValue().addCallbackAndRun((str) => { if (!options["isValid"](str, options.country)) { - return; + return } - const formatted = this.reformat(str, options.country); - input.GetValue().setData(formatted); + const formatted = this.reformat(str, options.country) + input.GetValue().setData(formatted) }) } @@ -139,20 +134,23 @@ export class TextFieldDef { // We have to create a dropdown with applicable denominations, and fuse those values const unit = options.unit - - const isSingular = input.GetValue().map(str => str?.trim() === "1") + const isSingular = input.GetValue().map((str) => str?.trim() === "1") const unitDropDown = - unit.denominations.length === 1 ? - new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0]) - : new DropDown("", - unit.denominations.map(denom => { - return { - shown: denom.getToggledHuman(isSingular), - value: denom - } - }) - ) + unit.denominations.length === 1 + ? new FixedInputElement( + unit.denominations[0].getToggledHuman(isSingular), + unit.denominations[0] + ) + : new DropDown( + "", + unit.denominations.map((denom) => { + return { + shown: denom.getToggledHuman(isSingular), + value: denom, + } + }) + ) unitDropDown.GetValue().setData(unit.getDefaultInput(options.country)) unitDropDown.SetClass("w-min") @@ -169,7 +167,7 @@ export class TextFieldDef { }, (valueWithDenom: string) => { // Take the value from OSM and feed it into the textfield and the dropdown - const withDenom = unit.findDenomination(valueWithDenom, options?.country); + const withDenom = unit.findDenomination(valueWithDenom, options?.country) if (withDenom === undefined) { // Not a valid value at all - we give it undefined and leave the details up to the other elements (but we keep the previous denomination) return [undefined, fixedDenom] @@ -186,31 +184,34 @@ export class TextFieldDef { location: options.location, mapBackgroundLayer: options.mapBackgroundLayer, args: options.args, - feature: options.feature + feature: options.feature, })?.SetClass("block") if (helper !== undefined) { - input = new CombinedInputElement(input, helper, + input = new CombinedInputElement( + input, + helper, (a, _) => a, // We can ignore b, as they are linked earlier - a => [a, a] - ).SetClass("block w-full"); + (a) => [a, a] + ).SetClass("block w-full") } if (this.postprocess !== undefined) { - input = new InputElementMap<string, string>(input, + input = new InputElementMap<string, string>( + input, (a, b) => a === b, this.postprocess, this.undoPostprocess ) } - return input; + return input } protected isValid(string: string, requestCountry: () => string): boolean { - return true; + return true } protected reformat(s: string, country?: () => string): string { - return s; + return s } /** @@ -221,42 +222,62 @@ export class TextFieldDef { } protected undoPostprocess(s: string): string { - return s; + return s } - protected inputHelper(value: UIEventSource<string>, options?: { - location: [number, number], - mapBackgroundLayer?: UIEventSource<any>, - args: (string | number | boolean | any)[] - feature?: any - }): InputElement<string> { + protected inputHelper( + value: UIEventSource<string>, + options?: { + location: [number, number] + mapBackgroundLayer?: UIEventSource<any> + args: (string | number | boolean | any)[] + feature?: any + } + ): InputElement<string> { return undefined } - - } class WikidataTextField extends TextFieldDef { - constructor() { super( "wikidata", new Combine([ "A wikidata identifier, e.g. Q42.", new Title("Helper arguments"), - new Table(["name", "doc"], + new Table( + ["name", "doc"], [ ["key", "the value of this tag will initialize search (default: name)"], - ["options", new Combine(["A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", - new Table( - ["subarg", "doc"], - [["removePrefixes", "remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list"], - ["removePostfixes", "remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list"], - ["instanceOf","A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"], - ["notInstanceof","A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results"] - ] - )]) - ]]), + [ + "options", + new Combine([ + "A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", + new Table( + ["subarg", "doc"], + [ + [ + "removePrefixes", + "remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list", + ], + [ + "removePostfixes", + "remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list", + ], + [ + "instanceOf", + "A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans", + ], + [ + "notInstanceof", + "A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results", + ], + ] + ), + ]), + ], + ] + ), new Title("Example usage"), `The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name @@ -298,106 +319,123 @@ Another example is to search for species and trees: }] } \`\`\` -` - ])); +`, + ]) + ) } - public isValid(str): boolean { - if (str === undefined) { - return false; + return false } if (str.length <= 2) { - return false; + return false } - return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined) + return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined) } public reformat(str) { if (str === undefined) { - return undefined; + return undefined } - let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ") + let out = str + .split(";") + .map((str) => Wikidata.ExtractKey(str)) + .join("; ") if (str.endsWith(";")) { out = out + ";" } - return out; + return out } public inputHelper(currentValue, inputHelperOptions) { const args = inputHelperOptions.args ?? [] const searchKey = args[0] ?? "name" - - - const searchFor = <string>(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "") + const searchFor = <string>( + (inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "") + ) - let searchForValue: UIEventSource<string> = new UIEventSource(searchFor); + let searchForValue: UIEventSource<string> = new UIEventSource(searchFor) const options: any = args[1] if (searchFor !== undefined && options !== undefined) { const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? [] const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? [] - Locale.language.map(lg => { - const prefixesUnrwapped: string[] = prefixes[lg] ?? prefixes - const postfixesUnwrapped: string[] = postfixes[lg] ?? postfixes - let clipped = searchFor; - - for (const postfix of postfixesUnwrapped) { - if (searchFor.endsWith(postfix)) { - clipped = searchFor.substring(0, searchFor.length - postfix.length) - break; + Locale.language + .map((lg) => { + const prefixesUnrwapped: string[] = prefixes[lg] ?? prefixes + const postfixesUnwrapped: string[] = postfixes[lg] ?? postfixes + let clipped = searchFor + + for (const postfix of postfixesUnwrapped) { + if (searchFor.endsWith(postfix)) { + clipped = searchFor.substring(0, searchFor.length - postfix.length) + break + } } - } - for (const prefix of prefixesUnrwapped) { - if (searchFor.startsWith(prefix)) { - clipped = searchFor.substring(prefix.length) - break; + for (const prefix of prefixesUnrwapped) { + if (searchFor.startsWith(prefix)) { + clipped = searchFor.substring(prefix.length) + break + } } - } - return clipped; - }).addCallbackAndRun(clipped => searchForValue.setData(clipped)) - - + return clipped + }) + .addCallbackAndRun((clipped) => searchForValue.setData(clipped)) } - let instanceOf : number[] = Utils.NoNull((options?.instanceOf ?? []).map(i => Wikidata.QIdToNumber(i))) - let notInstanceOf : number[] = Utils.NoNull((options?.notInstanceOf ?? []).map(i => Wikidata.QIdToNumber(i))) + let instanceOf: number[] = Utils.NoNull( + (options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)) + ) + let notInstanceOf: number[] = Utils.NoNull( + (options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)) + ) return new WikidataSearchBox({ value: currentValue, searchText: searchForValue, instanceOf, - notInstanceOf + notInstanceOf, }) } } class OpeningHoursTextField extends TextFieldDef { - constructor() { super( "opening_hours", new Combine([ "Has extra elements to easily input when a POI is opened.", new Title("Helper arguments"), - new Table(["name", "doc"], + new Table( + ["name", "doc"], [ - ["options", new Combine([ - "A JSON-object of type `{ prefix: string, postfix: string }`. ", - new Table(["subarg", "doc"], - [ - ["prefix", "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse"], - ["postfix", "Piece of text that will always be added to the end of the generated opening hours"], - ]) - - ]) - ] - ]), + [ + "options", + new Combine([ + "A JSON-object of type `{ prefix: string, postfix: string }`. ", + new Table( + ["subarg", "doc"], + [ + [ + "prefix", + "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse", + ], + [ + "postfix", + "Piece of text that will always be added to the end of the generated opening hours", + ], + ] + ), + ]), + ], + ] + ), new Title("Example usage"), - "To add a conditional (based on time) access restriction:\n\n```\n" + ` + "To add a conditional (based on time) access restriction:\n\n```\n" + + ` "freeform": { "key": "access:conditional", "type": "opening_hours", @@ -407,7 +445,10 @@ class OpeningHoursTextField extends TextFieldDef { "postfix":")" } ] -}` + "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"]),); +}` + + "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`", + ]) + ) } isValid() { @@ -418,12 +459,15 @@ class OpeningHoursTextField extends TextFieldDef { return str } - inputHelper(value: UIEventSource<string>, inputHelperOptions: { - location: [number, number], - mapBackgroundLayer?: UIEventSource<any>, - args: (string | number | boolean | any)[] - feature?: any - }) { + inputHelper( + value: UIEventSource<string>, + inputHelperOptions: { + location: [number, number] + mapBackgroundLayer?: UIEventSource<any> + args: (string | number | boolean | any)[] + feature?: any + } + ) { const args = (inputHelperOptions.args ?? [])[0] const prefix = <string>args?.prefix ?? "" const postfix = <string>args?.postfix ?? "" @@ -433,11 +477,13 @@ class OpeningHoursTextField extends TextFieldDef { } class UrlTextfieldDef extends TextFieldDef { - inputmode: "url" constructor() { - super("url", "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user") + super( + "url", + "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user" + ) } postprocess(str: string) { @@ -447,7 +493,7 @@ class UrlTextfieldDef extends TextFieldDef { if (!str.startsWith("http://") || !str.startsWith("https://")) { return "https://" + str } - return str; + return str } undoPostprocess(str: string) { @@ -460,27 +506,41 @@ class UrlTextfieldDef extends TextFieldDef { if (str.startsWith("https://")) { return str.substr("https://".length) } - return str; + return str } reformat(str: string): string { try { let url: URL // str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763 - if (!str.startsWith("http://") && !str.startsWith("https://") && !str.startsWith("http:")) { + if ( + !str.startsWith("http://") && + !str.startsWith("https://") && + !str.startsWith("http:") + ) { url = new URL("https://" + str) } else { - url = new URL(str); + url = new URL(str) } const blacklistedTrackingParams = [ - "fbclid",// Oh god, how I hate the fbclid. Let it burn, burn in hell! + "fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell! "gclid", - "cmpid", "agid", "utm", "utm_source", "utm_medium", - "campaignid", "campaign", "AdGroupId", "AdGroup", "TargetId", "msclkid"] + "cmpid", + "agid", + "utm", + "utm_source", + "utm_medium", + "campaignid", + "campaign", + "AdGroupId", + "AdGroup", + "TargetId", + "msclkid", + ] for (const dontLike of blacklistedTrackingParams) { url.searchParams.delete(dontLike.toLowerCase()) } - let cleaned = url.toString(); + let cleaned = url.toString() if (cleaned.endsWith("/") && !str.endsWith("/")) { // Do not add a trailing '/' if it wasn't typed originally cleaned = cleaned.substr(0, cleaned.length - 1) @@ -490,31 +550,34 @@ class UrlTextfieldDef extends TextFieldDef { cleaned = cleaned.substr("https://".length) } - return cleaned; + return cleaned } catch (e) { console.error(e) - return undefined; + return undefined } } isValid(str: string): boolean { try { - if (!str.startsWith("http://") && !str.startsWith("https://") && - !str.startsWith("http:")) { + if ( + !str.startsWith("http://") && + !str.startsWith("https://") && + !str.startsWith("http:") + ) { str = "https://" + str } - const url = new URL(str); + const url = new URL(str) const dotIndex = url.host.indexOf(".") - return dotIndex > 0 && url.host[url.host.length - 1] !== "."; + return dotIndex > 0 && url.host[url.host.length - 1] !== "." } catch (e) { - return false; + return false } } } class StringTextField extends TextFieldDef { constructor() { - super("string", "A simple piece of text"); + super("string", "A simple piece of text") } } @@ -522,31 +585,29 @@ class TextTextField extends TextFieldDef { inputmode: "text" constructor() { - super("text", "A longer piece of text"); + super("text", "A longer piece of text") } } class DateTextField extends TextFieldDef { constructor() { - super("date", "A date with date picker"); + super("date", "A date with date picker") } isValid = (str) => { - return !isNaN(new Date(str).getTime()); + return !isNaN(new Date(str).getTime()) } reformat(str) { - const d = new Date(str); - let month = '' + (d.getMonth() + 1); - let day = '' + d.getDate(); - const year = d.getFullYear(); + const d = new Date(str) + let month = "" + (d.getMonth() + 1) + let day = "" + d.getDate() + const year = d.getFullYear() - if (month.length < 2) - month = '0' + month; - if (day.length < 2) - day = '0' + day; + if (month.length < 2) month = "0" + month + if (day.length < 2) day = "0" + day - return [year, month, day].join('-'); + return [year, month, day].join("-") } inputHelper(value) { @@ -554,13 +615,13 @@ class DateTextField extends TextFieldDef { } } - class LengthTextField extends TextFieldDef { inputMode: "decimal" constructor() { super( - "distance", "A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]" + "distance", + 'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]' ) } @@ -569,8 +630,15 @@ class LengthTextField extends TextFieldDef { return !isNaN(t) } - inputHelper = (value: UIEventSource<string>, options: - { location?: [number,number]; args?: string[]; feature?: any; mapBackgroundLayer?: Store<BaseLayer>; }) => { + inputHelper = ( + value: UIEventSource<string>, + options: { + location?: [number, number] + args?: string[] + feature?: any + mapBackgroundLayer?: Store<BaseLayer> + } + ) => { options = options ?? {} options.location = options.location ?? [0, 0] @@ -579,7 +647,11 @@ class LengthTextField extends TextFieldDef { if (args[0]) { zoom = Number(args[0]) if (isNaN(zoom)) { - console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ", args[0], " (using 19 instead)") + console.error( + "Invalid zoom level for argument at 'length'-input. The offending argument is: ", + args[0], + " (using 19 instead)" + ) zoom = 19 } } @@ -588,26 +660,28 @@ class LengthTextField extends TextFieldDef { if (options?.feature !== undefined && options.feature.geometry.type !== "Point") { const lonlat = <[number, number]>[...options.location] lonlat.reverse(/*Changes a clone, this is safe */) - options.location = <[number, number]>GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates + options.location = <[number, number]>( + GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates + ) options.location.reverse(/*Changes a clone, this is safe */) } - const location = new UIEventSource<Loc>({ lat: options.location[0], lon: options.location[1], - zoom: zoom + zoom: zoom, }) if (args[1]) { // We have a prefered map! options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( - location, new UIEventSource<string[]>(args[1].split(",")) + location, + new UIEventSource<string[]>(args[1].split(",")) ) } const background = options?.mapBackgroundLayer const li = new LengthInput(new UIEventSource<BaseLayer>(background.data), location, value) li.SetStyle("height: 20rem;") - return li; + return li } } @@ -615,15 +689,15 @@ class FloatTextField extends TextFieldDef { inputmode = "decimal" constructor(name?: string, explanation?: string) { - super(name ?? "float", explanation ?? "A decimal"); + super(name ?? "float", explanation ?? "A decimal") } isValid(str) { return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",") } - reformat( str): string { - return "" + Number(str); + reformat(str): string { + return "" + Number(str) } getFeedback(s: string): Translation { @@ -639,11 +713,11 @@ class IntTextField extends FloatTextField { inputMode = "numeric" constructor(name?: string, explanation?: string) { - super(name ?? "int", explanation ?? "A number"); + super(name ?? "int", explanation ?? "A number") } isValid(str): boolean { - str = "" + str; + str = "" + str return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) } @@ -657,26 +731,24 @@ class IntTextField extends FloatTextField { } return undefined } - } class NatTextField extends IntTextField { inputMode = "numeric" constructor(name?: string, explanation?: string) { - super(name ?? "nat", explanation ?? "A positive number or zero"); + super(name ?? "nat", explanation ?? "A positive number or zero") } isValid(str): boolean { if (str === undefined) { - return false; + return false } - str = "" + str; + str = "" + str return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 } - getFeedback(s: string): Translation { const spr = super.getFeedback(s) if (spr !== undefined) { @@ -694,11 +766,11 @@ class PNatTextField extends NatTextField { inputmode = "numeric" constructor() { - super("pnat", "A strict positive number"); + super("pnat", "A strict positive number") } getFeedback(s: string): Translation { - const spr = super.getFeedback(s); + const spr = super.getFeedback(s) if (spr !== undefined) { return spr } @@ -714,27 +786,27 @@ class PNatTextField extends NatTextField { } return Number(str) > 0 } - } class PFloatTextField extends FloatTextField { inputmode = "decimal" constructor() { - super("pfloat", "A positive decimal (inclusive zero)"); + super("pfloat", "A positive decimal (inclusive zero)") } - isValid = (str) => !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",") + isValid = (str) => + !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",") getFeedback(s: string): Translation { - const spr = super.getFeedback(s); + const spr = super.getFeedback(s) if (spr !== undefined) { return spr } if (Number(s) < 0) { return Translations.t.validation.nat.mustBePositive } - return undefined; + return undefined } } @@ -742,7 +814,7 @@ class EmailTextField extends TextFieldDef { inputmode = "email" constructor() { - super("email", "An email adress"); + super("email", "An email adress") } isValid = (str) => { @@ -753,10 +825,10 @@ class EmailTextField extends TextFieldDef { if (str.startsWith("mailto:")) { str = str.substring("mailto:".length) } - return EmailValidator.validate(str); + return EmailValidator.validate(str) } - reformat = str => { + reformat = (str) => { if (str === undefined) { return undefined } @@ -764,13 +836,15 @@ class EmailTextField extends TextFieldDef { if (str.startsWith("mailto:")) { str = str.substring("mailto:".length) } - return str; + return str } - + getFeedback(s: string): Translation { - if(s.indexOf('@') < 0){return Translations.t.validation.email.noAt} - - return super.getFeedback(s); + if (s.indexOf("@") < 0) { + return Translations.t.validation.email.noAt + } + + return super.getFeedback(s) } } @@ -778,19 +852,19 @@ class PhoneTextField extends TextFieldDef { inputmode = "tel" constructor() { - super("phone", "A phone number"); + super("phone", "A phone number") } isValid(str, country: () => string): boolean { if (str === undefined) { - return false; + return false } if (str.startsWith("tel:")) { str = str.substring("tel:".length) } let countryCode = undefined - if(country !== undefined){ - countryCode = (country())?.toUpperCase() + if (country !== undefined) { + countryCode = country()?.toUpperCase() } return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false } @@ -799,19 +873,28 @@ class PhoneTextField extends TextFieldDef { if (str.startsWith("tel:")) { str = str.substring("tel:".length) } - return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.formatInternational(); + return parsePhoneNumberFromString( + str, + country()?.toUpperCase() as any + )?.formatInternational() } } class ColorTextField extends TextFieldDef { constructor() { - super("color", "Shows a color picker"); + super("color", "Shows a color picker") } inputHelper = (value) => { - return new ColorPicker(value.map(color => { - return Utils.ColourNameToHex(color ?? ""); - }, [], str => Utils.HexToColourName(str))) + return new ColorPicker( + value.map( + (color) => { + return Utils.ColourNameToHex(color ?? "") + }, + [], + (str) => Utils.HexToColourName(str) + ) + ) } } @@ -819,15 +902,17 @@ class DirectionTextField extends IntTextField { inputMode = "numeric" constructor() { - super("direction", "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"); - } - - reformat(str): string { - const n = (Number(str) % 360) - return ""+n + super( + "direction", + "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)" + ) + } + + reformat(str): string { + const n = Number(str) % 360 + return "" + n } - inputHelper = (value, options) => { const args = options.args ?? [] options.location = options.location ?? [0, 0] @@ -841,24 +926,23 @@ class DirectionTextField extends IntTextField { const location = new UIEventSource<Loc>({ lat: options.location[0], lon: options.location[1], - zoom: zoom + zoom: zoom, }) if (args[1]) { // We have a prefered map! options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( - location, new UIEventSource<string[]>(args[1].split(",")) + location, + new UIEventSource<string[]>(args[1].split(",")) ) } const di = new DirectionInput(options.mapBackgroundLayer, location, value) - di.SetStyle("max-width: 25rem;"); + di.SetStyle("max-width: 25rem;") - return di; + return di } } - export default class ValidatedTextField { - private static AllTextfieldDefs: TextFieldDef[] = [ new StringTextField(), new TextTextField(), @@ -875,39 +959,43 @@ export default class ValidatedTextField { new UrlTextfieldDef(), new PhoneTextField(), new OpeningHoursTextField(), - new ColorTextField() + new ColorTextField(), ] - public static allTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict(); + public static allTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict() public static ForType(type: string = "string"): TextFieldDef { const def = ValidatedTextField.allTypes.get(type) - if(def === undefined){ - console.warn("Something tried to load a validated text field named",type, "but this type does not exist") + if (def === undefined) { + console.warn( + "Something tried to load a validated text field named", + type, + "but this type does not exist" + ) return this.ForType() } return def } public static HelpText(): BaseUIElement { - const explanations: BaseUIElement[] = - ValidatedTextField.AllTextfieldDefs.map(type => - new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")) + const explanations: BaseUIElement[] = ValidatedTextField.AllTextfieldDefs.map((type) => + new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col") + ) return new Combine([ new Title("Available types for text fields", 1), "The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them", - ...explanations + ...explanations, ]).SetClass("flex flex-col") } public static AvailableTypes(): string[] { - return ValidatedTextField.AllTextfieldDefs.map(tp => tp.name) + return ValidatedTextField.AllTextfieldDefs.map((tp) => tp.name) } private static allTypesDict(): Map<string, TextFieldDef> { - const types = new Map<string, TextFieldDef>(); + const types = new Map<string, TextFieldDef>() for (const tp of ValidatedTextField.AllTextfieldDefs) { - types[tp.name] = tp; - types.set(tp.name, tp); + types[tp.name] = tp + types.set(tp.name, tp) } - return types; + return types } -} \ No newline at end of file +} diff --git a/UI/Input/VariableInputElement.ts b/UI/Input/VariableInputElement.ts index b79baf317..0fdae6c78 100644 --- a/UI/Input/VariableInputElement.ts +++ b/UI/Input/VariableInputElement.ts @@ -1,31 +1,32 @@ -import {ReadonlyInputElement} from "./InputElement"; -import {Store} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import { ReadonlyInputElement } from "./InputElement" +import { Store } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" -export default class VariableInputElement<T> extends BaseUIElement implements ReadonlyInputElement<T> { - - private readonly value: Store<T>; +export default class VariableInputElement<T> + extends BaseUIElement + implements ReadonlyInputElement<T> +{ + private readonly value: Store<T> private readonly element: BaseUIElement - private readonly upstream: Store<ReadonlyInputElement<T>>; + private readonly upstream: Store<ReadonlyInputElement<T>> constructor(upstream: Store<ReadonlyInputElement<T>>) { super() - this.upstream = upstream; - this.value = upstream.bind(v => v.GetValue()) + this.upstream = upstream + this.value = upstream.bind((v) => v.GetValue()) this.element = new VariableUiElement(upstream) } GetValue(): Store<T> { - return this.value; + return this.value } IsValid(t: T): boolean { - return this.upstream.data.IsValid(t); + return this.upstream.data.IsValid(t) } protected InnerConstructElement(): HTMLElement { - return this.element.ConstructElement(); + return this.element.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/LanguagePicker.ts b/UI/LanguagePicker.ts index 60c8eae58..40c6531b0 100644 --- a/UI/LanguagePicker.ts +++ b/UI/LanguagePicker.ts @@ -1,48 +1,46 @@ -import {DropDown} from "./Input/DropDown"; -import Locale from "./i18n/Locale"; -import BaseUIElement from "./BaseUIElement"; +import { DropDown } from "./Input/DropDown" +import Locale from "./i18n/Locale" +import BaseUIElement from "./BaseUIElement" import * as native from "../assets/language_native.json" import * as language_translations from "../assets/language_translations.json" -import {Translation} from "./i18n/Translation"; +import { Translation } from "./i18n/Translation" import * as used_languages from "../assets/generated/used_languages.json" -import Lazy from "./Base/Lazy"; -import Toggle from "./Input/Toggle"; +import Lazy from "./Base/Lazy" +import Toggle from "./Input/Toggle" export default class LanguagePicker extends Toggle { - - - constructor(languages: string[], - label: string | BaseUIElement = "") { - - + constructor(languages: string[], label: string | BaseUIElement = "") { if (languages === undefined || languages.length <= 1) { - super(undefined,undefined,undefined) - return undefined; + super(undefined, undefined, undefined) + return undefined } - const allLanguages: string[] = used_languages.languages; + const allLanguages: string[] = used_languages.languages - const normalPicker = LanguagePicker.dropdownFor(languages, label); + const normalPicker = LanguagePicker.dropdownFor(languages, label) const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, label)) - super(fullPicker, normalPicker, Locale.showLinkToWeblate); + super(fullPicker, normalPicker, Locale.showLinkToWeblate) } private static dropdownFor(languages: string[], label: string | BaseUIElement): BaseUIElement { - return new DropDown(label, languages - .filter(lang => lang !== "_context") - .map(lang => { - return {value: lang, shown: LanguagePicker.hybrid(lang)} - } - ), Locale.language) + return new DropDown( + label, + languages + .filter((lang) => lang !== "_context") + .map((lang) => { + return { value: lang, shown: LanguagePicker.hybrid(lang) } + }), + Locale.language + ) } private static hybrid(lang: string): Translation { const nativeText = native[lang] ?? lang - const allTranslations = (language_translations["default"] ?? language_translations) + const allTranslations = language_translations["default"] ?? language_translations const translation = {} const trans = allTranslations[lang] if (trans === undefined) { - return new Translation({"*": nativeText}) + return new Translation({ "*": nativeText }) } for (const key in trans) { const translationInKey = allTranslations[lang][key] @@ -51,10 +49,7 @@ export default class LanguagePicker extends Toggle { } else { translation[key] = nativeText + " (" + translationInKey + ")" } - } return new Translation(translation) } - - -} \ No newline at end of file +} diff --git a/UI/MapControlButton.ts b/UI/MapControlButton.ts index f03432952..004aae8ee 100644 --- a/UI/MapControlButton.ts +++ b/UI/MapControlButton.ts @@ -1,20 +1,23 @@ -import BaseUIElement from "./BaseUIElement"; -import Combine from "./Base/Combine"; +import BaseUIElement from "./BaseUIElement" +import Combine from "./Base/Combine" /** * A button floating above the map, in a uniform style */ export default class MapControlButton extends Combine { - constructor(contents: BaseUIElement, options?: { - dontStyle?: boolean - }) { - super([contents]); + constructor( + contents: BaseUIElement, + options?: { + dontStyle?: boolean + } + ) { + super([contents]) if (!options?.dontStyle) { contents.SetClass("mapcontrol p-1") } this.SetClass( "relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background m-0.5 md:m-1" - ); - this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); + ) + this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);") } } diff --git a/UI/NewPoint/ConfirmLocationOfPoint.ts b/UI/NewPoint/ConfirmLocationOfPoint.ts index 88671d310..bd47edc90 100644 --- a/UI/NewPoint/ConfirmLocationOfPoint.ts +++ b/UI/NewPoint/ConfirmLocationOfPoint.ts @@ -1,56 +1,61 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import BaseUIElement from "../BaseUIElement"; -import LocationInput from "../Input/LocationInput"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import {BBox} from "../../Logic/BBox"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import Svg from "../../Svg"; -import Toggle from "../Input/Toggle"; -import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI"; -import BaseLayer from "../../Models/BaseLayer"; -import Img from "../Base/Img"; -import Title from "../Base/Title"; -import {GlobalFilter} from "../../Logic/State/MapState"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Tag} from "../../Logic/Tags/Tag"; +import { UIEventSource } from "../../Logic/UIEventSource" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import BaseUIElement from "../BaseUIElement" +import LocationInput from "../Input/LocationInput" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import { BBox } from "../../Logic/BBox" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { SubtleButton } from "../Base/SubtleButton" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import Svg from "../../Svg" +import Toggle from "../Input/Toggle" +import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI" +import BaseLayer from "../../Models/BaseLayer" +import Img from "../Base/Img" +import Title from "../Base/Title" +import { GlobalFilter } from "../../Logic/State/MapState" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Tag } from "../../Logic/Tags/Tag" export default class ConfirmLocationOfPoint extends Combine { - - constructor( state: { - globalFilters: UIEventSource<GlobalFilter[]>; - featureSwitchIsTesting: UIEventSource<boolean>; - osmConnection: OsmConnection, - featurePipeline: FeaturePipeline, + globalFilters: UIEventSource<GlobalFilter[]> + featureSwitchIsTesting: UIEventSource<boolean> + osmConnection: OsmConnection + featurePipeline: FeaturePipeline backgroundLayer?: UIEventSource<BaseLayer> }, filterViewIsOpened: UIEventSource<boolean>, preset: PresetInfo, confirmText: BaseUIElement, - loc: { lon: number, lat: number }, - confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void, + loc: { lon: number; lat: number }, + confirm: ( + tags: any[], + location: { lat: number; lon: number }, + snapOntoWayId: string + ) => void, cancel: () => void, closePopup: () => void ) { - let preciseInput: LocationInput = undefined if (preset.preciseInput !== undefined) { // Create location input - // We uncouple the event source - const zloc = {...loc, zoom: 19} - const locationSrc = new UIEventSource(zloc); + const zloc = { ...loc, zoom: 19 } + const locationSrc = new UIEventSource(zloc) - let backgroundLayer = new UIEventSource(state?.backgroundLayer?.data ?? AvailableBaseLayers.osmCarto); + let backgroundLayer = new UIEventSource( + state?.backgroundLayer?.data ?? AvailableBaseLayers.osmCarto + ) if (preset.preciseInput.preferredBackground) { - const defaultBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)); + const defaultBackground = AvailableBaseLayers.SelectBestLayerAccordingTo( + locationSrc, + new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground) + ) // Note that we _break the link_ here, as the minimap will take care of the switching! backgroundLayer.setData(defaultBackground.data) } @@ -62,77 +67,89 @@ export default class ConfirmLocationOfPoint extends Combine { mapBounds = new UIEventSource<BBox>(undefined) } - - const tags = TagUtils.KVtoProperties(preset.tags ?? []); + const tags = TagUtils.KVtoProperties(preset.tags ?? []) preciseInput = new LocationInput({ mapBackground: backgroundLayer, centerLocation: locationSrc, snapTo: snapToFeatures, snappedPointTags: tags, maxSnapDistance: preset.preciseInput.maxSnapDistance, - bounds: mapBounds + bounds: mapBounds, }) preciseInput.installBounds(preset.boundsFactor ?? 0.25, true) - preciseInput.SetClass("rounded-xl overflow-hidden border border-gray").SetStyle("height: 18rem; max-height: 50vh") - + preciseInput + .SetClass("rounded-xl overflow-hidden border border-gray") + .SetStyle("height: 18rem; max-height: 50vh") if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) { // We have to snap to certain layers. // Lets fetch them let loadedBbox: BBox = undefined - mapBounds?.addCallbackAndRunD(bbox => { + mapBounds?.addCallbackAndRunD((bbox) => { if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) { // All is already there // return; } - bbox = bbox.pad(Math.max(preset.boundsFactor ?? 0.25, 2), Math.max(preset.boundsFactor ?? 0.25, 2)); - loadedBbox = bbox; + bbox = bbox.pad( + Math.max(preset.boundsFactor ?? 0.25, 2), + Math.max(preset.boundsFactor ?? 0.25, 2) + ) + loadedBbox = bbox const allFeatures: { feature: any }[] = [] - preset.preciseInput.snapToLayers.forEach(layerId => { + preset.preciseInput.snapToLayers.forEach((layerId) => { console.log("Snapping to", layerId) - state.featurePipeline.GetFeaturesWithin(layerId, bbox)?.forEach(feats => allFeatures.push(...feats.map(f => ({feature: f})))) + state.featurePipeline + .GetFeaturesWithin(layerId, bbox) + ?.forEach((feats) => + allFeatures.push(...feats.map((f) => ({ feature: f }))) + ) }) console.log("Snapping to", allFeatures) snapToFeatures.setData(allFeatures) }) } - } - - let confirmButton: BaseUIElement = new SubtleButton(preset.icon(), + let confirmButton: BaseUIElement = new SubtleButton( + preset.icon(), new Combine([ confirmText, - Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") + Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert"), ]).SetClass("flex flex-col") - ).SetClass("font-bold break-words") + ) + .SetClass("font-bold break-words") .onClick(() => { - console.log("The confirmLocationPanel - precise input yielded ", preciseInput?.GetValue()?.data) - const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data.filter(gf => gf.onNewPoint !== undefined) - .map(gf => gf.onNewPoint.tags) - const globalTags : Tag[] = [].concat(...globalFilterTagsToAdd) + console.log( + "The confirmLocationPanel - precise input yielded ", + preciseInput?.GetValue()?.data + ) + const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data + .filter((gf) => gf.onNewPoint !== undefined) + .map((gf) => gf.onNewPoint.tags) + const globalTags: Tag[] = [].concat(...globalFilterTagsToAdd) console.log("Global tags to add are: ", globalTags) - confirm([...preset.tags, ...globalTags], preciseInput?.GetValue()?.data ?? loc, preciseInput?.snappedOnto?.data?.properties?.id); - }); + confirm( + [...preset.tags, ...globalTags], + preciseInput?.GetValue()?.data ?? loc, + preciseInput?.snappedOnto?.data?.properties?.id + ) + }) if (preciseInput !== undefined) { confirmButton = new Combine([preciseInput, confirmButton]) } - const openLayerControl = - new SubtleButton( - Svg.layers_ui(), - new Combine([ - Translations.t.general.add.layerNotEnabled - .Subs({layer: preset.layerToAddTo.layerDef.name}) - .SetClass("alert"), - Translations.t.general.add.openLayerControl - ]) - ) - .onClick(() => filterViewIsOpened.setData(true)) - + const openLayerControl = new SubtleButton( + Svg.layers_ui(), + new Combine([ + Translations.t.general.add.layerNotEnabled + .Subs({ layer: preset.layerToAddTo.layerDef.name }) + .SetClass("alert"), + Translations.t.general.add.openLayerControl, + ]) + ).onClick(() => filterViewIsOpened.setData(true)) let openLayerOrConfirm = new Toggle( confirmButton, @@ -143,73 +160,82 @@ export default class ConfirmLocationOfPoint extends Combine { const disableFilter = new SubtleButton( new Combine([ Svg.filter_ui().SetClass("absolute w-full"), - Svg.cross_bottom_right_svg().SetClass("absolute red-svg") + Svg.cross_bottom_right_svg().SetClass("absolute red-svg"), ]).SetClass("relative"), - new Combine( - [ - Translations.t.general.add.disableFiltersExplanation.Clone(), - Translations.t.general.add.disableFilters.Clone().SetClass("text-xl") - ] - ).SetClass("flex flex-col") + new Combine([ + Translations.t.general.add.disableFiltersExplanation.Clone(), + Translations.t.general.add.disableFilters.Clone().SetClass("text-xl"), + ]).SetClass("flex flex-col") ).onClick(() => { - - const appliedFilters = preset.layerToAddTo.appliedFilters; + const appliedFilters = preset.layerToAddTo.appliedFilters appliedFilters.data.forEach((_, k) => appliedFilters.data.set(k, undefined)) appliedFilters.ping() cancel() closePopup() }) - // We assume the number of global filters won't change during the run of the program for (let i = 0; i < state.globalFilters.data.length; i++) { - const hasBeenCheckedOf = new UIEventSource(false); + const hasBeenCheckedOf = new UIEventSource(false) const filterConfirmPanel = new VariableUiElement( - state.globalFilters.map(gfs => { - const gf = gfs[i] - const confirm = gf.onNewPoint?.confirmAddNew?.Subs({preset: preset.title}) - return new Combine([ - gf.onNewPoint?.safetyCheck, - new SubtleButton(Svg.confirm_svg(), confirm).onClick(() => hasBeenCheckedOf.setData(true)) - ]) - } - )) - + state.globalFilters.map((gfs) => { + const gf = gfs[i] + const confirm = gf.onNewPoint?.confirmAddNew?.Subs({ preset: preset.title }) + return new Combine([ + gf.onNewPoint?.safetyCheck, + new SubtleButton(Svg.confirm_svg(), confirm).onClick(() => + hasBeenCheckedOf.setData(true) + ), + ]) + }) + ) openLayerOrConfirm = new Toggle( - openLayerOrConfirm, filterConfirmPanel, - state.globalFilters.map(f => hasBeenCheckedOf.data || f[i]?.onNewPoint === undefined, [hasBeenCheckedOf]) + openLayerOrConfirm, + filterConfirmPanel, + state.globalFilters.map( + (f) => hasBeenCheckedOf.data || f[i]?.onNewPoint === undefined, + [hasBeenCheckedOf] + ) ) } - const hasActiveFilter = preset.layerToAddTo.appliedFilters - .map(appliedFilters => { - const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined); - return activeFilters.length === 0; - }) + const hasActiveFilter = preset.layerToAddTo.appliedFilters.map((appliedFilters) => { + const activeFilters = Array.from(appliedFilters.values()).filter( + (f) => f?.currentFilter !== undefined + ) + return activeFilters.length === 0 + }) // If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled const disableFiltersOrConfirm = new Toggle( openLayerOrConfirm, disableFilter, - hasActiveFilter) + hasActiveFilter + ) + const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection) - const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection); - - const cancelButton = new SubtleButton(Svg.close_ui(), + const cancelButton = new SubtleButton( + Svg.close_ui(), Translations.t.general.cancel ).onClick(cancel) - - let examples: BaseUIElement = undefined; + let examples: BaseUIElement = undefined if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) { examples = new Combine([ - new Title(preset.exampleImages.length == 1 ? Translations.t.general.example : Translations.t.general.examples), - new Combine(preset.exampleImages.map(img => new Img(img).SetClass("h-64 m-1 w-auto rounded-lg"))).SetClass("flex flex-wrap items-stretch") + new Title( + preset.exampleImages.length == 1 + ? Translations.t.general.example + : Translations.t.general.examples + ), + new Combine( + preset.exampleImages.map((img) => + new Img(img).SetClass("h-64 m-1 w-auto rounded-lg") + ) + ).SetClass("flex flex-wrap items-stretch"), ]) - } super([ @@ -222,12 +248,9 @@ export default class ConfirmLocationOfPoint extends Combine { cancelButton, preset.description, examples, - tagInfo - + tagInfo, ]) this.SetClass("flex flex-col") - } - -} \ No newline at end of file +} diff --git a/UI/OpeningHours/OpeningHours.ts b/UI/OpeningHours/OpeningHours.ts index 57e4683e6..8587609b2 100644 --- a/UI/OpeningHours/OpeningHours.ts +++ b/UI/OpeningHours/OpeningHours.ts @@ -1,11 +1,11 @@ -import {Utils} from "../../Utils"; -import opening_hours from "opening_hours"; +import { Utils } from "../../Utils" +import opening_hours from "opening_hours" export interface OpeningHour { - weekday: number, // 0 is monday, 1 is tuesday, ... - startHour: number, - startMinutes: number, - endHour: number, + weekday: number // 0 is monday, 1 is tuesday, ... + startHour: number + startMinutes: number + endHour: number endMinutes: number } @@ -13,8 +13,6 @@ export interface OpeningHour { * Various utilities manipulating opening hours */ export class OH { - - private static readonly days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] private static readonly daysIndexed = { mo: 0, @@ -23,30 +21,30 @@ export class OH { th: 3, fr: 4, sa: 5, - su: 6 + su: 6, } public static hhmm(h: number, m: number): string { if (h == 24) { - return "00:00"; + return "00:00" } - return Utils.TwoDigits(h) + ":" + Utils.TwoDigits(m); + return Utils.TwoDigits(h) + ":" + Utils.TwoDigits(m) } /** - * const rules = [{weekday: 6,endHour: 17,endMinutes: 0,startHour: 13,startMinutes: 0}, - * {weekday: 1,endHour: 12,endMinutes: 0,startHour: 10,startMinutes: 0}] - * OH.ToString(rules) // => "Tu 10:00-12:00; Su 13:00-17:00" - * - * const rules = [{weekday: 3,endHour: 17,endMinutes: 0,startHour: 13,startMinutes: 0}, {weekday: 1,endHour: 12,endMinutes: 0,startHour: 10,startMinutes: 0}] - * OH.ToString(rules) // => "Tu 10:00-12:00; Th 13:00-17:00" - * - * const rules = [ { weekday: 1, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0 }, { weekday: 1, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }]); - * OH.ToString(rules) // => "Tu 10:00-12:00, 13:00-17:00" - * - * const rules = [ { weekday: 0, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }, { weekday: 0, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0}, { weekday: 1, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0 }, { weekday: 1, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }]; - * OH.ToString(rules) // => "Mo-Tu 10:00-12:00, 13:00-17:00" - * + * const rules = [{weekday: 6,endHour: 17,endMinutes: 0,startHour: 13,startMinutes: 0}, + * {weekday: 1,endHour: 12,endMinutes: 0,startHour: 10,startMinutes: 0}] + * OH.ToString(rules) // => "Tu 10:00-12:00; Su 13:00-17:00" + * + * const rules = [{weekday: 3,endHour: 17,endMinutes: 0,startHour: 13,startMinutes: 0}, {weekday: 1,endHour: 12,endMinutes: 0,startHour: 10,startMinutes: 0}] + * OH.ToString(rules) // => "Tu 10:00-12:00; Th 13:00-17:00" + * + * const rules = [ { weekday: 1, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0 }, { weekday: 1, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }]); + * OH.ToString(rules) // => "Tu 10:00-12:00, 13:00-17:00" + * + * const rules = [ { weekday: 0, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }, { weekday: 0, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0}, { weekday: 1, endHour: 17, endMinutes: 0, startHour: 13, startMinutes: 0 }, { weekday: 1, endHour: 12, endMinutes: 0, startHour: 10, startMinutes: 0 }]; + * OH.ToString(rules) // => "Mo-Tu 10:00-12:00, 13:00-17:00" + * * // should merge overlapping opening hours * const timerange0 = {weekday: 1, endHour: 23, endMinutes: 30, startHour: 23, startMinutes: 0 } * const touchingTimeRange = { weekday: 1, endHour: 0, endMinutes: 0, startHour: 23, startMinutes: 30 } @@ -56,64 +54,59 @@ export class OH { * const timerange0 = {weekday: 1, endHour: 23, endMinutes: 30, startHour: 23, startMinutes: 0 } * const overlappingTimeRange = { weekday: 1, endHour: 24, endMinutes: 0, startHour: 23, startMinutes: 30 } * OH.ToString(OH.MergeTimes([timerange0, overlappingTimeRange])) // => "Tu 23:00-00:00" - * - */ - + * + */ + public static ToString(ohs: OpeningHour[]) { if (ohs.length == 0) { - return ""; + return "" } - const partsPerWeekday: string [][] = [[], [], [], [], [], [], []]; - + const partsPerWeekday: string[][] = [[], [], [], [], [], [], []] for (const oh of ohs) { - partsPerWeekday[oh.weekday].push(OH.hhmm(oh.startHour, oh.startMinutes) + "-" + OH.hhmm(oh.endHour, oh.endMinutes)); + partsPerWeekday[oh.weekday].push( + OH.hhmm(oh.startHour, oh.startMinutes) + "-" + OH.hhmm(oh.endHour, oh.endMinutes) + ) } - const stringPerWeekday = partsPerWeekday.map(parts => parts.sort().join(", ")); + const stringPerWeekday = partsPerWeekday.map((parts) => parts.sort().join(", ")) - const rules = []; + const rules = [] - let rangeStart = 0; - let rangeEnd = 0; + let rangeStart = 0 + let rangeEnd = 0 function pushRule() { - const rule = stringPerWeekday[rangeStart]; + const rule = stringPerWeekday[rangeStart] if (rule === "") { - return; + return } - if (rangeStart == (rangeEnd - 1)) { - rules.push( - `${OH.days[rangeStart]} ${rule}` - ); + if (rangeStart == rangeEnd - 1) { + rules.push(`${OH.days[rangeStart]} ${rule}`) } else { - rules.push( - `${OH.days[rangeStart]}-${OH.days[rangeEnd - 1]} ${rule}` - ); + rules.push(`${OH.days[rangeStart]}-${OH.days[rangeEnd - 1]} ${rule}`) } } for (; rangeEnd < 7; rangeEnd++) { - if (stringPerWeekday[rangeStart] != stringPerWeekday[rangeEnd]) { - pushRule(); + pushRule() rangeStart = rangeEnd } - } - pushRule(); + pushRule() const oh = rules.join("; ") if (oh === "Mo-Su 00:00-00:00") { return "24/7" } - return oh; + return oh } /** * Merge duplicate opening-hour element in place. * Returns true if something changed - * + * * // should merge overlapping opening hours * const oh1: OpeningHour = { weekday: 0, startHour: 10, startMinutes: 0, endHour: 11, endMinutes: 0 }; * const oh0: OpeningHour = { weekday: 0, startHour: 10, startMinutes: 30, endHour: 12, endMinutes: 0 }; @@ -125,60 +118,70 @@ export class OH { * OH.MergeTimes([oh0, oh1]) // => [{ weekday: 0, startHour: 10, startMinutes: 0, endHour: 12, endMinutes: 0 }] */ public static MergeTimes(ohs: OpeningHour[]): OpeningHour[] { - const queue = ohs.map(oh => { + const queue = ohs.map((oh) => { if (oh.endHour === 0 && oh.endMinutes === 0) { const newOh = { - ...oh + ...oh, } newOh.endHour = 24 return newOh } - return oh; - }); - const newList = []; + return oh + }) + const newList = [] while (queue.length > 0) { - let maybeAdd = queue.pop(); + let maybeAdd = queue.pop() - let doAddEntry = true; + let doAddEntry = true if (maybeAdd.weekday == undefined) { - doAddEntry = false; + doAddEntry = false } for (let i = newList.length - 1; i >= 0 && doAddEntry; i--) { - let guard = newList[i]; + let guard = newList[i] if (maybeAdd.weekday != guard.weekday) { // Not the same day continue } - if (OH.startTimeLiesInRange(maybeAdd, guard) && OH.endTimeLiesInRange(maybeAdd, guard)) { + if ( + OH.startTimeLiesInRange(maybeAdd, guard) && + OH.endTimeLiesInRange(maybeAdd, guard) + ) { // Guard fully covers 'maybeAdd': we can safely ignore maybeAdd - doAddEntry = false; - break; + doAddEntry = false + break } - if (OH.startTimeLiesInRange(guard, maybeAdd) && OH.endTimeLiesInRange(guard, maybeAdd)) { + if ( + OH.startTimeLiesInRange(guard, maybeAdd) && + OH.endTimeLiesInRange(guard, maybeAdd) + ) { // 'maybeAdd' fully covers Guard - the guard is killed - newList.splice(i, 1); - break; + newList.splice(i, 1) + break } - if (OH.startTimeLiesInRange(maybeAdd, guard) || OH.endTimeLiesInRange(maybeAdd, guard) - || OH.startTimeLiesInRange(guard, maybeAdd) || OH.endTimeLiesInRange(guard, maybeAdd)) { + if ( + OH.startTimeLiesInRange(maybeAdd, guard) || + OH.endTimeLiesInRange(maybeAdd, guard) || + OH.startTimeLiesInRange(guard, maybeAdd) || + OH.endTimeLiesInRange(guard, maybeAdd) + ) { // At this point, the maybeAdd overlaps the guard: we should extend the guard and retest it - newList.splice(i, 1); - let startHour = guard.startHour; - let startMinutes = guard.startMinutes; + newList.splice(i, 1) + let startHour = guard.startHour + let startMinutes = guard.startMinutes if (OH.startTime(maybeAdd) < OH.startTime(guard)) { - startHour = maybeAdd.startHour; - startMinutes = maybeAdd.startMinutes; + startHour = maybeAdd.startHour + startMinutes = maybeAdd.startMinutes } - let endHour = guard.endHour; - let endMinutes = guard.endMinutes; + let endHour = guard.endHour + let endMinutes = guard.endMinutes if (OH.endTime(maybeAdd) > OH.endTime(guard)) { - endHour = maybeAdd.endHour; - endMinutes = maybeAdd.endMinutes; + endHour = maybeAdd.endHour + endMinutes = maybeAdd.endMinutes } queue.push({ @@ -186,16 +189,15 @@ export class OH { startMinutes: startMinutes, endHour: endHour, endMinutes: endMinutes, - weekday: guard.weekday - }); + weekday: guard.weekday, + }) - doAddEntry = false; - break; + doAddEntry = false + break } - } if (doAddEntry) { - newList.push(maybeAdd); + newList.push(maybeAdd) } } @@ -203,11 +205,10 @@ export class OH { // This means that the list is changed only if the lengths are different. // If the lengths are the same, we might just as well return the old list and be a bit more stable if (newList.length !== ohs.length) { - return newList; + return newList } else { - return ohs; + return ohs } - } /** @@ -217,107 +218,116 @@ export class OH { * @param oh */ public static startTime(oh: OpeningHour): number { - return oh.startHour + oh.startMinutes / 60; + return oh.startHour + oh.startMinutes / 60 } public static endTime(oh: OpeningHour): number { - return oh.endHour + oh.endMinutes / 60; + return oh.endHour + oh.endMinutes / 60 } public static startTimeLiesInRange(checked: OpeningHour, mightLieIn: OpeningHour) { - return OH.startTime(mightLieIn) <= OH.startTime(checked) && + return ( + OH.startTime(mightLieIn) <= OH.startTime(checked) && OH.startTime(checked) <= OH.endTime(mightLieIn) + ) } public static endTimeLiesInRange(checked: OpeningHour, mightLieIn: OpeningHour) { - return OH.startTime(mightLieIn) <= OH.endTime(checked) && + return ( + OH.startTime(mightLieIn) <= OH.endTime(checked) && OH.endTime(checked) <= OH.endTime(mightLieIn) + ) } public static parseHHMMRange(hhmmhhmm: string): { - startHour: number, - startMinutes: number, - endHour: number, + startHour: number + startMinutes: number + endHour: number endMinutes: number } { if (hhmmhhmm == "off") { - return null; + return null } - const timings = hhmmhhmm.split("-"); + const timings = hhmmhhmm.split("-") const start = OH.parseHHMM(timings[0]) - const end = OH.parseHHMM(timings[1]); + const end = OH.parseHHMM(timings[1]) return { startHour: start.hours, startMinutes: start.minutes, endHour: end.hours, - endMinutes: end.minutes + endMinutes: end.minutes, } } /** * Converts an OH-syntax rule into an object - * - * + * + * * const rules = OH.ParsePHRule("PH 12:00-17:00") * rules.mode // => " " * rules.start // => "12:00" * rules.end // => "17:00" - * + * * OH.ParseRule("PH 12:00-17:00") // => null * OH.ParseRule("Th[-1] off") // => null - * + * * const rules = OH.Parse("24/7"); * rules.length // => 7 * rules[0].startHour // => 0 * OH.ToString(rules) // => "24/7" - * + * * const rules = OH.ParseRule("11:00-19:00"); * rules.length // => 7 * rules[0].weekday // => 0 * rules[0].startHour // => 11 * rules[3].endHour // => 19 - * + * * const rules = OH.ParseRule("Mo-Th 11:00-19:00"); * rules.length // => 4 * rules[0].weekday // => 0 * rules[0].startHour // => 11 * rules[3].endHour // => 19 - * + * */ public static ParseRule(rule: string): OpeningHour[] { try { if (rule.trim() == "24/7") { - return OH.multiply([0, 1, 2, 3, 4, 5, 6], [{ - startHour: 0, - startMinutes: 0, - endHour: 24, - endMinutes: 0 - }]); + return OH.multiply( + [0, 1, 2, 3, 4, 5, 6], + [ + { + startHour: 0, + startMinutes: 0, + endHour: 24, + endMinutes: 0, + }, + ] + ) } - const split = rule.trim().replace(/, */g, ",").split(" "); + const split = rule.trim().replace(/, */g, ",").split(" ") if (split.length == 1) { // First, try to parse this rule as a rule without weekdays - let timeranges = OH.ParseHhmmRanges(rule); - let weekdays = [0, 1, 2, 3, 4, 5, 6]; - return OH.multiply(weekdays, timeranges); + let timeranges = OH.ParseHhmmRanges(rule) + let weekdays = [0, 1, 2, 3, 4, 5, 6] + return OH.multiply(weekdays, timeranges) } if (split.length == 2) { - const weekdays = OH.ParseWeekdayRanges(split[0]); - const timeranges = OH.ParseHhmmRanges(split[1]); - return OH.multiply(weekdays, timeranges); + const weekdays = OH.ParseWeekdayRanges(split[0]) + const timeranges = OH.ParseHhmmRanges(split[1]) + return OH.multiply(weekdays, timeranges) } - return null; + return null } catch (e) { - console.log("Could not parse weekday rule ", rule); - return null; + console.log("Could not parse weekday rule ", rule) + return null } } /** - * + * * OH.ParsePHRule("PH Off") // => {mode: "off"} * OH.ParsePHRule("PH OPEN") // => {mode: "open"} * OH.ParsePHRule("PH 10:00-12:00") // => {mode: " ", start: "10:00", end: "12:00"} @@ -326,49 +336,47 @@ export class OH { * OH.ParsePHRule("some random string") // => null */ public static ParsePHRule(str: string): { - mode: string, - start?: string, + mode: string + start?: string end?: string } { if (str === undefined || str === null) { return null } - str = str.trim(); + str = str.trim() if (!str.startsWith("PH")) { - return null; + return null } - str = str.trim(); + str = str.trim() if (str.toLowerCase() === "ph off") { return { - mode: "off" + mode: "off", } } if (str.toLowerCase() === "ph open") { return { - mode: "open" + mode: "open", } } if (!str.startsWith("PH ")) { - return null; + return null } try { - - const timerange = OH.parseHHMMRange(str.substring(2)); + const timerange = OH.parseHHMMRange(str.substring(2)) if (timerange === null) { - return null; + return null } return { mode: " ", start: OH.hhmm(timerange.startHour, timerange.startMinutes), end: OH.hhmm(timerange.endHour, timerange.endMinutes), - } } catch (e) { - return null; + return null } } @@ -386,23 +394,23 @@ export class OH { const ohs: OpeningHour[] = [] - const split = rules.split(";"); + const split = rules.split(";") for (const rule of split) { if (rule === "") { - continue; + continue } try { const parsed = OH.ParseRule(rule) if (parsed !== null) { - ohs.push(...parsed); + ohs.push(...parsed) } } catch (e) { console.error("Could not parse ", rule, ": ", e) } } - return ohs; + return ohs } /* @@ -414,222 +422,247 @@ export class OH { This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00 This list will be sorted */ - public static allChangeMoments(ranges: { - isOpen: boolean, - isSpecial: boolean, - comment: string, - startDate: Date, - endDate: Date - }[][]): [number[], string[]] { + public static allChangeMoments( + ranges: { + isOpen: boolean + isSpecial: boolean + comment: string + startDate: Date + endDate: Date + }[][] + ): [number[], string[]] { const changeHours: number[] = [] - const changeHourText: string[] = []; + const changeHourText: string[] = [] const extrachangeHours: number[] = [] - const extrachangeHourText: string[] = []; + const extrachangeHourText: string[] = [] for (const weekday of ranges) { for (const range of weekday) { if (!range.isOpen && !range.isSpecial) { - continue; + continue } - const startOfDay: Date = new Date(range.startDate); - startOfDay.setHours(0, 0, 0, 0); + const startOfDay: Date = new Date(range.startDate) + startOfDay.setHours(0, 0, 0, 0) // The number of seconds since the start of the day // @ts-ignore - const changeMoment: number = (range.startDate - startOfDay) / 1000; + const changeMoment: number = (range.startDate - startOfDay) / 1000 if (changeHours.indexOf(changeMoment) < 0) { - changeHours.push(changeMoment); - changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes())) + changeHours.push(changeMoment) + changeHourText.push( + OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes()) + ) } // The number of seconds till between the start of the day and closing // @ts-ignore - let changeMomentEnd: number = (range.endDate - startOfDay) / 1000; + let changeMomentEnd: number = (range.endDate - startOfDay) / 1000 if (changeMomentEnd >= 24 * 60 * 60) { if (extrachangeHours.indexOf(changeMomentEnd) < 0) { - extrachangeHours.push(changeMomentEnd); - extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) + extrachangeHours.push(changeMomentEnd) + extrachangeHourText.push( + OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes()) + ) } } else if (changeHours.indexOf(changeMomentEnd) < 0) { - changeHours.push(changeMomentEnd); - changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) + changeHours.push(changeMomentEnd) + changeHourText.push( + OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes()) + ) } } } // Note that 'changeHours' and 'changeHourText' will be more or less in sync - one is in numbers, the other in 'HH:MM' format. // But both can be sorted without problem; they'll stay in sync - changeHourText.sort(); - changeHours.sort(); - extrachangeHourText.sort(); - extrachangeHours.sort(); + changeHourText.sort() + changeHours.sort() + extrachangeHourText.sort() + extrachangeHours.sort() - changeHourText.push(...extrachangeHourText); - changeHours.push(...extrachangeHours); + changeHourText.push(...extrachangeHourText) + changeHours.push(...extrachangeHours) return [changeHours, changeHourText] } - public static CreateOhObject(tags: object & {_lat: number, _lon: number, _country?: string}, textToParse: string){ + public static CreateOhObject( + tags: object & { _lat: number; _lon: number; _country?: string }, + textToParse: string + ) { // noinspection JSPotentiallyInvalidConstructorUsage - return new opening_hours(textToParse, { - lat: tags._lat, - lon: tags._lon, - address: { - country_code: tags._country.toLowerCase(), - state: undefined + return new opening_hours( + textToParse, + { + lat: tags._lat, + lon: tags._lon, + address: { + country_code: tags._country.toLowerCase(), + state: undefined, + }, }, - }, <any> {tag_key: "opening_hours"}); + <any>{ tag_key: "opening_hours" } + ) } - + /* Calculates when the business is opened (or on holiday) between two dates. Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ... */ - public static GetRanges(oh: any, from: Date, to: Date): ({ - isOpen: boolean, - isSpecial: boolean, - comment: string, - startDate: Date, + public static GetRanges( + oh: any, + from: Date, + to: Date + ): { + isOpen: boolean + isSpecial: boolean + comment: string + startDate: Date endDate: Date - }[])[] { + }[][] { + const values = [[], [], [], [], [], [], []] - - const values = [[], [], [], [], [], [], []]; - - const start = new Date(from); + const start = new Date(from) // We go one day more into the past, in order to force rendering of holidays in the start of the period - start.setDate(from.getDate() - 1); + start.setDate(from.getDate() - 1) - const iterator = oh.getIterator(start); + const iterator = oh.getIterator(start) - let prevValue = undefined; + let prevValue = undefined while (iterator.advance(to)) { - if (prevValue) { prevValue.endDate = iterator.getDate() as Date } - const endDate = new Date(iterator.getDate()) as Date; + const endDate = new Date(iterator.getDate()) as Date endDate.setHours(0, 0, 0, 0) - endDate.setDate(endDate.getDate() + 1); + endDate.setDate(endDate.getDate() + 1) const value = { isSpecial: iterator.getUnknown(), isOpen: iterator.getState(), comment: iterator.getComment(), startDate: iterator.getDate() as Date, - endDate: endDate // Should be overwritten by the next iteration + endDate: endDate, // Should be overwritten by the next iteration } - prevValue = value; + prevValue = value if (value.comment === undefined && !value.isOpen && !value.isSpecial) { // simply closed, nothing special here - continue; + continue } if (value.startDate < from) { - continue; + continue } // Get day: sunday is 0, monday is 1. We move everything so that monday == 0 - values[(value.startDate.getDay() + 6) % 7].push(value); + values[(value.startDate.getDay() + 6) % 7].push(value) } - return values; + return values } - private static parseHHMM(hhmm: string): { hours: number, minutes: number } { + private static parseHHMM(hhmm: string): { hours: number; minutes: number } { if (hhmm === undefined || hhmm == null) { - return null; + return null } - const spl = hhmm.trim().split(":"); + const spl = hhmm.trim().split(":") if (spl.length != 2) { - return null; + return null } - const hm = {hours: Number(spl[0].trim()), minutes: Number(spl[1].trim())}; + const hm = { hours: Number(spl[0].trim()), minutes: Number(spl[1].trim()) } if (isNaN(hm.hours) || isNaN(hm.minutes)) { - return null; + return null } - return hm; + return hm } private static ParseHhmmRanges(hhmms: string): { - startHour: number, - startMinutes: number, - endHour: number, + startHour: number + startMinutes: number + endHour: number endMinutes: number }[] { if (hhmms === "off") { - return []; + return [] } - return hhmms.split(",") - .map(s => s.trim()) - .filter(str => str !== "") + return hhmms + .split(",") + .map((s) => s.trim()) + .filter((str) => str !== "") .map(OH.parseHHMMRange) - .filter(v => v != null) + .filter((v) => v != null) } private static ParseWeekday(weekday: string): number { - return OH.daysIndexed[weekday.trim().toLowerCase()]; + return OH.daysIndexed[weekday.trim().toLowerCase()] } private static ParseWeekdayRange(weekdays: string): number[] { - const split = weekdays.split("-"); + const split = weekdays.split("-") if (split.length == 1) { - const parsed = OH.ParseWeekday(weekdays); + const parsed = OH.ParseWeekday(weekdays) if (parsed == null) { - return null; + return null } - return [parsed]; + return [parsed] } else if (split.length == 2) { - let start = OH.ParseWeekday(split[0]); - let end = OH.ParseWeekday(split[1]); + let start = OH.ParseWeekday(split[0]) + let end = OH.ParseWeekday(split[1]) if ((start ?? null) === null || (end ?? null) === null) { - return null; + return null } - let range = []; + let range = [] for (let i = start; i <= end; i++) { - range.push(i); + range.push(i) } - return range; + return range } else { - return null; + return null } } private static ParseWeekdayRanges(weekdays: string): number[] { - let ranges = []; - let split = weekdays.split(","); + let ranges = [] + let split = weekdays.split(",") for (const weekday of split) { const parsed = OH.ParseWeekdayRange(weekday) if (parsed === undefined || parsed === null) { - return null; + return null } - ranges.push(...parsed); + ranges.push(...parsed) } - return ranges; + return ranges } - private static multiply(weekdays: number[], timeranges: { startHour: number, startMinutes: number, endHour: number, endMinutes: number }[]) { + private static multiply( + weekdays: number[], + timeranges: { + startHour: number + startMinutes: number + endHour: number + endMinutes: number + }[] + ) { if ((weekdays ?? null) == null || (timeranges ?? null) == null) { - return null; + return null } const ohs: OpeningHour[] = [] for (const timerange of timeranges) { for (const weekday of weekdays) { ohs.push({ weekday: weekday, - startHour: timerange.startHour, startMinutes: timerange.startMinutes, - endHour: timerange.endHour, endMinutes: timerange.endMinutes, - }); + startHour: timerange.startHour, + startMinutes: timerange.startMinutes, + endHour: timerange.endHour, + endMinutes: timerange.endMinutes, + }) } } - return ohs; + return ohs } public static getMondayBefore(d) { - d = new Date(d); - const day = d.getDay(); - const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday - return new Date(d.setDate(diff)); + d = new Date(d) + const day = d.getDay() + const diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday + return new Date(d.setDate(diff)) } - } - diff --git a/UI/OpeningHours/OpeningHoursInput.ts b/UI/OpeningHours/OpeningHoursInput.ts index ab0b0f62e..d301a19cd 100644 --- a/UI/OpeningHours/OpeningHoursInput.ts +++ b/UI/OpeningHours/OpeningHoursInput.ts @@ -3,147 +3,150 @@ * Keeps track of unparsed rules * Exports everything conventiently as a string, for direct use */ -import OpeningHoursPicker from "./OpeningHoursPicker"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {OH, OpeningHour} from "./OpeningHours"; -import {InputElement} from "../Input/InputElement"; -import PublicHolidayInput from "./PublicHolidayInput"; -import Translations from "../i18n/Translations"; -import BaseUIElement from "../BaseUIElement"; - +import OpeningHoursPicker from "./OpeningHoursPicker" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { VariableUiElement } from "../Base/VariableUIElement" +import Combine from "../Base/Combine" +import { FixedUiElement } from "../Base/FixedUiElement" +import { OH, OpeningHour } from "./OpeningHours" +import { InputElement } from "../Input/InputElement" +import PublicHolidayInput from "./PublicHolidayInput" +import Translations from "../i18n/Translations" +import BaseUIElement from "../BaseUIElement" export default class OpeningHoursInput extends InputElement<string> { + public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) + private readonly _value: UIEventSource<string> + private readonly _element: BaseUIElement - - public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private readonly _value: UIEventSource<string>; - private readonly _element: BaseUIElement; - - constructor(value: UIEventSource<string> = new UIEventSource<string>(""), prefix = "", postfix = "") { - super(); - this._value = value; + constructor( + value: UIEventSource<string> = new UIEventSource<string>(""), + prefix = "", + postfix = "" + ) { + super() + this._value = value let valueWithoutPrefix = value if (prefix !== "" && postfix !== "") { - valueWithoutPrefix = value.sync(str => { - if (str === undefined) { - return undefined; - } - if (str === "") { - return "" - } - if (str.startsWith(prefix) && str.endsWith(postfix)) { - return str.substring(prefix.length, str.length - postfix.length) - } - return str - }, [], - noPrefix => { - if (noPrefix === undefined) { - return undefined; - } - if (noPrefix === "") { - return "" - } - if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) { - return noPrefix - } + valueWithoutPrefix = value.sync( + (str) => { + if (str === undefined) { + return undefined + } + if (str === "") { + return "" + } + if (str.startsWith(prefix) && str.endsWith(postfix)) { + return str.substring(prefix.length, str.length - postfix.length) + } + return str + }, + [], + (noPrefix) => { + if (noPrefix === undefined) { + return undefined + } + if (noPrefix === "") { + return "" + } + if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) { + return noPrefix + } - return prefix + noPrefix + postfix - }) + return prefix + noPrefix + postfix + } + ) } - const leftoverRules: Store<string[]> = valueWithoutPrefix.map(str => { + const leftoverRules: Store<string[]> = valueWithoutPrefix.map((str) => { if (str === undefined) { return [] } - const leftOvers: string[] = []; - const rules = str.split(";"); + const leftOvers: string[] = [] + const rules = str.split(";") for (const rule of rules) { if (OH.ParseRule(rule) !== null) { - continue; + continue } if (OH.ParsePHRule(rule) !== null) { - continue; + continue } - leftOvers.push(rule); + leftOvers.push(rule) } - return leftOvers; + return leftOvers }) - - let ph = ""; - const rules = valueWithoutPrefix.data?.split(";") ?? []; + + let ph = "" + const rules = valueWithoutPrefix.data?.split(";") ?? [] for (const rule of rules) { if (OH.ParsePHRule(rule) !== null) { - ph = rule; - break; + ph = rule + break } } - const phSelector = new PublicHolidayInput(new UIEventSource<string>(ph)); - - + const phSelector = new PublicHolidayInput(new UIEventSource<string>(ph)) + // Note: MUST be bound AFTER the leftover rules! - const rulesFromOhPicker: UIEventSource<OpeningHour[]> = valueWithoutPrefix.sync(str => { - return OH.Parse(str); - }, [leftoverRules, phSelector.GetValue()], (rules, oldString) => { - // We always add a ';', to easily add new rules. We remove the ';' again at the end of the function - // Important: spaces are _not_ allowed after a ';' as it'll destabilize the parsing! - let str = OH.ToString(rules) + ";" - const ph = phSelector.GetValue().data; - if(ph){ - str += ph + ";" + const rulesFromOhPicker: UIEventSource<OpeningHour[]> = valueWithoutPrefix.sync( + (str) => { + return OH.Parse(str) + }, + [leftoverRules, phSelector.GetValue()], + (rules, oldString) => { + // We always add a ';', to easily add new rules. We remove the ';' again at the end of the function + // Important: spaces are _not_ allowed after a ';' as it'll destabilize the parsing! + let str = OH.ToString(rules) + ";" + const ph = phSelector.GetValue().data + if (ph) { + str += ph + ";" + } + + str += leftoverRules.data.join(";") + ";" + + str = str.trim() + if (str.endsWith(";")) { + str = str.substring(0, str.length - 1) + } + if (str.startsWith(";")) { + str = str.substring(1) + } + str.trim() + + if (str === oldString) { + return oldString // We pass a reference to the old string to stabilize the EventSource + } + return str } - - str += leftoverRules.data.join(";") + ";" - - str = str.trim() - if(str.endsWith(";")){ - str = str.substring(0, str.length - 1) - } - if(str.startsWith(";")){ - str = str.substring(1) - } - str.trim() - - if(str === oldString){ - return oldString; // We pass a reference to the old string to stabilize the EventSource - } - return str; - }); + ) + const leftoverWarning = new VariableUiElement( + leftoverRules.map((leftovers: string[]) => { + if (leftovers.length == 0) { + return "" + } + return new Combine([ + Translations.t.general.opening_hours.not_all_rules_parsed, + new FixedUiElement(leftovers.map((r) => `${r}<br/>`).join("")).SetClass( + "subtle" + ), + ]) + }) + ) - const leftoverWarning = new VariableUiElement(leftoverRules.map((leftovers: string[]) => { + const ohPicker = new OpeningHoursPicker(rulesFromOhPicker) - if (leftovers.length == 0) { - return ""; - } - return new Combine([ - Translations.t.general.opening_hours.not_all_rules_parsed, - new FixedUiElement(leftovers.map(r => `${r}<br/>`).join("")).SetClass("subtle") - ]); - - })) - - const ohPicker = new OpeningHoursPicker(rulesFromOhPicker); - - this._element = new Combine([ - leftoverWarning, - ohPicker, - phSelector - ]) + this._element = new Combine([leftoverWarning, ohPicker, phSelector]) } GetValue(): UIEventSource<string> { - return this._value; + return this._value } IsValid(t: string): boolean { - return true; + return true } protected InnerConstructElement(): HTMLElement { return this._element.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/OpeningHours/OpeningHoursPicker.ts b/UI/OpeningHours/OpeningHoursPicker.ts index 54f1676af..ea1c47d92 100644 --- a/UI/OpeningHours/OpeningHoursPicker.ts +++ b/UI/OpeningHours/OpeningHoursPicker.ts @@ -1,29 +1,28 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; -import {OH, OpeningHour} from "./OpeningHours"; -import {InputElement} from "../Input/InputElement"; -import BaseUIElement from "../BaseUIElement"; +import { UIEventSource } from "../../Logic/UIEventSource" +import OpeningHoursPickerTable from "./OpeningHoursPickerTable" +import { OH, OpeningHour } from "./OpeningHours" +import { InputElement } from "../Input/InputElement" +import BaseUIElement from "../BaseUIElement" export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { - public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private readonly _ohs: UIEventSource<OpeningHour[]>; - private readonly _backgroundTable: OpeningHoursPickerTable; - + public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) + private readonly _ohs: UIEventSource<OpeningHour[]> + private readonly _backgroundTable: OpeningHoursPickerTable constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) { - super(); - this._ohs = ohs; + super() + this._ohs = ohs - ohs.addCallback(oh => { - ohs.setData(OH.MergeTimes(oh)); + ohs.addCallback((oh) => { + ohs.setData(OH.MergeTimes(oh)) }) - this._backgroundTable = new OpeningHoursPickerTable(this._ohs); + this._backgroundTable = new OpeningHoursPickerTable(this._ohs) this._backgroundTable.ConstructElement() } InnerRender(): BaseUIElement { - return this._backgroundTable; + return this._backgroundTable } GetValue(): UIEventSource<OpeningHour[]> { @@ -31,11 +30,11 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { } IsValid(t: OpeningHour[]): boolean { - return true; + return true } /** - * + * * const rules = OH.ParseRule("Jul-Aug Sa closed; Mo,Tu,Th,Fr,PH 12:00-22:30, We 17:00-22:30, Sa 14:00-19:00, Su 10:00-21:00; Dec 24,25,31 off; Jan 1 off") * const v = new UIEventSource(rules) * const ohpicker = new OpeningHoursPicker(v) @@ -43,7 +42,6 @@ export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { * html !== undefined // => true */ protected InnerConstructElement(): HTMLElement { - return this._backgroundTable.ConstructElement(); + return this._backgroundTable.ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index c2388abcb..e2a6cfade 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -2,61 +2,61 @@ * This is the base-table which is selectable by hovering over it. * It will genarate the currently selected opening hour. */ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; -import {OpeningHour} from "./OpeningHours"; -import {InputElement} from "../Input/InputElement"; -import Translations from "../i18n/Translations"; -import {Translation} from "../i18n/Translation"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Combine from "../Base/Combine"; -import OpeningHoursRange from "./OpeningHoursRange"; +import { UIEventSource } from "../../Logic/UIEventSource" +import { Utils } from "../../Utils" +import { OpeningHour } from "./OpeningHours" +import { InputElement } from "../Input/InputElement" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" +import { FixedUiElement } from "../Base/FixedUiElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import Combine from "../Base/Combine" +import OpeningHoursRange from "./OpeningHoursRange" export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> { - public static readonly days: Translation[] = - [ - Translations.t.general.weekdays.abbreviations.monday, - Translations.t.general.weekdays.abbreviations.tuesday, - Translations.t.general.weekdays.abbreviations.wednesday, - Translations.t.general.weekdays.abbreviations.thursday, - Translations.t.general.weekdays.abbreviations.friday, - Translations.t.general.weekdays.abbreviations.saturday, - Translations.t.general.weekdays.abbreviations.sunday - ] + public static readonly days: Translation[] = [ + Translations.t.general.weekdays.abbreviations.monday, + Translations.t.general.weekdays.abbreviations.tuesday, + Translations.t.general.weekdays.abbreviations.wednesday, + Translations.t.general.weekdays.abbreviations.thursday, + Translations.t.general.weekdays.abbreviations.friday, + Translations.t.general.weekdays.abbreviations.saturday, + Translations.t.general.weekdays.abbreviations.sunday, + ] /* These html-elements are an overlay over the table columns and act as a host for the ranges in the weekdays */ - public readonly weekdayElements: HTMLElement[] = Utils.TimesT(7, () => document.createElement("div")) - private readonly source: UIEventSource<OpeningHour[]>; + public readonly weekdayElements: HTMLElement[] = Utils.TimesT(7, () => + document.createElement("div") + ) + private readonly source: UIEventSource<OpeningHour[]> constructor(source?: UIEventSource<OpeningHour[]>) { - super(); - this.source = source ?? new UIEventSource<OpeningHour[]>([]); - this.SetStyle("width:100%;height:100%;display:block;"); + super() + this.source = source ?? new UIEventSource<OpeningHour[]>([]) + this.SetStyle("width:100%;height:100%;display:block;") } IsValid(t: OpeningHour[]): boolean { - return true; + return true } GetValue(): UIEventSource<OpeningHour[]> { - return this.source; + return this.source } protected InnerConstructElement(): HTMLElement { - const table = document.createElement("table") table.classList.add("oh-table") table.classList.add("relative") // Workaround for webkit-based viewers, see #1019 - const cellHeightInPx = 14; + const cellHeightInPx = 14 const headerRow = document.createElement("tr") headerRow.appendChild(document.createElement("th")) headerRow.classList.add("relative") for (let i = 0; i < OpeningHoursPickerTable.days.length; i++) { - let weekday = OpeningHoursPickerTable.days[i].Clone(); + let weekday = OpeningHoursPickerTable.days[i].Clone() const cell = document.createElement("th") cell.style.width = "14%" cell.appendChild(weekday.ConstructElement()) @@ -65,22 +65,25 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> fullColumnSpan.classList.add("w-full", "relative") // We need to round! The table height is rounded as following, we use this to calculate the actual number of pixels afterwards - fullColumnSpan.style.height = ((cellHeightInPx) * 48) + "px" - + fullColumnSpan.style.height = cellHeightInPx * 48 + "px" const ranges = new VariableUiElement( - this.source.map(ohs => - (ohs ?? []).filter((oh: OpeningHour) => oh.weekday === i)) - .map(ohsForToday => { - return new Combine(ohsForToday.map(oh => new OpeningHoursRange(oh, () => { - this.source.data.splice(this.source.data.indexOf(oh), 1) - this.source.ping() - }))) + this.source + .map((ohs) => (ohs ?? []).filter((oh: OpeningHour) => oh.weekday === i)) + .map((ohsForToday) => { + return new Combine( + ohsForToday.map( + (oh) => + new OpeningHoursRange(oh, () => { + this.source.data.splice(this.source.data.indexOf(oh), 1) + this.source.ping() + }) + ) + ) }) ) fullColumnSpan.appendChild(ranges.ConstructElement()) - const fullColumnSpanWrapper = document.createElement("div") fullColumnSpanWrapper.classList.add("absolute") fullColumnSpanWrapper.style.zIndex = "10" @@ -95,26 +98,25 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> table.appendChild(headerRow) - const self = this; + const self = this for (let h = 0; h < 24; h++) { - - const hs = Utils.TwoDigits(h); + const hs = Utils.TwoDigits(h) const firstCell = document.createElement("td") firstCell.rowSpan = 2 firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box") firstCell.appendChild(new FixedUiElement(hs + ":00").ConstructElement()) const evenRow = document.createElement("tr") - evenRow.appendChild(firstCell); + evenRow.appendChild(firstCell) for (let weekday = 0; weekday < 7; weekday++) { const cell = document.createElement("td") cell.classList.add("oh-timecell", "oh-timecell-full", `oh-timecell-${weekday}`) evenRow.appendChild(cell) } - evenRow.style.height = (cellHeightInPx) + "px"; - evenRow.style.maxHeight = evenRow.style.height; - evenRow.style.minHeight = evenRow.style.height; + evenRow.style.height = cellHeightInPx + "px" + evenRow.style.maxHeight = evenRow.style.height + evenRow.style.minHeight = evenRow.style.height table.appendChild(evenRow) const oddRow = document.createElement("tr") @@ -124,64 +126,62 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> cell.classList.add("oh-timecell", "oh-timecell-half", `oh-timecell-${weekday}`) oddRow.appendChild(cell) } - oddRow.style.minHeight = evenRow.style.height; - oddRow.style.maxHeight = evenRow.style.height; + oddRow.style.minHeight = evenRow.style.height + oddRow.style.maxHeight = evenRow.style.height table.appendChild(oddRow) } - /**** Event handling below ***/ - - let mouseIsDown = false; - let selectionStart: [number, number] = undefined; - let selectionEnd: [number, number] = undefined; + let mouseIsDown = false + let selectionStart: [number, number] = undefined + let selectionEnd: [number, number] = undefined function h(timeSegment: number) { - return Math.floor(timeSegment / 2); + return Math.floor(timeSegment / 2) } function m(timeSegment: number) { - return (timeSegment % 2) * 30; + return (timeSegment % 2) * 30 } function startSelection(i: number, j: number) { - mouseIsDown = true; - selectionStart = [i, j]; - selectionEnd = [i, j]; + mouseIsDown = true + selectionStart = [i, j] + selectionEnd = [i, j] } function endSelection() { if (selectionStart === undefined) { - return; + return } if (!mouseIsDown) { - return; + return } mouseIsDown = false - const dStart = Math.min(selectionStart[1], selectionEnd[1]); - const dEnd = Math.max(selectionStart[1], selectionEnd[1]); - const timeStart = Math.min(selectionStart[0], selectionEnd[0]) - 1; - const timeEnd = Math.max(selectionStart[0], selectionEnd[0]) - 1; + const dStart = Math.min(selectionStart[1], selectionEnd[1]) + const dEnd = Math.max(selectionStart[1], selectionEnd[1]) + const timeStart = Math.min(selectionStart[0], selectionEnd[0]) - 1 + const timeEnd = Math.max(selectionStart[0], selectionEnd[0]) - 1 for (let weekday = dStart; weekday <= dEnd; weekday++) { const oh: OpeningHour = { weekday: weekday, startHour: h(timeStart), startMinutes: m(timeStart), endHour: h(timeEnd + 1), - endMinutes: m(timeEnd + 1) + endMinutes: m(timeEnd + 1), } if (oh.endHour > 23) { - oh.endHour = 24; - oh.endMinutes = 0; + oh.endHour = 24 + oh.endMinutes = 0 } - self.source.data.push(oh); + self.source.data.push(oh) } - self.source.ping(); + self.source.ping() // Clear the highlighting - let header = table.rows[0]; + let header = table.rows[0] for (let j = 1; j < header.cells.length; j++) { header.cells[j].classList?.remove("oh-timecol-selected") } @@ -189,66 +189,63 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> let row = table.rows[i] for (let j = 0; j < row.cells.length; j++) { let cell = row.cells[j] - cell?.classList?.remove("oh-timecell-selected"); - row.classList?.remove("oh-timerow-selected"); + cell?.classList?.remove("oh-timecell-selected") + row.classList?.remove("oh-timerow-selected") } } } table.onmouseup = () => { - endSelection(); - }; + endSelection() + } table.onmouseleave = () => { - endSelection(); - }; + endSelection() + } - let lastSelectionIend, lastSelectionJEnd; + let lastSelectionIend, lastSelectionJEnd function selectAllBetween(iEnd, jEnd) { - if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { - return; // We already did this + return // We already did this } - lastSelectionIend = iEnd; - lastSelectionJEnd = jEnd; + lastSelectionIend = iEnd + lastSelectionJEnd = jEnd - let iStart = selectionStart[0]; - let jStart = selectionStart[1]; + let iStart = selectionStart[0] + let jStart = selectionStart[1] if (iStart > iEnd) { - const h = iStart; - iStart = iEnd; - iEnd = h; + const h = iStart + iStart = iEnd + iEnd = h } if (jStart > jEnd) { - const h = jStart; - jStart = jEnd; - jEnd = h; + const h = jStart + jStart = jEnd + jEnd = h } - let header = table.rows[0]; + let header = table.rows[0] for (let j = 1; j < header.cells.length; j++) { let cell = header.cells[j] - cell.classList?.remove("oh-timecol-selected-round-left"); - cell.classList?.remove("oh-timecol-selected-round-right"); + cell.classList?.remove("oh-timecol-selected-round-left") + cell.classList?.remove("oh-timecol-selected-round-right") if (jStart + 1 <= j && j <= jEnd + 1) { cell.classList?.add("oh-timecol-selected") if (jStart + 1 == j) { - cell.classList?.add("oh-timecol-selected-round-left"); + cell.classList?.add("oh-timecol-selected-round-left") } if (jEnd + 1 == j) { - cell.classList?.add("oh-timecol-selected-round-right"); + cell.classList?.add("oh-timecol-selected-round-right") } - } else { cell.classList?.remove("oh-timecol-selected") } } - for (let i = 1; i < table.rows.length; i++) { - let row = table.rows[i]; + let row = table.rows[i] if (iStart <= i && i <= iEnd) { row.classList?.add("oh-timerow-selected") } else { @@ -257,28 +254,23 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> for (let j = 0; j < row.cells.length; j++) { let cell = row.cells[j] if (cell === undefined) { - continue; + continue } - let offset = 0; + let offset = 0 if (i % 2 == 1) { if (j == 0) { // This is the first column of a full hour -> This is the time indication (skip) - continue; + continue } - offset = -1; + offset = -1 } - - if (iStart <= i && i <= iEnd && - jStart <= j + offset && j + offset <= jEnd) { + if (iStart <= i && i <= iEnd && jStart <= j + offset && j + offset <= jEnd) { cell?.classList?.add("oh-timecell-selected") } else { cell?.classList?.remove("oh-timecell-selected") } - - } - } } @@ -286,63 +278,54 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> let row = table.rows[i] for (let j = 0; j < row.cells.length; j++) { let cell = row.cells[j] - let offset = 0; + let offset = 0 if (i % 2 == 1) { if (j == 0) { - continue; + continue } - offset = -1; + offset = -1 } - cell.onmousedown = (ev) => { - ev.preventDefault(); + ev.preventDefault() startSelection(i, j + offset) - selectAllBetween(i, j + offset); + selectAllBetween(i, j + offset) } cell.ontouchstart = (ev) => { - ev.preventDefault(); - startSelection(i, j + offset); - selectAllBetween(i, j + offset); + ev.preventDefault() + startSelection(i, j + offset) + selectAllBetween(i, j + offset) } cell.onmouseenter = () => { if (mouseIsDown) { - selectionEnd = [i, j + offset]; + selectionEnd = [i, j + offset] selectAllBetween(i, j + offset) } } - cell.ontouchmove = (ev: TouchEvent) => { - - ev.preventDefault(); + ev.preventDefault() for (const k in ev.targetTouches) { - const touch = ev.targetTouches[k]; + const touch = ev.targetTouches[k] if (touch.clientX === undefined || touch.clientY === undefined) { - continue; + continue } - const elUnderTouch = document.elementFromPoint( - touch.clientX, - touch.clientY - ); + const elUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY) // @ts-ignore - const f = elUnderTouch.onmouseenter; + const f = elUnderTouch.onmouseenter if (f) { - f(); + f() } } - } cell.ontouchend = (ev) => { - ev.preventDefault(); - endSelection(); + ev.preventDefault() + endSelection() } } - } return table } - -} \ No newline at end of file +} diff --git a/UI/OpeningHours/OpeningHoursRange.ts b/UI/OpeningHours/OpeningHoursRange.ts index 6142ea9a2..7227b1094 100644 --- a/UI/OpeningHours/OpeningHoursRange.ts +++ b/UI/OpeningHours/OpeningHoursRange.ts @@ -1,64 +1,66 @@ /** * A single opening hours range, shown on top of the OH-picker table */ -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; -import Combine from "../Base/Combine"; -import {OH, OpeningHour} from "./OpeningHours"; -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; +import Svg from "../../Svg" +import { Utils } from "../../Utils" +import Combine from "../Base/Combine" +import { OH, OpeningHour } from "./OpeningHours" +import BaseUIElement from "../BaseUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" export default class OpeningHoursRange extends BaseUIElement { - private _oh: OpeningHour; + private _oh: OpeningHour - private readonly _onDelete: () => void; + private readonly _onDelete: () => void constructor(oh: OpeningHour, onDelete: () => void) { - super(); - this._oh = oh; - this._onDelete = onDelete; - this.SetClass("oh-timerange"); - + super() + this._oh = oh + this._onDelete = onDelete + this.SetClass("oh-timerange") } InnerConstructElement(): HTMLElement { - const height = this.getHeight(); - const oh = this._oh; - const startTime = new FixedUiElement(Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)) - const endTime = new FixedUiElement(Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)) + const height = this.getHeight() + const oh = this._oh + const startTime = new FixedUiElement( + Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes) + ) + const endTime = new FixedUiElement( + Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes) + ) - const deleteRange = - Svg.delete_icon_ui() - .SetClass("rounded-full w-6 h-6 block bg-black") - .onClick(() => { - this._onDelete() - }); + const deleteRange = Svg.delete_icon_ui() + .SetClass("rounded-full w-6 h-6 block bg-black") + .onClick(() => { + this._onDelete() + }) - - let content: BaseUIElement; + let content: BaseUIElement if (height > 2) { - content = new Combine([startTime, deleteRange, endTime]).SetClass("flex flex-col h-full justify-between"); + content = new Combine([startTime, deleteRange, endTime]).SetClass( + "flex flex-col h-full justify-between" + ) } else { - content = new Combine([deleteRange]).SetClass("flex flex-col h-full").SetStyle("flex-content: center; overflow-x: unset;") + content = new Combine([deleteRange]) + .SetClass("flex flex-col h-full") + .SetStyle("flex-content: center; overflow-x: unset;") } - const el = new Combine([content]).ConstructElement(); + const el = new Combine([content]).ConstructElement() - el.style.top = `${100 * OH.startTime(oh) / 24}%` - el.style.height = `${100 * this.getHeight() / 24}%` - return el; + el.style.top = `${(100 * OH.startTime(oh)) / 24}%` + el.style.height = `${(100 * this.getHeight()) / 24}%` + return el } - private getHeight(): number { - const oh = this._oh; + const oh = this._oh - let endhour = oh.endHour; + let endhour = oh.endHour if (oh.endHour == 0 && oh.endMinutes == 0) { - endhour = 24; + endhour = 24 } - return (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); + return endhour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60 } - - } diff --git a/UI/OpeningHours/OpeningHoursVisualization.ts b/UI/OpeningHours/OpeningHoursVisualization.ts index b0a8ee95c..011d59bb2 100644 --- a/UI/OpeningHours/OpeningHoursVisualization.ts +++ b/UI/OpeningHours/OpeningHoursVisualization.ts @@ -1,15 +1,15 @@ -import { UIEventSource } from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import { FixedUiElement } from "../Base/FixedUiElement"; -import { OH } from "./OpeningHours"; -import Translations from "../i18n/Translations"; -import Constants from "../../Models/Constants"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "../Input/Toggle"; -import { VariableUiElement } from "../Base/VariableUIElement"; -import Table from "../Base/Table"; -import { Translation } from "../i18n/Translation"; -import { OsmConnection } from "../../Logic/Osm/OsmConnection"; +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import { FixedUiElement } from "../Base/FixedUiElement" +import { OH } from "./OpeningHours" +import Translations from "../i18n/Translations" +import Constants from "../../Models/Constants" +import BaseUIElement from "../BaseUIElement" +import Toggle from "../Input/Toggle" +import { VariableUiElement } from "../Base/VariableUIElement" +import Table from "../Base/Table" +import { Translation } from "../i18n/Translation" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" export default class OpeningHoursVisualization extends Toggle { private static readonly weekdays: Translation[] = [ @@ -22,129 +22,163 @@ export default class OpeningHoursVisualization extends Toggle { Translations.t.general.weekdays.abbreviations.sunday, ] - constructor(tags: UIEventSource<any>, state: { osmConnection?: OsmConnection }, key: string, prefix = "", postfix = "") { - const ohTable = new VariableUiElement(tags - .map(tags => { - const value: string = tags[key]; - if (value === undefined) { - return undefined - } - if (value.startsWith(prefix) && value.endsWith(postfix)) { - return value.substring(prefix.length, value.length - postfix.length).trim() - } - return value; - }) // This mapping will absorb all other changes to tags in order to prevent regeneration - .map(ohtext => { - if (ohtext === undefined) { - return new FixedUiElement("No opening hours defined with key " + key).SetClass("alert") - } - try { - return OpeningHoursVisualization.CreateFullVisualisation( - OH.CreateOhObject(tags.data, ohtext)) - } catch (e) { - console.warn(e, e.stack); - return new Combine([Translations.t.general.opening_hours.error_loading, - new Toggle( - new FixedUiElement(e).SetClass("subtle"), - undefined, - state?.osmConnection?.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) - ) - ]); - } - - } - )) + constructor( + tags: UIEventSource<any>, + state: { osmConnection?: OsmConnection }, + key: string, + prefix = "", + postfix = "" + ) { + const ohTable = new VariableUiElement( + tags + .map((tags) => { + const value: string = tags[key] + if (value === undefined) { + return undefined + } + if (value.startsWith(prefix) && value.endsWith(postfix)) { + return value.substring(prefix.length, value.length - postfix.length).trim() + } + return value + }) // This mapping will absorb all other changes to tags in order to prevent regeneration + .map((ohtext) => { + if (ohtext === undefined) { + return new FixedUiElement( + "No opening hours defined with key " + key + ).SetClass("alert") + } + try { + return OpeningHoursVisualization.CreateFullVisualisation( + OH.CreateOhObject(tags.data, ohtext) + ) + } catch (e) { + console.warn(e, e.stack) + return new Combine([ + Translations.t.general.opening_hours.error_loading, + new Toggle( + new FixedUiElement(e).SetClass("subtle"), + undefined, + state?.osmConnection?.userDetails.map( + (userdetails) => + userdetails.csCount >= + Constants.userJourney.tagsVisibleAndWikiLinked + ) + ), + ]) + } + }) + ) super( ohTable, Translations.t.general.opening_hours.loadingCountry.Clone(), - tags.map(tgs => tgs._country !== undefined) - ); + tags.map((tgs) => tgs._country !== undefined) + ) } private static CreateFullVisualisation(oh: any): BaseUIElement { - /** First, we determine which range of dates we want to visualize: this week or next week?**/ - const today = new Date(); - today.setHours(0, 0, 0, 0); - const lastMonday = OH.getMondayBefore(today); - const nextSunday = new Date(lastMonday); - nextSunday.setDate(nextSunday.getDate() + 7); + const today = new Date() + today.setHours(0, 0, 0, 0) + const lastMonday = OH.getMondayBefore(today) + const nextSunday = new Date(lastMonday) + nextSunday.setDate(nextSunday.getDate() + 7) if (!oh.getState() && !oh.getUnknown()) { // POI is currently closed - const nextChange: Date = oh.getNextChange(); + const nextChange: Date = oh.getNextChange() if ( // Shop isn't gonna open anymore in this timerange - nextSunday < nextChange + nextSunday < nextChange && // And we are already in the weekend to show next week - && (today.getDay() == 0 || today.getDay() == 6) + (today.getDay() == 0 || today.getDay() == 6) ) { // We move the range to next week! - lastMonday.setDate(lastMonday.getDate() + 7); - nextSunday.setDate(nextSunday.getDate() + 7); + lastMonday.setDate(lastMonday.getDate() + 7) + nextSunday.setDate(nextSunday.getDate() + 7) } } - /* We calculate the ranges when it is opened! */ - const ranges = OH.GetRanges(oh, lastMonday, nextSunday); + const ranges = OH.GetRanges(oh, lastMonday, nextSunday) /* First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special - * So, we have to handle the case that ranges is completely empty*/ - if (ranges.filter(range => range.length > 0).length === 0) { - return OpeningHoursVisualization.ShowSpecialCase(oh).SetClass("p-4 rounded-full block bg-gray-200") + * So, we have to handle the case that ranges is completely empty*/ + if (ranges.filter((range) => range.length > 0).length === 0) { + return OpeningHoursVisualization.ShowSpecialCase(oh).SetClass( + "p-4 rounded-full block bg-gray-200" + ) } /** With all the edge cases handled, we can actually construct the table! **/ return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday) - - } - private static ConstructVizTable(oh: any, ranges: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }[][], - rangeStart: Date): BaseUIElement { - - - const isWeekstable: boolean = oh.isWeekStable(); - let [changeHours, changeHourText] = OH.allChangeMoments(ranges); - const today = new Date(); - today.setHours(0, 0, 0, 0); + private static ConstructVizTable( + oh: any, + ranges: { + isOpen: boolean + isSpecial: boolean + comment: string + startDate: Date + endDate: Date + }[][], + rangeStart: Date + ): BaseUIElement { + const isWeekstable: boolean = oh.isWeekStable() + let [changeHours, changeHourText] = OH.allChangeMoments(ranges) + const today = new Date() + today.setHours(0, 0, 0, 0) // @ts-ignore const todayIndex = Math.ceil((today - rangeStart) / (1000 * 60 * 60 * 24)) // By default, we always show the range between 8 - 19h, in order to give a stable impression // Ofc, a bigger range is used if needed - const earliestOpen = Math.min(8 * 60 * 60, ...changeHours); - let latestclose = Math.max(...changeHours); + const earliestOpen = Math.min(8 * 60 * 60, ...changeHours) + let latestclose = Math.max(...changeHours) // We always make sure there is 30m of leeway in order to give enough room for the closing entry latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) - const availableArea = latestclose - earliestOpen; + const availableArea = latestclose - earliestOpen /* - * The OH-visualisation is a table, consisting of 8 rows and 2 columns: - * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times - * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars. - * Note that the bars are actually an embedded <div> spanning the full width, containing multiple sub-elements - * */ + * The OH-visualisation is a table, consisting of 8 rows and 2 columns: + * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times + * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars. + * Note that the bars are actually an embedded <div> spanning the full width, containing multiple sub-elements + * */ - const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement(availableArea, changeHours, changeHourText, earliestOpen) + const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement( + availableArea, + changeHours, + changeHourText, + earliestOpen + ) const weekdays = [] const weekdayStyles = [] for (let i = 0; i < 7; i++) { - - const day = OpeningHoursVisualization.weekdays[i].Clone(); + const day = OpeningHoursVisualization.weekdays[i].Clone() day.SetClass("w-full h-full block") - const rangesForDay = ranges[i].map(range => - OpeningHoursVisualization.CreateRangeElem(availableArea, earliestOpen, latestclose, range, isWeekstable) + const rangesForDay = ranges[i].map((range) => + OpeningHoursVisualization.CreateRangeElem( + availableArea, + earliestOpen, + latestclose, + range, + isWeekstable + ) ) const allRanges = new Combine([ - ...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen), - ...rangesForDay]).SetClass("w-full block"); + ...OpeningHoursVisualization.CreateLinesAtChangeHours( + changeHours, + availableArea, + earliestOpen + ), + ...rangesForDay, + ]).SetClass("w-full block") let extraStyle = "" if (todayIndex == i) { @@ -154,55 +188,73 @@ export default class OpeningHoursVisualization extends Toggle { extraStyle = "background-color: rgba(230, 231, 235, 1);" } weekdays.push([day, allRanges]) - weekdayStyles.push(["padding-left: 0.5em;" + extraStyle, `position: relative;` + extraStyle]) + weekdayStyles.push([ + "padding-left: 0.5em;" + extraStyle, + `position: relative;` + extraStyle, + ]) } - return new Table(undefined, - [[" ", header], ...weekdays], - { contentStyle: [["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles] } - ).SetClass("w-full") - .SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal") - - + return new Table(undefined, [[" ", header], ...weekdays], { + contentStyle: [ + ["width: 5%", `position: relative; height: ${headerHeight}`], + ...weekdayStyles, + ], + }) + .SetClass("w-full") + .SetStyle( + "border-collapse: collapse; word-break; word-break: normal; word-wrap: normal" + ) } - private static CreateRangeElem(availableArea: number, earliestOpen: number, latestclose: number, - range: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }, - isWeekstable: boolean): BaseUIElement { - - const textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString()); + private static CreateRangeElem( + availableArea: number, + earliestOpen: number, + latestclose: number, + range: { + isOpen: boolean + isSpecial: boolean + comment: string + startDate: Date + endDate: Date + }, + isWeekstable: boolean + ): BaseUIElement { + const textToShow = + range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString()) if (!range.isOpen && !range.isSpecial) { return new FixedUiElement(textToShow).SetClass("ohviz-day-off") } - const startOfDay: Date = new Date(range.startDate); - startOfDay.setHours(0, 0, 0, 0); + const startOfDay: Date = new Date(range.startDate) + startOfDay.setHours(0, 0, 0, 0) // @ts-ignore - const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen; + const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen // prettier-ignore // @ts-ignore const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen); - const startPercentage = (100 * startpoint / availableArea); - return new FixedUiElement(textToShow).SetStyle(`left:${startPercentage}%; width:${width}%`) - .SetClass("ohviz-range"); + const startPercentage = (100 * startpoint) / availableArea + return new FixedUiElement(textToShow) + .SetStyle(`left:${startPercentage}%; width:${width}%`) + .SetClass("ohviz-range") } - private static CreateLinesAtChangeHours(changeHours: number[], availableArea: number, earliestOpen: number): - BaseUIElement[] { - + private static CreateLinesAtChangeHours( + changeHours: number[], + availableArea: number, + earliestOpen: number + ): BaseUIElement[] { const allLines: BaseUIElement[] = [] for (const changeMoment of changeHours) { - const offset = 100 * (changeMoment - earliestOpen) / availableArea; + const offset = (100 * (changeMoment - earliestOpen)) / availableArea if (offset < 0 || offset > 100) { - continue; + continue } - const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line"); - allLines.push(el); + const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line") + allLines.push(el) } - return allLines; + return allLines } - /** * The OH-Visualization header element, a single bar with hours * @param availableArea @@ -212,55 +264,65 @@ export default class OpeningHoursVisualization extends Toggle { * @constructor * @private */ - private static ConstructHeaderElement(availableArea: number, changeHours: number[], changeHourText: string[], earliestOpen: number) - : [BaseUIElement, string] { - let header: BaseUIElement[] = []; + private static ConstructHeaderElement( + availableArea: number, + changeHours: number[], + changeHourText: string[], + earliestOpen: number + ): [BaseUIElement, string] { + let header: BaseUIElement[] = [] - header.push(...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen)) + header.push( + ...OpeningHoursVisualization.CreateLinesAtChangeHours( + changeHours, + availableArea, + earliestOpen + ) + ) - let showHigher = false; - let showHigherUsed = false; + let showHigher = false + let showHigherUsed = false for (let i = 0; i < changeHours.length; i++) { - let changeMoment = changeHours[i]; - const offset = 100 * (changeMoment - earliestOpen) / availableArea; + let changeMoment = changeHours[i] + const offset = (100 * (changeMoment - earliestOpen)) / availableArea if (offset < 0 || offset > 100) { - continue; + continue } - if (i > 0 && ((changeMoment - changeHours[i - 1]) / (60 * 60)) < 2) { + if (i > 0 && (changeMoment - changeHours[i - 1]) / (60 * 60) < 2) { // Quite close to the previous value // We alternate the heights - showHigherUsed = true; - showHigher = !showHigher; + showHigherUsed = true + showHigher = !showHigher } else { - showHigher = false; + showHigher = false } const el = new Combine([ - new FixedUiElement(changeHourText[i]) - .SetClass("relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black border-opacity-50") - .SetStyle("left: -50%; word-break:initial") - + .SetClass( + "relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black border-opacity-50" + ) + .SetStyle("left: -50%; word-break:initial"), ]) - .SetStyle(`left:${offset}%;margin-top: ${showHigher ? '1.4rem;' : "0.1rem"}`) - .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication"); - header.push(el); + .SetStyle(`left:${offset}%;margin-top: ${showHigher ? "1.4rem;" : "0.1rem"}`) + .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication") + header.push(el) } - const headerElem = new Combine(header).SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`) + const headerElem = new Combine(header) + .SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`) .SetStyle("margin-top: -1rem") - const headerHeight = showHigherUsed ? "4rem" : "2rem"; + const headerHeight = showHigherUsed ? "4rem" : "2rem" return [headerElem, headerHeight] - } /* - * Visualizes any special case: e.g. not open for a long time, 24/7 open, ... - * */ + * Visualizes any special case: e.g. not open for a long time, 24/7 open, ... + * */ private static ShowSpecialCase(oh: any) { - const opensAtDate = oh.getNextChange(); + const opensAtDate = oh.getNextChange() if (opensAtDate === undefined) { - const comm = oh.getComment() ?? oh.getUnknown(); + const comm = oh.getComment() ?? oh.getUnknown() if (!!comm) { return new FixedUiElement(comm) } @@ -270,9 +332,10 @@ export default class OpeningHoursVisualization extends Toggle { } return Translations.t.general.opening_hours.closed_permanently.Clone() } - const willOpenAt = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` + const willOpenAt = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm( + opensAtDate.getHours(), + opensAtDate.getMinutes() + )}` return Translations.t.general.opening_hours.closed_until.Subs({ date: willOpenAt }) } - - } diff --git a/UI/OpeningHours/PublicHolidayInput.ts b/UI/OpeningHours/PublicHolidayInput.ts index 05274552b..8cd434922 100644 --- a/UI/OpeningHours/PublicHolidayInput.ts +++ b/UI/OpeningHours/PublicHolidayInput.ts @@ -1,29 +1,28 @@ -import {OH} from "./OpeningHours"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import {TextField} from "../Input/TextField"; -import {DropDown} from "../Input/DropDown"; -import {InputElement} from "../Input/InputElement"; -import Translations from "../i18n/Translations"; -import Toggle from "../Input/Toggle"; +import { OH } from "./OpeningHours" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import { TextField } from "../Input/TextField" +import { DropDown } from "../Input/DropDown" +import { InputElement } from "../Input/InputElement" +import Translations from "../i18n/Translations" +import Toggle from "../Input/Toggle" export default class PublicHolidayInput extends InputElement<string> { - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) - private readonly _value: UIEventSource<string>; + private readonly _value: UIEventSource<string> constructor(value: UIEventSource<string> = new UIEventSource<string>("")) { - super(); - this._value = value; + super() + this._value = value } - GetValue(): UIEventSource<string> { - return this._value; + return this._value } IsValid(t: string): boolean { - return true; + return true } /** @@ -31,65 +30,62 @@ export default class PublicHolidayInput extends InputElement<string> { * // should construct an element * const html = new PublicHolidayInput().InnerConstructElement() * html !== undefined // => true - * + * * // should construct an element despite having an invalid input * const html = new PublicHolidayInput(new UIEventSource("invalid")).InnerConstructElement() * html !== undefined // => true - * + * * // should construct an element despite having null as input * const html = new PublicHolidayInput(new UIEventSource(null)).InnerConstructElement() * html !== undefined // => true */ protected InnerConstructElement(): HTMLElement { - const dropdown = new DropDown( - Translations.t.general.opening_hours.open_during_ph.Clone(), - [ - {shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: ""}, - {shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"}, - {shown: Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"}, - {shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "}, - ] - ).SetClass("inline-block"); + const dropdown = new DropDown(Translations.t.general.opening_hours.open_during_ph.Clone(), [ + { shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: "" }, + { shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off" }, + { shown: Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open" }, + { shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " " }, + ]).SetClass("inline-block") /* - * Either "" (unknown), " " (opened) or "off" (closed) - * */ - const mode = dropdown.GetValue(); - + * Either "" (unknown), " " (opened) or "off" (closed) + * */ + const mode = dropdown.GetValue() const start = new TextField({ placeholder: "starthour", - htmlType: "time" - }).SetClass("inline-block"); + htmlType: "time", + }).SetClass("inline-block") const end = new TextField({ placeholder: "starthour", - htmlType: "time" - }).SetClass("inline-block"); + htmlType: "time", + }).SetClass("inline-block") const askHours = new Toggle( new Combine([ Translations.t.general.opening_hours.opensAt.Clone(), start, Translations.t.general.opening_hours.openTill.Clone(), - end + end, ]), undefined, - mode.map(mode => mode === " ") + mode.map((mode) => mode === " ") ) this.SetupDataSync(mode, start.GetValue(), end.GetValue()) - return new Combine([ - dropdown, - askHours - ]).ConstructElement() + return new Combine([dropdown, askHours]).ConstructElement() } - private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) { - - const value = this._value; - value.map(ph => OH.ParsePHRule(ph)) - .addCallbackAndRunD(parsed => { - if(parsed === null){ + private SetupDataSync( + mode: UIEventSource<string>, + startTime: UIEventSource<string>, + endTime: UIEventSource<string> + ) { + const value = this._value + value + .map((ph) => OH.ParsePHRule(ph)) + .addCallbackAndRunD((parsed) => { + if (parsed === null) { return } mode.setData(parsed.mode) @@ -98,22 +94,22 @@ export default class PublicHolidayInput extends InputElement<string> { }) // We use this as a 'addCallbackAndRun' - mode.map(mode => { + mode.map( + (mode) => { if (mode === undefined || mode === "") { // not known value.setData(undefined) return } if (mode === "off") { - value.setData("PH off"); - return; + value.setData("PH off") + return } if (mode === "open") { - value.setData("PH open"); - return; + value.setData("PH open") + return } - // Open during PH with special hours if (startTime.data === undefined || endTime.data === undefined) { // hours not filled in - not saveable @@ -122,10 +118,8 @@ export default class PublicHolidayInput extends InputElement<string> { } const oh = `PH ${startTime.data}-${endTime.data}` value.setData(oh) - - - }, [startTime, endTime] + }, + [startTime, endTime] ) } - -} \ No newline at end of file +} diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index 2f824854d..d005c83e5 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -1,66 +1,79 @@ -import {SpecialVisualization} from "../SpecialVisualizations"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; -import BaseUIElement from "../BaseUIElement"; -import {Stores, UIEventSource} from "../../Logic/UIEventSource"; -import {DefaultGuiState} from "../DefaultGuiState"; -import {SubtleButton} from "../Base/SubtleButton"; -import Img from "../Base/Img"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Combine from "../Base/Combine"; -import Link from "../Base/Link"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import {Utils} from "../../Utils"; -import Minimap from "../Base/Minimap"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -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"; -import {UIElement} from "../UIElement"; -import FilteredLayer from "../../Models/FilteredLayer"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import Lazy from "../Base/Lazy"; -import List from "../Base/List"; +import { SpecialVisualization } from "../SpecialVisualizations" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" +import BaseUIElement from "../BaseUIElement" +import { Stores, UIEventSource } from "../../Logic/UIEventSource" +import { DefaultGuiState } from "../DefaultGuiState" +import { SubtleButton } from "../Base/SubtleButton" +import Img from "../Base/Img" +import { FixedUiElement } from "../Base/FixedUiElement" +import Combine from "../Base/Combine" +import Link from "../Base/Link" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import { Utils } from "../../Utils" +import Minimap from "../Base/Minimap" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +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" +import { UIElement } from "../UIElement" +import FilteredLayer from "../../Models/FilteredLayer" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import Lazy from "../Base/Lazy" +import List from "../Base/List" export interface AutoAction extends SpecialVisualization { supportsAutoAction: boolean - applyActionOn(state: { - layoutToUse: LayoutConfig, - changes: Changes - }, tagSource: UIEventSource<any>, argument: string[]): Promise<void> + applyActionOn( + state: { + layoutToUse: LayoutConfig + changes: Changes + }, + tagSource: UIEventSource<any>, + argument: string[] + ): Promise<void> } class ApplyButton extends UIElement { - private readonly icon: string; - private readonly text: string; - private readonly targetTagRendering: string; - private readonly target_layer_id: string; - private readonly state: FeaturePipelineState; - private readonly target_feature_ids: string[]; - private readonly buttonState = new UIEventSource<"idle" | "running" | "done" | { error: string }>("idle") - private readonly layer: FilteredLayer; - private readonly tagRenderingConfig: TagRenderingConfig; + private readonly icon: string + private readonly text: string + private readonly targetTagRendering: string + private readonly target_layer_id: string + private readonly state: FeaturePipelineState + private readonly target_feature_ids: string[] + private readonly buttonState = new UIEventSource< + "idle" | "running" | "done" | { error: string } + >("idle") + private readonly layer: FilteredLayer + private readonly tagRenderingConfig: TagRenderingConfig - constructor(state: FeaturePipelineState, target_feature_ids: string[], options: { - target_layer_id: string, - targetTagRendering: string, - text: string, - icon: string - }) { + constructor( + state: FeaturePipelineState, + target_feature_ids: string[], + options: { + target_layer_id: string + targetTagRendering: string + text: string + icon: string + } + ) { super() - this.state = state; - this.target_feature_ids = target_feature_ids; - this.target_layer_id = options.target_layer_id; - this.targetTagRendering = options.targetTagRendering; + this.state = state + this.target_feature_ids = target_feature_ids + this.target_layer_id = options.target_layer_id + this.targetTagRendering = options.targetTagRendering this.text = options.text this.icon = options.icon - this.layer = this.state.filteredLayers.data.find(l => l.layerDef.id === this.target_layer_id) - this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(tr => tr.id === this.targetTagRendering) - + this.layer = this.state.filteredLayers.data.find( + (l) => l.layerDef.id === this.target_layer_id + ) + this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find( + (tr) => tr.id === this.targetTagRendering + ) } protected InnerRender(): string | BaseUIElement { @@ -68,24 +81,25 @@ class ApplyButton extends UIElement { return new FixedUiElement("No elements found to perform action") } - if (this.tagRenderingConfig === undefined) { - return new FixedUiElement("Target tagrendering " + this.targetTagRendering + " not found").SetClass("alert") + return new FixedUiElement( + "Target tagrendering " + this.targetTagRendering + " not found" + ).SetClass("alert") } - const self = this; - const button = new SubtleButton( - new Img(this.icon), - this.text - ).onClick(() => { + const self = this + const button = new SubtleButton(new Img(this.icon), this.text).onClick(() => { this.buttonState.setData("running") window.setTimeout(() => { - - self.Run(); + self.Run() }, 50) - }); + }) - const explanation = new Combine(["The following objects will be updated: ", - ...this.target_feature_ids.map(id => new Combine([new Link(id, "https:/ /openstreetmap.org/" + id, true), ", "]))]).SetClass("subtle") + const explanation = new Combine([ + "The following objects will be updated: ", + ...this.target_feature_ids.map( + (id) => new Combine([new Link(id, "https:/ /openstreetmap.org/" + id, true), ", "]) + ), + ]).SetClass("subtle") const previewMap = Minimap.createMiniMap({ allowMoving: false, @@ -93,7 +107,9 @@ class ApplyButton extends UIElement { addLayerControl: true, }).SetClass("h-48") - const features = this.target_feature_ids.map(id => this.state.allElements.ContainingFeatures.get(id)) + const features = this.target_feature_ids.map((id) => + this.state.allElements.ContainingFeatures.get(id) + ) new ShowDataLayer({ leafletMap: previewMap.leafletMap, @@ -103,11 +119,10 @@ class ApplyButton extends UIElement { layerToShow: this.layer.layerDef, }) - - return new VariableUiElement(this.buttonState.map( - st => { + return new VariableUiElement( + this.buttonState.map((st) => { if (st === "idle") { - return new Combine([button, previewMap, explanation]); + return new Combine([button, previewMap, explanation]) } if (st === "done") { return new FixedUiElement("All done!").SetClass("thanks") @@ -116,26 +131,32 @@ class ApplyButton extends UIElement { return new Loading("Applying changes...") } const error = st.error - return new Combine([new FixedUiElement("Something went wrong...").SetClass("alert"), new FixedUiElement(error).SetClass("subtle")]).SetClass("flex flex-col") - } - )) + return new Combine([ + new FixedUiElement("Something went wrong...").SetClass("alert"), + new FixedUiElement(error).SetClass("subtle"), + ]).SetClass("flex flex-col") + }) + ) } private async Run() { - - try { console.log("Applying auto-action on " + this.target_feature_ids.length + " features") for (const targetFeatureId of this.target_feature_ids) { const featureTags = this.state.allElements.getEventSourceById(targetFeatureId) const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt - const specialRenderings = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering) - .map(x => x.special)) - .filter(v => v.func["supportsAutoAction"] === true) + const specialRenderings = Utils.NoNull( + SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) + ).filter((v) => v.func["supportsAutoAction"] === true) if (specialRenderings.length == 0) { - console.warn("AutoApply: feature " + targetFeatureId + " got a rendering without supported auto actions:", rendering) + console.warn( + "AutoApply: feature " + + targetFeatureId + + " got a rendering without supported auto actions:", + rendering + ) } for (const specialRendering of specialRenderings) { @@ -148,45 +169,53 @@ class ApplyButton extends UIElement { this.buttonState.setData("done") } catch (e) { console.error("Error while running autoApply: ", e) - this.buttonState.setData({error: e}) + this.buttonState.setData({ error: e }) } } - } export default class AutoApplyButton implements SpecialVisualization { - public readonly docs: BaseUIElement; - public readonly funcName: string = "auto_apply"; - public readonly args: { name: string; defaultValue?: string; doc: string, required?: boolean }[] = [ + public readonly docs: BaseUIElement + public readonly funcName: string = "auto_apply" + public readonly args: { + name: string + defaultValue?: string + doc: string + required?: boolean + }[] = [ { name: "target_layer", doc: "The layer that the target features will reside in", - required: true + required: true, }, { name: "target_feature_ids", doc: "The key, of which the value contains a list of ids", - required: true + required: true, }, { name: "tag_rendering_id", doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed", - required: true + required: true, }, { name: "text", doc: "The text to show on the button", - required: true + required: true, }, { name: "icon", doc: "The icon to show on the button", - defaultValue: "./assets/svg/robot.svg" - } - ]; + defaultValue: "./assets/svg/robot.svg", + }, + ] constructor(allSpecialVisualisations: SpecialVisualization[]) { - this.docs = AutoApplyButton.generateDocs(allSpecialVisualisations.filter(sv => sv["supportsAutoAction"] === true).map(sv => sv.funcName)) + this.docs = AutoApplyButton.generateDocs( + allSpecialVisualisations + .filter((sv) => sv["supportsAutoAction"] === true) + .map((sv) => sv.funcName) + ) } private static generateDocs(supportedActions: string[]) { @@ -194,21 +223,38 @@ export default class AutoApplyButton implements SpecialVisualization { "A button to run many actions for many features at once.", "To effectively use this button, you'll need some ingredients:", new List([ - "A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + supportedActions.join(", "), - "A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", new Link("current_view","./BuiltinLayers.md#current_view"), + "A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + + supportedActions.join(", "), + "A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", + new Link("current_view", "./BuiltinLayers.md#current_view"), "Then, use a calculated tag on the host feature to determine the overlapping object ids", - "At last, add this component" + "At last, add this component", ]), - ]) } - constr(state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState): BaseUIElement { + constr( + state: FeaturePipelineState, + tagSource: UIEventSource<any>, + argument: string[], + guistate: DefaultGuiState + ): BaseUIElement { try { - - if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) { - const t = Translations.t.general.add.import; - return new Combine([new FixedUiElement("The auto-apply button is only available in official themes (or in testing mode)").SetClass("alert"), t.howToTest]) + if ( + !state.layoutToUse.official && + !( + state.featureSwitchIsTesting.data || + state.osmConnection._oauth_config.url === + OsmConnection.oauth_configs["osm-test"].url + ) + ) { + const t = Translations.t.general.add.import + return new Combine([ + new FixedUiElement( + "The auto-apply button is only available in official themes (or in testing mode)" + ).SetClass("alert"), + t.howToTest, + ]) } const target_layer_id = argument[0] @@ -216,7 +262,10 @@ export default class AutoApplyButton implements SpecialVisualization { const text = argument[3] const icon = argument[4] const options = { - target_layer_id, targetTagRendering, text, icon + target_layer_id, + targetTagRendering, + text, + icon, } return new Lazy(() => { @@ -227,25 +276,25 @@ export default class AutoApplyButton implements SpecialVisualization { to_parse.setData(applicable) }) - const loading = new Loading("Gathering which elements support auto-apply... "); - return new VariableUiElement(to_parse.map(ids => { - if (ids === undefined) { - return loading - } + const loading = new Loading("Gathering which elements support auto-apply... ") + return new VariableUiElement( + to_parse.map((ids) => { + if (ids === undefined) { + return loading + } - return new ApplyButton(state, JSON.parse(ids), options); - })) + return new ApplyButton(state, JSON.parse(ids), options) + }) + ) }) - - } catch (e) { - return new FixedUiElement("Could not generate a auto_apply-button for key " + argument[0] + " due to " + e).SetClass("alert") + return new FixedUiElement( + "Could not generate a auto_apply-button for key " + argument[0] + " due to " + e + ).SetClass("alert") } } getLayerDependencies(args: string[]): string[] { return [args[0]] } - - -} \ No newline at end of file +} diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index 86f78bb68..b217a82b8 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -1,30 +1,29 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import Toggle from "../Input/Toggle"; -import Translations from "../i18n/Translations"; -import Svg from "../../Svg"; -import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import Combine from "../Base/Combine"; -import {SubtleButton} from "../Base/SubtleButton"; -import {Translation} from "../i18n/Translation"; -import BaseUIElement from "../BaseUIElement"; -import Constants from "../../Models/Constants"; -import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"; -import {OsmObject} from "../../Logic/Osm/OsmObject"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import {InputElement} from "../Input/InputElement"; -import {RadioButton} from "../Input/RadioButton"; -import {FixedInputElement} from "../Input/FixedInputElement"; -import Title from "../Base/Title"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; -import TagRenderingQuestion from "./TagRenderingQuestion"; +import { VariableUiElement } from "../Base/VariableUIElement" +import Toggle from "../Input/Toggle" +import Translations from "../i18n/Translations" +import Svg from "../../Svg" +import DeleteAction from "../../Logic/Osm/Actions/DeleteAction" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import Combine from "../Base/Combine" +import { SubtleButton } from "../Base/SubtleButton" +import { Translation } from "../i18n/Translation" +import BaseUIElement from "../BaseUIElement" +import Constants from "../../Models/Constants" +import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig" +import { OsmObject } from "../../Logic/Osm/OsmObject" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import { InputElement } from "../Input/InputElement" +import { RadioButton } from "../Input/RadioButton" +import { FixedInputElement } from "../Input/FixedInputElement" +import Title from "../Base/Title" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" +import TagRenderingQuestion from "./TagRenderingQuestion" export default class DeleteWizard extends Toggle { - /** * The UI-element which triggers 'deletion' (either soft or hard). * @@ -44,11 +43,7 @@ export default class DeleteWizard extends Toggle { * @param state: the state of the application * @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted */ - constructor(id: string, - state: FeaturePipelineState, - options: DeleteConfig) { - - + constructor(id: string, state: FeaturePipelineState, options: DeleteConfig) { const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets) const tagsSource = state.allElements.getEventSourceById(id) @@ -57,239 +52,262 @@ export default class DeleteWizard extends Toggle { const confirm = new UIEventSource<boolean>(false) - /** * This function is the actual delete function */ function doDelete(selected: { deleteReason: string } | { retagTo: TagsFilter }) { - let actionToTake: OsmChangeAction; + let actionToTake: OsmChangeAction if (selected["retagTo"] !== undefined) { // no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping - actionToTake = new ChangeTagAction( - id, - selected["retagTo"], - tagsSource.data, - { - theme: state?.layoutToUse?.id ?? "unkown", - changeType: "special-delete" - } - ) + actionToTake = new ChangeTagAction(id, selected["retagTo"], tagsSource.data, { + theme: state?.layoutToUse?.id ?? "unkown", + changeType: "special-delete", + }) } else { - - actionToTake = new DeleteAction(id, + actionToTake = new DeleteAction( + id, options.softDeletionTags, { theme: state?.layoutToUse?.id ?? "unkown", - specialMotivation: selected["deleteReason"] + specialMotivation: selected["deleteReason"], }, deleteAbility.canBeDeleted.data.canBeDeleted ) } state.changes?.applyAction(actionToTake) isDeleted.setData(true) - } - const t = Translations.t.delete - const cancelButton = t.cancel.SetClass("block btn btn-secondary").onClick(() => confirm.setData(false)); + const cancelButton = t.cancel + .SetClass("block btn btn-secondary") + .onClick(() => confirm.setData(false)) /** * The button which is shown first. Opening it will trigger the check for deletions */ const deleteButton = new SubtleButton( - Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete) - .onClick( - () => { - deleteAbility.CheckDeleteability(true) - confirm.setData(true); - } - ) + Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), + t.delete + ).onClick(() => { + deleteAbility.CheckDeleteability(true) + confirm.setData(true) + }) - const isShown: Store<boolean> = tagsSource.map(tgs => tgs.id.indexOf("-") < 0) + const isShown: Store<boolean> = tagsSource.map((tgs) => tgs.id.indexOf("-") < 0) - const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state); + const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state) const deleteDialog = new Combine([ - - - new Title(new SubstitutedTranslation(t.whyDelete, tagsSource, state) - .SetClass("question-text"), 3), + new Title( + new SubstitutedTranslation(t.whyDelete, tagsSource, state).SetClass( + "question-text" + ), + 3 + ), deleteOptionPicker, new Combine([ - DeleteWizard.constructExplanation(deleteOptionPicker.GetValue(), deleteAbility, tagsSource, state), + DeleteWizard.constructExplanation( + deleteOptionPicker.GetValue(), + deleteAbility, + tagsSource, + state + ), new Combine([ - - cancelButton, - DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue()) - .onClick(() => doDelete(deleteOptionPicker.GetValue().data)) - ]).SetClass("flex justify-end flex-wrap-reverse") - - ]).SetClass("flex mt-2 justify-between") - - + cancelButton, + DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue()).onClick(() => + doDelete(deleteOptionPicker.GetValue().data) + ), + ]).SetClass("flex justify-end flex-wrap-reverse"), + ]).SetClass("flex mt-2 justify-between"), ]).SetClass("question") - super( new Toggle( - new Combine([Svg.delete_icon_svg().SetClass("h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"), - t.isDeleted]).SetClass("flex m-2 rounded-full"), + new Combine([ + Svg.delete_icon_svg().SetClass( + "h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full" + ), + t.isDeleted, + ]).SetClass("flex m-2 rounded-full"), new Toggle( new Toggle( new Toggle( new Toggle( deleteDialog, new SubtleButton(Svg.envelope_ui(), t.readMessages), - state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0) + state.osmConnection.userDetails.map( + (ud) => + ud.csCount > + Constants.userJourney + .addNewPointWithUnreadMessagesUnlock || + ud.unreadMessages == 0 + ) ), deleteButton, - confirm), - new VariableUiElement(deleteAbility.canBeDeleted.map(cbd => - - new Combine([ - Svg.delete_not_allowed_svg().SetStyle("height: 2rem; width: auto").SetClass("mr-2"), + confirm + ), + new VariableUiElement( + deleteAbility.canBeDeleted.map((cbd) => new Combine([ - t.cannotBeDeleted, - cbd.reason.SetClass("subtle"), - t.useSomethingElse.SetClass("subtle")]).SetClass("flex flex-col") - ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200"))) + Svg.delete_not_allowed_svg() + .SetStyle("height: 2rem; width: auto") + .SetClass("mr-2"), + new Combine([ + t.cannotBeDeleted, + cbd.reason.SetClass("subtle"), + t.useSomethingElse.SetClass("subtle"), + ]).SetClass("flex flex-col"), + ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200") + ) + ), - - , - deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), + deleteAbility.canBeDeleted.map( + (cbd) => allowSoftDeletion || cbd.canBeDeleted !== false + ) + ), t.loginToDelete.onClick(state.osmConnection.AttemptLogin), state.osmConnection.isLoggedIn ), - isDeleted), + isDeleted + ), undefined, - isShown) - + isShown + ) } - - private static constructConfirmButton(deleteReasons: UIEventSource<any | undefined>): BaseUIElement { - const t = Translations.t.delete; + private static constructConfirmButton( + deleteReasons: UIEventSource<any | undefined> + ): BaseUIElement { + const t = Translations.t.delete const btn = new Combine([ Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), - t.delete + t.delete, ]).SetClass("flex btn bg-red-500") - const btnNonActive = new Combine([ Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), - t.delete + t.delete, ]).SetClass("flex btn btn-disabled bg-red-200") return new Toggle( btn, btnNonActive, - deleteReasons.map(reason => reason !== undefined) + deleteReasons.map((reason) => reason !== undefined) ) - } + private static constructExplanation( + selectedOption: UIEventSource<{ deleteReason: string } | { retagTo: TagsFilter }>, + deleteAction: DeleteabilityChecker, + currentTags: UIEventSource<object>, + state?: { osmConnection?: OsmConnection } + ) { + const t = Translations.t.delete + return new VariableUiElement( + selectedOption.map( + (selectedOption) => { + if (selectedOption === undefined) { + return t.explanations.selectReason.SetClass("subtle") + } - private static constructExplanation(selectedOption: UIEventSource< - {deleteReason: string} | {retagTo: TagsFilter}>, deleteAction: DeleteabilityChecker, - currentTags: UIEventSource<object>, - state?: {osmConnection?: OsmConnection}) { - const t = Translations.t.delete; - return new VariableUiElement(selectedOption.map( - selectedOption => { - if (selectedOption === undefined) { - return t.explanations.selectReason.SetClass("subtle"); - } + const retag: TagsFilter | undefined = selectedOption["retagTo"] + if (retag !== undefined) { + // This is a retagging, not a deletion of any kind + return new Combine([ + t.explanations.retagNoOtherThemes, + TagRenderingQuestion.CreateTagExplanation( + new UIEventSource<TagsFilter>(retag), + currentTags, + state + ).SetClass("subtle"), + ]) + } - const retag: TagsFilter | undefined = selectedOption["retagTo"] - if(retag !== undefined) { - // This is a retagging, not a deletion of any kind - return new Combine([t.explanations.retagNoOtherThemes, - TagRenderingQuestion.CreateTagExplanation(new UIEventSource<TagsFilter>(retag), - currentTags, state - ).SetClass("subtle") - ]) - } - - const deleteReason = selectedOption["deleteReason"]; - if(deleteReason !== undefined){ - return new VariableUiElement(deleteAction.canBeDeleted.map(({ - canBeDeleted, reason - }) => { - if(canBeDeleted){ - // This is a hard delete for which we give an explanation - return t.explanations.hardDelete; - } - // This is a soft deletion: we explain _why_ the deletion is soft - return t.explanations.softDelete.Subs({reason: reason}) - - })) - - } - } - , [deleteAction.canBeDeleted] - )).SetClass("block") + const deleteReason = selectedOption["deleteReason"] + if (deleteReason !== undefined) { + return new VariableUiElement( + deleteAction.canBeDeleted.map(({ canBeDeleted, reason }) => { + if (canBeDeleted) { + // This is a hard delete for which we give an explanation + return t.explanations.hardDelete + } + // This is a soft deletion: we explain _why_ the deletion is soft + return t.explanations.softDelete.Subs({ reason: reason }) + }) + ) + } + }, + [deleteAction.canBeDeleted] + ) + ).SetClass("block") } - private static constructMultipleChoice(config: DeleteConfig, tagsSource: UIEventSource<Record<string, string>>, state: FeaturePipelineState): - InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> { - - const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[ ] = [] + private static constructMultipleChoice( + config: DeleteConfig, + tagsSource: UIEventSource<Record<string, string>>, + state: FeaturePipelineState + ): InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> { + const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[] = [] for (const nonDeleteOption of config.nonDeleteMappings) { - elements.push(new FixedInputElement( - new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state), - { - retagTo: nonDeleteOption.if - } - )) + elements.push( + new FixedInputElement( + new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state), + { + retagTo: nonDeleteOption.if, + } + ) + ) } - for (const extraDeleteReason of (config.extraDeleteReasons ?? [])) { - elements.push(new FixedInputElement( - new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state), - { - deleteReason: extraDeleteReason.changesetMessage - } - )) + for (const extraDeleteReason of config.extraDeleteReasons ?? []) { + elements.push( + new FixedInputElement( + new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state), + { + deleteReason: extraDeleteReason.changesetMessage, + } + ) + ) } for (const extraDeleteReason of DeleteConfig.defaultDeleteReasons) { - elements.push(new FixedInputElement( - extraDeleteReason.explanation.Clone(/*Must clone here, as this explanation might be used on many locations*/), - { - deleteReason: extraDeleteReason.changesetMessage - } - )) + elements.push( + new FixedInputElement( + extraDeleteReason.explanation.Clone(/*Must clone here, as this explanation might be used on many locations*/), + { + deleteReason: extraDeleteReason.changesetMessage, + } + ) + ) } - return new RadioButton(elements, {selectFirstAsDefault: false}); + return new RadioButton(elements, { selectFirstAsDefault: false }) } - - } class DeleteabilityChecker { - - public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; - private readonly _id: string; - private readonly _allowDeletionAtChangesetCount: number; + public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }> + private readonly _id: string + private readonly _allowDeletionAtChangesetCount: number private readonly _state: { osmConnection: OsmConnection - }; + } - - constructor(id: string, - state: { osmConnection: OsmConnection }, - allowDeletionAtChangesetCount?: number) { - this._id = id; - this._state = state; - this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE; + constructor( + id: string, + state: { osmConnection: OsmConnection }, + allowDeletionAtChangesetCount?: number + ) { + this._id = id + this._state = state + this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({ canBeDeleted: undefined, - reason: Translations.t.delete.loading + reason: Translations.t.delete.loading, }) this.CheckDeleteability(false) } @@ -301,145 +319,151 @@ class DeleteabilityChecker { * @private */ public CheckDeleteability(useTheInternet: boolean): void { - const t = Translations.t.delete; - const id = this._id; + const t = Translations.t.delete + const id = this._id const state = this.canBeDeleted - const self = this; + const self = this if (!id.startsWith("node")) { this.canBeDeleted.setData({ canBeDeleted: false, - reason: t.isntAPoint + reason: t.isntAPoint, }) - return; + return } // Does the currently logged in user have enough experience to delete this point? - const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map(ud => { + const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map((ud) => { if (ud === undefined) { - return undefined; + return undefined } if (!ud.loggedIn) { - return false; + return false } - return ud.csCount >= Math.min(Constants.userJourney.deletePointsOfOthersUnlock, this._allowDeletionAtChangesetCount); + return ( + ud.csCount >= + Math.min( + Constants.userJourney.deletePointsOfOthersUnlock, + this._allowDeletionAtChangesetCount + ) + ) }) const previousEditors = new UIEventSource<number[]>(undefined) - const allByMyself = previousEditors.map(previous => { - if (previous === null || previous === undefined) { - // Not yet downloaded - return null; - } - const userId = self._state.osmConnection.userDetails.data.uid; - return !previous.some(editor => editor !== userId) - }, [self._state.osmConnection.userDetails]) - + const allByMyself = previousEditors.map( + (previous) => { + if (previous === null || previous === undefined) { + // Not yet downloaded + return null + } + const userId = self._state.osmConnection.userDetails.data.uid + return !previous.some((editor) => editor !== userId) + }, + [self._state.osmConnection.userDetails] + ) // User allowed OR only edited by self? - const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => { - if (isAllowed === undefined) { - // No logged in user => definitively not allowed to delete! - return false; - } - if (isAllowed === true) { - return true; - } + const deletetionAllowed = deletingPointsOfOtherAllowed.map( + (isAllowed) => { + if (isAllowed === undefined) { + // No logged in user => definitively not allowed to delete! + return false + } + if (isAllowed === true) { + return true + } - // At this point, the logged in user is not allowed to delete points created/edited by _others_ - // however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all! + // At this point, the logged in user is not allowed to delete points created/edited by _others_ + // however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all! - if (allByMyself.data === null && useTheInternet) { - // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above - const hist = OsmObject.DownloadHistory(id).map(versions => versions.map(version => Number(version.tags["_last_edit:contributor:uid"]))) - hist.addCallbackAndRunD(hist => previousEditors.setData(hist)) - } - - if (allByMyself.data === true) { - // Yay! We can download! - return true; - } - if (allByMyself.data === false) { - // Nope, downloading not allowed... - return false; - } + if (allByMyself.data === null && useTheInternet) { + // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above + const hist = OsmObject.DownloadHistory(id).map((versions) => + versions.map((version) => + Number(version.tags["_last_edit:contributor:uid"]) + ) + ) + hist.addCallbackAndRunD((hist) => previousEditors.setData(hist)) + } + if (allByMyself.data === true) { + // Yay! We can download! + return true + } + if (allByMyself.data === false) { + // Nope, downloading not allowed... + return false + } - // At this point, we don't have enough information yet to decide if the user is allowed to delete the current point... - return undefined; - }, [allByMyself]) - + // At this point, we don't have enough information yet to decide if the user is allowed to delete the current point... + return undefined + }, + [allByMyself] + ) const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null) const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null) - deletetionAllowed.addCallbackAndRunD(deletetionAllowed => { - + deletetionAllowed.addCallbackAndRunD((deletetionAllowed) => { if (deletetionAllowed === false) { // Nope, we are not allowed to delete state.setData({ canBeDeleted: false, - reason: t.notEnoughExperience + reason: t.notEnoughExperience, }) - return true; // unregister this caller! + return true // unregister this caller! } if (!useTheInternet) { - return; + return } // All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations - OsmObject.DownloadReferencingRelations(id).then(rels => { + OsmObject.DownloadReferencingRelations(id).then((rels) => { hasRelations.setData(rels.length > 0) }) - OsmObject.DownloadReferencingWays(id).then(ways => { + OsmObject.DownloadReferencingWays(id).then((ways) => { hasWays.setData(ways.length > 0) }) - return true; // unregister to only run once + return true // unregister to only run once }) - - const hasWaysOrRelations = hasRelations.map(hasRelationsData => { - if (hasRelationsData === true) { - return true; - } - if (hasWays.data === true) { - return true; - } - if (hasWays.data === null || hasRelationsData === null) { - return null; - } - if (hasWays.data === false && hasRelationsData === false) { - return false; - } - return null; - }, [hasWays]) - - hasWaysOrRelations.addCallbackAndRun( - waysOrRelations => { - if (waysOrRelations == null) { - // Not yet loaded - we still wait a little bit - return; + const hasWaysOrRelations = hasRelations.map( + (hasRelationsData) => { + if (hasRelationsData === true) { + return true } - if (waysOrRelations) { - // not deleteble by mapcomplete - state.setData({ - canBeDeleted: false, - reason: t.partOfOthers - }) - } else { - // alright, this point can be safely deleted! - state.setData({ - canBeDeleted: true, - reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete - }) + if (hasWays.data === true) { + return true } - - - } + if (hasWays.data === null || hasRelationsData === null) { + return null + } + if (hasWays.data === false && hasRelationsData === false) { + return false + } + return null + }, + [hasWays] ) - + hasWaysOrRelations.addCallbackAndRun((waysOrRelations) => { + if (waysOrRelations == null) { + // Not yet loaded - we still wait a little bit + return + } + if (waysOrRelations) { + // not deleteble by mapcomplete + state.setData({ + canBeDeleted: false, + reason: t.partOfOthers, + }) + } else { + // alright, this point can be safely deleted! + state.setData({ + canBeDeleted: true, + reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete, + }) + } + }) } - - -} \ No newline at end of file +} diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 11901dcb5..566257068 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -1,45 +1,52 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import TagRenderingQuestion from "./TagRenderingQuestion"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import TagRenderingAnswer from "./TagRenderingAnswer"; -import Svg from "../../Svg"; -import Toggle from "../Input/Toggle"; -import BaseUIElement from "../BaseUIElement"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import {Unit} from "../../Models/Unit"; -import Lazy from "../Base/Lazy"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; +import { UIEventSource } from "../../Logic/UIEventSource" +import TagRenderingQuestion from "./TagRenderingQuestion" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import TagRenderingAnswer from "./TagRenderingAnswer" +import Svg from "../../Svg" +import Toggle from "../Input/Toggle" +import BaseUIElement from "../BaseUIElement" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import { Unit } from "../../Models/Unit" +import Lazy from "../Base/Lazy" +import { FixedUiElement } from "../Base/FixedUiElement" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" export default class EditableTagRendering extends Toggle { - - constructor(tags: UIEventSource<any>, - configuration: TagRenderingConfig, - units: Unit [], - state, - options: { - editMode?: UIEventSource<boolean>, - innerElementClasses?: string - } + constructor( + tags: UIEventSource<any>, + configuration: TagRenderingConfig, + units: Unit[], + state, + options: { + editMode?: UIEventSource<boolean> + innerElementClasses?: string + } ) { - // The tagrendering is hidden if: // - The answer is unknown. The questionbox will then show the question // - There is a condition hiding the answer - const renderingIsShown = tags.map(tags => - configuration.IsKnown(tags) && - (configuration?.condition?.matchesProperties(tags) ?? true)) + const renderingIsShown = tags.map( + (tags) => + configuration.IsKnown(tags) && + (configuration?.condition?.matchesProperties(tags) ?? true) + ) super( new Lazy(() => { const editMode = options.editMode ?? new UIEventSource<boolean>(false) - let rendering = EditableTagRendering.CreateRendering(state, tags, configuration, units, editMode); + let rendering = EditableTagRendering.CreateRendering( + state, + tags, + configuration, + units, + editMode + ) rendering.SetClass(options.innerElementClasses) - if(state.featureSwitchIsDebugging.data || state.featureSwitchIsTesting.data){ + if (state.featureSwitchIsDebugging.data || state.featureSwitchIsTesting.data) { rendering = new Combine([ new FixedUiElement(configuration.id).SetClass("self-end subtle"), - rendering + rendering, ]).SetClass("flex flex-col") } return rendering @@ -49,45 +56,51 @@ export default class EditableTagRendering extends Toggle { ) } - private static CreateRendering(state: FeaturePipelineState, tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>): BaseUIElement { + private static CreateRendering( + state: FeaturePipelineState, + tags: UIEventSource<any>, + configuration: TagRenderingConfig, + units: Unit[], + editMode: UIEventSource<boolean> + ): BaseUIElement { const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, state) answer.SetClass("w-full") - let rendering = answer; + let rendering = answer if (configuration.question !== undefined && state?.featureSwitchUserbadge?.data) { // We have a question and editing is enabled - const answerWithEditButton = new Combine([answer, - new Toggle(new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em") + const answerWithEditButton = new Combine([ + answer, + new Toggle( + new Combine([Svg.pencil_ui()]) + .SetClass("block relative h-10 w-10 p-2 float-right") + .SetStyle("border: 1px solid black; border-radius: 0.7em") .onClick(() => { - editMode.setData(true); + editMode.setData(true) }), undefined, - state.osmConnection.isLoggedIn) + state.osmConnection.isLoggedIn + ), ]).SetClass("flex justify-between w-full") - - const question = new Lazy(() => - new TagRenderingQuestion(tags, configuration, state, - { + const question = new Lazy( + () => + new TagRenderingQuestion(tags, configuration, state, { units: units, - cancelButton: Translations.t.general.cancel.Clone() + cancelButton: Translations.t.general.cancel + .Clone() .SetClass("btn btn-secondary") .onClick(() => { editMode.setData(false) }), afterSave: () => { editMode.setData(false) - } - })) - - - rendering = new Toggle( - question, - answerWithEditButton, - editMode + }, + }) ) - } - return rendering; - } -} \ No newline at end of file + rendering = new Toggle(question, answerWithEditButton, editMode) + } + return rendering + } +} diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index f3be3a279..bf40fd32d 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -1,97 +1,111 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import EditableTagRendering from "./EditableTagRendering"; -import QuestionBox from "./QuestionBox"; -import Combine from "../Base/Combine"; -import TagRenderingAnswer from "./TagRenderingAnswer"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; -import Constants from "../../Models/Constants"; -import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import DeleteWizard from "./DeleteWizard"; -import SplitRoadWizard from "./SplitRoadWizard"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {Utils} from "../../Utils"; -import MoveWizard from "./MoveWizard"; -import Toggle from "../Input/Toggle"; -import Lazy from "../Base/Lazy"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; +import { UIEventSource } from "../../Logic/UIEventSource" +import EditableTagRendering from "./EditableTagRendering" +import QuestionBox from "./QuestionBox" +import Combine from "../Base/Combine" +import TagRenderingAnswer from "./TagRenderingAnswer" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" +import Constants from "../../Models/Constants" +import SharedTagRenderings from "../../Customizations/SharedTagRenderings" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import DeleteWizard from "./DeleteWizard" +import SplitRoadWizard from "./SplitRoadWizard" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { Utils } from "../../Utils" +import MoveWizard from "./MoveWizard" +import Toggle from "../Input/Toggle" +import Lazy from "../Base/Lazy" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" export default class FeatureInfoBox extends ScrollableFullScreen { - - public constructor( tags: UIEventSource<any>, layerConfig: LayerConfig, state: FeaturePipelineState, options?: { - hashToShow?: string, - isShown?: UIEventSource<boolean>, + hashToShow?: string + isShown?: UIEventSource<boolean> setHash?: true | boolean } ) { if (state === undefined) { throw "State is undefined!" } - super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state), + super( + () => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state), () => FeatureInfoBox.GenerateContent(tags, layerConfig, state), options?.hashToShow ?? tags.data.id ?? "item", options?.isShown, - options); + options + ) if (layerConfig === undefined) { - throw "Undefined layerconfig"; + throw "Undefined layerconfig" } - } - public static GenerateTitleBar(tags: UIEventSource<any>, - layerConfig: LayerConfig, - state: {}): BaseUIElement { - const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), state) - .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl"); + public static GenerateTitleBar( + tags: UIEventSource<any>, + layerConfig: LayerConfig, + state: {} + ): BaseUIElement { + const title = new TagRenderingAnswer( + tags, + layerConfig.title ?? new TagRenderingConfig("POI"), + state + ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl") const titleIcons = new Combine( - layerConfig.titleIcons.map(icon => { - return new TagRenderingAnswer(tags, icon, state, - "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon"); - } - )) - .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") + layerConfig.titleIcons.map((icon) => { + return new TagRenderingAnswer( + tags, + icon, + state, + "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon" + ) + }) + ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") return new Combine([ - new Combine([title, titleIcons]).SetClass("flex flex-col sm:flex-row flex-grow justify-between") + new Combine([title, titleIcons]).SetClass( + "flex flex-col sm:flex-row flex-grow justify-between" + ), ]) } - public static GenerateContent(tags: UIEventSource<any>, - layerConfig: LayerConfig, - state: FeaturePipelineState): BaseUIElement { - let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>(); + public static GenerateContent( + tags: UIEventSource<any>, + layerConfig: LayerConfig, + state: FeaturePipelineState + ): BaseUIElement { + let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>() - const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) + const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map((tr) => tr.group)) if (state?.featureSwitchUserbadge?.data ?? true) { - const questionSpecs = layerConfig.tagRenderings.filter(tr => tr.id === "questions") + const questionSpecs = layerConfig.tagRenderings.filter((tr) => tr.id === "questions") for (const groupName of allGroupNames) { - const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName) - const questionSpec = questionSpecs.filter(tr => tr.group === groupName)[0] + const questions = layerConfig.tagRenderings.filter((tr) => tr.group === groupName) + const questionSpec = questionSpecs.filter((tr) => tr.group === groupName)[0] const questionBox = new QuestionBox(state, { tagsSource: tags, tagRenderings: questions, units: layerConfig.units, - showAllQuestionsAtOnce: questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? state.featureSwitchShowAllQuestions - }); + showAllQuestionsAtOnce: + questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? + state.featureSwitchShowAllQuestions, + }) questionBoxes.set(groupName, questionBox) } } const allRenderings = [] for (let i = 0; i < allGroupNames.length; i++) { - const groupName = allGroupNames[i]; + const groupName = allGroupNames[i] - const trs = layerConfig.tagRenderings.filter(tr => tr.group === groupName) + const trs = layerConfig.tagRenderings.filter((tr) => tr.group === groupName) const renderingsForGroup: (EditableTagRendering | BaseUIElement)[] = [] - const innerClasses = "block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2"; + const innerClasses = + "block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2" for (const tr of trs) { if (tr.question === null || tr.id === "questions") { // This is a question box! @@ -100,20 +114,27 @@ export default class FeatureInfoBox extends ScrollableFullScreen { if (tr.render !== undefined) { questionBox.SetClass("text-sm") - const renderedQuestion = new TagRenderingAnswer(tags, tr, state, - tr.group + " questions", "", { - specialViz: new Map<string, BaseUIElement>([["questions", questionBox]]) - }) + const renderedQuestion = new TagRenderingAnswer( + tags, + tr, + state, + tr.group + " questions", + "", + { + specialViz: new Map<string, BaseUIElement>([ + ["questions", questionBox], + ]), + } + ) const possiblyHidden = new Toggle( renderedQuestion, undefined, - questionBox.restingQuestions.map(ls => ls?.length > 0) + questionBox.restingQuestions.map((ls) => ls?.length > 0) ) renderingsForGroup.push(possiblyHidden) } else { renderingsForGroup.push(questionBox) } - } else { let classes = innerClasses let isHeader = renderingsForGroup.length === 0 && i > 0 @@ -124,7 +145,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } const etr = new EditableTagRendering(tags, tr, layerConfig.units, state, { - innerElementClasses: innerClasses + innerElementClasses: innerClasses, }) if (isHeader) { etr.SetClass("sticky top-0") @@ -137,10 +158,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } allRenderings.push( new Toggle( - new Lazy(() => FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state)), + new Lazy(() => + FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state) + ), undefined, state.featureSwitchUserbadge - )) + ) + ) return new Combine(allRenderings).SetClass("block") } @@ -153,82 +177,98 @@ export default class FeatureInfoBox extends ScrollableFullScreen { * @param state * @private */ - private static createEditElements(questionBoxes: Map<string, QuestionBox>, - layerConfig: LayerConfig, - tags: UIEventSource<any>, - state: FeaturePipelineState) - : BaseUIElement { + private static createEditElements( + questionBoxes: Map<string, QuestionBox>, + layerConfig: LayerConfig, + tags: UIEventSource<any>, + state: FeaturePipelineState + ): BaseUIElement { let editElements: BaseUIElement[] = [] - questionBoxes.forEach(questionBox => { - editElements.push(questionBox); + questionBoxes.forEach((questionBox) => { + editElements.push(questionBox) }) if (layerConfig.allowMove) { editElements.push( - new VariableUiElement(tags.map(tags => tags.id).map(id => { - const feature = state.allElements.ContainingFeatures.get(id) - if (feature === undefined) { - return "This feature is not register in the state.allElements and cannot be moved" - } - return new MoveWizard( - feature, - state, - layerConfig.allowMove - ); - }) + new VariableUiElement( + tags + .map((tags) => tags.id) + .map((id) => { + const feature = state.allElements.ContainingFeatures.get(id) + if (feature === undefined) { + return "This feature is not register in the state.allElements and cannot be moved" + } + return new MoveWizard(feature, state, layerConfig.allowMove) + }) ).SetClass("text-base") - ); + ) } if (layerConfig.deletion) { editElements.push( - new VariableUiElement(tags.map(tags => tags.id).map(id => - new DeleteWizard( - id, - state, - layerConfig.deletion - )) - ).SetClass("text-base")) + new VariableUiElement( + tags + .map((tags) => tags.id) + .map((id) => new DeleteWizard(id, state, layerConfig.deletion)) + ).SetClass("text-base") + ) } if (layerConfig.allowSplit) { editElements.push( - new VariableUiElement(tags.map(tags => tags.id).map(id => - new SplitRoadWizard(id, state)) - ).SetClass("text-base")) + new VariableUiElement( + tags.map((tags) => tags.id).map((id) => new SplitRoadWizard(id, state)) + ).SetClass("text-base") + ) } - editElements.push( new VariableUiElement( state.osmConnection.userDetails - .map(ud => ud.csCount) - .map(csCount => { - if (csCount <= Constants.userJourney.historyLinkVisible - && state.featureSwitchIsDebugging.data == false - && state.featureSwitchIsTesting.data === false) { - return undefined - } + .map((ud) => ud.csCount) + .map( + (csCount) => { + if ( + csCount <= Constants.userJourney.historyLinkVisible && + state.featureSwitchIsDebugging.data == false && + state.featureSwitchIsTesting.data === false + ) { + return undefined + } - return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"), state); - - }, [state.featureSwitchIsDebugging, state.featureSwitchIsTesting]) + return new TagRenderingAnswer( + tags, + SharedTagRenderings.SharedTagRendering.get("last_edit"), + state + ) + }, + [state.featureSwitchIsDebugging, state.featureSwitchIsTesting] + ) ) ) - editElements.push( new VariableUiElement( - state.featureSwitchIsDebugging.map(isDebugging => { + state.featureSwitchIsDebugging.map((isDebugging) => { if (isDebugging) { - const config_all_tags: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, ""); - const config_download: TagRenderingConfig = new TagRenderingConfig({render: "{export_as_geojson()}"}, ""); - const config_id: TagRenderingConfig = new TagRenderingConfig({render: "{open_in_iD()}"}, ""); + const config_all_tags: TagRenderingConfig = new TagRenderingConfig( + { render: "{all_tags()}" }, + "" + ) + const config_download: TagRenderingConfig = new TagRenderingConfig( + { render: "{export_as_geojson()}" }, + "" + ) + const config_id: TagRenderingConfig = new TagRenderingConfig( + { render: "{open_in_iD()}" }, + "" + ) - return new Combine([new TagRenderingAnswer(tags, config_all_tags, state), + return new Combine([ + new TagRenderingAnswer(tags, config_all_tags, state), new TagRenderingAnswer(tags, config_download, state), new TagRenderingAnswer(tags, config_id, state), - "This is layer "+layerConfig.id + "This is layer " + layerConfig.id, ]) } }) diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index cdf299e7d..2f163edad 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -1,48 +1,50 @@ -import BaseUIElement from "../BaseUIElement"; -import {SubtleButton} from "../Base/SubtleButton"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Translations from "../i18n/Translations"; -import Toggle from "../Input/Toggle"; -import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; -import Loading from "../Base/Loading"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Lazy from "../Base/Lazy"; -import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; -import Img from "../Base/Img"; -import FilteredLayer from "../../Models/FilteredLayer"; -import SpecialVisualizations from "../SpecialVisualizations"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Svg from "../../Svg"; -import {Utils} from "../../Utils"; -import Minimap from "../Base/Minimap"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; -import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; -import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; -import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; -import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; -import {DefaultGuiState} from "../DefaultGuiState"; -import {PresetInfo} from "../BigComponents/SimpleAddUI"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; -import {And} from "../../Logic/Tags/And"; -import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; -import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; -import {Tag} from "../../Logic/Tags/Tag"; -import TagApplyButton from "./TagApplyButton"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import * as conflation_json from "../../assets/layers/conflation/conflation.json"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {LoginToggle} from "./LoginButton"; -import {AutoAction} from "./AutoApplyButton"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Changes} from "../../Logic/Osm/Changes"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import Hash from "../../Logic/Web/Hash"; -import {PreciseInput} from "../../Models/ThemeConfig/PresetConfig"; +import BaseUIElement from "../BaseUIElement" +import { SubtleButton } from "../Base/SubtleButton" +import { UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import { VariableUiElement } from "../Base/VariableUIElement" +import Translations from "../i18n/Translations" +import Toggle from "../Input/Toggle" +import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" +import Loading from "../Base/Loading" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Lazy from "../Base/Lazy" +import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" +import Img from "../Base/Img" +import FilteredLayer from "../../Models/FilteredLayer" +import SpecialVisualizations from "../SpecialVisualizations" +import { FixedUiElement } from "../Base/FixedUiElement" +import Svg from "../../Svg" +import { Utils } from "../../Utils" +import Minimap from "../Base/Minimap" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" +import CreateWayWithPointReuseAction, { + MergePointConfig, +} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction" +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" +import FeatureSource from "../../Logic/FeatureSource/FeatureSource" +import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" +import { DefaultGuiState } from "../DefaultGuiState" +import { PresetInfo } from "../BigComponents/SimpleAddUI" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { And } from "../../Logic/Tags/And" +import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction" +import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction" +import { Tag } from "../../Logic/Tags/Tag" +import TagApplyButton from "./TagApplyButton" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import * as conflation_json from "../../assets/layers/conflation/conflation.json" +import { GeoOperations } from "../../Logic/GeoOperations" +import { LoginToggle } from "./LoginButton" +import { AutoAction } from "./AutoApplyButton" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Changes } from "../../Logic/Osm/Changes" +import { ElementStorage } from "../../Logic/ElementStorage" +import Hash from "../../Logic/Web/Hash" +import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig" /** * A helper class for the various import-flows. @@ -52,14 +54,18 @@ abstract class AbstractImportButton implements SpecialVisualizations { protected static importedIds = new Set<string>() public readonly funcName: string public readonly docs: string - public readonly args: { name: string, defaultValue?: string, doc: string }[] - private readonly showRemovedTags: boolean; - private readonly cannotBeImportedMessage: BaseUIElement | undefined; + public readonly args: { name: string; defaultValue?: string; doc: string }[] + private readonly showRemovedTags: boolean + private readonly cannotBeImportedMessage: BaseUIElement | undefined - constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string, required?: boolean }[], - options?: {showRemovedTags? : true | boolean, cannotBeImportedMessage?: BaseUIElement}) { + constructor( + funcName: string, + docsIntro: string, + extraArgs: { name: string; doc: string; defaultValue?: string; required?: boolean }[], + options?: { showRemovedTags?: true | boolean; cannotBeImportedMessage?: BaseUIElement } + ) { this.funcName = funcName - this.showRemovedTags = options?.showRemovedTags ?? true; + this.showRemovedTags = options?.showRemovedTags ?? true this.cannotBeImportedMessage = options?.cannotBeImportedMessage this.docs = `${docsIntro} @@ -77,34 +83,43 @@ ${Utils.special_visualizations_importRequirementDocs} { name: "targetLayer", doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements", - required: true + required: true, }, { name: "tags", doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead", - required: true + required: true, }, { name: "text", doc: "The text to show on the button", - defaultValue: "Import this data into OpenStreetMap" + defaultValue: "Import this data into OpenStreetMap", }, { name: "icon", doc: "A nice icon to show in the button", - defaultValue: "./assets/svg/addSmall.svg" + defaultValue: "./assets/svg/addSmall.svg", }, - ...extraArgs] - - }; - - abstract constructElement(state: FeaturePipelineState, - args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, newTags: UIEventSource<any>, targetLayer: string }, - tagSource: UIEventSource<any>, - guiState: DefaultGuiState, - feature: any, - onCancelClicked: () => void): BaseUIElement; + ...extraArgs, + ] + } + abstract constructElement( + state: FeaturePipelineState, + args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + tags: string + newTags: UIEventSource<any> + targetLayer: string + }, + tagSource: UIEventSource<any>, + guiState: DefaultGuiState, + feature: any, + onCancelClicked: () => void + ): BaseUIElement constr(state, tagSource: UIEventSource<any>, argsRaw, guiState) { /** @@ -116,16 +131,25 @@ ${Utils.special_visualizations_importRequirementDocs} * The actual import flow (showing the conflation map, special cases) are handled in 'constructElement' */ - const t = Translations.t.general.add.import; - const t0 = Translations.t.general.add; + const t = Translations.t.general.add.import + const t0 = Translations.t.general.add const args = this.parseArgs(argsRaw, tagSource) { // Some initial validation - if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) { + if ( + !state.layoutToUse.official && + !( + state.featureSwitchIsTesting.data || + state.osmConnection._oauth_config.url === + OsmConnection.oauth_configs["osm-test"].url + ) + ) { return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest]) } - const targetLayer: FilteredLayer = state.filteredLayers.data.filter(fl => fl.layerDef.id === args.targetLayer)[0] + const targetLayer: FilteredLayer = state.filteredLayers.data.filter( + (fl) => fl.layerDef.id === args.targetLayer + )[0] if (targetLayer === undefined) { const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found` console.error(e) @@ -133,7 +157,6 @@ ${Utils.special_visualizations_importRequirementDocs} } } - let img: BaseUIElement if (args.icon !== undefined && args.icon !== "") { img = new Img(args.icon) @@ -142,41 +165,54 @@ ${Utils.special_visualizations_importRequirementDocs} } const inviteToImportButton = new SubtleButton(img, args.text) - const id = tagSource.data.id; + const id = tagSource.data.id const feature = state.allElements.ContainingFeatures.get(id) - // Explanation of the tags that will be applied onto the imported/conflated object - let tagSpec = args.tags; - if (tagSpec.indexOf(" ") < 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined) { + let tagSpec = args.tags + if ( + tagSpec.indexOf(" ") < 0 && + tagSpec.indexOf(";") < 0 && + tagSource.data[args.tags] !== undefined + ) { // This is probably a key tagSpec = tagSource.data[args.tags] - console.debug("The import button is using tags from properties[" + args.tags + "] of this object, namely ", tagSpec) + console.debug( + "The import button is using tags from properties[" + + args.tags + + "] of this object, namely ", + tagSpec + ) } - const importClicked = new UIEventSource(false); + const importClicked = new UIEventSource(false) inviteToImportButton.onClick(() => { - importClicked.setData(true); + importClicked.setData(true) }) - - const pleaseLoginButton = new Toggle(t0.pleaseLogin + const pleaseLoginButton = new Toggle( + t0.pleaseLogin .onClick(() => state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"), undefined, - state.featureSwitchUserbadge) + state.featureSwitchUserbadge + ) - - const isImported = tagSource.map(tags => { + const isImported = tagSource.map((tags) => { AbstractImportButton.importedIds.add(tags.id) - return tags._imported === "yes"; + return tags._imported === "yes" }) - /**** THe actual panel showing the import guiding map ****/ - const importGuidingPanel = this.constructElement(state, args, tagSource, guiState, feature, () => importClicked.setData(false)) - + const importGuidingPanel = this.constructElement( + state, + args, + tagSource, + guiState, + feature, + () => importClicked.setData(false) + ) const importFlow = new Toggle( new Toggle( @@ -186,25 +222,21 @@ ${Utils.special_visualizations_importRequirementDocs} ), inviteToImportButton, importClicked - ); + ) return new Toggle( new LoginToggle( new Toggle( - new Toggle( - t.hasBeenImported, - importFlow, - isImported - ), + new Toggle(t.hasBeenImported, importFlow, isImported), t.zoomInMore.SetClass("alert block"), - state.locationControl.map(l => l.zoom >= 18) + state.locationControl.map((l) => l.zoom >= 18) ), pleaseLoginButton, state ), this.cannotBeImportedMessage ?? t.wrongType, - new UIEventSource(this.canBeImported(feature))) - + new UIEventSource(this.canBeImported(feature)) + ) } getLayerDependencies(argsRaw: string[]) { @@ -215,7 +247,7 @@ ${Utils.special_visualizations_importRequirementDocs} // The target layer dependsOnLayers.push(args.targetLayer) - const snapOntoLayers = args.snap_onto_layers?.trim() ?? ""; + const snapOntoLayers = args.snap_onto_layers?.trim() ?? "" if (snapOntoLayers !== "") { dependsOnLayers.push(...snapOntoLayers.split(";")) } @@ -227,15 +259,23 @@ ${Utils.special_visualizations_importRequirementDocs} protected createConfirmPanelForWay( state: FeaturePipelineState, - args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<Tag[]>, targetLayer: string }, + args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + newTags: UIEventSource<Tag[]> + targetLayer: string + }, feature: any, originalFeatureTags: UIEventSource<any>, - action: (OsmChangeAction & { getPreview(): Promise<FeatureSource>, newElementId?: string }), - onCancel: () => void): BaseUIElement { - const self = this; + action: OsmChangeAction & { getPreview(): Promise<FeatureSource>; newElementId?: string }, + onCancel: () => void + ): BaseUIElement { + const self = this const confirmationMap = Minimap.createMiniMap({ allowMoving: state.featureSwitchIsDebugging.data ?? false, - background: state.backgroundLayer + background: state.backgroundLayer, }) confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") @@ -245,28 +285,33 @@ ${Utils.special_visualizations_importRequirementDocs} zoomToFeatures: true, features: StaticFeatureSource.fromGeojson([feature]), state: state, - layers: state.filteredLayers + layers: state.filteredLayers, }) - - action.getPreview().then(changePreview => { + action.getPreview().then((changePreview) => { new ShowDataLayer({ leafletMap: confirmationMap.leafletMap, zoomToFeatures: false, features: changePreview, state, - layerToShow: new LayerConfig(conflation_json, "all_known_layers", true) + layerToShow: new LayerConfig(conflation_json, "all_known_layers", true), }) }) - const tagsExplanation = new VariableUiElement(args.newTags.map(tagsToApply => { - const filteredTags = tagsToApply.filter(t => self.showRemovedTags || (t.value ?? "") !== "") + const tagsExplanation = new VariableUiElement( + args.newTags.map((tagsToApply) => { + const filteredTags = tagsToApply.filter( + (t) => self.showRemovedTags || (t.value ?? "") !== "" + ) const tagsStr = new And(filteredTags).asHumanString(false, true, {}) - return Translations.t.general.add.import.importTags.Subs({tags: tagsStr}); - } - )).SetClass("subtle") + return Translations.t.general.add.import.importTags.Subs({ tags: tagsStr }) + }) + ).SetClass("subtle") - const confirmButton = new SubtleButton(new Img(args.icon), new Combine([args.text, tagsExplanation]).SetClass("flex flex-col")) + const confirmButton = new SubtleButton( + new Img(args.icon), + new Combine([args.text, tagsExplanation]).SetClass("flex flex-col") + ) confirmButton.onClick(async () => { { originalFeatureTags.data["_imported"] = "yes" @@ -277,19 +322,41 @@ ${Utils.special_visualizations_importRequirementDocs} } }) - const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(onCancel) + const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick( + onCancel + ) return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col") } - protected parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } { + protected parseArgs( + argsRaw: string[], + originalFeatureTags: UIEventSource<any> + ): { + minzoom: string + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + tags: string + targetLayer: string + newTags: UIEventSource<Tag[]> + } { const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) if (originalFeatureTags !== undefined) { - const tags = baseArgs.tags - if (tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined) { + if ( + tags.indexOf(" ") < 0 && + tags.indexOf(";") < 0 && + originalFeatureTags.data[tags] !== undefined + ) { // This might be a property to expand... const items: string = originalFeatureTags.data[tags] - console.debug("The import button is using tags from properties[" + tags + "] of this object, namely ", items) + console.debug( + "The import button is using tags from properties[" + + tags + + "] of this object, namely ", + items + ) baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags) } else { baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) @@ -300,55 +367,66 @@ ${Utils.special_visualizations_importRequirementDocs} } export class ConflateButton extends AbstractImportButton { - constructor() { - super("conflate_button", "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)", - [{ - name: "way_to_conflate", - doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag" - }], + super( + "conflate_button", + "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)", + [ + { + name: "way_to_conflate", + doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag", + }, + ], { - cannotBeImportedMessage: Translations.t.general.add.import.wrongTypeToConflate + cannotBeImportedMessage: Translations.t.general.add.import.wrongTypeToConflate, } - ); + ) } getLayerDependencies(argsRaw: string[]): string[] { - const deps = super.getLayerDependencies(argsRaw); + const deps = super.getLayerDependencies(argsRaw) // Force 'type_node' as dependency deps.push("type_node") - return deps; + return deps } - constructElement(state: FeaturePipelineState, - args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string }, - tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement { - - const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) + constructElement( + state: FeaturePipelineState, + args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + tags: string + newTags: UIEventSource<Tag[]> + targetLayer: string + }, + tagSource: UIEventSource<any>, + guiState: DefaultGuiState, + feature: any, + onCancelClicked: () => void + ): BaseUIElement { + const nodesMustMatch = args.snap_onto_layers + ?.split(";") + ?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) const mergeConfigs = [] if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) { const mergeConfig: MergePointConfig = { mode: args["point_move_mode"] == "move_osm" ? "move_osm_point" : "reuse_osm_point", ifMatches: new And(nodesMustMatch), - withinRangeOfM: Number(args.max_snap_distance) + withinRangeOfM: Number(args.max_snap_distance), } mergeConfigs.push(mergeConfig) } - const key = args["way_to_conflate"] const wayToConflate = tagSource.data[key] - feature = GeoOperations.removeOvernoding(feature); - const action = new ReplaceGeometryAction( - state, - feature, - wayToConflate, - { - theme: state.layoutToUse.id, - newTags: args.newTags.data - } - ) + feature = GeoOperations.removeOvernoding(feature) + const action = new ReplaceGeometryAction(state, feature, wayToConflate, { + theme: state.layoutToUse.id, + newTags: args.newTags.data, + }) return this.createConfirmPanelForWay( state, @@ -361,16 +439,19 @@ export class ConflateButton extends AbstractImportButton { } protected canBeImported(feature: any) { - return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1) + return ( + feature.geometry.type === "LineString" || + (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1) + ) } - } export class ImportWayButton extends AbstractImportButton implements AutoAction { - public readonly supportsAutoAction = true; + public readonly supportsAutoAction = true constructor() { - super("import_way_button", + super( + "import_way_button", "This button will copy the data from an external dataset into OpenStreetMap", [ { @@ -380,34 +461,47 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction { name: "max_snap_distance", doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way", - defaultValue: "0.05" + defaultValue: "0.05", }, { name: "move_osm_point_if", doc: "Moves the OSM-point to the newly imported point if these conditions are met", - }, { - name: "max_move_distance", - doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m", - defaultValue: "0.05" - }, { - name: "snap_onto_layers", - doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead", - - }, { - name: "snap_to_layer_max_distance", - doc: "Distance to distort the geometry to snap to this layer", - defaultValue: "0.1" - }], - { showRemovedTags: false} + }, + { + name: "max_move_distance", + doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m", + defaultValue: "0.05", + }, + { + name: "snap_onto_layers", + doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead", + }, + { + name: "snap_to_layer_max_distance", + doc: "Distance to distort the geometry to snap to this layer", + defaultValue: "0.1", + }, + ], + { showRemovedTags: false } ) } - private static CreateAction(feature, - args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string }, - state: FeaturePipelineState, - mergeConfigs: any[]) { + private static CreateAction( + feature, + args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + tags: string + newTags: UIEventSource<any> + targetLayer: string + }, + state: FeaturePipelineState, + mergeConfigs: any[] + ) { const coors = feature.geometry.coordinates - if ((feature.geometry.type === "Polygon") && coors.length > 1) { + if (feature.geometry.type === "Polygon" && coors.length > 1) { const outer = coors[0] const inner = [...coors] inner.splice(0, 1) @@ -421,36 +515,27 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction ) } else if (feature.geometry.type === "Polygon") { const outer = coors[0] - return new CreateWayWithPointReuseAction( - args.newTags.data, - outer, - state, - mergeConfigs - ) + return new CreateWayWithPointReuseAction(args.newTags.data, outer, state, mergeConfigs) } else if (feature.geometry.type === "LineString") { - return new CreateWayWithPointReuseAction( - args.newTags.data, - coors, - state, - mergeConfigs - ) + return new CreateWayWithPointReuseAction(args.newTags.data, coors, state, mergeConfigs) } else { throw "Unsupported type" } } - async applyActionOn(state: { layoutToUse: LayoutConfig; changes: Changes, allElements: ElementStorage }, - originalFeatureTags: UIEventSource<any>, - argument: string[]): Promise<void> { - const id = originalFeatureTags.data.id; - if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id) - ) { - return; + async applyActionOn( + state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage }, + originalFeatureTags: UIEventSource<any>, + argument: string[] + ): Promise<void> { + const id = originalFeatureTags.data.id + if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id)) { + return } AbstractImportButton.importedIds.add(originalFeatureTags.data.id) const args = this.parseArgs(argument, originalFeatureTags) const feature = state.allElements.ContainingFeatures.get(id) - const mergeConfigs = this.GetMergeConfig(args); + const mergeConfigs = this.GetMergeConfig(args) const action = ImportWayButton.CreateAction( feature, args, @@ -466,18 +551,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction } getLayerDependencies(argsRaw: string[]): string[] { - const deps = super.getLayerDependencies(argsRaw); + const deps = super.getLayerDependencies(argsRaw) deps.push("type_node") return deps } - constructElement(state, args, - originalFeatureTags, - guiState, - feature, - onCancel): BaseUIElement { - - + constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement { const geometry = feature.geometry if (!(geometry.type == "LineString" || geometry.type === "Polygon")) { @@ -485,10 +564,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert") } - // Upload the way to OSM - const mergeConfigs = this.GetMergeConfig(args); - let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs); + const mergeConfigs = this.GetMergeConfig(args) + let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) return this.createConfirmPanelForWay( state, args, @@ -497,83 +575,105 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction action, onCancel ) - } - private GetMergeConfig(args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string }) - : MergePointConfig[] { - const nodesMustMatch = args["snap_to_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) + private GetMergeConfig(args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + tags: string + newTags: UIEventSource<any> + targetLayer: string + }): MergePointConfig[] { + const nodesMustMatch = args["snap_to_point_if"] + ?.split(";") + ?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) const mergeConfigs = [] if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) { const mergeConfig: MergePointConfig = { mode: "reuse_osm_point", ifMatches: new And(nodesMustMatch), - withinRangeOfM: Number(args.max_snap_distance) + withinRangeOfM: Number(args.max_snap_distance), } mergeConfigs.push(mergeConfig) } - - const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) + const moveOsmPointIfTags = args["move_osm_point_if"] + ?.split(";") + ?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) { const moveDistance = Math.min(20, Number(args["max_move_distance"])) const mergeConfig: MergePointConfig = { mode: "move_osm_point", ifMatches: new And(moveOsmPointIfTags), - withinRangeOfM: moveDistance + withinRangeOfM: moveDistance, } mergeConfigs.push(mergeConfig) } - return mergeConfigs; + return mergeConfigs } } export class ImportPointButton extends AbstractImportButton { - constructor() { - super("import_button", + super( + "import_button", "This button will copy the point from an external dataset into OpenStreetMap", [ { name: "snap_onto_layers", - doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list" + doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list", }, { name: "max_snap_distance", doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete", - defaultValue: "5" + defaultValue: "5", }, { name: "note_id", - doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'" + doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'", }, { - name:"location_picker", + name: "location_picker", defaultValue: "photo", - doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled" + doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled", }, { name: "maproulette_id", - doc: "If given, the maproulette challenge will be marked as fixed" - }], - { showRemovedTags: false} + doc: "If given, the maproulette challenge will be marked as fixed", + }, + ], + { showRemovedTags: false } ) } private static createConfirmPanelForPoint( - args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string, maproulette_id: string }, + args: { + max_snap_distance: string + snap_onto_layers: string + icon: string + text: string + newTags: UIEventSource<any> + targetLayer: string + note_id: string + maproulette_id: string + }, state: FeaturePipelineState, guiState: DefaultGuiState, originalFeatureTags: UIEventSource<any>, feature: any, onCancel: () => void, - close: () => void): BaseUIElement { - - async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) { - + close: () => void + ): BaseUIElement { + async function confirm( + tags: any[], + location: { lat: number; lon: number }, + snapOntoWayId: string + ) { originalFeatureTags.data["_imported"] = "yes" originalFeatureTags.ping() // will set isImported as per its definition let snapOnto: OsmObject = undefined @@ -592,13 +692,13 @@ export class ImportPointButton extends AbstractImportButton { theme: state.layoutToUse.id, changeType: "import", snapOnto: <OsmWay>snapOnto, - specialMotivation: specialMotivation + specialMotivation: specialMotivation, }) await state.changes.applyAction(newElementAction) - state.selectedElement.setData(state.allElements.ContainingFeatures.get( - newElementAction.newElementId - )) + state.selectedElement.setData( + state.allElements.ContainingFeatures.get(newElementAction.newElementId) + ) Hash.hash.setData(newElementAction.newElementId) if (note_id !== undefined) { @@ -607,47 +707,63 @@ export class ImportPointButton extends AbstractImportButton { originalFeatureTags.ping() } - let maproulette_id = originalFeatureTags.data[args.maproulette_id]; - console.log("Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")") + let maproulette_id = originalFeatureTags.data[args.maproulette_id] + console.log( + "Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")" + ) if (maproulette_id !== undefined) { - if (state.featureSwitchIsTesting.data){ - console.log("Not marking maproulette task " + maproulette_id + " as fixed, because we are in testing mode") + if (state.featureSwitchIsTesting.data) { + console.log( + "Not marking maproulette task " + + maproulette_id + + " as fixed, because we are in testing mode" + ) } else { console.log("Marking maproulette task as fixed") - state.maprouletteConnection.closeTask(Number(maproulette_id)); - originalFeatureTags.data["mr_taskStatus"] = "Fixed"; - originalFeatureTags.ping(); + state.maprouletteConnection.closeTask(Number(maproulette_id)) + originalFeatureTags.data["mr_taskStatus"] = "Fixed" + originalFeatureTags.ping() } } } let preciseInputOption = args["location_picker"] - let preciseInputSpec: PreciseInput = undefined + let preciseInputSpec: PreciseInput = undefined console.log("Precise input location is ", preciseInputOption) - if(preciseInputOption !== "none") { + if (preciseInputOption !== "none") { preciseInputSpec = { snapToLayers: args.snap_onto_layers?.split(";"), - maxSnapDistance: Number(args.max_snap_distance), - preferredBackground: args["location_picker"] ?? ["photo", "map"] + maxSnapDistance: Number(args.max_snap_distance), + preferredBackground: args["location_picker"] ?? ["photo", "map"], } } - + const presetInfo = <PresetInfo>{ tags: args.newTags.data, icon: () => new Img(args.icon), - layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0], + layerToAddTo: state.filteredLayers.data.filter( + (l) => l.layerDef.id === args.targetLayer + )[0], name: args.text, title: Translations.T(args.text), preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise - boundsFactor: 3 + boundsFactor: 3, } const [lon, lat] = feature.geometry.coordinates - return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), { - lon, - lat - }, confirm, onCancel, close) - + return new ConfirmLocationOfPoint( + state, + guiState.filterViewIsOpened, + presetInfo, + Translations.W(args.text), + { + lon, + lat, + }, + confirm, + onCancel, + close + ) } canBeImported(feature: any) { @@ -655,7 +771,7 @@ export class ImportPointButton extends AbstractImportButton { } getLayerDependencies(argsRaw: string[]): string[] { - const deps = super.getLayerDependencies(argsRaw); + const deps = super.getLayerDependencies(argsRaw) const layerSnap = argsRaw["snap_onto_layers"] ?? "" if (layerSnap === "") { return deps @@ -665,35 +781,34 @@ export class ImportPointButton extends AbstractImportButton { return deps } - constructElement(state, args, - originalFeatureTags, - guiState, - feature, - onCancel: () => void): BaseUIElement { - - + constructElement( + state, + args, + originalFeatureTags, + guiState, + feature, + onCancel: () => void + ): BaseUIElement { const geometry = feature.geometry if (geometry.type === "Point") { - return new Lazy(() => ImportPointButton.createConfirmPanelForPoint( - args, - state, - guiState, - originalFeatureTags, - feature, - onCancel, - () => { - // Close the current popup - state.selectedElement.setData(undefined) - } - )) + return new Lazy(() => + ImportPointButton.createConfirmPanelForPoint( + args, + state, + guiState, + originalFeatureTags, + feature, + onCancel, + () => { + // Close the current popup + state.selectedElement.setData(undefined) + } + ) + ) } - console.error("Invalid type to import", geometry.type) return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert") - } - - -} \ No newline at end of file +} diff --git a/UI/Popup/LoginButton.ts b/UI/Popup/LoginButton.ts index ecf859daf..d85b89a45 100644 --- a/UI/Popup/LoginButton.ts +++ b/UI/Popup/LoginButton.ts @@ -1,46 +1,52 @@ -import {SubtleButton} from "../Base/SubtleButton"; -import BaseUIElement from "../BaseUIElement"; -import Svg from "../../Svg"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Toggle from "../Input/Toggle"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Loading from "../Base/Loading"; -import Translations from "../i18n/Translations"; +import { SubtleButton } from "../Base/SubtleButton" +import BaseUIElement from "../BaseUIElement" +import Svg from "../../Svg" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Toggle from "../Input/Toggle" +import { VariableUiElement } from "../Base/VariableUIElement" +import Loading from "../Base/Loading" +import Translations from "../i18n/Translations" class LoginButton extends SubtleButton { - - constructor(text: BaseUIElement | string, state: { - osmConnection: OsmConnection - }, icon?: BaseUIElement | string) { - super(icon ?? Svg.osm_logo_ui(), text); + constructor( + text: BaseUIElement | string, + state: { + osmConnection: OsmConnection + }, + icon?: BaseUIElement | string + ) { + super(icon ?? Svg.osm_logo_ui(), text) this.onClick(() => { state.osmConnection.AttemptLogin() }) } - } export class LoginToggle extends VariableUiElement { - constructor(el, text: BaseUIElement | string, state: { - osmConnection: OsmConnection - }) { + constructor( + el, + text: BaseUIElement | string, + state: { + osmConnection: OsmConnection + } + ) { const loading = new Loading("Trying to log in...") const login = new LoginButton(text, state) super( - state.osmConnection.loadingStatus.map(osmConnectionState => { - if(osmConnectionState === "loading"){ + state.osmConnection.loadingStatus.map((osmConnectionState) => { + if (osmConnectionState === "loading") { return loading } - if(osmConnectionState === "not-attempted"){ + if (osmConnectionState === "not-attempted") { return login } - if(osmConnectionState === "logged-in"){ - return el + if (osmConnectionState === "logged-in") { + return el } - + // Error! return new LoginButton(Translations.t.general.loginFailed, state, Svg.invalid_svg()) }) - ) + ) } -} \ No newline at end of file +} diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts index b6eb6e712..b6aec3db7 100644 --- a/UI/Popup/MoveWizard.ts +++ b/UI/Popup/MoveWizard.ts @@ -1,33 +1,33 @@ -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Toggle from "../Input/Toggle"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Translation} from "../i18n/Translation"; -import BaseUIElement from "../BaseUIElement"; -import LocationInput from "../Input/LocationInput"; -import Loc from "../../Models/Loc"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {OsmObject} from "../../Logic/Osm/OsmObject"; -import {Changes} from "../../Logic/Osm/Changes"; -import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import MoveConfig from "../../Models/ThemeConfig/MoveConfig"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import BaseLayer from "../../Models/BaseLayer"; +import { SubtleButton } from "../Base/SubtleButton" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Toggle from "../Input/Toggle" +import { UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Translation } from "../i18n/Translation" +import BaseUIElement from "../BaseUIElement" +import LocationInput from "../Input/LocationInput" +import Loc from "../../Models/Loc" +import { GeoOperations } from "../../Logic/GeoOperations" +import { OsmObject } from "../../Logic/Osm/OsmObject" +import { Changes } from "../../Logic/Osm/Changes" +import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import MoveConfig from "../../Models/ThemeConfig/MoveConfig" +import { ElementStorage } from "../../Logic/ElementStorage" +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" +import BaseLayer from "../../Models/BaseLayer" interface MoveReason { - text: Translation | string, - invitingText: Translation | string, - icon: BaseUIElement, - changesetCommentValue: string, - lockBounds: true | boolean, - background: undefined | "map" | "photo" | string | string[], - startZoom: number, + text: Translation | string + invitingText: Translation | string + icon: BaseUIElement + changesetCommentValue: string + lockBounds: true | boolean + background: undefined | "map" | "photo" | string | string[] + startZoom: number minZoom: number } @@ -38,13 +38,14 @@ export default class MoveWizard extends Toggle { constructor( featureToMove: any, state: { - osmConnection: OsmConnection, - featureSwitchUserbadge: UIEventSource<boolean>, - changes: Changes, - layoutToUse: LayoutConfig, + osmConnection: OsmConnection + featureSwitchUserbadge: UIEventSource<boolean> + changes: Changes + layoutToUse: LayoutConfig allElements: ElementStorage - }, options: MoveConfig) { - + }, + options: MoveConfig + ) { const t = Translations.t.move const loginButton = new Toggle( t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()), @@ -62,7 +63,7 @@ export default class MoveWizard extends Toggle { lockBounds: false, background: undefined, startZoom: 12, - minZoom: 6 + minZoom: 6, }) } if (options.enableImproveAccuracy) { @@ -74,13 +75,15 @@ export default class MoveWizard extends Toggle { lockBounds: true, background: "photo", startZoom: 17, - minZoom: 16 + minZoom: 16, }) } - const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">("start") + const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">( + "start" + ) const moveReason = new UIEventSource<MoveReason>(undefined) - let moveButton: BaseUIElement; + let moveButton: BaseUIElement if (reasons.length === 1) { const reason = reasons[0] moveReason.setData(reason) @@ -99,32 +102,32 @@ export default class MoveWizard extends Toggle { }) } - - const moveAgainButton = new SubtleButton( - Svg.move_ui(), - t.inviteToMoveAgain - ).onClick(() => { + const moveAgainButton = new SubtleButton(Svg.move_ui(), t.inviteToMoveAgain).onClick(() => { currentStep.setData("reason") }) + const selectReason = new Combine( + reasons.map((r) => + new SubtleButton(r.icon, r.text).onClick(() => { + moveReason.setData(r) + currentStep.setData("pick_location") + }) + ) + ) - const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => { - moveReason.setData(r) - currentStep.setData("pick_location") - }))) - - const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() => currentStep.setData("start")) - + const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() => + currentStep.setData("start") + ) const [lon, lat] = GeoOperations.centerpointCoordinates(featureToMove) - const locationInput = moveReason.map(reason => { + const locationInput = moveReason.map((reason) => { if (reason === undefined) { return undefined } const loc = new UIEventSource<Loc>({ lon: lon, lat: lat, - zoom: reason?.startZoom ?? 16 + zoom: reason?.startZoom ?? 16, }) let background: string[] @@ -134,12 +137,14 @@ export default class MoveWizard extends Toggle { background = reason.background } - const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)).data + const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo( + loc, + new UIEventSource(background) + ).data const locationInput = new LocationInput({ minZoom: reason.minZoom, centerLocation: loc, - mapBackground: new UIEventSource<BaseLayer>(preferredBackground) // We detach the layer - + mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer }) if (reason.lockBounds) { @@ -151,10 +156,12 @@ export default class MoveWizard extends Toggle { const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove) confirmMove.onClick(() => { const loc = locationInput.GetValue().data - state.changes.applyAction(new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { - reason: reason.changesetCommentValue, - theme: state.layoutToUse.id - })) + state.changes.applyAction( + new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { + reason: reason.changesetCommentValue, + theme: state.layoutToUse.id, + }) + ) featureToMove.properties._lat = loc.lat featureToMove.properties._lon = loc.lon state.allElements.getEventSourceById(id).ping() @@ -163,28 +170,42 @@ export default class MoveWizard extends Toggle { const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6") return new Combine([ locationInput, - new Toggle(confirmMove, zoomInFurhter, locationInput.GetValue().map(l => l.zoom >= 19)) + new Toggle( + confirmMove, + zoomInFurhter, + locationInput.GetValue().map((l) => l.zoom >= 19) + ), ]).SetClass("flex flex-col") - }); + }) const dialogClasses = "p-2 md:p-4 m-2 border border-gray-400 rounded-xl flex flex-col" const moveFlow = new Toggle( - new VariableUiElement(currentStep.map(currentStep => { - switch (currentStep) { - case "start": - return moveButton; - case "reason": - return new Combine([t.whyMove.SetClass("text-lg font-bold"), selectReason, cancelButton]).SetClass(dialogClasses); - case "pick_location": - return new Combine([t.moveTitle.SetClass("text-lg font-bold"), new VariableUiElement(locationInput), cancelButton]).SetClass(dialogClasses) - case "moved": - return new Combine([t.pointIsMoved.SetClass("thanks"), moveAgainButton]).SetClass("flex flex-col"); - - } - - - })), + new VariableUiElement( + currentStep.map((currentStep) => { + switch (currentStep) { + case "start": + return moveButton + case "reason": + return new Combine([ + t.whyMove.SetClass("text-lg font-bold"), + selectReason, + cancelButton, + ]).SetClass(dialogClasses) + case "pick_location": + return new Combine([ + t.moveTitle.SetClass("text-lg font-bold"), + new VariableUiElement(locationInput), + cancelButton, + ]).SetClass(dialogClasses) + case "moved": + return new Combine([ + t.pointIsMoved.SetClass("thanks"), + moveAgainButton, + ]).SetClass("flex flex-col") + } + }) + ), loginButton, state.osmConnection.isLoggedIn ) @@ -200,14 +221,13 @@ export default class MoveWizard extends Toggle { } else if (id.startsWith("relation")) { moveDisallowedReason.setData(t.isRelation) } else if (id.indexOf("-") < 0) { - - OsmObject.DownloadReferencingWays(id).then(referencing => { + OsmObject.DownloadReferencingWays(id).then((referencing) => { if (referencing.length > 0) { console.log("Got a referencing way, move not allowed") moveDisallowedReason.setData(t.partOfAWay) } }) - OsmObject.DownloadReferencingRelations(id).then(partOf => { + OsmObject.DownloadReferencingRelations(id).then((partOf) => { if (partOf.length > 0) { moveDisallowedReason.setData(t.partOfRelation) } @@ -217,11 +237,12 @@ export default class MoveWizard extends Toggle { moveFlow, new Combine([ Svg.move_not_allowed_svg().SetStyle("height: 2rem").SetClass("m-2"), - new Combine([t.cannotBeMoved, - new VariableUiElement(moveDisallowedReason).SetClass("subtle") - ]).SetClass("flex flex-col") + new Combine([ + t.cannotBeMoved, + new VariableUiElement(moveDisallowedReason).SetClass("subtle"), + ]).SetClass("flex flex-col"), ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"), - moveDisallowedReason.map(r => r === undefined) + moveDisallowedReason.map((r) => r === undefined) ) } -} \ No newline at end of file +} diff --git a/UI/Popup/MultiApply.ts b/UI/Popup/MultiApply.ts index 2b3149027..df6829d8c 100644 --- a/UI/Popup/MultiApply.ts +++ b/UI/Popup/MultiApply.ts @@ -1,43 +1,41 @@ -import {Store} from "../../Logic/UIEventSource"; -import BaseUIElement from "../BaseUIElement"; -import Combine from "../Base/Combine"; -import {SubtleButton} from "../Base/SubtleButton"; -import {Changes} from "../../Logic/Osm/Changes"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import {Tag} from "../../Logic/Tags/Tag"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import {And} from "../../Logic/Tags/And"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import Toggle from "../Input/Toggle"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; - +import { Store } from "../../Logic/UIEventSource" +import BaseUIElement from "../BaseUIElement" +import Combine from "../Base/Combine" +import { SubtleButton } from "../Base/SubtleButton" +import { Changes } from "../../Logic/Osm/Changes" +import { FixedUiElement } from "../Base/FixedUiElement" +import Translations from "../i18n/Translations" +import { VariableUiElement } from "../Base/VariableUIElement" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import { Tag } from "../../Logic/Tags/Tag" +import { ElementStorage } from "../../Logic/ElementStorage" +import { And } from "../../Logic/Tags/And" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import Toggle from "../Input/Toggle" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" export interface MultiApplyParams { - featureIds: Store<string[]>, - keysToApply: string[], - text: string, - autoapply: boolean, - overwrite: boolean, - tagsSource: Store<any>, + featureIds: Store<string[]> + keysToApply: string[] + text: string + autoapply: boolean + overwrite: boolean + tagsSource: Store<any> state: { - changes: Changes, - allElements: ElementStorage, - layoutToUse: LayoutConfig, + changes: Changes + allElements: ElementStorage + layoutToUse: LayoutConfig osmConnection: OsmConnection } } class MultiApplyExecutor { - private static executorCache = new Map<string, MultiApplyExecutor>() private readonly originalValues = new Map<string, string>() - private readonly params: MultiApplyParams; + private readonly params: MultiApplyParams private constructor(params: MultiApplyParams) { - this.params = params; + this.params = params const p = params for (const key of p.keysToApply) { @@ -45,14 +43,13 @@ class MultiApplyExecutor { } if (p.autoapply) { - - const self = this; - const relevantValues = p.tagsSource.map(tags => { - const currentValues = p.keysToApply.map(key => tags[key]) + const self = this + const relevantValues = p.tagsSource.map((tags) => { + const currentValues = p.keysToApply.map((key) => tags[key]) // By stringifying, we have a very clear ping when they changec - return JSON.stringify(currentValues); + return JSON.stringify(currentValues) }) - relevantValues.addCallbackD(_ => { + relevantValues.addCallbackD((_) => { self.applyTaggingOnOtherFeatures() }) } @@ -74,7 +71,7 @@ class MultiApplyExecutor { const allElements = this.params.state.allElements const keysToChange = this.params.keysToApply const overwrite = this.params.overwrite - const selfTags = this.params.tagsSource.data; + const selfTags = this.params.tagsSource.data const theme = this.params.state.layoutToUse.id for (const id of featuresToChange) { const tagsToApply: Tag[] = [] @@ -86,42 +83,42 @@ class MultiApplyExecutor { } const otherValue = otherFeatureTags[key] if (newValue === otherValue) { - continue;// No changes to be made + continue // No changes to be made } if (overwrite) { tagsToApply.push(new Tag(key, newValue)) - continue; + continue } - - if (otherValue === undefined || otherValue === "" || otherValue === this.originalValues.get(key)) { + if ( + otherValue === undefined || + otherValue === "" || + otherValue === this.originalValues.get(key) + ) { tagsToApply.push(new Tag(key, newValue)) } } if (tagsToApply.length == 0) { - continue; + continue } - changes.applyAction( new ChangeTagAction(id, new And(tagsToApply), otherFeatureTags, { theme, - changeType: "answer" - })) + changeType: "answer", + }) + ) } } - } export default class MultiApply extends Toggle { - constructor(params: MultiApplyParams) { const p = params const t = Translations.t.multi_apply - const featureId = p.tagsSource.data.id if (featureId === undefined) { @@ -133,24 +130,30 @@ export default class MultiApply extends Toggle { const elems: (string | BaseUIElement)[] = [] if (p.autoapply) { elems.push(new FixedUiElement(p.text).SetClass("block")) - elems.push(new VariableUiElement(p.featureIds.map(featureIds => - t.autoApply.Subs({ - attr_names: p.keysToApply.join(", "), - count: "" + featureIds.length - }))).SetClass("block subtle text-sm")) + elems.push( + new VariableUiElement( + p.featureIds.map((featureIds) => + t.autoApply.Subs({ + attr_names: p.keysToApply.join(", "), + count: "" + featureIds.length, + }) + ) + ).SetClass("block subtle text-sm") + ) } else { elems.push( - new SubtleButton(undefined, p.text).onClick(() => applicator.applyTaggingOnOtherFeatures()) + new SubtleButton(undefined, p.text).onClick(() => + applicator.applyTaggingOnOtherFeatures() + ) ) } - - const isShown: Store<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => { - return loggedIn && p.featureIds.data.length > 0 - }, [p.featureIds]) - super(new Combine(elems), undefined, isShown); - + const isShown: Store<boolean> = p.state.osmConnection.isLoggedIn.map( + (loggedIn) => { + return loggedIn && p.featureIds.data.length > 0 + }, + [p.featureIds] + ) + super(new Combine(elems), undefined, isShown) } - - -} \ No newline at end of file +} diff --git a/UI/Popup/NearbyImages.ts b/UI/Popup/NearbyImages.ts index 4436c32f6..d11217b1a 100644 --- a/UI/Popup/NearbyImages.ts +++ b/UI/Popup/NearbyImages.ts @@ -1,94 +1,90 @@ -import Combine from "../Base/Combine"; -import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; -import {SlideShow} from "../Image/SlideShow"; -import {ClickableToggle} from "../Input/Toggle"; -import Loading from "../Base/Loading"; -import {AttributedImage} from "../Image/AttributedImage"; -import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"; -import Svg from "../../Svg"; -import BaseUIElement from "../BaseUIElement"; -import {InputElement} from "../Input/InputElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Translations from "../i18n/Translations"; -import {Mapillary} from "../../Logic/ImageProviders/Mapillary"; -import {SubtleButton} from "../Base/SubtleButton"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import Lazy from "../Base/Lazy"; +import Combine from "../Base/Combine" +import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" +import { SlideShow } from "../Image/SlideShow" +import { ClickableToggle } from "../Input/Toggle" +import Loading from "../Base/Loading" +import { AttributedImage } from "../Image/AttributedImage" +import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" +import Svg from "../../Svg" +import BaseUIElement from "../BaseUIElement" +import { InputElement } from "../Input/InputElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import Translations from "../i18n/Translations" +import { Mapillary } from "../../Logic/ImageProviders/Mapillary" +import { SubtleButton } from "../Base/SubtleButton" +import { GeoOperations } from "../../Logic/GeoOperations" +import { ElementStorage } from "../../Logic/ElementStorage" +import Lazy from "../Base/Lazy" export interface P4CPicture { - pictureUrl: string, - date?: number, - coordinates: { lat: number, lng: number }, - provider: "Mapillary" | string, - author?, - license?, - detailsUrl?: string, - direction?, + pictureUrl: string + date?: number + coordinates: { lat: number; lng: number } + provider: "Mapillary" | string + author? + license? + detailsUrl?: string + direction? osmTags?: object /*To copy straight into OSM!*/ - , - thumbUrl: string, + thumbUrl: string details: { - isSpherical: boolean, + isSpherical: boolean } } - export interface NearbyImageOptions { - lon: number, - lat: number, + lon: number + lat: number // Radius of the upstream search - searchRadius?: 500 | number, - maxDaysOld?: 1095 | number, - blacklist: Store<{ url: string }[]>, - shownImagesCount?: UIEventSource<number>, - towardscenter?: UIEventSource<boolean>; + searchRadius?: 500 | number + maxDaysOld?: 1095 | number + blacklist: Store<{ url: string }[]> + shownImagesCount?: UIEventSource<number> + towardscenter?: UIEventSource<boolean> allowSpherical?: UIEventSource<boolean> // Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius shownRadius?: UIEventSource<number> } class ImagesInLoadedDataFetcher { - private allElements: ElementStorage; + private allElements: ElementStorage constructor(state: { allElements: ElementStorage }) { this.allElements = state.allElements } - public fetchAround(loc: { lon: number, lat: number, searchRadius?: number }): P4CPicture[] { + public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { const foundImages: P4CPicture[] = [] this.allElements.ContainingFeatures.forEach((feature) => { - const props = feature.properties; + const props = feature.properties const images = [] if (props.image) { images.push(props.image) } for (let i = 0; i < 10; i++) { - if (props["image:" + i]) { images.push(props["image:" + i]) } } if (images.length == 0) { - return; + return } const centerpoint = GeoOperations.centerpointCoordinates(feature) const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat]) if (loc.searchRadius !== undefined && d > loc.searchRadius) { - return; + return } for (const image of images) { foundImages.push({ pictureUrl: image, thumbUrl: image, - coordinates: {lng: centerpoint[0], lat: centerpoint[1]}, + coordinates: { lng: centerpoint[0], lat: centerpoint[1] }, provider: "OpenStreetMap", details: { - isSpherical: false - } + isSpherical: false, + }, }) } - }) const cleaned: P4CPicture[] = [] const seen = new Set<string>() @@ -104,171 +100,207 @@ class ImagesInLoadedDataFetcher { } export default class NearbyImages extends Lazy { - constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { super(() => { - const t = Translations.t.image.nearbyPictures - const shownImages = options.shownImagesCount ?? new UIEventSource(25); + const shownImages = options.shownImagesCount ?? new UIEventSource(25) const loadedPictures = NearbyImages.buildPictureFetcher(options, state) - const loadMoreButton = new Combine([new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => { - shownImages.setData(shownImages.data + 25) - })]).SetClass("flex flex-col justify-center") + const loadMoreButton = new Combine([ + new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => { + shownImages.setData(shownImages.data + 25) + }), + ]).SetClass("flex flex-col justify-center") - const imageElements = loadedPictures.map(imgs => { - if(imgs === undefined){ - return [] - } - const elements = (imgs.images ?? []).slice(0, shownImages.data).map(i => this.prepareElement(i)); - if (imgs.images !== undefined && elements.length < imgs.images.length) { - // We effectively sliced some items, so we can increase the count - elements.push(loadMoreButton) - } - return elements; - }, [shownImages]); + const imageElements = loadedPictures.map( + (imgs) => { + if (imgs === undefined) { + return [] + } + const elements = (imgs.images ?? []) + .slice(0, shownImages.data) + .map((i) => this.prepareElement(i)) + if (imgs.images !== undefined && elements.length < imgs.images.length) { + // We effectively sliced some items, so we can increase the count + elements.push(loadMoreButton) + } + return elements + }, + [shownImages] + ) - return new VariableUiElement(loadedPictures.map(loaded => { - - if (loaded?.images === undefined) { - return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass("animate-pulse") - } - const images = loaded.images - const beforeFilter = loaded?.beforeFilter - if (beforeFilter === 0) { - return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block")) - }else if(images.length === 0){ - - const removeFiltersButton = new SubtleButton(Svg.filter_disable_svg(), t.removeFilters).onClick(() => { - options.shownRadius.setData(options.searchRadius) - options.allowSpherical.setData(true) - options.towardscenter.setData(false) - }); - - return NearbyImages.NoImagesView( - t.allFiltered.SetClass("font-bold"), - removeFiltersButton - ) - } - - return new SlideShow(imageElements) - },)); + return new VariableUiElement( + loadedPictures.map((loaded) => { + if (loaded?.images === undefined) { + return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass( + "animate-pulse" + ) + } + const images = loaded.images + const beforeFilter = loaded?.beforeFilter + if (beforeFilter === 0) { + return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block")) + } else if (images.length === 0) { + const removeFiltersButton = new SubtleButton( + Svg.filter_disable_svg(), + t.removeFilters + ).onClick(() => { + options.shownRadius.setData(options.searchRadius) + options.allowSpherical.setData(true) + options.towardscenter.setData(false) + }) + return NearbyImages.NoImagesView( + t.allFiltered.SetClass("font-bold"), + removeFiltersButton + ) + } + + return new SlideShow(imageElements) + }) + ) }) } - private static NoImagesView(...elems: BaseUIElement[]){ - return new Combine(elems).SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg") - .SetStyle("height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );") + private static NoImagesView(...elems: BaseUIElement[]) { + return new Combine(elems) + .SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg") + .SetStyle( + "height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );" + ) } - private static buildPictureFetcher(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { + private static buildPictureFetcher( + options: NearbyImageOptions, + state?: { allElements: ElementStorage } + ) { const P4C = require("../../vendor/P4C.min") - const picManager = new P4C.PicturesManager({}); - const searchRadius = options.searchRadius ?? 500; - - const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : [] + const picManager = new P4C.PicturesManager({}) + const searchRadius = options.searchRadius ?? 500 + const nearbyImages = + state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : [] return Stores.FromPromise<P4CPicture[]>( - picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.searchRadius ?? 500, { - mindate: new Date().getTime() - (options.maxDaysOld ?? (3 * 365)) * 24 * 60 * 60 * 1000, - towardscenter: false - }) - ).map(images => { - if (images === undefined) { - return undefined - } - images = (images ?? []).concat(nearbyImages) - const blacklisted = options.blacklist?.data - images = images?.filter(i => !blacklisted?.some(notAllowed => Mapillary.sameUrl(i.pictureUrl, notAllowed.url))); + picManager.startPicsRetrievalAround( + new P4C.LatLng(options.lat, options.lon), + options.searchRadius ?? 500, + { + mindate: + new Date().getTime() - + (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000, + towardscenter: false, + } + ) + ).map( + (images) => { + if (images === undefined) { + return undefined + } + images = (images ?? []).concat(nearbyImages) + const blacklisted = options.blacklist?.data + images = images?.filter( + (i) => + !blacklisted?.some((notAllowed) => + Mapillary.sameUrl(i.pictureUrl, notAllowed.url) + ) + ) - const beforeFilterCount = images.length + const beforeFilterCount = images.length - if (!(options?.allowSpherical?.data)) { - images = images?.filter(i => i.details.isSpherical !== true) - } - - const shownRadius = options?.shownRadius?.data ?? searchRadius; - if (shownRadius !== searchRadius) { - images = images.filter(i => { - const d = GeoOperations.distanceBetween([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat]) - return d <= shownRadius + if (!options?.allowSpherical?.data) { + images = images?.filter((i) => i.details.isSpherical !== true) + } + + const shownRadius = options?.shownRadius?.data ?? searchRadius + if (shownRadius !== searchRadius) { + images = images.filter((i) => { + const d = GeoOperations.distanceBetween( + [i.coordinates.lng, i.coordinates.lat], + [options.lon, options.lat] + ) + return d <= shownRadius + }) + } + if (options.towardscenter?.data) { + images = images.filter((i) => { + if (i.direction === undefined || isNaN(i.direction)) { + return false + } + const bearing = GeoOperations.bearing( + [i.coordinates.lng, i.coordinates.lat], + [options.lon, options.lat] + ) + const diff = Math.abs((i.direction - bearing) % 360) + return diff < 40 + }) + } + + images?.sort((a, b) => { + const distanceA = GeoOperations.distanceBetween( + [a.coordinates.lng, a.coordinates.lat], + [options.lon, options.lat] + ) + const distanceB = GeoOperations.distanceBetween( + [b.coordinates.lng, b.coordinates.lat], + [options.lon, options.lat] + ) + return distanceA - distanceB }) - } - if (options.towardscenter?.data) { - images = images.filter(i => { - if (i.direction === undefined || isNaN(i.direction)) { - return false - } - const bearing = GeoOperations.bearing([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat]) - const diff = Math.abs((i.direction - bearing) % 360); - return diff < 40 - }) - } - - images?.sort((a, b) => { - const distanceA = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], [options.lon, options.lat]) - const distanceB = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], [options.lon, options.lat]) - return distanceA - distanceB - }) - - - return {images, beforeFilter: beforeFilterCount}; - - }, [options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]) - + return { images, beforeFilter: beforeFilterCount } + }, + [options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius] + ) } protected prepareElement(info: P4CPicture): BaseUIElement { - const provider = AllImageProviders.byName(info.provider); - return new AttributedImage({url: info.pictureUrl, provider}) + const provider = AllImageProviders.byName(info.provider) + return new AttributedImage({ url: info.pictureUrl, provider }) } private static asAttributedImage(info: P4CPicture): AttributedImage { - const provider = AllImageProviders.byName(info.provider); - return new AttributedImage({url: info.thumbUrl, provider, date: new Date(info.date)}) + const provider = AllImageProviders.byName(info.provider) + return new AttributedImage({ url: info.thumbUrl, provider, date: new Date(info.date) }) } protected asToggle(info: P4CPicture): ClickableToggle { - const imgNonSelected = NearbyImages.asAttributedImage(info); - const imageSelected = NearbyImages.asAttributedImage(info); + const imgNonSelected = NearbyImages.asAttributedImage(info) + const imageSelected = NearbyImages.asAttributedImage(info) const nonSelected = new Combine([imgNonSelected]).SetClass("relative block") - const hoveringCheckmark = - new Combine([Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12")]).SetClass("absolute left-1/2 top-1/2 w-0") - const selected = new Combine([ - imageSelected, - hoveringCheckmark, - ]).SetClass("relative block") - - return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick(); + const hoveringCheckmark = new Combine([ + Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12"), + ]).SetClass("absolute left-1/2 top-1/2 w-0") + const selected = new Combine([imageSelected, hoveringCheckmark]).SetClass("relative block") + return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick() } - } export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> { - private readonly value: UIEventSource<P4CPicture>; + private readonly value: UIEventSource<P4CPicture> - constructor(options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, state?: { allElements: ElementStorage }) { + constructor( + options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, + state?: { allElements: ElementStorage } + ) { super(options, state) - this.value = options.value ?? new UIEventSource<P4CPicture>(undefined); + this.value = options.value ?? new UIEventSource<P4CPicture>(undefined) } GetValue(): UIEventSource<P4CPicture> { - return this.value; + return this.value } IsValid(t: P4CPicture): boolean { - return false; + return false } protected prepareElement(info: P4CPicture): BaseUIElement { const toggle = super.asToggle(info) - toggle.isEnabled.addCallback(enabled => { + toggle.isEnabled.addCallback((enabled) => { if (enabled) { this.value.setData(info) } else if (this.value.data === info) { @@ -276,7 +308,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P } }) - this.value.addCallback(inf => { + this.value.addCallback((inf) => { if (inf !== info) { toggle.isEnabled.setData(false) } @@ -284,5 +316,4 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P return toggle } - } diff --git a/UI/Popup/NewNoteUi.ts b/UI/Popup/NewNoteUi.ts index 118f9e125..350e78e8a 100644 --- a/UI/Popup/NewNoteUi.ts +++ b/UI/Popup/NewNoteUi.ts @@ -1,34 +1,34 @@ -import Combine from "../Base/Combine"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Translations from "../i18n/Translations"; -import Title from "../Base/Title"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; -import Toggle from "../Input/Toggle"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import FilteredLayer from "../../Models/FilteredLayer"; +import Combine from "../Base/Combine" +import { UIEventSource } from "../../Logic/UIEventSource" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Translations from "../i18n/Translations" +import Title from "../Base/Title" +import ValidatedTextField from "../Input/ValidatedTextField" +import { SubtleButton } from "../Base/SubtleButton" +import Svg from "../../Svg" +import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" +import Toggle from "../Input/Toggle" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" +import FilteredLayer from "../../Models/FilteredLayer" export default class NewNoteUi extends Toggle { - - constructor(noteLayer: FilteredLayer, - isShown: UIEventSource<boolean>, - state: { - LastClickLocation: UIEventSource<{ lat: number, lon: number }>, - osmConnection: OsmConnection, - layoutToUse: LayoutConfig, - featurePipeline: FeaturePipeline, - selectedElement: UIEventSource<any> - }) { - - const t = Translations.t.notes; - const isCreated = new UIEventSource(false); - state.LastClickLocation.addCallbackAndRun(_ => isCreated.setData(false)) // Reset 'isCreated' on every click + constructor( + noteLayer: FilteredLayer, + isShown: UIEventSource<boolean>, + state: { + LastClickLocation: UIEventSource<{ lat: number; lon: number }> + osmConnection: OsmConnection + layoutToUse: LayoutConfig + featurePipeline: FeaturePipeline + selectedElement: UIEventSource<any> + } + ) { + const t = Translations.t.notes + const isCreated = new UIEventSource(false) + state.LastClickLocation.addCallbackAndRun((_) => isCreated.setData(false)) // Reset 'isCreated' on every click const text = ValidatedTextField.ForType("text").ConstructInputElement({ - value: LocalStorageSource.Get("note-text") + value: LocalStorageSource.Get("note-text"), }) text.SetClass("border rounded-sm border-grey-500") @@ -36,29 +36,31 @@ export default class NewNoteUi extends Toggle { postNote.onClick(async () => { let txt = text.GetValue().data if (txt === undefined || txt === "") { - return; + return } txt += "\n\n #MapComplete #" + state?.layoutToUse?.id - const loc = state.LastClickLocation.data; + const loc = state.LastClickLocation.data const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt) const feature = { - type:"Feature", - geometry:{ - type:"Point", - coordinates: [loc.lon, loc.lat] + type: "Feature", + geometry: { + type: "Point", + coordinates: [loc.lon, loc.lat], }, properties: { - id: ""+id.id, + id: "" + id.id, date_created: new Date().toISOString(), - _first_comment : txt, - comments:JSON.stringify( [{ - text: txt, - html: txt, - user: state.osmConnection?.userDetails?.data?.name, - uid: state.osmConnection?.userDetails?.data?.uid - }]), + _first_comment: txt, + comments: JSON.stringify([ + { + text: txt, + html: txt, + user: state.osmConnection?.userDetails?.data?.name, + uid: state.osmConnection?.userDetails?.data?.uid, + }, + ]), }, - }; + } state?.featurePipeline?.InjectNewPoint(feature) state.selectedElement?.setData(feature) text.GetValue().setData("") @@ -68,56 +70,53 @@ export default class NewNoteUi extends Toggle { new Title(t.createNoteTitle), t.createNoteIntro, text, - new Combine([new Toggle(undefined, t.warnAnonymous.SetClass("alert"), state?.osmConnection?.isLoggedIn), - new Toggle(postNote, + new Combine([ + new Toggle( + undefined, + t.warnAnonymous.SetClass("alert"), + state?.osmConnection?.isLoggedIn + ), + new Toggle( + postNote, t.textNeeded.SetClass("alert"), - text.GetValue().map(txt => txt?.length > 3) - ) - - ]).SetClass("flex justify-end items-center") - ]).SetClass("flex flex-col border-2 border-black rounded-xl p-4"); - + text.GetValue().map((txt) => txt?.length > 3) + ), + ]).SetClass("flex justify-end items-center"), + ]).SetClass("flex flex-col border-2 border-black rounded-xl p-4") const newNoteUi = new Toggle( - new Toggle(t.isCreated.SetClass("thanks"), - createNoteDialog, - isCreated - ), + new Toggle(t.isCreated.SetClass("thanks"), createNoteDialog, isCreated), undefined, new UIEventSource<boolean>(true) ) super( new Toggle( - new Combine( - [ - t.noteLayerHasFilters.SetClass("alert"), - new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => { - const filters = noteLayer.appliedFilters.data; - for (const key of Array.from(filters.keys())) { - filters.set(key, undefined) - } - noteLayer.appliedFilters.ping() - isShown.setData(false); - }) - ] - ).SetClass("flex flex-col"), + new Combine([ + t.noteLayerHasFilters.SetClass("alert"), + new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => { + const filters = noteLayer.appliedFilters.data + for (const key of Array.from(filters.keys())) { + filters.set(key, undefined) + } + noteLayer.appliedFilters.ping() + isShown.setData(false) + }), + ]).SetClass("flex flex-col"), newNoteUi, - noteLayer.appliedFilters.map(filters => { + noteLayer.appliedFilters.map((filters) => { console.log("Applied filters for notes are: ", filters) - return Array.from(filters.values()).some(v => v?.currentFilter !== undefined); + return Array.from(filters.values()).some((v) => v?.currentFilter !== undefined) }) ), new Combine([ t.noteLayerNotEnabled.SetClass("alert"), new SubtleButton(Svg.layers_svg(), t.noteLayerDoEnable).onClick(() => { - noteLayer.isDisplayed.setData(true); - isShown.setData(false); - }) + noteLayer.isDisplayed.setData(true) + isShown.setData(false) + }), ]).SetClass("flex flex-col"), noteLayer.isDisplayed - ); + ) } - - } diff --git a/UI/Popup/NoteCommentElement.ts b/UI/Popup/NoteCommentElement.ts index f0eb7cb32..c72244924 100644 --- a/UI/Popup/NoteCommentElement.ts +++ b/UI/Popup/NoteCommentElement.ts @@ -1,30 +1,29 @@ -import Combine from "../Base/Combine"; -import BaseUIElement from "../BaseUIElement"; -import Svg from "../../Svg"; -import Link from "../Base/Link"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; -import {Utils} from "../../Utils"; -import Img from "../Base/Img"; -import {SlideShow} from "../Image/SlideShow"; -import {Stores, UIEventSource} from "../../Logic/UIEventSource"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import Combine from "../Base/Combine" +import BaseUIElement from "../BaseUIElement" +import Svg from "../../Svg" +import Link from "../Base/Link" +import { FixedUiElement } from "../Base/FixedUiElement" +import Translations from "../i18n/Translations" +import { Utils } from "../../Utils" +import Img from "../Base/Img" +import { SlideShow } from "../Image/SlideShow" +import { Stores, UIEventSource } from "../../Logic/UIEventSource" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { VariableUiElement } from "../Base/VariableUIElement" export default class NoteCommentElement extends Combine { - - constructor(comment: { - "date": string, - "uid": number, - "user": string, - "user_url": string, - "action": "closed" | "opened" | "reopened" | "commented", - "text": string, "html": string + date: string + uid: number + user: string + user_url: string + action: "closed" | "opened" | "reopened" | "commented" + text: string + html: string }) { - const t = Translations.t.notes; + const t = Translations.t.notes - let actionIcon: BaseUIElement; + let actionIcon: BaseUIElement if (comment.action === "opened" || comment.action === "reopened") { actionIcon = Svg.note_svg() } else if (comment.action === "closed") { @@ -40,30 +39,37 @@ export default class NoteCommentElement extends Combine { user = new Link(comment.user, comment.user_url ?? "", true) } - let userinfo = Stores.FromPromise( Utils.downloadJsonCached("https://www.openstreetmap.org/api/0.6/user/"+comment.uid, 24*60*60*1000)) - let userImg = new VariableUiElement( userinfo.map(userinfo => { - const href = userinfo?.user?.img?.href; - if(href !== undefined){ - return new Img(href).SetClass("rounded-full w-8 h-8 mr-4") - } - return undefined - })) - + let userinfo = Stores.FromPromise( + Utils.downloadJsonCached( + "https://www.openstreetmap.org/api/0.6/user/" + comment.uid, + 24 * 60 * 60 * 1000 + ) + ) + let userImg = new VariableUiElement( + userinfo.map((userinfo) => { + const href = userinfo?.user?.img?.href + if (href !== undefined) { + return new Img(href).SetClass("rounded-full w-8 h-8 mr-4") + } + return undefined + }) + ) + const htmlElement = document.createElement("div") htmlElement.innerHTML = comment.html const images = Array.from(htmlElement.getElementsByTagName("a")) - .map(link => link.href) - .filter(link => { + .map((link) => link.href) + .filter((link) => { link = link.toLowerCase() - const lastDotIndex = link.lastIndexOf('.') + const lastDotIndex = link.lastIndexOf(".") const extension = link.substring(lastDotIndex + 1, link.length) return Utils.imageExtensions.has(extension) }) - let imagesEl: BaseUIElement = undefined; + let imagesEl: BaseUIElement = undefined if (images.length > 0) { - const imageEls = images.map(i => new Img(i) - .SetClass("w-full block") - .SetStyle("min-width: 50px; background: grey;")); + const imageEls = images.map((i) => + new Img(i).SetClass("w-full block").SetStyle("min-width: 50px; background: grey;") + ) imagesEl = new SlideShow(new UIEventSource<BaseUIElement[]>(imageEls)).SetClass("mb-1") } @@ -73,32 +79,36 @@ export default class NoteCommentElement extends Combine { new FixedUiElement(comment.html).SetClass("flex flex-col").SetStyle("margin: 0"), ]).SetClass("flex"), imagesEl, - new Combine([userImg, user.SetClass("mr-2"), comment.date]).SetClass("flex justify-end items-center subtle") + new Combine([userImg, user.SetClass("mr-2"), comment.date]).SetClass( + "flex justify-end items-center subtle" + ), ]) this.SetClass("flex flex-col pb-2 mb-2 border-gray-500 border-b") - } - public static addCommentTo(txt: string, tags: UIEventSource<any>, state: { osmConnection: OsmConnection }) { + public static addCommentTo( + txt: string, + tags: UIEventSource<any>, + state: { osmConnection: OsmConnection } + ) { const comments: any[] = JSON.parse(tags.data["comments"]) const username = state.osmConnection.userDetails.data.name - var urlRegex = /(https?:\/\/[^\s]+)/g; + var urlRegex = /(https?:\/\/[^\s]+)/g const html = txt.replace(urlRegex, function (url) { - return '<a href="' + url + '">' + url + '</a>'; + return '<a href="' + url + '">' + url + "</a>" }) comments.push({ - "date": new Date().toISOString(), - "uid": state.osmConnection.userDetails.data.uid, - "user": username, - "user_url": "https://www.openstreetmap.org/user/" + username, - "action": "commented", - "text": txt, - "html": html + date: new Date().toISOString(), + uid: state.osmConnection.userDetails.data.uid, + user: username, + user_url: "https://www.openstreetmap.org/user/" + username, + action: "commented", + text: txt, + html: html, }) tags.data["comments"] = JSON.stringify(comments) tags.ping() } - -} \ No newline at end of file +} diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 2f94d9204..0a5d324ef 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -1,115 +1,126 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import TagRenderingQuestion from "./TagRenderingQuestion"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import {Unit} from "../../Models/Unit"; -import Lazy from "../Base/Lazy"; - +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import TagRenderingQuestion from "./TagRenderingQuestion" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import { Unit } from "../../Models/Unit" +import Lazy from "../Base/Lazy" /** * Generates all the questions, one by one */ export default class QuestionBox extends VariableUiElement { - public readonly skippedQuestions: UIEventSource<number[]>; - public readonly restingQuestions: Store<BaseUIElement[]>; - - constructor(state, options: { - tagsSource: UIEventSource<any>, - tagRenderings: TagRenderingConfig[], units: Unit[], - showAllQuestionsAtOnce?: boolean | UIEventSource<boolean> - }) { + public readonly skippedQuestions: UIEventSource<number[]> + public readonly restingQuestions: Store<BaseUIElement[]> + constructor( + state, + options: { + tagsSource: UIEventSource<any> + tagRenderings: TagRenderingConfig[] + units: Unit[] + showAllQuestionsAtOnce?: boolean | UIEventSource<boolean> + } + ) { const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([]) const tagsSource = options.tagsSource const units = options.units options.showAllQuestionsAtOnce = options.showAllQuestionsAtOnce ?? false const tagRenderings = options.tagRenderings - .filter(tr => tr.question !== undefined) - .filter(tr => tr.question !== null) + .filter((tr) => tr.question !== undefined) + .filter((tr) => tr.question !== null) + const tagRenderingQuestions = tagRenderings.map( + (tagRendering, i) => + new Lazy( + () => + new TagRenderingQuestion(tagsSource, tagRendering, state, { + units: units, + afterSave: () => { + // We save and indicate progress by pinging and recalculating + skippedQuestions.ping() + }, + cancelButton: Translations.t.general.skip + .Clone() + .SetClass("btn btn-secondary") + .onClick(() => { + skippedQuestions.data.push(i) + skippedQuestions.ping() + }), + }) + ) + ) - const tagRenderingQuestions = tagRenderings - .map((tagRendering, i) => - new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering, state, - { - units: units, - afterSave: () => { - // We save and indicate progress by pinging and recalculating - skippedQuestions.ping(); - }, - cancelButton: Translations.t.general.skip.Clone() - .SetClass("btn btn-secondary") - .onClick(() => { - skippedQuestions.data.push(i); - skippedQuestions.ping(); - }) + const skippedQuestionsButton = Translations.t.general.skippedQuestions.onClick(() => { + skippedQuestions.setData([]) + }) + tagsSource.map( + (tags) => { + if (tags === undefined) { + return undefined + } + for (let i = 0; i < tagRenderingQuestions.length; i++) { + let tagRendering = tagRenderings[i] + + if (skippedQuestions.data.indexOf(i) >= 0) { + continue + } + if (tagRendering.IsKnown(tags)) { + continue + } + if (tagRendering.condition) { + if (!tagRendering.condition.matchesProperties(tags)) { + // Filtered away by the condition, so it is kindof known + continue + } } - ))); - - const skippedQuestionsButton = Translations.t.general.skippedQuestions - .onClick(() => { - skippedQuestions.setData([]); - }) - tagsSource.map(tags => { - if (tags === undefined) { - return undefined; - } - for (let i = 0; i < tagRenderingQuestions.length; i++) { - let tagRendering = tagRenderings[i]; - - if (skippedQuestions.data.indexOf(i) >= 0) { - continue; + // this value is NOT known - this is the question we have to show! + return i } - if (tagRendering.IsKnown(tags)) { - continue; + return undefined // The questions are depleted + }, + [skippedQuestions] + ) + + const questionsToAsk: Store<BaseUIElement[]> = tagsSource.map( + (tags) => { + if (tags === undefined) { + return [] } - if (tagRendering.condition) { - if (!tagRendering.condition.matchesProperties(tags)) { + const qs = [] + for (let i = 0; i < tagRenderingQuestions.length; i++) { + let tagRendering = tagRenderings[i] + + if (skippedQuestions.data.indexOf(i) >= 0) { + continue + } + if (tagRendering.IsKnown(tags)) { + continue + } + if (tagRendering.condition && !tagRendering.condition.matchesProperties(tags)) { // Filtered away by the condition, so it is kindof known - continue; + continue } + + // this value is NOT known - this is the question we have to show! + qs.push(tagRenderingQuestions[i]) } + return qs + }, + [skippedQuestions] + ) - // this value is NOT known - this is the question we have to show! - return i - } - return undefined; // The questions are depleted - }, [skippedQuestions]); - - const questionsToAsk: Store<BaseUIElement[]> = tagsSource.map(tags => { - if (tags === undefined) { - return []; - } - const qs = [] - for (let i = 0; i < tagRenderingQuestions.length; i++) { - let tagRendering = tagRenderings[i]; - - if (skippedQuestions.data.indexOf(i) >= 0) { - continue; - } - if (tagRendering.IsKnown(tags)) { - continue; - } - if (tagRendering.condition && - !tagRendering.condition.matchesProperties(tags)) { - // Filtered away by the condition, so it is kindof known - continue; - } - - // this value is NOT known - this is the question we have to show! - qs.push(tagRenderingQuestions[i]) - } - return qs - }, [skippedQuestions]) - - super(questionsToAsk.map(allQuestions => { + super( + questionsToAsk.map((allQuestions) => { const els: BaseUIElement[] = [] - if (options.showAllQuestionsAtOnce === true || options.showAllQuestionsAtOnce["data"]) { + if ( + options.showAllQuestionsAtOnce === true || + options.showAllQuestionsAtOnce["data"] + ) { els.push(...questionsToAsk.data) } else { els.push(allQuestions[0]) @@ -123,10 +134,7 @@ export default class QuestionBox extends VariableUiElement { }) ) - this.skippedQuestions = skippedQuestions; + this.skippedQuestions = skippedQuestions this.restingQuestions = questionsToAsk - - } - -} \ No newline at end of file +} diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index de997dd8d..bd65bbecf 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,37 +1,33 @@ -import {ImmutableStore, Store} from "../../Logic/UIEventSource"; -import Translations from "../i18n/Translations"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import Toggle from "../Input/Toggle"; -import BaseUIElement from "../BaseUIElement"; +import { ImmutableStore, Store } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import Toggle from "../Input/Toggle" +import BaseUIElement from "../BaseUIElement" export class SaveButton extends Toggle { - - constructor(value: Store<any>, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) { + constructor( + value: Store<any>, + osmConnection: OsmConnection, + textEnabled?: BaseUIElement, + textDisabled?: BaseUIElement + ) { if (value === undefined) { throw "No event source for savebutton, something is wrong" } - const pleaseLogin = Translations.t.general.loginToStart.Clone() + const pleaseLogin = Translations.t.general.loginToStart + .Clone() .SetClass("login-button-friendly") .onClick(() => osmConnection?.AttemptLogin()) + const isSaveable = value.map((v) => v !== false && (v ?? "") !== "") - const isSaveable = value.map(v => v !== false && (v ?? "") !== "") - - const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`); - const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(`btn btn-disabled`); - - const save = new Toggle( - saveEnabled, - saveDisabled, - isSaveable - ) - super( - save, - pleaseLogin, - osmConnection?.isLoggedIn ?? new ImmutableStore(false) + const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`) + const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass( + `btn btn-disabled` ) + const save = new Toggle(saveEnabled, saveDisabled, isSaveable) + super(save, pleaseLogin, osmConnection?.isLoggedIn ?? new ImmutableStore(false)) } - -} \ No newline at end of file +} diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 53c7a6255..7d0c2bb11 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -1,31 +1,35 @@ -import Toggle from "../Input/Toggle"; -import Svg from "../../Svg"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {SubtleButton} from "../Base/SubtleButton"; -import Minimap from "../Base/Minimap"; -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {LeafletMouseEvent} from "leaflet"; -import Combine from "../Base/Combine"; -import {Button} from "../Base/Button"; -import Translations from "../i18n/Translations"; -import SplitAction from "../../Logic/Osm/Actions/SplitAction"; -import Title from "../Base/Title"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {BBox} from "../../Logic/BBox"; +import Toggle from "../Input/Toggle" +import Svg from "../../Svg" +import { UIEventSource } from "../../Logic/UIEventSource" +import { SubtleButton } from "../Base/SubtleButton" +import Minimap from "../Base/Minimap" +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" +import { GeoOperations } from "../../Logic/GeoOperations" +import { LeafletMouseEvent } from "leaflet" +import Combine from "../Base/Combine" +import { Button } from "../Base/Button" +import Translations from "../i18n/Translations" +import SplitAction from "../../Logic/Osm/Actions/SplitAction" +import Title from "../Base/Title" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { BBox } from "../../Logic/BBox" import * as split_point from "../../assets/layers/split_point/split_point.json" -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {Changes} from "../../Logic/Osm/Changes"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import BaseLayer from "../../Models/BaseLayer"; -import FilteredLayer from "../../Models/FilteredLayer"; +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { Changes } from "../../Logic/Osm/Changes" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { ElementStorage } from "../../Logic/ElementStorage" +import BaseLayer from "../../Models/BaseLayer" +import FilteredLayer from "../../Models/FilteredLayer" export default class SplitRoadWizard extends Toggle { // @ts-ignore - private static splitLayerStyling = new LayerConfig(split_point, "(BUILTIN) SplitRoadWizard.ts", true) + private static splitLayerStyling = new LayerConfig( + split_point, + "(BUILTIN) SplitRoadWizard.ts", + true + ) public dialogIsOpened: UIEventSource<boolean> @@ -35,42 +39,42 @@ export default class SplitRoadWizard extends Toggle { * @param id: The id of the road to remove * @param state: the state of the application */ - constructor(id: string, state: { - filteredLayers: UIEventSource<FilteredLayer[]>; - backgroundLayer: UIEventSource<BaseLayer>; - featureSwitchIsTesting: UIEventSource<boolean>; - featureSwitchIsDebugging: UIEventSource<boolean>; - featureSwitchShowAllQuestions: UIEventSource<boolean>; - osmConnection: OsmConnection, - featureSwitchUserbadge: UIEventSource<boolean>, - changes: Changes, - layoutToUse: LayoutConfig, - allElements: ElementStorage - }) { - - const t = Translations.t.split; + constructor( + id: string, + state: { + filteredLayers: UIEventSource<FilteredLayer[]> + backgroundLayer: UIEventSource<BaseLayer> + featureSwitchIsTesting: UIEventSource<boolean> + featureSwitchIsDebugging: UIEventSource<boolean> + featureSwitchShowAllQuestions: UIEventSource<boolean> + osmConnection: OsmConnection + featureSwitchUserbadge: UIEventSource<boolean> + changes: Changes + layoutToUse: LayoutConfig + allElements: ElementStorage + } + ) { + const t = Translations.t.split // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring - const splitPoints = new UIEventSource<{ feature: any, freshness: Date }[]>([]); + const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([]) const hasBeenSplit = new UIEventSource(false) // Toggle variable between show split button and map - const splitClicked = new UIEventSource<boolean>(false); + const splitClicked = new UIEventSource<boolean>(false) // Load the road with given id on the minimap const roadElement = state.allElements.ContainingFeatures.get(id) // Minimap on which you can select the points to be splitted - const miniMap = Minimap.createMiniMap( - { - background: state.backgroundLayer, - allowMoving: true, - leafletOptions: { - minZoom: 14 - } - }); - miniMap.SetStyle("width: 100%; height: 24rem") - .SetClass("rounded-xl overflow-hidden"); + const miniMap = Minimap.createMiniMap({ + background: state.backgroundLayer, + allowMoving: true, + leafletOptions: { + minZoom: 14, + }, + }) + miniMap.SetStyle("width: 100%; height: 24rem").SetClass("rounded-xl overflow-hidden") miniMap.installBounds(BBox.get(roadElement).pad(0.25), false) @@ -82,7 +86,7 @@ export default class SplitRoadWizard extends Toggle { layers: state.filteredLayers, leafletMap: miniMap.leafletMap, zoomToFeatures: true, - state + state, }) new ShowDataLayer({ @@ -90,10 +94,9 @@ export default class SplitRoadWizard extends Toggle { leafletMap: miniMap.leafletMap, zoomToFeatures: false, layerToShow: SplitRoadWizard.splitLayerStyling, - state + state, }) - /** * Handles a click on the overleaf map. * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. @@ -101,9 +104,12 @@ export default class SplitRoadWizard extends Toggle { */ function onMapClick(coordinates) { // First, we check if there is another, already existing point nearby - const points = splitPoints.data.map((f, i) => [f.feature, i]) - .filter(p => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5) - .map(p => p[1]) + const points = splitPoints.data + .map((f, i) => [f.feature, i]) + .filter( + (p) => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5 + ) + .map((p) => p[1]) .sort((a, b) => a - b) .reverse(/*Copy/derived list, inplace reverse is fine*/) if (points.length > 0) { @@ -111,70 +117,87 @@ export default class SplitRoadWizard extends Toggle { splitPoints.data.splice(point, 1) } splitPoints.ping() - return; + return } // Get nearest point on the road - const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates); // pointOnRoad is a geojson + const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates) // pointOnRoad is a geojson // Update point properties to let it match the layer - pointOnRoad.properties["_split_point"] = "yes"; - + pointOnRoad.properties["_split_point"] = "yes" // Add it to the list of all points and notify observers - splitPoints.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer - splitPoints.ping(); // not updated using .setData, so manually ping observers + splitPoints.data.push({ feature: pointOnRoad, freshness: new Date() }) // show the point on the data layer + splitPoints.ping() // not updated using .setData, so manually ping observers } // When clicked, pass clicked location coordinates to onMapClick function - miniMap.leafletMap.addCallbackAndRunD( - (leafletMap) => leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => { + miniMap.leafletMap.addCallbackAndRunD((leafletMap) => + leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => { onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) - })) - - // Toggle between splitmap - const splitButton = new SubtleButton(Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), t.inviteToSplit.Clone().SetClass("text-lg font-bold")); - splitButton.onClick( - () => { - splitClicked.setData(true) - } + }) ) + // Toggle between splitmap + const splitButton = new SubtleButton( + Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), + t.inviteToSplit.Clone().SetClass("text-lg font-bold") + ) + splitButton.onClick(() => { + splitClicked.setData(true) + }) + // Only show the splitButton if logged in, else show login prompt - const loginBtn = t.loginToSplit.Clone() + const loginBtn = t.loginToSplit + .Clone() .onClick(() => state.osmConnection.AttemptLogin()) - .SetClass("login-button-friendly"); + .SetClass("login-button-friendly") const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn) // Save button const saveButton = new Button(t.split.Clone(), () => { hasBeenSplit.setData(true) - state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), { - theme: state?.layoutToUse?.id - })) + state.changes.applyAction( + new SplitAction( + id, + splitPoints.data.map((ff) => ff.feature.geometry.coordinates), + { + theme: state?.layoutToUse?.id, + } + ) + ) }) - saveButton.SetClass("btn btn-primary mr-3"); - const disabledSaveButton = new Button("Split", undefined); - disabledSaveButton.SetClass("btn btn-disabled mr-3"); + saveButton.SetClass("btn btn-primary mr-3") + const disabledSaveButton = new Button("Split", undefined) + disabledSaveButton.SetClass("btn btn-disabled mr-3") // Only show the save button if there are split points defined - const saveToggle = new Toggle(disabledSaveButton, saveButton, splitPoints.map((data) => data.length === 0)) + const saveToggle = new Toggle( + disabledSaveButton, + saveButton, + splitPoints.map((data) => data.length === 0) + ) - const cancelButton = Translations.t.general.cancel.Clone() // Not using Button() element to prevent full width button + const cancelButton = Translations.t.general.cancel + .Clone() // Not using Button() element to prevent full width button .SetClass("btn btn-secondary mr-3") .onClick(() => { - splitPoints.setData([]); - splitClicked.setData(false); - }); + splitPoints.setData([]) + splitClicked.setData(false) + }) - cancelButton.SetClass("btn btn-secondary block"); + cancelButton.SetClass("btn btn-secondary block") - const splitTitle = new Title(t.splitTitle); + const splitTitle = new Title(t.splitTitle) - const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle]).SetClass("flex flex-row")]); + const mapView = new Combine([ + splitTitle, + miniMap, + new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"), + ]) mapView.SetClass("question") - const confirm = new Toggle(mapView, splitToggle, splitClicked); + const confirm = new Toggle(mapView, splitToggle, splitClicked) super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) this.dialogIsOpened = splitClicked } -} \ No newline at end of file +} diff --git a/UI/Popup/TagApplyButton.ts b/UI/Popup/TagApplyButton.ts index 7ccca7ca0..8d73e893a 100644 --- a/UI/Popup/TagApplyButton.ts +++ b/UI/Popup/TagApplyButton.ts @@ -1,56 +1,58 @@ -import {AutoAction} from "./AutoApplyButton"; -import Translations from "../i18n/Translations"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {SubtleButton} from "../Base/SubtleButton"; -import Combine from "../Base/Combine"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import {And} from "../../Logic/Tags/And"; -import Toggle from "../Input/Toggle"; -import {Utils} from "../../Utils"; -import {Tag} from "../../Logic/Tags/Tag"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Changes} from "../../Logic/Osm/Changes"; +import { AutoAction } from "./AutoApplyButton" +import Translations from "../i18n/Translations" +import { VariableUiElement } from "../Base/VariableUIElement" +import BaseUIElement from "../BaseUIElement" +import { FixedUiElement } from "../Base/FixedUiElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { SubtleButton } from "../Base/SubtleButton" +import Combine from "../Base/Combine" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import { And } from "../../Logic/Tags/And" +import Toggle from "../Input/Toggle" +import { Utils } from "../../Utils" +import { Tag } from "../../Logic/Tags/Tag" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import { Changes } from "../../Logic/Osm/Changes" export default class TagApplyButton implements AutoAction { - public readonly funcName = "tag_apply"; - 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; - public readonly supportsAutoAction = true; + public readonly funcName = "tag_apply" + 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 + public readonly supportsAutoAction = true public readonly args = [ { name: "tags_to_apply", - doc: "A specification of the tags to apply" + doc: "A specification of the tags to apply", }, { name: "message", - doc: "The text to show to the contributor" + doc: "The text to show to the contributor", }, { name: "image", - doc: "An image to show to the contributor on the button" + doc: "An image to show to the contributor on the button", }, { name: "id_of_object_to_apply_this_one", defaultValue: undefined, - doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element" - } - ]; - public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"; + doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element", + }, + ] + public readonly example = + "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)" public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> { - // Check whether we need to look up a single value - if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")){ + if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) { // We seem to be dealing with a single value, fetch it - spec = tagSource.data[spec.replace("$","")] + spec = tagSource.data[spec.replace("$", "")] } - const tgsSpec = spec.split(";").map(spec => { - const kv = spec.split("=").map(s => s.trim()); + const tgsSpec = spec.split(";").map((spec) => { + const kv = spec.split("=").map((s) => s.trim()) if (kv.length != 2) { throw "Invalid key spec: multiple '=' found in " + spec } @@ -58,16 +60,15 @@ export default class TagApplyButton implements AutoAction { }) for (const spec of tgsSpec) { - if (spec[0].endsWith(':')) { + if (spec[0].endsWith(":")) { throw "A tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey" } } - return tagSource.map(tags => { - const newTags: Tag [] = [] + return tagSource.map((tags) => { + const newTags: Tag[] = [] for (const [key, value] of tgsSpec) { - if (value.indexOf('$') >= 0) { - + if (value.indexOf("$") >= 0) { let parts = value.split("$") // THe first of the split won't start with a '$', so no substitution needed let actualValue = parts[0] @@ -84,29 +85,37 @@ export default class TagApplyButton implements AutoAction { } return newTags }) - } - async applyActionOn(state: { - layoutToUse: LayoutConfig, - changes: Changes - }, tags: UIEventSource<any>, args: string[]): Promise<void> { + async applyActionOn( + state: { + layoutToUse: LayoutConfig + changes: Changes + }, + tags: UIEventSource<any>, + args: string[] + ): Promise<void> { const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const targetIdKey = args[3] const targetId = tags.data[targetIdKey] ?? tags.data.id - const changeAction = new ChangeTagAction(targetId, + const changeAction = new ChangeTagAction( + targetId, new And(tagsToApply.data), tags.data, // We pass in the tags of the selected element, not the tags of the target element! { theme: state.layoutToUse.id, - changeType: "answer" + changeType: "answer", } ) await state.changes.applyAction(changeAction) } - public constr(state: FeaturePipelineState, tags: UIEventSource<any>, args: string[]): BaseUIElement { + public constr( + state: FeaturePipelineState, + tags: UIEventSource<any>, + args: string[] + ): BaseUIElement { const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const msg = args[1] let image = args[2]?.trim() @@ -116,32 +125,31 @@ export default class TagApplyButton implements AutoAction { const targetIdKey = args[3] const t = Translations.t.general.apply_button - const tagsExplanation = new VariableUiElement(tagsToApply.map(tagsToApply => { - const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&"); + const tagsExplanation = new VariableUiElement( + tagsToApply.map((tagsToApply) => { + const tagsStr = tagsToApply.map((t) => t.asHumanString(false, true)).join("&") let el: BaseUIElement = new FixedUiElement(tagsStr) if (targetIdKey !== undefined) { const targetId = tags.data[targetIdKey] ?? tags.data.id - el = t.appliedOnAnotherObject.Subs({tags: tagsStr, id: targetId}) + el = t.appliedOnAnotherObject.Subs({ tags: tagsStr, id: targetId }) } - return el; - } - )).SetClass("subtle") + return el + }) + ).SetClass("subtle") const self = this const applied = new UIEventSource(false) - const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col")) - .onClick(() => { - self.applyActionOn(state, tags, args) - applied.setData(true) - }) - + const applyButton = new SubtleButton( + image, + new Combine([msg, tagsExplanation]).SetClass("flex flex-col") + ).onClick(() => { + self.applyActionOn(state, tags, args) + applied.setData(true) + }) return new Toggle( - new Toggle( - t.isApplied.SetClass("thanks"), - applyButton, - applied - ), - undefined, state.osmConnection.isLoggedIn) + new Toggle(t.isApplied.SetClass("thanks"), applyButton, applied), + undefined, + state.osmConnection.isLoggedIn + ) } } - \ No newline at end of file diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index e5d731147..3263c8e56 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -1,61 +1,78 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {SubstitutedTranslation} from "../SubstitutedTranslation"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import Combine from "../Base/Combine"; -import Img from "../Base/Img"; +import { UIEventSource } from "../../Logic/UIEventSource" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import { VariableUiElement } from "../Base/VariableUIElement" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" +import Combine from "../Base/Combine" +import Img from "../Base/Img" /*** * Displays the correct value for a known tagrendering */ export default class TagRenderingAnswer extends VariableUiElement { - - constructor(tagsSource: UIEventSource<any>, configuration: TagRenderingConfig, - state: any, - contentClasses: string = "", contentStyle: string = "", options?: { + constructor( + tagsSource: UIEventSource<any>, + configuration: TagRenderingConfig, + state: any, + contentClasses: string = "", + contentStyle: string = "", + options?: { specialViz: Map<string, BaseUIElement> - }) { + } + ) { if (configuration === undefined) { throw "Trying to generate a tagRenderingAnswer without configuration..." } if (tagsSource === undefined) { throw "Trying to generate a tagRenderingAnswer without tagSource..." } - super(tagsSource.map(tags => { - if (tags === undefined) { - return undefined; - } + super( + tagsSource + .map((tags) => { + if (tags === undefined) { + return undefined + } - if (configuration.condition) { - if (!configuration.condition.matchesProperties(tags)) { - return undefined; - } - } + if (configuration.condition) { + if (!configuration.condition.matchesProperties(tags)) { + return undefined + } + } - const trs = Utils.NoNull(configuration.GetRenderValues(tags)); - if (trs.length === 0) { - return undefined; - } + const trs = Utils.NoNull(configuration.GetRenderValues(tags)) + if (trs.length === 0) { + return undefined + } - const valuesToRender: BaseUIElement[] = trs.map(tr => { - const text = new SubstitutedTranslation(tr.then, tagsSource, state, options?.specialViz); - if(tr.icon === undefined){ - return text - } - return new Combine([new Img(tr.icon).SetClass("mapping-icon-"+(tr.iconClass ?? "small")), text]).SetClass("flex items-center") - }) - if (valuesToRender.length === 1) { - return valuesToRender[0]; - } else if (valuesToRender.length > 1) { - return new Combine(valuesToRender).SetClass("flex flex-col") - } - return undefined; - }).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) + const valuesToRender: BaseUIElement[] = trs.map((tr) => { + const text = new SubstitutedTranslation( + tr.then, + tagsSource, + state, + options?.specialViz + ) + if (tr.icon === undefined) { + return text + } + return new Combine([ + new Img(tr.icon).SetClass("mapping-icon-" + (tr.iconClass ?? "small")), + text, + ]).SetClass("flex items-center") + }) + if (valuesToRender.length === 1) { + return valuesToRender[0] + } else if (valuesToRender.length > 1) { + return new Combine(valuesToRender).SetClass("flex flex-col") + } + return undefined + }) + .map((element: BaseUIElement) => + element?.SetClass(contentClasses)?.SetStyle(contentStyle) + ) + ) this.SetClass("flex items-center flex-row text-lg link-underline") - this.SetStyle("word-wrap: anywhere;"); + this.SetStyle("word-wrap: anywhere;") } - -} \ No newline at end of file +} diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index a8f13bb88..df7a2ec17 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -1,59 +1,58 @@ -import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import { InputElement, ReadonlyInputElement } from "../Input/InputElement"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import { FixedInputElement } from "../Input/FixedInputElement"; -import { RadioButton } from "../Input/RadioButton"; -import { Utils } from "../../Utils"; -import CheckBoxes from "../Input/Checkboxes"; -import InputElementMap from "../Input/InputElementMap"; -import { SaveButton } from "./SaveButton"; -import { VariableUiElement } from "../Base/VariableUIElement"; -import Translations from "../i18n/Translations"; -import { FixedUiElement } from "../Base/FixedUiElement"; -import { Translation } from "../i18n/Translation"; -import Constants from "../../Models/Constants"; -import { SubstitutedTranslation } from "../SubstitutedTranslation"; -import { TagsFilter } from "../../Logic/Tags/TagsFilter"; -import { Tag } from "../../Logic/Tags/Tag"; -import { And } from "../../Logic/Tags/And"; -import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"; -import BaseUIElement from "../BaseUIElement"; -import { DropDown } from "../Input/DropDown"; -import InputElementWrapper from "../Input/InputElementWrapper"; -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; -import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"; -import { Unit } from "../../Models/Unit"; -import VariableInputElement from "../Input/VariableInputElement"; -import Toggle from "../Input/Toggle"; -import Img from "../Base/Img"; -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; -import Title from "../Base/Title"; -import { OsmConnection } from "../../Logic/Osm/OsmConnection"; -import { GeoOperations } from "../../Logic/GeoOperations"; -import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"; -import { OsmTags } from "../../Models/OsmFeature"; +import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" +import Combine from "../Base/Combine" +import { InputElement, ReadonlyInputElement } from "../Input/InputElement" +import ValidatedTextField from "../Input/ValidatedTextField" +import { FixedInputElement } from "../Input/FixedInputElement" +import { RadioButton } from "../Input/RadioButton" +import { Utils } from "../../Utils" +import CheckBoxes from "../Input/Checkboxes" +import InputElementMap from "../Input/InputElementMap" +import { SaveButton } from "./SaveButton" +import { VariableUiElement } from "../Base/VariableUIElement" +import Translations from "../i18n/Translations" +import { FixedUiElement } from "../Base/FixedUiElement" +import { Translation } from "../i18n/Translation" +import Constants from "../../Models/Constants" +import { SubstitutedTranslation } from "../SubstitutedTranslation" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import { Tag } from "../../Logic/Tags/Tag" +import { And } from "../../Logic/Tags/And" +import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" +import BaseUIElement from "../BaseUIElement" +import { DropDown } from "../Input/DropDown" +import InputElementWrapper from "../Input/InputElementWrapper" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig" +import { Unit } from "../../Models/Unit" +import VariableInputElement from "../Input/VariableInputElement" +import Toggle from "../Input/Toggle" +import Img from "../Base/Img" +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" +import Title from "../Base/Title" +import { OsmConnection } from "../../Logic/Osm/OsmConnection" +import { GeoOperations } from "../../Logic/GeoOperations" +import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector" +import { OsmTags } from "../../Models/OsmFeature" /** * Shows the question element. * Note that the value _migh_ already be known, e.g. when selected or when changing the value */ export default class TagRenderingQuestion extends Combine { - - constructor(tags: UIEventSource<Record<string, string> & { id: string }>, + constructor( + tags: UIEventSource<Record<string, string> & { id: string }>, configuration: TagRenderingConfig, state?: FeaturePipelineState, options?: { - units?: Unit[], - afterSave?: () => void, - cancelButton?: BaseUIElement, - saveButtonConstr?: (src: Store<TagsFilter>) => BaseUIElement, + units?: Unit[] + afterSave?: () => void + cancelButton?: BaseUIElement + saveButtonConstr?: (src: Store<TagsFilter>) => BaseUIElement bottomText?: (src: Store<TagsFilter>) => BaseUIElement } ) { - - const applicableMappingsSrc = - Stores.ListStabilized(tags.map(tags => { + const applicableMappingsSrc = Stores.ListStabilized( + tags.map((tags) => { const applicableMappings: Mapping[] = [] for (const mapping of configuration.mappings ?? []) { if (mapping.hideInAnswer === true) { @@ -63,85 +62,107 @@ export default class TagRenderingQuestion extends Combine { applicableMappings.push(mapping) continue } - const condition = <TagsFilter>mapping.hideInAnswer; + const condition = <TagsFilter>mapping.hideInAnswer const isShown = !condition.matchesProperties(tags) if (isShown) { applicableMappings.push(mapping) } } return applicableMappings - })); + }) + ) if (configuration === undefined) { throw "A question is needed for a question visualization" } options = options ?? {} - const applicableUnit = (options.units ?? []).filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0]; - const question = new Title(new SubstitutedTranslation(configuration.question, tags, state) - .SetClass("question-text"), 3); - + const applicableUnit = (options.units ?? []).filter((unit) => + unit.isApplicableToKey(configuration.freeform?.key) + )[0] + const question = new Title( + new SubstitutedTranslation(configuration.question, tags, state).SetClass( + "question-text" + ), + 3 + ) const feedback = new UIEventSource<Translation>(undefined) - const inputElement: ReadonlyInputElement<UploadableTag> = - new VariableInputElement(applicableMappingsSrc.map(applicableMappings => { - return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) - } - )) + const inputElement: ReadonlyInputElement<UploadableTag> = new VariableInputElement( + applicableMappingsSrc.map((applicableMappings) => { + return TagRenderingQuestion.GenerateInputElement( + state, + configuration, + applicableMappings, + applicableUnit, + tags, + feedback + ) + }) + ) const save = () => { - - - const selection = TagUtils.FlattenMultiAnswer(TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)); + const selection = TagUtils.FlattenMultiAnswer( + TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data) + ) if (selection) { - (state?.changes) - .applyAction(new ChangeTagAction( - tags.data.id, selection, tags.data, { - theme: state?.layoutToUse?.id ?? "unkown", - changeType: "answer", - } - )).then(_ => { + ;(state?.changes) + .applyAction( + new ChangeTagAction(tags.data.id, selection, tags.data, { + theme: state?.layoutToUse?.id ?? "unkown", + changeType: "answer", + }) + ) + .then((_) => { console.log("Tagchanges applied") }) if (options.afterSave) { - options.afterSave(); + options.afterSave() } } } if (options.saveButtonConstr === undefined) { - options.saveButtonConstr = v => new SaveButton(v, - state?.osmConnection) - .onClick(save) + options.saveButtonConstr = (v) => new SaveButton(v, state?.osmConnection).onClick(save) } - const saveButton = new Combine([ - options.saveButtonConstr(inputElement.GetValue()), - ]) + const saveButton = new Combine([options.saveButtonConstr(inputElement.GetValue())]) - let bottomTags: BaseUIElement; + let bottomTags: BaseUIElement if (options.bottomText !== undefined) { bottomTags = options.bottomText(inputElement.GetValue()) } else { - bottomTags = TagRenderingQuestion.CreateTagExplanation(inputElement.GetValue(), tags, state) + bottomTags = TagRenderingQuestion.CreateTagExplanation( + inputElement.GetValue(), + tags, + state + ) } super([ question, inputElement, new Combine([ - new VariableUiElement(feedback.map(t => t?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem")?.SetClass("alert flex") ?? bottomTags)), - new Combine([ - new Combine([options.cancelButton]), - saveButton]).SetClass("flex justify-end flex-wrap-reverse") - + new VariableUiElement( + feedback.map( + (t) => + t + ?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem") + ?.SetClass("alert flex") ?? bottomTags + ) + ), + new Combine([new Combine([options.cancelButton]), saveButton]).SetClass( + "flex justify-end flex-wrap-reverse" + ), ]).SetClass("flex mt-2 justify-between"), - new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state?.featureSwitchIsTesting) + new Toggle( + Translations.t.general.testing.SetClass("alert"), + undefined, + state?.featureSwitchIsTesting + ), ]) - this.SetClass("question disable-links") } - private static GenerateInputElement( state: FeaturePipelineState, configuration: TagRenderingConfig, @@ -150,23 +171,33 @@ export default class TagRenderingQuestion extends Combine { tagsSource: UIEventSource<any>, feedback: UIEventSource<Translation> ): ReadonlyInputElement<UploadableTag> { + const hasImages = applicableMappings.findIndex((mapping) => mapping.icon !== undefined) >= 0 + let inputEls: InputElement<UploadableTag>[] + const ifNotsPresent = applicableMappings.some((mapping) => mapping.ifnot !== undefined) - const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0 - let inputEls: InputElement<UploadableTag>[]; - - const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined) - - if (applicableMappings.length > 8 && - (configuration.freeform?.type === undefined || configuration.freeform?.type === "string") && - (!configuration.multiAnswer || configuration.freeform === undefined)) { - - return TagRenderingQuestion.GenerateSearchableSelector(state, configuration, applicableMappings, tagsSource) + if ( + applicableMappings.length > 8 && + (configuration.freeform?.type === undefined || + configuration.freeform?.type === "string") && + (!configuration.multiAnswer || configuration.freeform === undefined) + ) { + return TagRenderingQuestion.GenerateSearchableSelector( + state, + configuration, + applicableMappings, + tagsSource + ) } - // FreeForm input will be undefined if not present; will already contain a special input element if applicable - const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); + const ff = TagRenderingQuestion.GenerateFreeform( + state, + configuration, + applicableUnit, + tagsSource, + feedback + ) function allIfNotsExcept(excludeIndex: number): UploadableTag[] { if (configuration.mappings === undefined || configuration.mappings.length === 0) { @@ -183,75 +214,109 @@ export default class TagRenderingQuestion extends Combine { const negativeMappings = [] for (let i = 0; i < applicableMappings.length; i++) { - const mapping = applicableMappings[i]; + const mapping = applicableMappings[i] if (i === excludeIndex || mapping.ifnot === undefined) { continue } negativeMappings.push(mapping.ifnot) } return Utils.NoNull(negativeMappings) - } - - if (applicableMappings.length < 8 || configuration.multiAnswer || (hasImages && applicableMappings.length < 16) || ifNotsPresent) { - inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(state, tagsSource, mapping, allIfNotsExcept(i))); - inputEls = Utils.NoNull(inputEls); + if ( + applicableMappings.length < 8 || + configuration.multiAnswer || + (hasImages && applicableMappings.length < 16) || + ifNotsPresent + ) { + inputEls = (applicableMappings ?? []).map((mapping, i) => + TagRenderingQuestion.GenerateMappingElement( + state, + tagsSource, + mapping, + allIfNotsExcept(i) + ) + ) + inputEls = Utils.NoNull(inputEls) } else { - const dropdown: InputElement<UploadableTag> = new DropDown("", + const dropdown: InputElement<UploadableTag> = new DropDown( + "", applicableMappings.map((mapping, i) => { return { value: new And([mapping.if, ...allIfNotsExcept(i)]), - shown: mapping.then.Subs(tagsSource.data) + shown: mapping.then.Subs(tagsSource.data), } }) ) if (ff == undefined) { - return dropdown; + return dropdown } else { inputEls = [dropdown] } } - if (inputEls.length == 0) { if (ff === undefined) { throw "Error: could not generate a question: freeform and all mappings are undefined" } - return ff; + return ff } if (ff) { - inputEls.push(ff); + inputEls.push(ff) } if (configuration.multiAnswer) { - return TagRenderingQuestion.GenerateMultiAnswer(configuration, inputEls, ff, applicableMappings.map(mp => mp.ifnot)) + return TagRenderingQuestion.GenerateMultiAnswer( + configuration, + inputEls, + ff, + applicableMappings.map((mp) => mp.ifnot) + ) } else { return new RadioButton(inputEls, { selectFirstAsDefault: false }) } - } - private static MappingToPillValue(applicableMappings: Mapping[], tagsSource: UIEventSource<OsmTags>, state: FeaturePipelineState): { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] { - const values: { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] = [] - const addIcons = applicableMappings.some(m => m.icon !== undefined) + private static MappingToPillValue( + applicableMappings: Mapping[], + tagsSource: UIEventSource<OsmTags>, + state: FeaturePipelineState + ): { + show: BaseUIElement + value: number + mainTerm: Record<string, string> + searchTerms?: Record<string, string[]> + original: Mapping + }[] { + const values: { + show: BaseUIElement + value: number + mainTerm: Record<string, string> + searchTerms?: Record<string, string[]> + original: Mapping + }[] = [] + const addIcons = applicableMappings.some((m) => m.icon !== undefined) for (let i = 0; i < applicableMappings.length; i++) { - const mapping = applicableMappings[i]; + const mapping = applicableMappings[i] const tr = mapping.then.Subs(tagsSource.data) const patchedMapping = <Mapping>{ ...mapping, iconClass: `small-height`, - icon: mapping.icon ?? (addIcons ? "./assets/svg/none.svg" : undefined) + icon: mapping.icon ?? (addIcons ? "./assets/svg/none.svg" : undefined), } - const fancy = TagRenderingQuestion.GenerateMappingContent(patchedMapping, tagsSource, state).SetClass("normal-background") + const fancy = TagRenderingQuestion.GenerateMappingContent( + patchedMapping, + tagsSource, + state + ).SetClass("normal-background") values.push({ show: fancy, value: i, mainTerm: tr.translations, searchTerms: mapping.searchTerms, - original: mapping + original: mapping, }) } return values @@ -327,41 +392,57 @@ export default class TagRenderingQuestion extends Combine { tagsSource: UIEventSource<OsmTags>, options?: { search: UIEventSource<string> - }): InputElement<UploadableTag> { + } + ): InputElement<UploadableTag> { + const values = TagRenderingQuestion.MappingToPillValue( + applicableMappings, + tagsSource, + state + ) - - const values = TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state) - - const searchValue: UIEventSource<string> = options?.search ?? new UIEventSource<string>(undefined) + const searchValue: UIEventSource<string> = + options?.search ?? new UIEventSource<string>(undefined) const ff = configuration.freeform let onEmpty: BaseUIElement = undefined if (ff !== undefined) { - onEmpty = new VariableUiElement(searchValue.map(search => configuration.render.Subs({ [ff.key]: search }))) + onEmpty = new VariableUiElement( + searchValue.map((search) => configuration.render.Subs({ [ff.key]: search })) + ) } - const mode = configuration.multiAnswer ? "select-many" : "select-one"; + const mode = configuration.multiAnswer ? "select-many" : "select-one" - const tooMuchElementsValue = new UIEventSource<number[]>([]); + const tooMuchElementsValue = new UIEventSource<number[]>([]) - - let priorityPresets: BaseUIElement = undefined; + let priorityPresets: BaseUIElement = undefined const classes = "h-64 overflow-scroll" - if (applicableMappings.some(m => m.priorityIf !== undefined)) { - const priorityValues = tagsSource.map(tags => - TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state) - .filter(v => v.original.priorityIf?.matchesProperties(tags))) - priorityPresets = new VariableUiElement(priorityValues.map(priority => { - if (priority.length === 0) { - return Translations.t.general.useSearch; - } - return new Combine([ - Translations.t.general.useSearchForMore.Subs({ total: applicableMappings.length }), - new SearchablePillsSelector(priority, { - selectedElements: tooMuchElementsValue, - hideSearchBar: true, - mode - })]).SetClass("flex flex-col items-center ").SetClass(classes); - })); + if (applicableMappings.some((m) => m.priorityIf !== undefined)) { + const priorityValues = tagsSource.map((tags) => + TagRenderingQuestion.MappingToPillValue( + applicableMappings, + tagsSource, + state + ).filter((v) => v.original.priorityIf?.matchesProperties(tags)) + ) + priorityPresets = new VariableUiElement( + priorityValues.map((priority) => { + if (priority.length === 0) { + return Translations.t.general.useSearch + } + return new Combine([ + Translations.t.general.useSearchForMore.Subs({ + total: applicableMappings.length, + }), + new SearchablePillsSelector(priority, { + selectedElements: tooMuchElementsValue, + hideSearchBar: true, + mode, + }), + ]) + .SetClass("flex flex-col items-center ") + .SetClass(classes) + }) + ) } const presetSearch = new SearchablePillsSelector<number>(values, { selectIfSingle: true, @@ -370,53 +451,60 @@ export default class TagRenderingQuestion extends Combine { onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"), searchAreaClass: classes, onManyElementsValue: tooMuchElementsValue, - onManyElements: priorityPresets + onManyElements: priorityPresets, }) - const fallbackTag = searchValue.map(s => { + const fallbackTag = searchValue.map((s) => { if (s === undefined || ff?.key === undefined) { return undefined } return new Tag(ff.key, s) - }); - return new InputElementMap<number[], And>(presetSearch, + }) + return new InputElementMap<number[], And>( + presetSearch, (x0, x1) => { if (x0 == x1) { - return true; + return true } if (x0 === undefined || x1 === undefined) { - return false; + return false } if (x0.and.length !== x1.and.length) { - return false; + return false } for (let i = 0; i < x0.and.length; i++) { if (x1.and[i] != x0.and[i]) { return false } } - return true; + return true }, (selected) => { - if (ff !== undefined && searchValue.data?.length > 0 && !presetSearch.someMatchFound.data) { - const t = fallbackTag.data; + if ( + ff !== undefined && + searchValue.data?.length > 0 && + !presetSearch.someMatchFound.data + ) { + const t = fallbackTag.data if (ff.addExtraTags) { return new And([t, ...ff.addExtraTags]) } - return new And([t]); + return new And([t]) } if (selected === undefined || selected.length == 0) { - return undefined; + return undefined } - const tfs = Utils.NoNull(applicableMappings.map((mapping, i) => { - if (selected.indexOf(i) >= 0) { - return mapping.if - } else { - return mapping.ifnot - } - })) - return new And(tfs); + const tfs = Utils.NoNull( + applicableMappings.map((mapping, i) => { + if (selected.indexOf(i) >= 0) { + return mapping.if + } else { + return mapping.ifnot + } + }) + ) + return new And(tfs) }, (tf) => { if (tf === undefined) { @@ -425,20 +513,23 @@ export default class TagRenderingQuestion extends Combine { const selected: number[] = [] for (let i = 0; i < applicableMappings.length; i++) { const mapping = applicableMappings[i] - if (tf.and.some(t => mapping.if == t)) { + if (tf.and.some((t) => mapping.if == t)) { selected.push(i) } } - return selected; + return selected }, [searchValue, presetSearch.someMatchFound] - ); + ) } private static GenerateMultiAnswer( configuration: TagRenderingConfig, - elements: InputElement<UploadableTag>[], freeformField: InputElement<UploadableTag>, ifNotSelected: UploadableTag[]): InputElement<UploadableTag> { - const checkBoxes = new CheckBoxes(elements); + elements: InputElement<UploadableTag>[], + freeformField: InputElement<UploadableTag>, + ifNotSelected: UploadableTag[] + ): InputElement<UploadableTag> { + const checkBoxes = new CheckBoxes(elements) const inputEl = new InputElementMap<number[], UploadableTag>( checkBoxes, @@ -447,27 +538,27 @@ export default class TagRenderingQuestion extends Combine { }, (indices) => { if (indices.length === 0) { - return undefined; + return undefined } - const tags: UploadableTag[] = indices.map(i => elements[i].GetValue().data); - const oppositeTags: UploadableTag[] = []; + const tags: UploadableTag[] = indices.map((i) => elements[i].GetValue().data) + const oppositeTags: UploadableTag[] = [] for (let i = 0; i < ifNotSelected.length; i++) { if (indices.indexOf(i) >= 0) { - continue; + continue } - const notSelected = ifNotSelected[i]; + const notSelected = ifNotSelected[i] if (notSelected === undefined) { - continue; + continue } - oppositeTags.push(notSelected); + oppositeTags.push(notSelected) } - tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); - return TagUtils.FlattenMultiAnswer(tags); + tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)) + return TagUtils.FlattenMultiAnswer(tags) }, (tags: UploadableTag) => { // {key --> values[]} - const presentTags = TagUtils.SplitKeys([tags]); + const presentTags = TagUtils.SplitKeys([tags]) const indices: number[] = [] // We also collect the values that have to be added to the freeform field let freeformExtras: string[] = [] @@ -476,67 +567,67 @@ export default class TagRenderingQuestion extends Combine { } for (let j = 0; j < elements.length; j++) { - const inputElement = elements[j]; + const inputElement = elements[j] if (inputElement === freeformField) { - continue; + continue } - const val = inputElement.GetValue(); - const neededTags = TagUtils.SplitKeys([val.data]); + const val = inputElement.GetValue() + const neededTags = TagUtils.SplitKeys([val.data]) // if every 'neededKeys'-value is present in presentKeys, we have a match and enable the index if (TagUtils.AllKeysAreContained(presentTags, neededTags)) { - indices.push(j); + indices.push(j) if (freeformExtras.length > 0) { - const freeformsToRemove: string[] = (neededTags[configuration.freeform.key] ?? []); + const freeformsToRemove: string[] = + neededTags[configuration.freeform.key] ?? [] for (const toRm of freeformsToRemove) { - const i = freeformExtras.indexOf(toRm); + const i = freeformExtras.indexOf(toRm) if (i >= 0) { - freeformExtras.splice(i, 1); + freeformExtras.splice(i, 1) } } } } - } if (freeformField) { if (freeformExtras.length > 0) { - freeformField.GetValue().setData(new Tag(configuration.freeform.key, freeformExtras.join(";"))); + freeformField + .GetValue() + .setData(new Tag(configuration.freeform.key, freeformExtras.join(";"))) indices.push(elements.indexOf(freeformField)) } else { - freeformField.GetValue().setData(undefined); + freeformField.GetValue().setData(undefined) } } - - return indices; + return indices }, - elements.map(el => el.GetValue()) - ); + elements.map((el) => el.GetValue()) + ) - freeformField?.GetValue()?.addCallbackAndRun(value => { + freeformField?.GetValue()?.addCallbackAndRun((value) => { // The list of indices of the selected elements - const es = checkBoxes.GetValue(); - const i = elements.length - 1; + const es = checkBoxes.GetValue() + const i = elements.length - 1 // The actual index of the freeform-element - const index = es.data.indexOf(i); + const index = es.data.indexOf(i) if (value === undefined) { // No data is set in the freeform text field; so we delete the checkmark if it is selected if (index >= 0) { - es.data.splice(index, 1); - es.ping(); + es.data.splice(index, 1) + es.ping() } } else if (index < 0) { // There is data defined in the checkmark, but the checkmark isn't checked, so we check it // This is of course because the data changed - es.data.push(i); - es.ping(); + es.data.push(i) + es.ping() } - }); + }) - return inputEl; + return inputEl } - /** * Generates a (Fixed) input element for this mapping. * Note that the mapping might hide itself if the condition is not met anymore. @@ -546,9 +637,10 @@ export default class TagRenderingQuestion extends Combine { private static GenerateMappingElement( state, tagsSource: UIEventSource<any>, - mapping: Mapping, ifNot?: UploadableTag[]): InputElement<UploadableTag> { - - let tagging: UploadableTag = mapping.if; + mapping: Mapping, + ifNot?: UploadableTag[] + ): InputElement<UploadableTag> { + let tagging: UploadableTag = mapping.if if (ifNot !== undefined) { tagging = new And([mapping.if, ...ifNot]) } @@ -556,69 +648,78 @@ export default class TagRenderingQuestion extends Combine { tagging = new And([tagging, ...mapping.addExtraTags]) } - return new FixedInputElement( TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), tagging, - (t0, t1) => t1.shadows(t0)); + (t0, t1) => t1.shadows(t0) + ) } - private static GenerateMappingContent(mapping: Mapping, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement { + private static GenerateMappingContent( + mapping: Mapping, + tagsSource: UIEventSource<any>, + state: FeaturePipelineState + ): BaseUIElement { const text = new SubstitutedTranslation(mapping.then, tagsSource, state) if (mapping.icon === undefined) { - return text; + return text } - return new Combine([new Img(mapping.icon).SetClass("mr-1 mapping-icon-" + (mapping.iconClass ?? "small")), text]).SetClass("flex items-center") + return new Combine([ + new Img(mapping.icon).SetClass("mr-1 mapping-icon-" + (mapping.iconClass ?? "small")), + text, + ]).SetClass("flex items-center") } - private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>, feedback: UIEventSource<Translation>) - : InputElement<UploadableTag> { - const freeform = configuration.freeform; + private static GenerateFreeform( + state: FeaturePipelineState, + configuration: TagRenderingConfig, + applicableUnit: Unit, + tags: UIEventSource<any>, + feedback: UIEventSource<Translation> + ): InputElement<UploadableTag> { + const freeform = configuration.freeform if (freeform === undefined) { - return undefined; + return undefined } - const pickString = - (string: any) => { - if (string === "" || string === undefined) { - return undefined; - } - if (string.length >= 255) { - return undefined - } + const pickString = (string: any) => { + if (string === "" || string === undefined) { + return undefined + } + if (string.length >= 255) { + return undefined + } - const tag = new Tag(freeform.key, string); + const tag = new Tag(freeform.key, string) - if (freeform.addExtraTags === undefined) { - return tag; - } - return new And([ - tag, - ...freeform.addExtraTags - ] - ); - }; + if (freeform.addExtraTags === undefined) { + return tag + } + return new And([tag, ...freeform.addExtraTags]) + } const toString = (tag) => { if (tag instanceof And) { for (const subtag of tag.and) { if (subtag instanceof Tag && subtag.key === freeform.key) { - return subtag.value; + return subtag.value } } - return undefined; + return undefined } else if (tag instanceof Tag) { return tag.value } - return undefined; + return undefined } - const tagsData = tags.data; + const tagsData = tags.data const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id) const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0, 0] console.log("Creating a tr-question with applicableUnit", applicableUnit) - const input: InputElement<string> = ValidatedTextField.ForType(configuration.freeform.type)?.ConstructInputElement({ + const input: InputElement<string> = ValidatedTextField.ForType( + configuration.freeform.type + )?.ConstructInputElement({ country: () => tagsData._country, location: [center[1], center[0]], mapBackgroundLayer: state?.backgroundLayer, @@ -626,11 +727,11 @@ export default class TagRenderingQuestion extends Combine { args: configuration.freeform.helperArgs, feature, placeholder: configuration.freeform.placeholder, - feedback - }); + feedback, + }) // Init with correct value - input?.GetValue().setData(tagsData[freeform.key] ?? freeform.default); + input?.GetValue().setData(tagsData[freeform.key] ?? freeform.default) // Add a length check input?.GetValue().addCallbackD((v: string | undefined) => { @@ -640,42 +741,52 @@ export default class TagRenderingQuestion extends Combine { }) let inputTagsFilter: InputElement<UploadableTag> = new InputElementMap( - input, (a, b) => a === b || (a?.shadows(b) ?? false), - pickString, toString - ); + input, + (a, b) => a === b || (a?.shadows(b) ?? false), + pickString, + toString + ) if (freeform.inline) { inputTagsFilter.SetClass("w-48-imp") - inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags, state) + inputTagsFilter = new InputElementWrapper( + inputTagsFilter, + configuration.render, + freeform.key, + tags, + state + ) inputTagsFilter.SetClass("block") } - return inputTagsFilter; - + return inputTagsFilter } - public static CreateTagExplanation(selectedValue: Store<TagsFilter>, + public static CreateTagExplanation( + selectedValue: Store<TagsFilter>, tags: Store<object>, - state?: { osmConnection?: OsmConnection }) { + state?: { osmConnection?: OsmConnection } + ) { return new VariableUiElement( selectedValue.map( (tagsFilter: TagsFilter) => { - const csCount = state?.osmConnection?.userDetails?.data?.csCount ?? Constants.userJourney.tagsVisibleAndWikiLinked + 1; + const csCount = + state?.osmConnection?.userDetails?.data?.csCount ?? + Constants.userJourney.tagsVisibleAndWikiLinked + 1 if (csCount < Constants.userJourney.tagsVisibleAt) { - return ""; + return "" } if (tagsFilter === undefined) { - return Translations.t.general.noTagsSelected.SetClass("subtle"); + return Translations.t.general.noTagsSelected.SetClass("subtle") } if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { - const tagsStr = tagsFilter.asHumanString(false, true, tags.data); - return new FixedUiElement(tagsStr).SetClass("subtle"); + const tagsStr = tagsFilter.asHumanString(false, true, tags.data) + return new FixedUiElement(tagsStr).SetClass("subtle") } - return tagsFilter.asHumanString(true, true, tags.data); + return tagsFilter.asHumanString(true, true, tags.data) }, [state?.osmConnection?.userDetails] ) ).SetClass("block break-all") } - } diff --git a/UI/ProfessionalGui.ts b/UI/ProfessionalGui.ts index fe51d06f2..de7eee3f5 100644 --- a/UI/ProfessionalGui.ts +++ b/UI/ProfessionalGui.ts @@ -1,24 +1,20 @@ -import {FixedUiElement} from "./Base/FixedUiElement"; -import Translations from "./i18n/Translations"; -import Combine from "./Base/Combine"; -import Title from "./Base/Title"; -import Toggleable, {Accordeon} from "./Base/Toggleable"; -import List from "./Base/List"; -import BaseUIElement from "./BaseUIElement"; -import LanguagePicker from "./LanguagePicker"; -import TableOfContents from "./Base/TableOfContents"; -import LeftIndex from "./Base/LeftIndex"; +import { FixedUiElement } from "./Base/FixedUiElement" +import Translations from "./i18n/Translations" +import Combine from "./Base/Combine" +import Title from "./Base/Title" +import Toggleable, { Accordeon } from "./Base/Toggleable" +import List from "./Base/List" +import BaseUIElement from "./BaseUIElement" +import LanguagePicker from "./LanguagePicker" +import TableOfContents from "./Base/TableOfContents" +import LeftIndex from "./Base/LeftIndex" class Snippet extends Toggleable { constructor(translations, ...extraContent: BaseUIElement[]) { - super( - new Title(translations.title, 3), - new SnippetContent(translations, ...extraContent) - ) + super(new Title(translations.title, 3), new SnippetContent(translations, ...extraContent)) } } - class SnippetContent extends Combine { constructor(translations: any, ...extras: BaseUIElement[]) { super([ @@ -33,26 +29,21 @@ class SnippetContent extends Combine { translations.li6, ]), translations.outro, - ...extras + ...extras, ]) this.SetClass("flex flex-col") } } class ProfessionalGui extends LeftIndex { - - constructor() { const t = Translations.t.professional - const header = new Combine([ - new FixedUiElement(`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`) - .SetClass("flex-none m-3"), - new Combine([ - new Title(t.title, 1), - t.intro - ]).SetClass("flex flex-col") + new FixedUiElement( + `<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">` + ).SetClass("flex-none m-3"), + new Combine([new Title(t.title, 1), t.intro]).SetClass("flex flex-col"), ]).SetClass("flex") const content = new Combine([ @@ -75,38 +66,37 @@ class ProfessionalGui extends LeftIndex { new Snippet(t.aboutMc.layers), new Snippet(t.aboutMc.survey), new Snippet(t.aboutMc.internalUse), - new Snippet(t.services) + new Snippet(t.services), ]), new Title(t.drawbacks.title, 2).SetClass("text-2xl"), t.drawbacks.intro, new Accordeon([ new Snippet(t.drawbacks.unsuitedData), - new Snippet(t.drawbacks.licenseNuances, + new Snippet( + t.drawbacks.licenseNuances, new Title(t.drawbacks.licenseNuances.usecaseMapDifferentSources.title, 4), new SnippetContent(t.drawbacks.licenseNuances.usecaseMapDifferentSources), new Title(t.drawbacks.licenseNuances.usecaseGatheringOpenData.title, 4), new SnippetContent(t.drawbacks.licenseNuances.usecaseGatheringOpenData) - ) + ), ]), - ]).SetClass("flex flex-col pb-12 m-3 lg:w-3/4 lg:ml-10 link-underline") - const leftContents: BaseUIElement[] = [ new TableOfContents(content, { noTopLevel: true, - maxDepth: 2 + maxDepth: 2, }).SetClass("subtle"), - new LanguagePicker(Translations.t.professional.title.SupportedLanguages(), "")?.SetClass("mt-4 self-end flex-col"), - ].map(el => el?.SetClass("pl-4")) + new LanguagePicker( + Translations.t.professional.title.SupportedLanguages(), + "" + )?.SetClass("mt-4 self-end flex-col"), + ].map((el) => el?.SetClass("pl-4")) super(leftContents, content) - } - - } new FixedUiElement("").AttachTo("decoration-desktop") -new ProfessionalGui().AttachTo("main") \ No newline at end of file +new ProfessionalGui().AttachTo("main") diff --git a/UI/QueryParameterDocumentation.ts b/UI/QueryParameterDocumentation.ts index 2ca945c76..0fa51d129 100644 --- a/UI/QueryParameterDocumentation.ts +++ b/UI/QueryParameterDocumentation.ts @@ -1,36 +1,36 @@ -import BaseUIElement from "./BaseUIElement"; -import Combine from "./Base/Combine"; -import Title from "./Base/Title"; -import List from "./Base/List"; -import Translations from "./i18n/Translations"; -import {QueryParameters} from "../Logic/Web/QueryParameters"; -import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import {DefaultGuiState} from "./DefaultGuiState"; +import BaseUIElement from "./BaseUIElement" +import Combine from "./Base/Combine" +import Title from "./Base/Title" +import List from "./Base/List" +import Translations from "./i18n/Translations" +import { QueryParameters } from "../Logic/Web/QueryParameters" +import FeatureSwitchState from "../Logic/State/FeatureSwitchState" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import { DefaultGuiState } from "./DefaultGuiState" export default class QueryParameterDocumentation { - - private static QueryParamDocsIntro = ([ + private static QueryParamDocsIntro = [ new Title("URL-parameters and URL-hash", 1), "This document gives an overview of which URL-parameters can be used to influence MapComplete.", new Title("What is a URL parameter?", 2), - "\"URL-parameters are extra parts of the URL used to set the state.", + '"URL-parameters are extra parts of the URL used to set the state.', "For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " + - "the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ", - new List([ + "the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ", + new List( + [ "The url-parameter `lat` is `51.0` in this instance", "The url-parameter `lon` is `4.3` in this instance", "The url-parameter `z` is `5` in this instance", - "The url-parameter `test` is `true` in this instance" - ].map(s => Translations.W(s)) + "The url-parameter `test` is `true` in this instance", + ].map((s) => Translations.W(s)) ), - "Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case." - ]) + "Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.", + ] public static UrlParamDocs(): Map<string, string> { const dummyLayout = new LayoutConfig({ id: ">theme<", - title: {en: "<theme>"}, + title: { en: "<theme>" }, description: "A theme to generate docs with", socialImage: "./assets/SocialImage.png", startLat: 0, @@ -42,33 +42,37 @@ export default class QueryParameterDocumentation { name: "<layer>", id: "<layer>", source: { - osmTags: "id~*" + osmTags: "id~*", }, mapRendering: null, - } - ] - + }, + ], }) - new DefaultGuiState(); // Init a featureSwitchState to init all the parameters + new DefaultGuiState() // Init a featureSwitchState to init all the parameters new FeatureSwitchState(dummyLayout) - QueryParameters.GetQueryParameter("layer-<layer-id>", "true", "Wether or not the layer with id <layer-id> is shown") + QueryParameters.GetQueryParameter( + "layer-<layer-id>", + "true", + "Wether or not the layer with id <layer-id> is shown" + ) return QueryParameters.documentation } public static GenerateQueryParameterDocs(): BaseUIElement { - - - const docs: (string | BaseUIElement)[] = [...QueryParameterDocumentation.QueryParamDocsIntro]; + const docs: (string | BaseUIElement)[] = [ + ...QueryParameterDocumentation.QueryParamDocsIntro, + ] this.UrlParamDocs().forEach((value, key) => { const c = new Combine([ new Title(key, 2), value, - QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_` - + QueryParameters.defaults[key] === undefined + ? "No default value set" + : `The default value is _${QueryParameters.defaults[key]}_`, ]) docs.push(c) }) return new Combine(docs).SetClass("flex flex-col") } -} \ No newline at end of file +} diff --git a/UI/Reviews/ReviewElement.ts b/UI/Reviews/ReviewElement.ts index 79fe40132..85f4093f2 100644 --- a/UI/Reviews/ReviewElement.ts +++ b/UI/Reviews/ReviewElement.ts @@ -1,54 +1,50 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Review} from "../../Logic/Web/Review"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import SingleReview from "./SingleReview"; -import BaseUIElement from "../BaseUIElement"; -import Img from "../Base/Img"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import Link from "../Base/Link"; +import { UIEventSource } from "../../Logic/UIEventSource" +import { Review } from "../../Logic/Web/Review" +import Combine from "../Base/Combine" +import Translations from "../i18n/Translations" +import SingleReview from "./SingleReview" +import BaseUIElement from "../BaseUIElement" +import Img from "../Base/Img" +import { VariableUiElement } from "../Base/VariableUIElement" +import Link from "../Base/Link" /** * Shows the reviews and scoring base on mangrove.reviews * The middle element is some other component shown in the middle, e.g. the review input element */ export default class ReviewElement extends VariableUiElement { - constructor(subject: string, reviews: UIEventSource<Review[]>, middleElement: BaseUIElement) { super( - reviews.map(revs => { - const elements = []; - revs.sort((a, b) => (b.date.getTime() - a.date.getTime())); // Sort with most recent first - const avg = (revs.map(review => review.rating).reduce((a, b) => a + b, 0) / revs.length); + reviews.map((revs) => { + const elements = [] + revs.sort((a, b) => b.date.getTime() - a.date.getTime()) // Sort with most recent first + const avg = + revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length elements.push( new Combine([ SingleReview.GenStars(avg), new Link( - revs.length === 1 ? Translations.t.reviews.title_singular.Clone() : - Translations.t.reviews.title - .Subs({count: "" + revs.length}), + revs.length === 1 + ? Translations.t.reviews.title_singular.Clone() + : Translations.t.reviews.title.Subs({ count: "" + revs.length }), `https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`, true ), - ]) + ]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2") + ) - .SetClass("font-2xl flex justify-between items-center pl-2 pr-2")); + elements.push(middleElement) - elements.push(middleElement); - - elements.push(...revs.map(review => new SingleReview(review))); + elements.push(...revs.map((review) => new SingleReview(review))) elements.push( new Combine([ Translations.t.reviews.attribution.Clone(), - new Img('./assets/mangrove_logo.png') - ]) + new Img("./assets/mangrove_logo.png"), + ]).SetClass("review-attribution") + ) - .SetClass("review-attribution")) - - return new Combine(elements).SetClass("block"); + return new Combine(elements).SetClass("block") }) - ); - + ) } - -} \ No newline at end of file +} diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 0eff4736a..0a7ccda2a 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -1,136 +1,134 @@ -import {InputElement} from "../Input/InputElement"; -import {Review} from "../../Logic/Web/Review"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {TextField} from "../Input/TextField"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import Svg from "../../Svg"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {SaveButton} from "../Popup/SaveButton"; -import CheckBoxes from "../Input/Checkboxes"; -import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import BaseUIElement from "../BaseUIElement"; -import Toggle from "../Input/Toggle"; +import { InputElement } from "../Input/InputElement" +import { Review } from "../../Logic/Web/Review" +import { UIEventSource } from "../../Logic/UIEventSource" +import { TextField } from "../Input/TextField" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import Svg from "../../Svg" +import { VariableUiElement } from "../Base/VariableUIElement" +import { SaveButton } from "../Popup/SaveButton" +import CheckBoxes from "../Input/Checkboxes" +import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" +import BaseUIElement from "../BaseUIElement" +import Toggle from "../Input/Toggle" export default class ReviewForm extends InputElement<Review> { + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) + private readonly _value: UIEventSource<Review> + private readonly _comment: BaseUIElement + private readonly _stars: BaseUIElement + private _saveButton: BaseUIElement + private readonly _isAffiliated: BaseUIElement + private readonly _postingAs: BaseUIElement + private readonly _osmConnection: OsmConnection - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); - private readonly _value: UIEventSource<Review>; - private readonly _comment: BaseUIElement; - private readonly _stars: BaseUIElement; - private _saveButton: BaseUIElement; - private readonly _isAffiliated: BaseUIElement; - private readonly _postingAs: BaseUIElement; - private readonly _osmConnection: OsmConnection; - - constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), osmConnection: OsmConnection) { - super(); - this._osmConnection = osmConnection; + constructor(onSave: (r: Review, doneSaving: () => void) => void, osmConnection: OsmConnection) { + super() + this._osmConnection = osmConnection this._value = new UIEventSource({ made_by_user: new UIEventSource<boolean>(true), rating: undefined, comment: undefined, author: osmConnection.userDetails.data.name, affiliated: false, - date: new Date() - }); + date: new Date(), + }) const comment = new TextField({ placeholder: Translations.t.reviews.write_a_comment.Clone(), htmlType: "area", - textAreaRows: 5 + textAreaRows: 5, }) - comment.GetValue().addCallback(comment => { - self._value.data.comment = comment; - self._value.ping(); + comment.GetValue().addCallback((comment) => { + self._value.data.comment = comment + self._value.ping() }) - const self = this; - - this._postingAs = - new Combine([Translations.t.reviews.posting_as.Clone(), - new VariableUiElement(osmConnection.userDetails.map((ud: UserDetails) => ud.name)) - .SetClass("review-author")]) - .SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;") + const self = this + this._postingAs = new Combine([ + Translations.t.reviews.posting_as.Clone(), + new VariableUiElement( + osmConnection.userDetails.map((ud: UserDetails) => ud.name) + ).SetClass("review-author"), + ]).SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;") const reviewIsSaved = new UIEventSource<boolean>(false) const reviewIsSaving = new UIEventSource<boolean>(false) - this._saveButton = - new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), - new Toggle( - Translations.t.reviews.saving_review.Clone(), - new SaveButton( - this._value.map(r => self.IsValid(r)), osmConnection - ).onClick(() => { - reviewIsSaving.setData(true); - onSave(this._value.data, () => { - reviewIsSaved.setData(true) - }); - }), - reviewIsSaving - ), - reviewIsSaved - ).SetClass("break-normal") - + this._saveButton = new Toggle( + Translations.t.reviews.saved.Clone().SetClass("thanks"), + new Toggle( + Translations.t.reviews.saving_review.Clone(), + new SaveButton( + this._value.map((r) => self.IsValid(r)), + osmConnection + ).onClick(() => { + reviewIsSaving.setData(true) + onSave(this._value.data, () => { + reviewIsSaved.setData(true) + }) + }), + reviewIsSaving + ), + reviewIsSaved + ).SetClass("break-normal") this._isAffiliated = new CheckBoxes([Translations.t.reviews.i_am_affiliated.Clone()]) - this._comment = comment; + this._comment = comment const stars = [] for (let i = 1; i <= 5; i++) { stars.push( - new VariableUiElement(this._value.map(review => { + new VariableUiElement( + this._value.map((review) => { if (review.rating === undefined) { - return Svg.star_outline.replace(/#000000/g, "#ccc"); + return Svg.star_outline.replace(/#000000/g, "#ccc") } - return review.rating < i * 20 ? - Svg.star_outline : - Svg.star - } - )) - .onClick(() => { - self._value.data.rating = i * 20; - self._value.ping(); + return review.rating < i * 20 ? Svg.star_outline : Svg.star }) + ).onClick(() => { + self._value.data.rating = i * 20 + self._value.ping() + }) ) } this._stars = new Combine(stars).SetClass("review-form-rating") } GetValue(): UIEventSource<Review> { - return this._value; + return this._value } InnerConstructElement(): HTMLElement { - const form = new Combine([ new Combine([this._stars, this._postingAs]).SetClass("flex"), this._comment, - new Combine([ - this._isAffiliated, - this._saveButton - ]).SetClass("review-form-bottom"), - Translations.t.reviews.tos.Clone().SetClass("subtle") + new Combine([this._isAffiliated, this._saveButton]).SetClass("review-form-bottom"), + Translations.t.reviews.tos.Clone().SetClass("subtle"), ]) .SetClass("flex flex-col p-4") - .SetStyle("border-radius: 1em;" + - " background-color: var(--subtle-detail-color);" + - " color: var(--subtle-detail-color-contrast);" + - " border: 2px solid var(--subtle-detail-color-contrast)") + .SetStyle( + "border-radius: 1em;" + + " background-color: var(--subtle-detail-color);" + + " color: var(--subtle-detail-color-contrast);" + + " border: 2px solid var(--subtle-detail-color-contrast)" + ) - const connection = this._osmConnection; - const login = Translations.t.reviews.plz_login.Clone().onClick(() => connection.AttemptLogin()) + const connection = this._osmConnection + const login = Translations.t.reviews.plz_login + .Clone() + .onClick(() => connection.AttemptLogin()) - return new Toggle(form, login, - connection.isLoggedIn) - .ConstructElement() + return new Toggle(form, login, connection.isLoggedIn).ConstructElement() } IsValid(r: Review): boolean { if (r === undefined) { - return false; + return false } - return (r.comment?.length ?? 0) <= 1000 && (r.author?.length ?? 0) <= 20 && r.rating >= 0 && r.rating <= 100; + return ( + (r.comment?.length ?? 0) <= 1000 && + (r.author?.length ?? 0) <= 20 && + r.rating >= 0 && + r.rating <= 100 + ) } - - -} \ No newline at end of file +} diff --git a/UI/Reviews/SingleReview.ts b/UI/Reviews/SingleReview.ts index a68efdabb..a3c996ca8 100644 --- a/UI/Reviews/SingleReview.ts +++ b/UI/Reviews/SingleReview.ts @@ -1,34 +1,30 @@ -import {Review} from "../../Logic/Web/Review"; -import Combine from "../Base/Combine"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import Img from "../Base/Img"; +import { Review } from "../../Logic/Web/Review" +import Combine from "../Base/Combine" +import { FixedUiElement } from "../Base/FixedUiElement" +import Translations from "../i18n/Translations" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import Img from "../Base/Img" export default class SingleReview extends Combine { - constructor(review: Review) { - const d = review.date; - super( - [ + const d = review.date + super([ + new Combine([SingleReview.GenStars(review.rating)]), + new FixedUiElement(review.comment), + new Combine([ new Combine([ - SingleReview.GenStars(review.rating) - ]), - new FixedUiElement(review.comment), - new Combine([ - new Combine([ - - new FixedUiElement(review.author).SetClass("font-bold"), - review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "", - ]).SetStyle("margin-right: 0.5em"), - new FixedUiElement(`${d.getFullYear()}-${Utils.TwoDigits(d.getMonth() + 1)}-${Utils.TwoDigits(d.getDate())} ${Utils.TwoDigits(d.getHours())}:${Utils.TwoDigits(d.getMinutes())}`) - .SetClass("subtle-lighter") - ]).SetClass("flex mb-4 justify-end") - - ] - ); - this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element"); + new FixedUiElement(review.author).SetClass("font-bold"), + review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "", + ]).SetStyle("margin-right: 0.5em"), + new FixedUiElement( + `${d.getFullYear()}-${Utils.TwoDigits(d.getMonth() + 1)}-${Utils.TwoDigits( + d.getDate() + )} ${Utils.TwoDigits(d.getHours())}:${Utils.TwoDigits(d.getMinutes())}` + ).SetClass("subtle-lighter"), + ]).SetClass("flex mb-4 justify-end"), + ]) + this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element") if (review.made_by_user.data) { this.SetClass("border-attention-catch") } @@ -36,16 +32,19 @@ export default class SingleReview extends Combine { public static GenStars(rating: number): BaseUIElement { if (rating === undefined) { - return Translations.t.reviews.no_rating; + return Translations.t.reviews.no_rating } if (rating < 10) { - rating = 10; + rating = 10 } - const scoreTen = Math.round(rating / 10); + const scoreTen = Math.round(rating / 10) return new Combine([ - ...Utils.TimesT(scoreTen / 2, _ => new Img('./assets/svg/star.svg').SetClass("'h-8 w-8 md:h-12")), - scoreTen % 2 == 1 ? new Img('./assets/svg/star_half.svg').SetClass('h-8 w-8 md:h-12') : undefined + ...Utils.TimesT(scoreTen / 2, (_) => + new Img("./assets/svg/star.svg").SetClass("'h-8 w-8 md:h-12") + ), + scoreTen % 2 == 1 + ? new Img("./assets/svg/star_half.svg").SetClass("h-8 w-8 md:h-12") + : undefined, ]).SetClass("flex w-max") } - -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 60de14a44..125021780 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -1,12 +1,13 @@ /** * The data layer shows all the given geojson elements with the appropriate icon etc */ -import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import { ShowDataLayerOptions } from "./ShowDataLayerOptions" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" export default class ShowDataLayer { - - public static actualContstructor : (options: ShowDataLayerOptions & { layerToShow: LayerConfig }) => void = undefined; + public static actualContstructor: ( + options: ShowDataLayerOptions & { layerToShow: LayerConfig } + ) => void = undefined /** * Creates a datalayer. @@ -15,11 +16,9 @@ export default class ShowDataLayer { * @param options */ constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { - if(ShowDataLayer.actualContstructor === undefined){ + if (ShowDataLayer.actualContstructor === undefined) { throw "Show data layer is called, but it isn't initialized yet. Call ` ShowDataLayer.actualContstructor = (options => new ShowDataLayerImplementation(options)) ` somewhere, e.g. in your init" } ShowDataLayer.actualContstructor(options) } - - -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowDataLayerImplementation.ts b/UI/ShowDataLayer/ShowDataLayerImplementation.ts index 34e45646e..a5f5e299a 100644 --- a/UI/ShowDataLayer/ShowDataLayerImplementation.ts +++ b/UI/ShowDataLayer/ShowDataLayerImplementation.ts @@ -1,9 +1,9 @@ -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { ShowDataLayerOptions } from "./ShowDataLayerOptions" +import { ElementStorage } from "../../Logic/ElementStorage" +import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" /* // import 'leaflet-polylineoffset'; We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object. @@ -18,23 +18,22 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'- * The data layer shows all the given geojson elements with the appropriate icon etc */ export default class ShowDataLayerImplementation { - private static dataLayerIds = 0 - private readonly _leafletMap: Store<L.Map>; - private readonly _enablePopups: boolean; + private readonly _leafletMap: Store<L.Map> + private readonly _enablePopups: boolean private readonly _features: RenderingMultiPlexerFeatureSource - private readonly _layerToShow: LayerConfig; + private readonly _layerToShow: LayerConfig private readonly _selectedElement: UIEventSource<any> private readonly allElements: ElementStorage // Used to generate a fresh ID when needed - private _cleanCount = 0; - private geoLayer = undefined; + private _cleanCount = 0 + private geoLayer = undefined /** * A collection of functions to call when the current geolayer is unregistered */ - private unregister: (() => void)[] = []; - private isDirty = false; + private unregister: (() => void)[] = [] + private isDirty = false /** * If the selected element triggers, this is used to lookup the correct layer and to open the popup * Used to avoid a lot of callbacks on the selected element @@ -42,9 +41,12 @@ export default class ShowDataLayerImplementation { * Note: the key of this dictionary is 'feature.properties.id+features.geometry.type' as one feature might have multiple presentations * @private */ - private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>() - private readonly showDataLayerid: number; - private readonly createPopup: (tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen + private readonly leafletLayersPerId = new Map<string, { feature: any; leafletlayer: any }>() + private readonly showDataLayerid: number + private readonly createPopup: ( + tags: UIEventSource<any>, + layer: LayerConfig + ) => ScrollableFullScreen /** * Creates a datalayer. @@ -53,38 +55,40 @@ export default class ShowDataLayerImplementation { * @param options */ constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { - this._leafletMap = options.leafletMap; - this.showDataLayerid = ShowDataLayerImplementation.dataLayerIds; + this._leafletMap = options.leafletMap + this.showDataLayerid = ShowDataLayerImplementation.dataLayerIds ShowDataLayerImplementation.dataLayerIds++ if (options.features === undefined) { console.error("Invalid ShowDataLayer invocation: options.features is undefed") throw "Invalid ShowDataLayer invocation: options.features is undefed" } - this._features = new RenderingMultiPlexerFeatureSource(options.features, options.layerToShow); - this._layerToShow = options.layerToShow; + this._features = new RenderingMultiPlexerFeatureSource( + options.features, + options.layerToShow + ) + this._layerToShow = options.layerToShow this._selectedElement = options.selectedElement - this.allElements = options.state?.allElements; - this.createPopup = undefined; - this._enablePopups = options.popup !== undefined; + this.allElements = options.state?.allElements + this.createPopup = undefined + this._enablePopups = options.popup !== undefined if (options.popup !== undefined) { this.createPopup = options.popup } - const self = this; + const self = this - options.leafletMap.addCallback(_ => { - return self.update(options) - } - ); + options.leafletMap.addCallback((_) => { + return self.update(options) + }) - this._features.features.addCallback(_ => self.update(options)); - options.doShowLayer?.addCallback(doShow => { - const mp = options.leafletMap.data; + this._features.features.addCallback((_) => self.update(options)) + options.doShowLayer?.addCallback((doShow) => { + const mp = options.leafletMap.data if (mp === null) { self.Destroy() - return true; + return true } if (mp == undefined) { - return; + return } if (doShow) { @@ -96,24 +100,21 @@ export default class ShowDataLayerImplementation { } else { if (this.geoLayer !== undefined) { mp.removeLayer(this.geoLayer) - this.unregister.forEach(f => f()) + this.unregister.forEach((f) => f()) this.unregister = [] } } - }) - - this._selectedElement?.addCallbackAndRunD(selected => { + this._selectedElement?.addCallbackAndRunD((selected) => { self.openPopupOfSelectedElement(selected) }) this.update(options) - } private Destroy() { - this.unregister.forEach(f => f()) + this.unregister.forEach((f) => f()) } private openPopupOfSelectedElement(selected) { @@ -121,28 +122,29 @@ export default class ShowDataLayerImplementation { return } if (this._leafletMap.data === undefined) { - return; + return } const v = this.leafletLayersPerId.get(selected.properties.id + selected.geometry.type) if (v === undefined) { - return; + return } const leafletLayer = v.leafletlayer const feature = v.feature if (leafletLayer.getPopup().isOpen()) { - return; + return } if (selected.properties.id !== feature.properties.id) { - return; + return } if (feature.id !== feature.properties.id) { // Probably a feature which has renamed // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too console.log("Not opening the popup for", feature, "as probably renamed") - return; + return } - if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again + if ( + selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again ) { leafletLayer.openPopup() } @@ -150,41 +152,42 @@ export default class ShowDataLayerImplementation { private update(options: ShowDataLayerOptions): boolean { if (this._features.features.data === undefined) { - return; + return } - this.isDirty = true; + this.isDirty = true if (options?.doShowLayer?.data === false) { - return; + return } - const mp = options.leafletMap.data; + const mp = options.leafletMap.data if (mp === null) { - return true; // Unregister as the map has been destroyed + return true // Unregister as the map has been destroyed } if (mp === undefined) { - return; + return } this._cleanCount++ // clean all the old stuff away, if any if (this.geoLayer !== undefined) { - mp.removeLayer(this.geoLayer); + mp.removeLayer(this.geoLayer) } - const self = this; + const self = this const data = { type: "FeatureCollection", - features: [] + features: [], } // @ts-ignore this.geoLayer = L.geoJSON(data, { - style: feature => self.createStyleFor(feature), + style: (feature) => self.createStyleFor(feature), pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), - onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer) - }); + onEachFeature: (feature, leafletLayer) => + self.postProcessFeature(feature, leafletLayer), + }) - const selfLayer = this.geoLayer; - const allFeats = this._features.features.data; + const selfLayer = this.geoLayer + const allFeats = this._features.features.data for (const feat of allFeats) { if (feat === undefined) { continue @@ -192,10 +195,16 @@ export default class ShowDataLayerImplementation { try { if (feat.geometry.type === "LineString") { const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates) - const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties); - let offsettedLine; + const tagsSource = + this.allElements?.addOrGetElement(feat) ?? + new UIEventSource<any>(feat.properties) + let offsettedLine tagsSource - .map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags)) + .map((tags) => + this._layerToShow.lineRendering[ + feat.lineRenderingIndex + ].GenerateLeafletStyle(tags) + ) .withEqualityStabilized((a, b) => { if (a === b) { return true @@ -203,14 +212,19 @@ export default class ShowDataLayerImplementation { if (a === undefined || b === undefined) { return false } - return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray + return ( + a.offset === b.offset && + a.color === b.color && + a.weight === b.weight && + a.dashArray === b.dashArray + ) }) - .addCallbackAndRunD(lineStyle => { + .addCallbackAndRunD((lineStyle) => { if (offsettedLine !== undefined) { self.geoLayer.removeLayer(offsettedLine) } // @ts-ignore - offsettedLine = L.polyline(coords, lineStyle); + offsettedLine = L.polyline(coords, lineStyle) this.postProcessFeature(feat, offsettedLine) offsettedLine.addTo(this.geoLayer) @@ -218,10 +232,16 @@ export default class ShowDataLayerImplementation { return self.geoLayer !== selfLayer }) } else { - this.geoLayer.addData(feat); + this.geoLayer.addData(feat) } } catch (e) { - console.error("Could not add ", feat, "to the geojson layer in leaflet due to", e, e.stack) + console.error( + "Could not add ", + feat, + "to the geojson layer in leaflet due to", + e, + e.stack + ) } } @@ -229,7 +249,7 @@ export default class ShowDataLayerImplementation { if (this.geoLayer.getLayers().length > 0) { try { const bounds = this.geoLayer.getBounds() - mp.fitBounds(bounds, {animate: false}) + mp.fitBounds(bounds, { animate: false }) } catch (e) { console.debug("Invalid bounds", e) } @@ -239,13 +259,13 @@ export default class ShowDataLayerImplementation { if (options.doShowLayer?.data ?? true) { mp.addLayer(this.geoLayer) } - this.isDirty = false; + this.isDirty = false this.openPopupOfSelectedElement(this._selectedElement?.data) } - private createStyleFor(feature) { - const tagsSource = this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties); + const tagsSource = + this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties) // Every object is tied to exactly one layer const layer = this._layerToShow @@ -253,9 +273,12 @@ export default class ShowDataLayerImplementation { const lineRenderingIndex = feature.lineRenderingIndex if (pointRenderingIndex !== undefined) { - const style = layer.mapRendering[pointRenderingIndex].GenerateLeafletStyle(tagsSource, this._enablePopups) + const style = layer.mapRendering[pointRenderingIndex].GenerateLeafletStyle( + tagsSource, + this._enablePopups + ) return { - icon: style + icon: style, } } if (lineRenderingIndex !== undefined) { @@ -272,19 +295,26 @@ export default class ShowDataLayerImplementation { const layer: LayerConfig = this._layerToShow if (layer === undefined) { - return; + return } - let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties) - const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) && this._enablePopups - let style: any = layer.mapRendering[feature.pointRenderingIndex].GenerateLeafletStyle(tagSource, clickable); - const baseElement = style.html; + let tagSource = + this.allElements?.getEventSourceById(feature.properties.id) ?? + new UIEventSource<any>(feature.properties) + const clickable = + !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0) && + this._enablePopups + let style: any = layer.mapRendering[feature.pointRenderingIndex].GenerateLeafletStyle( + tagSource, + clickable + ) + const baseElement = style.html if (!this._enablePopups) { baseElement.SetStyle("cursor: initial !important") } style.html = style.html.ConstructElement() return L.marker(latLng, { - icon: L.divIcon(style) - }); + icon: L.divIcon(style), + }) } /** @@ -298,53 +328,59 @@ export default class ShowDataLayerImplementation { if (layer.title === undefined || !this._enablePopups) { // No popup action defined -> Don't do anything // or probably a map in the popup - no popups needed! - return; + return } - const popup = L.popup({ - autoPan: true, - closeOnEscapeKey: true, - closeButton: false, - autoPanPaddingTopLeft: [15, 15], + const popup = L.popup( + { + autoPan: true, + closeOnEscapeKey: true, + closeButton: false, + autoPanPaddingTopLeft: [15, 15], + }, + leafletLayer + ) - }, leafletLayer); + leafletLayer.bindPopup(popup) - leafletLayer.bindPopup(popup); - - let infobox: ScrollableFullScreen = undefined; - const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}-${feature.multiLineStringIndex ?? ""}` - popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`) - const createpopup = this.createPopup; + let infobox: ScrollableFullScreen = undefined + const id = `popup-${feature.properties.id}-${feature.geometry.type}-${ + this.showDataLayerid + }-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}-${ + feature.multiLineStringIndex ?? "" + }` + popup.setContent( + `<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>` + ) + const createpopup = this.createPopup leafletLayer.on("popupopen", () => { if (infobox === undefined) { - const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties); - infobox = createpopup(tags, layer); + const tags = + this.allElements?.getEventSourceById(feature.properties.id) ?? + new UIEventSource<any>(feature.properties) + infobox = createpopup(tags, layer) - infobox.isShown.addCallback(isShown => { + infobox.isShown.addCallback((isShown) => { if (!isShown) { leafletLayer.closePopup() } - }); + }) } infobox.AttachTo(id) - infobox.Activate(); + infobox.Activate() this.unregister.push(() => { console.log("Destroying infobox") - infobox.Destroy(); + infobox.Destroy() }) if (this._selectedElement?.data?.properties?.id !== feature.properties.id) { this._selectedElement?.setData(feature) } - - }); - + }) // Add the feature to the index to open the popup when needed this.leafletLayersPerId.set(feature.properties.id + feature.geometry.type, { feature: feature, - leafletlayer: leafletLayer + leafletlayer: leafletLayer, }) - } - -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index 036faaf43..1b5832d2c 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -1,15 +1,15 @@ -import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; +import FeatureSource from "../../Logic/FeatureSource/FeatureSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { ElementStorage } from "../../Logic/ElementStorage" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import ScrollableFullScreen from "../Base/ScrollableFullScreen" export interface ShowDataLayerOptions { - features: FeatureSource, - selectedElement?: UIEventSource<any>, - leafletMap: Store<L.Map>, - popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen), - zoomToFeatures?: false | boolean, - doShowLayer?: Store<boolean>, + features: FeatureSource + selectedElement?: UIEventSource<any> + leafletMap: Store<L.Map> + popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen) + zoomToFeatures?: false | boolean + doShowLayer?: Store<boolean> state?: { allElements?: ElementStorage } -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowDataMultiLayer.ts b/UI/ShowDataLayer/ShowDataMultiLayer.ts index 4e20d1020..ef2eabab4 100644 --- a/UI/ShowDataLayer/ShowDataMultiLayer.ts +++ b/UI/ShowDataLayer/ShowDataMultiLayer.ts @@ -1,24 +1,25 @@ /** * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first */ -import {Store} from "../../Logic/UIEventSource"; -import ShowDataLayer from "./ShowDataLayer"; -import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; +import { Store } from "../../Logic/UIEventSource" +import ShowDataLayer from "./ShowDataLayer" +import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteredLayer from "../../Models/FilteredLayer" +import { ShowDataLayerOptions } from "./ShowDataLayerOptions" export default class ShowDataMultiLayer { constructor(options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }) { - - new PerLayerFeatureSourceSplitter(options.layers, (perLayer => { + new PerLayerFeatureSourceSplitter( + options.layers, + (perLayer) => { const newOptions = { ...options, layerToShow: perLayer.layer.layerDef, - features: perLayer + features: perLayer, } new ShowDataLayer(newOptions) - }), - options.features) - + }, + options.features + ) } -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowOverlayLayer.ts b/UI/ShowDataLayer/ShowOverlayLayer.ts index 8d9a49784..697d5e0d1 100644 --- a/UI/ShowDataLayer/ShowOverlayLayer.ts +++ b/UI/ShowDataLayer/ShowOverlayLayer.ts @@ -1,18 +1,21 @@ -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" +import { UIEventSource } from "../../Logic/UIEventSource" export default class ShowOverlayLayer { + public static implementation: ( + config: TilesourceConfig, + leafletMap: UIEventSource<any>, + isShown?: UIEventSource<boolean> + ) => void - public static implementation: (config: TilesourceConfig, - leafletMap: UIEventSource<any>, - isShown?: UIEventSource<boolean>) => void; - - constructor(config: TilesourceConfig, - leafletMap: UIEventSource<any>, - isShown: UIEventSource<boolean> = undefined) { + constructor( + config: TilesourceConfig, + leafletMap: UIEventSource<any>, + isShown: UIEventSource<boolean> = undefined + ) { if (ShowOverlayLayer.implementation === undefined) { throw "Call ShowOverlayLayerImplemenation.initialize() first before using this" } ShowOverlayLayer.implementation(config, leafletMap, isShown) } -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts index d9201fec5..82e461bbe 100644 --- a/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts +++ b/UI/ShowDataLayer/ShowOverlayLayerImplementation.ts @@ -1,45 +1,42 @@ -import * as L from "leaflet"; -import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import ShowOverlayLayer from "./ShowOverlayLayer"; +import * as L from "leaflet" +import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" +import { UIEventSource } from "../../Logic/UIEventSource" +import ShowOverlayLayer from "./ShowOverlayLayer" export default class ShowOverlayLayerImplementation { - public static Implement() { ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap } - public static AddToMap(config: TilesourceConfig, - leafletMap: UIEventSource<any>, - isShown: UIEventSource<boolean> = undefined) { - leafletMap.map(leaflet => { + public static AddToMap( + config: TilesourceConfig, + leafletMap: UIEventSource<any>, + isShown: UIEventSource<boolean> = undefined + ) { + leafletMap.map((leaflet) => { if (leaflet === undefined) { - return; + return } - const tileLayer = L.tileLayer(config.source, - { - attribution: "", - maxZoom: config.maxzoom, - minZoom: config.minzoom, - // @ts-ignore - wmts: false, - }); + const tileLayer = L.tileLayer(config.source, { + attribution: "", + maxZoom: config.maxzoom, + minZoom: config.minzoom, + // @ts-ignore + wmts: false, + }) if (isShown === undefined) { tileLayer.addTo(leaflet) } - isShown?.addCallbackAndRunD(isShown => { + isShown?.addCallbackAndRunD((isShown) => { if (isShown) { tileLayer.addTo(leaflet) } else { leaflet.removeLayer(tileLayer) } - }) - }) } - -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/ShowTileInfo.ts b/UI/ShowDataLayer/ShowTileInfo.ts index 1a04624bc..60542cf1f 100644 --- a/UI/ShowDataLayer/ShowTileInfo.ts +++ b/UI/ShowDataLayer/ShowTileInfo.ts @@ -1,54 +1,55 @@ -import FeatureSource, {Tiled} from "../../Logic/FeatureSource/FeatureSource"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import ShowDataLayer from "./ShowDataLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import {Tiles} from "../../Models/TileRange"; +import FeatureSource, { Tiled } from "../../Logic/FeatureSource/FeatureSource" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import ShowDataLayer from "./ShowDataLayer" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import { GeoOperations } from "../../Logic/GeoOperations" +import { Tiles } from "../../Models/TileRange" import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json" -import State from "../../State"; +import State from "../../State" export default class ShowTileInfo { public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true) constructor(options: { - source: FeatureSource & Tiled, leafletMap: UIEventSource<any>, layer?: LayerConfig, + source: FeatureSource & Tiled + leafletMap: UIEventSource<any> + layer?: LayerConfig doShowLayer?: UIEventSource<boolean> }) { - - const source = options.source - const metaFeature: Store<{feature, freshness: Date}[]> = - source.features.map(features => { + const metaFeature: Store<{ feature; freshness: Date }[]> = source.features.map( + (features) => { const bbox = source.bbox const [z, x, y] = Tiles.tile_from_index(source.tileIndex) const box = { - "type": "Feature", - "properties": { - "z": z, - "x": x, - "y": y, - "tileIndex": source.tileIndex, - "source": source.name, - "count": features.length, - tileId: source.name + "/" + source.tileIndex + type: "Feature", + properties: { + z: z, + x: x, + y: y, + tileIndex: source.tileIndex, + source: source.name, + count: features.length, + tileId: source.name + "/" + source.tileIndex, }, - "geometry": { - "type": "Polygon", - "coordinates": [ + geometry: { + type: "Polygon", + coordinates: [ [ [bbox.minLon, bbox.minLat], [bbox.minLon, bbox.maxLat], [bbox.maxLon, bbox.maxLat], [bbox.maxLon, bbox.minLat], - [bbox.minLon, bbox.minLat] - ] - ] - } + [bbox.minLon, bbox.minLat], + ], + ], + }, } const center = GeoOperations.centerpoint(box) - return [box, center].map(feature => ({feature, freshness: new Date()})) - }) + return [box, center].map((feature) => ({ feature, freshness: new Date() })) + } + ) new ShowDataLayer({ layerToShow: ShowTileInfo.styling, @@ -57,7 +58,5 @@ export default class ShowTileInfo { doShowLayer: options.doShowLayer, state: State.state, }) - } - -} \ No newline at end of file +} diff --git a/UI/ShowDataLayer/TileHierarchyAggregator.ts b/UI/ShowDataLayer/TileHierarchyAggregator.ts index a5f1f9d6c..7e0819968 100644 --- a/UI/ShowDataLayer/TileHierarchyAggregator.ts +++ b/UI/ShowDataLayer/TileHierarchyAggregator.ts @@ -1,10 +1,13 @@ -import FeatureSource, {FeatureSourceForLayer, Tiled} from "../../Logic/FeatureSource/FeatureSource"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Tiles} from "../../Models/TileRange"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import {BBox} from "../../Logic/BBox"; -import FilteredLayer from "../../Models/FilteredLayer"; +import FeatureSource, { + FeatureSourceForLayer, + Tiled, +} from "../../Logic/FeatureSource/FeatureSource" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { UIEventSource } from "../../Logic/UIEventSource" +import { Tiles } from "../../Models/TileRange" +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" +import { BBox } from "../../Logic/BBox" +import FilteredLayer from "../../Models/FilteredLayer" /** * A feature source containing but a single feature, which keeps stats about a tile @@ -14,32 +17,50 @@ export class TileHierarchyAggregator implements FeatureSource { public totalValue: number = 0 public showCount: number = 0 public hiddenCount: number = 0 - public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty) - public readonly name; - private _parent: TileHierarchyAggregator; - private _root: TileHierarchyAggregator; - private _z: number; - private _x: number; - private _y: number; + public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>( + TileHierarchyAggregator.empty + ) + public readonly name + private _parent: TileHierarchyAggregator + private _root: TileHierarchyAggregator + private _z: number + private _x: number + private _y: number private _tileIndex: number private _counter: SingleTileCounter - private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined] + private _subtiles: [ + TileHierarchyAggregator, + TileHierarchyAggregator, + TileHierarchyAggregator, + TileHierarchyAggregator + ] = [undefined, undefined, undefined, undefined] private readonly featuresStatic = [] - private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string }; - private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> }; + private readonly featureProperties: { + count: string + kilocount: string + tileId: string + id: string + showCount: string + totalCount: string + } + private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> } private readonly updateSignal = new UIEventSource<any>(undefined) - private constructor(parent: TileHierarchyAggregator, - state: { - filteredLayers: UIEventSource<FilteredLayer[]> - }, - z: number, x: number, y: number) { - this._parent = parent; - this._state = state; + private constructor( + parent: TileHierarchyAggregator, + state: { + filteredLayers: UIEventSource<FilteredLayer[]> + }, + z: number, + x: number, + y: number + ) { + this._parent = parent + this._state = state this._root = parent?._root ?? this - this._z = z; - this._x = x; - this._y = y; + this._z = z + this._x = x + this._y = y this._tileIndex = Tiles.tile_index(z, x, y) this.name = "Count(" + this._tileIndex + ")" @@ -49,39 +70,39 @@ export class TileHierarchyAggregator implements FeatureSource { count: `0`, kilocount: "0", showCount: "0", - totalCount: "0" + totalCount: "0", } this.featureProperties = totals const now = new Date() const feature = { - "type": "Feature", - "properties": totals, - "geometry": { - "type": "Point", - "coordinates": Tiles.centerPointOf(z, x, y) - } + type: "Feature", + properties: totals, + geometry: { + type: "Point", + coordinates: Tiles.centerPointOf(z, x, y), + }, } - this.featuresStatic.push({feature: feature, freshness: now}) + this.featuresStatic.push({ feature: feature, freshness: now }) const bbox = BBox.fromTile(z, x, y) const box = { - "type": "Feature", - "properties": totals, - "geometry": { - "type": "Polygon", - "coordinates": [ + type: "Feature", + properties: totals, + geometry: { + type: "Polygon", + coordinates: [ [ [bbox.minLon, bbox.minLat], [bbox.minLon, bbox.maxLat], [bbox.maxLon, bbox.maxLat], [bbox.maxLon, bbox.minLat], - [bbox.minLon, bbox.minLat] - ] - ] - } + [bbox.minLon, bbox.minLat], + ], + ], + }, } - this.featuresStatic.push({feature: box, freshness: now}) + this.featuresStatic.push({ feature: box, freshness: now }) } public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) { @@ -90,7 +111,7 @@ export class TileHierarchyAggregator implements FeatureSource { public getTile(tileIndex): TileHierarchyAggregator { if (tileIndex === this._tileIndex) { - return this; + return this } let [tileZ, tileX, tileY] = Tiles.tile_from_index(tileIndex) while (tileZ - 1 > this._z) { @@ -98,22 +119,21 @@ export class TileHierarchyAggregator implements FeatureSource { tileY = Math.floor(tileY / 2) tileZ-- } - const xDiff = tileX - (2 * this._x) - const yDiff = tileY - (2 * this._y) - const subtileIndex = yDiff * 2 + xDiff; + const xDiff = tileX - 2 * this._x + const yDiff = tileY - 2 * this._y + const subtileIndex = yDiff * 2 + xDiff return this._subtiles[subtileIndex]?.getTile(tileIndex) } public addTile(source: FeatureSourceForLayer & Tiled) { - const self = this; + const self = this if (source.tileIndex === this._tileIndex) { if (this._counter === undefined) { this._counter = new SingleTileCounter(this._tileIndex) - this._counter.countsPerLayer.addCallbackAndRun(_ => self.update()) + this._counter.countsPerLayer.addCallbackAndRun((_) => self.update()) } this._counter.addTileCount(source) } else { - // We have to give it to one of the subtiles let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex) while (tileZ - 1 > this._z) { @@ -121,42 +141,57 @@ export class TileHierarchyAggregator implements FeatureSource { tileY = Math.floor(tileY / 2) tileZ-- } - const xDiff = tileX - (2 * this._x) - const yDiff = tileY - (2 * this._y) + const xDiff = tileX - 2 * this._x + const yDiff = tileY - 2 * this._y - const subtileIndex = yDiff * 2 + xDiff; + const subtileIndex = yDiff * 2 + xDiff if (this._subtiles[subtileIndex] === undefined) { - this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY) + this._subtiles[subtileIndex] = new TileHierarchyAggregator( + this, + this._state, + tileZ, + tileX, + tileY + ) } this._subtiles[subtileIndex].addTile(source) } this.updateSignal.setData(source) } - getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource { + getCountsForZoom( + clusteringConfig: { maxZoom: number }, + locationControl: UIEventSource<{ zoom: number }>, + cutoff: number = 0 + ): FeatureSource { const self = this const empty = [] - const features = locationControl.map(loc => loc.zoom).map(targetZoom => { - if (targetZoom - 1 > clusteringConfig.maxZoom) { - return empty - } + const features = locationControl + .map((loc) => loc.zoom) + .map( + (targetZoom) => { + if (targetZoom - 1 > clusteringConfig.maxZoom) { + return empty + } - const features: {feature: any, freshness: Date}[] = [] - self.visitSubTiles(aggr => { - if (aggr.showCount < cutoff) { - return false - } - if (aggr._z === targetZoom) { - features.push(...aggr.features.data) - return false - } - return aggr._z <= targetZoom; - }) + const features: { feature: any; freshness: Date }[] = [] + self.visitSubTiles((aggr) => { + if (aggr.showCount < cutoff) { + return false + } + if (aggr._z === targetZoom) { + features.push(...aggr.features.data) + return false + } + return aggr._z <= targetZoom + }) - return features - }, [this.updateSignal.stabilized(500)]) + return features + }, + [this.updateSignal.stabilized(500)] + ) - return new StaticFeatureSource(features); + return new StaticFeatureSource(features) } private update() { @@ -176,14 +211,13 @@ export class TileHierarchyAggregator implements FeatureSource { if (flayer.isDisplayed.data && this._z >= flayer.layerDef.minzoom) { showCount += count } else { - hiddenCount += count; + hiddenCount += count } }) - for (const tile of this._subtiles) { if (tile === undefined) { - continue; + continue } total += tile.totalValue @@ -192,7 +226,10 @@ export class TileHierarchyAggregator implements FeatureSource { for (const key in tile.featureProperties) { if (key.startsWith("layer:")) { - newMap.set(key, (newMap.get(key) ?? 0) + Number(tile.featureProperties[key] ?? 0)) + newMap.set( + key, + (newMap.get(key) ?? 0) + Number(tile.featureProperties[key] ?? 0) + ) } } } @@ -205,8 +242,8 @@ export class TileHierarchyAggregator implements FeatureSource { if (total === 0) { this.features.setData(TileHierarchyAggregator.empty) } else { - this.featureProperties.count = "" + total; - this.featureProperties.kilocount = "" + Math.floor(total / 1000); + this.featureProperties.count = "" + total + this.featureProperties.kilocount = "" + Math.floor(total / 1000) this.featureProperties.showCount = "" + showCount this.featureProperties.totalCount = "" + total newMap.forEach((value, key) => { @@ -221,7 +258,7 @@ export class TileHierarchyAggregator implements FeatureSource { private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) { const visitFurther = f(this) if (visitFurther) { - this._subtiles.forEach(tile => tile?.visitSubTiles(f)) + this._subtiles.forEach((tile) => tile?.visitSubTiles(f)) } } } @@ -230,20 +267,22 @@ export class TileHierarchyAggregator implements FeatureSource { * Keeps track of a single tile */ class SingleTileCounter implements Tiled { - public readonly bbox: BBox; - public readonly tileIndex: number; - public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>()) + public readonly bbox: BBox + public readonly tileIndex: number + public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource< + Map<string, number> + >(new Map<string, number>()) public readonly z: number public readonly x: number public readonly y: number - private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>(); + private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>() constructor(tileIndex: number) { this.tileIndex = tileIndex this.bbox = BBox.fromTileIndex(tileIndex) const [z, x, y] = Tiles.tile_from_index(tileIndex) - this.z = z; - this.x = x; + this.z = z + this.x = x this.y = y } @@ -251,11 +290,13 @@ class SingleTileCounter implements Tiled { const layer = source.layer.layerDef this.registeredLayers.set(layer.id, layer) const self = this - source.features.map(f => { - const isDisplayed = source.layer.isDisplayed.data - self.countsPerLayer.data.set(layer.id, isDisplayed ? f.length : 0) - self.countsPerLayer.ping() - }, [source.layer.isDisplayed]) + source.features.map( + (f) => { + const isDisplayed = source.layer.isDisplayed.data + self.countsPerLayer.data.set(layer.id, isDisplayed ? f.length : 0) + self.countsPerLayer.ping() + }, + [source.layer.isDisplayed] + ) } - -} \ No newline at end of file +} diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index d29511112..eba94c135 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -1,96 +1,106 @@ -import {Store, Stores, UIEventSource} from "../Logic/UIEventSource"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; -import {ImageCarousel} from "./Image/ImageCarousel"; -import Combine from "./Base/Combine"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import {ImageUploadFlow} from "./Image/ImageUploadFlow"; -import ShareButton from "./BigComponents/ShareButton"; -import Svg from "../Svg"; -import ReviewElement from "./Reviews/ReviewElement"; -import MangroveReviews from "../Logic/Web/MangroveReviews"; -import Translations from "./i18n/Translations"; -import ReviewForm from "./Reviews/ReviewForm"; -import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; -import BaseUIElement from "./BaseUIElement"; -import Title from "./Base/Title"; -import Table from "./Base/Table"; -import Histogram from "./BigComponents/Histogram"; -import Loc from "../Models/Loc"; -import {Utils} from "../Utils"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; -import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; -import Minimap from "./Base/Minimap"; -import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; -import WikipediaBox from "./Wikipedia/WikipediaBox"; -import MultiApply from "./Popup/MultiApply"; -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; -import {SubtleButton} from "./Base/SubtleButton"; -import {DefaultGuiState} from "./DefaultGuiState"; -import {GeoOperations} from "../Logic/GeoOperations"; -import Hash from "../Logic/Web/Hash"; -import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; -import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/ImportButton"; -import TagApplyButton from "./Popup/TagApplyButton"; -import AutoApplyButton from "./Popup/AutoApplyButton"; -import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json"; -import {OpenIdEditor, OpenJosm} from "./BigComponents/CopyrightPanel"; -import Toggle from "./Input/Toggle"; -import Img from "./Base/Img"; -import NoteCommentElement from "./Popup/NoteCommentElement"; -import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"; -import FileSelectorButton from "./Input/FileSelectorButton"; -import {LoginToggle} from "./Popup/LoginButton"; -import {SubstitutedTranslation} from "./SubstitutedTranslation"; -import {TextField} from "./Input/TextField"; -import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; -import {Translation} from "./i18n/Translation"; -import {AllTagsPanel} from "./AllTagsPanel"; -import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage} from "./Popup/NearbyImages"; -import Lazy from "./Base/Lazy"; -import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; -import {Tag} from "../Logic/Tags/Tag"; -import {And} from "../Logic/Tags/And"; -import {SaveButton} from "./Popup/SaveButton"; -import {MapillaryLink} from "./BigComponents/MapillaryLink"; -import {CheckBox} from "./Input/Checkboxes"; -import Slider from "./Input/Slider"; -import List from "./Base/List"; -import StatisticsPanel from "./BigComponents/StatisticsPanel"; -import {OsmFeature} from "../Models/OsmFeature"; -import EditableTagRendering from "./Popup/EditableTagRendering"; -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; -import {ProvidedImage} from "../Logic/ImageProviders/ImageProvider"; -import PlantNetSpeciesSearch from "./BigComponents/PlantNetSpeciesSearch"; +import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" +import { VariableUiElement } from "./Base/VariableUIElement" +import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" +import { ImageCarousel } from "./Image/ImageCarousel" +import Combine from "./Base/Combine" +import { FixedUiElement } from "./Base/FixedUiElement" +import { ImageUploadFlow } from "./Image/ImageUploadFlow" +import ShareButton from "./BigComponents/ShareButton" +import Svg from "../Svg" +import ReviewElement from "./Reviews/ReviewElement" +import MangroveReviews from "../Logic/Web/MangroveReviews" +import Translations from "./i18n/Translations" +import ReviewForm from "./Reviews/ReviewForm" +import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" +import BaseUIElement from "./BaseUIElement" +import Title from "./Base/Title" +import Table from "./Base/Table" +import Histogram from "./BigComponents/Histogram" +import Loc from "../Models/Loc" +import { Utils } from "../Utils" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" +import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer" +import Minimap from "./Base/Minimap" +import AllImageProviders from "../Logic/ImageProviders/AllImageProviders" +import WikipediaBox from "./Wikipedia/WikipediaBox" +import MultiApply from "./Popup/MultiApply" +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" +import { SubtleButton } from "./Base/SubtleButton" +import { DefaultGuiState } from "./DefaultGuiState" +import { GeoOperations } from "../Logic/GeoOperations" +import Hash from "../Logic/Web/Hash" +import FeaturePipelineState from "../Logic/State/FeaturePipelineState" +import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/ImportButton" +import TagApplyButton from "./Popup/TagApplyButton" +import AutoApplyButton from "./Popup/AutoApplyButton" +import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json" +import { OpenIdEditor, OpenJosm } from "./BigComponents/CopyrightPanel" +import Toggle from "./Input/Toggle" +import Img from "./Base/Img" +import NoteCommentElement from "./Popup/NoteCommentElement" +import ImgurUploader from "../Logic/ImageProviders/ImgurUploader" +import FileSelectorButton from "./Input/FileSelectorButton" +import { LoginToggle } from "./Popup/LoginButton" +import { SubstitutedTranslation } from "./SubstitutedTranslation" +import { TextField } from "./Input/TextField" +import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" +import { Translation } from "./i18n/Translation" +import { AllTagsPanel } from "./AllTagsPanel" +import NearbyImages, { + NearbyImageOptions, + P4CPicture, + SelectOneNearbyImage, +} from "./Popup/NearbyImages" +import Lazy from "./Base/Lazy" +import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction" +import { Tag } from "../Logic/Tags/Tag" +import { And } from "../Logic/Tags/And" +import { SaveButton } from "./Popup/SaveButton" +import { MapillaryLink } from "./BigComponents/MapillaryLink" +import { CheckBox } from "./Input/Checkboxes" +import Slider from "./Input/Slider" +import List from "./Base/List" +import StatisticsPanel from "./BigComponents/StatisticsPanel" +import { OsmFeature } from "../Models/OsmFeature" +import EditableTagRendering from "./Popup/EditableTagRendering" +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" +import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" +import PlantNetSpeciesSearch from "./BigComponents/PlantNetSpeciesSearch" export interface SpecialVisualization { - funcName: string, - constr: ((state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState,) => BaseUIElement), - docs: string | BaseUIElement, - example?: string, - args: { name: string, defaultValue?: string, doc: string, required?: false | boolean }[], + funcName: string + constr: ( + state: FeaturePipelineState, + tagSource: UIEventSource<any>, + argument: string[], + guistate: DefaultGuiState + ) => BaseUIElement + docs: string | BaseUIElement + example?: string + args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[] getLayerDependencies?: (argument: string[]) => string[] } class CloseNoteButton implements SpecialVisualization { public readonly funcName = "close_note" - 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 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 = [ { name: "text", doc: "Text to show on this button", - required: true + required: true, }, { name: "icon", doc: "Icon to show", - defaultValue: "checkmark.svg" + defaultValue: "checkmark.svg", }, { name: "idkey", doc: "The property name where the ID of the note to close can be found", - defaultValue: "id" + defaultValue: "id", }, { name: "comment", @@ -98,23 +108,23 @@ class CloseNoteButton implements SpecialVisualization { }, { name: "minZoom", - doc: "If set, only show the closenote button if zoomed in enough" + doc: "If set, only show the closenote button if zoomed in enough", }, { name: "zoomButton", - doc: "Text to show if not zoomed in enough" - } + doc: "Text to show if not zoomed in enough", + }, ] public constr(state: FeaturePipelineState, tags, args): BaseUIElement { - const t = Translations.t.notes; + const t = Translations.t.notes const params: { - text: string, - icon: string, - idkey: string, - comment: string, - minZoom: string, + text: string + icon: string + idkey: string + comment: string + minZoom: string zoomButton: string } = Utils.ParseVisArgs(this.args, args) @@ -122,70 +132,80 @@ class CloseNoteButton implements SpecialVisualization { if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") { icon = new Img(args[1]) } - let textToShow = t.closeNote; + let textToShow = t.closeNote if ((params.text ?? "") !== "") { textToShow = Translations.T(args[0]) } let closeButton: BaseUIElement = new SubtleButton(icon, textToShow) - const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== ""); + const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") closeButton.onClick(() => { const id = tags.data[args[2] ?? "id"] - state.osmConnection.closeNote(id, args[3]) - ?.then(_ => { - tags.data["closed_at"] = new Date().toISOString(); - tags.ping() - }) + state.osmConnection.closeNote(id, args[3])?.then((_) => { + tags.data["closed_at"] = new Date().toISOString() + tags.ping() + }) }) if ((params.minZoom ?? "") !== "" && !isNaN(Number(params.minZoom))) { closeButton = new Toggle( closeButton, params.zoomButton ?? "", - state.locationControl.map(l => l.zoom >= Number(params.minZoom)) + state.locationControl.map((l) => l.zoom >= Number(params.minZoom)) ) } - return new LoginToggle(new Toggle( - t.isClosed.SetClass("thanks"), - closeButton, + return new LoginToggle( + new Toggle( + t.isClosed.SetClass("thanks"), + closeButton, - isClosed - ), t.loginToClose, state) + isClosed + ), + t.loginToClose, + state + ) } - } class NearbyImageVis implements SpecialVisualization { args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ - { name: "mode", defaultValue: "expandable", - doc: "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them." + doc: "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them.", }, { name: "mapillary", defaultValue: "true", - doc: "If 'true', includes a link to mapillary on this location." - } + doc: "If 'true', includes a link to mapillary on this location.", + }, ] - 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"; + 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" - constr(state: FeaturePipelineState, tagSource: UIEventSource<any>, args: string[], guistate: DefaultGuiState): BaseUIElement { + constr( + state: FeaturePipelineState, + tagSource: UIEventSource<any>, + args: string[], + guistate: DefaultGuiState + ): BaseUIElement { const t = Translations.t.image.nearbyPictures const mode: "open" | "expandable" | "collapsable" = <any>args[0] const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const id: string = tagSource.data["id"] - const canBeEdited: boolean = !!(id?.match("(node|way|relation)/-?[0-9]+")) - const selectedImage = new UIEventSource<P4CPicture>(undefined); - + const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") + const selectedImage = new UIEventSource<P4CPicture>(undefined) let saveButton: BaseUIElement = undefined if (canBeEdited) { - const confirmText: BaseUIElement = new SubstitutedTranslation(t.confirm, tagSource, state) + const confirmText: BaseUIElement = new SubstitutedTranslation( + t.confirm, + tagSource, + state + ) const onSave = async () => { console.log("Selected a picture...", selectedImage.data) @@ -195,290 +215,336 @@ class NearbyImageVis implements SpecialVisualization { tags.push(new Tag(key, osmTags[key])) } await state?.changes?.applyAction( - new ChangeTagAction( - id, - new And(tags), - tagSource.data, - { - theme: state?.layoutToUse.id, - changeType: "link-image" - } - ) + new ChangeTagAction(id, new And(tags), tagSource.data, { + theme: state?.layoutToUse.id, + changeType: "link-image", + }) ) - }; - saveButton = new SaveButton(selectedImage, state.osmConnection, confirmText, t.noImageSelected) - .onClick(onSave).SetClass("flex justify-end") + } + saveButton = new SaveButton( + selectedImage, + state.osmConnection, + confirmText, + t.noImageSelected + ) + .onClick(onSave) + .SetClass("flex justify-end") } const nearby = new Lazy(() => { const towardsCenter = new CheckBox(t.onlyTowards, false) - const radiusValue = state?.osmConnection?.GetPreference("nearby-images-radius", "300").sync(s => Number(s), [], i => "" + i) ?? new UIEventSource(300); + const radiusValue = + state?.osmConnection?.GetPreference("nearby-images-radius", "300").sync( + (s) => Number(s), + [], + (i) => "" + i + ) ?? new UIEventSource(300) const radius = new Slider(25, 500, { - value: - radiusValue, step: 25 + value: radiusValue, + step: 25, }) const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource) const options: NearbyImageOptions & { value } = { - lon, lat, + lon, + lat, searchRadius: 500, shownRadius: radius.GetValue(), value: selectedImage, blacklist: alreadyInTheImage, towardscenter: towardsCenter.GetValue(), - maxDaysOld: 365 * 5 - - }; - const slideshow = canBeEdited ? new SelectOneNearbyImage(options, state) : new NearbyImages(options, state); - const controls = new Combine([towardsCenter, + maxDaysOld: 365 * 5, + } + const slideshow = canBeEdited + ? new SelectOneNearbyImage(options, state) + : new NearbyImages(options, state) + const controls = new Combine([ + towardsCenter, new Combine([ - new VariableUiElement(radius.GetValue().map(radius => t.withinRadius.Subs({radius}))), radius - ]).SetClass("flex justify-between") - ]).SetClass("flex flex-col"); - return new Combine([slideshow, + new VariableUiElement( + radius.GetValue().map((radius) => t.withinRadius.Subs({ radius })) + ), + radius, + ]).SetClass("flex justify-between"), + ]).SetClass("flex flex-col") + return new Combine([ + slideshow, controls, saveButton, - new MapillaryLinkVis().constr(state, tagSource, []).SetClass("mt-6")]) - }); + new MapillaryLinkVis().constr(state, tagSource, []).SetClass("mt-6"), + ]) + }) - let withEdit: BaseUIElement = nearby; + let withEdit: BaseUIElement = nearby if (canBeEdited) { - withEdit = new Combine([ - t.hasMatchingPicture, - nearby - ]).SetClass("flex flex-col") + withEdit = new Combine([t.hasMatchingPicture, nearby]).SetClass("flex flex-col") } - if (mode === 'open') { + if (mode === "open") { return withEdit } - const toggleState = new UIEventSource<boolean>(mode === 'collapsable') + const toggleState = new UIEventSource<boolean>(mode === "collapsable") return new Toggle( new Combine([new Title(t.title), withEdit]), new Title(t.browseNearby).onClick(() => toggleState.setData(true)), toggleState ) } - } export class MapillaryLinkVis implements SpecialVisualization { funcName = "mapillary_link" docs = "Adds a button to open mapillary on the specified location" - args = [{ - name: "zoom", - doc: "The startzoom of mapillary", - defaultValue: "18" - }]; + args = [ + { + name: "zoom", + doc: "The startzoom of mapillary", + defaultValue: "18", + }, + ] public constr(state, tagsSource, args) { - const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id); - const [lon, lat] = GeoOperations.centerpointCoordinates(feat); + const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id) + const [lon, lat] = GeoOperations.centerpointCoordinates(feat) let zoom = Number(args[0]) if (isNaN(zoom)) { zoom = 18 } return new MapillaryLink({ locationControl: new UIEventSource<Loc>({ - lat, lon, zoom - }) + lat, + lon, + zoom, + }), }) } } export default class SpecialVisualizations { - public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.init() public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined { if (typeof viz === "string") { - viz = SpecialVisualizations.specialVisualizations.find(sv => sv.funcName === viz) + viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz) } if (viz === undefined) { - return undefined; + return undefined } - return new Combine( - [ - new Title(viz.funcName, 3), - viz.docs, - viz.args.length > 0 ? new Table(["name", "default", "description"], - viz.args.map(arg => { - let defaultArg = arg.defaultValue ?? "_undefined_" - if (defaultArg == "") { - defaultArg = "_empty string_" - } - return [arg.name, defaultArg, arg.doc]; - }) - ) : undefined, - new Title("Example usage of " + viz.funcName, 4), - new FixedUiElement( - viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`" - ).SetClass("literal-code"), - - ]) + return new Combine([ + new Title(viz.funcName, 3), + viz.docs, + viz.args.length > 0 + ? new Table( + ["name", "default", "description"], + viz.args.map((arg) => { + let defaultArg = arg.defaultValue ?? "_undefined_" + if (defaultArg == "") { + defaultArg = "_empty string_" + } + return [arg.name, defaultArg, arg.doc] + }) + ) + : undefined, + new Title("Example usage of " + viz.funcName, 4), + new FixedUiElement( + viz.example ?? + "`{" + + viz.funcName + + "(" + + viz.args.map((arg) => arg.defaultValue).join(",") + + ")}`" + ).SetClass("literal-code"), + ]) } public static HelpMessage() { - - const helpTexts = SpecialVisualizations.specialVisualizations.map(viz => SpecialVisualizations.DocumentationFor(viz)); + const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => + SpecialVisualizations.DocumentationFor(viz) + ) return new Combine([ - new Combine([ + new Combine([ + new Title("Special tag renderings", 1), - new Title("Special tag renderings", 1), - - "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", - "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", - new Title("Using expanded syntax", 4), - `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`, - new FixedUiElement(JSON.stringify({ - render: { - special: { - type: "some_special_visualisation", - before: { - en: "Some text to prefix before the special element (e.g. a title)", - nl: "Een tekst om voor het element te zetten (bv. een titel)" + "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", + "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", + new Title("Using expanded syntax", 4), + `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`, + new FixedUiElement( + JSON.stringify( + { + render: { + special: { + type: "some_special_visualisation", + before: { + en: "Some text to prefix before the special element (e.g. a title)", + nl: "Een tekst om voor het element te zetten (bv. een titel)", + }, + after: { + en: "Some text to put after the element, e.g. a footer", + }, + argname: "some_arg", + message: { + en: "some other really long message", + nl: "een boodschap in een andere taal", + }, + other_arg_name: "more args", }, - after: { - en: "Some text to put after the element, e.g. a footer" - }, - "argname": "some_arg", - "message": { - en: "some other really long message", - nl: "een boodschap in een andere taal" - }, - "other_arg_name": "more args" - } - } - }, null, " ")).SetClass("code") - ]).SetClass("flex flex-col"), - ...helpTexts - ] - ).SetClass("flex flex-col"); + }, + }, + null, + " " + ) + ).SetClass("code"), + ]).SetClass("flex flex-col"), + ...helpTexts, + ]).SetClass("flex flex-col") } private static init() { - const specialVisualizations: SpecialVisualization[] = - [ - { - funcName: "all_tags", - docs: "Prints all key-value pairs of the object - used for debugging", - args: [], - constr: ((state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state)) - }, - { - funcName: "image_carousel", - docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", - args: [{ + const specialVisualizations: SpecialVisualization[] = [ + { + funcName: "all_tags", + docs: "Prints all key-value pairs of the object - used for debugging", + args: [], + constr: (state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state), + }, + { + funcName: "image_carousel", + docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", + args: [ + { name: "image_key", defaultValue: AllImageProviders.defaultKeys.join(","), - doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated " - }], - constr: (state, tags, args) => { - let imagePrefixes: string[] = undefined; - if (args.length > 0) { - imagePrefixes = [].concat(...args.map(a => a.split(","))); - } - return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, state); + doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ", + }, + ], + constr: (state, tags, args) => { + let imagePrefixes: string[] = undefined + if (args.length > 0) { + imagePrefixes = [].concat(...args.map((a) => a.split(","))) } + return new ImageCarousel( + AllImageProviders.LoadImagesFor(tags, imagePrefixes), + tags, + state + ) }, - { - funcName: "image_upload", - docs: "Creates a button where a user can upload an image to IMGUR", - args: [{ + }, + { + funcName: "image_upload", + docs: "Creates a button where a user can upload an image to IMGUR", + args: [ + { name: "image-key", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", - defaultValue: "image" - }, { + defaultValue: "image", + }, + { name: "label", doc: "The text to show on the button", - defaultValue: "Add image" - }], - constr: (state, tags, args) => { - return new ImageUploadFlow(tags, state, args[0], args[1]) - } + defaultValue: "Add image", + }, + ], + constr: (state, tags, args) => { + return new ImageUploadFlow(tags, state, args[0], args[1]) }, - { - funcName: "wikipedia", - docs: "A box showing the corresponding wikipedia article - based on the wikidata tag", - args: [ - { - name: "keyToShowWikipediaFor", - doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used", - defaultValue: "wikidata;wikipedia" - } - ], - 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) => { - const keys = args[0].split(";").map(k => k.trim()) - return new VariableUiElement( - tagsSource.map(tags => { - const key = keys.find(k => tags[k] !== undefined && tags[k] !== "") - return tags[key]; + }, + { + funcName: "wikipedia", + docs: "A box showing the corresponding wikipedia article - based on the wikidata tag", + args: [ + { + name: "keyToShowWikipediaFor", + doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used", + defaultValue: "wikidata;wikipedia", + }, + ], + 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) => { + const keys = args[0].split(";").map((k) => k.trim()) + return new VariableUiElement( + tagsSource + .map((tags) => { + const key = keys.find( + (k) => tags[k] !== undefined && tags[k] !== "" + ) + return tags[key] }) - .map(wikidata => { - const wikidatas: string[] = - Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? []) - return new WikipediaBox(wikidatas) - }) - ); - } - + .map((wikidata) => { + const wikidatas: string[] = Utils.NoEmpty( + wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] + ) + return new WikipediaBox(wikidatas) + }) + ) }, - { - funcName: "wikidata_label", - docs: "Shows the label of the corresponding wikidata-item", - args: [ - { - name: "keyToShowWikidataFor", - doc: "Use the wikidata entry from this key to show the label", - defaultValue: "wikidata" - } - ], - example: "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", - constr: (_, tagsSource, args) => - new VariableUiElement( - tagsSource.map(tags => tags[args[0]]) - .map(wikidata => { - wikidata = Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? [])[0] - const entry = Wikidata.LoadWikidataEntry(wikidata) - return new VariableUiElement(entry.map(e => { + }, + { + funcName: "wikidata_label", + docs: "Shows the label of the corresponding wikidata-item", + args: [ + { + name: "keyToShowWikidataFor", + doc: "Use the wikidata entry from this key to show the label", + defaultValue: "wikidata", + }, + ], + example: + "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", + constr: (_, tagsSource, args) => + new VariableUiElement( + tagsSource + .map((tags) => tags[args[0]]) + .map((wikidata) => { + wikidata = Utils.NoEmpty( + wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] + )[0] + const entry = Wikidata.LoadWikidataEntry(wikidata) + return new VariableUiElement( + entry.map((e) => { if (e === undefined || e["success"] === undefined) { return wikidata } const response = <WikidataResponse>e["success"] return Translation.fromMap(response.labels) - })) - })) - }, - { - funcName: "minimap", - docs: "A small map showing the selected feature.", - 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", - name: "zoomlevel", - defaultValue: "18" - }, - { - doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap.", - name: "idKey", - defaultValue: "id" - } - ], - example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`", - constr: (state, tagSource, args, _) => { - - if (state === undefined) { - return undefined - } - const keys = [...args] - keys.splice(0, 1) - const featureStore = state.allElements.ContainingFeatures - const featuresToShow: Store<{ freshness: Date, feature: any }[]> = tagSource.map(properties => { - const values: string[] = Utils.NoNull(keys.map(key => properties[key])) - const features: { freshness: Date, feature: any }[] = [] + }) + ) + }) + ), + }, + { + funcName: "minimap", + docs: "A small map showing the selected feature.", + 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", + name: "zoomlevel", + defaultValue: "18", + }, + { + doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap.", + name: "idKey", + defaultValue: "id", + }, + ], + example: + "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`", + constr: (state, tagSource, args, _) => { + if (state === undefined) { + return undefined + } + const keys = [...args] + keys.splice(0, 1) + const featureStore = state.allElements.ContainingFeatures + const featuresToShow: Store<{ freshness: Date; feature: any }[]> = + tagSource.map((properties) => { + const values: string[] = Utils.NoNull( + keys.map((key) => properties[key]) + ) + const features: { freshness: Date; feature: any }[] = [] for (const value of values) { let idList = [value] if (value.startsWith("[")) { @@ -490,873 +556,1024 @@ export default class SpecialVisualizations { const feature = featureStore.get(id) features.push({ freshness: new Date(), - feature + feature, }) } } return features }) - const properties = tagSource.data; - let zoom = 18 - if (args[0]) { - const parsed = Number(args[0]) - if (!isNaN(parsed) && parsed > 0 && parsed < 25) { - zoom = parsed; - } + const properties = tagSource.data + let zoom = 18 + if (args[0]) { + const parsed = Number(args[0]) + if (!isNaN(parsed) && parsed > 0 && parsed < 25) { + zoom = parsed } - const locationSource = new UIEventSource<Loc>({ - lat: Number(properties._lat), - lon: Number(properties._lon), - zoom: zoom - }) - const minimap = Minimap.createMiniMap( - { - background: state.backgroundLayer, - location: locationSource, - allowMoving: false - } - ) - - locationSource.addCallback(loc => { - if (loc.zoom > zoom) { - // We zoom back - locationSource.data.zoom = zoom; - locationSource.ping(); - } - }) - - new ShowDataMultiLayer( - { - leafletMap: minimap["leafletMap"], - zoomToFeatures: true, - layers: state.filteredLayers, - features: new StaticFeatureSource(featuresToShow) - } - ) - - - minimap.SetStyle("overflow: hidden; pointer-events: none;") - return minimap; } + const locationSource = new UIEventSource<Loc>({ + lat: Number(properties._lat), + lon: Number(properties._lon), + zoom: zoom, + }) + const minimap = Minimap.createMiniMap({ + background: state.backgroundLayer, + location: locationSource, + allowMoving: false, + }) + + locationSource.addCallback((loc) => { + if (loc.zoom > zoom) { + // We zoom back + locationSource.data.zoom = zoom + locationSource.ping() + } + }) + + new ShowDataMultiLayer({ + leafletMap: minimap["leafletMap"], + zoomToFeatures: true, + layers: state.filteredLayers, + features: new StaticFeatureSource(featuresToShow), + }) + + minimap.SetStyle("overflow: hidden; pointer-events: none;") + return minimap }, - { - funcName: "sided_minimap", - docs: "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced", - args: [ - { - doc: "The side to show, either `left` or `right`", - name: "side", - required: true - } - ], - example: "`{sided_minimap(left)}`", - constr: (state, tagSource, args) => { - - const properties = tagSource.data; - const locationSource = new UIEventSource<Loc>({ - lat: Number(properties._lat), - lon: Number(properties._lon), - zoom: 18 - }) - const minimap = Minimap.createMiniMap( - { - background: state.backgroundLayer, - location: locationSource, - allowMoving: false - } - ) - const side = args[0] - const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) - const copy = {...feature} - copy.properties = { - id: side - } - new ShowDataLayer( - { - leafletMap: minimap["leafletMap"], - zoomToFeatures: true, - layerToShow: new LayerConfig(left_right_style_json, "all_known_layers", true), - features: StaticFeatureSource.fromGeojson([copy]), - state - } - ) - - - minimap.SetStyle("overflow: hidden; pointer-events: none;") - return minimap; + }, + { + funcName: "sided_minimap", + docs: "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced", + args: [ + { + doc: "The side to show, either `left` or `right`", + name: "side", + required: true, + }, + ], + example: "`{sided_minimap(left)}`", + constr: (state, tagSource, args) => { + const properties = tagSource.data + const locationSource = new UIEventSource<Loc>({ + lat: Number(properties._lat), + lon: Number(properties._lon), + zoom: 18, + }) + const minimap = Minimap.createMiniMap({ + background: state.backgroundLayer, + location: locationSource, + allowMoving: false, + }) + const side = args[0] + const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) + const copy = { ...feature } + copy.properties = { + id: side, } + new ShowDataLayer({ + leafletMap: minimap["leafletMap"], + zoomToFeatures: true, + layerToShow: new LayerConfig( + left_right_style_json, + "all_known_layers", + true + ), + features: StaticFeatureSource.fromGeojson([copy]), + state, + }) + + minimap.SetStyle("overflow: hidden; pointer-events: none;") + return minimap }, - { - 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", - 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: [{ + }, + { + 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", + 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: [ + { name: "subjectKey", defaultValue: "name", - doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" - }, { + doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>", + }, + { name: "fallback", - doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" - }], - constr: (state, tags, args) => { - const tgs = tags.data; - const key = args[0] ?? "name" - let subject = tgs[key] ?? args[1]; - if (subject === undefined || subject === "") { - return Translations.t.reviews.name_required; - } - const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), - encodeURIComponent(subject), - state.mangroveIdentity, - state.featureSwitchIsTesting.data - ); - const form = new ReviewForm((r, whenDone) => mangrove.AddReview(r, whenDone), state.osmConnection); - return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form); + doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value", + }, + ], + constr: (state, tags, args) => { + const tgs = tags.data + const key = args[0] ?? "name" + let subject = tgs[key] ?? args[1] + if (subject === undefined || subject === "") { + return Translations.t.reviews.name_required } + const mangrove = MangroveReviews.Get( + Number(tgs._lon), + Number(tgs._lat), + encodeURIComponent(subject), + state.mangroveIdentity, + state.featureSwitchIsTesting.data + ) + const form = new ReviewForm( + (r, whenDone) => mangrove.AddReview(r, whenDone), + state.osmConnection + ) + return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form) }, - { - funcName: "opening_hours_table", - docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", - args: [{ + }, + { + funcName: "opening_hours_table", + docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", + args: [ + { name: "key", defaultValue: "opening_hours", - doc: "The tagkey from which the table is constructed." - }, { + doc: "The tagkey from which the table is constructed.", + }, + { name: "prefix", defaultValue: "", - doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__" - }, { + doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__", + }, + { name: "postfix", defaultValue: "", - doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__" - }], - 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<any>, args) => { - return new OpeningHoursVisualization(tagSource, state, args[0], args[1], args[2]) - } + doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__", + }, + ], + 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<any>, args) => { + return new OpeningHoursVisualization( + tagSource, + state, + args[0], + args[1], + args[2] + ) }, - { - 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: [{ + }, + { + 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 - }, { + required: true, + }, + { name: "Shorthands", - doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;" - }, { + 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: (state, tagSource: UIEventSource<any>, 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...")); - } + doc: "The path (or shorthand) that should be returned", + }, + ], + constr: (state, tagSource: UIEventSource<any>, 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: "histogram", - docs: "Create a histogram for a list of given values, read from the properties.", - example: "`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram", - args: [ - { - name: "key", - doc: "The key to be read and to generate a histogram from", - required: true - }, - { - name: "title", - doc: "This text will be placed above the texts (in the first column of the visulasition)", - defaultValue: "" - }, - { - name: "countHeader", - doc: "This text will be placed above the bars", - defaultValue: "" - }, - { - name: "colors*", - doc: "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`" - - } - ], - constr: (state, tagSource: UIEventSource<any>, args: string[]) => { - - let assignColors = undefined; - if (args.length >= 3) { - const colors = [...args] - colors.splice(0, 3) - const mapping = colors.map(c => { - const splitted = c.split(":"); - const value = splitted.pop() - const regex = splitted.join(":") - return {regex: "^" + regex + "$", color: value} - }) - assignColors = (key) => { - for (const kv of mapping) { - if (key.match(kv.regex) !== null) { - return kv.color - } + }, + { + funcName: "histogram", + docs: "Create a histogram for a list of given values, read from the properties.", + example: + "`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram", + args: [ + { + name: "key", + doc: "The key to be read and to generate a histogram from", + required: true, + }, + { + name: "title", + doc: "This text will be placed above the texts (in the first column of the visulasition)", + defaultValue: "", + }, + { + name: "countHeader", + doc: "This text will be placed above the bars", + defaultValue: "", + }, + { + name: "colors*", + doc: "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`", + }, + ], + constr: (state, tagSource: UIEventSource<any>, args: string[]) => { + let assignColors = undefined + if (args.length >= 3) { + const colors = [...args] + colors.splice(0, 3) + const mapping = colors.map((c) => { + const splitted = c.split(":") + const value = splitted.pop() + const regex = splitted.join(":") + return { regex: "^" + regex + "$", color: value } + }) + assignColors = (key) => { + for (const kv of mapping) { + if (key.match(kv.regex) !== null) { + return kv.color } + } + return undefined + } + } + + const listSource: Store<string[]> = tagSource.map((tags) => { + try { + const value = tags[args[0]] + if (value === "" || value === undefined) { return undefined } + return JSON.parse(value) + } catch (e) { + console.error( + "Could not load histogram: parsing of the list failed: ", + e + ) + return undefined } - - const listSource: Store<string[]> = tagSource - .map(tags => { - try { - const value = tags[args[0]] - if (value === "" || value === undefined) { - return undefined - } - return JSON.parse(value) - } catch (e) { - console.error("Could not load histogram: parsing of the list failed: ", e) - return undefined; - } - }) - return new Histogram(listSource, args[1], args[2], {assignColor: assignColors}) - } + }) + return new Histogram(listSource, args[1], args[2], { + assignColor: assignColors, + }) }, - { - funcName: "share_link", - docs: "Creates a link that (attempts to) open the native 'share'-screen", - example: "{share_link()} to share the current page, {share_link(<some_url>)} to share the given url", - args: [ - { - name: "url", - doc: "The url to share (default: current URL)", - } - ], - constr: (state, tagSource: UIEventSource<any>, args) => { - if (window.navigator.share) { + }, + { + funcName: "share_link", + docs: "Creates a link that (attempts to) open the native 'share'-screen", + example: + "{share_link()} to share the current page, {share_link(<some_url>)} to share the given url", + args: [ + { + name: "url", + doc: "The url to share (default: current URL)", + }, + ], + constr: (state, tagSource: UIEventSource<any>, args) => { + if (window.navigator.share) { + const generateShareData = () => { + const title = state?.layoutToUse?.title?.txt ?? "MapComplete" - const generateShareData = () => { - - - const title = state?.layoutToUse?.title?.txt ?? "MapComplete"; - - let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer(tagSource?.data); - let name = matchingLayer?.title?.GetRenderValue(tagSource.data)?.txt ?? tagSource.data?.name ?? "POI"; - if (name) { - name = `${name} (${title})` - } else { - name = title; - } - let url = args[0] ?? "" - if (url === "") { - url = window.location.href - } - return { - title: name, - url: url, - text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete" - } + let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer( + tagSource?.data + ) + let name = + matchingLayer?.title?.GetRenderValue(tagSource.data)?.txt ?? + tagSource.data?.name ?? + "POI" + if (name) { + name = `${name} (${title})` + } else { + name = title + } + let url = args[0] ?? "" + if (url === "") { + url = window.location.href + } + return { + title: name, + url: url, + text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete", } - - return new ShareButton(Svg.share_svg().SetClass("w-8 h-8"), generateShareData) - } else { - return new FixedUiElement("") } + return new ShareButton( + Svg.share_svg().SetClass("w-8 h-8"), + generateShareData + ) + } else { + return new FixedUiElement("") } }, - { - funcName: "canonical", - 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), ...", - args: [{ + }, + { + funcName: "canonical", + 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), ...", + args: [ + { name: "key", doc: "The key of the tag to give the canonical text for", - required: true - }], - constr: (state, tagSource, args) => { - const key = args [0] - return new VariableUiElement( - tagSource.map(tags => tags[key]).map(value => { + required: true, + }, + ], + constr: (state, tagSource, args) => { + const key = args[0] + return new VariableUiElement( + tagSource + .map((tags) => tags[key]) + .map((value) => { if (value === undefined) { return undefined } - const allUnits = [].concat(...(state?.layoutToUse?.layers?.map(lyr => lyr.units) ?? [])) - const unit = allUnits.filter(unit => unit.isApplicableToKey(key))[0] + const allUnits = [].concat( + ...(state?.layoutToUse?.layers?.map((lyr) => lyr.units) ?? []) + ) + const unit = allUnits.filter((unit) => + unit.isApplicableToKey(key) + )[0] if (unit === undefined) { - return value; + return value } - return unit.asHumanLongValue(value); - + return unit.asHumanLongValue(value) }) - ) - } + ) }, - new ImportPointButton(), - new ImportWayButton(), - new ConflateButton(), - { - funcName: "multi_apply", - 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: [ - {name: "feature_ids", doc: "A JSON-serialized list of IDs of features to apply the tagging on"}, - { - name: "keys", - doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.", - required: true - }, - {name: "text", doc: "The text to show on the button"}, - { - name: "autoapply", - doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown", - required: true - }, - { - name: "overwrite", - doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change", - required: true - } - ], - example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}", - constr: (state, tagsSource, args) => { - const featureIdsKey = args[0] - const keysToApply = args[1].split(";") - const text = args[2] - const autoapply = args[3]?.toLowerCase() === "true" - const overwrite = args[4]?.toLowerCase() === "true" - const featureIds: Store<string[]> = tagsSource.map(tags => { - const ids = tags[featureIdsKey] - try { - if (ids === undefined) { - return [] - } - return JSON.parse(ids); - } catch (e) { - console.warn("Could not parse ", ids, "as JSON to extract IDS which should be shown on the map.") + }, + new ImportPointButton(), + new ImportWayButton(), + new ConflateButton(), + { + funcName: "multi_apply", + 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: [ + { + name: "feature_ids", + doc: "A JSON-serialized list of IDs of features to apply the tagging on", + }, + { + name: "keys", + doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.", + required: true, + }, + { name: "text", doc: "The text to show on the button" }, + { + name: "autoapply", + doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown", + required: true, + }, + { + name: "overwrite", + doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change", + required: true, + }, + ], + example: + "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}", + constr: (state, tagsSource, args) => { + const featureIdsKey = args[0] + const keysToApply = args[1].split(";") + const text = args[2] + const autoapply = args[3]?.toLowerCase() === "true" + const overwrite = args[4]?.toLowerCase() === "true" + const featureIds: Store<string[]> = tagsSource.map((tags) => { + const ids = tags[featureIdsKey] + try { + if (ids === undefined) { return [] } - }) - return new MultiApply( - { - featureIds, - keysToApply, - text, - autoapply, - overwrite, - tagsSource, - state - } - ); - - } - }, - new TagApplyButton(), - { - funcName: "export_as_gpx", - docs: "Exports the selected feature as GPX-file", - args: [], - constr: (state, tagSource) => { - const t = Translations.t.general.download; - - return new SubtleButton(Svg.download_ui(), - new Combine([t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), - t.downloadGpxHelper.SetClass("subtle")]).SetClass("flex flex-col") - ).onClick(() => { - console.log("Exporting as GPX!") - const tags = tagSource.data - const feature = state.allElements.ContainingFeatures.get(tags.id) - const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) - const gpx = GeoOperations.AsGpx(feature, matchingLayer) - const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" - Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { - mimetype: "{gpx=application/gpx+xml}" - }) - - - }) - } - }, - { - funcName: "export_as_geojson", - docs: "Exports the selected feature as GeoJson-file", - args: [], - constr: (state, tagSource) => { - const t = Translations.t.general.download; - - return new SubtleButton(Svg.download_ui(), - new Combine([t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), - t.downloadGeoJsonHelper.SetClass("subtle")]).SetClass("flex flex-col") - ).onClick(() => { - console.log("Exporting as Geojson") - const tags = tagSource.data - const feature = state.allElements.ContainingFeatures.get(tags.id) - const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) - const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" - const data = JSON.stringify(feature, null, " "); - Utils.offerContentsAsDownloadableFile(data, title + "_mapcomplete_export.geojson", { - mimetype: "application/vnd.geo+json" - }) - - - }) - } - }, - { - funcName: "open_in_iD", - docs: "Opens the current view in the iD-editor", - args: [], - constr: (state, feature) => { - return new OpenIdEditor(state, undefined, feature.data.id) - } - }, - { - funcName: "open_in_josm", - docs: "Opens the current view in the JOSM-editor", - args: [], - constr: (state, feature) => { - return new OpenJosm(state) - } - }, - - { - funcName: "clear_location_history", - docs: "A button to remove the travelled track information from the device", - args: [], - constr: state => { - return new SubtleButton( - Svg.delete_icon_svg().SetStyle("height: 1.5rem"), Translations.t.general.removeLocationHistory - ).onClick(() => { - state.historicalUserLocations.features.setData([]) - Hash.hash.setData(undefined) - }) - } - }, - new CloseNoteButton(), - { - funcName: "add_note_comment", - docs: "A textfield to add a comment to a node (with the option to close the note).", - args: [ - { - name: "Id-key", - doc: "The property name where the ID of the note to close can be found", - defaultValue: "id" + return JSON.parse(ids) + } catch (e) { + console.warn( + "Could not parse ", + ids, + "as JSON to extract IDS which should be shown on the map." + ) + return [] } - ], - constr: (state, tags, args) => { + }) + return new MultiApply({ + featureIds, + keysToApply, + text, + autoapply, + overwrite, + tagsSource, + state, + }) + }, + }, + new TagApplyButton(), + { + funcName: "export_as_gpx", + docs: "Exports the selected feature as GPX-file", + args: [], + constr: (state, tagSource) => { + const t = Translations.t.general.download - const t = Translations.t.notes; - const textField = new TextField( + return new SubtleButton( + Svg.download_ui(), + new Combine([ + t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), + t.downloadGpxHelper.SetClass("subtle"), + ]).SetClass("flex flex-col") + ).onClick(() => { + console.log("Exporting as GPX!") + const tags = tagSource.data + const feature = state.allElements.ContainingFeatures.get(tags.id) + const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) + const gpx = GeoOperations.AsGpx(feature, matchingLayer) + const title = + matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? + "gpx_track" + Utils.offerContentsAsDownloadableFile( + gpx, + title + "_mapcomplete_export.gpx", { - placeholder: t.addCommentPlaceholder, - inputStyle: "width: 100%; height: 6rem;", - textAreaRows: 3, - htmlType: "area" + mimetype: "{gpx=application/gpx+xml}", } ) - textField.SetClass("rounded-l border border-grey") - const txt = textField.GetValue() - - const addCommentButton = new SubtleButton(Svg.speech_bubble_svg().SetClass("max-h-7"), t.addCommentPlaceholder) - .onClick(async () => { - const id = tags.data[args[1] ?? "id"] - - if ((txt.data ?? "") == "") { - return; - } - - if (isClosed.data) { - await state.osmConnection.reopenNote(id, txt.data) - await state.osmConnection.closeNote(id) - } else { - await state.osmConnection.addCommentToNote(id, txt.data) - } - NoteCommentElement.addCommentTo(txt.data, tags, state) - txt.setData("") - - }) - - - const close = new SubtleButton(Svg.resolved_svg().SetClass("max-h-7"), new VariableUiElement(txt.map(txt => { - if (txt === undefined || txt === "") { - return t.closeNote - } - return t.addCommentAndClose - }))).onClick(() => { - const id = tags.data[args[1] ?? "id"] - state.osmConnection.closeNote(id, txt.data).then(_ => { - tags.data["closed_at"] = new Date().toISOString(); - tags.ping() - }) - }) - - const reopen = new SubtleButton(Svg.note_svg().SetClass("max-h-7"), new VariableUiElement(txt.map(txt => { - if (txt === undefined || txt === "") { - return t.reopenNote - } - return t.reopenNoteAndComment - }))).onClick(() => { - const id = tags.data[args[1] ?? "id"] - state.osmConnection.reopenNote(id, txt.data).then(_ => { - tags.data["closed_at"] = undefined; - tags.ping() - }) - }) - - const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== ""); - const stateButtons = new Toggle(new Toggle(reopen, close, isClosed), undefined, state.osmConnection.isLoggedIn) - - return new LoginToggle( - new Combine([ - new Title(t.addAComment), - textField, - new Combine([ - stateButtons.SetClass("sm:mr-2"), - new Toggle(addCommentButton, - new Combine([t.typeText]).SetClass("flex items-center h-full subtle"), - textField.GetValue().map(t => t !== undefined && t.length >= 1)).SetClass("sm:mr-2") - ]).SetClass("sm:flex sm:justify-between sm:items-stretch") - ]).SetClass("border-2 border-black rounded-xl p-4 block"), - t.loginToAddComment, state) - } + }) }, - { - funcName: "visualize_note_comments", - docs: "Visualises the comments for notes", - args: [ - { - name: "commentsKey", - doc: "The property name of the comments, which should be stringified json", - defaultValue: "comments" - }, - { - name: "start", - doc: "Drop the first 'start' comments", - defaultValue: "0" - } - ] - , constr: (state, tags, args) => - new VariableUiElement( - tags.map(tags => tags[args[0]]) - .map(commentsStr => { - const comments: any[] = JSON.parse(commentsStr) - const startLoc = Number(args[1] ?? 0) - if (!isNaN(startLoc) && startLoc > 0) { - comments.splice(0, startLoc) - } - return new Combine(comments - .filter(c => c.text !== "") - .map(c => new NoteCommentElement(c))).SetClass("flex flex-col") - }) + }, + { + funcName: "export_as_geojson", + docs: "Exports the selected feature as GeoJson-file", + args: [], + constr: (state, tagSource) => { + const t = Translations.t.general.download + + return new SubtleButton( + Svg.download_ui(), + new Combine([ + t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), + t.downloadGeoJsonHelper.SetClass("subtle"), + ]).SetClass("flex flex-col") + ).onClick(() => { + console.log("Exporting as Geojson") + const tags = tagSource.data + const feature = state.allElements.ContainingFeatures.get(tags.id) + const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) + const title = + matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" + const data = JSON.stringify(feature, null, " ") + Utils.offerContentsAsDownloadableFile( + data, + title + "_mapcomplete_export.geojson", + { + mimetype: "application/vnd.geo+json", + } ) + }) }, - { - funcName: "add_image_to_note", - docs: "Adds an image to a node", - args: [{ + }, + { + funcName: "open_in_iD", + docs: "Opens the current view in the iD-editor", + args: [], + constr: (state, feature) => { + return new OpenIdEditor(state, undefined, feature.data.id) + }, + }, + { + funcName: "open_in_josm", + docs: "Opens the current view in the JOSM-editor", + args: [], + constr: (state, feature) => { + return new OpenJosm(state) + }, + }, + + { + funcName: "clear_location_history", + docs: "A button to remove the travelled track information from the device", + args: [], + constr: (state) => { + return new SubtleButton( + Svg.delete_icon_svg().SetStyle("height: 1.5rem"), + Translations.t.general.removeLocationHistory + ).onClick(() => { + state.historicalUserLocations.features.setData([]) + Hash.hash.setData(undefined) + }) + }, + }, + new CloseNoteButton(), + { + funcName: "add_note_comment", + docs: "A textfield to add a comment to a node (with the option to close the note).", + args: [ + { name: "Id-key", doc: "The property name where the ID of the note to close can be found", - defaultValue: "id" - }], - constr: (state, tags, args) => { - const isUploading = new UIEventSource(false); - const t = Translations.t.notes; - const id = tags.data[args[0] ?? "id"] + defaultValue: "id", + }, + ], + constr: (state, tags, args) => { + const t = Translations.t.notes + const textField = new TextField({ + placeholder: t.addCommentPlaceholder, + inputStyle: "width: 100%; height: 6rem;", + textAreaRows: 3, + htmlType: "area", + }) + textField.SetClass("rounded-l border border-grey") + const txt = textField.GetValue() - const uploader = new ImgurUploader(async url => { - isUploading.setData(false) - await state.osmConnection.addCommentToNote(id, url) - NoteCommentElement.addCommentTo(url, tags, state) + const addCommentButton = new SubtleButton( + Svg.speech_bubble_svg().SetClass("max-h-7"), + t.addCommentPlaceholder + ).onClick(async () => { + const id = tags.data[args[1] ?? "id"] + if ((txt.data ?? "") == "") { + return + } + + if (isClosed.data) { + await state.osmConnection.reopenNote(id, txt.data) + await state.osmConnection.closeNote(id) + } else { + await state.osmConnection.addCommentToNote(id, txt.data) + } + NoteCommentElement.addCommentTo(txt.data, tags, state) + txt.setData("") + }) + + const close = new SubtleButton( + Svg.resolved_svg().SetClass("max-h-7"), + new VariableUiElement( + txt.map((txt) => { + if (txt === undefined || txt === "") { + return t.closeNote + } + return t.addCommentAndClose + }) + ) + ).onClick(() => { + const id = tags.data[args[1] ?? "id"] + state.osmConnection.closeNote(id, txt.data).then((_) => { + tags.data["closed_at"] = new Date().toISOString() + tags.ping() }) + }) - const label = new Combine([ - Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "), - Translations.t.image.addPicture - ]).SetClass("p-2 border-4 border-black rounded-full font-bold h-full align-center w-full flex justify-center") - - const fileSelector = new FileSelectorButton(label) - fileSelector.GetValue().addCallback(filelist => { - isUploading.setData(true) - uploader.uploadMany("Image for osm.org/note/" + id, "CC0", filelist) - + const reopen = new SubtleButton( + Svg.note_svg().SetClass("max-h-7"), + new VariableUiElement( + txt.map((txt) => { + if (txt === undefined || txt === "") { + return t.reopenNote + } + return t.reopenNoteAndComment + }) + ) + ).onClick(() => { + const id = tags.data[args[1] ?? "id"] + state.osmConnection.reopenNote(id, txt.data).then((_) => { + tags.data["closed_at"] = undefined + tags.ping() }) - const ti = Translations.t.image - const uploadPanel = new Combine([ - fileSelector, - new Combine([ti.willBePublished, ti.cco]), - ti.ccoExplanation.SetClass("subtle text-sm"), - ti.respectPrivacy.SetClass("text-sm") - ]).SetClass("flex flex-col") - return new LoginToggle(new Toggle( + }) + + const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") + const stateButtons = new Toggle( + new Toggle(reopen, close, isClosed), + undefined, + state.osmConnection.isLoggedIn + ) + + return new LoginToggle( + new Combine([ + new Title(t.addAComment), + textField, + new Combine([ + stateButtons.SetClass("sm:mr-2"), + new Toggle( + addCommentButton, + new Combine([t.typeText]).SetClass( + "flex items-center h-full subtle" + ), + textField + .GetValue() + .map((t) => t !== undefined && t.length >= 1) + ).SetClass("sm:mr-2"), + ]).SetClass("sm:flex sm:justify-between sm:items-stretch"), + ]).SetClass("border-2 border-black rounded-xl p-4 block"), + t.loginToAddComment, + state + ) + }, + }, + { + funcName: "visualize_note_comments", + docs: "Visualises the comments for notes", + args: [ + { + name: "commentsKey", + doc: "The property name of the comments, which should be stringified json", + defaultValue: "comments", + }, + { + name: "start", + doc: "Drop the first 'start' comments", + defaultValue: "0", + }, + ], + constr: (state, tags, args) => + new VariableUiElement( + tags + .map((tags) => tags[args[0]]) + .map((commentsStr) => { + const comments: any[] = JSON.parse(commentsStr) + const startLoc = Number(args[1] ?? 0) + if (!isNaN(startLoc) && startLoc > 0) { + comments.splice(0, startLoc) + } + return new Combine( + comments + .filter((c) => c.text !== "") + .map((c) => new NoteCommentElement(c)) + ).SetClass("flex flex-col") + }) + ), + }, + { + funcName: "add_image_to_note", + docs: "Adds an image to a node", + args: [ + { + name: "Id-key", + doc: "The property name where the ID of the note to close can be found", + defaultValue: "id", + }, + ], + constr: (state, tags, args) => { + const isUploading = new UIEventSource(false) + const t = Translations.t.notes + const id = tags.data[args[0] ?? "id"] + + const uploader = new ImgurUploader(async (url) => { + isUploading.setData(false) + await state.osmConnection.addCommentToNote(id, url) + NoteCommentElement.addCommentTo(url, tags, state) + }) + + const label = new Combine([ + Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "), + Translations.t.image.addPicture, + ]).SetClass( + "p-2 border-4 border-black rounded-full font-bold h-full align-center w-full flex justify-center" + ) + + const fileSelector = new FileSelectorButton(label) + fileSelector.GetValue().addCallback((filelist) => { + isUploading.setData(true) + uploader.uploadMany("Image for osm.org/note/" + id, "CC0", filelist) + }) + const ti = Translations.t.image + const uploadPanel = new Combine([ + fileSelector, + new Combine([ti.willBePublished, ti.cco]), + ti.ccoExplanation.SetClass("subtle text-sm"), + ti.respectPrivacy.SetClass("text-sm"), + ]).SetClass("flex flex-col") + return new LoginToggle( + new Toggle( Translations.t.image.uploadingPicture.SetClass("alert"), uploadPanel, - isUploading), t.loginToAddPicture, state) - } - + isUploading + ), + t.loginToAddPicture, + state + ) }, - { - funcName: "title", - args: [], - 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`.", - constr: (state, tagsSource) => - new VariableUiElement(tagsSource.map(tags => { + }, + { + funcName: "title", + args: [], + 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`.", + constr: (state, tagsSource) => + new VariableUiElement( + tagsSource.map((tags) => { const layer = state.layoutToUse.getMatchingLayer(tags) const title = layer?.title?.GetRenderValue(tags) if (title === undefined) { return undefined } return new SubstitutedTranslation(title, tagsSource, state) - })) - }, - new NearbyImageVis(), - new MapillaryLinkVis(), - { - funcName: "maproulette_task", - args: [], - constr(state, tagSource, argument, guistate) { - let parentId = tagSource.data.mr_challengeId; - let challenge = Stores.FromPromise(Utils.downloadJsonCached(`https://maproulette.org/api/v2/challenge/${parentId}`, 24 * 60 * 60 * 1000)); + }) + ), + }, + new NearbyImageVis(), + new MapillaryLinkVis(), + { + funcName: "maproulette_task", + args: [], + constr(state, tagSource, argument, guistate) { + let parentId = tagSource.data.mr_challengeId + let challenge = Stores.FromPromise( + Utils.downloadJsonCached( + `https://maproulette.org/api/v2/challenge/${parentId}`, + 24 * 60 * 60 * 1000 + ) + ) - let details = new VariableUiElement(challenge.map(challenge => { - let listItems: BaseUIElement[] = []; - let title: BaseUIElement; + let details = new VariableUiElement( + challenge.map((challenge) => { + let listItems: BaseUIElement[] = [] + let title: BaseUIElement if (challenge?.name) { - title = new Title(challenge.name); + title = new Title(challenge.name) } if (challenge?.description) { - listItems.push(new FixedUiElement(challenge.description)); + listItems.push(new FixedUiElement(challenge.description)) } if (challenge?.instruction) { - listItems.push(new FixedUiElement(challenge.instruction)); + listItems.push(new FixedUiElement(challenge.instruction)) } if (listItems.length === 0) { - return undefined; + return undefined } else { - return [title, new List(listItems)]; - } - })) - return details; - }, - docs: "Show details of a MapRoulette task" - }, - { - funcName: "statistics", - docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", - args: [], - constr: (state, tagsSource, args, guiState) => { - const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]); - - function update() { - const mapCenter = <[number, number]>[state.locationControl.data.lon, state.locationControl.data.lon] - const bbox = state.currentBounds.data - const elements = state.featurePipeline.getAllVisibleElementsWithmeta(bbox).map(el => { - const distance = GeoOperations.distanceBetween(el.center, mapCenter) - return {...el, distance} - }) - elements.sort((e0, e1) => e0.distance - e1.distance) - elementsInview.setData(elements) - - } - - state.currentBounds.addCallbackAndRun(update) - state.featurePipeline.newDataLoadedSignal.addCallback(update); - state.filteredLayers.addCallbackAndRun(fls => { - for (const fl of fls) { - fl.isDisplayed.addCallback(update) - fl.appliedFilters.addCallback(update) + return [title, new List(listItems)] } }) - return new StatisticsPanel(elementsInview, state) - } + ) + return details }, - { - funcName: "send_email", - docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email", - args: [ + docs: "Show details of a MapRoulette task", + }, + { + funcName: "statistics", + docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", + args: [], + constr: (state, tagsSource, args, guiState) => { + const elementsInview = new UIEventSource< { - name: "to", - doc: "Who to send the email to?", - required: true - }, - { - name: "subject", - doc: "The subject of the email", - required: true - }, - { - name: "body", - doc: "The text in the email", - required: true - }, + distance: number + center: [number, number] + element: OsmFeature + layer: LayerConfig + }[] + >([]) - { - name: "button_text", - doc: "The text shown on the button in the UI", - required: true - } - ], - constr(state, tags, args) { - return new VariableUiElement(tags.map(tags => { - - const [to, subject, body, button_text] = args.map(str => Utils.SubstituteKeys(str, tags)) - const url = "mailto:" + to + "?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body) - return new SubtleButton(Svg.envelope_svg(), button_text, { - url + function update() { + const mapCenter = <[number, number]>[ + state.locationControl.data.lon, + state.locationControl.data.lon, + ] + const bbox = state.currentBounds.data + const elements = state.featurePipeline + .getAllVisibleElementsWithmeta(bbox) + .map((el) => { + const distance = GeoOperations.distanceBetween(el.center, mapCenter) + return { ...el, distance } }) - - })) + elements.sort((e0, e1) => e0.distance - e1.distance) + elementsInview.setData(elements) } + + state.currentBounds.addCallbackAndRun(update) + state.featurePipeline.newDataLoadedSignal.addCallback(update) + state.filteredLayers.addCallbackAndRun((fls) => { + for (const fl of fls) { + fl.isDisplayed.addCallback(update) + fl.appliedFilters.addCallback(update) + } + }) + return new StatisticsPanel(elementsInview, state) }, - { - 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", - example: "```json\n" + JSON.stringify({ - render: { - special: { - type: "multi", - key: "_doors_from_building_properties", - tagRendering: { - render: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}" - } - } - } - }, null, " ") + "```", - args: [ + }, + { + funcName: "send_email", + docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email", + args: [ + { + name: "to", + doc: "Who to send the email to?", + required: true, + }, + { + name: "subject", + doc: "The subject of the email", + required: true, + }, + { + name: "body", + doc: "The text in the email", + required: true, + }, + + { + name: "button_text", + doc: "The text shown on the button in the UI", + required: true, + }, + ], + constr(state, tags, args) { + return new VariableUiElement( + tags.map((tags) => { + const [to, subject, body, button_text] = args.map((str) => + Utils.SubstituteKeys(str, tags) + ) + const url = + "mailto:" + + to + + "?subject=" + + encodeURIComponent(subject) + + "&body=" + + encodeURIComponent(body) + return new SubtleButton(Svg.envelope_svg(), button_text, { + url, + }) + }) + ) + }, + }, + { + 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", + example: + "```json\n" + + JSON.stringify( { - name: "key", - doc: "The property to read and to interpret as a list of properties", - required: true + render: { + special: { + type: "multi", + key: "_doors_from_building_properties", + tagRendering: { + render: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}", + }, + }, + }, }, - { - name: "tagrendering", - doc: "An entire tagRenderingConfig", - required: true - } - ] - , - constr(state, featureTags, args) { - const [key, tr] = args - const translation = new Translation({"*": tr}) - return new VariableUiElement(featureTags.map(tags => { + null, + " " + ) + + "```", + args: [ + { + name: "key", + doc: "The property to read and to interpret as a list of properties", + required: true, + }, + { + name: "tagrendering", + doc: "An entire tagRenderingConfig", + required: true, + }, + ], + constr(state, featureTags, args) { + const [key, tr] = args + const translation = new Translation({ "*": tr }) + return new VariableUiElement( + featureTags.map((tags) => { const properties: object[] = JSON.parse(tags[key]) const elements = [] for (const property of properties) { - const subsTr = new SubstitutedTranslation(translation, new UIEventSource<any>(property), state) + const subsTr = new SubstitutedTranslation( + translation, + new UIEventSource<any>(property), + state + ) elements.push(subsTr) } return new List(elements) - })) - } + }) + ) }, - { - funcName: "steal", - docs: "Shows a tagRendering from a different object as if this was the object itself", - args: [{ + }, + { + funcName: "steal", + docs: "Shows a tagRendering from a different object as if this was the object itself", + args: [ + { name: "featureId", doc: "The key of the attribute which contains the id of the feature from which to use the tags", - required: true + required: true, }, - { - name: "tagRenderingId", - doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection", - required: true - }], - constr(state, featureTags, args) { - const [featureIdKey, layerAndtagRenderingIds] = args - const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] - for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { - const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") - const layer = state.layoutToUse.layers.find(l => l.id === layerId) - const tagRendering = layer.tagRenderings.find(tr => tr.id === tagRenderingId) - tagRenderings.push([layer, tagRendering]) - } - if(tagRenderings.length === 0){ - throw "Could not create stolen tagrenddering: tagRenderings not found" - } - return new VariableUiElement(featureTags.map(tags => { + { + name: "tagRenderingId", + doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection", + required: true, + }, + ], + constr(state, featureTags, args) { + const [featureIdKey, layerAndtagRenderingIds] = args + const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] + for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { + const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") + const layer = state.layoutToUse.layers.find((l) => l.id === layerId) + const tagRendering = layer.tagRenderings.find( + (tr) => tr.id === tagRenderingId + ) + tagRenderings.push([layer, tagRendering]) + } + if (tagRenderings.length === 0) { + throw "Could not create stolen tagrenddering: tagRenderings not found" + } + return new VariableUiElement( + featureTags.map((tags) => { const featureId = tags[featureIdKey] if (featureId === undefined) { - return undefined; + return undefined } const otherTags = state.allElements.getEventSourceById(featureId) const elements: BaseUIElement[] = [] for (const [layer, tagRendering] of tagRenderings) { - const el = new EditableTagRendering(otherTags, tagRendering, layer.units, state, {}) + const el = new EditableTagRendering( + otherTags, + tagRendering, + layer.units, + state, + {} + ) elements.push(el) } if (elements.length === 1) { return elements[0] } - return new Combine(elements).SetClass("flex flex-col"); - })) - }, - - getLayerDependencies(args): string[] { - const [_, tagRenderingId] = args - if (tagRenderingId.indexOf(".") < 0) { - throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot" - } - const [layerId, __] = tagRenderingId.split(".") - return [layerId] - } + return new Combine(elements).SetClass("flex flex-col") + }) + ) }, - { - funcName: "plantnet_detection", - 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`). ", - args: [{ + getLayerDependencies(args): string[] { + const [_, tagRenderingId] = args + if (tagRenderingId.indexOf(".") < 0) { + throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot" + } + const [layerId, __] = tagRenderingId.split(".") + return [layerId] + }, + }, + { + funcName: "plantnet_detection", + + 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`). ", + args: [ + { name: "image_key", defaultValue: AllImageProviders.defaultKeys.join(","), - doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated " - }], - constr: (state, tags, args) => { - let imagePrefixes: string[] = undefined; - if (args.length > 0) { - imagePrefixes = [].concat(...args.map(a => a.split(","))); - } + doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ", + }, + ], + constr: (state, tags, args) => { + let imagePrefixes: string[] = undefined + if (args.length > 0) { + imagePrefixes = [].concat(...args.map((a) => a.split(","))) + } - const detect = new UIEventSource(false) - const toggle = new Toggle( - new Lazy(() => { - const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor(tags, imagePrefixes) - const allImages: Store<string[]> = allProvidedImages.map(pi => pi.map(pi => pi.url)) - return new PlantNetSpeciesSearch(allImages, async selectedWikidata => { + const detect = new UIEventSource(false) + const toggle = new Toggle( + new Lazy(() => { + const allProvidedImages: Store<ProvidedImage[]> = + AllImageProviders.LoadImagesFor(tags, imagePrefixes) + const allImages: Store<string[]> = allProvidedImages.map((pi) => + pi.map((pi) => pi.url) + ) + return new PlantNetSpeciesSearch( + allImages, + async (selectedWikidata) => { selectedWikidata = Wikidata.ExtractKey(selectedWikidata) - const change = new ChangeTagAction(tags.data.id, - new And([new Tag("species:wikidata", selectedWikidata), - new Tag("source:species:wikidata","PlantNet.org AI") + const change = new ChangeTagAction( + tags.data.id, + new And([ + new Tag("species:wikidata", selectedWikidata), + new Tag("source:species:wikidata", "PlantNet.org AI"), ]), tags.data, { theme: state.layoutToUse.id, - changeType: "plantnet-ai-detection" + changeType: "plantnet-ai-detection", } - ) - await state.changes.applyAction(change) - }) - }), - new SubtleButton(undefined, "Detect plant species with plantnet.org").onClick(() => detect.setData(true)), - detect - ) - - return new Combine([ - toggle, - new Combine([Svg.plantnet_logo_svg().SetClass("w-10 h-10 p-1 mr-1 bg-white rounded-full"), - Translations.t.plantDetection.poweredByPlantnet]) - .SetClass("flex p-2 bg-gray-200 rounded-xl self-end") - ]).SetClass("flex flex-col") - } - } - ] + ) + await state.changes.applyAction(change) + } + ) + }), + new SubtleButton( + undefined, + "Detect plant species with plantnet.org" + ).onClick(() => detect.setData(true)), + detect + ) + + return new Combine([ + toggle, + new Combine([ + Svg.plantnet_logo_svg().SetClass( + "w-10 h-10 p-1 mr-1 bg-white rounded-full" + ), + Translations.t.plantDetection.poweredByPlantnet, + ]).SetClass("flex p-2 bg-gray-200 rounded-xl self-end"), + ]).SetClass("flex flex-col") + }, + }, + ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) - return specialVisualizations; + return specialVisualizations } - -} \ No newline at end of file +} diff --git a/UI/StatisticsGUI.ts b/UI/StatisticsGUI.ts index 2ddeaaf3b..4206368f5 100644 --- a/UI/StatisticsGUI.ts +++ b/UI/StatisticsGUI.ts @@ -1,32 +1,35 @@ /** * The statistics-gui shows statistics from previous MapComplete-edits */ -import {UIEventSource} from "../Logic/UIEventSource"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import Loading from "./Base/Loading"; -import {Utils} from "../Utils"; -import Combine from "./Base/Combine"; -import {StackedRenderingChart} from "./BigComponents/TagRenderingChart"; -import {LayerFilterPanel} from "./BigComponents/FilterView"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import MapState from "../Logic/State/MapState"; -import BaseUIElement from "./BaseUIElement"; -import Title from "./Base/Title"; -import {FixedUiElement} from "./Base/FixedUiElement"; +import { UIEventSource } from "../Logic/UIEventSource" +import { VariableUiElement } from "./Base/VariableUIElement" +import Loading from "./Base/Loading" +import { Utils } from "../Utils" +import Combine from "./Base/Combine" +import { StackedRenderingChart } from "./BigComponents/TagRenderingChart" +import { LayerFilterPanel } from "./BigComponents/FilterView" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import MapState from "../Logic/State/MapState" +import BaseUIElement from "./BaseUIElement" +import Title from "./Base/Title" +import { FixedUiElement } from "./Base/FixedUiElement" -class StatisticsForOverviewFile extends Combine{ +class StatisticsForOverviewFile extends Combine { constructor(homeUrl: string, paths: string[]) { const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0] - const filteredLayer = MapState.InitializeFilteredLayers({id: "statistics-view", layers: [layer]}, undefined)[0] - const filterPanel = new LayerFilterPanel(undefined, filteredLayer) + const filteredLayer = MapState.InitializeFilteredLayers( + { id: "statistics-view", layers: [layer] }, + undefined + )[0] + const filterPanel = new LayerFilterPanel(undefined, filteredLayer) const appliedFilters = filteredLayer.appliedFilters const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([]) for (const filepath of paths) { - Utils.downloadJson(homeUrl + filepath).then(data => { - data?.features?.forEach(item => { - item.properties = {...item.properties, ...item.properties.metadata} + Utils.downloadJson(homeUrl + filepath).then((data) => { + data?.features?.forEach((item) => { + item.properties = { ...item.properties, ...item.properties.metadata } delete item.properties.metadata }) downloaded.data.push(data) @@ -34,125 +37,149 @@ class StatisticsForOverviewFile extends Combine{ }) } - const loading = new Loading( new VariableUiElement( - downloaded.map(dl => "Downloaded " + dl.length + " items out of "+paths.length)) - ); - + const loading = new Loading( + new VariableUiElement( + downloaded.map((dl) => "Downloaded " + dl.length + " items out of " + paths.length) + ) + ) + super([ filterPanel, - new VariableUiElement(downloaded.map(downloaded => { - if(downloaded.length !== paths.length){ - return loading - } - - let overview = ChangesetsOverview.fromDirtyData([].concat(...downloaded.map(d => d.features))) - if (appliedFilters.data.size > 0) { - appliedFilters.data.forEach((filterSpec) => { - const tf = filterSpec?.currentFilter - if (tf === undefined) { - return + new VariableUiElement( + downloaded.map( + (downloaded) => { + if (downloaded.length !== paths.length) { + return loading } - overview = overview.filter(cs => tf.matchesProperties(cs.properties)) - }) - } - if (overview._meta.length === 0) { - return "No data matched the filter" - } - - const dateStrings = Utils.NoNull(overview._meta.map(cs => cs.properties.date)) - const dates : number[] = dateStrings.map(d => new Date(d).getTime()) - const mindate= Math.min(...dates) - const maxdate = Math.max(...dates) - - const diffInDays = (maxdate - mindate) / (1000 * 60 * 60 * 24); - console.log("Diff in days is ", diffInDays, "got", overview._meta.length) - const trs =layer.tagRenderings - .filter(tr => tr.mappings?.length > 0 || tr.freeform?.key !== undefined); - const elements : BaseUIElement[] = [] - for (const tr of trs) { - let total = undefined - if(tr.freeform?.key !== undefined) { - total = new Set( overview._meta.map(f => f.properties[tr.freeform.key])).size - } - - try{ - - elements.push(new Combine([ - new Title(tr.question ?? tr.id).SetClass("p-2") , - total > 1 ? total + " unique value" : undefined, - new StackedRenderingChart(tr, <any>overview._meta, { - period: diffInDays <= 367 ? "day" : "month", - groupToOtherCutoff: total > 50 ? 25 : (total > 10 ? 3 : 0) - - }).SetStyle("width: 100%; height: 600px") - ]).SetClass("block border-2 border-subtle p-2 m-2 rounded-xl" )) - }catch(e){ - console.log("Could not generate a chart", e) - elements.push(new FixedUiElement("No relevant information for "+tr.question.txt)) - } - } - - return new Combine(elements) - }, [appliedFilters])).SetClass("block w-full h-full") + let overview = ChangesetsOverview.fromDirtyData( + [].concat(...downloaded.map((d) => d.features)) + ) + if (appliedFilters.data.size > 0) { + appliedFilters.data.forEach((filterSpec) => { + const tf = filterSpec?.currentFilter + if (tf === undefined) { + return + } + overview = overview.filter((cs) => + tf.matchesProperties(cs.properties) + ) + }) + } + + if (overview._meta.length === 0) { + return "No data matched the filter" + } + + const dateStrings = Utils.NoNull( + overview._meta.map((cs) => cs.properties.date) + ) + const dates: number[] = dateStrings.map((d) => new Date(d).getTime()) + const mindate = Math.min(...dates) + const maxdate = Math.max(...dates) + + const diffInDays = (maxdate - mindate) / (1000 * 60 * 60 * 24) + console.log("Diff in days is ", diffInDays, "got", overview._meta.length) + const trs = layer.tagRenderings.filter( + (tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined + ) + const elements: BaseUIElement[] = [] + for (const tr of trs) { + let total = undefined + if (tr.freeform?.key !== undefined) { + total = new Set( + overview._meta.map((f) => f.properties[tr.freeform.key]) + ).size + } + + try { + elements.push( + new Combine([ + new Title(tr.question ?? tr.id).SetClass("p-2"), + total > 1 ? total + " unique value" : undefined, + new StackedRenderingChart(tr, <any>overview._meta, { + period: diffInDays <= 367 ? "day" : "month", + groupToOtherCutoff: + total > 50 ? 25 : total > 10 ? 3 : 0, + }).SetStyle("width: 100%; height: 600px"), + ]).SetClass("block border-2 border-subtle p-2 m-2 rounded-xl") + ) + } catch (e) { + console.log("Could not generate a chart", e) + elements.push( + new FixedUiElement( + "No relevant information for " + tr.question.txt + ) + ) + } + } + + return new Combine(elements) + }, + [appliedFilters] + ) + ).SetClass("block w-full h-full"), ]) - this.SetClass("block w-full h-full") + this.SetClass("block w-full h-full") } } -export default class StatisticsGUI extends VariableUiElement{ - - private static readonly homeUrl = "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/" +export default class StatisticsGUI extends VariableUiElement { + private static readonly homeUrl = + "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/" private static readonly stats_files = "file-overview.json" -constructor() { - const index = UIEventSource.FromPromise(Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files)) - super(index.map(paths => { - if (paths === undefined) { - return new Loading("Loading overview...") - } - - return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, paths) + constructor() { + const index = UIEventSource.FromPromise( + Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files) + ) + super( + index.map((paths) => { + if (paths === undefined) { + return new Loading("Loading overview...") + } - })) + return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, paths) + }) + ) this.SetClass("block w-full h-full") } } class ChangesetsOverview { - private static readonly theme_remappings = { - "metamap": "maps", - "groen": "buurtnatuur", + metamap: "maps", + groen: "buurtnatuur", "updaten van metadata met mapcomplete": "buurtnatuur", "Toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur", "wiki:mapcomplete/fritures": "fritures", "wiki:MapComplete/Fritures": "fritures", - "lits": "lit", - "pomp": "cyclofix", + lits: "lit", + pomp: "cyclofix", "wiki:user:joost_schouppe/campersite": "campersite", "wiki-user-joost_schouppe-geveltuintjes": "geveltuintjes", "wiki-user-joost_schouppe-campersite": "campersite", "wiki-User-joost_schouppe-campersite": "campersite", "wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes", "wiki:User:joost_schouppe/campersite": "campersite", - "arbres": "arbres_llefia", - "aed_brugge": "aed", + arbres: "arbres_llefia", + aed_brugge: "aed", "https://llefia.org/arbres/mapcomplete.json": "arbres_llefia", "https://llefia.org/arbres/mapcomplete1.json": "arbres_llefia", "toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur", "testing mapcomplete 0.0.0": "buurtnatuur", - "entrances": "indoor", - "https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes" + entrances: "indoor", + "https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": + "geveltuintjes", } - public readonly _meta: ChangeSetData[]; + public readonly _meta: ChangeSetData[] public static fromDirtyData(meta: ChangeSetData[]) { - return new ChangesetsOverview(meta?.map(cs => ChangesetsOverview.cleanChangesetData(cs))) + return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs))) } private constructor(meta: ChangeSetData[]) { - this._meta = Utils.NoNull(meta); + this._meta = Utils.NoNull(meta) } public filter(predicate: (cs: ChangeSetData) => boolean) { @@ -160,67 +187,67 @@ class ChangesetsOverview { } private static cleanChangesetData(cs: ChangeSetData): ChangeSetData { - if(cs === undefined){ + if (cs === undefined) { return undefined } - if(cs.properties.editor?.startsWith("iD")){ - // We also fetch based on hashtag, so some edits with iD show up as well + if (cs.properties.editor?.startsWith("iD")) { + // We also fetch based on hashtag, so some edits with iD show up as well return undefined } if (cs.properties.theme === undefined) { - cs.properties.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1) + cs.properties.theme = cs.properties.comment.substr( + cs.properties.comment.lastIndexOf("#") + 1 + ) } cs.properties.theme = cs.properties.theme.toLowerCase() const remapped = ChangesetsOverview.theme_remappings[cs.properties.theme] cs.properties.theme = remapped ?? cs.properties.theme if (cs.properties.theme.startsWith("https://raw.githubusercontent.com/")) { - cs.properties.theme = "gh://" + cs.properties.theme.substr("https://raw.githubusercontent.com/".length) + cs.properties.theme = + "gh://" + cs.properties.theme.substr("https://raw.githubusercontent.com/".length) } if (cs.properties.modify + cs.properties.delete + cs.properties.create == 0) { cs.properties.theme = "EMPTY CS" } try { cs.properties.host = new URL(cs.properties.host).host - } catch (e) { - - } + } catch (e) {} return cs } - } interface ChangeSetData { - "id": number, - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [number, number][][] - }, - "properties": { - "check_user": null, - "reasons": [], - "tags": [], - "features": [], - "user": string, - "uid": string, - "editor": string, - "comment": string, - "comments_count": number, - "source": string, - "imagery_used": string, - "date": string, - "reviewed_features": [], - "create": number, - "modify": number, - "delete": number, - "area": number, - "is_suspect": boolean, - "harmful": any, - "checked": boolean, - "check_date": any, - "host": string, - "theme": string, - "imagery": string, - "language": string + id: number + type: "Feature" + geometry: { + type: "Polygon" + coordinates: [number, number][][] + } + properties: { + check_user: null + reasons: [] + tags: [] + features: [] + user: string + uid: string + editor: string + comment: string + comments_count: number + source: string + imagery_used: string + date: string + reviewed_features: [] + create: number + modify: number + delete: number + area: number + is_suspect: boolean + harmful: any + checked: boolean + check_date: any + host: string + theme: string + imagery: string + language: string } } diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 8a25daa1b..da132496d 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -1,44 +1,52 @@ -import {UIEventSource} from "../Logic/UIEventSource"; -import {Translation} from "./i18n/Translation"; -import Locale from "./i18n/Locale"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizations"; -import {Utils} from "../Utils"; -import {VariableUiElement} from "./Base/VariableUIElement"; -import Combine from "./Base/Combine"; -import BaseUIElement from "./BaseUIElement"; -import {DefaultGuiState} from "./DefaultGuiState"; -import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; -import LinkToWeblate from "./Base/LinkToWeblate"; +import { UIEventSource } from "../Logic/UIEventSource" +import { Translation } from "./i18n/Translation" +import Locale from "./i18n/Locale" +import { FixedUiElement } from "./Base/FixedUiElement" +import SpecialVisualizations, { SpecialVisualization } from "./SpecialVisualizations" +import { Utils } from "../Utils" +import { VariableUiElement } from "./Base/VariableUIElement" +import Combine from "./Base/Combine" +import BaseUIElement from "./BaseUIElement" +import { DefaultGuiState } from "./DefaultGuiState" +import FeaturePipelineState from "../Logic/State/FeaturePipelineState" +import LinkToWeblate from "./Base/LinkToWeblate" export class SubstitutedTranslation extends VariableUiElement { - public constructor( translation: Translation, tagsSource: UIEventSource<Record<string, string>>, state: FeaturePipelineState, - mapping: Map<string, BaseUIElement | - ((state: FeaturePipelineState, tagSource: UIEventSource<Record<string, string>>, argument: string[], guistate: DefaultGuiState) => BaseUIElement)> = undefined) { - - const extraMappings: SpecialVisualization[] = []; + mapping: Map< + string, + | BaseUIElement + | (( + state: FeaturePipelineState, + tagSource: UIEventSource<Record<string, string>>, + argument: string[], + guistate: DefaultGuiState + ) => BaseUIElement) + > = undefined + ) { + const extraMappings: SpecialVisualization[] = [] mapping?.forEach((value, key) => { - extraMappings.push( - { - funcName: key, - constr: typeof value === "function" ? value : () => value, - docs: "Dynamically injected input element", - args: [], - example: "" - } - ) + extraMappings.push({ + funcName: key, + constr: typeof value === "function" ? value : () => value, + docs: "Dynamically injected input element", + args: [], + example: "", + }) }) - const linkToWeblate = translation !== undefined ? new LinkToWeblate(translation.context, translation.translations) : undefined; - + const linkToWeblate = + translation !== undefined + ? new LinkToWeblate(translation.context, translation.translations) + : undefined + super( - Locale.language.map(language => { - let txt = translation?.textFor(language); + Locale.language.map((language) => { + let txt = translation?.textFor(language) if (txt === undefined) { return undefined } @@ -46,32 +54,43 @@ export class SubstitutedTranslation extends VariableUiElement { txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) }) - const allElements = SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( - proto => { - if (proto.fixed !== undefined) { - if(tagsSource === undefined){ - return Utils.SubstituteKeys(proto.fixed, undefined) - } - return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); + const allElements = SubstitutedTranslation.ExtractSpecialComponents( + txt, + extraMappings + ).map((proto) => { + if (proto.fixed !== undefined) { + if (tagsSource === undefined) { + return Utils.SubstituteKeys(proto.fixed, undefined) } - const viz = proto.special; - if(viz === undefined){ - console.error("SPECIALRENDERING UNDEFINED for", tagsSource.data?.id, "THIS IS REALLY WEIRD") - return undefined - - } - try { - return viz.func.constr(state, tagsSource, proto.special.args, DefaultGuiState.state)?.SetStyle(proto.special.style); - } catch (e) { - console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) - return new FixedUiElement(`Could not generate special rendering for ${viz.func.funcName}(${viz.args.join(", ")}) ${e}`).SetStyle("alert") - } - }); + return new VariableUiElement( + tagsSource.map((tags) => Utils.SubstituteKeys(proto.fixed, tags)) + ) + } + const viz = proto.special + if (viz === undefined) { + console.error( + "SPECIALRENDERING UNDEFINED for", + tagsSource.data?.id, + "THIS IS REALLY WEIRD" + ) + return undefined + } + try { + return viz.func + .constr(state, tagsSource, proto.special.args, DefaultGuiState.state) + ?.SetStyle(proto.special.style) + } catch (e) { + console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) + return new FixedUiElement( + `Could not generate special rendering for ${ + viz.func.funcName + }(${viz.args.join(", ")}) ${e}` + ).SetStyle("alert") + } + }) allElements.push(linkToWeblate) - - return new Combine( - allElements - ) + + return new Combine(allElements) }) ) @@ -79,64 +98,77 @@ export class SubstitutedTranslation extends VariableUiElement { } /** - * + * * // Return empty list on empty input * SubstitutedTranslation.ExtractSpecialComponents("") // => [] - * + * * // Advanced cases with commas, braces and newlines should be handled without problem * const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}") * const templ = templates[0] * templ.special.func.funcName // => "send_email" * templ.special.args[0] = "{email}" */ - public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): { - fixed?: string, + public static ExtractSpecialComponents( + template: string, + extraMappings: SpecialVisualization[] = [] + ): { + fixed?: string special?: { - func: SpecialVisualization, - args: string[], + func: SpecialVisualization + args: string[] style: string } }[] { - - if(template === ""){ + if (template === "") { return [] } - for (const knownSpecial of extraMappings.concat(SpecialVisualizations.specialVisualizations)) { - + for (const knownSpecial of extraMappings.concat( + SpecialVisualizations.specialVisualizations + )) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' - const matched = template.match(new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")); + const matched = template.match( + new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") + ) if (matched != null) { - // We found a special component that should be brought to live - const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings); - const argument = matched[2].trim(); + const partBefore = SubstitutedTranslation.ExtractSpecialComponents( + matched[1], + extraMappings + ) + const argument = matched[2].trim() const style = matched[3]?.substring(1) ?? "" - const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings); - const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); + const partAfter = SubstitutedTranslation.ExtractSpecialComponents( + matched[4], + extraMappings + ) + const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") if (argument.length > 0) { - const realArgs = argument.split(",").map(str => str.trim() - .replace(/&LPARENS/g, '(') - .replace(/&RPARENS/g, ')') - .replace(/&LBRACE/g, '{') - .replace(/&RBRACE/g, '}') - .replace(/&COMMA/g, ',')); + const realArgs = argument.split(",").map((str) => + str + .trim() + .replace(/&LPARENS/g, "(") + .replace(/&RPARENS/g, ")") + .replace(/&LBRACE/g, "{") + .replace(/&RBRACE/g, "}") + .replace(/&COMMA/g, ",") + ) for (let i = 0; i < realArgs.length; i++) { if (args.length <= i) { - args.push(realArgs[i]); + args.push(realArgs[i]) } else { - args[i] = realArgs[i]; + args[i] = realArgs[i] } } } - let element; + let element element = { special: { args: args, style: style, - func: knownSpecial - } + func: knownSpecial, + }, } return [...partBefore, element, ...partAfter] } @@ -145,11 +177,17 @@ export class SubstitutedTranslation extends VariableUiElement { // Let's to a small sanity check to help the theme designers: if (template.search(/{[^}]+\([^}]*\)}/) >= 0) { // Hmm, we might have found an invalid rendering name - console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName + "()").join(", ")) + console.warn( + "Found a suspicious special rendering value in: ", + template, + " did you mean one of: ", + SpecialVisualizations.specialVisualizations + .map((sp) => sp.funcName + "()") + .join(", ") + ) } - - // IF we end up here, no changes have to be made - except to remove any resting {} - return [{fixed: template}]; - } -} \ No newline at end of file + // IF we end up here, no changes have to be made - except to remove any resting {} + return [{ fixed: template }] + } +} diff --git a/UI/UIElement.ts b/UI/UIElement.ts index 6b47e6c87..6ae128b29 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -1,33 +1,27 @@ -import BaseUIElement from "./BaseUIElement"; +import BaseUIElement from "./BaseUIElement" export abstract class UIElement extends BaseUIElement { - - /** * Should be overridden for specific HTML functionality */ protected InnerConstructElement(): HTMLElement { // Uses the old fashioned way to construct an element using 'InnerRender' - const innerRender = this.InnerRender(); + const innerRender = this.InnerRender() if (innerRender === undefined || innerRender === "") { - return undefined; + return undefined } const el = document.createElement("span") if (typeof innerRender === "string") { el.innerHTML = innerRender } else { - const subElement = innerRender.ConstructElement(); + const subElement = innerRender.ConstructElement() if (subElement === undefined) { - return undefined; + return undefined } el.appendChild(subElement) } - return el; + return el } - protected abstract InnerRender(): string | BaseUIElement; - + protected abstract InnerRender(): string | BaseUIElement } - - - diff --git a/UI/Wikipedia/WikidataPreviewBox.ts b/UI/Wikipedia/WikidataPreviewBox.ts index bd6770164..89982cd2e 100644 --- a/UI/Wikipedia/WikidataPreviewBox.ts +++ b/UI/Wikipedia/WikidataPreviewBox.ts @@ -1,125 +1,149 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; -import {Translation, TypedTranslation} from "../i18n/Translation"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Loading from "../Base/Loading"; -import Translations from "../i18n/Translations"; -import Combine from "../Base/Combine"; -import Img from "../Base/Img"; -import {WikimediaImageProvider} from "../../Logic/ImageProviders/WikimediaImageProvider"; -import Link from "../Base/Link"; -import Svg from "../../Svg"; -import BaseUIElement from "../BaseUIElement"; -import {Utils} from "../../Utils"; +import { VariableUiElement } from "../Base/VariableUIElement" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" +import { Translation, TypedTranslation } from "../i18n/Translation" +import { FixedUiElement } from "../Base/FixedUiElement" +import Loading from "../Base/Loading" +import Translations from "../i18n/Translations" +import Combine from "../Base/Combine" +import Img from "../Base/Img" +import { WikimediaImageProvider } from "../../Logic/ImageProviders/WikimediaImageProvider" +import Link from "../Base/Link" +import Svg from "../../Svg" +import BaseUIElement from "../BaseUIElement" +import { Utils } from "../../Utils" export default class WikidataPreviewBox extends VariableUiElement { - - private static isHuman = [ - {p: 31/*is a*/, q: 5 /* human */}, - ] + private static isHuman = [{ p: 31 /*is a*/, q: 5 /* human */ }] // @ts-ignore private static extraProperties: { - requires?: { p: number, q?: number }[], - property: string, - display: TypedTranslation<{value}> | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>, + requires?: { p: number; q?: number }[] + property: string + display: + | TypedTranslation<{ value }> + | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */> textMode?: Map<string, string> }[] = [ { requires: WikidataPreviewBox.isHuman, property: "P21", display: new Map([ - ['Q6581097', () => Svg.gender_male_svg().SetStyle("width: 1rem; height: auto")], - ['Q6581072', () => Svg.gender_female_svg().SetStyle("width: 1rem; height: auto")], - ['Q1097630', () => Svg.gender_inter_svg().SetStyle("width: 1rem; height: auto")], - ['Q1052281', () => Svg.gender_trans_svg().SetStyle("width: 1rem; height: auto")/*'transwomen'*/], - ['Q2449503', () => Svg.gender_trans_svg().SetStyle("width: 1rem; height: auto")/*'transmen'*/], - ['Q48270', () => Svg.gender_queer_svg().SetStyle("width: 1rem; height: auto")] + ["Q6581097", () => Svg.gender_male_svg().SetStyle("width: 1rem; height: auto")], + ["Q6581072", () => Svg.gender_female_svg().SetStyle("width: 1rem; height: auto")], + ["Q1097630", () => Svg.gender_inter_svg().SetStyle("width: 1rem; height: auto")], + [ + "Q1052281", + () => + Svg.gender_trans_svg().SetStyle( + "width: 1rem; height: auto" + ) /*'transwomen'*/, + ], + [ + "Q2449503", + () => + Svg.gender_trans_svg().SetStyle("width: 1rem; height: auto") /*'transmen'*/, + ], + ["Q48270", () => Svg.gender_queer_svg().SetStyle("width: 1rem; height: auto")], ]), textMode: new Map([ - ['Q6581097', "♂️"], - ['Q6581072', "♀️"], - ['Q1097630', "⚥️"], - ['Q1052281', "🏳️‍⚧️"/*'transwomen'*/], - ['Q2449503', "🏳️‍⚧️"/*'transmen'*/], - ['Q48270', "🏳️‍🌈 ⚧"] - ]) + ["Q6581097", "♂️"], + ["Q6581072", "♀️"], + ["Q1097630", "⚥️"], + ["Q1052281", "🏳️‍⚧️" /*'transwomen'*/], + ["Q2449503", "🏳️‍⚧️" /*'transmen'*/], + ["Q48270", "🏳️‍🌈 ⚧"], + ]), }, { property: "P569", requires: WikidataPreviewBox.isHuman, - display: Translations.t.general.wikipedia.previewbox.born + display: Translations.t.general.wikipedia.previewbox.born, }, { property: "P570", requires: WikidataPreviewBox.isHuman, - display:Translations.t.general.wikipedia.previewbox.died - } + display: Translations.t.general.wikipedia.previewbox.died, + }, ] - constructor(wikidataId: Store<string>, options?: { - noImages?: boolean, - imageStyle?: string, - whileLoading?: BaseUIElement | string, - extraItems?: (BaseUIElement | string)[]}) { - let inited = false; - const wikidata = wikidataId - .stabilized(250) - .bind(id => { - if (id === undefined || id === "" || id === "Q") { - return null; + constructor( + wikidataId: Store<string>, + options?: { + noImages?: boolean + imageStyle?: string + whileLoading?: BaseUIElement | string + extraItems?: (BaseUIElement | string)[] + } + ) { + let inited = false + const wikidata = wikidataId.stabilized(250).bind((id) => { + if (id === undefined || id === "" || id === "Q") { + return null + } + inited = true + return Wikidata.LoadWikidataEntry(id) + }) + + super( + wikidata.map((maybeWikidata) => { + if (maybeWikidata === null || !inited) { + return options?.whileLoading } - inited = true; - return Wikidata.LoadWikidataEntry(id) + + if (maybeWikidata === undefined) { + return new Loading(Translations.t.general.loading) + } + + if (maybeWikidata["error"] !== undefined) { + return new FixedUiElement(maybeWikidata["error"]).SetClass("alert") + } + const wikidata = <WikidataResponse>maybeWikidata["success"] + return WikidataPreviewBox.WikidataResponsePreview(wikidata, options) }) - - super(wikidata.map(maybeWikidata => { - if (maybeWikidata === null || !inited) { - return options?.whileLoading; - } - - if (maybeWikidata === undefined) { - return new Loading(Translations.t.general.loading) - } - - if (maybeWikidata["error"] !== undefined) { - return new FixedUiElement(maybeWikidata["error"]).SetClass("alert") - } - const wikidata = <WikidataResponse>maybeWikidata["success"] - return WikidataPreviewBox.WikidataResponsePreview(wikidata, options) - })) - + ) } - public static WikidataResponsePreview(wikidata: WikidataResponse, options?: { - noImages?: boolean, - imageStyle?: string, - extraItems?: (BaseUIElement | string)[]}): BaseUIElement { + public static WikidataResponsePreview( + wikidata: WikidataResponse, + options?: { + noImages?: boolean + imageStyle?: string + extraItems?: (BaseUIElement | string)[] + } + ): BaseUIElement { let link = new Link( new Combine([ wikidata.id, - options?.noImages ? wikidata.id : Svg.wikidata_svg().SetStyle("width: 2.5rem").SetClass("block") + options?.noImages + ? wikidata.id + : Svg.wikidata_svg().SetStyle("width: 2.5rem").SetClass("block"), ]).SetClass("flex"), - Wikidata.IdToArticle(wikidata.id), true)?.SetClass("must-link") + Wikidata.IdToArticle(wikidata.id), + true + )?.SetClass("must-link") let info = new Combine([ - new Combine( - [Translation.fromMap(wikidata.labels)?.SetClass("font-bold"), - link]).SetClass("flex justify-between"), + new Combine([ + Translation.fromMap(wikidata.labels)?.SetClass("font-bold"), + link, + ]).SetClass("flex justify-between"), Translation.fromMap(wikidata.descriptions), WikidataPreviewBox.QuickFacts(wikidata, options), - ...(options?.extraItems ?? []) + ...(options?.extraItems ?? []), ]).SetClass("flex flex-col link-underline") - let imageUrl = undefined if (wikidata.claims.get("P18")?.size > 0) { imageUrl = Array.from(wikidata.claims.get("P18"))[0] } if (imageUrl && !options?.noImages) { imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url - info = new Combine([new Img(imageUrl).SetStyle(options?.imageStyle ?? "max-width: 5rem; width: unset; height: 4rem").SetClass("rounded-xl mr-2"), - info.SetClass("w-full")]).SetClass("flex") + info = new Combine([ + new Img(imageUrl) + .SetStyle(options?.imageStyle ?? "max-width: 5rem; width: unset; height: 4rem") + .SetClass("rounded-xl mr-2"), + info.SetClass("w-full"), + ]).SetClass("flex") } info.SetClass("p-2 w-full") @@ -127,18 +151,20 @@ export default class WikidataPreviewBox extends VariableUiElement { return info } - public static QuickFacts(wikidata: WikidataResponse, options?: {noImages?: boolean}): BaseUIElement { - + public static QuickFacts( + wikidata: WikidataResponse, + options?: { noImages?: boolean } + ): BaseUIElement { const els: BaseUIElement[] = [] for (const extraProperty of WikidataPreviewBox.extraProperties) { let hasAllRequirements = true for (const requirement of extraProperty.requires) { if (!wikidata.claims?.has("P" + requirement.p)) { - hasAllRequirements = false; + hasAllRequirements = false break } if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) { - hasAllRequirements = false; + hasAllRequirements = false break } } @@ -147,31 +173,32 @@ export default class WikidataPreviewBox extends VariableUiElement { } const key = extraProperty.property - const display = (options?.noImages ? extraProperty.textMode: extraProperty.display) ?? extraProperty.display + const display = + (options?.noImages ? extraProperty.textMode : extraProperty.display) ?? + extraProperty.display if (wikidata.claims?.get(key) === undefined) { continue } const value: string[] = Array.from(wikidata.claims.get(key)) if (display instanceof Translation) { - els.push(display.Subs({value: value.join(", ")}).SetClass("m-2")) + els.push(display.Subs({ value: value.join(", ") }).SetClass("m-2")) continue } - const constructors = Utils.NoNull(value.map(property => display.get(property))) - const elems = constructors.map(v => { + const constructors = Utils.NoNull(value.map((property) => display.get(property))) + const elems = constructors.map((v) => { if (typeof v === "string") { return new FixedUiElement(v) } else { - return v(); + return v() } }) els.push(new Combine(elems).SetClass("flex m-2")) } if (els.length === 0) { - return undefined; + return undefined } return new Combine(els).SetClass("flex") } - -} \ No newline at end of file +} diff --git a/UI/Wikipedia/WikidataSearchBox.ts b/UI/Wikipedia/WikidataSearchBox.ts index ccbfcce7b..5c7f2ba3b 100644 --- a/UI/Wikipedia/WikidataSearchBox.ts +++ b/UI/Wikipedia/WikidataSearchBox.ts @@ -1,142 +1,150 @@ -import Combine from "../Base/Combine"; -import {InputElement} from "../Input/InputElement"; -import {TextField} from "../Input/TextField"; -import Translations from "../i18n/Translations"; -import {ImmutableStore, Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; -import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; -import Locale from "../i18n/Locale"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import WikidataPreviewBox from "./WikidataPreviewBox"; -import Title from "../Base/Title"; -import WikipediaBox from "./WikipediaBox"; -import Svg from "../../Svg"; -import Loading from "../Base/Loading"; +import Combine from "../Base/Combine" +import { InputElement } from "../Input/InputElement" +import { TextField } from "../Input/TextField" +import Translations from "../i18n/Translations" +import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource" +import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" +import Locale from "../i18n/Locale" +import { VariableUiElement } from "../Base/VariableUIElement" +import WikidataPreviewBox from "./WikidataPreviewBox" +import Title from "../Base/Title" +import WikipediaBox from "./WikipediaBox" +import Svg from "../../Svg" +import Loading from "../Base/Loading" export default class WikidataSearchBox extends InputElement<string> { - private static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>() - IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); + IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) private readonly wikidataId: UIEventSource<string> private readonly searchText: UIEventSource<string> - private readonly instanceOf?: number[]; - private readonly notInstanceOf?: number[]; + private readonly instanceOf?: number[] + private readonly notInstanceOf?: number[] constructor(options?: { - searchText?: UIEventSource<string>, - value?: UIEventSource<string>, - notInstanceOf?: number[], + searchText?: UIEventSource<string> + value?: UIEventSource<string> + notInstanceOf?: number[] instanceOf?: number[] }) { - super(); + super() this.searchText = options?.searchText - this.wikidataId = options?.value ?? new UIEventSource<string>(undefined); + this.wikidataId = options?.value ?? new UIEventSource<string>(undefined) this.instanceOf = options?.instanceOf this.notInstanceOf = options?.notInstanceOf } GetValue(): UIEventSource<string> { - return this.wikidataId; + return this.wikidataId } IsValid(t: string): boolean { - return t.startsWith("Q") && !isNaN(Number(t.substring(1))); + return t.startsWith("Q") && !isNaN(Number(t.substring(1))) } protected InnerConstructElement(): HTMLElement { - const searchField = new TextField({ placeholder: Translations.t.general.wikipedia.searchWikidata, value: this.searchText, - inputStyle: "width: calc(100% - 0.5rem); border: 1px solid black" - + inputStyle: "width: calc(100% - 0.5rem); border: 1px solid black", }) const selectedWikidataId = this.wikidataId - const tooShort = new ImmutableStore<{success: WikidataResponse[]}>({success: undefined}) - const searchResult: Store<{success?: WikidataResponse[], error?: any}> = searchField.GetValue().bind( - searchText => { + const tooShort = new ImmutableStore<{ success: WikidataResponse[] }>({ success: undefined }) + const searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchField + .GetValue() + .bind((searchText) => { if (searchText.length < 3) { - return tooShort; + return tooShort } const lang = Locale.language.data const key = lang + ":" + searchText let promise = WikidataSearchBox._searchCache.get(key) if (promise === undefined) { promise = Wikidata.searchAndFetch(searchText, { - lang, - maxCount: 5, - notInstanceOf: this.notInstanceOf, - instanceOf: this.instanceOf - } - ) + lang, + maxCount: 5, + notInstanceOf: this.notInstanceOf, + instanceOf: this.instanceOf, + }) WikidataSearchBox._searchCache.set(key, promise) } return Stores.FromPromiseWithErr(promise) - } - ) - + }) - const previews = new VariableUiElement(searchResult.map(searchResultsOrFail => { - - if (searchField.GetValue().data.length === 0) { - return Translations.t.general.wikipedia.doSearch - } - - if (searchField.GetValue().data.length < 3) { - return Translations.t.general.wikipedia.searchToShort - } - - if( searchResultsOrFail === undefined) { - return new Loading(Translations.t.general.loading) - } - - if (searchResultsOrFail.error !== undefined) { - return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchResultsOrFail.error]) - } - - - const searchResults = searchResultsOrFail.success; - if (searchResults.length === 0) { - return Translations.t.general.wikipedia.noResults.Subs({search: searchField.GetValue().data ?? ""}) - } - - - return new Combine(searchResults.map(wikidataresponse => { - const el = WikidataPreviewBox.WikidataResponsePreview(wikidataresponse).SetClass("rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors") - el.onClick(() => { - selectedWikidataId.setData(wikidataresponse.id) - }) - selectedWikidataId.addCallbackAndRunD(selected => { - if (selected === wikidataresponse.id) { - el.SetClass("subtle-background border-attention") - } else { - el.RemoveClass("subtle-background") - el.RemoveClass("border-attention") + const previews = new VariableUiElement( + searchResult.map( + (searchResultsOrFail) => { + if (searchField.GetValue().data.length === 0) { + return Translations.t.general.wikipedia.doSearch } - }) - return el; - })).SetClass("flex flex-col") + if (searchField.GetValue().data.length < 3) { + return Translations.t.general.wikipedia.searchToShort + } - }, [searchField.GetValue()])) + if (searchResultsOrFail === undefined) { + return new Loading(Translations.t.general.loading) + } + + if (searchResultsOrFail.error !== undefined) { + return new Combine([ + Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), + searchResultsOrFail.error, + ]) + } + + const searchResults = searchResultsOrFail.success + if (searchResults.length === 0) { + return Translations.t.general.wikipedia.noResults.Subs({ + search: searchField.GetValue().data ?? "", + }) + } + + return new Combine( + searchResults.map((wikidataresponse) => { + const el = WikidataPreviewBox.WikidataResponsePreview( + wikidataresponse + ).SetClass( + "rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors" + ) + el.onClick(() => { + selectedWikidataId.setData(wikidataresponse.id) + }) + selectedWikidataId.addCallbackAndRunD((selected) => { + if (selected === wikidataresponse.id) { + el.SetClass("subtle-background border-attention") + } else { + el.RemoveClass("subtle-background") + el.RemoveClass("border-attention") + } + }) + return el + }) + ).SetClass("flex flex-col") + }, + [searchField.GetValue()] + ) + ) const full = new Combine([ new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"), new Combine([ Svg.search_ui().SetStyle("width: 1.5rem"), - searchField.SetClass("m-2 w-full")]).SetClass("flex"), - previews + searchField.SetClass("m-2 w-full"), + ]).SetClass("flex"), + previews, ]).SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2") return new Combine([ - new VariableUiElement(selectedWikidataId.map(wid => { - if (wid === undefined) { - return undefined - } - return new WikipediaBox(wid.split(";")); - })).SetStyle("max-height:12.5rem"), - full - ]).ConstructElement(); + new VariableUiElement( + selectedWikidataId.map((wid) => { + if (wid === undefined) { + return undefined + } + return new WikipediaBox(wid.split(";")) + }) + ).SetStyle("max-height:12.5rem"), + full, + ]).ConstructElement() } - -} \ No newline at end of file +} diff --git a/UI/Wikipedia/WikipediaBox.ts b/UI/Wikipedia/WikipediaBox.ts index a3f3ad66c..0ce23fb13 100644 --- a/UI/Wikipedia/WikipediaBox.ts +++ b/UI/Wikipedia/WikipediaBox.ts @@ -1,83 +1,99 @@ -import BaseUIElement from "../BaseUIElement"; -import Locale from "../i18n/Locale"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import {Translation} from "../i18n/Translation"; -import Svg from "../../Svg"; -import Combine from "../Base/Combine"; -import Title from "../Base/Title"; -import Wikipedia from "../../Logic/Web/Wikipedia"; -import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; -import {TabbedComponent} from "../Base/TabbedComponent"; -import {Store, UIEventSource} from "../../Logic/UIEventSource"; -import Loading from "../Base/Loading"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; -import Link from "../Base/Link"; -import WikidataPreviewBox from "./WikidataPreviewBox"; -import {Paragraph} from "../Base/Paragraph"; +import BaseUIElement from "../BaseUIElement" +import Locale from "../i18n/Locale" +import { VariableUiElement } from "../Base/VariableUIElement" +import { Translation } from "../i18n/Translation" +import Svg from "../../Svg" +import Combine from "../Base/Combine" +import Title from "../Base/Title" +import Wikipedia from "../../Logic/Web/Wikipedia" +import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" +import { TabbedComponent } from "../Base/TabbedComponent" +import { Store, UIEventSource } from "../../Logic/UIEventSource" +import Loading from "../Base/Loading" +import { FixedUiElement } from "../Base/FixedUiElement" +import Translations from "../i18n/Translations" +import Link from "../Base/Link" +import WikidataPreviewBox from "./WikidataPreviewBox" +import { Paragraph } from "../Base/Paragraph" export interface WikipediaBoxOptions { - addHeader: boolean, - firstParagraphOnly: boolean, + addHeader: boolean + firstParagraphOnly: boolean noImages: boolean currentState?: UIEventSource<"loading" | "loaded" | "error"> } export default class WikipediaBox extends Combine { - constructor(wikidataIds: string[], options?: WikipediaBoxOptions) { const mainContents = [] - options = options??{addHeader: false, firstParagraphOnly: true, noImages: false}; - const pages = wikidataIds.map(entry => WikipediaBox.createLinkedContent(entry.trim(), options)) + options = options ?? { addHeader: false, firstParagraphOnly: true, noImages: false } + const pages = wikidataIds.map((entry) => + WikipediaBox.createLinkedContent(entry.trim(), options) + ) if (wikidataIds.length == 1) { const page = pages[0] mainContents.push( new Combine([ new Combine([ - options.noImages ? undefined : Svg.wikipedia_ui().SetStyle("width: 1.5rem").SetClass("inline-block mr-3"), - page.titleElement]).SetClass("flex"), - page.linkElement - ]).SetClass("flex justify-between align-middle"), + options.noImages + ? undefined + : Svg.wikipedia_ui() + .SetStyle("width: 1.5rem") + .SetClass("inline-block mr-3"), + page.titleElement, + ]).SetClass("flex"), + page.linkElement, + ]).SetClass("flex justify-between align-middle") ) mainContents.push(page.contents.SetClass("overflow-auto normal-background rounded-lg")) } else if (wikidataIds.length > 1) { - const tabbed = new TabbedComponent( - pages.map(page => { - const contents = page.contents.SetClass("overflow-auto normal-background rounded-lg block").SetStyle("max-height: inherit; height: inherit; padding-bottom: 3.3rem") + pages.map((page) => { + const contents = page.contents + .SetClass("overflow-auto normal-background rounded-lg block") + .SetStyle("max-height: inherit; height: inherit; padding-bottom: 3.3rem") return { header: page.titleElement.SetClass("pl-2 pr-2"), content: new Combine([ page.linkElement .SetStyle("top: 2rem; right: 2.5rem;") - .SetClass("absolute subtle-background rounded-full p-3 opacity-50 hover:opacity-100 transition-opacity"), - contents - ]).SetStyle("max-height: inherit; height: inherit").SetClass("relative") + .SetClass( + "absolute subtle-background rounded-full p-3 opacity-50 hover:opacity-100 transition-opacity" + ), + contents, + ]) + .SetStyle("max-height: inherit; height: inherit") + .SetClass("relative"), } - }), 0, { - leftOfHeader: options.noImages ? undefined : Svg.wikipedia_svg().SetStyle("width: 1.5rem; align-self: center;").SetClass("mr-4"), - styleHeader: header => header.SetClass("subtle-background").SetStyle("height: 3.3rem") + leftOfHeader: options.noImages + ? undefined + : Svg.wikipedia_svg() + .SetStyle("width: 1.5rem; align-self: center;") + .SetClass("mr-4"), + styleHeader: (header) => + header.SetClass("subtle-background").SetStyle("height: 3.3rem"), } ) tabbed.SetStyle("height: inherit; max-height: inherit; overflow: hidden") mainContents.push(tabbed) - } - super(mainContents) - - this.SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col") - .SetStyle("max-height: inherit") + this.SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col").SetStyle( + "max-height: inherit" + ) } - private static createLinkedContent(entry: string, options: WikipediaBoxOptions): { - titleElement: BaseUIElement, - contents: BaseUIElement, + private static createLinkedContent( + entry: string, + options: WikipediaBoxOptions + ): { + titleElement: BaseUIElement + contents: BaseUIElement linkElement: BaseUIElement } { if (entry.match("[qQ][0-9]+")) { @@ -90,77 +106,94 @@ export default class WikipediaBox extends Combine { /** * Given a '<language>:<article-name>'-string, constructs the wikipedia article */ - private static createWikipediabox(wikipediaArticle: string, options: WikipediaBoxOptions): { - titleElement: BaseUIElement, - contents: BaseUIElement, + private static createWikipediabox( + wikipediaArticle: string, + options: WikipediaBoxOptions + ): { + titleElement: BaseUIElement + contents: BaseUIElement linkElement: BaseUIElement } { - const wp = Translations.t.general.wikipedia; + const wp = Translations.t.general.wikipedia const article = Wikipedia.extractLanguageAndName(wikipediaArticle) if (article === undefined) { return { titleElement: undefined, contents: wp.noWikipediaPage, - linkElement: undefined + linkElement: undefined, } } - const wikipedia = new Wikipedia({language: article.language}) + const wikipedia = new Wikipedia({ language: article.language }) const url = wikipedia.getPageUrl(article.pageName) - const linkElement = new Link(Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block "), url, true) .SetClass("flex items-center enable-links") + const linkElement = new Link( + Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block "), + url, + true + ).SetClass("flex items-center enable-links") return { titleElement: new Title(article.pageName, 3), contents: WikipediaBox.createContents(article.pageName, wikipedia, options), - linkElement + linkElement, } } /** * Given a `Q1234`, constructs a wikipedia box (if a wikipedia page is available) or wikidata box as fallback. - * + * */ - private static createWikidatabox(wikidataId: string, options: WikipediaBoxOptions): { - titleElement: BaseUIElement, - contents: BaseUIElement, + private static createWikidatabox( + wikidataId: string, + options: WikipediaBoxOptions + ): { + titleElement: BaseUIElement + contents: BaseUIElement linkElement: BaseUIElement } { + const wp = Translations.t.general.wikipedia - const wp = Translations.t.general.wikipedia; - - const wikiLink: Store<[string, string, WikidataResponse] | "loading" | "failed" | ["no page", WikidataResponse]> = - Wikidata.LoadWikidataEntry(wikidataId) - .map(maybewikidata => { - if (maybewikidata === undefined) { - return "loading" - } - if (maybewikidata["error"] !== undefined) { - return "failed" - - } - const wikidata = <WikidataResponse>maybewikidata["success"] - if (wikidata === undefined) { - return "failed" - } - if (wikidata.wikisites.size === 0) { - return ["no page", wikidata] - } - - const preferredLanguage = [Locale.language.data, "en", Array.from(wikidata.wikisites.keys())[0]] - let language - let pagetitle; - let i = 0 - do { - language = preferredLanguage[i] - pagetitle = wikidata.wikisites.get(language) - i++; - } while (pagetitle === undefined) - return [pagetitle, language, wikidata] - }, [Locale.language]) + const wikiLink: Store< + | [string, string, WikidataResponse] + | "loading" + | "failed" + | ["no page", WikidataResponse] + > = Wikidata.LoadWikidataEntry(wikidataId).map( + (maybewikidata) => { + if (maybewikidata === undefined) { + return "loading" + } + if (maybewikidata["error"] !== undefined) { + return "failed" + } + const wikidata = <WikidataResponse>maybewikidata["success"] + if (wikidata === undefined) { + return "failed" + } + if (wikidata.wikisites.size === 0) { + return ["no page", wikidata] + } + const preferredLanguage = [ + Locale.language.data, + "en", + Array.from(wikidata.wikisites.keys())[0], + ] + let language + let pagetitle + let i = 0 + do { + language = preferredLanguage[i] + pagetitle = wikidata.wikisites.get(language) + i++ + } while (pagetitle === undefined) + return [pagetitle, language, wikidata] + }, + [Locale.language] + ) const contents = new VariableUiElement( - wikiLink.map(status => { + wikiLink.map((status) => { if (status === "loading") { return new Loading(wp.loading.Clone()).SetClass("pl-6 pt-2") } @@ -173,106 +206,116 @@ export default class WikipediaBox extends Combine { options.currentState?.setData("loaded") return new Combine([ WikidataPreviewBox.WikidataResponsePreview(wd), - wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4") + wp.noWikipediaPage.Clone().SetClass("subtle"), + ]).SetClass("flex flex-col p-4") } const [pagetitle, language, wd] = <[string, string, WikidataResponse]>status - const wikipedia = new Wikipedia({language}) - const quickFacts = WikidataPreviewBox.QuickFacts(wd); - return WikipediaBox.createContents(pagetitle, wikipedia, {topBar: quickFacts, ...options}) - + const wikipedia = new Wikipedia({ language }) + const quickFacts = WikidataPreviewBox.QuickFacts(wd) + return WikipediaBox.createContents(pagetitle, wikipedia, { + topBar: quickFacts, + ...options, + }) }) ) - const titleElement = new VariableUiElement(wikiLink.map(state => { - if (typeof state !== "string") { - const [pagetitle, _] = state - if (pagetitle === "no page") { - const wd = <WikidataResponse>state[1] - return new Title(Translation.fromMap(wd.labels), 3) + const titleElement = new VariableUiElement( + wikiLink.map((state) => { + if (typeof state !== "string") { + const [pagetitle, _] = state + if (pagetitle === "no page") { + const wd = <WikidataResponse>state[1] + return new Title(Translation.fromMap(wd.labels), 3) + } + return new Title(pagetitle, 3) } - return new Title(pagetitle, 3) - } - return new Link(new Title(wikidataId, 3), "https://www.wikidata.org/wiki/" + wikidataId, true) + return new Link( + new Title(wikidataId, 3), + "https://www.wikidata.org/wiki/" + wikidataId, + true + ) + }) + ) - })) + const linkElement = new VariableUiElement( + wikiLink.map((state) => { + if (typeof state !== "string") { + const [pagetitle, language] = state + const popout = options.noImages + ? "Source" + : Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block") + if (pagetitle === "no page") { + const wd = <WikidataResponse>state[1] + return new Link(popout, "https://www.wikidata.org/wiki/" + wd.id, true) + } - const linkElement = new VariableUiElement(wikiLink.map(state => { - if (typeof state !== "string") { - const [pagetitle, language] = state - const popout = options.noImages ? "Source" : Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block") - if (pagetitle === "no page") { - const wd = <WikidataResponse>state[1] - return new Link(popout, - "https://www.wikidata.org/wiki/" + wd.id - , true) + const url = `https://${language}.wikipedia.org/wiki/${pagetitle}` + return new Link(popout, url, true) } - - const url = `https://${language}.wikipedia.org/wiki/${pagetitle}` - return new Link(popout, url, true) - } - return undefined - })) - .SetClass("flex items-center enable-links") + return undefined + }) + ).SetClass("flex items-center enable-links") return { contents: contents, linkElement: linkElement, - titleElement: titleElement + titleElement: titleElement, } } - /** * Returns the actual content in a scrollable way for the given wikipedia page */ - private static createContents(pagename: string, wikipedia: Wikipedia, options:{ - topBar?: BaseUIElement} & WikipediaBoxOptions): BaseUIElement { + private static createContents( + pagename: string, + wikipedia: Wikipedia, + options: { + topBar?: BaseUIElement + } & WikipediaBoxOptions + ): BaseUIElement { const htmlContent = wikipedia.GetArticle(pagename, options) const wp = Translations.t.general.wikipedia - const contents: VariableUiElement =new VariableUiElement( - htmlContent.map(htmlContent => { - if (htmlContent === undefined) { - // Still loading - return new Loading(wp.loading.Clone()) - } - if (htmlContent["success"] !== undefined) { - let content: BaseUIElement = new FixedUiElement(htmlContent["success"]); - if (options?.addHeader) { - content = new Combine( - [ - new Paragraph( - new Link(wp.fromWikipedia, wikipedia.getPageUrl(pagename), true), - ), - new Paragraph( - content - ) - ] - ) + const contents: VariableUiElement = new VariableUiElement( + htmlContent.map((htmlContent) => { + if (htmlContent === undefined) { + // Still loading + return new Loading(wp.loading.Clone()) + } + if (htmlContent["success"] !== undefined) { + let content: BaseUIElement = new FixedUiElement(htmlContent["success"]) + if (options?.addHeader) { + content = new Combine([ + new Paragraph( + new Link(wp.fromWikipedia, wikipedia.getPageUrl(pagename), true) + ), + new Paragraph(content), + ]) + } + return content.SetClass("wikipedia-article") + } + if (htmlContent["error"]) { + console.warn("Loading wikipage failed due to", htmlContent["error"]) + return wp.failed.Clone().SetClass("alert p-4") } - return content.SetClass("wikipedia-article") - } - if (htmlContent["error"]) { - console.warn("Loading wikipage failed due to", htmlContent["error"]) - return wp.failed.Clone().SetClass("alert p-4") - } - return undefined - })) + return undefined + }) + ) - htmlContent.addCallbackAndRunD(c => { - if(c["success"] !== undefined){ + htmlContent.addCallbackAndRunD((c) => { + if (c["success"] !== undefined) { options.currentState?.setData("loaded") - }else if (c["error"] !== undefined){ + } else if (c["error"] !== undefined) { options.currentState?.setData("error") - }else { + } else { options.currentState?.setData("loading") } }) - + return new Combine([ options?.topBar?.SetClass("border-2 border-grey rounded-lg m-1 mb-0"), - contents .SetClass("block pl-6 pt-2")]) + contents.SetClass("block pl-6 pt-2"), + ]) } - -} \ No newline at end of file +} diff --git a/UI/i18n/Locale.ts b/UI/i18n/Locale.ts index 9fda1d07b..c88a655c7 100644 --- a/UI/i18n/Locale.ts +++ b/UI/i18n/Locale.ts @@ -1,16 +1,14 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; -import {Utils} from "../../Utils"; -import {QueryParameters} from "../../Logic/Web/QueryParameters"; - +import { UIEventSource } from "../../Logic/UIEventSource" +import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" +import { Utils } from "../../Utils" +import { QueryParameters } from "../../Logic/Web/QueryParameters" export default class Locale { + public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false) + public static language: UIEventSource<string> = Locale.setup() - public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false); - public static language: UIEventSource<string> = Locale.setup(); - private static setup() { - const source = LocalStorageSource.Get('language', "en"); + const source = LocalStorageSource.Get("language", "en") if (!Utils.runningFromConsole) { // @ts-ignore window.setLanguage = function (language: string) { @@ -18,17 +16,21 @@ export default class Locale { } } source.syncWith( - QueryParameters.GetQueryParameter("language", undefined, "The language to display mapcomplete in. Will be ignored in case a logged-in-user did set their language before. If the specified language does not exist, it will default to the first language in the theme."), + QueryParameters.GetQueryParameter( + "language", + undefined, + "The language to display mapcomplete in. Will be ignored in case a logged-in-user did set their language before. If the specified language does not exist, it will default to the first language in the theme." + ), true ) - QueryParameters.GetBooleanQueryParameter("fs-translation-mode",false,"If set, will show a translation button next to every string.") - .addCallbackAndRunD(tr => { - Locale.showLinkToWeblate.setData(Locale.showLinkToWeblate.data || tr); + QueryParameters.GetBooleanQueryParameter( + "fs-translation-mode", + false, + "If set, will show a translation button next to every string." + ).addCallbackAndRunD((tr) => { + Locale.showLinkToWeblate.setData(Locale.showLinkToWeblate.data || tr) }) - - return source; + return source } } - - diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index eef10b59e..3a7eb7c3f 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -1,72 +1,82 @@ -import Locale from "./Locale"; -import {Utils} from "../../Utils"; -import BaseUIElement from "../BaseUIElement"; -import LinkToWeblate from "../Base/LinkToWeblate"; +import Locale from "./Locale" +import { Utils } from "../../Utils" +import BaseUIElement from "../BaseUIElement" +import LinkToWeblate from "../Base/LinkToWeblate" export class Translation extends BaseUIElement { - - public static forcedLanguage = undefined; + public static forcedLanguage = undefined public readonly translations: Record<string, string> - context?: string; + context?: string constructor(translations: Record<string, string>, context?: string) { super() if (translations === undefined) { - console.error("Translation without content at "+context) + console.error("Translation without content at " + context) throw `Translation without content (${context})` } - this.context = translations["_context"] ?? context; - if(translations["_context"] !== undefined){ - translations = {...translations} + this.context = translations["_context"] ?? context + if (translations["_context"] !== undefined) { + translations = { ...translations } delete translations["_context"] } if (typeof translations === "string") { - translations = {"*": translations}; + translations = { "*": translations } } - let count = 0; + let count = 0 for (const translationsKey in translations) { if (!translations.hasOwnProperty(translationsKey)) { continue } - if(translationsKey === "_context"){ + if (translationsKey === "_context") { continue } - count++; - if (typeof (translations[translationsKey]) != "string") { + count++ + if (typeof translations[translationsKey] != "string") { console.error("Non-string object in translation: ", translations[translationsKey]) - throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n You probably put some other section accidentally in the translation" + throw ( + "Error in an object depicting a translation: a non-string object was found. (" + + context + + ")\n You probably put some other section accidentally in the translation" + ) } } - this.translations = translations; + this.translations = translations if (count === 0) { - console.error("Constructing a translation, but the object containing translations is empty "+context) + console.error( + "Constructing a translation, but the object containing translations is empty " + + context + ) throw `Constructing a translation, but the object containing translations is empty (${context})` } } get txt(): string { return this.textFor(Translation.forcedLanguage ?? Locale.language.data) - } - - public toString(){ - return this.txt; } - - static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] { - const allTranslations: { context: string, tr: Translation }[] = [] + + public toString() { + return this.txt + } + + static ExtractAllTranslationsFrom( + object: any, + context = "" + ): { context: string; tr: Translation }[] { + const allTranslations: { context: string; tr: Translation }[] = [] for (const key in object) { const v = object[key] if (v === undefined || v === null) { continue } if (v instanceof Translation) { - allTranslations.push({context: context + "." + key, tr: v}) + allTranslations.push({ context: context + "." + key, tr: v }) continue } if (typeof v === "object") { - allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key)) - + allTranslations.push( + ...Translation.ExtractAllTranslationsFrom(v, context + "." + key) + ) } } return allTranslations @@ -74,7 +84,7 @@ export class Translation extends BaseUIElement { static fromMap(transl: Map<string, string>) { const translations = {} - let hasTranslation = false; + let hasTranslation = false transl?.forEach((value, key) => { translations[key] = value hasTranslation = true @@ -82,38 +92,38 @@ export class Translation extends BaseUIElement { if (!hasTranslation) { return undefined } - return new Translation(translations); + return new Translation(translations) } Destroy() { - super.Destroy(); - this.isDestroyed = true; + super.Destroy() + this.isDestroyed = true } public textFor(language: string): string { if (this.translations["*"]) { - return this.translations["*"]; + return this.translations["*"] } - const txt = this.translations[language]; + const txt = this.translations[language] if (txt !== undefined) { - return txt; + return txt } - const en = this.translations["en"]; + const en = this.translations["en"] if (en !== undefined) { - return en; + return en } for (const i in this.translations) { if (!this.translations.hasOwnProperty(i)) { - continue; + continue } - return this.translations[i]; // Return a random language + return this.translations[i] // Return a random language } console.error("Missing language ", Locale.language.data, "for", this.translations) - return ""; + return "" } /** - * + * * // Should actually change the content based on the current language * const tr = new Translation({"en":"English", nl: "Nederlands"}) * Locale.language.setData("en") @@ -121,7 +131,7 @@ export class Translation extends BaseUIElement { * html.innerHTML // => "English" * Locale.language.setData("nl") * html.innerHTML // => "Nederlands" - * + * * // Should include a link to weblate if context is set * const tr = new Translation({"en":"English"}, "core:test.xyz") * Locale.language.setData("nl") @@ -135,88 +145,87 @@ export class Translation extends BaseUIElement { el.innerHTML = self.txt if (self.translations["*"] !== undefined) { - return el; + return el } - - - Locale.language.addCallback(_ => { + + Locale.language.addCallback((_) => { if (self.isDestroyed) { return true } el.innerHTML = self.txt }) - - if(self.context === undefined || self.context?.indexOf(":") < 0){ - return el; + + if (self.context === undefined || self.context?.indexOf(":") < 0) { + return el } const linkToWeblate = new LinkToWeblate(self.context, self.translations) const wrapper = document.createElement("span") wrapper.appendChild(el) - Locale.showLinkToWeblate.addCallbackAndRun(doShow => { - + Locale.showLinkToWeblate.addCallbackAndRun((doShow) => { if (!doShow) { - return; + return } wrapper.appendChild(linkToWeblate.ConstructElement()) - return true; + return true }) - - return wrapper ; + return wrapper } public SupportedLanguages(): string[] { const langs = [] for (const translationsKey in this.translations) { if (!this.translations.hasOwnProperty(translationsKey)) { - continue; + continue } if (translationsKey === "#") { - continue; + continue } if (!this.translations.hasOwnProperty(translationsKey)) { continue } langs.push(translationsKey) } - return langs; + return langs } public AllValues(): string[] { - return this.SupportedLanguages().map(lng => this.translations[lng]); + return this.SupportedLanguages().map((lng) => this.translations[lng]) } /** * Constructs a new Translation where every contained string has been modified */ - public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation { - const newTranslations = {}; + public OnEveryLanguage( + f: (s: string, language: string) => string, + context?: string + ): Translation { + const newTranslations = {} for (const lang in this.translations) { if (!this.translations.hasOwnProperty(lang)) { - continue; + continue } - newTranslations[lang] = f(this.translations[lang], lang); + newTranslations[lang] = f(this.translations[lang], lang) } - return new Translation(newTranslations, context ?? this.context); - + return new Translation(newTranslations, context ?? this.context) } - + /** * Replaces the given string with the given text in the language. * Other substitutions are left in place - * + * * const tr = new Translation( - * {"nl": "Een voorbeeldtekst met {key} en {key1}, en nogmaals {key}", + * {"nl": "Een voorbeeldtekst met {key} en {key1}, en nogmaals {key}", * "en": "Just a single {key}"}) * const r = tr.replace("{key}", "value") * r.textFor("nl") // => "Een voorbeeldtekst met value en {key1}, en nogmaals value" * r.textFor("en") // => "Just a single value" - * + * */ public replace(a: string, b: string) { - return this.OnEveryLanguage(str => str.replace(new RegExp(a, "g"), b)) + return this.OnEveryLanguage((str) => str.replace(new RegExp(a, "g"), b)) } public Clone() { @@ -224,24 +233,23 @@ export class Translation extends BaseUIElement { } FirstSentence() { - - const tr = {}; + const tr = {} for (const lng in this.translations) { if (!this.translations.hasOwnProperty(lng)) { continue } - let txt = this.translations[lng]; - txt = txt.replace(/\..*/, ""); - txt = Utils.EllipsesAfter(txt, 255); - tr[lng] = txt; + let txt = this.translations[lng] + txt = txt.replace(/\..*/, "") + txt = Utils.EllipsesAfter(txt, 255) + tr[lng] = txt } - return new Translation(tr); + return new Translation(tr) } /** * Extracts all images (including HTML-images) from all the embedded translations - * + * * // should detect sources of <img> * const tr = new Translation({en: "XYZ <img src='a.svg'/> XYZ <img src=\"some image.svg\"></img> XYZ <img src=b.svg/>"}) * new Set<string>(tr.ExtractImages(false)) // new Set(["a.svg", "b.svg", "some image.svg"]) @@ -250,18 +258,22 @@ export class Translation extends BaseUIElement { const allIcons: string[] = [] for (const key in this.translations) { if (!this.translations.hasOwnProperty(key)) { - continue; + continue } const render = this.translations[key] if (isIcon) { - const icons = render.split(";").filter(part => part.match(/(\.svg|\.png|\.jpg)$/) != null) + const icons = render + .split(";") + .filter((part) => part.match(/(\.svg|\.png|\.jpg)$/) != null) allIcons.push(...icons) } else if (!Utils.runningFromConsole) { // This might be a tagrendering containing some img as html const htmlElement = document.createElement("div") htmlElement.innerHTML = render - const images = Array.from(htmlElement.getElementsByTagName("img")).map(img => img.src) + const images = Array.from(htmlElement.getElementsByTagName("img")).map( + (img) => img.src + ) allIcons.push(...images) } else { // We are running this in ts-node (~= nodejs), and can not access document @@ -269,9 +281,12 @@ export class Translation extends BaseUIElement { try { const matches = render.match(/<img[^>]+>/g) if (matches != null) { - const sources = matches.map(img => img.match(/src=("[^"]+"|'[^']+'|[^/ ]+)/)) - .filter(match => match != null) - .map(match => match[1].trim().replace(/^['"]/, '').replace(/['"]$/, '')); + const sources = matches + .map((img) => img.match(/src=("[^"]+"|'[^']+'|[^/ ]+)/)) + .filter((match) => match != null) + .map((match) => + match[1].trim().replace(/^['"]/, "").replace(/['"]$/, "") + ) allIcons.push(...sources) } } catch (e) { @@ -280,18 +295,17 @@ export class Translation extends BaseUIElement { } } } - return allIcons.filter(icon => icon != undefined) + return allIcons.filter((icon) => icon != undefined) } AsMarkdown(): string { return this.txt } - } export class TypedTranslation<T> extends Translation { constructor(translations: Record<string, string>, context?: string) { - super(translations, context); + super(translations, context) } /** @@ -307,29 +321,30 @@ export class TypedTranslation<T> extends Translation { * const subbed = tr.Subs({part: subpart}) * subbed.textFor("en") // => "Full sentence with subpart" * subbed.textFor("nl") // => "Volledige zin met onderdeel" - * + * */ Subs(text: T, context?: string): Translation { return this.OnEveryLanguage((template, lang) => { - if(lang === "_context"){ + if (lang === "_context") { return template } - return Utils.SubstituteKeys(template, text, lang); + return Utils.SubstituteKeys(template, text, lang) }, context) } - - PartialSubs<X extends string>(text: Partial<T> & Record<X, string>): TypedTranslation<Omit<T, X>> { - const newTranslations : Record<string, string> = {} + PartialSubs<X extends string>( + text: Partial<T> & Record<X, string> + ): TypedTranslation<Omit<T, X>> { + const newTranslations: Record<string, string> = {} for (const lang in this.translations) { const template = this.translations[lang] - if(lang === "_context"){ - newTranslations[lang] = template + if (lang === "_context") { + newTranslations[lang] = template continue } newTranslations[lang] = Utils.SubstituteKeys(template, text, lang) } - + return new TypedTranslation<Omit<T, X>>(newTranslations, this.context) } -} \ No newline at end of file +} diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 56b48e2f7..6c5d5922d 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -1,117 +1,124 @@ -import {FixedUiElement} from "../Base/FixedUiElement"; -import {Translation, TypedTranslation} from "./Translation"; -import BaseUIElement from "../BaseUIElement"; +import { FixedUiElement } from "../Base/FixedUiElement" +import { Translation, TypedTranslation } from "./Translation" +import BaseUIElement from "../BaseUIElement" import * as known_languages from "../../assets/generated/used_languages.json" -import CompiledTranslations from "../../assets/generated/CompiledTranslations"; +import CompiledTranslations from "../../assets/generated/CompiledTranslations" export default class Translations { - - static readonly t : typeof CompiledTranslations.t & Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t; + static readonly t: typeof CompiledTranslations.t & Readonly<typeof CompiledTranslations.t> = + CompiledTranslations.t private static knownLanguages = new Set(known_languages.languages) constructor() { throw "Translations is static. If you want to intitialize a new translation, use the singular form" } public static W(s: string | number | BaseUIElement): BaseUIElement { - if (typeof (s) === "string") { - return new FixedUiElement(s); + if (typeof s === "string") { + return new FixedUiElement(s) } if (typeof s === "number") { return new FixedUiElement("" + s) } - return s; + return s } /** * Converts a string or an object into a typed translation. * Translation objects ('Translation' and 'TypedTranslation') are converted/returned - * + * * Translations.T("some text") // => new TypedTranslation({"*": "some text"}) * Translations.T("some text").txt // => "some text" * * const t = new Translation({"nl": "vertaling", "en": "translation"}) * Translations.T(t) // => new TypedTranslation<object>({"nl": "vertaling", "en": "translation"}) - * + * * const t = new TypedTranslation({"nl": "vertaling", "en": "translation"}) * Translations.T(t) // => t - * + * * const json: any = {"en": "English", "nl": "Nederlands"}; * const translation = Translations.T(new Translation(json)); * translation.textFor("en") // => "English" * translation.textFor("nl") // => "Nederlands" - * + * */ static T(t: string | any, context = undefined): TypedTranslation<object> { if (t === undefined || t === null) { - return undefined; + return undefined } if (typeof t === "number") { t = "" + t } if (typeof t === "string") { - return new TypedTranslation<object>({"*": t}, context); + return new TypedTranslation<object>({ "*": t }, context) } if (t.render !== undefined) { - const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly" - console.error(msg, t); + const msg = + "Creating a translation, but this object contains a 'render'-field. Use the translation directly" + console.error(msg, t) throw msg } if (t instanceof TypedTranslation) { - return t; + return t } - if(t instanceof Translation){ + if (t instanceof Translation) { return new TypedTranslation<object>(t.translations) } - return new TypedTranslation<object>(t, context); + return new TypedTranslation<object>(t, context) } - public static CountTranslations() { - const queue: any = [Translations.t]; - const tr: Translation[] = []; + const queue: any = [Translations.t] + const tr: Translation[] = [] while (queue.length > 0) { - const item = queue.pop(); + const item = queue.pop() if (item instanceof Translation || item.translations !== undefined) { - tr.push(item); - } else if (typeof (item) === "string") { - console.warn("Got single string in translationgs file: ", item); + tr.push(item) + } else if (typeof item === "string") { + console.warn("Got single string in translationgs file: ", item) } else { for (const t in item) { - const x = item[t]; + const x = item[t] queue.push(x) } } } - const langaugeCounts = {}; + const langaugeCounts = {} for (const translation of tr) { for (const language in translation.translations) { if (langaugeCounts[language] === undefined) { langaugeCounts[language] = 1 } else { - langaugeCounts[language]++; + langaugeCounts[language]++ } } } for (const language in langaugeCounts) { - console.log("Total translations in ", language, langaugeCounts[language], "/", tr.length) + console.log( + "Total translations in ", + language, + langaugeCounts[language], + "/", + tr.length + ) } - } static isProbablyATranslation(transl: any) { - if(typeof transl !== "object"){ - return false; + if (typeof transl !== "object") { + return false } - if(Object.keys(transl).length == 0){ + if (Object.keys(transl).length == 0) { // No translations found; not a translation return false } // is a weird key found? - if(Object.keys(transl).some(key => key !== '_context' && !this.knownLanguages.has(key))){ + if ( + Object.keys(transl).some((key) => key !== "_context" && !this.knownLanguages.has(key)) + ) { return false } - - return true; + + return true } } diff --git a/Utils.ts b/Utils.ts index e72df84ef..2ccbdf90c 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1,15 +1,17 @@ import * as colors from "./assets/colors.json" export class Utils { - /** * In the 'deploy'-step, some code needs to be run by ts-node. * However, ts-node crashes when it sees 'document'. When running from console, we flag this and disable all code where document is needed. * This is a workaround and yet another hack */ - public static runningFromConsole = typeof window === "undefined"; - public static readonly assets_path = "./assets/svg/"; - public static externalDownloadFunction: (url: string, headers?: any) => Promise<{ content: string } | { redirect: string }>; + public static runningFromConsole = typeof window === "undefined" + public static readonly assets_path = "./assets/svg/" + public static externalDownloadFunction: ( + url: string, + headers?: any + ) => Promise<{ content: string } | { redirect: string }> public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. @@ -46,29 +48,125 @@ There are also some technicalities in your theme to keep in mind: The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org` - private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] - private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] + private static knownKeys = [ + "addExtraTags", + "and", + "calculatedTags", + "changesetmessage", + "clustering", + "color", + "condition", + "customCss", + "dashArray", + "defaultBackgroundId", + "description", + "descriptionTail", + "doNotDownload", + "enableAddNewPoints", + "enableBackgroundLayerSelection", + "enableGeolocation", + "enableLayers", + "enableMoreQuests", + "enableSearch", + "enableShareScreen", + "enableUserBadge", + "freeform", + "hideFromOverview", + "hideInAnswer", + "icon", + "iconOverlays", + "iconSize", + "id", + "if", + "ifnot", + "isShown", + "key", + "language", + "layers", + "lockLocation", + "maintainer", + "mappings", + "maxzoom", + "maxZoom", + "minNeededElements", + "minzoom", + "multiAnswer", + "name", + "or", + "osmTags", + "passAllFeatures", + "presets", + "question", + "render", + "roaming", + "roamingRenderings", + "rotation", + "shortDescription", + "socialImage", + "source", + "startLat", + "startLon", + "startZoom", + "tagRenderings", + "tags", + "then", + "title", + "titleIcons", + "type", + "version", + "wayHandling", + "widenFactor", + "width", + ] + private static extraKeys = [ + "nl", + "en", + "fr", + "de", + "pt", + "es", + "name", + "phone", + "email", + "amenity", + "leisure", + "highway", + "building", + "yes", + "no", + "true", + "false", + ] private static injectedDownloads = {} - private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>() + private static _download_cache = new Map<string, { promise: Promise<any>; timestamp: number }>() /** * Parses the arguments for special visualisations */ - public static ParseVisArgs(specs: { name: string, defaultValue?: string }[], args: string[]): any { - const parsed = {}; + public static ParseVisArgs( + specs: { name: string; defaultValue?: string }[], + args: string[] + ): any { + const parsed = {} if (args.length > specs.length) { - throw "To much arguments for special visualization: got " + args.join(",") + " but expected only " + args.length + " arguments" + throw ( + "To much arguments for special visualization: got " + + args.join(",") + + " but expected only " + + args.length + + " arguments" + ) } for (let i = 0; i < specs.length; i++) { - const spec = specs[i]; - let arg = args[i]?.trim(); + const spec = specs[i] + let arg = args[i]?.trim() if (arg === undefined || arg === "") { arg = spec.defaultValue } parsed[spec.name] = arg } - return parsed; + return parsed } static EncodeXmlValue(str) { @@ -76,11 +174,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be str = "" + str } - return str.replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') + return str + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") } /** @@ -89,24 +188,24 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ static asFloat(str): number { if (str) { - const i = parseFloat(str); + const i = parseFloat(str) if (isNaN(i)) { - return undefined; + return undefined } - return i; + return i } - return undefined; + return undefined } public static Upper(str: string) { - return str.substr(0, 1).toUpperCase() + str.substr(1); + return str.substr(0, 1).toUpperCase() + str.substr(1) } public static TwoDigits(i: number) { if (i < 10) { - return "0" + i; + return "0" + i } - return "" + i; + return "" + i } /** @@ -126,62 +225,62 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static Round(i: number): string { if (i < 0) { - return "-" + Utils.Round(-i); + return "-" + Utils.Round(-i) } - const j = "" + Math.floor(i * 10); + const j = "" + Math.floor(i * 10) if (j.length == 1) { - return "0." + j; + return "0." + j } - return j.substr(0, j.length - 1) + "." + j.substr(j.length - 1, j.length); + return j.substr(0, j.length - 1) + "." + j.substr(j.length - 1, j.length) } - public static Times(f: ((i: number) => string), count: number): string { - let res = ""; + public static Times(f: (i: number) => string, count: number): string { + let res = "" for (let i = 0; i < count; i++) { - res += f(i); + res += f(i) } - return res; + return res } - public static TimesT<T>(count: number, f: ((i: number) => T)): T[] { - let res: T[] = []; + public static TimesT<T>(count: number, f: (i: number) => T): T[] { + let res: T[] = [] for (let i = 0; i < count; i++) { - res.push(f(i)); + res.push(f(i)) } - return res; + return res } public static NoNull<T>(array: T[]): NonNullable<T>[] { - return <any> array?.filter(o => o !== undefined && o !== null) + return <any>array?.filter((o) => o !== undefined && o !== null) } public static Hist(array: string[]): Map<string, number> { - const hist = new Map<string, number>(); + const hist = new Map<string, number>() for (const s of array) { hist.set(s, 1 + (hist.get(s) ?? 0)) } - return hist; + return hist } public static NoEmpty(array: string[]): string[] { - const ls: string[] = []; + const ls: string[] = [] for (const t of array) { if (t === "") { - continue; + continue } - ls.push(t); + ls.push(t) } - return ls; + return ls } public static EllipsesAfter(str: string, l: number = 100) { if (str === undefined || str === null) { - return undefined; + return undefined } if (str.length <= l) { - return str; + return str } - return str.substr(0, l - 3) + "..."; + return str.substr(0, l - 3) + "..." } public static FixedLength(str: string, l: number) { @@ -189,35 +288,35 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be while (str.length < l) { str = " " + str } - return str; + return str } public static Dedup(arr: string[]): string[] { if (arr === undefined) { - return undefined; + return undefined } - const newArr = []; + const newArr = [] for (const string of arr) { if (newArr.indexOf(string) < 0) { - newArr.push(string); + newArr.push(string) } } - return newArr; + return newArr } public static Dupiclates(arr: string[]): string[] { if (arr === undefined) { - return undefined; + return undefined } - const newArr = []; - const seen = new Set<string>(); + const newArr = [] + const seen = new Set<string>() for (const string of arr) { if (seen.has(string)) { newArr.push(string) } seen.add(string) } - return newArr; + return newArr } /** @@ -234,7 +333,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be result.push(value) } } - return result; + return result } /** @@ -252,29 +351,29 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return false } } - return true; + return true } /** * Utils.MergeTags({k0:"v0","common":"0"},{k1:"v1", common: "1"}) // => {k0: "v0", k1:"v1", common: "1"} */ public static MergeTags(a: any, b: any) { - const t = {}; + const t = {} for (const k in a) { - t[k] = a[k]; + t[k] = a[k] } for (const k in b) { - t[k] = b[k]; + t[k] = b[k] } - return t; + return t } public static SplitFirst(a: string, sep: string): string[] { - const index = a.indexOf(sep); + const index = a.indexOf(sep) if (index < 0) { - return [a]; + return [a] } - return [a.substr(0, index), a.substr(index + sep.length)]; + return [a.substr(0, index), a.substr(index + sep.length)] } /** @@ -284,7 +383,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * @param useLang * @constructor */ - public static SubstituteKeys(txt: string | undefined, tags?: Record<string, any>, useLang?: string): string | undefined { + public static SubstituteKeys( + txt: string | undefined, + tags?: Record<string, any>, + useLang?: string + ): string | undefined { if (txt === undefined) { return undefined } @@ -296,20 +399,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be const key = match[1] let v = tags === undefined ? undefined : tags[key] if (v !== undefined) { - if (v["toISOString"] != undefined) { // This is a date, probably the timestamp of the object // @ts-ignore - const date: Date = el; + const date: Date = el v = date.toISOString() } if (useLang !== undefined && v?.translations !== undefined) { - v = v.translations[useLang] ?? v.translations["*"] ?? (v.textFor !== undefined ? v.textFor(useLang) : v); + v = + v.translations[useLang] ?? + v.translations["*"] ?? + (v.textFor !== undefined ? v.textFor(useLang) : v) } if (v.InnerConstructElement !== undefined) { - console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key, "\nThe value is", v) + console.warn( + "SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", + key, + "\nThe value is", + v + ) v = (<HTMLElement>v.InnerConstructElement())?.textContent } @@ -318,25 +428,25 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } v = v.replace(/\n/g, "<br/>") } else { - // v === undefined + // v === undefined v = "" } txt = txt.replace("{" + key + "}", v) match = txt.match(regex) } - return txt; + return txt } public static LoadCustomCss(location: string) { - const head = document.getElementsByTagName('head')[0]; - const link = document.createElement('link'); - link.id = "customCss"; - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = location; - link.media = 'all'; - head.appendChild(link); + const head = document.getElementsByTagName("head")[0] + const link = document.createElement("link") + link.id = "customCss" + link.rel = "stylesheet" + link.type = "text/css" + link.href = location + link.media = "all" + head.appendChild(link) console.log("Added custom css file ", location) } @@ -378,7 +488,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * result.list2.length // => 1 * result.list2[0] // => "should-be-untouched" */ - static Merge<T, S>(source: S, target: T): (T & S) { + static Merge<T, S>(source: S, target: T): T & S { if (target === null) { return <T & S>source } @@ -388,17 +498,17 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be continue } if (key.startsWith("=")) { - const trimmedKey = key.substr(1); + const trimmedKey = key.substr(1) target[trimmedKey] = source[key] continue } if (key.startsWith("+") || key.endsWith("+")) { - const trimmedKey = key.replace("+", ""); - const sourceV = source[key]; - const targetV = (target[trimmedKey] ?? []) + const trimmedKey = key.replace("+", "") + const sourceV = source[key] + const targetV = target[trimmedKey] ?? [] - let newList: any[]; + let newList: any[] if (key.startsWith("+")) { // @ts-ignore newList = sourceV.concat(targetV) @@ -406,8 +516,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be newList = targetV.concat(sourceV) } - target[trimmedKey] = newList; - continue; + target[trimmedKey] = newList + continue } const sourceV = source[key] @@ -419,22 +529,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be target[key] = null } else if (targetV === undefined) { // @ts-ignore - target[key] = sourceV; + target[key] = sourceV } else { - Utils.Merge(sourceV, targetV); + Utils.Merge(sourceV, targetV) } - } else { // @ts-ignore - target[key] = sourceV; + target[key] = sourceV } - } // @ts-ignore - return target; + return target } - /** * Walks the specified path into the object till the end. * @@ -442,35 +549,41 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * * The leaf objects are replaced in the object itself by the specified function */ - public static WalkPath(path: string[], object: any, replaceLeaf: ((leaf: any, travelledPath: string[]) => any), travelledPath: string[] = []) : void { - if(object == null){ - return; + public static WalkPath( + path: string[], + object: any, + replaceLeaf: (leaf: any, travelledPath: string[]) => any, + travelledPath: string[] = [] + ): void { + if (object == null) { + return } - + const head = path[0] if (path.length === 1) { // We have reached the leaf - const leaf = object[head]; + const leaf = object[head] if (leaf !== undefined) { if (Array.isArray(leaf)) { - object[head] = leaf.map(o => replaceLeaf(o, travelledPath)) + object[head] = leaf.map((o) => replaceLeaf(o, travelledPath)) } else { object[head] = replaceLeaf(leaf, travelledPath) } } return - } const sub = object[head] if (sub === undefined) { - return; + return } if (typeof sub !== "object") { - return; + return } if (Array.isArray(sub)) { - sub.forEach((el, i) => Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i])) - return; + sub.forEach((el, i) => + Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]) + ) + return } Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, head]) } @@ -481,39 +594,46 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * * The leaf objects are collected in the list */ - public static CollectPath(path: string[], object: any, collectedList: { leaf: any, path: string[] }[] = [], travelledPath: string[] = []): { leaf: any, path: string[] }[] { + public static CollectPath( + path: string[], + object: any, + collectedList: { leaf: any; path: string[] }[] = [], + travelledPath: string[] = [] + ): { leaf: any; path: string[] }[] { if (object === undefined || object === null) { - return collectedList; + return collectedList } const head = path[0] travelledPath = [...travelledPath, head] if (path.length === 1) { // We have reached the leaf - const leaf = object[head]; + const leaf = object[head] if (leaf === undefined || leaf === null) { return collectedList } if (Array.isArray(leaf)) { for (let i = 0; i < (<any[]>leaf).length; i++) { - const l = (<any[]>leaf)[i]; - collectedList.push({leaf: l, path: [...travelledPath, "" + i]}) + const l = (<any[]>leaf)[i] + collectedList.push({ leaf: l, path: [...travelledPath, "" + i] }) } } else { - collectedList.push({leaf, path: travelledPath}) + collectedList.push({ leaf, path: travelledPath }) } return collectedList } const sub = object[head] if (sub === undefined || sub === null) { - return collectedList; + return collectedList } if (Array.isArray(sub)) { - sub.forEach((el, i) => Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i])) - return collectedList; + sub.forEach((el, i) => + Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]) + ) + return collectedList } if (typeof sub !== "object") { - return collectedList; + return collectedList } return Utils.CollectPath(path.slice(1), sub, collectedList, travelledPath) } @@ -548,7 +668,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * }, (x) => return x}, _ => false) * walked // => {v: "value", u: undefined, n: null} */ - static WalkJson(json: any, f: (v: object | number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, path: string[] = []) { + static WalkJson( + json: any, + f: (v: object | number | string | boolean | undefined, path: string[]) => any, + isLeaf: (object) => boolean = undefined, + path: string[] = [] + ) { if (json === undefined || json === null) { return f(json, path) } @@ -566,11 +691,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } if (Array.isArray(json)) { return json.map((sub, i) => { - return Utils.WalkJson(sub, f, isLeaf, [...path, "" + i]); + return Utils.WalkJson(sub, f, isLeaf, [...path, "" + i]) }) } - const cp = {...json} + const cp = { ...json } for (const key in json) { cp[key] = Utils.WalkJson(json[key], f, isLeaf, [...path, key]) } @@ -582,9 +707,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * * Will hang on objects with loops */ - static WalkObject(json: any, collect: (v: number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, path = []): void { + static WalkObject( + json: any, + collect: (v: number | string | boolean | undefined, path: string[]) => any, + isLeaf: (object) => boolean = undefined, + path = [] + ): void { if (json === undefined) { - return; + return } const jtp = typeof json if (isLeaf !== undefined) { @@ -601,7 +731,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } if (Array.isArray(json)) { json.map((sub, i) => { - return Utils.WalkObject(sub, collect, isLeaf, [...path, i]); + return Utils.WalkObject(sub, collect, isLeaf, [...path, i]) }) return } @@ -611,45 +741,43 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } } - static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) { - let found = dict.get(k); + let found = dict.get(k) if (found !== undefined) { - return found; + return found } - dict.set(k, v()); - return dict.get(k); + dict.set(k, v()) + return dict.get(k) } /** * Tries to minify the given JSON by applying some compression */ public static MinifyJSON(stringified: string): string { - stringified = stringified.replace(/\|/g, "||"); + stringified = stringified.replace(/\|/g, "||") - const keys = Utils.knownKeys.concat(Utils.extraKeys); + const keys = Utils.knownKeys.concat(Utils.extraKeys) for (let i = 0; i < keys.length; i++) { - const knownKey = keys[i]; - let code = i; + const knownKey = keys[i] + let code = i if (i >= 124) { - code += 1; // Character 127 is our 'escape' character | + code += 1 // Character 127 is our 'escape' character | } let replacement = "|" + String.fromCharCode(code) - stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement); + stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement) } - return stringified; + return stringified } public static UnMinify(minified: string): string { - if (minified === undefined || minified === null) { - return undefined; + return undefined } - const parts = minified.split("|"); - let result = parts.shift(); - const keys = Utils.knownKeys.concat(Utils.extraKeys); + const parts = minified.split("|") + let result = parts.shift() + const keys = Utils.knownKeys.concat(Utils.extraKeys) for (const part of parts) { if (part == "") { @@ -657,11 +785,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be result += "|" continue } - const i = part.charCodeAt(0); - result += "\"" + keys[i] + "\":" + part.substring(1) + const i = part.charCodeAt(0) + result += '"' + keys[i] + '":' + part.substring(1) } - return result; + return result } public static injectJsonDownloadForTests(url: string, data) { @@ -677,74 +805,79 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * @param url * @param headers */ - public static downloadAdvanced(url: string, headers?: any): Promise<{ content: string } | { redirect: string }> { + public static downloadAdvanced( + url: string, + headers?: any + ): Promise<{ content: string } | { redirect: string }> { if (this.externalDownloadFunction !== undefined) { return this.externalDownloadFunction(url, headers) } return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.onload = () => { - if (xhr.status == 200) { - resolve({content: xhr.response}) - } else if (xhr.status === 302) { - resolve({redirect: xhr.getResponseHeader("location")}) - } else if (xhr.status === 509 || xhr.status === 429) { - reject("rate limited") - } else { - reject(xhr.statusText) - } - }; - xhr.open('GET', url); - if (headers !== undefined) { - for (const key in headers) { - xhr.setRequestHeader(key, headers[key]) - } + const xhr = new XMLHttpRequest() + xhr.onload = () => { + if (xhr.status == 200) { + resolve({ content: xhr.response }) + } else if (xhr.status === 302) { + resolve({ redirect: xhr.getResponseHeader("location") }) + } else if (xhr.status === 509 || xhr.status === 429) { + reject("rate limited") + } else { + reject(xhr.statusText) } - - xhr.send(); - xhr.onerror = reject } - ) + xhr.open("GET", url) + if (headers !== undefined) { + for (const key in headers) { + xhr.setRequestHeader(key, headers[key]) + } + } + + xhr.send() + xhr.onerror = reject + }) } public static upload(url: string, data, headers?: any): Promise<string> { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.onload = () => { - if (xhr.status == 200) { - resolve(xhr.response) - } else if (xhr.status === 509 || xhr.status === 429) { - reject("rate limited") - } else { - reject(xhr.statusText) - } - }; - xhr.open('POST', url); - if (headers !== undefined) { - - for (const key in headers) { - xhr.setRequestHeader(key, headers[key]) - } + const xhr = new XMLHttpRequest() + xhr.onload = () => { + if (xhr.status == 200) { + resolve(xhr.response) + } else if (xhr.status === 509 || xhr.status === 429) { + reject("rate limited") + } else { + reject(xhr.statusText) } - - xhr.send(data); - xhr.onerror = reject } - ) + xhr.open("POST", url) + if (headers !== undefined) { + for (const key in headers) { + xhr.setRequestHeader(key, headers[key]) + } + } + + xhr.send(data) + xhr.onerror = reject + }) } - - public static async downloadJsonCached(url: string, maxCacheTimeMs: number, headers?: any): Promise<any> { + public static async downloadJsonCached( + url: string, + maxCacheTimeMs: number, + headers?: any + ): Promise<any> { const cached = Utils._download_cache.get(url) if (cached !== undefined) { - if ((new Date().getTime() - cached.timestamp) <= maxCacheTimeMs) { + if (new Date().getTime() - cached.timestamp <= maxCacheTimeMs) { return cached.promise } } - const promise = /*NO AWAIT as we work with the promise directly */Utils.downloadJson(url, headers) - Utils._download_cache.set(url, {promise, timestamp: new Date().getTime()}) + const promise = /*NO AWAIT as we work with the promise directly */ Utils.downloadJson( + url, + headers + ) + Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }) return await promise } @@ -754,7 +887,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be console.log("Using injected resource for test for URL", url) return new Promise((resolve, _) => resolve(injected)) } - const data = await Utils.download(url, Utils.Merge({"accept": "application/json"}, headers ?? {})) + const data = await Utils.download( + url, + Utils.Merge({ accept: "application/json" }, headers ?? {}) + ) try { if (typeof data === "string") { return JSON.parse(data) @@ -762,46 +898,57 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return data } catch (e) { console.error("Could not parse ", data, "due to", e, "\n", e.stack) - throw e; + throw e } } /** * Triggers a 'download file' popup which will download the contents */ - public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt", - options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" | "application/json" }) { - const element = document.createElement("a"); - let file; - if (typeof (contents) === "string") { - file = new Blob([contents], {type: options?.mimetype ?? 'text/plain'}); - } else { - file = contents; + public static offerContentsAsDownloadableFile( + contents: string | Blob, + fileName: string = "download.txt", + options?: { + mimetype: + | string + | "text/plain" + | "text/csv" + | "application/vnd.geo+json" + | "{gpx=application/gpx+xml}" + | "application/json" } - element.href = URL.createObjectURL(file); - element.download = fileName; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); + ) { + const element = document.createElement("a") + let file + if (typeof contents === "string") { + file = new Blob([contents], { type: options?.mimetype ?? "text/plain" }) + } else { + file = contents + } + element.href = URL.createObjectURL(file) + element.download = fileName + document.body.appendChild(element) // Required for this to work in FireFox + element.click() } public static ColourNameToHex(color: string): string { - return colors[color.toLowerCase()] ?? color; + return colors[color.toLowerCase()] ?? color } public static HexToColourName(hex: string): string { hex = hex.toLowerCase() if (!hex.startsWith("#")) { - return hex; + return hex } - const c = Utils.color(hex); + const c = Utils.color(hex) - let smallestDiff = Number.MAX_VALUE; - let bestColor = undefined; + let smallestDiff = Number.MAX_VALUE + let bestColor = undefined for (const color in colors) { if (!colors.hasOwnProperty(color)) { - continue; + continue } - const foundhex = colors[color]; + const foundhex = colors[color] if (typeof foundhex !== "string") { continue } @@ -810,14 +957,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } const diff = this.colorDiff(Utils.color(foundhex), c) if (diff > 50) { - continue; + continue } if (diff < smallestDiff) { - smallestDiff = diff; - bestColor = color; + smallestDiff = diff + bestColor = color } } - return bestColor ?? hex; + return bestColor ?? hex } /** @@ -842,7 +989,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static async waitFor(timeMillis: number): Promise<void> { return new Promise((resolve) => { - window.setTimeout(resolve, timeMillis); + window.setTimeout(resolve, timeMillis) }) } @@ -862,24 +1009,34 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static DisableLongPresses() { // Remove all context event listeners on mobile to prevent long presses - window.addEventListener('contextmenu', (e) => { // Not compatible with IE < 9 - - if (e.target["nodeName"] === "INPUT") { - return; - } - e.preventDefault(); - return false; - }, false); + window.addEventListener( + "contextmenu", + (e) => { + // Not compatible with IE < 9 + if (e.target["nodeName"] === "INPUT") { + return + } + e.preventDefault() + return false + }, + false + ) } public static OsmChaLinkFor(daysInThePast, theme = undefined): string { const now = new Date() const lastWeek = new Date(now.getTime() - daysInThePast * 24 * 60 * 60 * 1000) - const date = lastWeek.getFullYear() + "-" + Utils.TwoDigits(lastWeek.getMonth() + 1) + "-" + Utils.TwoDigits(lastWeek.getDate()) + const date = + lastWeek.getFullYear() + + "-" + + Utils.TwoDigits(lastWeek.getMonth() + 1) + + "-" + + Utils.TwoDigits(lastWeek.getDate()) let osmcha_link = `"date__gte":[{"label":"${date}","value":"${date}"}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}]` if (theme !== undefined) { - osmcha_link = osmcha_link + "," + `"comment":[{"label":"#${theme}","value":"#${theme}"}]` + osmcha_link = + osmcha_link + "," + `"comment":[{"label":"#${theme}","value":"#${theme}"}]` } return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}") } @@ -891,9 +1048,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ static Clone<T>(x: T): T { if (x === undefined) { - return undefined; + return undefined } - return JSON.parse(JSON.stringify(x)); + return JSON.parse(JSON.stringify(x)) } public static ParseDate(str: string): Date { @@ -903,81 +1060,94 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return new Date(str) } - public static sortedByLevenshteinDistance<T>(reference: string, ts: T[], getName: (t: T) => string): T[] { - const withDistance: [T, number][] = ts.map(t => [t, Utils.levenshteinDistance(getName(t), reference)]) + public static sortedByLevenshteinDistance<T>( + reference: string, + ts: T[], + getName: (t: T) => string + ): T[] { + const withDistance: [T, number][] = ts.map((t) => [ + t, + Utils.levenshteinDistance(getName(t), reference), + ]) withDistance.sort(([_, a], [__, b]) => a - b) - return withDistance.map(n => n[0]) + return withDistance.map((n) => n[0]) } public static levenshteinDistance(str1: string, str2: string) { - const track = Array(str2.length + 1).fill(null).map(() => - Array(str1.length + 1).fill(null)); + const track = Array(str2.length + 1) + .fill(null) + .map(() => Array(str1.length + 1).fill(null)) for (let i = 0; i <= str1.length; i += 1) { - track[0][i] = i; + track[0][i] = i } for (let j = 0; j <= str2.length; j += 1) { - track[j][0] = j; + track[j][0] = j } for (let j = 1; j <= str2.length; j += 1) { for (let i = 1; i <= str1.length; i += 1) { - const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; + const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1 track[j][i] = Math.min( track[j][i - 1] + 1, // deletion track[j - 1][i] + 1, // insertion - track[j - 1][i - 1] + indicator, // substitution - ); + track[j - 1][i - 1] + indicator // substitution + ) } } - return track[str2.length][str1.length]; + return track[str2.length][str1.length] } - public static MapToObj<V, T>(d: Map<string, V>, onValue: ((t: V, key: string) => T)): Record<string, T> { + public static MapToObj<V, T>( + d: Map<string, V>, + onValue: (t: V, key: string) => T + ): Record<string, T> { const o = {} const keys = Array.from(d.keys()) - keys.sort(); + keys.sort() for (const key of keys) { - o[key] = onValue(d.get(key), key); + o[key] = onValue(d.get(key), key) } return o } /** * Switches keys and values around - * + * * Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]} */ - public static TransposeMap<K extends string, V extends string>(d: Record<K, V[]>) : Record<V, K[]>{ - const newD : Record<V, K[]> = <any> {}; + public static TransposeMap<K extends string, V extends string>( + d: Record<K, V[]> + ): Record<V, K[]> { + const newD: Record<V, K[]> = <any>{} for (const k in d) { const vs = d[k] for (let v of vs) { const list = newD[v] - if(list === undefined){ + if (list === undefined) { newD[v] = [k] // Left: indexing; right: list with one element - }else{ + } else { list.push(k) } } } - return newD; + return newD } /** * Utils.colorAsHex({r: 255, g: 128, b: 0}) // => "#ff8000" * Utils.colorAsHex(undefined) // => undefined */ - public static colorAsHex(c: { r: number, g: number, b: number }) { + public static colorAsHex(c: { r: number; g: number; b: number }) { if (c === undefined) { return undefined } function componentToHex(n) { - let hex = n.toString(16); - return hex.length == 1 ? "0" + hex : hex; + let hex = n.toString(16) + return hex.length == 1 ? "0" + hex : hex } - return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b); + return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b) } /** @@ -987,7 +1157,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * Utils.color(" rgba (12,34,56,0.5) ") // => {r: 12, g:34, b: 56} * Utils.color(undefined) // => undefined */ - public static color(hex: string): { r: number, g: number, b: number } { + public static color(hex: string): { r: number; g: number; b: number } { if (hex === undefined) { return undefined } @@ -997,11 +1167,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (match == undefined) { return undefined } - return {r: Number(match[1]), g: Number(match[2]), b: Number(match[3])} + return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) } } if (!hex.startsWith("#")) { - return undefined; + return undefined } if (hex.length === 4) { return { @@ -1018,7 +1188,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } } - public static asDict(tags: { key: string, value: string | number }[]): Map<string, string | number> { + public static asDict( + tags: { key: string; value: string | number }[] + ): Map<string, string | number> { const d = new Map<string, string | number>() for (const tag of tags) { @@ -1028,23 +1200,25 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return d } - private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { - return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); + private static colorDiff( + c0: { r: number; g: number; b: number }, + c1: { r: number; g: number; b: number } + ) { + return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b) } - static toIdRecord<T extends {id: string}>(ts: T[]): Record<string, T> { - const result : Record<string, T> = {} + static toIdRecord<T extends { id: string }>(ts: T[]): Record<string, T> { + const result: Record<string, T> = {} for (const t of ts) { result[t.id] = t } return result } - - public static SetMidnight(d : Date): void{ + + public static SetMidnight(d: Date): void { d.setUTCHours(0) d.setUTCSeconds(0) d.setUTCMilliseconds(0) d.setUTCMinutes(0) } } - diff --git a/Utils/LanguageUtils.ts b/Utils/LanguageUtils.ts index 045e5e2da..734a4725f 100644 --- a/Utils/LanguageUtils.ts +++ b/Utils/LanguageUtils.ts @@ -1,10 +1,8 @@ import * as used_languages from "../assets/generated/used_languages.json" export default class LanguageUtils { - /** * All the languages there is currently language support for in MapComplete */ - public static readonly usedLanguages : Set<string> = new Set(used_languages.languages) + public static readonly usedLanguages: Set<string> = new Set(used_languages.languages) } - diff --git a/Utils/WikidataUtils.ts b/Utils/WikidataUtils.ts index 8b0146060..5f934a16a 100644 --- a/Utils/WikidataUtils.ts +++ b/Utils/WikidataUtils.ts @@ -1,14 +1,12 @@ - export default class WikidataUtils { - /** * Mapping from wikidata-codes to weblate-codes. The wikidata-code is the key, mapcomplete/weblate is the value */ public static readonly languageRemapping = { - "nb":"nb_NO", - "zh-hant":"zh_Hant", - "zh-hans":"zh_Hans", - "pt-br":"pt_BR" + nb: "nb_NO", + "zh-hant": "zh_Hant", + "zh-hans": "zh_Hans", + "pt-br": "pt_BR", } /** @@ -17,23 +15,25 @@ export default class WikidataUtils { * @param data * @param remapLanguages */ - public static extractLanguageData(data: {lang: {value:string}, code: {value: string}, label: {value: string}} [], remapLanguages: Record<string, string>): Map<string, Map<string, string>>{ - console.log("Got "+data.length+" entries") - const perId = new Map<string, Map<string, string>>(); + public static extractLanguageData( + data: { lang: { value: string }; code: { value: string }; label: { value: string } }[], + remapLanguages: Record<string, string> + ): Map<string, Map<string, string>> { + console.log("Got " + data.length + " entries") + const perId = new Map<string, Map<string, string>>() for (const element of data) { let id = element.code.value id = remapLanguages[id] ?? id let labelLang = element.label["xml:lang"] labelLang = remapLanguages[labelLang] ?? labelLang const value = element.label.value - if(!perId.has(id)){ + if (!perId.has(id)) { perId.set(id, new Map<string, string>()) } perId.get(id).set(labelLang, value) } - console.log("Got "+perId.size+" languages") + console.log("Got " + perId.size + " languages") return perId } - -} \ No newline at end of file +} diff --git a/all_themes_index.ts b/all_themes_index.ts index d359f08d6..88eb51251 100644 --- a/all_themes_index.ts +++ b/all_themes_index.ts @@ -1,32 +1,47 @@ -import {Utils} from "./Utils"; -import AllThemesGui from "./UI/AllThemesGui"; -import {QueryParameters} from "./Logic/Web/QueryParameters"; -import StatisticsGUI from "./UI/StatisticsGUI"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; - +import { Utils } from "./Utils" +import AllThemesGui from "./UI/AllThemesGui" +import { QueryParameters } from "./Logic/Web/QueryParameters" +import StatisticsGUI from "./UI/StatisticsGUI" +import { FixedUiElement } from "./UI/Base/FixedUiElement" const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? "" const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? "" -const l = window.location; +const l = window.location if (layout !== "") { if (window.location.host.startsWith("127.0.0.1")) { - window.location.replace(l.protocol + "//" + window.location.host + "/theme.html" + l.search + "&layout=" + layout + l.hash); + window.location.replace( + l.protocol + + "//" + + window.location.host + + "/theme.html" + + l.search + + "&layout=" + + layout + + l.hash + ) } else { - window.location.replace(l.protocol + "//" + window.location.host + "/" + layout + ".html" + l.search + l.hash); + window.location.replace( + l.protocol + "//" + window.location.host + "/" + layout + ".html" + l.search + l.hash + ) } } else if (customLayout !== "") { - window.location.replace(l.protocol + "//" + window.location.host + "/theme.html" + l.search + l.hash); + window.location.replace( + l.protocol + "//" + window.location.host + "/theme.html" + l.search + l.hash + ) } - Utils.DisableLongPresses() -document.getElementById("decoration-desktop").remove(); -const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'statistics'") +document.getElementById("decoration-desktop").remove() +const mode = QueryParameters.GetQueryParameter( + "mode", + "map", + "The mode the application starts in, e.g. 'statistics'" +) if (mode.data === "statistics") { console.log("Statistics mode!") new FixedUiElement("").AttachTo("centermessage") new StatisticsGUI().SetClass("w-full h-full pointer-events-auto").AttachTo("topleft-tools") -} else{ - new AllThemesGui().setup(); +} else { + new AllThemesGui().setup() } diff --git a/index.ts b/index.ts index b21feaa1f..baad5d910 100644 --- a/index.ts +++ b/index.ts @@ -1,46 +1,49 @@ -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import Combine from "./UI/Base/Combine"; -import MinimapImplementation from "./UI/Base/MinimapImplementation"; -import {Utils} from "./Utils"; -import AllThemesGui from "./UI/AllThemesGui"; -import DetermineLayout from "./Logic/DetermineLayout"; -import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; -import DefaultGUI from "./UI/DefaultGUI"; -import State from "./State"; -import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"; -import {DefaultGuiState} from "./UI/DefaultGuiState"; -import {QueryParameters} from "./Logic/Web/QueryParameters"; -import DashboardGui from "./UI/DashboardGui"; -import StatisticsGUI from "./UI/StatisticsGUI"; +import { FixedUiElement } from "./UI/Base/FixedUiElement" +import Combine from "./UI/Base/Combine" +import MinimapImplementation from "./UI/Base/MinimapImplementation" +import { Utils } from "./Utils" +import AllThemesGui from "./UI/AllThemesGui" +import DetermineLayout from "./Logic/DetermineLayout" +import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" +import DefaultGUI from "./UI/DefaultGUI" +import State from "./State" +import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation" +import { DefaultGuiState } from "./UI/DefaultGuiState" +import { QueryParameters } from "./Logic/Web/QueryParameters" +import DashboardGui from "./UI/DashboardGui" +import StatisticsGUI from "./UI/StatisticsGUI" // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console MinimapImplementation.initialize() -ShowOverlayLayerImplementation.Implement(); +ShowOverlayLayerImplementation.Implement() // Miscelleanous Utils.DisableLongPresses() class Init { public static Init(layoutToUse: LayoutConfig) { - if (layoutToUse === null) { // Something went wrong, error message is already on screen - return; + return } if (layoutToUse === undefined) { // No layout found new AllThemesGui().setup() - return; + return } const guiState = new DefaultGuiState() - State.state = new State(layoutToUse); - DefaultGuiState.state = guiState; + State.state = new State(layoutToUse) + DefaultGuiState.state = guiState // This 'leaks' the global state via the window object, useful for debugging // @ts-ignore - window.mapcomplete_state = State.state; + window.mapcomplete_state = State.state - const mode = QueryParameters.GetQueryParameter("mode", "map", "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'") + const mode = QueryParameters.GetQueryParameter( + "mode", + "map", + "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'" + ) if (mode.data === "dashboard") { new DashboardGui(State.state, guiState).setup() } else { @@ -49,24 +52,25 @@ class Init { } } - -document.getElementById("decoration-desktop").remove(); -new Combine(["Initializing... <br/>", - new FixedUiElement("<a>If this message persist, something went wrong - click here to try again</a>") +document.getElementById("decoration-desktop").remove() +new Combine([ + "Initializing... <br/>", + new FixedUiElement( + "<a>If this message persist, something went wrong - click here to try again</a>" + ) .SetClass("link-underline small") .onClick(() => { - localStorage.clear(); - window.location.reload(true); - - })]) - .AttachTo("centermessage"); // Add an initialization and reset button if something goes wrong + localStorage.clear() + window.location.reload(true) + }), +]).AttachTo("centermessage") // Add an initialization and reset button if something goes wrong // @ts-ignore -DetermineLayout.GetLayout().then(value => { - console.log("Got ", value) - Init.Init(value) -}).catch(err => { - console.error("Error while initializing: ", err, err.stack) -}) - - +DetermineLayout.GetLayout() + .then((value) => { + console.log("Got ", value) + Init.Init(value) + }) + .catch((err) => { + console.error("Error while initializing: ", err, err.stack) + }) diff --git a/notfound.ts b/notfound.ts index 2c2be6c6c..0da141d4f 100644 --- a/notfound.ts +++ b/notfound.ts @@ -1,6 +1,5 @@ -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import Combine from "./UI/Base/Combine"; -import BackToIndex from "./UI/BigComponents/BackToIndex"; +import { FixedUiElement } from "./UI/Base/FixedUiElement" +import Combine from "./UI/Base/Combine" +import BackToIndex from "./UI/BigComponents/BackToIndex" -new Combine([new FixedUiElement("This page is not found"), - new BackToIndex()]).AttachTo("maindiv") \ No newline at end of file +new Combine([new FixedUiElement("This page is not found"), new BackToIndex()]).AttachTo("maindiv") diff --git a/scripts/CycleHighwayFix.ts b/scripts/CycleHighwayFix.ts index b6e4509d6..637e13c28 100644 --- a/scripts/CycleHighwayFix.ts +++ b/scripts/CycleHighwayFix.ts @@ -1,21 +1,34 @@ -import ScriptUtils from "./ScriptUtils"; -import {appendFileSync, readFileSync, writeFileSync} from "fs"; -import {OsmObject} from "../Logic/Osm/OsmObject"; +import ScriptUtils from "./ScriptUtils" +import { appendFileSync, readFileSync, writeFileSync } from "fs" +import { OsmObject } from "../Logic/Osm/OsmObject" ScriptUtils.fixUtils() ScriptUtils.erasableLog("Fixing the cycle highways...") -writeFileSync("cycleHighwayFix.osc", "<osmChange version=\"0.6\" generator=\"Handmade\" copyright=\"OpenStreetMap and Contributors\"\n" + - " attribution=\"http://www.openstreetmap.org/copyright\" license=\"http://opendatacommons.org/licenses/odbl/1-0/\">\n" + - " <modify>", "utf8") -const ids = JSON.parse(readFileSync("export.geojson", "utf-8")).features.map(f => f.properties["@id"]) +writeFileSync( + "cycleHighwayFix.osc", + '<osmChange version="0.6" generator="Handmade" copyright="OpenStreetMap and Contributors"\n' + + ' attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">\n' + + " <modify>", + "utf8" +) +const ids = JSON.parse(readFileSync("export.geojson", "utf-8")).features.map( + (f) => f.properties["@id"] +) console.log(ids) -ids.map(id => OsmObject.DownloadReferencingRelations(id).then(relations => { - console.log(relations) - const changeparts = relations.filter(relation => relation.tags["cycle_highway"] == "yes" && relation.tags["note:state"] == undefined) - .map(relation => { - relation.tags["note:state"] = "has_highway_under_construction"; - return relation.ChangesetXML(undefined) - }) - appendFileSync("cycleHighwayFix.osc", changeparts.join("\n"), "utf8") -})) \ No newline at end of file +ids.map((id) => + OsmObject.DownloadReferencingRelations(id).then((relations) => { + console.log(relations) + const changeparts = relations + .filter( + (relation) => + relation.tags["cycle_highway"] == "yes" && + relation.tags["note:state"] == undefined + ) + .map((relation) => { + relation.tags["note:state"] = "has_highway_under_construction" + return relation.ChangesetXML(undefined) + }) + appendFileSync("cycleHighwayFix.osc", changeparts.join("\n"), "utf8") + }) +) diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index a86f3b2d4..21d7079ae 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -1,18 +1,16 @@ -import * as fs from "fs"; -import {existsSync, lstatSync, readdirSync, readFileSync} from "fs"; -import {Utils} from "../Utils"; -import * as https from "https"; -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import xml2js from 'xml2js'; +import * as fs from "fs" +import { existsSync, lstatSync, readdirSync, readFileSync } from "fs" +import { Utils } from "../Utils" +import * as https from "https" +import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import xml2js from "xml2js" export default class ScriptUtils { - public static fixUtils() { Utils.externalDownloadFunction = ScriptUtils.Download } - public static readDirRecSync(path, maxDepth = 999): string[] { const result = [] if (maxDepth <= 0) { @@ -29,20 +27,18 @@ export default class ScriptUtils { result.push(fullEntry) } } - return result; + return result } public static DownloadFileTo(url, targetFilePath: string): void { console.log("Downloading ", url, "to", targetFilePath) https.get(url, (res) => { - const filePath = fs.createWriteStream(targetFilePath); - res.pipe(filePath); - filePath.on('finish', () => { - filePath.close(); - console.log('Download Completed'); + const filePath = fs.createWriteStream(targetFilePath) + res.pipe(filePath) + filePath.on("finish", () => { + filePath.close() + console.log("Download Completed") }) - - }) } @@ -53,35 +49,35 @@ export default class ScriptUtils { public static sleep(ms) { if (ms <= 0) { process.stdout.write("\r \r") - return; + return } return new Promise((resolve) => { - process.stdout.write("\r Sleeping for " + (ms / 1000) + "s \r") - setTimeout(resolve, 1000); - }).then(() => ScriptUtils.sleep(ms - 1000)); + process.stdout.write("\r Sleeping for " + ms / 1000 + "s \r") + setTimeout(resolve, 1000) + }).then(() => ScriptUtils.sleep(ms - 1000)) } public static getLayerPaths(): string[] { return ScriptUtils.readDirRecSync("./assets/layers") - .filter(path => path.indexOf(".json") > 0) - .filter(path => path.indexOf(".proto.json") < 0) - .filter(path => path.indexOf("license_info.json") < 0) + .filter((path) => path.indexOf(".json") > 0) + .filter((path) => path.indexOf(".proto.json") < 0) + .filter((path) => path.indexOf("license_info.json") < 0) } - public static getLayerFiles(): { parsed: LayerConfigJson, path: string }[] { + public static getLayerFiles(): { parsed: LayerConfigJson; path: string }[] { return ScriptUtils.readDirRecSync("./assets/layers") - .filter(path => path.indexOf(".json") > 0) - .filter(path => path.indexOf(".proto.json") < 0) - .filter(path => path.indexOf("license_info.json") < 0) - .map(path => { + .filter((path) => path.indexOf(".json") > 0) + .filter((path) => path.indexOf(".proto.json") < 0) + .filter((path) => path.indexOf("license_info.json") < 0) + .map((path) => { try { const contents = readFileSync(path, "UTF8") if (contents === "") { throw "The file " + path + " is empty, did you properly save?" } - const parsed = JSON.parse(contents); - return {parsed, path} + const parsed = JSON.parse(contents) + return { parsed, path } } catch (e) { console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e) throw e @@ -91,29 +87,28 @@ export default class ScriptUtils { public static getThemePaths(): string[] { return ScriptUtils.readDirRecSync("./assets/themes") - .filter(path => path.endsWith(".json") && !path.endsWith(".proto.json")) - .filter(path => path.indexOf("license_info.json") < 0) + .filter((path) => path.endsWith(".json") && !path.endsWith(".proto.json")) + .filter((path) => path.indexOf("license_info.json") < 0) } - public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] { - return this.getThemePaths() - .map(path => { - try { - const contents = readFileSync(path, "UTF8"); - if (contents === "") { - throw "The file " + path + " is empty, did you properly save?" - } - const parsed = JSON.parse(contents); - return {parsed: parsed, path: path} - } catch (e) { - console.error("Could not read file ", path, "due to ", e) - throw e + public static getThemeFiles(): { parsed: LayoutConfigJson; path: string }[] { + return this.getThemePaths().map((path) => { + try { + const contents = readFileSync(path, "UTF8") + if (contents === "") { + throw "The file " + path + " is empty, did you properly save?" } - }); + const parsed = JSON.parse(contents) + return { parsed: parsed, path: path } + } catch (e) { + console.error("Could not read file ", path, "due to ", e) + throw e + } + }) } public static TagInfoHistogram(key: string): Promise<{ - data: { count: number, value: string, fraction: number }[] + data: { count: number; value: string; fraction: number }[] }> { const url = `https://taginfo.openstreetmap.org/api/4/key/values?key=${key}&filter=all&lang=en&sortname=count&sortorder=desc&page=1&rp=17&qtype=value` return ScriptUtils.DownloadJSON(url) @@ -127,17 +122,17 @@ export default class ScriptUtils { return root.svg } - public static async ReadSvgSync(path: string, callback: ((svg: any) => void)): Promise<any> { - xml2js.parseString(readFileSync(path, "UTF8"), {async: false}, (err, root) => { + public static async ReadSvgSync(path: string, callback: (svg: any) => void): Promise<any> { + xml2js.parseString(readFileSync(path, "UTF8"), { async: false }, (err, root) => { if (err) { throw err } - callback(root["svg"]); + callback(root["svg"]) }) } private static async DownloadJSON(url: string, headers?: any): Promise<any> { - const data = await ScriptUtils.Download(url, headers); + const data = await ScriptUtils.Download(url, headers) return JSON.parse(data.content) } @@ -148,29 +143,30 @@ export default class ScriptUtils { headers.accept = "application/json" console.log(" > ScriptUtils.DownloadJson(", url, ")") const urlObj = new URL(url) - https.get({ - host: urlObj.host, - path: urlObj.pathname + urlObj.search, + https.get( + { + host: urlObj.host, + path: urlObj.pathname + urlObj.search, - port: urlObj.port, - headers: headers - }, (res) => { - const parts: string[] = [] - res.setEncoding('utf8'); - res.on('data', function (chunk) { - // @ts-ignore - parts.push(chunk) - }); + port: urlObj.port, + headers: headers, + }, + (res) => { + const parts: string[] = [] + res.setEncoding("utf8") + res.on("data", function (chunk) { + // @ts-ignore + parts.push(chunk) + }) - res.addListener('end', function () { - resolve({content: parts.join("")}) - }); - }) + res.addListener("end", function () { + resolve({ content: parts.join("") }) + }) + } + ) } catch (e) { reject(e) } }) - } - } diff --git a/scripts/automoveTranslations.ts b/scripts/automoveTranslations.ts index 662b91fb0..f7e637952 100644 --- a/scripts/automoveTranslations.ts +++ b/scripts/automoveTranslations.ts @@ -1,5 +1,5 @@ import * as languages from "../assets/generated/used_languages.json" -import {readFileSync, writeFileSync} from "fs"; +import { readFileSync, writeFileSync } from "fs" /** * Moves values around in 'section'. Section will be changed @@ -8,54 +8,57 @@ import {readFileSync, writeFileSync} from "fs"; * @param language */ function fixSection(section, referenceSection, language: string) { - if(section === undefined){ + if (section === undefined) { return } outer: for (const key of Object.keys(section)) { const v = section[key] - if(typeof v ==="string" && referenceSection[key] === undefined){ + if (typeof v === "string" && referenceSection[key] === undefined) { // Not found in reference, search for a subsection with this key for (const subkey of Object.keys(referenceSection)) { const subreference = referenceSection[subkey] - if(subreference[key] !== undefined){ - if(section[subkey] !== undefined && section[subkey][key] !== undefined) { + if (subreference[key] !== undefined) { + if (section[subkey] !== undefined && section[subkey][key] !== undefined) { console.log(`${subkey}${key} is already defined... Looking furhter`) continue } - if(typeof section[subkey] === "string"){ - console.log(`NOT overwriting '${section[subkey]}' for ${subkey} (needed for ${key})`) - }else{ + if (typeof section[subkey] === "string") { + console.log( + `NOT overwriting '${section[subkey]}' for ${subkey} (needed for ${key})` + ) + } else { // apply fix - if(section[subkey] === undefined){ + if (section[subkey] === undefined) { section[subkey] = {} } section[subkey][key] = section[key] - delete section[key] - console.log(`Rewritten key: ${key} --> ${subkey}.${key} in language ${language}`) + delete section[key] + console.log( + `Rewritten key: ${key} --> ${subkey}.${key} in language ${language}` + ) continue outer } } } - console.log("No solution found for "+key) + console.log("No solution found for " + key) } } } - -function main(args:string[]):void{ +function main(args: string[]): void { const sectionName = args[0] const l = args[1] - if(sectionName === undefined){ - console.log("Tries to automatically move translations to a new subsegment. Usage: 'sectionToCheck' 'language'") + if (sectionName === undefined) { + console.log( + "Tries to automatically move translations to a new subsegment. Usage: 'sectionToCheck' 'language'" + ) return } - const reference = JSON.parse( readFileSync("./langs/en.json","UTF8")) + const reference = JSON.parse(readFileSync("./langs/en.json", "UTF8")) const path = `./langs/${l}.json` - const file = JSON.parse( readFileSync(path,"UTF8")) + const file = JSON.parse(readFileSync(path, "UTF8")) fixSection(file[sectionName], reference[sectionName], l) - writeFileSync(path, JSON.stringify(file, null, " ")+"\n") - - + writeFileSync(path, JSON.stringify(file, null, " ") + "\n") } -main(process.argv.slice(2)) \ No newline at end of file +main(process.argv.slice(2)) diff --git a/scripts/csvToGeojson.ts b/scripts/csvToGeojson.ts index d0a7fe09f..ba90c5ed6 100644 --- a/scripts/csvToGeojson.ts +++ b/scripts/csvToGeojson.ts @@ -1,9 +1,8 @@ -import {parse} from 'csv-parse/sync'; -import {readFileSync} from "fs"; +import { parse } from "csv-parse/sync" +import { readFileSync } from "fs" -var lambert72toWGS84 = function(x, y){ - - var newLongitude, newLatitude; +var lambert72toWGS84 = function (x, y) { + var newLongitude, newLatitude var n = 0.77164219, F = 1.81329763, @@ -12,78 +11,81 @@ var lambert72toWGS84 = function(x, y){ a = 6378388, xDiff = 149910, yDiff = 5400150, - theta0 = 0.07604294; + theta0 = 0.07604294 var xReal = xDiff - x, - yReal = yDiff - y; + yReal = yDiff - y var rho = Math.sqrt(xReal * xReal + yReal * yReal), - theta = Math.atan(xReal / -yReal); + theta = Math.atan(xReal / -yReal) - newLongitude = (theta0 + (theta + thetaFudge) / n) * 180 / Math.PI; - newLatitude = 0; + newLongitude = ((theta0 + (theta + thetaFudge) / n) * 180) / Math.PI + newLatitude = 0 - for (var i = 0; i < 5 ; ++i) { - newLatitude = (2 * Math.atan(Math.pow(F * a / rho, 1 / n) * Math.pow((1 + e * Math.sin(newLatitude)) / (1 - e * Math.sin(newLatitude)), e / 2))) - Math.PI / 2; + for (var i = 0; i < 5; ++i) { + newLatitude = + 2 * + Math.atan( + Math.pow((F * a) / rho, 1 / n) * + Math.pow( + (1 + e * Math.sin(newLatitude)) / (1 - e * Math.sin(newLatitude)), + e / 2 + ) + ) - + Math.PI / 2 } - newLatitude *= 180 / Math.PI; - return [newLongitude, newLatitude]; - + newLatitude *= 180 / Math.PI + return [newLongitude, newLatitude] } function main(args: string[]): void { - - - if (args.length == 0) { - /* args = ["/home/pietervdvn/Downloads/Scholen/aantallen.csv", + /* args = ["/home/pietervdvn/Downloads/Scholen/aantallen.csv", "/home/pietervdvn/Downloads/Scholen/perschool.csv", "/home/pietervdvn/Downloads/Scholen/Vestigingsplaatsen-van-scholen-gewoon-secundair-onderwijs-cleaned.csv"] */ - console.log("Usage: csvToGeojson input.csv name-of-lat-field name-of-lon-field") + console.log("Usage: csvToGeojson input.csv name-of-lat-field name-of-lon-field") return } - - let file = args[0] - if(file.startsWith("file://")){ + + let file = args[0] + if (file.startsWith("file://")) { file = file.substr("file://".length) } const latField = args[1] const lonField = args[2] - + const csvOptions = { columns: true, skip_empty_lines: true, - trim: true + trim: true, } - + const csv: Record<any, string>[] = parse(readFileSync(file), csvOptions) - - const features = csv.map((csvElement, i) => { + + const features = csv.map((csvElement, i) => { const lat = Number(csvElement[latField]) const lon = Number(csvElement[lonField]) - if(isNaN(lat) || isNaN(lon)){ - throw `Not a valid lat or lon for entry ${i}: ${JSON.stringify(csvElement)}` - } - - + if (isNaN(lat) || isNaN(lon)) { + throw `Not a valid lat or lon for entry ${i}: ${JSON.stringify(csvElement)}` + } - return { + return { type: "Feature", properties: csvElement, geometry: { type: "Point", - coordinates: lambert72toWGS84(lon, lat) - } + coordinates: lambert72toWGS84(lon, lat), + }, } - }) - - console.log(JSON.stringify({ - type: "FeatureCollection", - features - })) + console.log( + JSON.stringify({ + type: "FeatureCollection", + features, + }) + ) } -main(process.argv.slice(2)) \ No newline at end of file +main(process.argv.slice(2)) diff --git a/scripts/downloadFile.ts b/scripts/downloadFile.ts index c119915e7..ca136d5ab 100644 --- a/scripts/downloadFile.ts +++ b/scripts/downloadFile.ts @@ -1,12 +1,12 @@ -const http = require('https'); -const fs = require('fs'); +const http = require("https") +const fs = require("fs") // Could use yargs to have more validation but wanted to keep it simple -const args = process.argv.slice(2); -const FILE_URL = args[0]; -const DESTINATION = args[1]; +const args = process.argv.slice(2) +const FILE_URL = args[0] +const DESTINATION = args[1] console.log(`Downloading ${FILE_URL} to ${DESTINATION}`) -const file = fs.createWriteStream(DESTINATION); -http.get(FILE_URL, response => response.pipe(file)); +const file = fs.createWriteStream(DESTINATION) +http.get(FILE_URL, (response) => response.pipe(file)) diff --git a/scripts/extractBikeRental.ts b/scripts/extractBikeRental.ts index 4ae2f6d38..7eb67cac8 100644 --- a/scripts/extractBikeRental.ts +++ b/scripts/extractBikeRental.ts @@ -1,6 +1,5 @@ -import * as fs from "fs"; -import {OH} from "../UI/OpeningHours/OpeningHours"; - +import * as fs from "fs" +import { OH } from "../UI/OpeningHours/OpeningHours" function extractValue(vs: { __value }[]) { if (vs === undefined) { @@ -10,12 +9,11 @@ function extractValue(vs: { __value }[]) { if ((v.__value ?? "") === "") { continue } - return v.__value; + return v.__value } return undefined } - function extract_oh_block(days): string { const oh = [] for (const day of days.day) { @@ -23,7 +21,7 @@ function extract_oh_block(days): string { const block = day.time_block[0] const from = block.time_from.substr(0, 5) const to = block.time_until.substr(0, 5) - const by_appointment = block.by_appointment ? " \"by appointment\"" : "" + const by_appointment = block.by_appointment ? ' "by appointment"' : "" oh.push(`${abbr} ${from}-${to}${by_appointment}`) } return oh.join("; ") @@ -32,7 +30,7 @@ function extract_oh_block(days): string { function extract_oh(opening_periods) { const rules = [] if (opening_periods === undefined) { - return undefined; + return undefined } for (const openingPeriod of opening_periods.opening_period ?? []) { let rule = extract_oh_block(openingPeriod.days) @@ -51,17 +49,19 @@ function rewrite(obj, key) { obj[key] = extractValue(obj[key]["value"]) } -const stuff = fs.readFileSync("/home/pietervdvn/Documents/Freelance/ToerismeVlaanderen 2021-09/TeImporteren/allchannels-bike_rental.json", "UTF8") +const stuff = fs.readFileSync( + "/home/pietervdvn/Documents/Freelance/ToerismeVlaanderen 2021-09/TeImporteren/allchannels-bike_rental.json", + "UTF8" +) const data: any[] = JSON.parse(stuff) const results: { geometry: { - type: "Point", + type: "Point" coordinates: [number, number] - }, - type: "Feature", + } + type: "Feature" properties: any - }[] = [] const skipped = [] console.log("[") @@ -77,7 +77,11 @@ for (const item of data) { skipped.push(item) continue } - const toDelete = ["id", "uuid", "update_date", "creation_date", + const toDelete = [ + "id", + "uuid", + "update_date", + "creation_date", "deleted", "aborted", "partner_id", @@ -85,7 +89,7 @@ for (const item of data) { "winref", "winref_uuid", "root_product_type", - "parent" + "parent", ] for (const key of toDelete) { delete metadata[key] @@ -111,7 +115,7 @@ for (const item of data) { metadata["phone"] = item.contact_info["telephone"] ?? item.contact_info["mobile"] metadata["email"] = item.contact_info["email_address"] - const links = item.links?.link?.map(l => l.url) ?? [] + const links = item.links?.link?.map((l) => l.url) ?? [] metadata["website"] = item.contact_info["website"] ?? links[0] delete item["links"] @@ -127,7 +131,8 @@ for (const item of data) { console.error("Unkown product type: ", metadata["touristic_product_type"]) } - const descriptions = item.descriptions?.description?.map(d => extractValue(d?.text?.value)) ?? [] + const descriptions = + item.descriptions?.description?.map((d) => extractValue(d?.text?.value)) ?? [] delete item.descriptions metadata["description"] = metadata["description"] ?? descriptions[0] if (item.price_info?.prices?.free == true) { @@ -138,28 +143,25 @@ for (const item of data) { metadata.charge = extractValue(item.price_info?.extra_information?.value) const methods = item.price_info?.payment_methods?.payment_method if (methods !== undefined) { - methods.map(v => extractValue(v.value)).forEach(method => { - metadata["payment:" + method.toLowerCase()] = "yes" - }) + methods + .map((v) => extractValue(v.value)) + .forEach((method) => { + metadata["payment:" + method.toLowerCase()] = "yes" + }) } delete item.price_info } else if (item.price_info?.prices?.length === 0) { delete item.price_info } - try { - if (item.labels_info?.labels_own?.label[0]?.code === "Billenkar") { metadata.rental = "quadricycle" delete item.labels_info } - } catch (e) { - - } + } catch (e) {} delete item["publishing_channels"] - try { metadata["image"] = item.media.file[0].url[0] } catch (e) { @@ -167,7 +169,6 @@ for (const item of data) { } delete item.media - const time_info = item.time_info?.time_info_regular if (time_info?.permantly_open === true) { metadata.opening_hours = "24/7" @@ -176,7 +177,6 @@ for (const item of data) { } delete item.time_info - const properties = {} for (const key in metadata) { const v = metadata[key] @@ -189,22 +189,25 @@ for (const item of data) { results.push({ geometry: { type: "Point", - coordinates: item.coordinates + coordinates: item.coordinates, }, type: "Feature", - properties + properties, }) delete item.coordinates delete item.properties console.log(JSON.stringify(item, null, " ") + ",") - } console.log("]") -fs.writeFileSync("west-vlaanderen.geojson", JSON.stringify( - { - type: "FeatureCollection", - features: results - } - , null, " " -)) \ No newline at end of file +fs.writeFileSync( + "west-vlaanderen.geojson", + JSON.stringify( + { + type: "FeatureCollection", + features: results, + }, + null, + " " + ) +) diff --git a/scripts/extractLayer.ts b/scripts/extractLayer.ts index 827642a69..5fd0c4306 100644 --- a/scripts/extractLayer.ts +++ b/scripts/extractLayer.ts @@ -1,10 +1,12 @@ -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"; +import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" -function main(args: string[]){ - if(args.length === 0){ - console.log("Extracts an inline layer from a theme and places it in it's own layer directory.") +function main(args: string[]) { + if (args.length === 0) { + console.log( + "Extracts an inline layer from a theme and places it in it's own layer directory." + ) console.log("USAGE: ts-node scripts/extractLayerFromTheme.ts <themeid> <layerid>") console.log("(Invoke with only the themename to see which layers can be extracted)") return @@ -12,43 +14,46 @@ function main(args: string[]){ const themeId = args[0] const layerId = args[1] - const themePath = "./assets/themes/"+themeId+"/"+themeId+".json" - const contents = <LayoutConfigJson> JSON.parse(readFileSync(themePath, "UTF-8")) - const layers = <LayerConfigJson[]> contents.layers.filter(l => { - if(typeof l === "string"){ + const themePath = "./assets/themes/" + themeId + "/" + themeId + ".json" + const contents = <LayoutConfigJson>JSON.parse(readFileSync(themePath, "UTF-8")) + const layers = <LayerConfigJson[]>contents.layers.filter((l) => { + if (typeof l === "string") { return false } - if(l["override"] !== undefined){ + if (l["override"] !== undefined) { return false } return true }) - if(layers.length === 0){ - console.log("No layers can be extracted from this theme. The "+contents.layers.length+" layers are already substituted layers") + if (layers.length === 0) { + console.log( + "No layers can be extracted from this theme. The " + + contents.layers.length + + " layers are already substituted layers" + ) return } - const layerConfig = layers.find(l => l.id === layerId) - if(layerId === undefined || layerConfig === undefined){ - if(layerId !== undefined){ - console.error( "Layer "+layerId+" not found as inline layer") + const layerConfig = layers.find((l) => l.id === layerId) + if (layerId === undefined || layerConfig === undefined) { + if (layerId !== undefined) { + console.error("Layer " + layerId + " not found as inline layer") } console.log("Layers available for extraction are:") - console.log(layers.map(l => l.id).join("\n")) + console.log(layers.map((l) => l.id).join("\n")) return } - - const dir = "./assets/layers/"+layerId - if(!existsSync(dir)){ + const dir = "./assets/layers/" + layerId + if (!existsSync(dir)) { mkdirSync(dir) } - writeFileSync(dir+"/"+layerId+".json", JSON.stringify(layerConfig, null, " ")) + writeFileSync(dir + "/" + layerId + ".json", JSON.stringify(layerConfig, null, " ")) - const index = contents.layers.findIndex(l => l["id"] === layerId) + const index = contents.layers.findIndex((l) => l["id"] === layerId) contents.layers[index] = layerId writeFileSync(themePath, JSON.stringify(contents, null, " ")) } -main(process.argv.slice(2)) \ No newline at end of file +main(process.argv.slice(2)) diff --git a/scripts/fetchLanguages.ts b/scripts/fetchLanguages.ts index a622a8c07..e4641f279 100644 --- a/scripts/fetchLanguages.ts +++ b/scripts/fetchLanguages.ts @@ -3,76 +3,77 @@ */ import * as wds from "wikidata-sdk" -import {Utils} from "../Utils"; -import ScriptUtils from "./ScriptUtils"; -import {existsSync, readFileSync, writeFileSync} from "fs"; -import {QuestionableTagRenderingConfigJson} from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import WikidataUtils from "../Utils/WikidataUtils"; -import LanguageUtils from "../Utils/LanguageUtils"; +import { Utils } from "../Utils" +import ScriptUtils from "./ScriptUtils" +import { existsSync, readFileSync, writeFileSync } from "fs" +import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import WikidataUtils from "../Utils/WikidataUtils" +import LanguageUtils from "../Utils/LanguageUtils" -async function fetch(target: string){ +async function fetch(target: string) { const regular = await fetchRegularLanguages() writeFileSync(target, JSON.stringify(regular, null, " ")) - console.log("Written to "+target) + console.log("Written to " + target) } async function fetchRegularLanguages() { - console.log("Fetching languages") - const sparql = 'SELECT ?lang ?label ?code \n' + - 'WHERE \n' + - '{ \n' + - ' ?lang wdt:P31 wd:Q1288568. \n' + // language instanceOf (p31) modern language(Q1288568) - ' ?lang rdfs:label ?label. \n' + - ' ?lang wdt:P424 ?code' + // Wikimedia language code seems to be close to the weblate entries - '} ' + const sparql = + "SELECT ?lang ?label ?code \n" + + "WHERE \n" + + "{ \n" + + " ?lang wdt:P31 wd:Q1288568. \n" + // language instanceOf (p31) modern language(Q1288568) + " ?lang rdfs:label ?label. \n" + + " ?lang wdt:P424 ?code" + // Wikimedia language code seems to be close to the weblate entries + "} " const url = wds.sparqlQuery(sparql) -// request the generated URL with your favorite HTTP request library - const result = await Utils.downloadJson(url, {"User-Agent": "MapComplete script"}) + // request the generated URL with your favorite HTTP request library + const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" }) const bindings = result.results.bindings - + const zh_hant = await fetchSpecial(18130932, "zh_Hant") const zh_hans = await fetchSpecial(13414913, "zh_Hant") - const pt_br = await fetchSpecial( 750553, "pt_BR") - const fil = await fetchSpecial( 33298, "fil") + const pt_br = await fetchSpecial(750553, "pt_BR") + const fil = await fetchSpecial(33298, "fil") bindings.push(...zh_hant) bindings.push(...zh_hans) bindings.push(...pt_br) bindings.push(...fil) - - return result.results.bindings + return result.results.bindings } async function fetchSpecial(id: number, code: string) { - ScriptUtils.fixUtils() console.log("Fetching languages") - const sparql = 'SELECT ?lang ?label ?code \n' + - 'WHERE \n' + - '{ \n' + - ' wd:Q'+id+' rdfs:label ?label. \n' + - '} ' + const sparql = + "SELECT ?lang ?label ?code \n" + + "WHERE \n" + + "{ \n" + + " wd:Q" + + id + + " rdfs:label ?label. \n" + + "} " const url = wds.sparqlQuery(sparql) - const result = await Utils.downloadJson(url, {"User-Agent": "MapComplete script"}) + const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" }) const bindings = result.results.bindings - bindings.forEach(binding => binding["code"] = {value: code}) + bindings.forEach((binding) => (binding["code"] = { value: code })) return bindings } -function getNativeList(langs: Map<string, Map<string, string>>){ +function getNativeList(langs: Map<string, Map<string, string>>) { const native = {} const keys: string[] = Array.from(langs.keys()) keys.sort() for (const key of keys) { const translations: Map<string, string> = langs.get(key) - if(!LanguageUtils.usedLanguages.has(key)){ + if (!LanguageUtils.usedLanguages.has(key)) { continue } native[key] = translations.get(key) @@ -80,8 +81,8 @@ function getNativeList(langs: Map<string, Map<string, string>>){ return native } -async function getOfficialLanguagesPerCountry() : Promise<Map<string, string[]>>{ - const lngs = new Map<string, string[]>(); +async function getOfficialLanguagesPerCountry(): Promise<Map<string, string[]>> { + const lngs = new Map<string, string[]>() const sparql = `SELECT ?country ?countryLabel ?countryCode ?language ?languageCode ?languageLabel WHERE { @@ -93,85 +94,88 @@ async function getOfficialLanguagesPerCountry() : Promise<Map<string, string[]>> }` const url = wds.sparqlQuery(sparql) - const result = await Utils.downloadJson(url, {"User-Agent": "MapComplete script"}) - const bindings : {countryCode: {value: string}, languageCode: {value: string}}[]= result.results.bindings + const result = await Utils.downloadJson(url, { "User-Agent": "MapComplete script" }) + const bindings: { countryCode: { value: string }; languageCode: { value: string } }[] = + result.results.bindings for (const binding of bindings) { const countryCode = binding.countryCode.value const language = binding.languageCode.value - if(lngs.get(countryCode) === undefined){ + if (lngs.get(countryCode) === undefined) { lngs.set(countryCode, []) } lngs.get(countryCode).push(language) } - return lngs; + return lngs } -async function main(wipeCache = false){ +async function main(wipeCache = false) { const cacheFile = "./assets/generated/languages-wd.json" - if(wipeCache || !existsSync(cacheFile)){ + if (wipeCache || !existsSync(cacheFile)) { console.log("Refreshing cache") - await fetch(cacheFile); - }else{ + await fetch(cacheFile) + } else { console.log("Reusing the cached file") } - const data = JSON.parse(readFileSync( cacheFile, "UTF8")) + const data = JSON.parse(readFileSync(cacheFile, "UTF8")) const perId = WikidataUtils.extractLanguageData(data, WikidataUtils.languageRemapping) const nativeList = getNativeList(perId) writeFileSync("./assets/language_native.json", JSON.stringify(nativeList, null, " ")) - const translations = Utils.MapToObj(perId, (value, key) => { - if(!LanguageUtils.usedLanguages.has(key)){ + if (!LanguageUtils.usedLanguages.has(key)) { return undefined // Remove unused languages } - return Utils.MapToObj(value, (v, k ) => { - if(!LanguageUtils.usedLanguages.has(k)){ + return Utils.MapToObj(value, (v, k) => { + if (!LanguageUtils.usedLanguages.has(k)) { return undefined } return v }) }) - - writeFileSync("./assets/language_translations.json", - JSON.stringify(translations, null, " ")) - - - let officialLanguages : Record<string, string[]>; + + writeFileSync("./assets/language_translations.json", JSON.stringify(translations, null, " ")) + + let officialLanguages: Record<string, string[]> const officialLanguagesPath = "./assets/language_in_country.json" - if(existsSync("./assets/languages_in_country.json") && !wipeCache){ + if (existsSync("./assets/languages_in_country.json") && !wipeCache) { officialLanguages = JSON.parse(readFileSync(officialLanguagesPath, "utf8")) - }else { - officialLanguages = Utils.MapToObj(await getOfficialLanguagesPerCountry(), t => t) + } else { + officialLanguages = Utils.MapToObj(await getOfficialLanguagesPerCountry(), (t) => t) writeFileSync(officialLanguagesPath, JSON.stringify(officialLanguages, null, " ")) } - - const perLanguage = Utils.TransposeMap(officialLanguages); + + const perLanguage = Utils.TransposeMap(officialLanguages) console.log(JSON.stringify(perLanguage, null, " ")) - const mappings: {if: string, then: Record<string, string>, hideInAnswer: string}[] = [] + const mappings: { if: string; then: Record<string, string>; hideInAnswer: string }[] = [] for (const language of Object.keys(perLanguage)) { - const countries = Utils.Dedup(perLanguage[language].map(c => c.toLowerCase())) + const countries = Utils.Dedup(perLanguage[language].map((c) => c.toLowerCase())) mappings.push({ - if: "language="+language, + if: "language=" + language, then: translations[language], - hideInAnswer : "_country="+countries.join("|") + hideInAnswer: "_country=" + countries.join("|"), }) } - - const tagRenderings = <QuestionableTagRenderingConfigJson> { + + const tagRenderings = <QuestionableTagRenderingConfigJson>{ id: "official-language", mappings, - question: "What languages are spoken here?" + question: "What languages are spoken here?", } - - writeFileSync("./assets/layers/language/language.json", JSON.stringify(<LayerConfigJson>{ - id:"language", - description: "Various tagRenderings to help language tooling", - tagRenderings - }, null, " ")) - + + writeFileSync( + "./assets/layers/language/language.json", + JSON.stringify( + <LayerConfigJson>{ + id: "language", + description: "Various tagRenderings to help language tooling", + tagRenderings, + }, + null, + " " + ) + ) } const forceRefresh = process.argv[2] === "--force-refresh" ScriptUtils.fixUtils() main(forceRefresh).then(() => console.log("Done!")) - diff --git a/scripts/filter.ts b/scripts/filter.ts index 1a9713658..f4da2fc9d 100644 --- a/scripts/filter.ts +++ b/scripts/filter.ts @@ -1,11 +1,13 @@ -import * as fs from "fs"; -import {TagUtils} from "../Logic/Tags/TagUtils"; -import {writeFileSync} from "fs"; -import {TagsFilter} from "../Logic/Tags/TagsFilter"; +import * as fs from "fs" +import { TagUtils } from "../Logic/Tags/TagUtils" +import { writeFileSync } from "fs" +import { TagsFilter } from "../Logic/Tags/TagsFilter" function main(args) { if (args.length < 2) { - console.log("Given a single geojson file and a filter specification, will print all the entries to std-out which pass the property") + console.log( + "Given a single geojson file and a filter specification, will print all the entries to std-out which pass the property" + ) console.log("USAGE: perProperty `file.geojson` `key=value` [outputfile]") return } @@ -14,32 +16,38 @@ function main(args) { const output = args[2] const data = JSON.parse(fs.readFileSync(path, "UTF8")) - let filter : TagsFilter ; - try{ - filter = TagUtils.Tag(JSON.parse(spec)) - - }catch(e){ + let filter: TagsFilter + try { + filter = TagUtils.Tag(JSON.parse(spec)) + } catch (e) { filter = TagUtils.Tag(spec) } - const features = data.features.filter(f => filter.matchesProperties(f.properties)) + const features = data.features.filter((f) => filter.matchesProperties(f.properties)) - if(features.length === 0){ + if (features.length === 0) { console.log("Warning: no features matched the filter. Exiting now") return } - + const collection = { - type:"FeatureCollection", - features + type: "FeatureCollection", + features, } const stringified = JSON.stringify(collection, null, " ") - if(output === undefined){ + if (output === undefined) { console.log(stringified) - }else{ - console.log("Filtered "+path+": kept "+features.length+" out of "+data.features.length+" objects") + } else { + console.log( + "Filtered " + + path + + ": kept " + + features.length + + " out of " + + data.features.length + + " objects" + ) writeFileSync(output, stringified) } - } -main(process.argv.slice(2)) \ No newline at end of file +main(process.argv.slice(2)) diff --git a/scripts/fixImagesInTagRenderings.ts b/scripts/fixImagesInTagRenderings.ts index 5440238fb..ff06f5994 100644 --- a/scripts/fixImagesInTagRenderings.ts +++ b/scripts/fixImagesInTagRenderings.ts @@ -1,49 +1,56 @@ -import {readFileSync, writeFileSync} from "fs"; -import {DesugaringStep} from "../Models/ThemeConfig/Conversion/Conversion"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import {Utils} from "../Utils"; -import Translations from "../UI/i18n/Translations"; +import { readFileSync, writeFileSync } from "fs" +import { DesugaringStep } from "../Models/ThemeConfig/Conversion/Conversion" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import { Utils } from "../Utils" +import Translations from "../UI/i18n/Translations" class ConvertImagesToIcon extends DesugaringStep<LayerConfigJson> { - private _iconClass: string; + private _iconClass: string constructor(iconClass: string) { - super("Searches for images in the 'then' path, removes the <img> block and extracts the image itself a 'icon'", - [], "ConvertImagesToIcon") - this._iconClass = iconClass; + super( + "Searches for images in the 'then' path, removes the <img> block and extracts the image itself a 'icon'", + [], + "ConvertImagesToIcon" + ) + this._iconClass = iconClass } - convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { + convert( + json: LayerConfigJson, + context: string + ): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } { const information = [] const errors = [] json = Utils.Clone(json) - Utils.WalkPath( - ["tagRenderings", "mappings"], - json, - mapping => { - const then = Translations.T(mapping.then) - const images = Utils.Dedup(then.ExtractImages()) - if (images.length == 0) { - return mapping - } - if (images.length > 1) { - errors.push("The mapping " + mapping.then + " has multiple images: " + images.join(", ")) - } - information.push("Replaced image " + images[0]) - const replaced = then.OnEveryLanguage((s) => { - return s.replace(/(<div [^>]*>)?<img [^>]*> ?/, "").replace(/<\/div>$/, "").trim() - }) - - mapping.then = replaced.translations - mapping.icon = {path: images[0], class: this._iconClass} + Utils.WalkPath(["tagRenderings", "mappings"], json, (mapping) => { + const then = Translations.T(mapping.then) + const images = Utils.Dedup(then.ExtractImages()) + if (images.length == 0) { return mapping } - ) + if (images.length > 1) { + errors.push( + "The mapping " + mapping.then + " has multiple images: " + images.join(", ") + ) + } + information.push("Replaced image " + images[0]) + const replaced = then.OnEveryLanguage((s) => { + return s + .replace(/(<div [^>]*>)?<img [^>]*> ?/, "") + .replace(/<\/div>$/, "") + .trim() + }) + + mapping.then = replaced.translations + mapping.icon = { path: images[0], class: this._iconClass } + return mapping + }) return { information, - result: json - }; + result: json, + } } } @@ -57,9 +64,12 @@ function main() { const iconClass = args[1] ?? "small" const targetFile = args[2] ?? path + ".autoconverted.json" const parsed = JSON.parse(readFileSync(path, "UTF8")) - const converted = new ConvertImagesToIcon(iconClass).convertStrict(parsed, "While running the fixImagesInTagRenderings-script") + const converted = new ConvertImagesToIcon(iconClass).convertStrict( + parsed, + "While running the fixImagesInTagRenderings-script" + ) writeFileSync(targetFile, JSON.stringify(converted, null, " ")) console.log("Written fixed version to " + targetFile) } -main(); \ No newline at end of file +main() diff --git a/scripts/fixSchemas.ts b/scripts/fixSchemas.ts index ad71e2efa..dd62bdf64 100644 --- a/scripts/fixSchemas.ts +++ b/scripts/fixSchemas.ts @@ -1,21 +1,25 @@ -import ScriptUtils from "./ScriptUtils"; -import {readFileSync, writeFileSync} from "fs"; +import ScriptUtils from "./ScriptUtils" +import { readFileSync, writeFileSync } from "fs" /** * Extracts the data from the scheme file and writes them in a flatter structure */ -export type JsonSchemaType = string | {$ref: string, description: string} | {type: string} | JsonSchemaType[] +export type JsonSchemaType = + | string + | { $ref: string; description: string } + | { type: string } + | JsonSchemaType[] export interface JsonSchema { - description?: string, - type?: JsonSchemaType, - properties?: any, - items?: JsonSchema, - allOf?: JsonSchema[], - anyOf: JsonSchema[], - enum: JsonSchema[], - "$ref": string + description?: string + type?: JsonSchemaType + properties?: any + items?: JsonSchema + allOf?: JsonSchema[] + anyOf: JsonSchema[] + enum: JsonSchema[] + $ref: string } function WalkScheme<T>( @@ -24,9 +28,8 @@ function WalkScheme<T>( fullScheme: JsonSchema & { definitions?: any } = undefined, path: string[] = [], isHandlingReference = [] -): { path: string[], t: T }[] { - - const results: { path: string[], t: T } [] = [] +): { path: string[]; t: T }[] { + const results: { path: string[]; t: T }[] = [] if (scheme === undefined) { return [] } @@ -39,10 +42,13 @@ function WalkScheme<T>( } const definitionName = ref.substr(prefix.length) if (isHandlingReference.indexOf(definitionName) >= 0) { - return; + return } const loadedScheme = fullScheme.definitions[definitionName] - return WalkScheme(onEach, loadedScheme, fullScheme, path, [...isHandlingReference, definitionName]); + return WalkScheme(onEach, loadedScheme, fullScheme, path, [ + ...isHandlingReference, + definitionName, + ]) } fullScheme = fullScheme ?? scheme @@ -50,11 +56,10 @@ function WalkScheme<T>( if (t !== undefined) { results.push({ path, - t + t, }) } - function walk(v: JsonSchema) { if (v === undefined) { return @@ -67,11 +72,9 @@ function WalkScheme<T>( return } - scheme.forEach(v => walk(v)) - + scheme.forEach((v) => walk(v)) } - { walkEach(scheme.enum) walkEach(scheme.anyOf) @@ -85,7 +88,9 @@ function WalkScheme<T>( for (const key in scheme.properties) { const prop = scheme.properties[key] - results.push(...WalkScheme(onEach, prop, fullScheme, [...path, key], isHandlingReference)) + results.push( + ...WalkScheme(onEach, prop, fullScheme, [...path, key], isHandlingReference) + ) } } @@ -93,30 +98,31 @@ function WalkScheme<T>( } function extractMeta(typename: string, path: string) { - const themeSchema = JSON.parse(readFileSync("./Docs/Schemas/" + typename + ".schema.json", "UTF-8")) + const themeSchema = JSON.parse( + readFileSync("./Docs/Schemas/" + typename + ".schema.json", "UTF-8") + ) const withTypes = WalkScheme((schemePart) => { if (schemePart.description === undefined) { - return; + return } - const typeHint = schemePart.description.split("\n") - .find(line => line.trim().toLocaleLowerCase().startsWith("type:")) - ?.substr("type:".length)?.trim() - const type = schemePart.items?.anyOf ?? schemePart.type ?? schemePart.anyOf; - return {typeHint, type, description: schemePart.description} + const typeHint = schemePart.description + .split("\n") + .find((line) => line.trim().toLocaleLowerCase().startsWith("type:")) + ?.substr("type:".length) + ?.trim() + const type = schemePart.items?.anyOf ?? schemePart.type ?? schemePart.anyOf + return { typeHint, type, description: schemePart.description } }, themeSchema) - const paths = withTypes.map(({ - path, - t - }) => ({path, ...t})) + const paths = withTypes.map(({ path, t }) => ({ path, ...t })) writeFileSync("./assets/" + path + ".json", JSON.stringify(paths, null, " ")) console.log("Written meta to ./assets/" + path) } - function main() { - - const allSchemas = ScriptUtils.readDirRecSync("./Docs/Schemas").filter(pth => pth.endsWith("JSC.ts")) + const allSchemas = ScriptUtils.readDirRecSync("./Docs/Schemas").filter((pth) => + pth.endsWith("JSC.ts") + ) for (const path of allSchemas) { const dir = path.substring(0, path.lastIndexOf("/")) const name = path.substring(path.lastIndexOf("/"), path.length - "JSC.ts".length) @@ -137,7 +143,6 @@ function main() { extractMeta("LayoutConfigJson", "layoutconfigmeta") extractMeta("TagRenderingConfigJson", "tagrenderingconfigmeta") extractMeta("QuestionableTagRenderingConfigJson", "questionabletagrenderingconfigmeta") - } main() diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index e1cb4415d..a01024849 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -1,56 +1,59 @@ /** * Generates a collection of geojson files based on an overpass query for a given theme */ -import {Utils} from "../Utils"; -import {Overpass} from "../Logic/Osm/Overpass"; -import {existsSync, readFileSync, writeFileSync} from "fs"; -import {TagsFilter} from "../Logic/Tags/TagsFilter"; -import {Or} from "../Logic/Tags/Or"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import RelationsTracker from "../Logic/Osm/RelationsTracker"; -import * as OsmToGeoJson from "osmtogeojson"; -import MetaTagging from "../Logic/MetaTagging"; -import {ImmutableStore, UIEventSource} from "../Logic/UIEventSource"; -import {TileRange, Tiles} from "../Models/TileRange"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import ScriptUtils from "./ScriptUtils"; -import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; -import FilteredLayer from "../Models/FilteredLayer"; -import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/FeatureSource"; -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; -import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource"; -import Constants from "../Models/Constants"; -import {GeoOperations} from "../Logic/GeoOperations"; -import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"; -import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; -import Loc from "../Models/Loc"; +import { Utils } from "../Utils" +import { Overpass } from "../Logic/Osm/Overpass" +import { existsSync, readFileSync, writeFileSync } from "fs" +import { TagsFilter } from "../Logic/Tags/TagsFilter" +import { Or } from "../Logic/Tags/Or" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import RelationsTracker from "../Logic/Osm/RelationsTracker" +import * as OsmToGeoJson from "osmtogeojson" +import MetaTagging from "../Logic/MetaTagging" +import { ImmutableStore, UIEventSource } from "../Logic/UIEventSource" +import { TileRange, Tiles } from "../Models/TileRange" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import ScriptUtils from "./ScriptUtils" +import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" +import FilteredLayer from "../Models/FilteredLayer" +import FeatureSource, { FeatureSourceForLayer } from "../Logic/FeatureSource/FeatureSource" +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" +import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource" +import Constants from "../Models/Constants" +import { GeoOperations } from "../Logic/GeoOperations" +import SimpleMetaTaggers from "../Logic/SimpleMetaTagger" +import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" +import Loc from "../Models/Loc" ScriptUtils.fixUtils() -function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker, backend: string) { - let filters: TagsFilter[] = []; - let extraScripts: string[] = []; +function createOverpassObject( + theme: LayoutConfig, + relationTracker: RelationsTracker, + backend: string +) { + let filters: TagsFilter[] = [] + let extraScripts: string[] = [] for (const layer of theme.layers) { - if (typeof (layer) === "string") { + if (typeof layer === "string") { throw "A layer was not expanded!" } if (layer.doNotDownload) { - continue; + continue } if (layer.source.geojsonSource !== undefined) { // This layer defines a geoJson-source // SHould it be cached? if (layer.source.isOsmCacheLayer !== true) { - continue; + continue } } - // Check if data for this layer has already been loaded if (layer.source.overpassScript !== undefined) { extraScripts.push(layer.source.overpassScript) } else { - filters.push(layer.source.osmTags); + filters.push(layer.source.osmTags) } } filters = Utils.NoNull(filters) @@ -58,8 +61,13 @@ function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTra if (filters.length + extraScripts.length === 0) { throw "Nothing to download! The theme doesn't declare anything to download" } - return new Overpass(new Or(filters), extraScripts, backend, - new UIEventSource<number>(60), relationTracker); + return new Overpass( + new Or(filters), + extraScripts, + backend, + new UIEventSource<number>(60), + relationTracker + ) } function rawJsonName(targetDir: string, x: number, y: number, z: number): string { @@ -71,102 +79,143 @@ function geoJsonName(targetDir: string, x: number, y: number, z: number): string } /// Downloads the given feature and saves them to disk -async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig, relationTracker: RelationsTracker)/* : {failed: number, skipped :number} */ { +async function downloadRaw( + targetdir: string, + r: TileRange, + theme: LayoutConfig, + relationTracker: RelationsTracker +) /* : {failed: number, skipped :number} */ { let downloaded = 0 let failed = 0 let skipped = 0 const startTime = new Date().getTime() for (let x = r.xstart; x <= r.xend; x++) { for (let y = r.ystart; y <= r.yend; y++) { - downloaded++; + downloaded++ const filename = rawJsonName(targetdir, x, y, r.zoomlevel) if (existsSync(filename)) { console.log("Already exists (not downloading again): ", filename) skipped++ - continue; + continue } const runningSeconds = (new Date().getTime() - startTime) / 1000 const resting = failed + (r.total - downloaded) - const perTile = (runningSeconds / (downloaded - skipped)) + const perTile = runningSeconds / (downloaded - skipped) const estimated = Math.floor(resting * perTile) - console.log("total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped, "running time: ", Utils.toHumanTime(runningSeconds) + "s", "estimated left: ", Utils.toHumanTime(estimated), "(" + Math.floor(perTile) + "s/tile)") + console.log( + "total: ", + downloaded, + "/", + r.total, + "failed: ", + failed, + "skipped: ", + skipped, + "running time: ", + Utils.toHumanTime(runningSeconds) + "s", + "estimated left: ", + Utils.toHumanTime(estimated), + "(" + Math.floor(perTile) + "s/tile)" + ) const boundsArr = Tiles.tile_bounds(r.zoomlevel, x, y) const bounds = { north: Math.max(boundsArr[0][0], boundsArr[1][0]), south: Math.min(boundsArr[0][0], boundsArr[1][0]), east: Math.max(boundsArr[0][1], boundsArr[1][1]), - west: Math.min(boundsArr[0][1], boundsArr[1][1]) + west: Math.min(boundsArr[0][1], boundsArr[1][1]), } - const overpass = createOverpassObject(theme, relationTracker, Constants.defaultOverpassUrls[(failed) % Constants.defaultOverpassUrls.length]) - const url = overpass.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") + const overpass = createOverpassObject( + theme, + relationTracker, + Constants.defaultOverpassUrls[failed % Constants.defaultOverpassUrls.length] + ) + const url = overpass.buildQuery( + "[bbox:" + + bounds.south + + "," + + bounds.west + + "," + + bounds.north + + "," + + bounds.east + + "]" + ) try { - const json = await Utils.downloadJson(url) if ((<string>json.remark ?? "").startsWith("runtime error")) { console.error("Got a runtime error: ", json.remark) - failed++; + failed++ } else if (json.elements.length === 0) { console.log("Got an empty response! Writing anyway") } - - console.log("Got the response - writing ",json.elements.length," elements to ", filename) - writeFileSync(filename, JSON.stringify(json, null, " ")); + console.log( + "Got the response - writing ", + json.elements.length, + " elements to ", + filename + ) + writeFileSync(filename, JSON.stringify(json, null, " ")) } catch (err) { console.log(url) - console.log("Could not download - probably hit the rate limit; waiting a bit. (" + err + ")") - failed++; + console.log( + "Could not download - probably hit the rate limit; waiting a bit. (" + err + ")" + ) + failed++ await ScriptUtils.sleep(1000) } } } - return {failed: failed, skipped: skipped} + return { failed: failed, skipped: skipped } } -/* +/* * Downloads extra geojson sources and returns the features. * Extra geojson layers should not be tiled */ -async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { +async function downloadExtraData(theme: LayoutConfig) /* : any[] */ { const allFeatures: any[] = [] for (const layer of theme.layers) { - const source = layer.source.geojsonSource; + const source = layer.source.geojsonSource if (source === undefined) { - continue; + continue } if (layer.source.isOsmCacheLayer !== undefined && layer.source.isOsmCacheLayer !== false) { // Cached layers are not considered here - continue; + continue } console.log("Downloading extra data: ", source) - await Utils.downloadJson(source).then(json => allFeatures.push(...json.features)) + await Utils.downloadJson(source).then((json) => allFeatures.push(...json.features)) } - return allFeatures; + return allFeatures } - -function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]): FeatureSource { - +function loadAllTiles( + targetdir: string, + r: TileRange, + theme: LayoutConfig, + extraFeatures: any[] +): FeatureSource { let allFeatures = [...extraFeatures] - let processed = 0; + let processed = 0 for (let x = r.xstart; x <= r.xend; x++) { for (let y = r.ystart; y <= r.yend; y++) { - processed++; + processed++ const filename = rawJsonName(targetdir, x, y, r.zoomlevel) console.log(" Loading and processing", processed, "/", r.total, filename) if (!existsSync(filename)) { console.error("Not found - and not downloaded. Run this script again!: " + filename) - continue; + continue } // We read the raw OSM-file and convert it to a geojson const rawOsm = JSON.parse(readFileSync(filename, "UTF8")) // Create and save the geojson file - which is the main chunk of the data - const geojson = OsmToGeoJson.default(rawOsm); + const geojson = OsmToGeoJson.default(rawOsm) console.log(" which as", geojson.features.length, "features") allFeatures.push(...geojson.features) @@ -178,18 +227,24 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr /** * Load all the tiles into memory from disk */ -function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string, pointsOnlyLayers: string[]) { +function sliceToTiles( + allFeatures: FeatureSource, + theme: LayoutConfig, + relationsTracker: RelationsTracker, + targetdir: string, + pointsOnlyLayers: string[] +) { const skippedLayers = new Set<string>() const indexedFeatures: Map<string, any> = new Map<string, any>() - let indexisBuilt = false; + let indexisBuilt = false function buildIndex() { for (const ff of allFeatures.features.data) { const f = ff.feature indexedFeatures.set(f.properties.id, f) } - indexisBuilt = true; + indexisBuilt = true } function getFeatureById(id) { @@ -200,38 +255,49 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations } async function handleLayer(source: FeatureSourceForLayer) { - const layer = source.layer.layerDef; + const layer = source.layer.layerDef const targetZoomLevel = layer.source.geojsonZoomLevel ?? 0 const layerId = layer.id if (layer.source.isOsmCacheLayer !== true) { console.log("Skipping layer ", layerId, ": not a caching layer") skippedLayers.add(layer.id) - return; + return } - console.log("Handling layer ", layerId, "which has", source.features.data.length, "features") + console.log( + "Handling layer ", + layerId, + "which has", + source.features.data.length, + "features" + ) if (source.features.data.length === 0) { - return; + return } - MetaTagging.addMetatags(source.features.data, + MetaTagging.addMetatags( + source.features.data, { memberships: relationsTracker, - getFeaturesWithin: _ => { - return [allFeatures.features.data.map(f => f.feature)] + getFeaturesWithin: (_) => { + return [allFeatures.features.data.map((f) => f.feature)] }, - getFeatureById: getFeatureById + getFeatureById: getFeatureById, }, layer, {}, { includeDates: false, includeNonDates: true, - evaluateStrict: true - }); - + evaluateStrict: true, + } + ) while (SimpleMetaTaggers.country.runningTasks.size > 0) { - console.log("Still waiting for ", SimpleMetaTaggers.country.runningTasks.size, " features which don't have a country yet") + console.log( + "Still waiting for ", + SimpleMetaTaggers.country.runningTasks.size, + " features which don't have a country yet" + ) await ScriptUtils.sleep(1) } @@ -242,25 +308,36 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations minZoomLevel: targetZoomLevel, maxZoomLevel: targetZoomLevel, maxFeatureCount: undefined, - registerTile: tile => { - const tileIndex = tile.tileIndex; + registerTile: (tile) => { + const tileIndex = tile.tileIndex console.log("Got tile:", tileIndex, tile.layer.layerDef.id) if (tile.features.data.length === 0) { return } - const filteredTile = new FilteringFeatureSource({ + const filteredTile = new FilteringFeatureSource( + { locationControl: new ImmutableStore<Loc>(undefined), allElements: undefined, selectedElement: new ImmutableStore<any>(undefined), - globalFilters: new ImmutableStore([]) + globalFilters: new ImmutableStore([]), }, tileIndex, tile, new UIEventSource<any>(undefined) ) - console.log("Tile " + layer.id + "." + tileIndex + " contains " + filteredTile.features.data.length + " features after filtering (" + tile.features.data.length + ") features before") + console.log( + "Tile " + + layer.id + + "." + + tileIndex + + " contains " + + filteredTile.features.data.length + + " features after filtering (" + + tile.features.data.length + + ") features before" + ) if (filteredTile.features.data.length === 0) { return } @@ -271,26 +348,37 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations delete feature.feature["bbox"] if (tile.layer.layerDef.calculatedTags !== undefined) { - // Evaluate all the calculated tags strictly - const calculatedTagKeys = tile.layer.layerDef.calculatedTags.map(ct => ct[0]) + const calculatedTagKeys = tile.layer.layerDef.calculatedTags.map( + (ct) => ct[0] + ) featureCount++ const props = feature.feature.properties for (const calculatedTagKey of calculatedTagKeys) { const strict = props[calculatedTagKey] - - if(props.hasOwnProperty(calculatedTagKey)){ + + if (props.hasOwnProperty(calculatedTagKey)) { delete props[calculatedTagKey] } props[calculatedTagKey] = strict - strictlyCalculated++; + strictlyCalculated++ if (strictlyCalculated % 100 === 0) { - console.log("Strictly calculated ", strictlyCalculated, "values for tile", tileIndex, ": now at ", featureCount, "/", filteredTile.features.data.length, "examle value: ", strict) + console.log( + "Strictly calculated ", + strictlyCalculated, + "values for tile", + tileIndex, + ": now at ", + featureCount, + "/", + filteredTile.features.data.length, + "examle value: ", + strict + ) } } } - } // Lets save this tile! const [z, x, y] = Tiles.tile_from_index(tileIndex) @@ -298,78 +386,100 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) createdTiles.push(tileIndex) // This is the geojson file containing all features for this tile - writeFileSync(targetPath, JSON.stringify({ - type: "FeatureCollection", - features: filteredTile.features.data.map(f => f.feature) - }, null, " ")) + writeFileSync( + targetPath, + JSON.stringify( + { + type: "FeatureCollection", + features: filteredTile.features.data.map((f) => f.feature), + }, + null, + " " + ) + ) console.log("Written tile", targetPath, "with", filteredTile.features.data.length) - } + }, }) // All the tiles are written at this point // Only thing left to do is to create the index const path = targetdir + "_" + layerId + "_" + targetZoomLevel + "_overview.json" const perX = {} - createdTiles.map(i => Tiles.tile_from_index(i)).forEach(([z, x, y]) => { - const key = "" + x - if (perX[key] === undefined) { - perX[key] = [] - } - perX[key].push(y) - }) + createdTiles + .map((i) => Tiles.tile_from_index(i)) + .forEach(([z, x, y]) => { + const key = "" + x + if (perX[key] === undefined) { + perX[key] = [] + } + perX[key].push(y) + }) console.log("Written overview: ", path, "with ", createdTiles.length, "tiles") writeFileSync(path, JSON.stringify(perX)) // And, if needed, to create a points-only layer if (pointsOnlyLayers.indexOf(layer.id) >= 0) { - - const filtered = new FilteringFeatureSource({ + const filtered = new FilteringFeatureSource( + { locationControl: new ImmutableStore<Loc>(undefined), allElements: undefined, selectedElement: new ImmutableStore<any>(undefined), - globalFilters: new ImmutableStore([]) + globalFilters: new ImmutableStore([]), }, Tiles.tile_index(0, 0, 0), source, new UIEventSource<any>(undefined) ) - const features = filtered.features.data.map(f => f.feature) + const features = filtered.features.data.map((f) => f.feature) - const points = features.map(feature => GeoOperations.centerpoint(feature)) + const points = features.map((feature) => GeoOperations.centerpoint(feature)) console.log("Writing points overview for ", layerId) const targetPath = targetdir + "_" + layerId + "_points.geojson" // This is the geojson file containing all features for this tile - writeFileSync(targetPath, JSON.stringify({ - type: "FeatureCollection", - features: points - }, null, " ")) + writeFileSync( + targetPath, + JSON.stringify( + { + type: "FeatureCollection", + features: points, + }, + null, + " " + ) + ) } } new PerLayerFeatureSourceSplitter( - new UIEventSource<FilteredLayer[]>(theme.layers.map(l => ({ - layerDef: l, - isDisplayed: new UIEventSource<boolean>(true), - appliedFilters: new UIEventSource(undefined) - }))), + new UIEventSource<FilteredLayer[]>( + theme.layers.map((l) => ({ + layerDef: l, + isDisplayed: new UIEventSource<boolean>(true), + appliedFilters: new UIEventSource(undefined), + })) + ), handleLayer, allFeatures ) const skipped = Array.from(skippedLayers) if (skipped.length > 0) { - console.warn("Did not save any cache files for layers " + skipped.join(", ") + " as these didn't set the flag `isOsmCache` to true") + console.warn( + "Did not save any cache files for layers " + + skipped.join(", ") + + " as these didn't set the flag `isOsmCache` to true" + ) } } - export async function main(args: string[]) { - console.log("Cache builder started with args ", args.join(", ")) if (args.length < 6) { - console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" + - "Note: a new directory named <theme> will be created in targetdirectory") - return; + console.error( + "Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" + + "Note: a new directory named <theme> will be created in targetdirectory" + ) + return } const themeName = args[0] const zoomlevel = Number(args[1]) @@ -400,7 +510,6 @@ export async function main(args: string[]) { throw "The fourth number (a longitude) is not a valid number" } - const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) if (isNaN(tileRange.total)) { @@ -418,7 +527,7 @@ export async function main(args: string[]) { AllKnownLayouts.allKnownLayouts.forEach((_, key) => { keys.push(key) }) - console.error("The theme " + theme + " was not found; try one of ", keys); + console.error("The theme " + theme + " was not found; try one of ", keys) return } @@ -426,15 +535,17 @@ export async function main(args: string[]) { if (args[7] == "--generate-point-overview") { if (args[8] === undefined) { throw "--generate-point-overview needs a list of layers to generate the overview for (or * for all)" - } else if (args[8] === '*') { - generatePointLayersFor = theme.layers.map(l => l.id) + } else if (args[8] === "*") { + generatePointLayersFor = theme.layers.map((l) => l.id) } else { generatePointLayersFor = args[8].split(",") } - console.log("Also generating a point overview for layers ", generatePointLayersFor.join(",")) + console.log( + "Also generating a point overview for layers ", + generatePointLayersFor.join(",") + ) } { - const index = args.indexOf("--force-zoom-level") if (index >= 0) { const forcedZoomLevel = Number(args[index + 1]) @@ -446,10 +557,9 @@ export async function main(args: string[]) { } } - const relationTracker = new RelationsTracker() - let failed = 0; + let failed = 0 do { const cachingResult = await downloadRaw(targetdir, tileRange, theme, relationTracker) failed = cachingResult.failed @@ -458,20 +568,18 @@ export async function main(args: string[]) { } } while (failed > 0) - const extraFeatures = await downloadExtraData(theme); + const extraFeatures = await downloadExtraData(theme) const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures) sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor) - } - let args = [...process.argv] if (!args[1]?.endsWith("test/TestAll.ts")) { args.splice(0, 2) try { main(args) .then(() => console.log("All done!")) - .catch(e => console.error("Error building cache:", e)); + .catch((e) => console.error("Error building cache:", e)) } catch (e) { console.error("Error building cache:", e) } diff --git a/scripts/generateContributors.ts b/scripts/generateContributors.ts index 66e06a9e2..80643a3a6 100644 --- a/scripts/generateContributors.ts +++ b/scripts/generateContributors.ts @@ -1,44 +1,49 @@ -import {exec} from "child_process"; -import {writeFile, writeFileSync} from "fs"; +import { exec } from "child_process" +import { writeFile, writeFileSync } from "fs" -function asList(hist: Map<string, number>): {contributors: { contributor: string, commits: number }[] -}{ +function asList(hist: Map<string, number>): { + contributors: { contributor: string; commits: number }[] +} { const ls = [] hist.forEach((commits, contributor) => { - ls.push({commits, contributor}) + ls.push({ commits, contributor }) }) - ls.sort((a, b) => (b.commits - a.commits)) - return {contributors: ls} + ls.sort((a, b) => b.commits - a.commits) + return { contributors: ls } } function main() { - exec("git log --pretty='%aN %%!%% %s' ", ((error, stdout, stderr) => { - - const entries = stdout.split("\n").filter(str => str !== "") + exec("git log --pretty='%aN %%!%% %s' ", (error, stdout, stderr) => { + const entries = stdout.split("\n").filter((str) => str !== "") const codeContributors = new Map<string, number>() const translationContributors = new Map<string, number>() for (const entry of entries) { console.log(entry) - let [author, message] = entry.split("%!%").map(s => s.trim()) - if(author === "Weblate"){ + let [author, message] = entry.split("%!%").map((s) => s.trim()) + if (author === "Weblate") { continue } if (author === "pietervdvn") { author = "Pieter Vander Vennet" } - let hist = codeContributors; - if (message.startsWith("Translated using Weblate") || message.startsWith("Translated using Hosted Weblate")) { + let hist = codeContributors + if ( + message.startsWith("Translated using Weblate") || + message.startsWith("Translated using Hosted Weblate") + ) { hist = translationContributors } hist.set(author, 1 + (hist.get(author) ?? 0)) } - + const codeContributorsTarget = "assets/contributors.json" writeFileSync(codeContributorsTarget, JSON.stringify(asList(codeContributors), null, " ")) const translatorsTarget = "assets/translators.json" - writeFileSync(translatorsTarget, JSON.stringify(asList(translationContributors), null, " ")) - - })); + writeFileSync( + translatorsTarget, + JSON.stringify(asList(translationContributors), null, " ") + ) + }) } -main() \ No newline at end of file +main() diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index cdf296b60..ad83f7632 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -1,131 +1,152 @@ -import Combine from "../UI/Base/Combine"; -import BaseUIElement from "../UI/BaseUIElement"; -import Translations from "../UI/i18n/Translations"; -import {existsSync, mkdirSync, writeFileSync} from "fs"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import TableOfContents from "../UI/Base/TableOfContents"; -import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"; -import ValidatedTextField from "../UI/Input/ValidatedTextField"; -import SpecialVisualizations from "../UI/SpecialVisualizations"; -import {ExtraFunctions} from "../Logic/ExtraFunctions"; -import Title from "../UI/Base/Title"; -import Minimap from "../UI/Base/Minimap"; -import QueryParameterDocumentation from "../UI/QueryParameterDocumentation"; -import ScriptUtils from "./ScriptUtils"; -import List from "../UI/Base/List"; -import SharedTagRenderings from "../Customizations/SharedTagRenderings"; - -function WriteFile(filename, html: BaseUIElement, autogenSource: string[], options?: { - noTableOfContents: boolean -}): void { - +import Combine from "../UI/Base/Combine" +import BaseUIElement from "../UI/BaseUIElement" +import Translations from "../UI/i18n/Translations" +import { existsSync, mkdirSync, writeFileSync } from "fs" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import TableOfContents from "../UI/Base/TableOfContents" +import SimpleMetaTaggers from "../Logic/SimpleMetaTagger" +import ValidatedTextField from "../UI/Input/ValidatedTextField" +import SpecialVisualizations from "../UI/SpecialVisualizations" +import { ExtraFunctions } from "../Logic/ExtraFunctions" +import Title from "../UI/Base/Title" +import Minimap from "../UI/Base/Minimap" +import QueryParameterDocumentation from "../UI/QueryParameterDocumentation" +import ScriptUtils from "./ScriptUtils" +import List from "../UI/Base/List" +import SharedTagRenderings from "../Customizations/SharedTagRenderings" +function WriteFile( + filename, + html: BaseUIElement, + autogenSource: string[], + options?: { + noTableOfContents: boolean + } +): void { for (const source of autogenSource) { - if(source.indexOf("*") > 0){ + if (source.indexOf("*") > 0) { continue } - if(!existsSync(source)){ - throw "While creating a documentation file and checking that the generation sources are properly linked: source file "+source+" was not found. Typo?" + if (!existsSync(source)) { + throw ( + "While creating a documentation file and checking that the generation sources are properly linked: source file " + + source + + " was not found. Typo?" + ) } } - - if (html instanceof Combine && !(options?.noTableOfContents)) { - const toc = new TableOfContents(html); - const els = html.getElements(); - html = new Combine( - [els.shift(), - toc, - ...els - ] - ).SetClass("flex flex-col") + + if (html instanceof Combine && !options?.noTableOfContents) { + const toc = new TableOfContents(html) + const els = html.getElements() + html = new Combine([els.shift(), toc, ...els]).SetClass("flex flex-col") } - let md = new Combine([Translations.W(html), - "\n\nThis document is autogenerated from " + autogenSource.map(file => `[${file}](https://github.com/pietervdvn/MapComplete/blob/develop/${file})`).join(", ") + let md = new Combine([ + Translations.W(html), + "\n\nThis document is autogenerated from " + + autogenSource + .map( + (file) => + `[${file}](https://github.com/pietervdvn/MapComplete/blob/develop/${file})` + ) + .join(", "), ]).AsMarkdown() - md.replace(/\n\n\n+/g, "\n\n"); + md.replace(/\n\n\n+/g, "\n\n") - writeFileSync(filename, md); + writeFileSync(filename, md) } console.log("Starting documentation generation...") AllKnownLayouts.GenOverviewsForSingleLayer((layer, element, inlineSource) => { console.log("Exporting ", layer.id) - if(!existsSync("./Docs/Layers")){ + if (!existsSync("./Docs/Layers")) { mkdirSync("./Docs/Layers") } let source: string = `assets/layers/${layer.id}/${layer.id}.json` - if(inlineSource !== undefined){ + if (inlineSource !== undefined) { source = `assets/themes/${inlineSource}/${inlineSource}.json` } - WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source], {noTableOfContents: true}) - + WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source], { noTableOfContents: true }) }) -WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), ["UI/SpecialVisualizations.ts"]) -WriteFile("./Docs/CalculatedTags.md", new Combine([new Title("Metatags", 1), - SimpleMetaTaggers.HelpText(), ExtraFunctions.HelpText()]).SetClass("flex-col"), - ["Logic/SimpleMetaTagger.ts", "Logic/ExtraFunctions.ts"]) -WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(), ["UI/Input/ValidatedTextField.ts"]); -WriteFile("./Docs/BuiltinLayers.md", AllKnownLayouts.GenLayerOverviewText(), ["Customizations/AllKnownLayouts.ts"]) -WriteFile("./Docs/BuiltinQuestions.md", SharedTagRenderings.HelpText(), ["Customizations/SharedTagRenderings.ts","assets/tagRenderings/questions.json"]) +WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [ + "UI/SpecialVisualizations.ts", +]) +WriteFile( + "./Docs/CalculatedTags.md", + new Combine([ + new Title("Metatags", 1), + SimpleMetaTaggers.HelpText(), + ExtraFunctions.HelpText(), + ]).SetClass("flex-col"), + ["Logic/SimpleMetaTagger.ts", "Logic/ExtraFunctions.ts"] +) +WriteFile("./Docs/SpecialInputElements.md", ValidatedTextField.HelpText(), [ + "UI/Input/ValidatedTextField.ts", +]) +WriteFile("./Docs/BuiltinLayers.md", AllKnownLayouts.GenLayerOverviewText(), [ + "Customizations/AllKnownLayouts.ts", +]) +WriteFile("./Docs/BuiltinQuestions.md", SharedTagRenderings.HelpText(), [ + "Customizations/SharedTagRenderings.ts", + "assets/tagRenderings/questions.json", +]) { // Generate the builtinIndex which shows interlayer dependencies - var layers = ScriptUtils.getLayerFiles().map(f => f.parsed) - var builtinsPerLayer= new Map<string, string[]>(); - var layersUsingBuiltin = new Map<string /* Builtin */, string[]>(); + var layers = ScriptUtils.getLayerFiles().map((f) => f.parsed) + var builtinsPerLayer = new Map<string, string[]>() + var layersUsingBuiltin = new Map<string /* Builtin */, string[]>() for (const layer of layers) { - if(layer.tagRenderings === undefined){ + if (layer.tagRenderings === undefined) { continue } - const usedBuiltins : string[] = [] + const usedBuiltins: string[] = [] for (const tagRendering of layer.tagRenderings) { - if(typeof tagRendering === "string"){ + if (typeof tagRendering === "string") { usedBuiltins.push(tagRendering) continue } - if(tagRendering["builtin"] !== undefined){ + if (tagRendering["builtin"] !== undefined) { const builtins = tagRendering["builtin"] - if(typeof builtins === "string"){ + if (typeof builtins === "string") { usedBuiltins.push(builtins) - }else{ + } else { usedBuiltins.push(...builtins) } } } for (const usedBuiltin of usedBuiltins) { var using = layersUsingBuiltin.get(usedBuiltin) - if(using === undefined){ + if (using === undefined) { layersUsingBuiltin.set(usedBuiltin, [layer.id]) - }else{ + } else { using.push(layer.id) } } - + builtinsPerLayer.set(layer.id, usedBuiltins) } - + const docs = new Combine([ - new Title("Index of builtin TagRendering" ,1), + new Title("Index of builtin TagRendering", 1), new Title("Existing builtin tagrenderings", 2), - ... Array.from(layersUsingBuiltin.entries()).map(([builtin, usedByLayers]) => - new Combine([ - new Title(builtin), - new List(usedByLayers) - ]).SetClass("flex flex-col") - ) + ...Array.from(layersUsingBuiltin.entries()).map(([builtin, usedByLayers]) => + new Combine([new Title(builtin), new List(usedByLayers)]).SetClass("flex flex-col") + ), ]).SetClass("flex flex-col") WriteFile("./Docs/BuiltinIndex.md", docs, ["assets/layers/*.json"]) } -Minimap.createMiniMap = _ => { - console.log("Not creating a minimap, it is disabled"); +Minimap.createMiniMap = (_) => { + console.log("Not creating a minimap, it is disabled") return undefined } - -WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryParameterDocs(), ["Logic/Web/QueryParameters.ts", "UI/QueryParameterDocumentation.ts"]) +WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryParameterDocs(), [ + "Logic/Web/QueryParameters.ts", + "UI/QueryParameterDocumentation.ts", +]) console.log("Generated docs") - diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index 67eab59e3..5ddc59b24 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -1,44 +1,43 @@ -import * as fs from "fs"; +import * as fs from "fs" function genImages(dryrun = false) { - console.log("Generating images") const dir = fs.readdirSync("./assets/svg") - let module = "import Img from \"./UI/Base/Img\";\nimport {FixedUiElement} from \"./UI/Base/FixedUiElement\";\n\nexport default class Svg {\n\n\n"; - const allNames: string[] = []; + let module = + 'import Img from "./UI/Base/Img";\nimport {FixedUiElement} from "./UI/Base/FixedUiElement";\n\nexport default class Svg {\n\n\n' + const allNames: string[] = [] for (const path of dir) { - if (path.endsWith("license_info.json")) { - continue; + continue } if (!path.endsWith(".svg")) { - throw "Non-svg file detected in the svg files: " + path; + throw "Non-svg file detected in the svg files: " + path } - let svg : string = fs.readFileSync("./assets/svg/" + path, "utf-8") + let svg: string = fs + .readFileSync("./assets/svg/" + path, "utf-8") .replace(/<\?xml.*?>/, "") .replace(/fill: ?none;/g, "fill: none !important;") // This is such a brittle hack... .replace(/\n/g, " ") .replace(/\r/g, "") .replace(/\\/g, "\\") - .replace(/"/g, "\\\"") + .replace(/"/g, '\\"') - let hasNonAsciiChars = Array.from(svg).some(char => char.charCodeAt(0) > 127); - if(hasNonAsciiChars){ - throw "The svg '"+path+"' has non-ascii characters"; + let hasNonAsciiChars = Array.from(svg).some((char) => char.charCodeAt(0) > 127) + if (hasNonAsciiChars) { + throw "The svg '" + path + "' has non-ascii characters" } - const name = path.substr(0, path.length - 4) - .replace(/[ -]/g, "_"); + const name = path.substr(0, path.length - 4).replace(/[ -]/g, "_") if (dryrun) { svg = "xxx" } - let rawName = name; + let rawName = name if (dryrun) { - rawName = "add"; + rawName = "add" } module += ` public static ${name} = "${svg}"\n` @@ -50,9 +49,9 @@ function genImages(dryrun = false) { } } module += `public static All = {${allNames.join(",")}};` - module += "}\n"; - fs.writeFileSync("Svg.ts", module); + module += "}\n" + fs.writeFileSync("Svg.ts", module) console.log("Done") } -genImages() \ No newline at end of file +genImages() diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 3bc8fc720..67b3097c4 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -1,43 +1,44 @@ -import ScriptUtils from "./ScriptUtils"; -import {existsSync, mkdirSync, readFileSync, statSync, writeFileSync} from "fs"; +import ScriptUtils from "./ScriptUtils" +import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs" import * as licenses from "../assets/generated/license_info.json" -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import Constants from "../Models/Constants"; +import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import Constants from "../Models/Constants" import { DoesImageExist, PrevalidateTheme, ValidateLayer, ValidateTagRenderings, - ValidateThemeAndLayers -} from "../Models/ThemeConfig/Conversion/Validation"; -import {Translation} from "../UI/i18n/Translation"; -import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import * as questions from "../assets/tagRenderings/questions.json"; -import * as icons from "../assets/tagRenderings/icons.json"; -import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"; -import {PrepareLayer} from "../Models/ThemeConfig/Conversion/PrepareLayer"; -import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; -import {DesugaringContext} from "../Models/ThemeConfig/Conversion/Conversion"; -import {Utils} from "../Utils"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; + ValidateThemeAndLayers, +} from "../Models/ThemeConfig/Conversion/Validation" +import { Translation } from "../UI/i18n/Translation" +import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" +import * as questions from "../assets/tagRenderings/questions.json" +import * as icons from "../assets/tagRenderings/icons.json" +import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson" +import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer" +import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" +import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion" +import { Utils } from "../Utils" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" // This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files. // It spits out an overview of those to be used to load them class LayerOverviewUtils { - public static readonly layerPath = "./assets/generated/layers/" public static readonly themePath = "./assets/generated/themes/" private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set<string> { - const publicThemes = [].concat(...themefiles - .filter(th => !th.hideFromOverview)) + const publicThemes = [].concat(...themefiles.filter((th) => !th.hideFromOverview)) - return new Set([].concat(...publicThemes.map(th => this.extractLayerIdsFrom(th)))) + return new Set([].concat(...publicThemes.map((th) => this.extractLayerIdsFrom(th)))) } - private static extractLayerIdsFrom(themeFile: LayoutConfigJson, includeInlineLayers = true): string[] { + private static extractLayerIdsFrom( + themeFile: LayoutConfigJson, + includeInlineLayers = true + ): string[] { const publicLayerIds = [] for (const publicLayer of themeFile.layers) { if (typeof publicLayer === "string") { @@ -50,7 +51,7 @@ class LayerOverviewUtils { publicLayerIds.push(bi) continue } - bi.forEach(id => publicLayerIds.push(id)) + bi.forEach((id) => publicLayerIds.push(id)) continue } if (includeInlineLayers) { @@ -62,24 +63,33 @@ class LayerOverviewUtils { shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean { if (!existsSync(targetfile)) { - return true; + return true } const targetModified = statSync(targetfile).mtime if (typeof sourcefile === "string") { sourcefile = [sourcefile] } - return sourcefile.some(sourcefile => statSync(sourcefile).mtime > targetModified) + return sourcefile.some((sourcefile) => statSync(sourcefile).mtime > targetModified) } - writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean, mustHaveLanguage: boolean, layers: (LayerConfigJson | string | { builtin })[] }[]) { - const perId = new Map<string, any>(); + writeSmallOverview( + themes: { + id: string + title: any + shortDescription: any + icon: string + hideFromOverview: boolean + mustHaveLanguage: boolean + layers: (LayerConfigJson | string | { builtin })[] + }[] + ) { + const perId = new Map<string, any>() for (const theme of themes) { - const keywords: {}[] = [] - for (const layer of (theme.layers ?? [])) { - const l = <LayerConfigJson>layer; - keywords.push({"*": l.id}) + for (const layer of theme.layers ?? []) { + const l = <LayerConfigJson>layer + keywords.push({ "*": l.id }) keywords.push(l.title) keywords.push(l.description) } @@ -91,56 +101,69 @@ class LayerOverviewUtils { icon: theme.icon, hideFromOverview: theme.hideFromOverview, mustHaveLanguage: theme.mustHaveLanguage, - keywords: Utils.NoNull(keywords) + keywords: Utils.NoNull(keywords), } - perId.set(theme.id, data); + perId.set(theme.id, data) } - - const sorted = Constants.themeOrder.map(id => { + const sorted = Constants.themeOrder.map((id) => { if (!perId.has(id)) { throw "Ordered theme id " + id + " not found" } - return perId.get(id); - }); - + return perId.get(id) + }) perId.forEach((value) => { if (Constants.themeOrder.indexOf(value.id) >= 0) { - return; // actually a continue + return // actually a continue } sorted.push(value) }) - writeFileSync("./assets/generated/theme_overview.json", JSON.stringify(sorted, null, " "), "UTF8"); + writeFileSync( + "./assets/generated/theme_overview.json", + JSON.stringify(sorted, null, " "), + "UTF8" + ) } writeTheme(theme: LayoutConfigJson) { if (!existsSync(LayerOverviewUtils.themePath)) { - mkdirSync(LayerOverviewUtils.themePath); + mkdirSync(LayerOverviewUtils.themePath) } - writeFileSync(`${LayerOverviewUtils.themePath}${theme.id}.json`, JSON.stringify(theme, null, " "), "UTF8"); + writeFileSync( + `${LayerOverviewUtils.themePath}${theme.id}.json`, + JSON.stringify(theme, null, " "), + "UTF8" + ) } writeLayer(layer: LayerConfigJson) { if (!existsSync(LayerOverviewUtils.layerPath)) { - mkdirSync(LayerOverviewUtils.layerPath); + mkdirSync(LayerOverviewUtils.layerPath) } - writeFileSync(`${LayerOverviewUtils.layerPath}${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8"); + writeFileSync( + `${LayerOverviewUtils.layerPath}${layer.id}.json`, + JSON.stringify(layer, null, " "), + "UTF8" + ) } getSharedTagRenderings(doesImageExist: DoesImageExist): Map<string, TagRenderingConfigJson> { - const dict = new Map<string, TagRenderingConfigJson>(); + const dict = new Map<string, TagRenderingConfigJson>() - const validator = new ValidateTagRenderings(undefined, doesImageExist); + const validator = new ValidateTagRenderings(undefined, doesImageExist) for (const key in questions["default"]) { if (key === "id") { continue } - questions[key].id = key; + questions[key].id = key questions[key]["source"] = "shared-questions" const config = <TagRenderingConfigJson>questions[key] - validator.convertStrict(config, "generate-layer-overview:tagRenderings/questions.json:" + key) + validator.convertStrict( + config, + "generate-layer-overview:tagRenderings/questions.json:" + key + ) dict.set(key, config) } for (const key in icons["default"]) { @@ -150,9 +173,12 @@ class LayerOverviewUtils { if (typeof icons[key] !== "object") { continue } - icons[key].id = key; + icons[key].id = key const config = <TagRenderingConfigJson>icons[key] - validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:" + key) + validator.convertStrict( + config, + "generate-layer-overview:tagRenderings/icons.json:" + key + ) dict.set(key, config) } @@ -160,35 +186,43 @@ class LayerOverviewUtils { if (key === "id") { return } - value.id = value.id ?? key; + value.id = value.id ?? key }) - return dict; + return dict } checkAllSvgs() { const allSvgs = ScriptUtils.readDirRecSync("./assets") - .filter(path => path.endsWith(".svg")) - .filter(path => !path.startsWith("./assets/generated")) - let errCount = 0; - const exempt = ["assets/SocialImageTemplate.svg", "assets/SocialImageTemplateWide.svg", "assets/SocialImageBanner.svg", "assets/svg/osm-logo.svg"]; + .filter((path) => path.endsWith(".svg")) + .filter((path) => !path.startsWith("./assets/generated")) + let errCount = 0 + const exempt = [ + "assets/SocialImageTemplate.svg", + "assets/SocialImageTemplateWide.svg", + "assets/SocialImageBanner.svg", + "assets/svg/osm-logo.svg", + ] for (const path of allSvgs) { - if (exempt.some(p => "./" + p === path)) { + if (exempt.some((p) => "./" + p === path)) { continue } const contents = readFileSync(path, "UTF8") if (contents.indexOf("data:image/png;") >= 0) { console.warn("The SVG at " + path + " is a fake SVG: it contains PNG data!") - errCount++; + errCount++ if (path.startsWith("./assets/svg")) { throw "A core SVG is actually a PNG. Don't do this!" } } if (contents.indexOf("<text") > 0) { - console.warn("The SVG at " + path + " contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path") - errCount++; - + console.warn( + "The SVG at " + + path + + " contains a `text`-tag. This is highly discouraged. Every machine viewing your theme has their own font libary, and the font you choose might not be present, resulting in a different font being rendered. Solution: open your .svg in inkscape (or another program), select the text and convert it to a path" + ) + errCount++ } } if (errCount > 0) { @@ -197,91 +231,115 @@ class LayerOverviewUtils { } main(args: string[]) { - - const forceReload = args.some(a => a == "--force") + const forceReload = args.some((a) => a == "--force") const licensePaths = new Set<string>() for (const i in licenses) { licensePaths.add(licenses[i].path) } const doesImageExist = new DoesImageExist(licensePaths, existsSync) - const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload); + const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload) const recompiledThemes: string[] = [] - const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers, recompiledThemes, forceReload) + const sharedThemes = this.buildThemeIndex( + doesImageExist, + sharedLayers, + recompiledThemes, + forceReload + ) - writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ - "layers": Array.from(sharedLayers.values()), - "themes": Array.from(sharedThemes.values()) - })) + writeFileSync( + "./assets/generated/known_layers_and_themes.json", + JSON.stringify({ + layers: Array.from(sharedLayers.values()), + themes: Array.from(sharedThemes.values()), + }) + ) - writeFileSync("./assets/generated/known_layers.json", JSON.stringify({layers: Array.from(sharedLayers.values())})) + writeFileSync( + "./assets/generated/known_layers.json", + JSON.stringify({ layers: Array.from(sharedLayers.values()) }) + ) - - if (recompiledThemes.length > 0 && !(recompiledThemes.length === 1 && recompiledThemes[0] === "mapcomplete-changes")) { + if ( + recompiledThemes.length > 0 && + !(recompiledThemes.length === 1 && recompiledThemes[0] === "mapcomplete-changes") + ) { // mapcomplete-changes shows an icon for each corresponding mapcomplete-theme - const iconsPerTheme = - Array.from(sharedThemes.values()).map(th => ({ - if: "theme=" + th.id, - then: th.icon - })) - const proto: LayoutConfigJson = JSON.parse(readFileSync("./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", "UTF8")); - const protolayer = <LayerConfigJson>(proto.layers.filter(l => l["id"] === "mapcomplete-changes")[0]) - const rendering = (<PointRenderingConfigJson>protolayer.mapRendering[0]) + const iconsPerTheme = Array.from(sharedThemes.values()).map((th) => ({ + if: "theme=" + th.id, + then: th.icon, + })) + const proto: LayoutConfigJson = JSON.parse( + readFileSync( + "./assets/themes/mapcomplete-changes/mapcomplete-changes.proto.json", + "UTF8" + ) + ) + const protolayer = <LayerConfigJson>( + proto.layers.filter((l) => l["id"] === "mapcomplete-changes")[0] + ) + const rendering = <PointRenderingConfigJson>protolayer.mapRendering[0] rendering.icon["mappings"] = iconsPerTheme - writeFileSync('./assets/themes/mapcomplete-changes/mapcomplete-changes.json', JSON.stringify(proto, null, " ")) + writeFileSync( + "./assets/themes/mapcomplete-changes/mapcomplete-changes.json", + JSON.stringify(proto, null, " ") + ) } this.checkAllSvgs() - - if(AllKnownLayouts.getSharedLayersConfigs().size == 0){ - console.error( "This was a bootstrapping-run. Run generate layeroverview again!") - }else{ - const green = s => '\x1b[92m' + s + '\x1b[0m' + + if (AllKnownLayouts.getSharedLayersConfigs().size == 0) { + console.error("This was a bootstrapping-run. Run generate layeroverview again!") + } else { + const green = (s) => "\x1b[92m" + s + "\x1b[0m" console.log(green("All done!")) } } - private buildLayerIndex(doesImageExist: DoesImageExist, forceReload: boolean): Map<string, LayerConfigJson> { + private buildLayerIndex( + doesImageExist: DoesImageExist, + forceReload: boolean + ): Map<string, LayerConfigJson> { // First, we expand and validate all builtin layers. These are written to assets/generated/layers // At the same time, an index of available layers is built. console.log(" ---------- VALIDATING BUILTIN LAYERS ---------") - const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist); + const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist) const state: DesugaringContext = { tagRenderings: sharedTagRenderings, - sharedLayers: AllKnownLayouts.getSharedLayersConfigs() + sharedLayers: AllKnownLayouts.getSharedLayersConfigs(), } const sharedLayers = new Map<string, LayerConfigJson>() - const prepLayer = new PrepareLayer(state); + const prepLayer = new PrepareLayer(state) const skippedLayers: string[] = [] const recompiledLayers: string[] = [] for (const sharedLayerPath of ScriptUtils.getLayerPaths()) { - { - const targetPath = LayerOverviewUtils.layerPath + sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/")) + const targetPath = + LayerOverviewUtils.layerPath + + sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/")) if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) { const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8")) sharedLayers.set(sharedLayer.id, sharedLayer) skippedLayers.push(sharedLayer.id) - console.log("Loaded "+sharedLayer.id) - continue; + console.log("Loaded " + sharedLayer.id) + continue } - } - let parsed; + let parsed try { parsed = JSON.parse(readFileSync(sharedLayerPath, "utf8")) } catch (e) { - throw ("Could not parse or read file " + sharedLayerPath) + throw "Could not parse or read file " + sharedLayerPath } const context = "While building builtin layer " + sharedLayerPath const fixed = prepLayer.convertStrict(parsed, context) if (fixed.source.osmTags["and"] === undefined) { - fixed.source.osmTags = {"and": [fixed.source.osmTags]} + fixed.source.osmTags = { and: [fixed.source.osmTags] } } - const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist); + const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist) validator.convertStrict(fixed, context) if (sharedLayers.has(fixed.id)) { @@ -292,74 +350,108 @@ class LayerOverviewUtils { recompiledLayers.push(fixed.id) this.writeLayer(fixed) - - } - console.log("Recompiled layers " + recompiledLayers.join(", ") + " and skipped " + skippedLayers.length + " layers") + console.log( + "Recompiled layers " + + recompiledLayers.join(", ") + + " and skipped " + + skippedLayers.length + + " layers" + ) - return sharedLayers; + return sharedLayers } - private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map<string, LayerConfigJson>, recompiledThemes: string[], forceReload: boolean): Map<string, LayoutConfigJson> { + private buildThemeIndex( + doesImageExist: DoesImageExist, + sharedLayers: Map<string, LayerConfigJson>, + recompiledThemes: string[], + forceReload: boolean + ): Map<string, LayoutConfigJson> { console.log(" ---------- VALIDATING BUILTIN THEMES ---------") - const themeFiles = ScriptUtils.getThemeFiles(); - const fixed = new Map<string, LayoutConfigJson>(); + const themeFiles = ScriptUtils.getThemeFiles() + const fixed = new Map<string, LayoutConfigJson>() - const publicLayers = LayerOverviewUtils.publicLayerIdsFrom(themeFiles.map(th => th.parsed)) + const publicLayers = LayerOverviewUtils.publicLayerIdsFrom( + themeFiles.map((th) => th.parsed) + ) const convertState: DesugaringContext = { sharedLayers, tagRenderings: this.getSharedTagRenderings(doesImageExist), - publicLayers + publicLayers, } const skippedThemes: string[] = [] for (const themeInfo of themeFiles) { - - const themePath = themeInfo.path; + const themePath = themeInfo.path let themeFile = themeInfo.parsed { - const targetPath = LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/")) - const usedLayers = Array.from(LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)) - .map(id => LayerOverviewUtils.layerPath + id + ".json") + const targetPath = + LayerOverviewUtils.themePath + + "/" + + themePath.substring(themePath.lastIndexOf("/")) + const usedLayers = Array.from( + LayerOverviewUtils.extractLayerIdsFrom(themeFile, false) + ).map((id) => LayerOverviewUtils.layerPath + id + ".json") if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) { - fixed.set(themeFile.id, JSON.parse(readFileSync(LayerOverviewUtils.themePath + themeFile.id + ".json", 'utf8'))) + fixed.set( + themeFile.id, + JSON.parse( + readFileSync( + LayerOverviewUtils.themePath + themeFile.id + ".json", + "utf8" + ) + ) + ) skippedThemes.push(themeFile.id) - continue; + continue } recompiledThemes.push(themeFile.id) } new PrevalidateTheme().convertStrict(themeFile, themePath) try { - themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath) - new ValidateThemeAndLayers(doesImageExist, themePath, true, convertState.tagRenderings) - .convertStrict(themeFile, themePath) + new ValidateThemeAndLayers( + doesImageExist, + themePath, + true, + convertState.tagRenderings + ).convertStrict(themeFile, themePath) this.writeTheme(themeFile) fixed.set(themeFile.id, themeFile) } catch (e) { console.error("ERROR: could not prepare theme " + themePath + " due to " + e) - throw e; + throw e } } - this.writeSmallOverview(Array.from(fixed.values()).map(t => { - return { - ...t, - hideFromOverview: t.hideFromOverview ?? false, - shortDescription: t.shortDescription ?? new Translation(t.description).FirstSentence().translations, - mustHaveLanguage: t.mustHaveLanguage?.length > 0, - } - })); + this.writeSmallOverview( + Array.from(fixed.values()).map((t) => { + return { + ...t, + hideFromOverview: t.hideFromOverview ?? false, + shortDescription: + t.shortDescription ?? + new Translation(t.description).FirstSentence().translations, + mustHaveLanguage: t.mustHaveLanguage?.length > 0, + } + }) + ) - console.log("Recompiled themes " + recompiledThemes.join(", ") + " and skipped " + skippedThemes.length + " themes") - - return fixed; + console.log( + "Recompiled themes " + + recompiledThemes.join(", ") + + " and skipped " + + skippedThemes.length + + " themes" + ) + return fixed } } diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 1934ff9b7..f702e4e6e 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -1,35 +1,34 @@ -import {appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; -import Locale from "../UI/i18n/Locale"; -import Translations from "../UI/i18n/Translations"; -import {Translation} from "../UI/i18n/Translation"; +import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs" +import Locale from "../UI/i18n/Locale" +import Translations from "../UI/i18n/Translations" +import { Translation } from "../UI/i18n/Translation" import * as all_known_layouts from "../assets/generated/known_layers_and_themes.json" -import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import xml2js from 'xml2js'; -import ScriptUtils from "./ScriptUtils"; -import {Utils} from "../Utils"; - -const sharp = require('sharp'); -const template = readFileSync("theme.html", "utf8"); -const codeTemplate = readFileSync("index_theme.ts.template", "utf8"); +import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import xml2js from "xml2js" +import ScriptUtils from "./ScriptUtils" +import { Utils } from "../Utils" +const sharp = require("sharp") +const template = readFileSync("theme.html", "utf8") +const codeTemplate = readFileSync("index_theme.ts.template", "utf8") function enc(str: string): string { - return encodeURIComponent(str.toLowerCase()); + return encodeURIComponent(str.toLowerCase()) } async function createIcon(iconPath: string, size: number, alreadyWritten: string[]) { - let name = iconPath.split(".").slice(0, -1).join("."); // drop svg suffix + let name = iconPath.split(".").slice(0, -1).join(".") // drop svg suffix if (name.startsWith("./")) { name = name.substr(2) } - const newname = `assets/generated/images/${name.replace(/\//g,"_")}${size}.png`; + const newname = `assets/generated/images/${name.replace(/\//g, "_")}${size}.png` if (alreadyWritten.indexOf(newname) >= 0) { - return newname; + return newname } - alreadyWritten.push(newname); + alreadyWritten.push(newname) if (existsSync(newname)) { return newname } @@ -48,20 +47,25 @@ async function createIcon(iconPath: string, size: number, alreadyWritten: string console.error("Could not read icon", iconPath, " to create a PNG due to", e) } - return newname; + return newname } async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): Promise<string> { if (!layout.icon.endsWith(".svg")) { - console.warn("Not creating a social image for " + layout.id + " as it is _not_ a .svg: " + layout.icon) + console.warn( + "Not creating a social image for " + + layout.id + + " as it is _not_ a .svg: " + + layout.icon + ) return undefined } const path = `./assets/generated/images/social_image_${layout.id}_${template}.svg` - if(existsSync(path)){ - return path; + if (existsSync(path)) { + return path } const svg = await ScriptUtils.ReadSvg(layout.icon) - let width: string = svg.$.width; + let width: string = svg.$.width if (width === undefined) { throw "The logo at " + layout.icon + " does not have a defined width" } @@ -74,15 +78,16 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P delete svg["defs"] delete svg["$"] let templateSvg = await ScriptUtils.ReadSvg("./assets/SocialImageTemplate" + template + ".svg") - templateSvg = Utils.WalkJson(templateSvg, + templateSvg = Utils.WalkJson( + templateSvg, (leaf) => { - const {cx, cy, r} = leaf["circle"][0].$ + const { cx, cy, r } = leaf["circle"][0].$ return { $: { id: "icon", - transform: `translate(${cx - r},${cy - r}) scale(${(r * 2) / Number(width)}) ` + transform: `translate(${cx - r},${cy - r}) scale(${(r * 2) / Number(width)}) `, }, - g: [svg] + g: [svg], } }, (mightBeTokenToReplace) => { @@ -93,73 +98,76 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P } ) - - const builder = new xml2js.Builder(); - const xml = builder.buildObject({svg: templateSvg}); + const builder = new xml2js.Builder() + const xml = builder.buildObject({ svg: templateSvg }) writeFileSync(path, xml) console.log("Created social image at ", path) return path } -async function createManifest(layout: LayoutConfig, alreadyWritten: string[]): Promise<{ - manifest: any, +async function createManifest( + layout: LayoutConfig, + alreadyWritten: string[] +): Promise<{ + manifest: any whiteIcons: string[] }> { Translation.forcedLanguage = "en" - const icons = []; + const icons = [] const whiteIcons: string[] = [] - let icon = layout.icon; + let icon = layout.icon if (icon.endsWith(".svg") || icon.startsWith("<svg") || icon.startsWith("<?xml")) { // This is an svg. Lets create the needed pngs and do some checkes! - const whiteBackgroundPath = "./assets/generated/images/theme_" + layout.id + "_white_background.svg" + const whiteBackgroundPath = + "./assets/generated/images/theme_" + layout.id + "_white_background.svg" { const svg = await ScriptUtils.ReadSvg(icon) - const width: string = svg.$.width; - const height: string = svg.$.height; + const width: string = svg.$.width + const height: string = svg.$.height - const builder = new xml2js.Builder(); - const withRect = {rect: {"$": {width, height, style: "fill:#ffffff;"}}, ...svg} - const xml = builder.buildObject({svg: withRect}); + const builder = new xml2js.Builder() + const withRect = { rect: { $: { width, height, style: "fill:#ffffff;" } }, ...svg } + const xml = builder.buildObject({ svg: withRect }) writeFileSync(whiteBackgroundPath, xml) } - let path = layout.icon; + let path = layout.icon if (layout.icon.startsWith("<")) { // THis is already the svg path = "./assets/generated/images/" + layout.id + "_logo.svg" writeFileSync(path, layout.icon) } - const sizes = [72, 96, 120, 128, 144, 152, 180, 192, 384, 512]; + const sizes = [72, 96, 120, 128, 144, 152, 180, 192, 384, 512] for (const size of sizes) { - const name = await createIcon(path, size, alreadyWritten); + const name = await createIcon(path, size, alreadyWritten) const whiteIcon = await createIcon(whiteBackgroundPath, size, alreadyWritten) whiteIcons.push(whiteIcon) icons.push({ src: name, sizes: size + "x" + size, - type: "image/png" + type: "image/png", }) } icons.push({ src: path, sizes: "513x513", - type: "image/svg" + type: "image/svg", }) } else if (icon.endsWith(".png")) { icons.push({ src: icon, sizes: "513x513", - type: "image/png" + type: "image/png", }) } else { console.log(icon) throw "Icon is not an svg for " + layout.id } - const ogTitle = Translations.T(layout.title).txt; - const ogDescr = Translations.T(layout.description ?? "").txt; + const ogTitle = Translations.T(layout.title).txt + const ogDescr = Translations.T(layout.description ?? "").txt const manifest = { name: ogTitle, @@ -171,48 +179,50 @@ async function createManifest(layout: LayoutConfig, alreadyWritten: string[]): P description: ogDescr, orientation: "portrait-primary, landscape-primary", icons: icons, - categories: ["map", "navigation"] - }; + categories: ["map", "navigation"], + } return { manifest, - whiteIcons + whiteIcons, } } async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { - - Locale.language.setData(layout.language[0]); + Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] - const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"'); - const ogDescr = Translations.T(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap").textFor(targetLanguage).replace(/"/g, '\\"'); - let ogImage = layout.socialImage; + const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"') + const ogDescr = Translations.T( + layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap" + ) + .textFor(targetLanguage) + .replace(/"/g, '\\"') + let ogImage = layout.socialImage let twitterImage = ogImage if (ogImage === LayoutConfig.defaultSocialImage && layout.official) { - ogImage = await createSocialImage(layout, "") ?? layout.socialImage - twitterImage = await createSocialImage(layout, "Wide") ?? layout.socialImage + ogImage = (await createSocialImage(layout, "")) ?? layout.socialImage + twitterImage = (await createSocialImage(layout, "Wide")) ?? layout.socialImage } if (twitterImage.endsWith(".svg")) { // svgs are badly supported as social image, we use a generated svg instead - twitterImage = await createIcon(twitterImage, 512, alreadyWritten); + twitterImage = await createIcon(twitterImage, 512, alreadyWritten) } - - if(ogImage.endsWith(".svg")){ + + if (ogImage.endsWith(".svg")) { ogImage = await createIcon(ogImage, 512, alreadyWritten) } - let customCss = ""; + let customCss = "" if (layout.customCss !== undefined && layout.customCss !== "") { - try { - const cssContent = readFileSync(layout.customCss); - customCss = "<style>" + cssContent + "</style>"; + const cssContent = readFileSync(layout.customCss) + customCss = "<style>" + cssContent + "</style>" } catch (e) { customCss = `<link rel='stylesheet' href="${layout.customCss}"/>` } } const og = ` - <meta property="og:image" content="${ogImage ?? 'assets/SocialImage.png'}"> + <meta property="og:image" content="${ogImage ?? "assets/SocialImage.png"}"> <meta property="og:title" content="${ogTitle}"> <meta property="og:description" content="${ogDescr}"> <meta name="twitter:card" content="summary_large_image"> @@ -222,11 +232,11 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr <meta name="twitter:description" content="${ogDescr}"> <meta name="twitter:image" content="${twitterImage}">` - let icon = layout.icon; + let icon = layout.icon if (icon.startsWith("<?xml") || icon.startsWith("<svg")) { // This already is an svg icon = `./assets/generated/images/${layout.id}_icon.svg` - writeFileSync(icon, layout.icon); + writeFileSync(icon, layout.icon) } const apple_icons = [] @@ -244,31 +254,49 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr og, customCss, `<link rel="icon" href="${icon}" sizes="any" type="image/svg+xml">`, - ...apple_icons + ...apple_icons, ].join("\n") - const loadingText = Translations.t.general.loadingTheme.Subs({theme: ogTitle}); + const loadingText = Translations.t.general.loadingTheme.Subs({ theme: ogTitle }) let output = template .replace("Loading MapComplete, hang on...", loadingText.textFor(targetLanguage)) - .replace("Powered by OpenStreetMap", Translations.t.general.poweredByOsm.textFor(targetLanguage)) + .replace( + "Powered by OpenStreetMap", + Translations.t.general.poweredByOsm.textFor(targetLanguage) + ) .replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific) - .replace(/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, layout.shortDescription.textFor(targetLanguage)) - .replace("<script src=\"./index.ts\"></script>", `<script src='./index_${layout.id}.ts'></script>`); -0 + .replace( + /<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, + layout.shortDescription.textFor(targetLanguage) + ) + .replace( + '<script src="./index.ts"></script>', + `<script src='./index_${layout.id}.ts'></script>` + ) + 0 try { output = output - .replace(/<!-- DECORATION 0 START -->.*<!-- DECORATION 0 END -->/s, `<img src='${icon}' width="100%" height="100%">`) - .replace(/<!-- DECORATION 1 START -->.*<!-- DECORATION 1 END -->/s, `<img src='${icon}' width="100%" height="100%">`); + .replace( + /<!-- DECORATION 0 START -->.*<!-- DECORATION 0 END -->/s, + `<img src='${icon}' width="100%" height="100%">` + ) + .replace( + /<!-- DECORATION 1 START -->.*<!-- DECORATION 1 END -->/s, + `<img src='${icon}' width="100%" height="100%">` + ) } catch (e) { console.warn("Error while applying logo: ", e) } - return output; + return output } async function createIndexFor(theme: LayoutConfig) { const filename = "index_" + theme.id + ".ts" - writeFileSync(filename, `import * as themeConfig from "./assets/generated/themes/${theme.id}.json"\n`) + writeFileSync( + filename, + `import * as themeConfig from "./assets/generated/themes/${theme.id}.json"\n` + ) appendFileSync(filename, codeTemplate) } @@ -279,17 +307,28 @@ function createDir(path) { } async function main(): Promise<void> { - - const alreadyWritten = [] createDir("./assets/generated") createDir("./assets/generated/layers") createDir("./assets/generated/themes") createDir("./assets/generated/images") - const blacklist = ["", "test", ".", "..", "manifest", "index", "land", "preferences", "account", "openstreetmap", "custom", "theme"] + const blacklist = [ + "", + "test", + ".", + "..", + "manifest", + "index", + "land", + "preferences", + "account", + "openstreetmap", + "custom", + "theme", + ] // @ts-ignore - const all: LayoutConfigJson[] = all_known_layouts.themes; + const all: LayoutConfigJson[] = all_known_layouts.themes const args = process.argv const theme = args[2] if (theme !== undefined) { @@ -303,18 +342,18 @@ async function main(): Promise<void> { const layout = new LayoutConfig(layoutConfigJson, true) const layoutName = layout.id if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { - console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`); - continue; + console.log(`Skipping a layout with name${layoutName}, it is on the blacklist`) + continue } - const err = err => { + const err = (err) => { if (err !== null) { console.log("Could not write manifest for ", layoutName, " because ", err) } - }; - const {manifest, whiteIcons} = await createManifest(layout, alreadyWritten) - const manif = JSON.stringify(manifest, undefined, 2); - const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest"; - writeFile(manifestLocation, manif, err); + } + const { manifest, whiteIcons } = await createManifest(layout, alreadyWritten) + const manif = JSON.stringify(manifest, undefined, 2) + const manifestLocation = encodeURIComponent(layout.id.toLowerCase()) + ".webmanifest" + writeFile(manifestLocation, manif, err) // Create a landing page for the given theme const landing = await createLandingPage(layout, manifest, whiteIcons, alreadyWritten) @@ -322,23 +361,25 @@ async function main(): Promise<void> { await createIndexFor(layout) } + const { manifest } = await createManifest( + new LayoutConfig({ + icon: "./assets/svg/mapcomplete_logo.svg", + id: "index", + layers: [], + socialImage: "assets/SocialImage.png", + startLat: 0, + startLon: 0, + startZoom: 0, + title: { en: "MapComplete" }, + description: { en: "A thematic map viewer and editor based on OpenStreetMap" }, + }), + alreadyWritten + ) - const {manifest} = await createManifest(new LayoutConfig({ - icon: "./assets/svg/mapcomplete_logo.svg", - id: "index", - layers: [], - socialImage: "assets/SocialImage.png", - startLat: 0, - startLon: 0, - startZoom: 0, - title: {en: "MapComplete"}, - description: {en: "A thematic map viewer and editor based on OpenStreetMap"} - }), alreadyWritten); - - const manif = JSON.stringify(manifest, undefined, 2); + const manif = JSON.stringify(manifest, undefined, 2) writeFileSync("index.manifest", manif) } main().then(() => { console.log("All done!") -}) \ No newline at end of file +}) diff --git a/scripts/generateLicenseInfo.ts b/scripts/generateLicenseInfo.ts index 45d698621..c5e4e03ae 100644 --- a/scripts/generateLicenseInfo.ts +++ b/scripts/generateLicenseInfo.ts @@ -1,18 +1,17 @@ -import {existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync} from "fs"; -import SmallLicense from "../Models/smallLicense"; -import ScriptUtils from "./ScriptUtils"; +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs" +import SmallLicense from "../Models/smallLicense" +import ScriptUtils from "./ScriptUtils" -const prompt = require('prompt-sync')(); +const prompt = require("prompt-sync")() -function validateLicenseInfo(l : SmallLicense){ - l.sources.map(s => { - try{ - - return new URL(s); - }catch (e) { - throw "Could not parse URL "+s+" for a license for "+l.path+" due to "+ e - } - }) +function validateLicenseInfo(l: SmallLicense) { + l.sources.map((s) => { + try { + return new URL(s) + } catch (e) { + throw "Could not parse URL " + s + " for a license for " + l.path + " due to " + e + } + }) } /** * Sweeps the entire 'assets/' (except assets/generated) directory for image files and any 'license_info.json'-file. @@ -27,18 +26,19 @@ function generateLicenseInfos(paths: string[]): SmallLicense[] { if (Array.isArray(parsed)) { const l: SmallLicense[] = parsed for (const smallLicens of l) { - smallLicens.path = path.substring(0, path.length - "license_info.json".length) + smallLicens.path + smallLicens.path = + path.substring(0, path.length - "license_info.json".length) + + smallLicens.path } licenses.push(...l) } else { - const smallLicens: SmallLicense = parsed; + const smallLicens: SmallLicense = parsed smallLicens.path = path.substring(0, 1 + path.lastIndexOf("/")) + smallLicens.path licenses.push(smallLicens) } } catch (e) { console.error("Error: ", e, "while handling", path) } - } return licenses } @@ -53,77 +53,81 @@ function missingLicenseInfos(licenseInfos: SmallLicense[], allIcons: string[]) { for (const iconPath of allIcons) { if (iconPath.indexOf("license_info.json") >= 0) { - continue; + continue } if (knownPaths.has(iconPath)) { - continue; + continue } missing.push(iconPath) } - return missing; + return missing } - const knownLicenses = new Map<string, SmallLicense>() knownLicenses.set("me", { authors: ["Pieter Vander Vennet"], path: undefined, license: "CC0", - sources: [] + sources: [], }) knownLicenses.set("streetcomplete", { authors: ["Tobias Zwick (westnordost)"], path: undefined, license: "CC0", - sources: ["https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics", "https://f-droid.org/packages/de.westnordost.streetcomplete/"] + sources: [ + "https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics", + "https://f-droid.org/packages/de.westnordost.streetcomplete/", + ], }) knownLicenses.set("temaki", { authors: ["Temaki"], path: undefined, license: "CC0", - sources: ["https://github.com/ideditor/temaki","https://ideditor.github.io/temaki/docs/"] + sources: ["https://github.com/ideditor/temaki", "https://ideditor.github.io/temaki/docs/"], }) knownLicenses.set("maki", { authors: ["Maki"], path: undefined, license: "CC0", - sources: ["https://labs.mapbox.com/maki-icons/"] + sources: ["https://labs.mapbox.com/maki-icons/"], }) knownLicenses.set("t", { authors: [], path: undefined, license: "CC0; trivial", - sources: [] + sources: [], }) knownLicenses.set("na", { authors: [], path: undefined, license: "CC0", - sources: [] + sources: [], }) knownLicenses.set("tv", { authors: ["Toerisme Vlaanderen"], path: undefined, license: "CC0", - sources: ["https://toerismevlaanderen.be/pinjepunt","https://mapcomplete.osm.be/toerisme_vlaanderenn"] + sources: [ + "https://toerismevlaanderen.be/pinjepunt", + "https://mapcomplete.osm.be/toerisme_vlaanderenn", + ], }) knownLicenses.set("tvf", { authors: ["Jo De Baerdemaeker "], path: undefined, license: "All rights reserved", - sources: ["https://www.studiotype.be/fonts/flandersart"] + sources: ["https://www.studiotype.be/fonts/flandersart"], }) knownLicenses.set("twemoji", { authors: ["Twemoji"], path: undefined, license: "CC-BY 4.0", - sources: ["https://github.com/twitter/twemoji"] + sources: ["https://github.com/twitter/twemoji"], }) - function promptLicenseFor(path): SmallLicense { console.log("License abbreviations:") knownLicenses.forEach((value, key) => { @@ -133,13 +137,13 @@ function promptLicenseFor(path): SmallLicense { path = path.substring(path.lastIndexOf("/") + 1) if (knownLicenses.has(author)) { - const license = knownLicenses.get(author); - license.path = path; - return license; + const license = knownLicenses.get(author) + license.path = path + return license } if (author == "s") { - return null; + return null } if (author == "Q" || author == "q" || author == "") { throw "Quitting now!" @@ -152,14 +156,14 @@ function promptLicenseFor(path): SmallLicense { authors: author.split(";"), path: path, license: prompt("What is the license for artwork " + path + "? > "), - sources: prompt("Where was this artwork found? > ").split(";") + sources: prompt("Where was this artwork found? > ").split(";"), } } function createLicenseInfoFor(path): void { - const li = promptLicenseFor(path); + const li = promptLicenseFor(path) if (li == null) { - return; + return } writeFileSync(path + ".license_info.json", JSON.stringify(li, null, " ")) } @@ -185,39 +189,40 @@ function cleanLicenseInfo(allPaths: string[], allLicenseInfos: SmallLicense[]) { path: license.path, license: license.license, authors: license.authors, - sources: license.sources + sources: license.sources, } perDirectory.get(dir).push(cloned) } perDirectory.forEach((licenses, dir) => { - - for (let i = licenses.length - 1; i >= 0; i--) { - const license = licenses[i]; + const license = licenses[i] const path = dir + "/" + license.path if (!existsSync(path)) { - console.log("Found license for now missing file: ", path, " - removing this license") + console.log( + "Found license for now missing file: ", + path, + " - removing this license" + ) licenses.splice(i, 1) } } - licenses.sort((a, b) => a.path < b.path ? -1 : 1) + licenses.sort((a, b) => (a.path < b.path ? -1 : 1)) writeFileSync(dir + "/license_info.json", JSON.stringify(licenses, null, 2)) }) - } function queryMissingLicenses(missingLicenses: string[]) { - process.on('SIGINT', function () { - console.log("Aborting... Bye!"); - process.exit(); - }); + process.on("SIGINT", function () { + console.log("Aborting... Bye!") + process.exit() + }) - let i = 1; + let i = 1 for (const missingLicens of missingLicenses) { console.log(i + " / " + missingLicenses.length) - i++; + i++ if (i < missingLicenses.length - 5) { // continue } @@ -227,16 +232,14 @@ function queryMissingLicenses(missingLicenses: string[]) { console.log("You're through!") } - /** * Creates the humongous license_info in the generated assets, containing all licenses with a path relative to the root * @param licensePaths */ function createFullLicenseOverview(licensePaths: string[]) { - const allLicenses: SmallLicense[] = [] for (const licensePath of licensePaths) { - if(!existsSync(licensePath)){ + if (!existsSync(licensePath)) { continue } const licenses = <SmallLicense[]>JSON.parse(readFileSync(licensePath, "UTF-8")) @@ -251,43 +254,46 @@ function createFullLicenseOverview(licensePaths: string[]) { writeFileSync("./assets/generated/license_info.json", JSON.stringify(allLicenses, null, " ")) } -function main(args: string[]){ - +function main(args: string[]) { console.log("Checking and compiling license info") - + if (!existsSync("./assets/generated")) { mkdirSync("./assets/generated") } - - - let contents = ScriptUtils.readDirRecSync("./assets") - .filter(entry => entry.indexOf("./assets/generated") != 0) - let licensePaths = contents.filter(entry => entry.indexOf("license_info.json") >= 0) - let licenseInfos = generateLicenseInfos(licensePaths); - - - - const artwork = contents.filter(pth => pth.match(/(.svg|.png|.jpg|.ttf|.otf|.woff)$/i) != null) + + let contents = ScriptUtils.readDirRecSync("./assets").filter( + (entry) => entry.indexOf("./assets/generated") != 0 + ) + let licensePaths = contents.filter((entry) => entry.indexOf("license_info.json") >= 0) + let licenseInfos = generateLicenseInfos(licensePaths) + + const artwork = contents.filter( + (pth) => pth.match(/(.svg|.png|.jpg|.ttf|.otf|.woff)$/i) != null + ) const missingLicenses = missingLicenseInfos(licenseInfos, artwork) if (args.indexOf("--prompt") >= 0 || args.indexOf("--query") >= 0) { queryMissingLicenses(missingLicenses) return main([]) } - - const invalidLicenses = licenseInfos.filter(l => (l.license ?? "") === "").map(l => `License for artwork ${l.path} is empty string or undefined`) + + const invalidLicenses = licenseInfos + .filter((l) => (l.license ?? "") === "") + .map((l) => `License for artwork ${l.path} is empty string or undefined`) for (const licenseInfo of licenseInfos) { for (const source of licenseInfo.sources) { if (source == "") { - invalidLicenses.push("Invalid license: empty string in " + JSON.stringify(licenseInfo)) + invalidLicenses.push( + "Invalid license: empty string in " + JSON.stringify(licenseInfo) + ) } try { - new URL(source); + new URL(source) } catch { invalidLicenses.push("Not a valid URL: " + source) } } } - + if (missingLicenses.length > 0) { const msg = `There are ${missingLicenses.length} licenses missing and ${invalidLicenses.length} invalid licenses.` console.log(missingLicenses.concat(invalidLicenses).join("\n")) @@ -296,7 +302,7 @@ function main(args: string[]){ throw msg } } - + cleanLicenseInfo(licensePaths, licenseInfos) createFullLicenseOverview(licensePaths) } diff --git a/scripts/generateStats.ts b/scripts/generateStats.ts index 8ad991945..126d31f12 100644 --- a/scripts/generateStats.ts +++ b/scripts/generateStats.ts @@ -1,10 +1,10 @@ import * as known_layers from "../assets/generated/known_layers.json" -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; -import {TagUtils} from "../Logic/Tags/TagUtils"; -import {Utils} from "../Utils"; -import {writeFileSync} from "fs"; -import ScriptUtils from "./ScriptUtils"; -import Constants from "../Models/Constants"; +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" +import { TagUtils } from "../Logic/Tags/TagUtils" +import { Utils } from "../Utils" +import { writeFileSync } from "fs" +import ScriptUtils from "./ScriptUtils" +import Constants from "../Models/Constants" /* Downloads stats on osmSource-tags and keys from tagInfo */ @@ -15,7 +15,6 @@ async function main(includeTags = true) { const keysAndTags = new Map<string, Set<string>>() for (const layer of layers) { - if (layer.source["geoJson"] !== undefined && !layer.source["isOsmCache"]) { continue } @@ -41,37 +40,42 @@ async function main(includeTags = true) { const keyTotal = new Map<string, number>() const tagTotal = new Map<string, Map<string, number>>() - await Promise.all(Array.from(keysAndTags.keys()).map(async key => { - const values = keysAndTags.get(key) - const data = await Utils.downloadJson(`https://taginfo.openstreetmap.org/api/4/key/stats?key=${key}`) - const count = data.data.find(item => item.type === "all").count - keyTotal.set(key, count) - console.log(key, "-->", count) - - - if (values.size > 0) { - - tagTotal.set(key, new Map<string, number>()) - await Promise.all( - Array.from(values).map(async value => { - const tagData = await Utils.downloadJson(`https://taginfo.openstreetmap.org/api/4/tag/stats?key=${key}&value=${value}`) - const count = tagData.data.find(item => item.type === "all").count - tagTotal.get(key).set(value, count) - console.log(key + "=" + value, "-->", count) - }) + await Promise.all( + Array.from(keysAndTags.keys()).map(async (key) => { + const values = keysAndTags.get(key) + const data = await Utils.downloadJson( + `https://taginfo.openstreetmap.org/api/4/key/stats?key=${key}` ) + const count = data.data.find((item) => item.type === "all").count + keyTotal.set(key, count) + console.log(key, "-->", count) - } - })) - writeFileSync("./assets/key_totals.json", + if (values.size > 0) { + tagTotal.set(key, new Map<string, number>()) + await Promise.all( + Array.from(values).map(async (value) => { + const tagData = await Utils.downloadJson( + `https://taginfo.openstreetmap.org/api/4/tag/stats?key=${key}&value=${value}` + ) + const count = tagData.data.find((item) => item.type === "all").count + tagTotal.get(key).set(value, count) + console.log(key + "=" + value, "-->", count) + }) + ) + } + }) + ) + writeFileSync( + "./assets/key_totals.json", JSON.stringify( { - keys: Utils.MapToObj(keyTotal, t => t), - tags: Utils.MapToObj(tagTotal, v => Utils.MapToObj(v, t => t)) + keys: Utils.MapToObj(keyTotal, (t) => t), + tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)), }, - null, " " + null, + " " ) ) } -main().then(() => console.log("All done")) \ No newline at end of file +main().then(() => console.log("All done")) diff --git a/scripts/generateTaginfoProjectFiles.ts b/scripts/generateTaginfoProjectFiles.ts index 5ebd0f7eb..f6ce32915 100644 --- a/scripts/generateTaginfoProjectFiles.ts +++ b/scripts/generateTaginfoProjectFiles.ts @@ -1,11 +1,11 @@ -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import Locale from "../UI/i18n/Locale"; -import {Translation} from "../UI/i18n/Translation"; -import {readFileSync, writeFileSync} from "fs"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import Constants from "../Models/Constants"; -import {Utils} from "../Utils"; +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import Locale from "../UI/i18n/Locale" +import { Translation } from "../UI/i18n/Translation" +import { readFileSync, writeFileSync } from "fs" +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import Constants from "../Models/Constants" +import { Utils } from "../Utils" /** * Generates all the files in "Docs/TagInfo". These are picked up by the taginfo project, showing a link to the mapcomplete theme if the key is used @@ -13,17 +13,20 @@ import {Utils} from "../Utils"; const outputDirectory = "Docs/TagInfo" -function generateTagOverview(kv: { k: string, v: string }, description: string): { - key: string, - description: string, +function generateTagOverview( + kv: { k: string; v: string }, + description: string +): { + key: string + description: string value?: string } { const overview = { // OSM tag key (required) key: kv.k, description: description, - value: undefined - }; + value: undefined, + } if (kv.v !== undefined) { // OSM tag value (optional, if not supplied it means "all values") overview.value = kv.v @@ -31,20 +34,24 @@ function generateTagOverview(kv: { k: string, v: string }, description: string): return overview } -function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any [] { - +function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any[] { if (layer.name === undefined) { return [] // Probably a duplicate or irrelevant layer } const usedTags = layer.source.osmTags.asChange({}) const result: { - key: string, - description: string, + key: string + description: string value?: string }[] = [] for (const kv of usedTags) { - const description = "The MapComplete theme " + layout.title.txt + " has a layer " + layer.name.txt + " showing features with this tag" + const description = + "The MapComplete theme " + + layout.title.txt + + " has a layer " + + layer.name.txt + + " showing features with this tag" result.push(generateTagOverview(kv, description)) } @@ -54,54 +61,57 @@ function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any [] { const usesImageUpload = (tr.render?.txt?.indexOf("image_upload") ?? -2) > 0 if (usesImageCarousel || usesImageUpload) { + const descrNoUpload = `The layer '${layer.name.txt} shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary` + const descrUpload = `The layer '${layer.name.txt} allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary` - const descrNoUpload = `The layer '${layer.name.txt} shows images based on the keys image, image:0, image:1,... and wikidata, wikipedia, wikimedia_commons and mapillary`; - const descrUpload = `The layer '${layer.name.txt} allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary`; - - const descr = usesImageUpload ? descrUpload : descrNoUpload; - result.push(generateTagOverview({k: "image", v: undefined}, descr)); - result.push(generateTagOverview({k: "mapillary", v: undefined}, descr)); - result.push(generateTagOverview({k: "wikidata", v: undefined}, descr)); - result.push(generateTagOverview({k: "wikipedia", v: undefined}, descr)); + const descr = usesImageUpload ? descrUpload : descrNoUpload + result.push(generateTagOverview({ k: "image", v: undefined }, descr)) + result.push(generateTagOverview({ k: "mapillary", v: undefined }, descr)) + result.push(generateTagOverview({ k: "wikidata", v: undefined }, descr)) + result.push(generateTagOverview({ k: "wikipedia", v: undefined }, descr)) } } - const q = tr.question?.txt; - const key = tr.freeform?.key; + const q = tr.question?.txt + const key = tr.freeform?.key if (key != undefined) { - let descr = `Layer '${layer.name.txt}'`; + let descr = `Layer '${layer.name.txt}'` if (q == undefined) { - descr += " shows values with"; + descr += " shows values with" } else { descr += " shows and asks freeform values for" } descr += ` key '${key}' (in the MapComplete.osm.be theme '${layout.title.txt}')` - result.push(generateTagOverview({k: key, v: undefined}, descr)) + result.push(generateTagOverview({ k: key, v: undefined }, descr)) } const mappings = tr.mappings ?? [] for (const mapping of mappings) { - - let descr = "Layer '" + layer.name.txt + "'"; - descr += " shows " + mapping.if.asHumanString(false, false, {}) + " with a fixed text, namely '" + mapping.then.txt + "'"; - if (q != undefined - && mapping.hideInAnswer != true // != true will also match if a + let descr = "Layer '" + layer.name.txt + "'" + descr += + " shows " + + mapping.if.asHumanString(false, false, {}) + + " with a fixed text, namely '" + + mapping.then.txt + + "'" + if ( + q != undefined && + mapping.hideInAnswer != true // != true will also match if a ) { descr += " and allows to pick this as a default answer" } descr += ` (in the MapComplete.osm.be theme '${layout.title.txt}')` for (const kv of mapping.if.asChange({})) { - let d = descr; + let d = descr if (q != undefined && kv.v == "") { d = `${descr} Picking this answer will delete the key ${kv.k}.` } result.push(generateTagOverview(kv, d)) } - } } - return result.filter(result => !result.key.startsWith("_")) + return result.filter((result) => !result.key.startsWith("_")) } /** @@ -111,39 +121,38 @@ function generateLayerUsage(layer: LayerConfig, layout: LayoutConfig): any [] { function generateTagInfoEntry(layout: LayoutConfig): any { const usedTags = [] for (const layer of layout.layers) { - if(Constants.priviliged_layers.indexOf(layer.id) >= 0){ + if (Constants.priviliged_layers.indexOf(layer.id) >= 0) { continue } - if(layer.source.geojsonSource !== undefined && layer.source.isOsmCacheLayer !== true){ + if (layer.source.geojsonSource !== undefined && layer.source.isOsmCacheLayer !== true) { continue } usedTags.push(...generateLayerUsage(layer, layout)) } - - if(usedTags.length == 0){ + + if (usedTags.length == 0) { return undefined } - - let icon = layout.icon; + let icon = layout.icon if (icon.startsWith("./")) { icon = icon.substring(2) } const themeInfo = { // data format version, currently always 1, will get updated if there are incompatible changes to the format (required) - "data_format": 1, + data_format: 1, // timestamp when project file was updated is not given as it pollutes the github history - "project": { - "name": "MapComplete " + layout.title.txt, // name of the project (required) - "description": layout.shortDescription.txt, // short description of the project (required) - "project_url": "https://mapcomplete.osm.be/" + layout.id, // home page of the project with general information (required) - "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", // documentation of the project and especially the tags used (optional) - "icon_url": "https://mapcomplete.osm.be/" + icon, // project logo, should work in 16x16 pixels on white and light gray backgrounds (optional) - "contact_name": "Pieter Vander Vennet", // contact name, needed for taginfo maintainer (required) - "contact_email": "pietervdvn@posteo.net" // contact email, needed for taginfo maintainer (required) + project: { + name: "MapComplete " + layout.title.txt, // name of the project (required) + description: layout.shortDescription.txt, // short description of the project (required) + project_url: "https://mapcomplete.osm.be/" + layout.id, // home page of the project with general information (required) + doc_url: "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", // documentation of the project and especially the tags used (optional) + icon_url: "https://mapcomplete.osm.be/" + icon, // project logo, should work in 16x16 pixels on white and light gray backgrounds (optional) + contact_name: "Pieter Vander Vennet", // contact name, needed for taginfo maintainer (required) + contact_email: "pietervdvn@posteo.net", // contact email, needed for taginfo maintainer (required) }, - tags: usedTags + tags: usedTags, } const filename = "mapcomplete_" + layout.id @@ -158,39 +167,40 @@ function generateProjectsOverview(files: string[]) { const tagInfoList = "../taginfo-projects/project_list.txt" let projectList = readFileSync(tagInfoList, "UTF8") .split("\n") - .filter(entry => entry.indexOf("mapcomplete_") < 0) - .concat(files.map(f => `${f} https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/TagInfo/${f}.json`)) + .filter((entry) => entry.indexOf("mapcomplete_") < 0) + .concat( + files.map( + (f) => + `${f} https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/TagInfo/${f}.json` + ) + ) .sort() - .filter(entry => entry != "") - - console.log("Writing taginfo project filelist"); - writeFileSync(tagInfoList, projectList.join("\n") + "\n"); - + .filter((entry) => entry != "") + console.log("Writing taginfo project filelist") + writeFileSync(tagInfoList, projectList.join("\n") + "\n") } catch (e) { - console.warn("Could not write the taginfo-projects list - the repository is probably not checked out. Are you creating a fork? Ignore this message then.") + console.warn( + "Could not write the taginfo-projects list - the repository is probably not checked out. Are you creating a fork? Ignore this message then." + ) } - } +function main() { + console.log("Creating taginfo project files") -function main(){ - - -console.log("Creating taginfo project files") - -Locale.language.setData("en") -Translation.forcedLanguage = "en" + Locale.language.setData("en") + Translation.forcedLanguage = "en" const files = [] - + for (const layout of AllKnownLayouts.layoutsList) { if (layout.hideFromOverview) { - continue; + continue } - files.push(generateTagInfoEntry(layout)); + files.push(generateTagInfoEntry(layout)) } - generateProjectsOverview(Utils.NoNull(files)); + generateProjectsOverview(Utils.NoNull(files)) } -main() \ No newline at end of file +main() diff --git a/scripts/generateTileOverview.ts b/scripts/generateTileOverview.ts index 24445f2fa..57ec8ffc4 100644 --- a/scripts/generateTileOverview.ts +++ b/scripts/generateTileOverview.ts @@ -1,20 +1,17 @@ /** * Generates an overview for which tiles exist and which don't */ -import ScriptUtils from "./ScriptUtils"; -import {writeFileSync} from "fs"; +import ScriptUtils from "./ScriptUtils" +import { writeFileSync } from "fs" function main(args: string[]) { - const directory = args[0] let zoomLevel = args[1] - const files = ScriptUtils.readDirRecSync(directory, 1) - .filter(f => f.endsWith(".geojson")) + const files = ScriptUtils.readDirRecSync(directory, 1).filter((f) => f.endsWith(".geojson")) const indices /* Map<string, number[]>*/ = {} for (const path of files) { - - const match = path.match(".*_\([0-9]*\)_\([0-9]*\)_\([0-9]*\).geojson") + const match = path.match(".*_([0-9]*)_([0-9]*)_([0-9]*).geojson") if (match === null) { continue } @@ -32,8 +29,8 @@ function main(args: string[]) { } indices[x].push(Number(y)) } - indices["zoom"] = zoomLevel; - const match = files[0].match("\(.*\)_\([0-9]*\)_\([0-9]*\)_\([0-9]*\).geojson") + indices["zoom"] = zoomLevel + const match = files[0].match("(.*)_([0-9]*)_([0-9]*)_([0-9]*).geojson") const path = match[1] + "_" + zoomLevel + "_overview.json" writeFileSync(path, JSON.stringify(indices)) console.log("Written overview for", files.length, " tiles at", path) @@ -41,10 +38,10 @@ function main(args: string[]) { let args = [...process.argv] args.splice(0, 2) -args.forEach(file => { +args.forEach((file) => { try { main(args) } catch (e) { console.error("Could not handle file ", file, " due to ", e) } -}) \ No newline at end of file +}) diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index 844132848..73c9f790d 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -1,16 +1,15 @@ -import * as fs from "fs"; -import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"; -import {Utils} from "../Utils"; -import ScriptUtils from "./ScriptUtils"; +import * as fs from "fs" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" +import { Utils } from "../Utils" +import ScriptUtils from "./ScriptUtils" -const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]; +const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] class TranslationPart { - contents: Map<string, TranslationPart | string> = new Map<string, TranslationPart | string>() static fromDirectory(path): TranslationPart { - const files = ScriptUtils.readDirRecSync(path, 1).filter(file => file.endsWith(".json")) + const files = ScriptUtils.readDirRecSync(path, 1).filter((file) => file.endsWith(".json")) const rootTranslation = new TranslationPart() for (const file of files) { const content = JSON.parse(readFileSync(file, "UTF8")) @@ -37,36 +36,49 @@ class TranslationPart { } else { subpart.add(language, v) } - } } addTranslationObject(translations: any, context?: string) { if (translations["#"] === "no-translations") { console.log("Ignoring object at ", context, "which has '#':'no-translations'") - return; + return } for (const translationsKey in translations) { if (!translations.hasOwnProperty(translationsKey)) { - continue; + continue } const v = translations[translationsKey] - if (typeof (v) != "string") { - console.error(`Non-string object at ${context} in translation while trying to add the translation ` + JSON.stringify(v) + ` to '` + translationsKey + "'. The offending object which _should_ be a translation is: ", v, "\n\nThe current object is (only showing en):", this.toJson(), "and has translations for", Array.from(this.contents.keys())) - throw "Error in an object depicting a translation: a non-string object was found. (" + context + ")\n You probably put some other section accidentally in the translation" + if (typeof v != "string") { + console.error( + `Non-string object at ${context} in translation while trying to add the translation ` + + JSON.stringify(v) + + ` to '` + + translationsKey + + "'. The offending object which _should_ be a translation is: ", + v, + "\n\nThe current object is (only showing en):", + this.toJson(), + "and has translations for", + Array.from(this.contents.keys()) + ) + throw ( + "Error in an object depicting a translation: a non-string object was found. (" + + context + + ")\n You probably put some other section accidentally in the translation" + ) } this.contents.set(translationsKey, v) } } recursiveAdd(object: any, context: string) { - const isProbablyTranslationObject = knownLanguages.some(l => object.hasOwnProperty(l)); // TODO FIXME ID + const isProbablyTranslationObject = knownLanguages.some((l) => object.hasOwnProperty(l)) // TODO FIXME ID if (isProbablyTranslationObject) { this.addTranslationObject(object, context) - return; + return } - let dontTranslateKeys: string[] = undefined { const noTranslate = <string | string[]>object["#dont-translate"] @@ -80,13 +92,18 @@ class TranslationPart { dontTranslateKeys = noTranslate } if (noTranslate !== undefined) { - console.log("Ignoring some translations for " + context + ": " + dontTranslateKeys.join(", ")) + console.log( + "Ignoring some translations for " + + context + + ": " + + dontTranslateKeys.join(", ") + ) } } for (let key in object) { if (!object.hasOwnProperty(key)) { - continue; + continue } if (dontTranslateKeys?.indexOf(key) >= 0) { @@ -99,7 +116,7 @@ class TranslationPart { continue } if (typeof v !== "object") { - continue; + continue } if (context.endsWith(".tagRenderings")) { @@ -107,17 +124,21 @@ class TranslationPart { if (v["builtin"] !== undefined && typeof v["builtin"] === "string") { key = v["builtin"] } else { - throw "At " + context + ": every object within a tagRenderings-list should have an id. " + JSON.stringify(v) + " has no id" + throw ( + "At " + + context + + ": every object within a tagRenderings-list should have an id. " + + JSON.stringify(v) + + " has no id" + ) } } else { - // We use the embedded id as key instead of the index as this is more stable // Note: indonesian is shortened as 'id' as well! if (v["en"] !== undefined || v["nl"] !== undefined) { // This is probably a translation already! // pass } else { - key = v["id"] if (typeof key !== "string") { throw "Panic: found a non-string ID at" + context @@ -126,31 +147,29 @@ class TranslationPart { } } - if (!this.contents.get(key)) { this.contents.set(key, new TranslationPart()) } - - (this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key); + ;(this.contents.get(key) as TranslationPart).recursiveAdd(v, context + "." + key) } } knownLanguages(): string[] { const languages = [] for (let key of Array.from(this.contents.keys())) { - const value = this.contents.get(key); + const value = this.contents.get(key) if (typeof value === "string") { if (key === "#") { - continue; + continue } languages.push(key) } else { languages.push(...(value as TranslationPart).knownLanguages()) } } - return Utils.Dedup(languages); + return Utils.Dedup(languages) } toJson(neededLanguage?: string): string { @@ -158,35 +177,34 @@ class TranslationPart { let keys = Array.from(this.contents.keys()) keys = keys.sort() for (let key of keys) { - let value = this.contents.get(key); + let value = this.contents.get(key) if (typeof value === "string") { - value = value.replace(/"/g, "\\\"") - .replace(/\n/g, "\\n") + value = value.replace(/"/g, '\\"').replace(/\n/g, "\\n") if (neededLanguage === undefined) { parts.push(`\"${key}\": \"${value}\"`) } else if (key === neededLanguage) { return `"${value}"` } - } else { const sub = (value as TranslationPart).toJson(neededLanguage) if (sub !== "") { - parts.push(`\"${key}\": ${sub}`); + parts.push(`\"${key}\": ${sub}`) } - } } if (parts.length === 0) { - return ""; + return "" } - return `{${parts.join(",")}}`; + return `{${parts.join(",")}}` } validateStrict(ctx?: string): void { const errors = this.validate() for (const err of errors) { - console.error("ERROR in " + (ctx ?? "") + " " + err.path.join(".") + "\n " + err.error) + console.error( + "ERROR in " + (ctx ?? "") + " " + err.path.join(".") + "\n " + err.error + ) } if (errors.length > 0) { throw ctx + " has " + errors.length + " inconsistencies in the translation" @@ -196,21 +214,24 @@ class TranslationPart { /** * Checks the leaf objects: special values must be present and identical in every leaf */ - validate(path = []): { error: string, path: string[] } [] { - const errors: { error: string, path: string[] } [] = [] - const neededSubparts = new Set<{ part: string, usedByLanguage: string }>() + validate(path = []): { error: string; path: string[] }[] { + const errors: { error: string; path: string[] }[] = [] + const neededSubparts = new Set<{ part: string; usedByLanguage: string }>() let isLeaf: boolean = undefined this.contents.forEach((value, key) => { if (typeof value !== "string") { const recErrors = value.validate([...path, key]) errors.push(...recErrors) - return; + return } if (isLeaf === undefined) { isLeaf = true } else if (!isLeaf) { - errors.push({error: "Mixed node: non-leaf node has translation strings", path: path}) + errors.push({ + error: "Mixed node: non-leaf node has translation strings", + path: path, + }) } let subparts: string[] = value.match(/{[^}]*}/g) @@ -220,19 +241,18 @@ class TranslationPart { // This is a core translation, it has one less path segment lang = weblatepart } - subparts = subparts.map(p => p.split(/\(.*\)/)[0]) + subparts = subparts.map((p) => p.split(/\(.*\)/)[0]) for (const subpart of subparts) { - neededSubparts.add({part: subpart, usedByLanguage: lang}) + neededSubparts.add({ part: subpart, usedByLanguage: lang }) } } }) - // Actually check for the needed sub-parts, e.g. that {key} isn't translated into {sleutel} this.contents.forEach((value, key) => { - neededSubparts.forEach(({part, usedByLanguage}) => { + neededSubparts.forEach(({ part, usedByLanguage }) => { if (typeof value !== "string") { - return; + return } let [_, __, weblatepart, lang] = key.split("/") if (lang === undefined) { @@ -240,26 +260,40 @@ class TranslationPart { lang = weblatepart weblatepart = "core" } - const fixLink = `Fix it on https://hosted.weblate.org/translate/mapcomplete/${weblatepart}/${lang}/?offset=1&q=context%3A%3D%22${encodeURIComponent(path.join("."))}%22`; + const fixLink = `Fix it on https://hosted.weblate.org/translate/mapcomplete/${weblatepart}/${lang}/?offset=1&q=context%3A%3D%22${encodeURIComponent( + path.join(".") + )}%22` let subparts: string[] = value.match(/{[^}]*}/g) if (subparts === null) { if (neededSubparts.size > 0) { errors.push({ - error: "The translation for " + key + " does not have any subparts, but expected " + Array.from(neededSubparts).map(part => part.part + " (used in " + part.usedByLanguage + ")").join(",") + " . The full translation is " + value + "\n" + fixLink, - path: path + error: + "The translation for " + + key + + " does not have any subparts, but expected " + + Array.from(neededSubparts) + .map( + (part) => + part.part + " (used in " + part.usedByLanguage + ")" + ) + .join(",") + + " . The full translation is " + + value + + "\n" + + fixLink, + path: path, }) } return } - subparts = subparts.map(p => p.split(/\(.*\)/)[0]) + subparts = subparts.map((p) => p.split(/\(.*\)/)[0]) if (subparts.indexOf(part) < 0) { - if (lang === "en" || usedByLanguage === "en") { errors.push({ error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}). \tThe full translation is ${value} \t${fixLink}`, - path: path + path: path, }) } } @@ -278,7 +312,7 @@ class TranslationPart { private addTranslation(language: string, object: any) { for (const key in object) { const v = object[key] - if(v === ""){ + if (v === "") { delete object[key] continue } @@ -293,9 +327,7 @@ class TranslationPart { subpart.addTranslation(language, v) } } - } - } /** @@ -308,10 +340,10 @@ function isTranslation(tr: any): boolean { } for (const key in tr) { if (typeof tr[key] !== "string") { - return false; + return false } } - return true; + return true } /** @@ -319,8 +351,11 @@ function isTranslation(tr: any): boolean { * * To debug the 'compiledTranslations', add a languageWhiteList to only generate a single language */ -function transformTranslation(obj: any, path: string[] = [], languageWhitelist: string[] = undefined) { - +function transformTranslation( + obj: any, + path: string[] = [], + languageWhitelist: string[] = undefined +) { if (isTranslation(obj)) { return `new Translation( ${JSON.stringify(obj)} )` } @@ -328,7 +363,7 @@ function transformTranslation(obj: any, path: string[] = [], languageWhitelist: let values = "" for (const key in obj) { if (key === "#") { - continue; + continue } if (key.match("^[a-zA-Z0-9_]*$") === null) { @@ -342,33 +377,46 @@ function transformTranslation(obj: any, path: string[] = [], languageWhitelist: for (const ln of languageWhitelist) { nv[ln] = value[ln] } - value = nv; + value = nv } - if (value["en"] === undefined) { - throw `At ${path.join(".")}: Missing 'en' translation at path ${path.join(".")}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` + throw `At ${path.join(".")}: Missing 'en' translation at path ${path.join( + "." + )}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` } const subParts: string[] = value["en"].match(/{[^}]*}/g) - let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")` + let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join( + "." + )}.${key}")` if (subParts !== null) { // convert '{to_substitute}' into 'to_substitute' - const types = Utils.Dedup(subParts.map(tp => tp.substring(1, tp.length - 1))) - const invalid = types.filter(part => part.match(/^[a-z0-9A-Z_]+(\(.*\))?$/) == null) + const types = Utils.Dedup(subParts.map((tp) => tp.substring(1, tp.length - 1))) + const invalid = types.filter( + (part) => part.match(/^[a-z0-9A-Z_]+(\(.*\))?$/) == null + ) if (invalid.length > 0) { - throw `At ${path.join(".")}: A subpart contains invalid characters: ${subParts.join(', ')}` + throw `At ${path.join( + "." + )}: A subpart contains invalid characters: ${subParts.join(", ")}` } - expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")` + expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify( + value + )}, "core:${path.join(".")}.${key}")` } values += `${Utils.Times((_) => " ", path.length + 1)}get ${key}() { ${expr} }, ` } else { - values += (Utils.Times((_) => " ", path.length + 1)) + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) + ",\n" + values += + Utils.Times((_) => " ", path.length + 1) + + key + + ": " + + transformTranslation(value, [...path, key], languageWhitelist) + + ",\n" } } - return `{${values}}`; - + return `{${values}}` } function sortKeys(o: object): object { @@ -386,14 +434,13 @@ function sortKeys(o: object): object { return nw } - function removeEmptyString(object: object) { for (const k in object) { - if(object[k] === ""){ + if (object[k] === "") { delete object[k] continue } - if(typeof object[k] === "object"){ + if (typeof object[k] === "object") { removeEmptyString(object[k]) } } @@ -415,15 +462,16 @@ function formatFile(path) { * Generates the big compiledTranslations file */ function genTranslations() { - const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8")) - const transformed = transformTranslation(translations); + const translations = JSON.parse( + fs.readFileSync("./assets/generated/translations.json", "utf-8") + ) + const transformed = transformTranslation(translations) - let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`; - module += " public static t = " + transformed; + let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n` + module += " public static t = " + transformed module += "\n }" - fs.writeFileSync("./assets/generated/CompiledTranslations.ts", module); - + fs.writeFileSync("./assets/generated/CompiledTranslations.ts", module) } /** @@ -431,18 +479,17 @@ function genTranslations() { * This is only for the core translations */ function compileTranslationsFromWeblate() { - const translations = ScriptUtils.readDirRecSync("./langs", 1) - .filter(path => path.indexOf(".json") > 0) + const translations = ScriptUtils.readDirRecSync("./langs", 1).filter( + (path) => path.indexOf(".json") > 0 + ) const allTranslations = new TranslationPart() allTranslations.validateStrict() - for (const translationFile of translations) { try { - - const contents = JSON.parse(readFileSync(translationFile, "utf-8")); + const contents = JSON.parse(readFileSync(translationFile, "utf-8")) let language = translationFile.substring(translationFile.lastIndexOf("/") + 1) language = language.substring(0, language.length - 5) allTranslations.add(language, contents) @@ -451,8 +498,10 @@ function compileTranslationsFromWeblate() { } } - writeFileSync("./assets/generated/translations.json", JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ")) - + writeFileSync( + "./assets/generated/translations.json", + JSON.stringify(JSON.parse(allTranslations.toJson()), null, " ") + ) } /** @@ -460,12 +509,15 @@ function compileTranslationsFromWeblate() { * @param objects * @param target */ -function generateTranslationsObjectFrom(objects: { path: string, parsed: { id: string } }[], target: string): string[] { - const tr = new TranslationPart(); +function generateTranslationsObjectFrom( + objects: { path: string; parsed: { id: string } }[], + target: string +): string[] { + const tr = new TranslationPart() for (const layerFile of objects) { - const config: { id: string } = layerFile.parsed; - const layerTr = new TranslationPart(); + const config: { id: string } = layerFile.parsed + const layerTr = new TranslationPart() if (config === undefined) { throw "Got something not parsed! Path is " + layerFile.path } @@ -473,16 +525,15 @@ function generateTranslationsObjectFrom(objects: { path: string, parsed: { id: s tr.contents.set(config.id, layerTr) } - const langs = tr.knownLanguages(); + const langs = tr.knownLanguages() for (const lang of langs) { if (lang === "#" || lang === "*") { // Lets not export our comments or non-translated stuff - continue; + continue } let json = tr.toJson(lang) try { - - json = JSON.stringify(JSON.parse(json), null, " "); // MUST BE FOUR SPACES + json = JSON.stringify(JSON.parse(json), null, " ") // MUST BE FOUR SPACES } catch (e) { console.error(e) } @@ -501,7 +552,6 @@ function generateTranslationsObjectFrom(objects: { path: string, parsed: { id: s * @constructor */ function MergeTranslation(source: any, target: any, language: string, context: string = "") { - let keyRemapping: Map<string, string> = undefined if (context.endsWith(".tagRenderings")) { keyRemapping = new Map<string, string>() @@ -515,29 +565,36 @@ function MergeTranslation(source: any, target: any, language: string, context: s continue } - const sourceV = source[key]; + const sourceV = source[key] const targetV = target[keyRemapping?.get(key) ?? key] if (typeof sourceV === "string") { // Add the translation if (targetV === undefined) { if (typeof target === "string") { - throw "Trying to merge a translation into a fixed string at " + context + " for key " + key; + throw ( + "Trying to merge a translation into a fixed string at " + + context + + " for key " + + key + ) } - target[key] = source[key]; - continue; + target[key] = source[key] + continue } if (targetV[language] === sourceV) { // Already the same - continue; + continue } if (typeof targetV === "string") { - throw `At context ${context}: Could not add a translation in language ${language}. The target object has a string at the given path, whereas the translation contains an object.\n String at target: ${targetV}\n Object at translation source: ${JSON.stringify(sourceV)}` + throw `At context ${context}: Could not add a translation in language ${language}. The target object has a string at the given path, whereas the translation contains an object.\n String at target: ${targetV}\n Object at translation source: ${JSON.stringify( + sourceV + )}` } - targetV[language] = sourceV; + targetV[language] = sourceV let was = "" if (targetV[language] !== undefined && targetV[language] !== sourceV) { was = " (overwritten " + targetV[language] + ")" @@ -548,35 +605,38 @@ function MergeTranslation(source: any, target: any, language: string, context: s if (typeof sourceV === "object") { if (targetV === undefined) { try { - target[language] = sourceV; + target[language] = sourceV } catch (e) { throw `At context${context}: Could not add a translation in language ${language} due to ${e}` } } else { - MergeTranslation(sourceV, targetV, language, context + "." + key); + MergeTranslation(sourceV, targetV, language, context + "." + key) } - continue; + continue } throw "Case fallthrough" - } - return target; + return target } -function mergeLayerTranslation(layerConfig: { id: string }, path: string, translationFiles: Map<string, any>) { - const id = layerConfig.id; +function mergeLayerTranslation( + layerConfig: { id: string }, + path: string, + translationFiles: Map<string, any> +) { + const id = layerConfig.id translationFiles.forEach((translations, lang) => { const translationsForLayer = translations[id] MergeTranslation(translationsForLayer, layerConfig, lang, path + ":" + id) }) - } function loadTranslationFilesFrom(target: string): Map<string, any> { - const translationFilePaths = ScriptUtils.readDirRecSync("./langs/" + target) - .filter(path => path.endsWith(".json")) + const translationFilePaths = ScriptUtils.readDirRecSync("./langs/" + target).filter((path) => + path.endsWith(".json") + ) - const translationFiles = new Map<string, any>(); + const translationFiles = new Map<string, any>() for (const translationFilePath of translationFilePaths) { let language = translationFilePath.substr(translationFilePath.lastIndexOf("/") + 1) language = language.substr(0, language.length - 5) @@ -584,18 +644,17 @@ function loadTranslationFilesFrom(target: string): Map<string, any> { translationFiles.set(language, JSON.parse(readFileSync(translationFilePath, "utf8"))) } catch (e) { console.error("Invalid JSON file or file does not exist", translationFilePath) - throw e; + throw e } } - return translationFiles; + return translationFiles } /** * Load the translations from the weblate files back into the layers */ function mergeLayerTranslations() { - - const layerFiles = ScriptUtils.getLayerFiles(); + const layerFiles = ScriptUtils.getLayerFiles() for (const layerFile of layerFiles) { mergeLayerTranslation(layerFile.parsed, layerFile.path, loadTranslationFilesFrom("layers")) writeFileSync(layerFile.path, JSON.stringify(layerFile.parsed, null, " ")) // layers use 2 spaces @@ -606,12 +665,12 @@ function mergeLayerTranslations() { * Load the translations into the theme files */ function mergeThemeTranslations() { - const themeFiles = ScriptUtils.getThemeFiles(); + const themeFiles = ScriptUtils.getThemeFiles() for (const themeFile of themeFiles) { - const config = themeFile.parsed; + const config = themeFile.parsed mergeLayerTranslation(config, themeFile.path, loadTranslationFilesFrom("themes")) - const allTranslations = new TranslationPart(); + const allTranslations = new TranslationPart() allTranslations.recursiveAdd(config, themeFile.path) writeFileSync(themeFile.path, JSON.stringify(config, null, " ")) // Themefiles use 2 spaces } @@ -622,32 +681,46 @@ if (!existsSync("./langs/themes")) { } const themeOverwritesWeblate = process.argv[2] === "--ignore-weblate" const questionsPath = "assets/tagRenderings/questions.json" -const questionsParsed = JSON.parse(readFileSync(questionsPath, 'utf8')) +const questionsParsed = JSON.parse(readFileSync(questionsPath, "utf8")) if (!themeOverwritesWeblate) { - mergeLayerTranslations(); - mergeThemeTranslations(); + mergeLayerTranslations() + mergeThemeTranslations() - mergeLayerTranslation(questionsParsed, questionsPath, loadTranslationFilesFrom("shared-questions")) + mergeLayerTranslation( + questionsParsed, + questionsPath, + loadTranslationFilesFrom("shared-questions") + ) writeFileSync(questionsPath, JSON.stringify(questionsParsed, null, " ")) - } else { console.log("Ignore weblate") } const l1 = generateTranslationsObjectFrom(ScriptUtils.getLayerFiles(), "layers") -const l2 = generateTranslationsObjectFrom(ScriptUtils.getThemeFiles().filter(th => th.parsed.mustHaveLanguage === undefined), "themes") -const l3 = generateTranslationsObjectFrom([{path: questionsPath, parsed: questionsParsed}], "shared-questions") +const l2 = generateTranslationsObjectFrom( + ScriptUtils.getThemeFiles().filter((th) => th.parsed.mustHaveLanguage === undefined), + "themes" +) +const l3 = generateTranslationsObjectFrom( + [{ path: questionsPath, parsed: questionsParsed }], + "shared-questions" +) -const usedLanguages: string[] = Utils.Dedup(l1.concat(l2).concat(l3)).filter(v => v !== "*") +const usedLanguages: string[] = Utils.Dedup(l1.concat(l2).concat(l3)).filter((v) => v !== "*") usedLanguages.sort() -fs.writeFileSync("./assets/generated/used_languages.json", JSON.stringify({languages: usedLanguages})) +fs.writeFileSync( + "./assets/generated/used_languages.json", + JSON.stringify({ languages: usedLanguages }) +) if (!themeOverwritesWeblate) { -// Generates the core translations - compileTranslationsFromWeblate(); + // Generates the core translations + compileTranslationsFromWeblate() } genTranslations() -const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter(path => path.endsWith(".json")) +const allTranslationFiles = ScriptUtils.readDirRecSync("langs").filter((path) => + path.endsWith(".json") +) for (const path of allTranslationFiles) { formatFile(path) } diff --git a/scripts/generateWikiPage.ts b/scripts/generateWikiPage.ts index 0e7a7ac28..97ba993e0 100644 --- a/scripts/generateWikiPage.ts +++ b/scripts/generateWikiPage.ts @@ -1,10 +1,14 @@ -import {writeFile} from "fs"; -import Translations from "../UI/i18n/Translations"; +import { writeFile } from "fs" +import Translations from "../UI/i18n/Translations" import * as themeOverview from "../assets/generated/theme_overview.json" -function generateWikiEntry(layout: { hideFromOverview: boolean, id: string, shortDescription: any }) { +function generateWikiEntry(layout: { + hideFromOverview: boolean + id: string + shortDescription: any +}) { if (layout.hideFromOverview) { - return ""; + return "" } const languagesInDescr = [] @@ -12,8 +16,8 @@ function generateWikiEntry(layout: { hideFromOverview: boolean, id: string, shor languagesInDescr.push(shortDescriptionKey) } - const languages = languagesInDescr.map(ln => `{{#language:${ln}|en}}`).join(", ") - let auth = "Yes"; + const languages = languagesInDescr.map((ln) => `{{#language:${ln}|en}}`).join(", ") + let auth = "Yes" return `{{service_item |name= [https://mapcomplete.osm.be/${layout.id} ${layout.id}] |region= Worldwide @@ -21,29 +25,29 @@ function generateWikiEntry(layout: { hideFromOverview: boolean, id: string, shor |descr= A MapComplete theme: ${Translations.T(layout.shortDescription) .textFor("en") .replace("<a href='", "[[") - .replace(/'>.*<\/a>/, "]]") - } + .replace(/'>.*<\/a>/, "]]")} |material= {{yes|[https://mapcomplete.osm.be/ ${auth}]}} |image= MapComplete_Screenshot.png |genre= POI, editor, ${layout.id} }}` } -let wikiPage = "{|class=\"wikitable sortable\"\n" + +let wikiPage = + '{|class="wikitable sortable"\n' + "! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" + - "|-"; + "|-" for (const layout of themeOverview) { if (layout.hideFromOverview) { - continue; + continue } - wikiPage += "\n" + generateWikiEntry(layout); + wikiPage += "\n" + generateWikiEntry(layout) } wikiPage += "\n|}" writeFile("Docs/wikiIndex.txt", wikiPage, (err) => { if (err !== null) { - console.log("Could not save wikiindex", err); + console.log("Could not save wikiindex", err) } -}); \ No newline at end of file +}) diff --git a/scripts/lint.ts b/scripts/lint.ts index 6416b2f9f..ac10c45f1 100644 --- a/scripts/lint.ts +++ b/scripts/lint.ts @@ -1,23 +1,26 @@ -import ScriptUtils from "./ScriptUtils"; -import {writeFileSync} from "fs"; -import {FixLegacyTheme, UpdateLegacyLayer} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; -import Translations from "../UI/i18n/Translations"; -import {Translation} from "../UI/i18n/Translation"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; +import ScriptUtils from "./ScriptUtils" +import { writeFileSync } from "fs" +import { + FixLegacyTheme, + UpdateLegacyLayer, +} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" +import Translations from "../UI/i18n/Translations" +import { Translation } from "../UI/i18n/Translation" +import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" /* * This script reads all theme and layer files and reformats them inplace * Use with caution, make a commit beforehand! */ -const t : Translation = Translations.t.general.add.addNew +const t: Translation = Translations.t.general.add.addNew t.OnEveryLanguage((txt, ln) => { console.log(ln, txt) return txt }) const articles = { - /* de: "eine", + /* de: "eine", es: 'una', fr: 'une', it: 'una', @@ -27,7 +30,7 @@ const articles = { pt_BR : 'uma',//*/ } -function addArticleToPresets(layerConfig: {presets?: {title: any}[]}){ +function addArticleToPresets(layerConfig: { presets?: { title: any }[] }) { /* if(layerConfig.presets === undefined){ return @@ -59,10 +62,15 @@ function addArticleToPresets(layerConfig: {presets?: {title: any}[]}){ //*/ } -const layerFiles = ScriptUtils.getLayerFiles(); +const layerFiles = ScriptUtils.getLayerFiles() for (const layerFile of layerFiles) { try { - const fixed =<LayerConfigJson> new UpdateLegacyLayer().convertStrict(layerFile.parsed, "While linting " + layerFile.path); + const fixed = <LayerConfigJson>( + new UpdateLegacyLayer().convertStrict( + layerFile.parsed, + "While linting " + layerFile.path + ) + ) addArticleToPresets(fixed) writeFileSync(layerFile.path, JSON.stringify(fixed, null, " ")) } catch (e) { @@ -73,13 +81,16 @@ for (const layerFile of layerFiles) { const themeFiles = ScriptUtils.getThemeFiles() for (const themeFile of themeFiles) { try { - const fixed = new FixLegacyTheme().convertStrict(themeFile.parsed, "While linting " + themeFile.path); + const fixed = new FixLegacyTheme().convertStrict( + themeFile.parsed, + "While linting " + themeFile.path + ) for (const layer of fixed.layers) { - if(layer["presets"] !== undefined){ - addArticleToPresets(<any> layer) + if (layer["presets"] !== undefined) { + addArticleToPresets(<any>layer) } } - // extractInlineLayer(fixed) + // extractInlineLayer(fixed) writeFileSync(themeFile.path, JSON.stringify(fixed, null, " ")) } catch (e) { console.error("COULD NOT LINT THEME" + themeFile.path + ":\n\t" + e) diff --git a/scripts/makeConvex.ts b/scripts/makeConvex.ts index ddcca0818..7a0c340fe 100644 --- a/scripts/makeConvex.ts +++ b/scripts/makeConvex.ts @@ -1,6 +1,6 @@ -import fs from "fs"; -import {GeoOperations} from "../Logic/GeoOperations"; -import ScriptUtils from "./ScriptUtils"; +import fs from "fs" +import { GeoOperations } from "../Logic/GeoOperations" +import ScriptUtils from "./ScriptUtils" /** * Given one of more files, calculates a somewhat convex hull for them @@ -10,21 +10,19 @@ import ScriptUtils from "./ScriptUtils"; function makeConvex(file) { ScriptUtils.erasableLog("Handling", file) const geoJson = JSON.parse(fs.readFileSync(file, "UTF8")) - const convex = GeoOperations.convexHull(geoJson, {concavity: 2}) + const convex = GeoOperations.convexHull(geoJson, { concavity: 2 }) if (convex.properties === undefined) { convex.properties = {} } fs.writeFileSync(file + ".convex.geojson", JSON.stringify(convex)) - } - let args = [...process.argv] args.splice(0, 2) -args.forEach(file => { +args.forEach((file) => { try { makeConvex(file) } catch (e) { console.error("Could not handle file ", file, " due to ", e) } -}) \ No newline at end of file +}) diff --git a/scripts/mergeJsonFiles.ts b/scripts/mergeJsonFiles.ts index 082d45386..b260c2042 100644 --- a/scripts/mergeJsonFiles.ts +++ b/scripts/mergeJsonFiles.ts @@ -1,8 +1,7 @@ -import {readFileSync, writeFileSync} from "fs"; -import {Utils} from "../Utils"; +import { readFileSync, writeFileSync } from "fs" +import { Utils } from "../Utils" function main(args: string[]) { - console.log("File Merge") if (args.length != 3) { @@ -17,4 +16,4 @@ function main(args: string[]) { writeFileSync(output, JSON.stringify(f2, null, " ")) } -main(process.argv.slice(2)) \ No newline at end of file +main(process.argv.slice(2)) diff --git a/scripts/onwheels/constants.ts b/scripts/onwheels/constants.ts index dbe0cbd36..d850fb3f2 100644 --- a/scripts/onwheels/constants.ts +++ b/scripts/onwheels/constants.ts @@ -1,102 +1,102 @@ /** * Class containing all constants and tables used in the script - * + * * @class Constants */ export default class Constants { - /** - * Table used to determine tags for the category - * - * Keys are the original category names, - * values are an object containing the tags - */ - public static categories = { - restaurant: { - amenity: "restaurant", - }, - parking: { - amenity: "parking", - }, - hotel: { - tourism: "hotel", - }, - wc: { - amenity: "toilets", - }, - winkel: { - shop: "yes", - }, - apotheek: { - amenity: "pharmacy", - healthcare: "pharmacy", - }, - ziekenhuis: { - amenity: "hospital", - healthcare: "hospital", - }, - bezienswaardigheid: { - tourism: "attraction", - }, - ontspanning: { - fixme: "Needs proper tags", - }, - cafe: { - amenity: "cafe", - }, - dienst: { - fixme: "Needs proper tags", - }, - bank: { - amenity: "bank", - }, - gas: { - amenity: "fuel", - }, - medical: { - fixme: "Needs proper tags", - }, - obstacle: { - fixme: "Needs proper tags", - }, - }; + /** + * Table used to determine tags for the category + * + * Keys are the original category names, + * values are an object containing the tags + */ + public static categories = { + restaurant: { + amenity: "restaurant", + }, + parking: { + amenity: "parking", + }, + hotel: { + tourism: "hotel", + }, + wc: { + amenity: "toilets", + }, + winkel: { + shop: "yes", + }, + apotheek: { + amenity: "pharmacy", + healthcare: "pharmacy", + }, + ziekenhuis: { + amenity: "hospital", + healthcare: "hospital", + }, + bezienswaardigheid: { + tourism: "attraction", + }, + ontspanning: { + fixme: "Needs proper tags", + }, + cafe: { + amenity: "cafe", + }, + dienst: { + fixme: "Needs proper tags", + }, + bank: { + amenity: "bank", + }, + gas: { + amenity: "fuel", + }, + medical: { + fixme: "Needs proper tags", + }, + obstacle: { + fixme: "Needs proper tags", + }, + } - /** - * Table used to rename original Onwheels properties to their corresponding OSM properties - * - * Keys are the original Onwheels properties, values are the corresponding OSM properties - */ - public static names = { - ID: "id", - Naam: "name", - Straat: "addr:street", - Nummer: "addr:housenumber", - Postcode: "addr:postcode", - Plaats: "addr:city", - Website: "website", - Email: "email", - "Aantal aangepaste parkeerplaatsen": "capacity:disabled", - "Aantal treden": "step_count", - "Hellend vlak aanwezig": "ramp", - "Baby verzorging aanwezig": "changing_table", - "Totale hoogte van de treden": "kerb:height", - "Deurbreedte": "door:width", - }; + /** + * Table used to rename original Onwheels properties to their corresponding OSM properties + * + * Keys are the original Onwheels properties, values are the corresponding OSM properties + */ + public static names = { + ID: "id", + Naam: "name", + Straat: "addr:street", + Nummer: "addr:housenumber", + Postcode: "addr:postcode", + Plaats: "addr:city", + Website: "website", + Email: "email", + "Aantal aangepaste parkeerplaatsen": "capacity:disabled", + "Aantal treden": "step_count", + "Hellend vlak aanwezig": "ramp", + "Baby verzorging aanwezig": "changing_table", + "Totale hoogte van de treden": "kerb:height", + Deurbreedte: "door:width", + } - /** - * In some cases types might need to be converted as well - * - * Keys are the OSM properties, values are the wanted type - */ - public static types = { - "Hellend vlak aanwezig": "boolean", - "Baby verzorging aanwezig": "boolean", - }; + /** + * In some cases types might need to be converted as well + * + * Keys are the OSM properties, values are the wanted type + */ + public static types = { + "Hellend vlak aanwezig": "boolean", + "Baby verzorging aanwezig": "boolean", + } - /** - * Some tags also need to have units added - */ - public static units = { - "Totale hoogte van de treden": "cm", - "Deurbreedte": "cm", - }; + /** + * Some tags also need to have units added + */ + public static units = { + "Totale hoogte van de treden": "cm", + Deurbreedte: "cm", + } } diff --git a/scripts/onwheels/convertData.ts b/scripts/onwheels/convertData.ts index 4183d1cd7..7df778d31 100644 --- a/scripts/onwheels/convertData.ts +++ b/scripts/onwheels/convertData.ts @@ -1,7 +1,7 @@ -import { parse } from "csv-parse/sync"; -import { readFileSync, writeFileSync } from "fs"; -import { Feature, FeatureCollection, GeoJsonProperties } from "geojson"; -import Constants from "./constants"; +import { parse } from "csv-parse/sync" +import { readFileSync, writeFileSync } from "fs" +import { Feature, FeatureCollection, GeoJsonProperties } from "geojson" +import Constants from "./constants" /** * Function to determine the tags for a category @@ -10,15 +10,15 @@ import Constants from "./constants"; * @returns List of tags for the category */ function categoryTags(category: string): GeoJsonProperties { - const tags = { - tags: Object.keys(Constants.categories[category]).map((tag) => { - return `${tag}=${Constants.categories[category][tag]}`; - }), - }; - if (!tags) { - throw `Unknown category: ${category}`; - } - return tags; + const tags = { + tags: Object.keys(Constants.categories[category]).map((tag) => { + return `${tag}=${Constants.categories[category][tag]}` + }), + } + if (!tags) { + throw `Unknown category: ${category}` + } + return tags } /** @@ -28,70 +28,68 @@ function categoryTags(category: string): GeoJsonProperties { * @returns GeoJsonProperties for the item */ function renameTags(item): GeoJsonProperties { - const properties: GeoJsonProperties = {}; - properties.tags = []; - // Loop through the original item tags - for (const key in item) { - // Check if we need it and it's not a null value - if (Constants.names[key] && item[key]) { - // Name and id tags need to be in the properties - if (Constants.names[key] == "name" || Constants.names[key] == "id") { - properties[Constants.names[key]] = item[key]; - } - // Other tags need to be in the tags variable - if (Constants.names[key] !== "id") { - // Website needs to have at least any = encoded - if(Constants.names[key] == "website") { - let website = item[key]; - // Encode URL - website = website.replace("=", "%3D"); - item[key] = website; + const properties: GeoJsonProperties = {} + properties.tags = [] + // Loop through the original item tags + for (const key in item) { + // Check if we need it and it's not a null value + if (Constants.names[key] && item[key]) { + // Name and id tags need to be in the properties + if (Constants.names[key] == "name" || Constants.names[key] == "id") { + properties[Constants.names[key]] = item[key] + } + // Other tags need to be in the tags variable + if (Constants.names[key] !== "id") { + // Website needs to have at least any = encoded + if (Constants.names[key] == "website") { + let website = item[key] + // Encode URL + website = website.replace("=", "%3D") + item[key] = website + } + properties.tags.push(Constants.names[key] + "=" + item[key]) + } } - properties.tags.push(Constants.names[key] + "=" + item[key]); - } } - } - return properties; + return properties } /** * Convert types to match the OSM standard - * + * * @param properties The properties to convert * @returns The converted properties */ function convertTypes(properties: GeoJsonProperties): GeoJsonProperties { - // Split the tags into a list - let tags = properties.tags.split(";"); + // Split the tags into a list + let tags = properties.tags.split(";") - for (const tag in tags) { - // Split the tag into key and value - const key = tags[tag].split("=")[0]; - const value = tags[tag].split("=")[1]; - const originalKey = Object.keys(Constants.names).find( - (tag) => Constants.names[tag] === key - ); + for (const tag in tags) { + // Split the tag into key and value + const key = tags[tag].split("=")[0] + const value = tags[tag].split("=")[1] + const originalKey = Object.keys(Constants.names).find((tag) => Constants.names[tag] === key) - if (Constants.types[originalKey]) { - // We need to convert the value to the correct type - let newValue; - switch (Constants.types[originalKey]) { - case "boolean": - newValue = value === "1" ? "yes" : "no"; - break; - default: - newValue = value; - break; - } - tags[tag] = `${key}=${newValue}`; + if (Constants.types[originalKey]) { + // We need to convert the value to the correct type + let newValue + switch (Constants.types[originalKey]) { + case "boolean": + newValue = value === "1" ? "yes" : "no" + break + default: + newValue = value + break + } + tags[tag] = `${key}=${newValue}` + } } - } - // Rejoin the tags - properties.tags = tags.join(";"); + // Rejoin the tags + properties.tags = tags.join(";") - // Return the properties - return properties; + // Return the properties + return properties } /** @@ -101,27 +99,25 @@ function convertTypes(properties: GeoJsonProperties): GeoJsonProperties { * @returns The properties with units added */ function addUnits(properties: GeoJsonProperties): GeoJsonProperties { - // Split the tags into a list - let tags = properties.tags.split(";"); + // Split the tags into a list + let tags = properties.tags.split(";") - for (const tag in tags) { - const key = tags[tag].split("=")[0]; - const value = tags[tag].split("=")[1]; - const originalKey = Object.keys(Constants.names).find( - (tag) => Constants.names[tag] === key - ); + for (const tag in tags) { + const key = tags[tag].split("=")[0] + const value = tags[tag].split("=")[1] + const originalKey = Object.keys(Constants.names).find((tag) => Constants.names[tag] === key) - // Check if the property needs units, and doesn't already have them - if (Constants.units[originalKey] && value.match(/.*([A-z]).*/gi) === null) { - tags[tag] = `${key}=${value} ${Constants.units[originalKey]}`; + // Check if the property needs units, and doesn't already have them + if (Constants.units[originalKey] && value.match(/.*([A-z]).*/gi) === null) { + tags[tag] = `${key}=${value} ${Constants.units[originalKey]}` + } } - } - // Rejoin the tags - properties.tags = tags.join(";"); + // Rejoin the tags + properties.tags = tags.join(";") - // Return the properties - return properties; + // Return the properties + return properties } /** @@ -131,15 +127,15 @@ function addUnits(properties: GeoJsonProperties): GeoJsonProperties { * @param item The original CSV item */ function addMaprouletteTags(properties: GeoJsonProperties, item: any): GeoJsonProperties { - properties[ - "blurb" - ] = `This is feature out of the ${item["Categorie"]} category. + properties["blurb"] = `This is feature out of the ${item["Categorie"]} category. It may match another OSM item, if so, you can add any missing tags to it. If it doesn't match any other OSM item, you can create a new one. Here is a list of tags that can be added: ${properties["tags"].split(";").join("\n")} - You can also easily import this item using MapComplete: https://mapcomplete.osm.be/onwheels.html#${properties["id"]}`; - return properties; + You can also easily import this item using MapComplete: https://mapcomplete.osm.be/onwheels.html#${ + properties["id"] + }` + return properties } /** @@ -148,87 +144,84 @@ function addMaprouletteTags(properties: GeoJsonProperties, item: any): GeoJsonPr * @param args List of arguments [input.csv] */ function main(args: string[]): void { - const csvOptions = { - columns: true, - skip_empty_lines: true, - trim: true, - }; - const file = args[0]; - const output = args[1]; + const csvOptions = { + columns: true, + skip_empty_lines: true, + trim: true, + } + const file = args[0] + const output = args[1] - // Create an empty list to store the converted features - var items: Feature[] = []; + // Create an empty list to store the converted features + var items: Feature[] = [] - // Read CSV file - const csv: Record<any, string>[] = parse(readFileSync(file), csvOptions); + // Read CSV file + const csv: Record<any, string>[] = parse(readFileSync(file), csvOptions) - // Loop through all the entries - for (var i = 0; i < csv.length; i++) { - const item = csv[i]; + // Loop through all the entries + for (var i = 0; i < csv.length; i++) { + const item = csv[i] - // Determine coordinates - const lat = Number(item["Latitude"]); - const lon = Number(item["Longitude"]); + // Determine coordinates + const lat = Number(item["Latitude"]) + const lon = Number(item["Longitude"]) - // Check if coordinates are valid - if (isNaN(lat) || isNaN(lon)) { - throw `Not a valid lat or lon for entry ${i}: ${JSON.stringify(item)}`; + // Check if coordinates are valid + if (isNaN(lat) || isNaN(lon)) { + throw `Not a valid lat or lon for entry ${i}: ${JSON.stringify(item)}` + } + + // Create a new collection to store the converted properties + var properties: GeoJsonProperties = {} + + // Add standard tags for category + const category = item["Categorie"] + const tagsCategory = categoryTags(category) + + // Add the rest of the needed tags + properties = { ...properties, ...renameTags(item) } + + // Merge them together + properties.tags = [...tagsCategory.tags, ...properties.tags] + properties.tags = properties.tags.join(";") + + // Convert types + properties = convertTypes(properties) + + // Add units if necessary + properties = addUnits(properties) + + // Add Maproulette tags + properties = addMaprouletteTags(properties, item) + + // Create the new feature + const feature: Feature = { + type: "Feature", + id: item["ID"], + geometry: { + type: "Point", + coordinates: [lon, lat], + }, + properties, + } + + // Push it to the list we created earlier + items.push(feature) } - // Create a new collection to store the converted properties - var properties: GeoJsonProperties = {}; + // Make a FeatureCollection out of it + const featureCollection: FeatureCollection = { + type: "FeatureCollection", + features: items, + } - // Add standard tags for category - const category = item["Categorie"]; - const tagsCategory = categoryTags(category); - - // Add the rest of the needed tags - properties = { ...properties, ...renameTags(item) }; - - // Merge them together - properties.tags = [...tagsCategory.tags, ...properties.tags]; - properties.tags = properties.tags.join(";"); - - // Convert types - properties = convertTypes(properties); - - // Add units if necessary - properties = addUnits(properties); - - // Add Maproulette tags - properties = addMaprouletteTags(properties, item); - - // Create the new feature - const feature: Feature = { - type: "Feature", - id: item["ID"], - geometry: { - type: "Point", - coordinates: [lon, lat], - }, - properties, - }; - - // Push it to the list we created earlier - items.push(feature); - } - - // Make a FeatureCollection out of it - const featureCollection: FeatureCollection = { - type: "FeatureCollection", - features: items, - }; - - // Write the data to a file or output to the console - if (output) { - writeFileSync( - `${output}.geojson`, - JSON.stringify(featureCollection, null, 2) - ); - } else { - console.log(JSON.stringify(featureCollection)); - } + // Write the data to a file or output to the console + if (output) { + writeFileSync(`${output}.geojson`, JSON.stringify(featureCollection, null, 2)) + } else { + console.log(JSON.stringify(featureCollection)) + } } // Execute the main function, with the stripped arguments -main(process.argv.slice(2)); +main(process.argv.slice(2)) diff --git a/scripts/perProperty.ts b/scripts/perProperty.ts index 30e796fba..f076493b1 100644 --- a/scripts/perProperty.ts +++ b/scripts/perProperty.ts @@ -1,8 +1,10 @@ -import * as fs from "fs"; +import * as fs from "fs" function main(args) { if (args.length < 2) { - console.log("Given a single geojson file and an attribute-key, will generate a new file for every value of the partition.") + console.log( + "Given a single geojson file and an attribute-key, will generate a new file for every value of the partition." + ) console.log("USAGE: perProperty `file.geojson` `property-key`") return } @@ -24,15 +26,14 @@ function main(args) { const stripped = path.substr(0, path.length - ".geojson".length) perProperty.forEach((features, v) => { - - fs.writeFileSync(stripped + "." + v.replace(/[^a-zA-Z0-9_]/g, "_") + ".geojson", + fs.writeFileSync( + stripped + "." + v.replace(/[^a-zA-Z0-9_]/g, "_") + ".geojson", JSON.stringify({ type: "FeatureCollection", - features - })) + features, + }) + ) }) - - } main(process.argv.slice(2)) diff --git a/scripts/postal_code_tools/createRoutablePoint.ts b/scripts/postal_code_tools/createRoutablePoint.ts index 4c1fff40a..0169f168f 100644 --- a/scripts/postal_code_tools/createRoutablePoint.ts +++ b/scripts/postal_code_tools/createRoutablePoint.ts @@ -1,11 +1,10 @@ -import {appendFileSync, existsSync, readFileSync, writeFileSync} from "fs"; -import {GeoOperations} from "../../Logic/GeoOperations"; -import ScriptUtils from "../ScriptUtils"; -import {Utils} from "../../Utils"; - +import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs" +import { GeoOperations } from "../../Logic/GeoOperations" +import ScriptUtils from "../ScriptUtils" +import { Utils } from "../../Utils" async function main(args: string[]) { -ScriptUtils.fixUtils() + ScriptUtils.fixUtils() const pointCandidates = JSON.parse(readFileSync(args[0], "utf8")) const postcodes = JSON.parse(readFileSync(args[1], "utf8")) const output = args[2] ?? "centralCoordinates.csv" @@ -16,7 +15,7 @@ ScriptUtils.fixUtils() if (existsSync(output)) { const lines = readFileSync(output, "UTF8").split("\n") lines.shift() - lines.forEach(line => { + lines.forEach((line) => { const postalCode = Number(line.split(",")[0]) alreadyLoaded.add(postalCode) }) @@ -35,7 +34,6 @@ ScriptUtils.fixUtils() } else { perPostCode.set(postcode, [boundary]) } - } for (const postcode of Array.from(perPostCode.keys())) { @@ -48,45 +46,68 @@ ScriptUtils.fixUtils() continue } candidates.push(candidate.geometry.coordinates) - } } if (candidates.length === 0) { - console.log("Postcode ", postcode, "has", candidates.length, "candidates, using centerpoint instead") - candidates.push(...boundaries.map(boundary => GeoOperations.centerpointCoordinates(boundary))) + console.log( + "Postcode ", + postcode, + "has", + candidates.length, + "candidates, using centerpoint instead" + ) + candidates.push( + ...boundaries.map((boundary) => GeoOperations.centerpointCoordinates(boundary)) + ) } + const url = + "https://staging.anyways.eu/routing-api/v1/routes?access_token=postal_code_script&turn_by_turn=false&format=geojson&language=en" + const depPoints: [number, number][] = Utils.NoNull( + await Promise.all( + candidates.map(async (candidate) => { + try { + const result = await Utils.downloadJson( + url + + "&loc=" + + candidate.join("%2C") + + "&loc=3.22000%2C51.21577&profile=car.short" + ) + const depPoint = result.features.filter( + (f) => f.geometry.type === "LineString" + )[0].geometry.coordinates[0] + return <[number, number]>[depPoint[0], depPoint[1]] // Drop elevation + } catch (e) { + console.error("No result or could not calculate a route") + } + }) + ) + ) - const url = "https://staging.anyways.eu/routing-api/v1/routes?access_token=postal_code_script&turn_by_turn=false&format=geojson&language=en" - const depPoints: [number, number][] = Utils.NoNull(await Promise.all(candidates.map(async candidate => { - try { - - const result = await Utils.downloadJson(url + "&loc=" + candidate.join("%2C") + "&loc=3.22000%2C51.21577&profile=car.short") - const depPoint = result.features.filter(f => f.geometry.type === "LineString")[0].geometry.coordinates[0] - return <[number, number]>[depPoint[0], depPoint[1]] // Drop elevation - } catch (e) { - console.error("No result or could not calculate a route") - } - }))) - - const centers = boundaries.map(b => GeoOperations.centerpointCoordinates(b)) + const centers = boundaries.map((b) => GeoOperations.centerpointCoordinates(b)) const center = GeoOperations.centerpointCoordinates({ type: "Feature", geometry: { type: "LineString", - coordinates: centers - } + coordinates: centers, + }, }) - depPoints.sort((c0, c1) => GeoOperations.distanceBetween(c0, center) - GeoOperations.distanceBetween(c1, center)) - console.log("Sorted departure point candidates for ", postcode, " are ", JSON.stringify(depPoints)) + depPoints.sort( + (c0, c1) => + GeoOperations.distanceBetween(c0, center) - + GeoOperations.distanceBetween(c1, center) + ) + console.log( + "Sorted departure point candidates for ", + postcode, + " are ", + JSON.stringify(depPoints) + ) appendFileSync(output, [postcode, ...depPoints[0]].join(", ") + "\n", "UTF-8") } - - } - let args = [...process.argv] args.splice(0, 2) -main(args).then(_ => console.log("Done!")) \ No newline at end of file +main(args).then((_) => console.log("Done!")) diff --git a/scripts/postal_code_tools/openaddressestogeojson.ts b/scripts/postal_code_tools/openaddressestogeojson.ts index e3264a386..d28819b78 100644 --- a/scripts/postal_code_tools/openaddressestogeojson.ts +++ b/scripts/postal_code_tools/openaddressestogeojson.ts @@ -1,49 +1,70 @@ -import * as fs from "fs"; -import {existsSync, writeFileSync} from "fs"; -import * as readline from "readline"; -import ScriptUtils from "../ScriptUtils"; +import * as fs from "fs" +import { existsSync, writeFileSync } from "fs" +import * as readline from "readline" +import ScriptUtils from "../ScriptUtils" /** * Converts an open-address CSV file into a big geojson file */ async function main(args: string[]) { - const inputFile = args[0] const outputFile = args[1] - const fileStream = fs.createReadStream(inputFile); + const fileStream = fs.createReadStream(inputFile) const perPostalCode = args[2] == "--per-postal-code" const rl = readline.createInterface({ input: fileStream, - crlfDelay: Infinity - }); + crlfDelay: Infinity, + }) // Note: we use the crlfDelay option to recognize all instances of CR LF // ('\r\n') in input.txt as a single line break. const fields = [ - "EPSG:31370_x", "EPSG:31370_y", "EPSG:4326_lat", "EPSG:4326_lon", - "address_id", "box_number", - "house_number", "municipality_id", "municipality_name_de", "municipality_name_fr", "municipality_name_nl", "postcode", "postname_fr", - "postname_nl", "street_id", "streetname_de", "streetname_fr", "streetname_nl", "region_code", "status" + "EPSG:31370_x", + "EPSG:31370_y", + "EPSG:4326_lat", + "EPSG:4326_lon", + "address_id", + "box_number", + "house_number", + "municipality_id", + "municipality_name_de", + "municipality_name_fr", + "municipality_name_nl", + "postcode", + "postname_fr", + "postname_nl", + "street_id", + "streetname_de", + "streetname_fr", + "streetname_nl", + "region_code", + "status", ] - let i = 0; + let i = 0 let failed = 0 - let createdFiles: string [] = [] + let createdFiles: string[] = [] if (!perPostalCode) { fs.writeFileSync(outputFile, "") } // @ts-ignore for await (const line of rl) { - i++; + i++ if (i % 10000 == 0) { - ScriptUtils.erasableLog("Converted ", i, "features (of which ", failed, "features don't have a coordinate)") + ScriptUtils.erasableLog( + "Converted ", + i, + "features (of which ", + failed, + "features don't have a coordinate)" + ) } const data = line.split(",") const parsed: any = {} for (let i = 0; i < fields.length; i++) { - const field = fields[i]; + const field = fields[i] parsed[field] = data[i] } const lat = Number(parsed["EPSG:4326_lat"]) @@ -67,7 +88,7 @@ async function main(args: string[]) { continue } if (isNaN(Number(parsed["postcode"]))) { - continue; + continue } targetFile = outputFile + "-" + parsed["postcode"] + ".geojson" let isFirst = false @@ -81,29 +102,30 @@ async function main(args: string[]) { fs.appendFileSync(targetFile, ",\n") } - fs.appendFileSync(targetFile, JSON.stringify({ - type: "Feature", - properties: parsed, - geometry: { - type: "Point", - coordinates: [lon, lat] - } - })) - + fs.appendFileSync( + targetFile, + JSON.stringify({ + type: "Feature", + properties: parsed, + geometry: { + type: "Point", + coordinates: [lon, lat], + }, + }) + ) } else { - - - fs.appendFileSync(outputFile, JSON.stringify({ - type: "Feature", - properties: parsed, - geometry: { - type: "Point", - coordinates: [lon, lat] - } - }) + "\n") + fs.appendFileSync( + outputFile, + JSON.stringify({ + type: "Feature", + properties: parsed, + geometry: { + type: "Point", + coordinates: [lon, lat], + }, + }) + "\n" + ) } - - } console.log("Closing files...") @@ -113,8 +135,13 @@ async function main(args: string[]) { fs.appendFileSync(createdFile, "]}") } - console.log("Done! Converted ", i, "features (of which ", failed, "features don't have a coordinate)") - + console.log( + "Done! Converted ", + i, + "features (of which ", + failed, + "features don't have a coordinate)" + ) } let args = [...process.argv] @@ -123,5 +150,5 @@ args.splice(0, 2) if (args.length == 0) { console.log("USAGE: input-csv-file output.newline-delimited-geojson.json [--per-postal-code]") } else { - main(args).catch(e => console.error(e)) + main(args).catch((e) => console.error(e)) } diff --git a/scripts/postal_code_tools/prepPostalCodesHulls.ts b/scripts/postal_code_tools/prepPostalCodesHulls.ts index 749123ad5..1fe1f0182 100644 --- a/scripts/postal_code_tools/prepPostalCodesHulls.ts +++ b/scripts/postal_code_tools/prepPostalCodesHulls.ts @@ -1,22 +1,22 @@ -import * as fs from "fs"; -import {writeFileSync} from "fs"; -import ScriptUtils from "../ScriptUtils"; +import * as fs from "fs" +import { writeFileSync } from "fs" +import ScriptUtils from "../ScriptUtils" function handleFile(file: string, postalCode: number) { - const geojson = JSON.parse(fs.readFileSync(file, "UTF8")) geojson.properties = { type: "boundary", - "boundary": "postal_code", - "postal_code": postalCode + "" + boundary: "postal_code", + postal_code: postalCode + "", } return geojson } - function getKnownPostalCodes(): number[] { - return fs.readFileSync("./scripts/postal_code_tools/knownPostalCodes.csv", "UTF8").split("\n") - .map(line => Number(line.split(",")[1])) + return fs + .readFileSync("./scripts/postal_code_tools/knownPostalCodes.csv", "UTF8") + .split("\n") + .map((line) => Number(line.split(",")[1])) } function main(args: string[]) { @@ -28,27 +28,36 @@ function main(args: string[]) { for (const file of files) { const nameParts = file.split("-") const postalCodeStr = nameParts[nameParts.length - 1] - const postalCode = Number(postalCodeStr.substr(0, postalCodeStr.length - ".geojson.convex.geojson".length)) + const postalCode = Number( + postalCodeStr.substr(0, postalCodeStr.length - ".geojson.convex.geojson".length) + ) if (isNaN(postalCode)) { console.error("Not a number: ", postalCodeStr) continue } if (knownPostals.has(postalCode)) { skipped.push(postalCode) - ScriptUtils.erasableLog("Skipping boundary for ", postalCode, "as it is already known - skipped ", skipped.length, "already") + ScriptUtils.erasableLog( + "Skipping boundary for ", + postalCode, + "as it is already known - skipped ", + skipped.length, + "already" + ) continue } allFiles.push(handleFile(file, postalCode)) } - - writeFileSync("all_postal_codes_filtered.geojson", JSON.stringify({ - type: "FeatureCollection", - features: allFiles - })) - + writeFileSync( + "all_postal_codes_filtered.geojson", + JSON.stringify({ + type: "FeatureCollection", + features: allFiles, + }) + ) } let args = [...process.argv] args.splice(0, 2) -main(args) \ No newline at end of file +main(args) diff --git a/scripts/printVersion.ts b/scripts/printVersion.ts index 204b43283..10bdf4765 100644 --- a/scripts/printVersion.ts +++ b/scripts/printVersion.ts @@ -1,3 +1,3 @@ -import Constants from "../Models/Constants"; +import Constants from "../Models/Constants" -console.log("git tag -a", Constants.vNumber, `-m "Deployed on ${new Date()}"`) \ No newline at end of file +console.log("git tag -a", Constants.vNumber, `-m "Deployed on ${new Date()}"`) diff --git a/scripts/schools/amendSchoolData.ts b/scripts/schools/amendSchoolData.ts index 606c6fa88..48bbf201a 100644 --- a/scripts/schools/amendSchoolData.ts +++ b/scripts/schools/amendSchoolData.ts @@ -1,24 +1,23 @@ -import {parse} from 'csv-parse/sync'; -import {readFileSync, writeFileSync} from "fs"; -import {Utils} from "../../Utils"; -import {GeoJSONObject, geometry} from "@turf/turf"; +import { parse } from "csv-parse/sync" +import { readFileSync, writeFileSync } from "fs" +import { Utils } from "../../Utils" +import { GeoJSONObject, geometry } from "@turf/turf" function parseAndClean(filename: string): Record<any, string>[] { const csvOptions = { columns: true, skip_empty_lines: true, - trim: true + trim: true, } const records: Record<any, string>[] = parse(readFileSync(filename), csvOptions) - return records.map(r => { - + return records.map((r) => { for (const key of Object.keys(r)) { if (r[key].endsWith("niet van toepassing")) { delete r[key] } } - return r; + return r }) } @@ -26,52 +25,82 @@ const structuren = { "Voltijds Gewoon Secundair Onderwijs": "secondary", "Gewoon Lager Onderwijs": "primary", "Gewoon Kleuteronderwijs": "kindergarten", - "Kleuteronderwijs": "kindergarten", + Kleuteronderwijs: "kindergarten", "Buitengewoon Lager Onderwijs": "primary", "Buitengewoon Secundair Onderwijs": "secondary", "Buitengewoon Kleuteronderwijs": "kindergarten", - "Deeltijds Beroepssecundair Onderwijs": "secondary" - + "Deeltijds Beroepssecundair Onderwijs": "secondary", } const degreesMapping = { - "Derde graad":"upper_secondary", - "Tweede graad":"middle_secondary", - "Eerste graad" :"lower_secondary" + "Derde graad": "upper_secondary", + "Tweede graad": "middle_secondary", + "Eerste graad": "lower_secondary", } -const classificationOrder = ["kindergarten","primary","secondary","lower_secondary","middle_secondary","upper_secondary"] - +const classificationOrder = [ + "kindergarten", + "primary", + "secondary", + "lower_secondary", + "middle_secondary", + "upper_secondary", +] const stelselsMapping = { - "Beide stelsels":"linear_courses;modular_courses", - "Lineair stelsel":"linear_courses", - "Modulair stelsel" :"modular_courses" + "Beide stelsels": "linear_courses;modular_courses", + "Lineair stelsel": "linear_courses", + "Modulair stelsel": "modular_courses", } - - -const rmKeys = ["schoolnummer", "instellingstype", - "adres", "begindatum","hoofdzetel","huisnummer","kbo-nummer", - "beheerder(s)", "bestuur", "clb", "ingerichte hoofdstructuren", "busnummer", "crab-code", "crab-huisnr", - "einddatum", "fax", "gemeente", "intern_vplnummer", "kbo_nummer", "lx", "ly", "niscode", - "onderwijsniveau","onderwijsvorm","scholengemeenschap", - "postcode", "provincie", - "provinciecode", "soort instelling", "status erkenning", "straat", "VWO-vestigingsplaatscode", "taalstelsel", -"net"] +const rmKeys = [ + "schoolnummer", + "instellingstype", + "adres", + "begindatum", + "hoofdzetel", + "huisnummer", + "kbo-nummer", + "beheerder(s)", + "bestuur", + "clb", + "ingerichte hoofdstructuren", + "busnummer", + "crab-code", + "crab-huisnr", + "einddatum", + "fax", + "gemeente", + "intern_vplnummer", + "kbo_nummer", + "lx", + "ly", + "niscode", + "onderwijsniveau", + "onderwijsvorm", + "scholengemeenschap", + "postcode", + "provincie", + "provinciecode", + "soort instelling", + "status erkenning", + "straat", + "VWO-vestigingsplaatscode", + "taalstelsel", + "net", +] const rename = { - "e-mail":"email", - "naam":"name", - "telefoon":"phone" - + "e-mail": "email", + naam: "name", + telefoon: "phone", } -function fuzzIdenticals(features: {geometry: {coordinates: [number,number]}}[]){ +function fuzzIdenticals(features: { geometry: { coordinates: [number, number] } }[]) { var seen = new Set<string>() for (const feature of features) { - var coors = feature.geometry.coordinates; + var coors = feature.geometry.coordinates let k = coors[0] + "," + coors[1] - while(seen.has(k)){ + while (seen.has(k)) { coors[0] += 0.00025 k = coors[0] + "," + coors[1] } @@ -81,60 +110,158 @@ function fuzzIdenticals(features: {geometry: {coordinates: [number,number]}}[]){ /** * Sorts classifications in order - * + * * sortClassifications(["primary","secondary","kindergarten"] // => ["kindergarten", "primary", "secondary"] */ -function sortClassifications(classification: string[]){ - return classification.sort((a, b) => classificationOrder.indexOf(a) - classificationOrder.indexOf(b)) +function sortClassifications(classification: string[]) { + return classification.sort( + (a, b) => classificationOrder.indexOf(a) - classificationOrder.indexOf(b) + ) } - function main() { console.log("Parsing schools...") const aantallen = "/home/pietervdvn/Downloads/Scholen/aantallen.csv" const perSchool = "/home/pietervdvn/Downloads/Scholen/perschool.csv" - const schoolfields = ["schoolnummer", "intern_vplnummer", "net", "naam", "hoofdzetel", "adres", "straat", "huisnummer", "busnummer", "postcode", "gemeente", "niscode", "provinciecode", "provincie", "VWO-vestigingsplaatscode", "crab-code", "crab-huisnr", "lx", "ly", "kbo-nummer", "telefoon", "fax", "e-mail", "website", "beheerder(s)", "soort instelling", "onderwijsniveau", "instellingstype", "begindatum", "einddatum", "status erkenning", "clb", "bestuur", "scholengemeenschap", "taalstelsel", "ingerichte hoofdstructuren"] as const + const schoolfields = [ + "schoolnummer", + "intern_vplnummer", + "net", + "naam", + "hoofdzetel", + "adres", + "straat", + "huisnummer", + "busnummer", + "postcode", + "gemeente", + "niscode", + "provinciecode", + "provincie", + "VWO-vestigingsplaatscode", + "crab-code", + "crab-huisnr", + "lx", + "ly", + "kbo-nummer", + "telefoon", + "fax", + "e-mail", + "website", + "beheerder(s)", + "soort instelling", + "onderwijsniveau", + "instellingstype", + "begindatum", + "einddatum", + "status erkenning", + "clb", + "bestuur", + "scholengemeenschap", + "taalstelsel", + "ingerichte hoofdstructuren", + ] as const const schoolGeojson: { features: { - properties: Record<(typeof schoolfields)[number], string>, - geometry:{ - type: "Point", - coordinates: [number,number] + properties: Record<typeof schoolfields[number], string> + geometry: { + type: "Point" + coordinates: [number, number] } }[] } = JSON.parse(readFileSync("scripts/schools/scholen.geojson", "utf8")) - + fuzzIdenticals(schoolGeojson.features) - const aantallenFields = ["schooljaar", "nr koepel", "koepel", "instellingscode", "intern volgnr vpl", "volgnr vpl", "naam instelling", "GON-school", "GOK-school", "instellingsnummer scholengemeenschap", "scholengemeenschap", "code schoolbestuur", "schoolbestuur", "type vestigingsplaats", "fusiegemeente hoofdvestigingsplaats", "straatnaam vestigingsplaats", "huisnr vestigingsplaats", "bus vestigingsplaats", "postcode vestigingsplaats", "deelgemeente vestigingsplaats", "fusiegemeente vestigingsplaats", "hoofdstructuur (code)", "hoofdstructuur", "administratieve groep (code)", "administratieve groep", "graad lager onderwijs", "pedagogische methode", "graad secundair onderwijs", "leerjaar", "A of B-stroom", "basisopties", "beroepenveld", "onderwijsvorm", "studiegebied", "studierichting", "stelsel", "okan cluster", "type buitengewoon onderwijs", "opleidingsvorm (code)", "opleidingsvorm", "fase", "opleidingen", "geslacht", "aantal inschrijvingen"] as const - const aantallenParsed: Record<(typeof aantallenFields)[number], string>[] = parseAndClean(aantallen) - const perschoolFields = ["schooljaar", "nr koepel", "koepel", "instellingscode", "naam instelling", "straatnaam", "huisnr", "bus", "postcode", "deelgemeente", "fusiegemeente", "aantal inschrijvingen"] as const - const perschoolParsed: Record<(typeof perschoolFields)[number], string>[] = parseAndClean(perSchool) + const aantallenFields = [ + "schooljaar", + "nr koepel", + "koepel", + "instellingscode", + "intern volgnr vpl", + "volgnr vpl", + "naam instelling", + "GON-school", + "GOK-school", + "instellingsnummer scholengemeenschap", + "scholengemeenschap", + "code schoolbestuur", + "schoolbestuur", + "type vestigingsplaats", + "fusiegemeente hoofdvestigingsplaats", + "straatnaam vestigingsplaats", + "huisnr vestigingsplaats", + "bus vestigingsplaats", + "postcode vestigingsplaats", + "deelgemeente vestigingsplaats", + "fusiegemeente vestigingsplaats", + "hoofdstructuur (code)", + "hoofdstructuur", + "administratieve groep (code)", + "administratieve groep", + "graad lager onderwijs", + "pedagogische methode", + "graad secundair onderwijs", + "leerjaar", + "A of B-stroom", + "basisopties", + "beroepenveld", + "onderwijsvorm", + "studiegebied", + "studierichting", + "stelsel", + "okan cluster", + "type buitengewoon onderwijs", + "opleidingsvorm (code)", + "opleidingsvorm", + "fase", + "opleidingen", + "geslacht", + "aantal inschrijvingen", + ] as const + const aantallenParsed: Record<typeof aantallenFields[number], string>[] = + parseAndClean(aantallen) + const perschoolFields = [ + "schooljaar", + "nr koepel", + "koepel", + "instellingscode", + "naam instelling", + "straatnaam", + "huisnr", + "bus", + "postcode", + "deelgemeente", + "fusiegemeente", + "aantal inschrijvingen", + ] as const + const perschoolParsed: Record<typeof perschoolFields[number], string>[] = + parseAndClean(perSchool) schoolGeojson.features = schoolGeojson.features - .filter(sch => sch.properties.lx != "0" && sch.properties.ly != "0") - .filter(sch => sch.properties.instellingstype !== "Universiteit") + .filter((sch) => sch.properties.lx != "0" && sch.properties.ly != "0") + .filter((sch) => sch.properties.instellingstype !== "Universiteit") const c = schoolGeojson.features.length console.log("Got ", schoolGeojson.features.length, "items after filtering") let i = 0 - let lastWrite = 0; + let lastWrite = 0 for (const feature of schoolGeojson.features) { i++ - const now = Date.now(); - if(now - lastWrite > 1000){ - lastWrite = now; - console.log("Processing "+i+"/"+c) + const now = Date.now() + if (now - lastWrite > 1000) { + lastWrite = now + console.log("Processing " + i + "/" + c) } const props = feature.properties - - const aantallen = aantallenParsed.filter(i => i.instellingscode == props.schoolnummer) + + const aantallen = aantallenParsed.filter((i) => i.instellingscode == props.schoolnummer) if (aantallen.length > 0) { - - const fetch = (key: (typeof aantallenFields)[number]) => Utils.NoNull(Utils.Dedup(aantallen.map(x => x[key]))) + const fetch = (key: typeof aantallenFields[number]) => + Utils.NoNull(Utils.Dedup(aantallen.map((x) => x[key]))) props["onderwijsvorm"] = fetch("onderwijsvorm").join(";") @@ -148,14 +275,12 @@ function main() { */ const hoofdstructuur = fetch("hoofdstructuur") - - let specialEducation = false - let classification = hoofdstructuur.map(s => { + let classification = hoofdstructuur.map((s) => { const v = structuren[s] if (s.startsWith("Buitengewoon")) { - specialEducation = true; + specialEducation = true } if (v === undefined) { console.error("Type not found: " + s) @@ -164,45 +289,46 @@ function main() { return v }) const graden = fetch("graad secundair onderwijs") - if(classification[0] === "secondary"){ - if(graden.length !== 3){ - classification = graden.map(degree => degreesMapping[degree]) + if (classification[0] === "secondary") { + if (graden.length !== 3) { + classification = graden.map((degree) => degreesMapping[degree]) } - } sortClassifications(classification) props["school"] = Utils.Dedup(classification).join("; ") - // props["koepel"] = koepel.join(";") // props["scholengemeenschap"] = scholengemeenschap.join(";") // props["stelsel"] = stelselsMapping[stelsel] - + if (specialEducation) { props["school:for"] = "special_education" } if (props.taalstelsel === "Nederlandstalig") { props["language:nl"] = "yes" } - - if(props.instellingstype === "Instelling voor deeltijds kunstonderwijs") { - props["amenity"] = "college" + + if (props.instellingstype === "Instelling voor deeltijds kunstonderwijs") { + props["amenity"] = "college" props["school:subject"] = "art" } } - const schoolinfo = perschoolParsed.filter(i => i.instellingscode == props.schoolnummer) + const schoolinfo = perschoolParsed.filter((i) => i.instellingscode == props.schoolnummer) if (schoolinfo.length == 0) { // pass } else if (schoolinfo.length == 1) { - props["capacity"] = schoolinfo[0]["aantal inschrijvingen"].split(";").map(i => Number(i)).reduce((sum, i) => sum + i, 0) + props["capacity"] = schoolinfo[0]["aantal inschrijvingen"] + .split(";") + .map((i) => Number(i)) + .reduce((sum, i) => sum + i, 0) } else { throw "Multiple schoolinfo's found for " + props.schoolnummer } - + //props["source:ref"] = props.schoolnummer - props["amenity"]="school" - if ( props["school"] === "kindergarten" ) { + props["amenity"] = "school" + if (props["school"] === "kindergarten") { props["amenity"] = "kindergarten" props["isced:2011:level"] = "early_education" delete props["school"] @@ -210,28 +336,29 @@ function main() { for (const renameKey in rename) { const into = rename[renameKey] - if(props[renameKey] !== undefined){ + if (props[renameKey] !== undefined) { props[into] = props[renameKey] delete props[renameKey] } } - + for (const rmKey of rmKeys) { delete props[rmKey] } - } - + //schoolGeojson.features = schoolGeojson.features.filter(f => f.properties["capacity"] !== undefined) /*schoolGeojson.features.forEach((f, i) => { f.properties["id"] = "school/"+i })*/ - schoolGeojson.features = schoolGeojson.features.filter(f => f.properties["amenity"] === "kindergarten") + schoolGeojson.features = schoolGeojson.features.filter( + (f) => f.properties["amenity"] === "kindergarten" + ) writeFileSync("scripts/schools/amended_schools.geojson", JSON.stringify(schoolGeojson), "utf8") console.log("Done") } -if(!process.argv[1].endsWith("mocha")){ +if (!process.argv[1].endsWith("mocha")) { main() } diff --git a/scripts/slice.ts b/scripts/slice.ts index 365e239f9..041619c8d 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -1,9 +1,9 @@ -import * as fs from "fs"; -import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource"; -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; -import * as readline from "readline"; -import ScriptUtils from "./ScriptUtils"; -import {Utils} from "../Utils"; +import * as fs from "fs" +import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource" +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" +import * as readline from "readline" +import ScriptUtils from "./ScriptUtils" +import { Utils } from "../Utils" /** * This script slices a big newline-delimeted geojson file into tiled geojson @@ -11,12 +11,12 @@ import {Utils} from "../Utils"; */ async function readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise<any[]> { - const fileStream = fs.createReadStream(inputFile); + const fileStream = fs.createReadStream(inputFile) const rl = readline.createInterface({ input: fileStream, - crlfDelay: Infinity - }); + crlfDelay: Infinity, + }) // Note: we use the crlfDelay option to recognize all instances of CR LF // ('\r\n') in input.txt as a single line break. @@ -37,12 +37,12 @@ async function readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise } async function readGeojsonLineByLine(inputFile: string): Promise<any[]> { - const fileStream = fs.createReadStream(inputFile); + const fileStream = fs.createReadStream(inputFile) const rl = readline.createInterface({ input: fileStream, - crlfDelay: Infinity - }); + crlfDelay: Infinity, + }) // Note: we use the crlfDelay option to recognize all instances of CR LF // ('\r\n') in input.txt as a single line break. @@ -50,9 +50,9 @@ async function readGeojsonLineByLine(inputFile: string): Promise<any[]> { let featuresSeen = false // @ts-ignore for await (let line: string of rl) { - if (!featuresSeen && line.startsWith("\"features\":")) { - featuresSeen = true; - continue; + if (!featuresSeen && line.startsWith('"features":')) { + featuresSeen = true + continue } if (!featuresSeen) { continue @@ -84,7 +84,6 @@ async function readFeaturesFromGeoJson(inputFile: string): Promise<any[]> { } async function main(args: string[]) { - console.log("GeoJSON slicer") if (args.length < 3) { console.log("USAGE: <input-file.geojson> <target-zoom-level> <output-directory>") @@ -101,8 +100,7 @@ async function main(args: string[]) { } console.log("Using directory ", outputDirectory) - - let allFeatures: any []; + let allFeatures: any[] if (inputFile.endsWith(".geojson")) { console.log("Detected geojson") allFeatures = await readFeaturesFromGeoJson(inputFile) @@ -112,7 +110,6 @@ async function main(args: string[]) { } allFeatures = Utils.NoNull(allFeatures) - console.log("Loaded all", allFeatures.length, "points") const keysToRemove = ["STRAATNMID", "GEMEENTE", "POSTCODE"] @@ -126,31 +123,40 @@ async function main(args: string[]) { } delete f.bbox } - TiledFeatureSource.createHierarchy( - StaticFeatureSource.fromGeojson(allFeatures), - { - minZoomLevel: zoomlevel, - maxZoomLevel: zoomlevel, - maxFeatureCount: Number.MAX_VALUE, - registerTile: tile => { - const path = `${outputDirectory}/tile_${tile.z}_${tile.x}_${tile.y}.geojson` - const features = tile.features.data.map(ff => ff.feature) - features.forEach(f => { - delete f.bbox - }) - fs.writeFileSync(path, JSON.stringify({ - "type": "FeatureCollection", - "features": features - }, null, " ")) - ScriptUtils.erasableLog("Written ", path, "which has ", tile.features.data.length, "features") - } - } - ) - + TiledFeatureSource.createHierarchy(StaticFeatureSource.fromGeojson(allFeatures), { + minZoomLevel: zoomlevel, + maxZoomLevel: zoomlevel, + maxFeatureCount: Number.MAX_VALUE, + registerTile: (tile) => { + const path = `${outputDirectory}/tile_${tile.z}_${tile.x}_${tile.y}.geojson` + const features = tile.features.data.map((ff) => ff.feature) + features.forEach((f) => { + delete f.bbox + }) + fs.writeFileSync( + path, + JSON.stringify( + { + type: "FeatureCollection", + features: features, + }, + null, + " " + ) + ) + ScriptUtils.erasableLog( + "Written ", + path, + "which has ", + tile.features.data.length, + "features" + ) + }, + }) } let args = [...process.argv] args.splice(0, 2) -main(args).then(_ => { +main(args).then((_) => { console.log("All done!") -}); +}) diff --git a/scripts/thieves/readIdPresets.ts b/scripts/thieves/readIdPresets.ts index 37e414e13..3d8537c6a 100644 --- a/scripts/thieves/readIdPresets.ts +++ b/scripts/thieves/readIdPresets.ts @@ -1,34 +1,34 @@ /*** * Parses presets from the iD repository and extracts some usefull tags from them */ -import ScriptUtils from "../ScriptUtils"; -import {existsSync, readFileSync, writeFileSync} from "fs"; +import ScriptUtils from "../ScriptUtils" +import { existsSync, readFileSync, writeFileSync } from "fs" import * as known_languages from "../../assets/language_native.json" -import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"; -import {MappingConfigJson} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; -import SmallLicense from "../../Models/smallLicense"; +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" +import { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import SmallLicense from "../../Models/smallLicense" interface IconThief { steal(iconName: string): boolean } interface IdPresetJson { - icon: string, + icon: string geometry: ("point" | "line" | "area")[] /** * Extra search terms */ - terms: string [] + terms: string[] tags: Record<string, string> - name: string, - searchable?: boolean, + name: string + searchable?: boolean } class IdPreset implements IdPresetJson { - private _preset: IdPresetJson; + private _preset: IdPresetJson constructor(preset: IdPresetJson) { - this._preset = preset; + this._preset = preset } public get searchable(): boolean { @@ -56,36 +56,38 @@ class IdPreset implements IdPresetJson { } static fromFile(file: string): IdPreset { - return new IdPreset(JSON.parse(readFileSync(file, 'utf8'))) + return new IdPreset(JSON.parse(readFileSync(file, "utf8"))) } public parseTags(): string | { and: string[] } { - const preset = this._preset; + const preset = this._preset const tagKeys = Object.keys(preset.tags) if (tagKeys.length === 1) { return tagKeys[0] + "=" + preset.tags[tagKeys[0]] } else { return { - and: tagKeys.map(key => key + "=" + preset.tags[key]) + and: tagKeys.map((key) => key + "=" + preset.tags[key]), } } } } - class MakiThief implements IconThief { - public readonly _prefix: string; - private readonly _directory: string; - private readonly _license: SmallLicense; - private readonly _targetDir: string; + public readonly _prefix: string + private readonly _directory: string + private readonly _license: SmallLicense + private readonly _targetDir: string - constructor(directory: string, targetDir: string, - license: SmallLicense, - prefix: string = "maki-") { - this._license = license; - this._directory = directory; - this._targetDir = targetDir; - this._prefix = prefix; + constructor( + directory: string, + targetDir: string, + license: SmallLicense, + prefix: string = "maki-" + ) { + this._license = license + this._directory = directory + this._targetDir = targetDir + this._prefix = prefix } public steal(iconName: string): boolean { @@ -94,13 +96,14 @@ class MakiThief implements IconThief { return true } try { - const file = readFileSync(this._directory + iconName + ".svg", "utf8") - writeFileSync(target, file, 'utf8') + writeFileSync(target, file, "utf8") - writeFileSync(target + ".license_info.json", - JSON.stringify( - {...this._license, path: this._prefix + iconName + ".svg"}), 'utf8') + writeFileSync( + target + ".license_info.json", + JSON.stringify({ ...this._license, path: this._prefix + iconName + ".svg" }), + "utf8" + ) console.log("Successfully stolen " + iconName) return true } catch (e) { @@ -108,17 +111,15 @@ class MakiThief implements IconThief { return false } } - } class AggregateIconThief implements IconThief { - private readonly makiThiefs: MakiThief[]; + private readonly makiThiefs: MakiThief[] constructor(makiThiefs: MakiThief[]) { - this.makiThiefs = makiThiefs; + this.makiThiefs = makiThiefs } - public steal(iconName: string): boolean { for (const makiThief1 of this.makiThiefs) { if (iconName.startsWith(makiThief1._prefix)) { @@ -129,23 +130,31 @@ class AggregateIconThief implements IconThief { } } - class IdThief { - private readonly _idPresetsRepository: string; + private readonly _idPresetsRepository: string private readonly _tranlationFiles: Record<string, object> = {} private readonly _knownLanguages: string[] - private readonly _iconThief: IconThief; + private readonly _iconThief: IconThief public constructor(idPresetsRepository: string, iconThief: IconThief) { - this._idPresetsRepository = idPresetsRepository; - this._iconThief = iconThief; - const knownById = ScriptUtils.readDirRecSync(`${this._idPresetsRepository}/dist/translations/`) - .map(pth => pth.substring(pth.lastIndexOf('/') + 1, pth.length - '.json'.length)) - .filter(lng => !lng.endsWith('.min')); - const missing = Object.keys(known_languages).filter(lng => knownById.indexOf(lng.replace('-', '_')) < 0) - this._knownLanguages = knownById.filter(lng => known_languages[lng] !== undefined) - console.log("Id knows following languages:", this._knownLanguages.join(", "), "missing:", missing) + this._idPresetsRepository = idPresetsRepository + this._iconThief = iconThief + const knownById = ScriptUtils.readDirRecSync( + `${this._idPresetsRepository}/dist/translations/` + ) + .map((pth) => pth.substring(pth.lastIndexOf("/") + 1, pth.length - ".json".length)) + .filter((lng) => !lng.endsWith(".min")) + const missing = Object.keys(known_languages).filter( + (lng) => knownById.indexOf(lng.replace("-", "_")) < 0 + ) + this._knownLanguages = knownById.filter((lng) => known_languages[lng] !== undefined) + console.log( + "Id knows following languages:", + this._knownLanguages.join(", "), + "missing:", + missing + ) } public getTranslation(language: string, ...path: string[]): string { @@ -153,28 +162,25 @@ class IdThief { for (const p of path) { obj = obj[p] if (obj === undefined) { - return undefined; + return undefined } } return obj } - /** * Creates a mapRendering-mapping for the 'shop' theme */ - public readShopIcons(): { if: string | { and: string[] }, then: string }[] { - + public readShopIcons(): { if: string | { and: string[] }; then: string }[] { const dir = this._idPresetsRepository + "/data/presets/shop" - const mappings: - { - if: string | { and: string[] }, - then: string - }[] = [] - const files = ScriptUtils.readDirRecSync(dir, 1); + const mappings: { + if: string | { and: string[] } + then: string + }[] = [] + const files = ScriptUtils.readDirRecSync(dir, 1) for (const file of files) { - const preset = IdPreset.fromFile(file); + const preset = IdPreset.fromFile(file) if (!this._iconThief.steal(preset.icon)) { continue @@ -182,27 +188,24 @@ class IdThief { const mapping = { if: preset.parseTags(), - then: "circle:white;./assets/layers/id_presets/" + preset.icon + ".svg" + then: "circle:white;./assets/layers/id_presets/" + preset.icon + ".svg", } mappings.push(mapping) - } return mappings } - /** * Creates a tagRenderingConfigJson for the 'shop' theme */ public readShopPresets(): MappingConfigJson[] { - const dir = this._idPresetsRepository + "/data/presets/shop" const mappings: MappingConfigJson[] = [] - const files = ScriptUtils.readDirRecSync(dir, 1); + const files = ScriptUtils.readDirRecSync(dir, 1) for (const file of files) { - const name = file.substring(file.lastIndexOf('/') + 1, file.length - '.json'.length) + const name = file.substring(file.lastIndexOf("/") + 1, file.length - ".json".length) const preset = IdPreset.fromFile(file) if (preset.searchable === false) { @@ -212,30 +215,35 @@ class IdThief { console.log(` ${name} (shop=${preset.tags["shop"]}), ${preset.icon}`) const thenClause: Record<string, string> = { - en: preset.name + en: preset.name, } const terms: Record<string, string[]> = { - en: preset.terms + en: preset.terms, } for (const lng of this._knownLanguages) { - const lngMc = lng.replace('-', '_') + const lngMc = lng.replace("-", "_") const tr = this.getTranslation(lng, "presets", "presets", "shop/" + name, "name") if (tr !== undefined) { thenClause[lngMc] = tr } - const termsTr = this.getTranslation(lng, "presets", "presets", "shop/" + name, "terms") + const termsTr = this.getTranslation( + lng, + "presets", + "presets", + "shop/" + name, + "terms" + ) if (termsTr !== undefined) { terms[lngMc] = termsTr.split(",") } - } - let tag = preset.parseTags(); - const mapping : MappingConfigJson= { + let tag = preset.parseTags() + const mapping: MappingConfigJson = { if: tag, then: thenClause, - searchTerms: terms + searchTerms: terms, } if (preset.tags["shop"] == "yes") { mapping["hideInAnswer"] = true @@ -245,14 +253,13 @@ class IdThief { if (this._iconThief.steal(preset.icon)) { mapping["icon"] = { path: "./assets/layers/id_presets/" + preset.icon + ".svg", - class: "medium" + class: "medium", } } else { console.log(preset.icon + " could not be stolen :(") } mappings.push(mapping) - } return mappings @@ -263,50 +270,63 @@ class IdThief { if (cached) { return cached } - return this._tranlationFiles[language] = JSON.parse(readFileSync(`${this._idPresetsRepository}/dist/translations/${language}.json`, 'utf8')) + return (this._tranlationFiles[language] = JSON.parse( + readFileSync(`${this._idPresetsRepository}/dist/translations/${language}.json`, "utf8") + )) } - } const targetDir = "./assets/layers/id_presets/" -const makiThief = new MakiThief('../maki/icons/', targetDir + "maki-", { - authors: ['Maki icon set'], - license: 'CC0', - path: null, - sources: ["https://github.com/mapbox/maki"] -}, 'maki-'); - - -const temakiThief = new MakiThief('../temaki/icons/', targetDir + "temaki-", { - authors: ['Temaki icon set'], - license: 'CC0', - path: null, - sources: ["https://github.com/ideditor/temaki"] -}, 'temaki-'); -const fasThief = new MakiThief('../Font-Awesome/svgs/solid/', targetDir + "fas-", { - authors: ['Font-Awesome icon set'], - license: 'CC-BY 4.0', - path: null, - sources: ["https://github.com/FortAwesome/Font-Awesome"] -}, 'fas-'); -const iconThief = new AggregateIconThief( - [makiThief, temakiThief, fasThief] +const makiThief = new MakiThief( + "../maki/icons/", + targetDir + "maki-", + { + authors: ["Maki icon set"], + license: "CC0", + path: null, + sources: ["https://github.com/mapbox/maki"], + }, + "maki-" ) +const temakiThief = new MakiThief( + "../temaki/icons/", + targetDir + "temaki-", + { + authors: ["Temaki icon set"], + license: "CC0", + path: null, + sources: ["https://github.com/ideditor/temaki"], + }, + "temaki-" +) +const fasThief = new MakiThief( + "../Font-Awesome/svgs/solid/", + targetDir + "fas-", + { + authors: ["Font-Awesome icon set"], + license: "CC-BY 4.0", + path: null, + sources: ["https://github.com/FortAwesome/Font-Awesome"], + }, + "fas-" +) +const iconThief = new AggregateIconThief([makiThief, temakiThief, fasThief]) + const thief = new IdThief("../id-tagging-schema/", iconThief) const id_presets_path = targetDir + "id_presets.json" -const idPresets = <LayerConfigJson>JSON.parse(readFileSync(id_presets_path, 'utf8')) +const idPresets = <LayerConfigJson>JSON.parse(readFileSync(id_presets_path, "utf8")) idPresets.tagRenderings = [ { id: "shop_types", - mappings: thief.readShopPresets() + mappings: thief.readShopPresets(), }, { id: "shop_rendering", - mappings: thief.readShopIcons() - } + mappings: thief.readShopIcons(), + }, ] -writeFileSync(id_presets_path, JSON.stringify(idPresets, null, " "), 'utf8') \ No newline at end of file +writeFileSync(id_presets_path, JSON.stringify(idPresets, null, " "), "utf8") diff --git a/scripts/thieves/stealLanguages.ts b/scripts/thieves/stealLanguages.ts index 94cf95362..68eed2a14 100644 --- a/scripts/thieves/stealLanguages.ts +++ b/scripts/thieves/stealLanguages.ts @@ -1,68 +1,75 @@ /* -* Uses the languages in and to every translation from wikidata to generate a language question in wikidata/wikidata -* */ + * Uses the languages in and to every translation from wikidata to generate a language question in wikidata/wikidata + * */ -import WikidataUtils from "../../Utils/WikidataUtils"; -import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"; -import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"; -import {MappingConfigJson} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; -import LanguageUtils from "../../Utils/LanguageUtils"; +import WikidataUtils from "../../Utils/WikidataUtils" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" +import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" +import { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import LanguageUtils from "../../Utils/LanguageUtils" import * as perCountry from "../../assets/language_in_country.json" -import {Utils} from "../../Utils"; -function main(){ - const sourcepath = "assets/generated/languages-wd.json"; +import { Utils } from "../../Utils" +function main() { + const sourcepath = "assets/generated/languages-wd.json" console.log(`Converting language data file '${sourcepath}' into a tagMapping`) - const languages = WikidataUtils.extractLanguageData(JSON.parse(readFileSync(sourcepath, "utf8")), {}) - const mappings : MappingConfigJson[] = [] - const schoolmappings : MappingConfigJson[] = [] - - const countryToLanguage : Record<string, string[]> = perCountry - const officialLanguagesPerCountry = Utils.TransposeMap(countryToLanguage); - + const languages = WikidataUtils.extractLanguageData( + JSON.parse(readFileSync(sourcepath, "utf8")), + {} + ) + const mappings: MappingConfigJson[] = [] + const schoolmappings: MappingConfigJson[] = [] + + const countryToLanguage: Record<string, string[]> = perCountry + const officialLanguagesPerCountry = Utils.TransposeMap(countryToLanguage) + languages.forEach((l, code) => { - const then : Record<string, string>= {} + const then: Record<string, string> = {} l.forEach((tr, lng) => { - const languageCodeWeblate = WikidataUtils.languageRemapping[lng] ?? lng; - if(!LanguageUtils.usedLanguages.has(languageCodeWeblate)){ - return; + const languageCodeWeblate = WikidataUtils.languageRemapping[lng] ?? lng + if (!LanguageUtils.usedLanguages.has(languageCodeWeblate)) { + return } then[languageCodeWeblate] = tr }) - - const officialCountries = Utils.Dedup(officialLanguagesPerCountry[code]?.map(s => s.toLowerCase()) ?? []) - const prioritySearch = officialCountries.length > 0 ? "_country~" + officialCountries.map(c => "((^|;)"+c+"($|;))").join("|") : undefined + + const officialCountries = Utils.Dedup( + officialLanguagesPerCountry[code]?.map((s) => s.toLowerCase()) ?? [] + ) + const prioritySearch = + officialCountries.length > 0 + ? "_country~" + officialCountries.map((c) => "((^|;)" + c + "($|;))").join("|") + : undefined mappings.push(<MappingConfigJson>{ if: "language:" + code + "=yes", ifnot: "language:" + code + "=", searchTerms: { - "*": [code] + "*": [code], }, then, - priorityIf: prioritySearch + priorityIf: prioritySearch, }) - schoolmappings.push(<MappingConfigJson>{ + schoolmappings.push(<MappingConfigJson>{ if: "school:language=" + code, then, priorityIf: prioritySearch, searchTerms: { - "*":[code] - } + "*": [code], + }, }) }) - - + const wikidataLayer = <LayerConfigJson>{ id: "wikidata", description: { - en: "Various tagrenderings which are generated from Wikidata. Automatically generated with a script, don't edit manually" + en: "Various tagrenderings which are generated from Wikidata. Automatically generated with a script, don't edit manually", }, "#dont-translate": "*", - "source": { - "osmTags": "id~*" + source: { + osmTags: "id~*", }, title: null, - "mapRendering": null, + mapRendering: null, tagRenderings: [ { id: "language", @@ -75,27 +82,27 @@ function main(){ override: { id: "language-multi", // @ts-ignore - description: "Enables to pick *multiple* 'language:<lng>=yes' within the mappings", - multiAnswer: true - } - + description: + "Enables to pick *multiple* 'language:<lng>=yes' within the mappings", + multiAnswer: true, + }, }, { - id:"school-language", + id: "school-language", // @ts-ignore description: "Enables to pick a single 'school:language=<lng>' within the mappings", multiAnswer: true, - mappings: schoolmappings - } - ] + mappings: schoolmappings, + }, + ], } const dir = "./assets/layers/wikidata/" - if(!existsSync(dir)){ + if (!existsSync(dir)) { mkdirSync(dir) } const path = dir + "wikidata.json" writeFileSync(path, JSON.stringify(wikidataLayer, null, " ")) - console.log("Written "+path) + console.log("Written " + path) } -main() \ No newline at end of file +main() diff --git a/scripts/translationStatistics.ts b/scripts/translationStatistics.ts index 12a81b938..bf178f4c2 100644 --- a/scripts/translationStatistics.ts +++ b/scripts/translationStatistics.ts @@ -1,22 +1,24 @@ -import {Utils} from "../Utils"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import TranslatorsPanel from "../UI/BigComponents/TranslatorsPanel"; +import { Utils } from "../Utils" +import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import TranslatorsPanel from "../UI/BigComponents/TranslatorsPanel" import * as languages from "../assets/generated/used_languages.json" { const usedLanguages = languages.languages - + // Some statistics - console.log(Utils.FixedLength("", 12) + " " + usedLanguages.map(l => Utils.FixedLength(l, 6)).join("")) + console.log( + Utils.FixedLength("", 12) + " " + usedLanguages.map((l) => Utils.FixedLength(l, 6)).join("") + ) const all = new Map<string, number[]>() - usedLanguages.forEach(ln => all.set(ln, [])) + usedLanguages.forEach((ln) => all.set(ln, [])) for (const layoutId of Array.from(AllKnownLayouts.allKnownLayouts.keys())) { const layout = AllKnownLayouts.allKnownLayouts.get(layoutId) - if(layout.hideFromOverview){ + if (layout.hideFromOverview) { continue } - const {completeness, total} = TranslatorsPanel.MissingTranslationsFor(layout) + const { completeness, total } = TranslatorsPanel.MissingTranslationsFor(layout) process.stdout.write(Utils.FixedLength(layout.id, 12) + " ") for (const language of usedLanguages) { const compl = completeness.get(language) @@ -25,7 +27,7 @@ import * as languages from "../assets/generated/used_languages.json" process.stdout.write(" ") continue } - const percentage = Math.round(100 * compl / total) + const percentage = Math.round((100 * compl) / total) process.stdout.write(Utils.FixedLength(percentage + "%", 6)) } process.stdout.write("\n") @@ -35,10 +37,12 @@ import * as languages from "../assets/generated/used_languages.json" for (const language of usedLanguages) { const ratios = all.get(language) let sum = 0 - ratios.forEach(x => sum += x) + ratios.forEach((x) => (sum += x)) const percentage = Math.round(100 * (sum / ratios.length)) process.stdout.write(Utils.FixedLength(percentage + "%", 6)) } process.stdout.write("\n") - console.log(Utils.FixedLength("", 12) + " " + usedLanguages.map(l => Utils.FixedLength(l, 6)).join("")) -} \ No newline at end of file + console.log( + Utils.FixedLength("", 12) + " " + usedLanguages.map((l) => Utils.FixedLength(l, 6)).join("") + ) +} diff --git a/service-worker.ts b/service-worker.ts index d50009104..946b51d89 100644 --- a/service-worker.ts +++ b/service-worker.ts @@ -1,8 +1,8 @@ const version = "0.0.8-GITHUB-COMMIT" interface ServiceWorkerFetchEvent extends Event { - request: RequestInfo & {url: string}, - respondWith: ((response: any | PromiseLike<Response>) => Promise<void>) + request: RequestInfo & { url: string } + respondWith: (response: any | PromiseLike<Response>) => Promise<void> } async function install() { @@ -17,8 +17,8 @@ async function install() { })//*/ } -addEventListener('install', e => (<any>e).waitUntil(install())); -addEventListener('activate', e => (<any>e).waitUntil(activate())); +addEventListener("install", (e) => (<any>e).waitUntil(install())) +addEventListener("activate", (e) => (<any>e).waitUntil(activate())) async function activate() { console.log("Activating service worker") @@ -28,59 +28,58 @@ async function activate() { title: "Some action" }] })*/ - - caches.keys().then(keys => { - // Remove all old caches - Promise.all( - keys.map(key => key !== version && caches.delete(key)) - ); - }).catch(console.error) + + caches + .keys() + .then((keys) => { + // Remove all old caches + Promise.all(keys.map((key) => key !== version && caches.delete(key))) + }) + .catch(console.error) } - - const cacheFirst = (event) => { event.respondWith( - caches.match(event.request, {ignoreSearch: true}).then((cacheResponse) => { - if(cacheResponse !== undefined){ + caches.match(event.request, { ignoreSearch: true }).then((cacheResponse) => { + if (cacheResponse !== undefined) { console.log("Loaded from cache: ", event.request) return cacheResponse } return fetch(event.request).then((networkResponse) => { - return caches.open(version).then((cache) => { - cache.put(event.request, networkResponse.clone()); + cache.put(event.request, networkResponse.clone()) console.log("Cached", event.request) - return networkResponse; + return networkResponse }) }) }) ) -}; +} - -self.addEventListener('fetch', - e => { - // Important: this lambda must run synchronously, as the browser will otherwise handle the request - const event = <ServiceWorkerFetchEvent> e; - try { - const origin = new URL(self.origin) - const requestUrl = new URL(event.request.url) - if (requestUrl.pathname.endsWith("service-worker-version")) { - console.log("Sending version number...") - event.respondWith(new Response(JSON.stringify({"service-worker-version": version}))); - return - } - const shouldBeCached = origin.host === requestUrl.host && origin.hostname !== "127.0.0.1" && origin.hostname !== "localhost" && !origin.host.endsWith(".gitpod.io") - if (!shouldBeCached) { - console.log("Not intercepting ", requestUrl.toString(), origin.host, requestUrl.host) - // We return _without_ calling event.respondWith, which signals the browser that it'll have to handle it himself - return - } - cacheFirst(event) - - } catch (e) { - console.error("CRASH IN SW:", e) - event.respondWith(fetch(event.request.url)); +self.addEventListener("fetch", (e) => { + // Important: this lambda must run synchronously, as the browser will otherwise handle the request + const event = <ServiceWorkerFetchEvent>e + try { + const origin = new URL(self.origin) + const requestUrl = new URL(event.request.url) + if (requestUrl.pathname.endsWith("service-worker-version")) { + console.log("Sending version number...") + event.respondWith(new Response(JSON.stringify({ "service-worker-version": version }))) + return } - }); + const shouldBeCached = + origin.host === requestUrl.host && + origin.hostname !== "127.0.0.1" && + origin.hostname !== "localhost" && + !origin.host.endsWith(".gitpod.io") + if (!shouldBeCached) { + console.log("Not intercepting ", requestUrl.toString(), origin.host, requestUrl.host) + // We return _without_ calling event.respondWith, which signals the browser that it'll have to handle it himself + return + } + cacheFirst(event) + } catch (e) { + console.error("CRASH IN SW:", e) + event.respondWith(fetch(event.request.url)) + } +}) diff --git a/test/Chai.spec.ts b/test/Chai.spec.ts index 9a434dee5..e88e1757e 100644 --- a/test/Chai.spec.ts +++ b/test/Chai.spec.ts @@ -1,16 +1,17 @@ -import {describe} from 'mocha' -import {expect} from 'chai' +import { describe } from "mocha" +import { expect } from "chai" describe("TestSuite", () => { - describe("function under test", () => { it("should work", () => { - expect("abc").eq("abc") + expect("abc").eq("abc") }) }) }) -it("global test", async() => { +it("global test", async () => { expect("abc").eq("abc") - expect(() => {throw "hi"}).throws(/hi/) -}) \ No newline at end of file + expect(() => { + throw "hi" + }).throws(/hi/) +}) diff --git a/test/CodeQuality.spec.ts b/test/CodeQuality.spec.ts index 2d6c3d113..b0e58c6e5 100644 --- a/test/CodeQuality.spec.ts +++ b/test/CodeQuality.spec.ts @@ -1,5 +1,5 @@ -import {describe} from 'mocha' -import {exec} from "child_process"; +import { describe } from "mocha" +import { exec } from "child_process" /** * @@ -8,33 +8,51 @@ import {exec} from "child_process"; * @private */ function detectInCode(forbidden: string, reason: string) { + const excludedDirs = [ + ".git", + "node_modules", + "dist", + ".cache", + ".parcel-cache", + "assets", + "vendor", + ".idea/", + ] - const excludedDirs = [".git", "node_modules", "dist", ".cache", ".parcel-cache", "assets", "vendor", ".idea/"] + exec( + 'grep -n "' + + forbidden + + '" -r . ' + + excludedDirs.map((d) => "--exclude-dir=" + d).join(" "), + (error, stdout, stderr) => { + if (error?.message?.startsWith("Command failed: grep")) { + console.warn("Command failed!") + return + } + if (error !== null) { + throw error + } + if (stderr !== "") { + throw stderr + } - exec("grep -n \"" + forbidden + "\" -r . " + excludedDirs.map(d => "--exclude-dir=" + d).join(" "), ((error, stdout, stderr) => { - if (error?.message?.startsWith("Command failed: grep")) { - console.warn("Command failed!") - return; + const found = stdout + .split("\n") + .filter((s) => s !== "") + .filter((s) => !s.startsWith("./test/")) + if (found.length > 0) { + throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}` + } } - if (error !== null) { - throw error - - } - if (stderr !== "") { - throw stderr - } - - const found = stdout.split("\n").filter(s => s !== "").filter(s => !s.startsWith("./test/")); - if (found.length > 0) { - throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}` - } - - })) + ) } describe("Code quality", () => { it("should not contain reverse", () => { - detectInCode("reverse()", "Reverse is stateful and changes the source list. This often causes subtle bugs") + detectInCode( + "reverse()", + "Reverse is stateful and changes the source list. This often causes subtle bugs" + ) }) it("should not contain 'constructor.name'", () => { @@ -42,8 +60,9 @@ describe("Code quality", () => { }) it("should not contain 'innerText'", () => { - detectInCode("innerText", "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead.") + detectInCode( + "innerText", + "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead." + ) }) - }) - diff --git a/test/Logic/Actors/Actors.spec.ts b/test/Logic/Actors/Actors.spec.ts index 2aed28b52..6aabca600 100644 --- a/test/Logic/Actors/Actors.spec.ts +++ b/test/Logic/Actors/Actors.spec.ts @@ -1,87 +1,83 @@ -import {expect} from 'chai' -import {Utils} from "../../../Utils"; -import UserRelatedState from "../../../Logic/State/UserRelatedState"; -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; -import SelectedElementTagsUpdater from "../../../Logic/Actors/SelectedElementTagsUpdater"; +import { expect } from "chai" +import { Utils } from "../../../Utils" +import UserRelatedState from "../../../Logic/State/UserRelatedState" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" +import SelectedElementTagsUpdater from "../../../Logic/Actors/SelectedElementTagsUpdater" import * as bookcaseJson from "../../../assets/generated/themes/bookcases.json" -import {UIEventSource} from "../../../Logic/UIEventSource"; -import Loc from "../../../Models/Loc"; -import SelectedFeatureHandler from "../../../Logic/Actors/SelectedFeatureHandler"; -import {ElementStorage} from "../../../Logic/ElementStorage"; +import { UIEventSource } from "../../../Logic/UIEventSource" +import Loc from "../../../Models/Loc" +import SelectedFeatureHandler from "../../../Logic/Actors/SelectedFeatureHandler" +import { ElementStorage } from "../../../Logic/ElementStorage" const latestTags = { - "amenity": "public_bookcase", - "books": "children;adults", - "capacity": "25", - "description": "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem", + amenity: "public_bookcase", + books: "children;adults", + capacity: "25", + description: "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem", "image:0": "https://i.imgur.com/Z8a69UG.jpg", - "name": "Stubbekwartier-buurtbibliotheek", - "nobrand": "yes", - "opening_hours": "24/7", - "operator": "Huisbewoner", - "public_bookcase:type": "reading_box" + name: "Stubbekwartier-buurtbibliotheek", + nobrand: "yes", + opening_hours: "24/7", + operator: "Huisbewoner", + "public_bookcase:type": "reading_box", } -Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/node/5568693115", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (1815943 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 5568693115, - "lat": 51.2179199, - "lon": 3.2154662, - "timestamp": "2021-08-21T16:22:55Z", - "version": 6, - "changeset": 110034454, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "tags": latestTags - }] - } -) +Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/5568693115", { + version: "0.6", + generator: "CGImap 0.8.5 (1815943 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 5568693115, + lat: 51.2179199, + lon: 3.2154662, + timestamp: "2021-08-21T16:22:55Z", + version: 6, + changeset: 110034454, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: latestTags, + }, + ], +}) -it("should download the latest version", () => { - const state = new UserRelatedState(new LayoutConfig(<any> bookcaseJson, true)) +it("should download the latest version", () => { + const state = new UserRelatedState(new LayoutConfig(<any>bookcaseJson, true)) const feature = { - "type": "Feature", - "id": "node/5568693115", - "properties": { - "amenity": "public_bookcase", - "books": "children;adults", - "capacity": "25", - "description": "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem", + type: "Feature", + id: "node/5568693115", + properties: { + amenity: "public_bookcase", + books: "children;adults", + capacity: "25", + description: "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem", "image:0": "https://i.imgur.com/Z8a69UG.jpg", - "name": "OUTDATED NAME", - "nobrand": "yes", - "opening_hours": "24/7", - "operator": "Huisbewoner", + name: "OUTDATED NAME", + nobrand: "yes", + opening_hours: "24/7", + operator: "Huisbewoner", "public_bookcase:type": "reading_box", - "id": "node/5568693115", - "_lat": "51.2179199", - "_lon": "3.2154662", - "fixme": "SOME FIXME" + id: "node/5568693115", + _lat: "51.2179199", + _lon: "3.2154662", + fixme: "SOME FIXME", }, - "geometry": { - "type": "Point", - "coordinates": [ - 3.2154662, - 51.2179199 - ] + geometry: { + type: "Point", + coordinates: [3.2154662, 51.2179199], }, - "bbox": { - "maxLat": 51.2179199, - "maxLon": 3.2154662, - "minLat": 51.2179199, - "minLon": 3.2154662 + bbox: { + maxLat: 51.2179199, + maxLon: 3.2154662, + minLat: 51.2179199, + minLon: 3.2154662, }, - "_lon": 3.2154662, - "_lat": 51.2179199 + _lon: 3.2154662, + _lat: 51.2179199, } state.allElements.addOrGetElement(feature) SelectedElementTagsUpdater.installCallback(state) @@ -96,7 +92,6 @@ it("should download the latest version", () => { expect(feature.properties.name).deep.equal("Stubbekwartier-buurtbibliotheek") // The fixme should be removed expect(feature.properties.fixme).deep.equal(undefined) - }) it("Hash without selected element should download geojson from OSM-API", async () => { const hash = new UIEventSource("node/5568693115") @@ -104,11 +99,10 @@ it("Hash without selected element should download geojson from OSM-API", async ( const loc = new UIEventSource<Loc>({ lat: 0, lon: 0, - zoom: 0 + zoom: 0, }) - - loc.addCallback(_ => { + loc.addCallback((_) => { expect(selected.data.properties.id).deep.equal("node/5568693115") expect(loc.data.zoom).deep.equal(14) expect(loc.data.lat).deep.equal(51.2179199) @@ -119,8 +113,6 @@ it("Hash without selected element should download geojson from OSM-API", async ( allElements: new ElementStorage(), featurePipeline: undefined, locationControl: loc, - layoutToUse: undefined + layoutToUse: undefined, }) - - -}) \ No newline at end of file +}) diff --git a/test/Logic/Actors/CreateMultiPolygonWithPointReuseAction.spec.ts b/test/Logic/Actors/CreateMultiPolygonWithPointReuseAction.spec.ts index b4e6e889b..22e53bb17 100644 --- a/test/Logic/Actors/CreateMultiPolygonWithPointReuseAction.spec.ts +++ b/test/Logic/Actors/CreateMultiPolygonWithPointReuseAction.spec.ts @@ -1,209 +1,126 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; -import {Tag} from "../../../Logic/Tags/Tag"; -import {Changes} from "../../../Logic/Osm/Changes"; +import { describe } from "mocha" +import { expect } from "chai" +import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction" +import { Tag } from "../../../Logic/Tags/Tag" +import { Changes } from "../../../Logic/Osm/Changes" describe("CreateMultiPolygonWithPointReuseAction", () => { - - it("should produce a correct changeset", () => { - - async () => { - - const feature = { - "type": "Feature", - "properties": { - "osm_id": "41097039", - "size_grb_building": "1374.89", - "addr:housenumber": "53", - "addr:street": "Startelstraat", - "building": "house", - "source:geometry:entity": "Gbg", - "source:geometry:date": "2014-04-28", - "source:geometry:oidn": "150044", - "source:geometry:uidn": "5403181", - "H_DTM_MIN": "50.35", - "H_DTM_GEM": "50.97", - "H_DSM_MAX": "59.40", - "H_DSM_P99": "59.09", - "HN_MAX": "8.43", - "HN_P99": "8.12", - "detection_method": "derived from OSM landuse: farmyard", - "auto_target_landuse": "farmyard", - "size_source_landuse": "8246.28", - "auto_building": "farm", - "id": "41097039", - "_lat": "50.84633355000016", - "_lon": "5.262964150000011", - "_layer": "grb", - "_length": "185.06002152312757", - "_length:km": "0.2", - "_now:date": "2022-02-22", - "_now:datetime": "2022-02-22 10:15:51", - "_loaded:date": "2022-02-22", - "_loaded:datetime": "2022-02-22 10:15:51", - "_geometry:type": "Polygon", - "_intersects_with_other_features": "", - "_country": "be", - "_overlaps_with_buildings": "[]", - "_overlap_percentage": "null", - "_grb_date": "2014-04-28", - "_grb_ref": "Gbg/150044", - "_building:min_level": "", - "_surface": "548.1242491529038", - "_surface:ha": "0", - "_reverse_overlap_percentage": "null", - "_imported_osm_object_found": "false", - "_imported_osm_still_fresh": "false", - "_target_building_type": "house" - }, - "geometry": { - "type": "Polygon", - "coordinates": <[number, number][][]>[ - [ - [ - 5.262684300000043, - 50.84624409999995 - ], - [ - 5.262777500000024, - 50.84620759999988 - ], - [ - 5.262798899999998, - 50.84621390000019 - ], - [ - 5.262999799999994, - 50.84619519999999 - ], - [ - 5.263107500000007, - 50.84618920000014 - ], - [ - 5.263115, - 50.84620990000026 - ], - [ - 5.26310279999998, - 50.84623050000014 - ], - [ - 5.263117999999977, - 50.846247400000166 - ], - [ - 5.263174599999989, - 50.84631019999971 - ], - [ - 5.263166999999989, - 50.84631459999995 - ], - [ - 5.263243999999979, - 50.84640239999989 - ], - [ - 5.2631607000000065, - 50.84643459999996 - ], - [ - 5.26313309999997, - 50.84640089999985 - ], - [ - 5.262907499999996, - 50.84647790000018 - ], - [ - 5.2628939999999576, - 50.846463699999774 - ], - [ - 5.262872100000033, - 50.846440700000294 - ], - [ - 5.262784699999991, - 50.846348899999924 - ], - [ - 5.262684300000043, - 50.84624409999995 - ] - ], - [ - [ - 5.262801899999976, - 50.84623269999982 - ], - [ - 5.2629535000000285, - 50.84638830000012 - ], - [ - 5.263070700000018, - 50.84634720000008 - ], - [ - 5.262998000000025, - 50.84626279999982 - ], - [ - 5.263066799999966, - 50.84623959999975 - ], - [ - 5.263064000000004, - 50.84623330000007 - ], - [ - 5.263009599999997, - 50.84623730000026 - ], - [ - 5.263010199999956, - 50.84621629999986 - ], - [ - 5.262801899999976, - 50.84623269999982 - ] - ] - ] - }, - } - - const innerRings = [...feature.geometry.coordinates] - innerRings.splice(0, 1) - - const action = new CreateMultiPolygonWithPointReuseAction( - [new Tag("building", "yes")], - feature.geometry.coordinates[0], - innerRings, - undefined, - [], - "import" - ) - const descriptions = await action.Perform(new Changes()) - - const ways= descriptions.filter(d => d.type === "way") - expect(ways[0].id == -18, "unexpected id").true - expect(ways[1].id == -27, "unexpected id").true - const outer = ways[0].changes["coordinates"] - expect(outer).deep.equal(feature.geometry.coordinates[0]) - const inner = ways[1].changes["coordinates"] - expect(inner).deep.equal(feature.geometry.coordinates[1]) - const members = <{type: string, role: string, ref: number}[]> descriptions.find(d => d.type === "relation").changes["members"] - expect(members[0].role, "incorrect role").eq("outer") - expect(members[1].role, "incorrect role").eq("inner") - expect(members[0].type , "incorrect type").eq("way") - expect(members[1].type , "incorrect type").eq("way") - expect(members[0].ref, "incorrect id").eq(-18) - expect(members[1].ref , "incorrect id").eq(-27) + it("should produce a correct changeset", () => { + ;async () => { + const feature = { + type: "Feature", + properties: { + osm_id: "41097039", + size_grb_building: "1374.89", + "addr:housenumber": "53", + "addr:street": "Startelstraat", + building: "house", + "source:geometry:entity": "Gbg", + "source:geometry:date": "2014-04-28", + "source:geometry:oidn": "150044", + "source:geometry:uidn": "5403181", + H_DTM_MIN: "50.35", + H_DTM_GEM: "50.97", + H_DSM_MAX: "59.40", + H_DSM_P99: "59.09", + HN_MAX: "8.43", + HN_P99: "8.12", + detection_method: "derived from OSM landuse: farmyard", + auto_target_landuse: "farmyard", + size_source_landuse: "8246.28", + auto_building: "farm", + id: "41097039", + _lat: "50.84633355000016", + _lon: "5.262964150000011", + _layer: "grb", + _length: "185.06002152312757", + "_length:km": "0.2", + "_now:date": "2022-02-22", + "_now:datetime": "2022-02-22 10:15:51", + "_loaded:date": "2022-02-22", + "_loaded:datetime": "2022-02-22 10:15:51", + "_geometry:type": "Polygon", + _intersects_with_other_features: "", + _country: "be", + _overlaps_with_buildings: "[]", + _overlap_percentage: "null", + _grb_date: "2014-04-28", + _grb_ref: "Gbg/150044", + "_building:min_level": "", + _surface: "548.1242491529038", + "_surface:ha": "0", + _reverse_overlap_percentage: "null", + _imported_osm_object_found: "false", + _imported_osm_still_fresh: "false", + _target_building_type: "house", + }, + geometry: { + type: "Polygon", + coordinates: <[number, number][][]>[ + [ + [5.262684300000043, 50.84624409999995], + [5.262777500000024, 50.84620759999988], + [5.262798899999998, 50.84621390000019], + [5.262999799999994, 50.84619519999999], + [5.263107500000007, 50.84618920000014], + [5.263115, 50.84620990000026], + [5.26310279999998, 50.84623050000014], + [5.263117999999977, 50.846247400000166], + [5.263174599999989, 50.84631019999971], + [5.263166999999989, 50.84631459999995], + [5.263243999999979, 50.84640239999989], + [5.2631607000000065, 50.84643459999996], + [5.26313309999997, 50.84640089999985], + [5.262907499999996, 50.84647790000018], + [5.2628939999999576, 50.846463699999774], + [5.262872100000033, 50.846440700000294], + [5.262784699999991, 50.846348899999924], + [5.262684300000043, 50.84624409999995], + ], + [ + [5.262801899999976, 50.84623269999982], + [5.2629535000000285, 50.84638830000012], + [5.263070700000018, 50.84634720000008], + [5.262998000000025, 50.84626279999982], + [5.263066799999966, 50.84623959999975], + [5.263064000000004, 50.84623330000007], + [5.263009599999997, 50.84623730000026], + [5.263010199999956, 50.84621629999986], + [5.262801899999976, 50.84623269999982], + ], + ], + }, } - - }) -}) \ No newline at end of file + + const innerRings = [...feature.geometry.coordinates] + innerRings.splice(0, 1) + + const action = new CreateMultiPolygonWithPointReuseAction( + [new Tag("building", "yes")], + feature.geometry.coordinates[0], + innerRings, + undefined, + [], + "import" + ) + const descriptions = await action.Perform(new Changes()) + + const ways = descriptions.filter((d) => d.type === "way") + expect(ways[0].id == -18, "unexpected id").true + expect(ways[1].id == -27, "unexpected id").true + const outer = ways[0].changes["coordinates"] + expect(outer).deep.equal(feature.geometry.coordinates[0]) + const inner = ways[1].changes["coordinates"] + expect(inner).deep.equal(feature.geometry.coordinates[1]) + const members = <{ type: string; role: string; ref: number }[]>( + descriptions.find((d) => d.type === "relation").changes["members"] + ) + expect(members[0].role, "incorrect role").eq("outer") + expect(members[1].role, "incorrect role").eq("inner") + expect(members[0].type, "incorrect type").eq("way") + expect(members[1].type, "incorrect type").eq("way") + expect(members[0].ref, "incorrect id").eq(-18) + expect(members[1].ref, "incorrect id").eq(-27) + } + }) +}) diff --git a/test/Logic/ExtraFunctions.spec.ts b/test/Logic/ExtraFunctions.spec.ts index 0f3bb60d6..173c765a7 100644 --- a/test/Logic/ExtraFunctions.spec.ts +++ b/test/Logic/ExtraFunctions.spec.ts @@ -1,57 +1,52 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {ExtraFuncParams, ExtraFunctions} from "../../Logic/ExtraFunctions"; -import {OsmFeature} from "../../Models/OsmFeature"; - +import { describe } from "mocha" +import { expect } from "chai" +import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions" +import { OsmFeature } from "../../Models/OsmFeature" describe("OverlapFunc", () => { - it("should give doors on the edge", () => { const door: OsmFeature = { - "type": "Feature", - "id": "node/9909268725", - "properties": { - "automatic_door": "no", - "door": "hinged", - "indoor": "door", + type: "Feature", + id: "node/9909268725", + properties: { + automatic_door: "no", + door: "hinged", + indoor: "door", "kerb:height": "0 cm", - "width": "1", - "id": "node/9909268725", + width: "1", + id: "node/9909268725", }, - "geometry": { - "type": "Point", - "coordinates": [ - 4.3494436, - 50.8657928 - ] + geometry: { + type: "Point", + coordinates: [4.3494436, 50.8657928], }, } const hermanTeirlinck = { - "type": "Feature", - "id": "way/444059131", - "properties": { - "timestamp": "2022-07-27T15:15:01Z", - "version": 27, - "changeset": 124146283, - "user": "Pieter Vander Vennet", - "uid": 3818858, + type: "Feature", + id: "way/444059131", + properties: { + timestamp: "2022-07-27T15:15:01Z", + version: 27, + changeset: 124146283, + user: "Pieter Vander Vennet", + uid: 3818858, "addr:city": "Bruxelles - Brussel", "addr:housenumber": "88", "addr:postcode": "1000", "addr:street": "Avenue du Port - Havenlaan", - "building": "government", + building: "government", "building:levels": "5", - "name": "Herman Teirlinckgebouw", - "operator": "Vlaamse overheid", - "wikidata": "Q47457146", - "wikipedia": "nl:Herman Teirlinckgebouw", - "id": "way/444059131", - "_backend": "https://www.openstreetmap.org", - "_lat": "50.86622355", - "_lon": "4.3501212", - "_layer": "walls_and_buildings", - "_length": "380.5933566256343", + name: "Herman Teirlinckgebouw", + operator: "Vlaamse overheid", + wikidata: "Q47457146", + wikipedia: "nl:Herman Teirlinckgebouw", + id: "way/444059131", + _backend: "https://www.openstreetmap.org", + _lat: "50.86622355", + _lon: "4.3501212", + _layer: "walls_and_buildings", + _length: "380.5933566256343", "_length:km": "0.4", "_now:date": "2022-07-29", "_now:datetime": "2022-07-29 14:19:25", @@ -61,183 +56,72 @@ describe("OverlapFunc", () => { "_last_edit:contributor:uid": 3818858, "_last_edit:changeset": 124146283, "_last_edit:timestamp": "2022-07-27T15:15:01Z", - "_version_number": 27, + _version_number: 27, "_geometry:type": "Polygon", - "_surface": "7461.252251355437", + _surface: "7461.252251355437", "_surface:ha": "0.7", - "_country": "be" + _country: "be", }, - "geometry": { - "type": "Polygon", - "coordinates": [ + geometry: { + type: "Polygon", + coordinates: [ [ - [ - 4.3493369, - 50.8658274 - ], - [ - 4.3493393, - 50.8658266 - ], - [ - 4.3494436, - 50.8657928 - ], - [ - 4.3495272, - 50.8657658 - ], - [ - 4.349623, - 50.8657348 - ], - [ - 4.3497442, - 50.8656956 - ], - [ - 4.3498441, - 50.8656632 - ], - [ - 4.3500768, - 50.8655878 - ], - [ - 4.3501619, - 50.8656934 - ], - [ - 4.3502113, - 50.8657551 - ], - [ - 4.3502729, - 50.8658321 - ], - [ - 4.3503063, - 50.8658737 - ], - [ - 4.3503397, - 50.8659153 - ], - [ - 4.3504159, - 50.8660101 - ], - [ - 4.3504177, - 50.8660123 - ], - [ - 4.3504354, - 50.8660345 - ], - [ - 4.3505348, - 50.8661584 - ], - [ - 4.3504935, - 50.866172 - ], - [ - 4.3506286, - 50.8663405 - ], - [ - 4.3506701, - 50.8663271 - ], - [ - 4.3508563, - 50.8665592 - ], - [ - 4.3509055, - 50.8666206 - ], - [ - 4.3506278, - 50.8667104 - ], - [ - 4.3504502, - 50.8667675 - ], - [ - 4.3503132, - 50.8668115 - ], - [ - 4.3502162, - 50.8668427 - ], - [ - 4.3501645, - 50.8668593 - ], - [ - 4.3499296, - 50.8665664 - ], - [ - 4.3498821, - 50.8665073 - ], - [ - 4.3498383, - 50.8664527 - ], - [ - 4.3498126, - 50.8664207 - ], - [ - 4.3497459, - 50.8663376 - ], - [ - 4.3497227, - 50.8663086 - ], - [ - 4.3496517, - 50.8662201 - ], - [ - 4.3495158, - 50.8660507 - ], - [ - 4.3493369, - 50.8658274 - ] - ] - ] + [4.3493369, 50.8658274], + [4.3493393, 50.8658266], + [4.3494436, 50.8657928], + [4.3495272, 50.8657658], + [4.349623, 50.8657348], + [4.3497442, 50.8656956], + [4.3498441, 50.8656632], + [4.3500768, 50.8655878], + [4.3501619, 50.8656934], + [4.3502113, 50.8657551], + [4.3502729, 50.8658321], + [4.3503063, 50.8658737], + [4.3503397, 50.8659153], + [4.3504159, 50.8660101], + [4.3504177, 50.8660123], + [4.3504354, 50.8660345], + [4.3505348, 50.8661584], + [4.3504935, 50.866172], + [4.3506286, 50.8663405], + [4.3506701, 50.8663271], + [4.3508563, 50.8665592], + [4.3509055, 50.8666206], + [4.3506278, 50.8667104], + [4.3504502, 50.8667675], + [4.3503132, 50.8668115], + [4.3502162, 50.8668427], + [4.3501645, 50.8668593], + [4.3499296, 50.8665664], + [4.3498821, 50.8665073], + [4.3498383, 50.8664527], + [4.3498126, 50.8664207], + [4.3497459, 50.8663376], + [4.3497227, 50.8663086], + [4.3496517, 50.8662201], + [4.3495158, 50.8660507], + [4.3493369, 50.8658274], + ], + ], + }, + bbox: { + maxLat: 50.8668593, + maxLon: 4.3509055, + minLat: 50.8655878, + minLon: 4.3493369, }, - "bbox": { - "maxLat": 50.8668593, - "maxLon": 4.3509055, - "minLat": 50.8655878, - "minLon": 4.3493369 - } } const params: ExtraFuncParams = { - getFeatureById: id => undefined, + getFeatureById: (id) => undefined, getFeaturesWithin: () => [[door]], - memberships: undefined + memberships: undefined, } - ExtraFunctions.FullPatchFeature(params, hermanTeirlinck) const overlap = (<any>hermanTeirlinck).overlapWith("*") console.log(JSON.stringify(overlap)) expect(overlap[0].feat == door).true - }) - -}) \ No newline at end of file +}) diff --git a/test/Logic/FeatureSource/OsmFeatureSource.spec.ts b/test/Logic/FeatureSource/OsmFeatureSource.spec.ts index 64e358e26..02dce4a55 100644 --- a/test/Logic/FeatureSource/OsmFeatureSource.spec.ts +++ b/test/Logic/FeatureSource/OsmFeatureSource.spec.ts @@ -1,234 +1,111 @@ -import {describe} from 'mocha' -import OsmFeatureSource from "../../../Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource"; -import {UIEventSource} from "../../../Logic/UIEventSource"; -import ScriptUtils from "../../../scripts/ScriptUtils"; -import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer"; -import {Tiles} from "../../../Models/TileRange"; -import {readFileSync} from "fs"; -import {Utils} from "../../../Utils"; -import {Tag} from "../../../Logic/Tags/Tag"; -import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; -import {expect} from "chai"; - +import { describe } from "mocha" +import OsmFeatureSource from "../../../Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource" +import { UIEventSource } from "../../../Logic/UIEventSource" +import ScriptUtils from "../../../scripts/ScriptUtils" +import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" +import { Tiles } from "../../../Models/TileRange" +import { readFileSync } from "fs" +import { Utils } from "../../../Utils" +import { Tag } from "../../../Logic/Tags/Tag" +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" +import { expect } from "chai" const expected = { - "type": "Feature", - "id": "relation/5759328", - "properties": { - "timestamp": "2022-06-10T00:46:55Z", - "version": 6, - "changeset": 122187206, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "amenity": "school", + type: "Feature", + id: "relation/5759328", + properties: { + timestamp: "2022-06-10T00:46:55Z", + version: 6, + changeset: 122187206, + user: "Pieter Vander Vennet", + uid: 3818858, + amenity: "school", "isced:2011:level": "vocational_lower_secondary;vocational_upper_secondary", - "name": "Koninklijk Technisch Atheneum Pro Technica", + name: "Koninklijk Technisch Atheneum Pro Technica", "school:gender": "mixed", - "type": "multipolygon", - "website": "http://ktahalle.be/", - "id": "relation/5759328", - "_backend":"https://osm.org" + type: "multipolygon", + website: "http://ktahalle.be/", + id: "relation/5759328", + _backend: "https://osm.org", }, - "geometry": { - "type": "MultiPolygon", - "coordinates": [ + geometry: { + type: "MultiPolygon", + coordinates: [ [ [ - [ - 4.2461832, - 50.7335751 - ], - [ - 4.2463167, - 50.7336785 - ], - [ - 4.2463473, - 50.7337021 - ], - [ - 4.2464497, - 50.7337814 - ], - [ - 4.2471698, - 50.7343389 - ], - [ - 4.2469541, - 50.7344768 - ], - [ - 4.2467571, - 50.7346116 - ], - [ - 4.2467727, - 50.7346199 - ], - [ - 4.2465714, - 50.7347511 - ], - [ - 4.2462398, - 50.7349687 - ], - [ - 4.2453546, - 50.734601 - ], - [ - 4.2451895, - 50.7345103 - ], - [ - 4.2448867, - 50.7342629 - ], - [ - 4.244899, - 50.7342069 - ], - [ - 4.2461832, - 50.7335751 - ] - ] + [4.2461832, 50.7335751], + [4.2463167, 50.7336785], + [4.2463473, 50.7337021], + [4.2464497, 50.7337814], + [4.2471698, 50.7343389], + [4.2469541, 50.7344768], + [4.2467571, 50.7346116], + [4.2467727, 50.7346199], + [4.2465714, 50.7347511], + [4.2462398, 50.7349687], + [4.2453546, 50.734601], + [4.2451895, 50.7345103], + [4.2448867, 50.7342629], + [4.244899, 50.7342069], + [4.2461832, 50.7335751], + ], ], [ [ - [ - 4.2444209, - 50.7353737 - ], - [ - 4.2439986, - 50.7352034 - ], - [ - 4.2440303, - 50.7351755 - ], - [ - 4.2440602, - 50.7351058 - ], - [ - 4.2439776, - 50.7350326 - ], - [ - 4.2439558, - 50.7350132 - ], - [ - 4.2438246, - 50.7348961 - ], - [ - 4.2437848, - 50.73486 - ], - [ - 4.2436555, - 50.7347455 - ], - [ - 4.2435905, - 50.734689 - ], - [ - 4.2435494, - 50.7346601 - ], - [ - 4.2435038, - 50.7346256 - ], - [ - 4.2434769, - 50.7346026 - ], - [ - 4.2430948, - 50.734275 - ], - [ - 4.2427978, - 50.7340052 - ], - [ - 4.2430556, - 50.7338391 - ], - [ - 4.2438957, - 50.7334942 - ], - [ - 4.2440204, - 50.7336368 - ], - [ - 4.2442806, - 50.7338922 - ], - [ - 4.2444173, - 50.7340119 - ], - [ - 4.2447379, - 50.7342925 - ], - [ - 4.2450107, - 50.7345294 - ], - [ - 4.2450236, - 50.7346021 - ], - [ - 4.2449643, - 50.7347019 - ], - [ - 4.244711, - 50.7350821 - ], - [ - 4.2444209, - 50.7353737 - ] - ] - ] - ] - } + [4.2444209, 50.7353737], + [4.2439986, 50.7352034], + [4.2440303, 50.7351755], + [4.2440602, 50.7351058], + [4.2439776, 50.7350326], + [4.2439558, 50.7350132], + [4.2438246, 50.7348961], + [4.2437848, 50.73486], + [4.2436555, 50.7347455], + [4.2435905, 50.734689], + [4.2435494, 50.7346601], + [4.2435038, 50.7346256], + [4.2434769, 50.7346026], + [4.2430948, 50.734275], + [4.2427978, 50.7340052], + [4.2430556, 50.7338391], + [4.2438957, 50.7334942], + [4.2440204, 50.7336368], + [4.2442806, 50.7338922], + [4.2444173, 50.7340119], + [4.2447379, 50.7342925], + [4.2450107, 50.7345294], + [4.2450236, 50.7346021], + [4.2449643, 50.7347019], + [4.244711, 50.7350821], + [4.2444209, 50.7353737], + ], + ], + ], + }, } -function test(done: () => void){ - let fetchedTile = undefined; - const neededTiles = new UIEventSource<number[]>([Tiles.tile_index(17, 67081, 44033)]); +function test(done: () => void) { + let fetchedTile = undefined + const neededTiles = new UIEventSource<number[]>([Tiles.tile_index(17, 67081, 44033)]) new OsmFeatureSource({ allowedFeatures: new Tag("amenity", "school"), - handleTile: tile => { + handleTile: (tile) => { fetchedTile = tile const data = tile.features.data[0].feature expect(data.properties).deep.eq({ - id: 'relation/5759328', timestamp: '2022-06-10T00:46:55Z', + id: "relation/5759328", + timestamp: "2022-06-10T00:46:55Z", version: 6, changeset: 122187206, - user: 'Pieter Vander Vennet', + user: "Pieter Vander Vennet", uid: 3818858, - amenity: 'school', - 'isced:2011:level': 'vocational_lower_secondary;vocational_upper_secondary', - name: 'Koninklijk Technisch Atheneum Pro Technica', - 'school:gender': 'mixed', - type: 'multipolygon', - website: 'http://ktahalle.be/', - _backend: 'https://osm.org' + amenity: "school", + "isced:2011:level": "vocational_lower_secondary;vocational_upper_secondary", + name: "Koninklijk Technisch Atheneum Pro Technica", + "school:gender": "mixed", + type: "multipolygon", + website: "http://ktahalle.be/", + _backend: "https://osm.org", }) expect(data.geometry.type).eq("MultiPolygon") expect(data).deep.eq(expected) @@ -240,7 +117,7 @@ function test(done: () => void){ osmConnection: { Backend(): string { return "https://osm.org" - } + }, }, filteredLayers: new UIEventSource<FilteredLayer[]>([ { @@ -248,31 +125,39 @@ function test(done: () => void){ layerDef: new LayerConfig({ id: "school", source: { - osmTags: "amenity=school" + osmTags: "amenity=school", }, - mapRendering: null + mapRendering: null, }), - isDisplayed: new UIEventSource<boolean>(true) - } - ]) - } + isDisplayed: new UIEventSource<boolean>(true), + }, + ]), + }, }) } describe("OsmFeatureSource", () => { - it("downloading the full school should give a multipolygon", (done) => { ScriptUtils.fixUtils() let data = JSON.parse(readFileSync("./test/Logic/FeatureSource/osmdata.json", "utf8")) - Utils.injectJsonDownloadForTests("https://osm.org/api/0.6/map?bbox=4.24346923828125,50.732978448277514,4.2462158203125,50.73471682490244", data) + Utils.injectJsonDownloadForTests( + "https://osm.org/api/0.6/map?bbox=4.24346923828125,50.732978448277514,4.2462158203125,50.73471682490244", + data + ) test(done) }) it("downloading the partial school polygon should give a multipolygon", (done) => { ScriptUtils.fixUtils() - Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/relation/5759328/full", JSON.parse(readFileSync("./test/data/relation_5759328.json","UTF-8"))) + Utils.injectJsonDownloadForTests( + "https://www.openstreetmap.org/api/0.6/relation/5759328/full", + JSON.parse(readFileSync("./test/data/relation_5759328.json", "UTF-8")) + ) let data = JSON.parse(readFileSync("./test/Logic/FeatureSource/small_box.json", "utf8")) - Utils.injectJsonDownloadForTests("https://osm.org/api/0.6/map?bbox=4.24346923828125,50.732978448277514,4.2462158203125,50.73471682490244", data) + Utils.injectJsonDownloadForTests( + "https://osm.org/api/0.6/map?bbox=4.24346923828125,50.732978448277514,4.2462158203125,50.73471682490244", + data + ) test(done) }) -}) \ No newline at end of file +}) diff --git a/test/Logic/FeatureSource/TileFreshnessCalculator.spec.ts b/test/Logic/FeatureSource/TileFreshnessCalculator.spec.ts index 7d055876e..853dd19ff 100644 --- a/test/Logic/FeatureSource/TileFreshnessCalculator.spec.ts +++ b/test/Logic/FeatureSource/TileFreshnessCalculator.spec.ts @@ -1,25 +1,23 @@ -import {describe} from 'mocha' -import TileFreshnessCalculator from "../../../Logic/FeatureSource/TileFreshnessCalculator"; -import {Tiles} from "../../../Models/TileRange"; -import {expect} from "chai" +import { describe } from "mocha" +import TileFreshnessCalculator from "../../../Logic/FeatureSource/TileFreshnessCalculator" +import { Tiles } from "../../../Models/TileRange" +import { expect } from "chai" describe("TileFreshnessCalculator", () => { + it("should get the freshness for loaded tiles", () => { + const calc = new TileFreshnessCalculator() + // 19/266407/175535 + const date = new Date() + date.setTime(42) + calc.addTileLoad(Tiles.tile_index(19, 266406, 175534), date) - it("should get the freshness for loaded tiles", - () => { - const calc = new TileFreshnessCalculator(); - // 19/266407/175535 - const date = new Date() - date.setTime(42) - calc.addTileLoad(Tiles.tile_index(19, 266406, 175534), date) - - expect(calc.freshnessFor(19, 266406, 175534).getTime()).eq(42) - expect(calc.freshnessFor(20, 266406 * 2, 175534 * 2 + 1).getTime()).eq(42) - expect(calc.freshnessFor(19, 266406, 175535)).undefined - expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2)).undefined - calc.addTileLoad(Tiles.tile_index(19, 266406, 175534 + 1), date) - calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534), date) - calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534 + 1), date) - expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2).getTime()).eq(42) - }) + expect(calc.freshnessFor(19, 266406, 175534).getTime()).eq(42) + expect(calc.freshnessFor(20, 266406 * 2, 175534 * 2 + 1).getTime()).eq(42) + expect(calc.freshnessFor(19, 266406, 175535)).undefined + expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2)).undefined + calc.addTileLoad(Tiles.tile_index(19, 266406, 175534 + 1), date) + calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534), date) + calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534 + 1), date) + expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2).getTime()).eq(42) + }) }) diff --git a/test/Logic/GeoOperations.spec.ts b/test/Logic/GeoOperations.spec.ts index 83e396a9c..749dce613 100644 --- a/test/Logic/GeoOperations.spec.ts +++ b/test/Logic/GeoOperations.spec.ts @@ -1,174 +1,125 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import * as turf from "@turf/turf"; -import {GeoOperations} from "../../Logic/GeoOperations"; +import { describe } from "mocha" +import { expect } from "chai" +import * as turf from "@turf/turf" +import { GeoOperations } from "../../Logic/GeoOperations" describe("GeoOperations", () => { - describe("calculateOverlap", () => { it("should not give too much overlap (regression test)", () => { const polyGrb = { - "type": "Feature", - "properties": { - "osm_id": "25189153", - "size_grb_building": "217.14", + type: "Feature", + properties: { + osm_id: "25189153", + size_grb_building: "217.14", "addr:housenumber": "173", "addr:street": "Kortrijksestraat", - "building": "house", + building: "house", "source:geometry:entity": "Gbg", "source:geometry:date": "2015/02/27", "source:geometry:oidn": "1729460", "source:geometry:uidn": "8713648", - "H_DTM_MIN": "17.28", - "H_DTM_GEM": "17.59", - "H_DSM_MAX": "29.04", - "H_DSM_P99": "28.63", - "HN_MAX": "11.45", - "HN_P99": "11.04", - "detection_method": "from existing OSM building source: house ,hits (3)", - "auto_building": "house", - "size_shared": "210.68", - "size_source_building": "212.63", - "id": "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37", - "_lat": "50.83736194999996", - "_lon": "3.2432137000000116", - "_layer": "GRB", - "_length": "48.51529464293261", + H_DTM_MIN: "17.28", + H_DTM_GEM: "17.59", + H_DSM_MAX: "29.04", + H_DSM_P99: "28.63", + HN_MAX: "11.45", + HN_P99: "11.04", + detection_method: "from existing OSM building source: house ,hits (3)", + auto_building: "house", + size_shared: "210.68", + size_source_building: "212.63", + id: "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37", + _lat: "50.83736194999996", + _lon: "3.2432137000000116", + _layer: "GRB", + _length: "48.51529464293261", "_length:km": "0.0", "_now:date": "2021-12-05", "_now:datetime": "2021-12-05 21:51:40", "_loaded:date": "2021-12-05", - "_loaded:datetime": "2021-12-05 21:51:40" + "_loaded:datetime": "2021-12-05 21:51:40", }, - "geometry": { - "type": "Polygon", - "coordinates": [ + geometry: { + type: "Polygon", + coordinates: [ [ - [ - 3.2431059999999974, - 50.83730270000021 - ], - [ - 3.243174299999987, - 50.83728850000007 - ], - [ - 3.2432116000000173, - 50.83736910000003 - ], - [ - 3.2433214000000254, - 50.83740350000011 - ], - [ - 3.24329779999996, - 50.837435399999855 - ], - [ - 3.2431881000000504, - 50.83740090000025 - ], - [ - 3.243152699999997, - 50.83738980000017 - ], - [ - 3.2431059999999974, - 50.83730270000021 - ] - ] - ] + [3.2431059999999974, 50.83730270000021], + [3.243174299999987, 50.83728850000007], + [3.2432116000000173, 50.83736910000003], + [3.2433214000000254, 50.83740350000011], + [3.24329779999996, 50.837435399999855], + [3.2431881000000504, 50.83740090000025], + [3.243152699999997, 50.83738980000017], + [3.2431059999999974, 50.83730270000021], + ], + ], + }, + id: "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37", + _lon: 3.2432137000000116, + _lat: 50.83736194999996, + bbox: { + maxLat: 50.837435399999855, + maxLon: 3.2433214000000254, + minLat: 50.83728850000007, + minLon: 3.2431059999999974, }, - "id": "https://betadata.grbosm.site/grb?bbox=360935.6475626023,6592540.815539878,361088.52161917265,6592693.689596449/37", - "_lon": 3.2432137000000116, - "_lat": 50.83736194999996, - "bbox": { - "maxLat": 50.837435399999855, - "maxLon": 3.2433214000000254, - "minLat": 50.83728850000007, - "minLon": 3.2431059999999974 - } } const polyHouse = { - "type": "Feature", - "id": "way/594963177", - "properties": { - "timestamp": "2021-12-05T04:04:55Z", - "version": 3, - "changeset": 114571409, - "user": "Pieter Vander Vennet", - "uid": 3818858, + type: "Feature", + id: "way/594963177", + properties: { + timestamp: "2021-12-05T04:04:55Z", + version: 3, + changeset: 114571409, + user: "Pieter Vander Vennet", + uid: 3818858, "addr:housenumber": "171", "addr:street": "Kortrijksestraat", - "building": "house", + building: "house", "source:geometry:date": "2018-10-22", "source:geometry:ref": "Gbg/5096537", "_last_edit:contributor": "Pieter Vander Vennet", "_last_edit:contributor:uid": 3818858, "_last_edit:changeset": 114571409, "_last_edit:timestamp": "2021-12-05T04:04:55Z", - "_version_number": 3, - "id": "way/594963177", - "_backend": "https://www.openstreetmap.org", - "_lat": "50.83736395", - "_lon": "3.2430937", - "_layer": "OSM-buildings", - "_length": "43.561938680928506", + _version_number: 3, + id: "way/594963177", + _backend: "https://www.openstreetmap.org", + _lat: "50.83736395", + _lon: "3.2430937", + _layer: "OSM-buildings", + _length: "43.561938680928506", "_length:km": "0.0", "_now:date": "2021-12-05", "_now:datetime": "2021-12-05 21:51:40", "_loaded:date": "2021-12-05", "_loaded:datetime": "2021-12-05 21:51:39", - "_surface": "93.32785810484549", - "_surface:ha": "0" + _surface: "93.32785810484549", + "_surface:ha": "0", }, - "geometry": { - "type": "Polygon", - "coordinates": [ + geometry: { + type: "Polygon", + coordinates: [ [ - [ - 3.2429993, - 50.8373243 - ], - [ - 3.243106, - 50.8373027 - ], - [ - 3.2431527, - 50.8373898 - ], - [ - 3.2431881, - 50.8374009 - ], - [ - 3.2431691, - 50.8374252 - ], - [ - 3.2430936, - 50.837401 - ], - [ - 3.243046, - 50.8374112 - ], - [ - 3.2429993, - 50.8373243 - ] - ] - ] + [3.2429993, 50.8373243], + [3.243106, 50.8373027], + [3.2431527, 50.8373898], + [3.2431881, 50.8374009], + [3.2431691, 50.8374252], + [3.2430936, 50.837401], + [3.243046, 50.8374112], + [3.2429993, 50.8373243], + ], + ], + }, + _lon: 3.2430937, + _lat: 50.83736395, + bbox: { + maxLat: 50.8374252, + maxLon: 3.2431881, + minLat: 50.8373027, + minLon: 3.2429993, }, - "_lon": 3.2430937, - "_lat": 50.83736395, - "bbox": { - "maxLat": 50.8374252, - "maxLon": 3.2431881, - "minLat": 50.8373027, - "minLon": 3.2429993 - } } const p0 = turf.polygon(polyGrb.geometry.coordinates) @@ -176,11 +127,10 @@ describe("GeoOperations", () => { const p1 = turf.polygon(polyHouse.geometry.coordinates) expect(p1).not.null - const overlaps = GeoOperations.calculateOverlap(polyGrb, [polyHouse]) expect(overlaps).empty const overlapsRev = GeoOperations.calculateOverlap(polyHouse, [polyGrb]) expect(overlapsRev).empty }) }) -}) \ No newline at end of file +}) diff --git a/test/Logic/ImageProviders/ImageProviders.spec.ts b/test/Logic/ImageProviders/ImageProviders.spec.ts index 0000633b1..af53e3d23 100644 --- a/test/Logic/ImageProviders/ImageProviders.spec.ts +++ b/test/Logic/ImageProviders/ImageProviders.spec.ts @@ -1,77 +1,102 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import AllImageProviders from "../../../Logic/ImageProviders/AllImageProviders"; -import {UIEventSource} from "../../../Logic/UIEventSource"; -import {Utils} from "../../../Utils"; +import { describe } from "mocha" +import { expect } from "chai" +import AllImageProviders from "../../../Logic/ImageProviders/AllImageProviders" +import { UIEventSource } from "../../../Logic/UIEventSource" +import { Utils } from "../../../Utils" describe("ImageProviders", () => { - - it("should work on a variaty of inputs", () => { - let i = 0 - function expects(url, tags, providerName = undefined) { - tags.id = "test/" + i - i++ - AllImageProviders.LoadImagesFor(new UIEventSource(tags)).addCallbackD(images => { - console.log("ImageProvider test", tags.id, "for", tags) - const img = images[0] - if (img === undefined) { - throw "No image found" - } - expect(img.url).deep.equal(url) - if (providerName) { - expect(providerName).deep.equal(img.provider.constructor.name) - } - console.log("OK") - }) - } - - const muntpoort_expected = "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABr%C3%BCgge-Muntpoort_6-29510-58192.jpg?width=500&height=400" - expects( - muntpoort_expected, - { - "wikimedia_commons": "File:Brügge-Muntpoort_6-29510-58192.jpg" - }, "WikimediaImageProvider") - - - expects(muntpoort_expected, - { - "wikimedia_commons": "https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg" - }, "WikimediaImageProvider") - - expects(muntpoort_expected, { - "image": "https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg" - }, "WikimediaImageProvider") - - - expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400", { - "image": "File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg" - }, "WikimediaImageProvider") - - expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400", { - "wikimedia_commons": "File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg" - }, "WikimediaImageProvider") - - - expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABrugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg?width=500&height=400", { - image: "File:Brugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg" - }, "WikimediaImageProvider") - - expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3APapageno_Jef_Claerhout.jpg?width=500&height=400", { - "wikimedia_commons": "File:Papageno_Jef_Claerhout.jpg" - }, "WikimediaImageProvider") - - Utils.injectJsonDownloadForTests( - "https://graph.mapillary.com/196804715753265?fields=thumb_1024_url&&access_token=MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85", - { - "thumb_1024_url": "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8HQ3DrfU76tWMC602spvM_e_rqOHyiUcYUTetXM7K52DDBEY5J4FWg4WKQqVUlMsWJn4nLXk0pxlBLx31146FqZ2Kg65z7lJUfR6wpW6WPSR5_y7RKdv4YEuzPjwIN0lagBnQONV3UjmXnEGpMouU?stp=s1024x768&ccb=10-5&oh=d460b401c505714ee1cb8bd6baf8ae5d&oe=61731FC3&_nc_sid=122ab1", - "id": "196804715753265" + it("should work on a variaty of inputs", () => { + let i = 0 + function expects(url, tags, providerName = undefined) { + tags.id = "test/" + i + i++ + AllImageProviders.LoadImagesFor(new UIEventSource(tags)).addCallbackD((images) => { + console.log("ImageProvider test", tags.id, "for", tags) + const img = images[0] + if (img === undefined) { + throw "No image found" } - ) - - expects("https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8HQ3DrfU76tWMC602spvM_e_rqOHyiUcYUTetXM7K52DDBEY5J4FWg4WKQqVUlMsWJn4nLXk0pxlBLx31146FqZ2Kg65z7lJUfR6wpW6WPSR5_y7RKdv4YEuzPjwIN0lagBnQONV3UjmXnEGpMouU?stp=s1024x768&ccb=10-5&oh=d460b401c505714ee1cb8bd6baf8ae5d&oe=61731FC3&_nc_sid=122ab1", { - "mapillary": "https://www.mapillary.com/app/?pKey=196804715753265" + expect(img.url).deep.equal(url) + if (providerName) { + expect(providerName).deep.equal(img.provider.constructor.name) + } + console.log("OK") }) + } + const muntpoort_expected = + "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABr%C3%BCgge-Muntpoort_6-29510-58192.jpg?width=500&height=400" + expects( + muntpoort_expected, + { + wikimedia_commons: "File:Brügge-Muntpoort_6-29510-58192.jpg", + }, + "WikimediaImageProvider" + ) - }) -}) \ No newline at end of file + expects( + muntpoort_expected, + { + wikimedia_commons: + "https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg", + }, + "WikimediaImageProvider" + ) + + expects( + muntpoort_expected, + { + image: "https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg", + }, + "WikimediaImageProvider" + ) + + expects( + "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400", + { + image: "File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg", + }, + "WikimediaImageProvider" + ) + + expects( + "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400", + { + wikimedia_commons: "File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg", + }, + "WikimediaImageProvider" + ) + + expects( + "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABrugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg?width=500&height=400", + { + image: "File:Brugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg", + }, + "WikimediaImageProvider" + ) + + expects( + "https://commons.wikimedia.org/wiki/Special:FilePath/File%3APapageno_Jef_Claerhout.jpg?width=500&height=400", + { + wikimedia_commons: "File:Papageno_Jef_Claerhout.jpg", + }, + "WikimediaImageProvider" + ) + + Utils.injectJsonDownloadForTests( + "https://graph.mapillary.com/196804715753265?fields=thumb_1024_url&&access_token=MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85", + { + thumb_1024_url: + "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8HQ3DrfU76tWMC602spvM_e_rqOHyiUcYUTetXM7K52DDBEY5J4FWg4WKQqVUlMsWJn4nLXk0pxlBLx31146FqZ2Kg65z7lJUfR6wpW6WPSR5_y7RKdv4YEuzPjwIN0lagBnQONV3UjmXnEGpMouU?stp=s1024x768&ccb=10-5&oh=d460b401c505714ee1cb8bd6baf8ae5d&oe=61731FC3&_nc_sid=122ab1", + id: "196804715753265", + } + ) + + expects( + "https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/An8HQ3DrfU76tWMC602spvM_e_rqOHyiUcYUTetXM7K52DDBEY5J4FWg4WKQqVUlMsWJn4nLXk0pxlBLx31146FqZ2Kg65z7lJUfR6wpW6WPSR5_y7RKdv4YEuzPjwIN0lagBnQONV3UjmXnEGpMouU?stp=s1024x768&ccb=10-5&oh=d460b401c505714ee1cb8bd6baf8ae5d&oe=61731FC3&_nc_sid=122ab1", + { + mapillary: "https://www.mapillary.com/app/?pKey=196804715753265", + } + ) + }) +}) diff --git a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts index 3fa54a139..3153c13e8 100644 --- a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts +++ b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts @@ -1,638 +1,675 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../../../Utils"; -import {OsmObject, OsmRelation} from "../../../../Logic/Osm/OsmObject"; -import {InPlaceReplacedmentRTSH, TurnRestrictionRSH} from "../../../../Logic/Osm/Actions/RelationSplitHandler"; -import {Changes} from "../../../../Logic/Osm/Changes"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../../../Utils" +import { OsmObject, OsmRelation } from "../../../../Logic/Osm/OsmObject" +import { + InPlaceReplacedmentRTSH, + TurnRestrictionRSH, +} from "../../../../Logic/Osm/Actions/RelationSplitHandler" +import { Changes } from "../../../../Logic/Osm/Changes" describe("RelationSplitHandler", () => { - - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/node/1124134958/ways", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (2937646 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "way", - "id": 97038428, - "timestamp": "2019-06-19T12:26:24Z", - "version": 6, - "changeset": 71399984, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [1124134958, 323729212, 323729351, 2542460408, 187073405], - "tags": { - "highway": "residential", - "name": "Brugs-Kerkhofstraat", + Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/1124134958/ways", { + version: "0.6", + generator: "CGImap 0.8.5 (2937646 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "way", + id: 97038428, + timestamp: "2019-06-19T12:26:24Z", + version: 6, + changeset: 71399984, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1124134958, 323729212, 323729351, 2542460408, 187073405], + tags: { + highway: "residential", + name: "Brugs-Kerkhofstraat", "sett:pattern": "arc", - "surface": "sett" - } - }, { - "type": "way", - "id": 97038434, - "timestamp": "2019-06-19T12:26:24Z", - "version": 5, - "changeset": 71399984, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [1124134958, 1124135024, 187058607], - "tags": { - "bicycle": "use_sidepath", - "highway": "residential", - "name": "Kerkhofblommenstraat", + surface: "sett", + }, + }, + { + type: "way", + id: 97038434, + timestamp: "2019-06-19T12:26:24Z", + version: 5, + changeset: 71399984, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1124134958, 1124135024, 187058607], + tags: { + bicycle: "use_sidepath", + highway: "residential", + name: "Kerkhofblommenstraat", "sett:pattern": "arc", - "surface": "sett" - } - }, { - "type": "way", - "id": 97038435, - "timestamp": "2017-12-21T21:41:08Z", - "version": 4, - "changeset": 54826837, - "user": "Jakka", - "uid": 2403313, - "nodes": [1124134958, 2576628889, 1124135035, 5298371485, 5298371495], - "tags": {"bicycle": "use_sidepath", "highway": "residential", "name": "Kerkhofblommenstraat"} - }, { - "type": "way", - "id": 251446313, - "timestamp": "2019-01-07T19:22:47Z", - "version": 4, - "changeset": 66106872, - "user": "M!dgard", - "uid": 763799, - "nodes": [1124134958, 5243143198, 4555715455], - "tags": {"foot": "yes", "highway": "service"} - }] - } - ) + surface: "sett", + }, + }, + { + type: "way", + id: 97038435, + timestamp: "2017-12-21T21:41:08Z", + version: 4, + changeset: 54826837, + user: "Jakka", + uid: 2403313, + nodes: [1124134958, 2576628889, 1124135035, 5298371485, 5298371495], + tags: { + bicycle: "use_sidepath", + highway: "residential", + name: "Kerkhofblommenstraat", + }, + }, + { + type: "way", + id: 251446313, + timestamp: "2019-01-07T19:22:47Z", + version: 4, + changeset: 66106872, + user: "M!dgard", + uid: 763799, + nodes: [1124134958, 5243143198, 4555715455], + tags: { foot: "yes", highway: "service" }, + }, + ], + }) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/relation/9572808/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (3128319 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "relation", - "id": 9572808, - "timestamp": "2021-08-12T12:44:06Z", - "version": 11, - "changeset": 109573204, - "user": "A67-A67", - "uid": 553736, - "members": [{"type": "way", "ref": 173662702, "role": ""}, { - "type": "way", - "ref": 467606230, - "role": "" - }, {"type": "way", "ref": 126267167, "role": ""}, { - "type": "way", - "ref": 301897426, - "role": "" - }, {"type": "way", "ref": 687866206, "role": ""}, { - "type": "way", - "ref": 295132739, - "role": "" - }, {"type": "way", "ref": 690497698, "role": ""}, { - "type": "way", - "ref": 627893684, - "role": "" - }, {"type": "way", "ref": 295132741, "role": ""}, { - "type": "way", - "ref": 301903120, - "role": "" - }, {"type": "way", "ref": 672541156, "role": ""}, { - "type": "way", - "ref": 126264330, - "role": "" - }, {"type": "way", "ref": 280440853, "role": ""}, { - "type": "way", - "ref": 838499667, - "role": "" - }, {"type": "way", "ref": 838499663, "role": ""}, { - "type": "way", - "ref": 690497623, - "role": "" - }, {"type": "way", "ref": 301902946, "role": ""}, { - "type": "way", - "ref": 280460715, - "role": "" - }, {"type": "way", "ref": 972534369, "role": ""}, { - "type": "way", - "ref": 695680702, - "role": "" - }, {"type": "way", "ref": 690497860, "role": ""}, { - "type": "way", - "ref": 295410363, - "role": "" - }, {"type": "way", "ref": 823864063, "role": ""}, { - "type": "way", - "ref": 663172088, - "role": "" - }, {"type": "way", "ref": 659950322, "role": ""}, { - "type": "way", - "ref": 659950323, - "role": "" - }, {"type": "way", "ref": 230180094, "role": ""}, { - "type": "way", - "ref": 690497912, - "role": "" - }, {"type": "way", "ref": 39588765, "role": ""}], - "tags": { - "distance": "13 km", - "name": "Abdijenroute", - "network": "lcn", - "old_name": "Spoorlijn 58", - "operator": "Toerisme West-Vlaanderen", - "railway": "abandoned", - "route": "bicycle", - "type": "route", - "wikipedia": "nl:Spoorlijn 58" - } - }] + version: "0.6", + generator: "CGImap 0.8.5 (3128319 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "relation", + id: 9572808, + timestamp: "2021-08-12T12:44:06Z", + version: 11, + changeset: 109573204, + user: "A67-A67", + uid: 553736, + members: [ + { type: "way", ref: 173662702, role: "" }, + { + type: "way", + ref: 467606230, + role: "", + }, + { type: "way", ref: 126267167, role: "" }, + { + type: "way", + ref: 301897426, + role: "", + }, + { type: "way", ref: 687866206, role: "" }, + { + type: "way", + ref: 295132739, + role: "", + }, + { type: "way", ref: 690497698, role: "" }, + { + type: "way", + ref: 627893684, + role: "", + }, + { type: "way", ref: 295132741, role: "" }, + { + type: "way", + ref: 301903120, + role: "", + }, + { type: "way", ref: 672541156, role: "" }, + { + type: "way", + ref: 126264330, + role: "", + }, + { type: "way", ref: 280440853, role: "" }, + { + type: "way", + ref: 838499667, + role: "", + }, + { type: "way", ref: 838499663, role: "" }, + { + type: "way", + ref: 690497623, + role: "", + }, + { type: "way", ref: 301902946, role: "" }, + { + type: "way", + ref: 280460715, + role: "", + }, + { type: "way", ref: 972534369, role: "" }, + { + type: "way", + ref: 695680702, + role: "", + }, + { type: "way", ref: 690497860, role: "" }, + { + type: "way", + ref: 295410363, + role: "", + }, + { type: "way", ref: 823864063, role: "" }, + { + type: "way", + ref: 663172088, + role: "", + }, + { type: "way", ref: 659950322, role: "" }, + { + type: "way", + ref: 659950323, + role: "", + }, + { type: "way", ref: 230180094, role: "" }, + { + type: "way", + ref: 690497912, + role: "", + }, + { type: "way", ref: 39588765, role: "" }, + ], + tags: { + distance: "13 km", + name: "Abdijenroute", + network: "lcn", + old_name: "Spoorlijn 58", + operator: "Toerisme West-Vlaanderen", + railway: "abandoned", + route: "bicycle", + type: "route", + wikipedia: "nl:Spoorlijn 58", + }, + }, + ], } ) - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/way/687866206/full", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (2601512 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 5273988959, - "lat": 51.1811406, - "lon": 3.2427712, - "timestamp": "2021-07-29T21:14:53Z", - "version": 6, - "changeset": 108847202, - "user": "kaart_fietser", - "uid": 11022240, - "tags": {"network:type": "node_network", "rwn_ref": "32"} - }, { - "type": "node", - "id": 6448669326, - "lat": 51.1811346, - "lon": 3.242891, - "timestamp": "2019-05-04T22:44:12Z", - "version": 1, - "changeset": 69891295, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "tags": {"barrier": "bollard"} - }, { - "type": "way", - "id": 687866206, - "timestamp": "2019-05-06T20:52:20Z", - "version": 2, - "changeset": 69951497, - "user": "noelbov", - "uid": 8054928, - "nodes": [6448669326, 5273988959], - "tags": { - "highway": "cycleway", - "name": "Abdijenroute", - "railway": "abandoned", - "surface": "asphalt" - } - }] - } - ) + Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/way/687866206/full", { + version: "0.6", + generator: "CGImap 0.8.5 (2601512 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 5273988959, + lat: 51.1811406, + lon: 3.2427712, + timestamp: "2021-07-29T21:14:53Z", + version: 6, + changeset: 108847202, + user: "kaart_fietser", + uid: 11022240, + tags: { "network:type": "node_network", rwn_ref: "32" }, + }, + { + type: "node", + id: 6448669326, + lat: 51.1811346, + lon: 3.242891, + timestamp: "2019-05-04T22:44:12Z", + version: 1, + changeset: 69891295, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { barrier: "bollard" }, + }, + { + type: "way", + id: 687866206, + timestamp: "2019-05-06T20:52:20Z", + version: 2, + changeset: 69951497, + user: "noelbov", + uid: 8054928, + nodes: [6448669326, 5273988959], + tags: { + highway: "cycleway", + name: "Abdijenroute", + railway: "abandoned", + surface: "asphalt", + }, + }, + ], + }) - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/way/690497698/full", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (3023311 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 170497152, - "lat": 51.1832353, - "lon": 3.2498759, - "timestamp": "2018-04-24T00:29:37Z", - "version": 7, - "changeset": 58357376, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 2988218625, - "lat": 51.1835053, - "lon": 3.2503067, - "timestamp": "2018-09-24T21:48:46Z", - "version": 2, - "changeset": 62895918, - "user": "A67-A67", - "uid": 553736 - }, { - "type": "node", - "id": 5273988967, - "lat": 51.182659, - "lon": 3.249004, - "timestamp": "2017-12-09T18:40:21Z", - "version": 1, - "changeset": 54493533, - "user": "CacherB", - "uid": 1999108 - }, { - "type": "way", - "id": 690497698, - "timestamp": "2021-07-29T21:14:53Z", - "version": 3, - "changeset": 108847202, - "user": "kaart_fietser", - "uid": 11022240, - "nodes": [2988218625, 170497152, 5273988967], - "tags": { - "highway": "cycleway", - "lit": "no", - "name": "Abdijenroute", - "oneway": "no", - "railway": "abandoned", - "surface": "compacted" - } - }] - } - ) + Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/way/690497698/full", { + version: "0.6", + generator: "CGImap 0.8.5 (3023311 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 170497152, + lat: 51.1832353, + lon: 3.2498759, + timestamp: "2018-04-24T00:29:37Z", + version: 7, + changeset: 58357376, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 2988218625, + lat: 51.1835053, + lon: 3.2503067, + timestamp: "2018-09-24T21:48:46Z", + version: 2, + changeset: 62895918, + user: "A67-A67", + uid: 553736, + }, + { + type: "node", + id: 5273988967, + lat: 51.182659, + lon: 3.249004, + timestamp: "2017-12-09T18:40:21Z", + version: 1, + changeset: 54493533, + user: "CacherB", + uid: 1999108, + }, + { + type: "way", + id: 690497698, + timestamp: "2021-07-29T21:14:53Z", + version: 3, + changeset: 108847202, + user: "kaart_fietser", + uid: 11022240, + nodes: [2988218625, 170497152, 5273988967], + tags: { + highway: "cycleway", + lit: "no", + name: "Abdijenroute", + oneway: "no", + railway: "abandoned", + surface: "compacted", + }, + }, + ], + }) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/relation/4374576/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (1266692 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "relation", - "id": 4374576, - "timestamp": "2014-12-23T21:42:27Z", - "version": 2, - "changeset": 27660623, - "user": "escada", - "uid": 436365, - "members": [{"type": "way", "ref": 318616190, "role": "from"}, { - "type": "node", - "ref": 1407529979, - "role": "via" - }, {"type": "way", "ref": 143298912, "role": "to"}], - "tags": {"restriction": "no_right_turn", "type": "restriction"} - }] - } - ) - - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/way/143298912/full", - { - "version": "0.6", - "generator": "CGImap 0.8.5 (4046166 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 26343912, - "lat": 51.2146847, - "lon": 3.2397007, - "timestamp": "2015-04-11T10:40:56Z", - "version": 5, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 26343913, - "lat": 51.2161912, - "lon": 3.2386907, - "timestamp": "2015-04-11T10:40:56Z", - "version": 6, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 26343914, - "lat": 51.2193456, - "lon": 3.2360696, - "timestamp": "2015-04-11T10:40:56Z", - "version": 5, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 26343915, - "lat": 51.2202816, - "lon": 3.2352429, - "timestamp": "2015-04-11T10:40:56Z", - "version": 5, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 875668688, - "lat": 51.2131868, - "lon": 3.2406009, - "timestamp": "2015-04-11T10:40:56Z", - "version": 4, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 1109632153, - "lat": 51.2207068, - "lon": 3.234882, - "timestamp": "2015-04-11T10:40:55Z", - "version": 3, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 1109632154, - "lat": 51.220784, - "lon": 3.2348394, - "timestamp": "2021-05-30T08:01:17Z", - "version": 4, - "changeset": 105557550, - "user": "albertino", - "uid": 499281 - }, { - "type": "node", - "id": 1109632177, - "lat": 51.2205082, - "lon": 3.2350441, - "timestamp": "2015-04-11T10:40:55Z", - "version": 3, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 1407529961, - "lat": 51.2168476, - "lon": 3.2381772, - "timestamp": "2015-04-11T10:40:55Z", - "version": 2, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 1407529969, - "lat": 51.2155155, - "lon": 3.23917, - "timestamp": "2011-08-21T20:08:27Z", - "version": 1, - "changeset": 9088257, - "user": "toeklk", - "uid": 219908 - }, { - "type": "node", - "id": 1407529979, - "lat": 51.212694, - "lon": 3.2409595, - "timestamp": "2015-04-11T10:40:55Z", - "version": 6, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799, - "tags": {"highway": "traffic_signals"} - }, { - "type": "node", - "id": 1634435395, - "lat": 51.2129189, - "lon": 3.2408257, - "timestamp": "2012-02-15T19:37:51Z", - "version": 1, - "changeset": 10695640, - "user": "Eimai", - "uid": 6072 - }, { - "type": "node", - "id": 1634435396, - "lat": 51.2132508, - "lon": 3.2405417, - "timestamp": "2012-02-15T19:37:51Z", - "version": 1, - "changeset": 10695640, - "user": "Eimai", - "uid": 6072 - }, { - "type": "node", - "id": 1634435397, - "lat": 51.2133918, - "lon": 3.2404416, - "timestamp": "2015-04-11T10:40:55Z", - "version": 2, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 1974988033, - "lat": 51.2127459, - "lon": 3.240928, - "timestamp": "2012-10-20T12:24:13Z", - "version": 1, - "changeset": 13566903, - "user": "skyman81", - "uid": 955688 - }, { - "type": "node", - "id": 3250129361, - "lat": 51.2127906, - "lon": 3.2409016, - "timestamp": "2018-12-19T00:00:33Z", - "version": 2, - "changeset": 65596519, - "user": "beardhatcode", - "uid": 5439560, - "tags": {"crossing": "traffic_signals", "highway": "crossing"} - }, { - "type": "node", - "id": 3250129363, - "lat": 51.2149189, - "lon": 3.2395571, - "timestamp": "2015-04-11T10:40:56Z", - "version": 2, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 3450326133, - "lat": 51.2139571, - "lon": 3.2401205, - "timestamp": "2015-04-11T10:40:26Z", - "version": 1, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 3450326135, - "lat": 51.2181385, - "lon": 3.2370893, - "timestamp": "2015-04-11T10:40:26Z", - "version": 1, - "changeset": 30139621, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 4794847239, - "lat": 51.2191224, - "lon": 3.2362584, - "timestamp": "2019-08-27T23:07:05Z", - "version": 2, - "changeset": 73816461, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 8493044168, - "lat": 51.2130348, - "lon": 3.2407284, - "timestamp": "2021-03-06T21:52:51Z", - "version": 1, - "changeset": 100555232, - "user": "kaart_fietser", - "uid": 11022240, - "tags": {"highway": "traffic_signals", "traffic_signals": "traffic_lights"} - }, { - "type": "node", - "id": 8792687918, - "lat": 51.2207505, - "lon": 3.2348579, - "timestamp": "2021-06-02T18:27:15Z", - "version": 1, - "changeset": 105735092, - "user": "albertino", - "uid": 499281 - }, { - "type": "way", - "id": 143298912, - "timestamp": "2021-06-02T18:27:15Z", - "version": 15, - "changeset": 105735092, - "user": "albertino", - "uid": 499281, - "nodes": [1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, 1634435396, 1634435397, 3450326133, 26343912, 3250129363, 1407529969, 26343913, 1407529961, 3450326135, 4794847239, 26343914, 26343915, 1109632177, 1109632153, 8792687918, 1109632154], - "tags": { - "cycleway:right": "track", - "highway": "primary", - "lanes": "2", - "lit": "yes", - "maxspeed": "70", - "name": "Buiten Kruisvest", - "oneway": "yes", - "ref": "R30", - "surface": "asphalt", - "wikipedia": "nl:Buiten Kruisvest" - } - }] - } - ) - - - - - it("should split all cycling relation (split 295132739)", - async () => { - // Lets mimic a split action of https://www.openstreetmap.org/way/295132739 - - const relation: OsmRelation = <OsmRelation>await OsmObject.DownloadObjectAsync("relation/9572808") - const originalNodeIds = [5273988967, - 170497153, - 1507524582, - 4524321710, - 170497155, - 170497157, - 170497158, - 3208166179, - 1507524610, - 170497160, - 3208166178, - 1507524573, - 1575932830, - 6448669326] - - const withSplit = [[5273988967, - 170497153, - 1507524582, - 4524321710, - 170497155, - 170497157, - 170497158], - [ - 3208166179, - 1507524610, - 170497160, - 3208166178, - 1507524573, - 1575932830, - 6448669326]] - - const splitter = new InPlaceReplacedmentRTSH( + version: "0.6", + generator: "CGImap 0.8.5 (1266692 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ { - relation: relation, - originalWayId: 295132739, - allWayIdsInOrder: [295132739, -1], - originalNodes: originalNodeIds, - allWaysNodesInOrder: withSplit - }, "no-theme") - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - const allIds = changeDescription[0].changes["members"].map(m => m.ref).join(",") - const expected = "687866206,295132739,-1,690497698" - expect(allIds.indexOf(expected) >= 0, "didn't find the expected order of ids in the relation to test").true - }) - - it( - "should split turn restrictions (split of https://www.openstreetmap.org/way/143298912)", - async () => { + type: "relation", + id: 4374576, + timestamp: "2014-12-23T21:42:27Z", + version: 2, + changeset: 27660623, + user: "escada", + uid: 436365, + members: [ + { type: "way", ref: 318616190, role: "from" }, + { + type: "node", + ref: 1407529979, + role: "via", + }, + { type: "way", ref: 143298912, role: "to" }, + ], + tags: { restriction: "no_right_turn", type: "restriction" }, + }, + ], + } + ) - const relation: OsmRelation = <OsmRelation>await OsmObject.DownloadObjectAsync("relation/4374576") - const originalNodeIds = - [ - 1407529979, - 1974988033, - 3250129361, - 1634435395, - 8493044168, - 875668688, - 1634435396, - 1634435397, - 3450326133, - 26343912, - 3250129363, - 1407529969, - 26343913, - 1407529961, - 3450326135, - 4794847239, - 26343914, - 26343915, - 1109632177, - 1109632153, - 8792687918, - 1109632154 + Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/way/143298912/full", { + version: "0.6", + generator: "CGImap 0.8.5 (4046166 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 26343912, + lat: 51.2146847, + lon: 3.2397007, + timestamp: "2015-04-11T10:40:56Z", + version: 5, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 26343913, + lat: 51.2161912, + lon: 3.2386907, + timestamp: "2015-04-11T10:40:56Z", + version: 6, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 26343914, + lat: 51.2193456, + lon: 3.2360696, + timestamp: "2015-04-11T10:40:56Z", + version: 5, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 26343915, + lat: 51.2202816, + lon: 3.2352429, + timestamp: "2015-04-11T10:40:56Z", + version: 5, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 875668688, + lat: 51.2131868, + lon: 3.2406009, + timestamp: "2015-04-11T10:40:56Z", + version: 4, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 1109632153, + lat: 51.2207068, + lon: 3.234882, + timestamp: "2015-04-11T10:40:55Z", + version: 3, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 1109632154, + lat: 51.220784, + lon: 3.2348394, + timestamp: "2021-05-30T08:01:17Z", + version: 4, + changeset: 105557550, + user: "albertino", + uid: 499281, + }, + { + type: "node", + id: 1109632177, + lat: 51.2205082, + lon: 3.2350441, + timestamp: "2015-04-11T10:40:55Z", + version: 3, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 1407529961, + lat: 51.2168476, + lon: 3.2381772, + timestamp: "2015-04-11T10:40:55Z", + version: 2, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 1407529969, + lat: 51.2155155, + lon: 3.23917, + timestamp: "2011-08-21T20:08:27Z", + version: 1, + changeset: 9088257, + user: "toeklk", + uid: 219908, + }, + { + type: "node", + id: 1407529979, + lat: 51.212694, + lon: 3.2409595, + timestamp: "2015-04-11T10:40:55Z", + version: 6, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + tags: { highway: "traffic_signals" }, + }, + { + type: "node", + id: 1634435395, + lat: 51.2129189, + lon: 3.2408257, + timestamp: "2012-02-15T19:37:51Z", + version: 1, + changeset: 10695640, + user: "Eimai", + uid: 6072, + }, + { + type: "node", + id: 1634435396, + lat: 51.2132508, + lon: 3.2405417, + timestamp: "2012-02-15T19:37:51Z", + version: 1, + changeset: 10695640, + user: "Eimai", + uid: 6072, + }, + { + type: "node", + id: 1634435397, + lat: 51.2133918, + lon: 3.2404416, + timestamp: "2015-04-11T10:40:55Z", + version: 2, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 1974988033, + lat: 51.2127459, + lon: 3.240928, + timestamp: "2012-10-20T12:24:13Z", + version: 1, + changeset: 13566903, + user: "skyman81", + uid: 955688, + }, + { + type: "node", + id: 3250129361, + lat: 51.2127906, + lon: 3.2409016, + timestamp: "2018-12-19T00:00:33Z", + version: 2, + changeset: 65596519, + user: "beardhatcode", + uid: 5439560, + tags: { crossing: "traffic_signals", highway: "crossing" }, + }, + { + type: "node", + id: 3250129363, + lat: 51.2149189, + lon: 3.2395571, + timestamp: "2015-04-11T10:40:56Z", + version: 2, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 3450326133, + lat: 51.2139571, + lon: 3.2401205, + timestamp: "2015-04-11T10:40:26Z", + version: 1, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 3450326135, + lat: 51.2181385, + lon: 3.2370893, + timestamp: "2015-04-11T10:40:26Z", + version: 1, + changeset: 30139621, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 4794847239, + lat: 51.2191224, + lon: 3.2362584, + timestamp: "2019-08-27T23:07:05Z", + version: 2, + changeset: 73816461, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 8493044168, + lat: 51.2130348, + lon: 3.2407284, + timestamp: "2021-03-06T21:52:51Z", + version: 1, + changeset: 100555232, + user: "kaart_fietser", + uid: 11022240, + tags: { highway: "traffic_signals", traffic_signals: "traffic_lights" }, + }, + { + type: "node", + id: 8792687918, + lat: 51.2207505, + lon: 3.2348579, + timestamp: "2021-06-02T18:27:15Z", + version: 1, + changeset: 105735092, + user: "albertino", + uid: 499281, + }, + { + type: "way", + id: 143298912, + timestamp: "2021-06-02T18:27:15Z", + version: 15, + changeset: 105735092, + user: "albertino", + uid: 499281, + nodes: [ + 1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, + 1634435396, 1634435397, 3450326133, 26343912, 3250129363, 1407529969, 26343913, + 1407529961, 3450326135, 4794847239, 26343914, 26343915, 1109632177, 1109632153, + 8792687918, 1109632154, + ], + tags: { + "cycleway:right": "track", + highway: "primary", + lanes: "2", + lit: "yes", + maxspeed: "70", + name: "Buiten Kruisvest", + oneway: "yes", + ref: "R30", + surface: "asphalt", + wikipedia: "nl:Buiten Kruisvest", + }, + }, + ], + }) - ] + it("should split all cycling relation (split 295132739)", async () => { + // Lets mimic a split action of https://www.openstreetmap.org/way/295132739 - const withSplit = [[ + const relation: OsmRelation = <OsmRelation>( + await OsmObject.DownloadObjectAsync("relation/9572808") + ) + const originalNodeIds = [ + 5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158, + 3208166179, 1507524610, 170497160, 3208166178, 1507524573, 1575932830, 6448669326, + ] + + const withSplit = [ + [5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158], + [3208166179, 1507524610, 170497160, 3208166178, 1507524573, 1575932830, 6448669326], + ] + + const splitter = new InPlaceReplacedmentRTSH( + { + relation: relation, + originalWayId: 295132739, + allWayIdsInOrder: [295132739, -1], + originalNodes: originalNodeIds, + allWaysNodesInOrder: withSplit, + }, + "no-theme" + ) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",") + const expected = "687866206,295132739,-1,690497698" + expect( + allIds.indexOf(expected) >= 0, + "didn't find the expected order of ids in the relation to test" + ).true + }) + + it("should split turn restrictions (split of https://www.openstreetmap.org/way/143298912)", async () => { + const relation: OsmRelation = <OsmRelation>( + await OsmObject.DownloadObjectAsync("relation/4374576") + ) + const originalNodeIds = [ + 1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, 1634435396, + 1634435397, 3450326133, 26343912, 3250129363, 1407529969, 26343913, 1407529961, + 3450326135, 4794847239, 26343914, 26343915, 1109632177, 1109632153, 8792687918, + 1109632154, + ] + + const withSplit = [ + [ 1407529979, // The via point 1974988033, 3250129361, @@ -645,46 +682,43 @@ describe("RelationSplitHandler", () => { 26343912, 3250129363, 1407529969, - 26343913], - [ - 1407529961, - 3450326135, - 4794847239, - 26343914, - 26343915, - 1109632177, - 1109632153, - 8792687918, - 1109632154 + 26343913, + ], + [ + 1407529961, 3450326135, 4794847239, 26343914, 26343915, 1109632177, 1109632153, + 8792687918, 1109632154, + ], + ] - ]] + const splitter = new TurnRestrictionRSH( + { + relation: relation, + originalWayId: 143298912, + allWayIdsInOrder: [-1, 143298912], + originalNodes: originalNodeIds, + allWaysNodesInOrder: withSplit, + }, + "no-theme" + ) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const allIds = changeDescription[0].changes["members"] + .map((m) => m.type + "/" + m.ref + "-->" + m.role) + .join(",") + const expected = "way/318616190-->from,node/1407529979-->via,way/-1-->to" + expect(allIds).deep.equal(expected) - const splitter = new TurnRestrictionRSH( - { - relation: relation, - originalWayId: 143298912, - allWayIdsInOrder: [-1, 143298912], - originalNodes: originalNodeIds, - allWaysNodesInOrder: withSplit - }, "no-theme") - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - const allIds = changeDescription[0].changes["members"].map(m => m.type + "/" + m.ref + "-->" + m.role).join(",") - const expected = "way/318616190-->from,node/1407529979-->via,way/-1-->to" - expect(allIds).deep.equal(expected) - - - // Reversing the ids has no effect - const splitterReverse = new TurnRestrictionRSH( - { - relation: relation, - originalWayId: 143298912, - allWayIdsInOrder: [143298912, -1], - originalNodes: originalNodeIds, - allWaysNodesInOrder: withSplit - }, "no-theme") - const changesReverse = await splitterReverse.CreateChangeDescriptions(new Changes()) - expect(changesReverse.length).deep.equal(0) - - } - ) + // Reversing the ids has no effect + const splitterReverse = new TurnRestrictionRSH( + { + relation: relation, + originalWayId: 143298912, + allWayIdsInOrder: [143298912, -1], + originalNodes: originalNodeIds, + allWaysNodesInOrder: withSplit, + }, + "no-theme" + ) + const changesReverse = await splitterReverse.CreateChangeDescriptions(new Changes()) + expect(changesReverse.length).deep.equal(0) + }) }) diff --git a/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts b/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts index d24d39db1..58fb88b0d 100644 --- a/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts +++ b/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts @@ -1,292 +1,274 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import Minimap from "../../../../UI/Base/Minimap"; -import {Utils} from "../../../../Utils"; -import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig"; -import State from "../../../../State"; -import {BBox} from "../../../../Logic/BBox"; -import ReplaceGeometryAction from "../../../../Logic/Osm/Actions/ReplaceGeometryAction"; -import ShowDataLayerImplementation from "../../../../UI/ShowDataLayer/ShowDataLayerImplementation"; -import ShowDataLayer from "../../../../UI/ShowDataLayer/ShowDataLayer"; +import { describe } from "mocha" +import { expect } from "chai" +import Minimap from "../../../../UI/Base/Minimap" +import { Utils } from "../../../../Utils" +import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig" +import State from "../../../../State" +import { BBox } from "../../../../Logic/BBox" +import ReplaceGeometryAction from "../../../../Logic/Osm/Actions/ReplaceGeometryAction" +import ShowDataLayerImplementation from "../../../../UI/ShowDataLayer/ShowDataLayerImplementation" +import ShowDataLayer from "../../../../UI/ShowDataLayer/ShowDataLayer" describe("ReplaceGeometryAction", () => { - - const grbStripped = { - "id": "grb", - "title": { - "nl": "GRB import helper" + const grbStripped = { + id: "grb", + title: { + nl: "GRB import helper", }, - "description": "Smaller version of the GRB theme", - "language": [ - "nl", - "en" - ], + description: "Smaller version of the GRB theme", + language: ["nl", "en"], socialImage: "img.jpg", - "version": "0", - "startLat": 51.0249, - "startLon": 4.026489, - "startZoom": 9, - "clustering": false, - "overrideAll": { - "minzoom": 19 + version: "0", + startLat: 51.0249, + startLon: 4.026489, + startZoom: 9, + clustering: false, + overrideAll: { + minzoom: 19, }, - "layers": [ + layers: [ { - "id": "type_node", + id: "type_node", source: { - osmTags: "type=node" + osmTags: "type=node", }, mapRendering: null, - "override": { - "calculatedTags": [ + override: { + calculatedTags: [ "_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false", "_is_part_of_grb_building=feat.get('parent_ways')?.some(p => p['source:geometry:ref'] !== undefined) ?? false", "_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false", "_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)", "_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false", - "_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')" + "_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')", ], - "mapRendering": [ + mapRendering: [ { - "icon": "square:#cc0", - "iconSize": "5,5,center", - "location": [ - "point" - ] - } + icon: "square:#cc0", + iconSize: "5,5,center", + location: ["point"], + }, ], - "passAllFeatures": true - } + passAllFeatures: true, + }, }, { - "id": "osm-buildings", - "name": "All OSM-buildings", - "source": { - "osmTags": "building~*", - "maxCacheAge": 0 + id: "osm-buildings", + name: "All OSM-buildings", + source: { + osmTags: "building~*", + maxCacheAge: 0, }, - "calculatedTags": [ - "_surface:strict:=feat.get('_surface')" - ], - "mapRendering": [ + calculatedTags: ["_surface:strict:=feat.get('_surface')"], + mapRendering: [ { - "width": { - "render": "2", - "mappings": [ + width: { + render: "2", + mappings: [ { - "if": "fixme~*", - "then": "5" - } - ] + if: "fixme~*", + then: "5", + }, + ], }, - "color": { - "render": "#00c", - "mappings": [ + color: { + render: "#00c", + mappings: [ { - "if": "fixme~*", - "then": "#ff00ff" + if: "fixme~*", + then: "#ff00ff", }, { - "if": "building=house", - "then": "#a00" + if: "building=house", + then: "#a00", }, { - "if": "building=shed", - "then": "#563e02" + if: "building=shed", + then: "#563e02", }, { - "if": { - "or": [ - "building=garage", - "building=garages" - ] + if: { + or: ["building=garage", "building=garages"], }, - "then": "#f9bfbb" + then: "#f9bfbb", }, { - "if": "building=yes", - "then": "#0774f2" - } - ] - } - } - ], - "title": "OSM-gebouw", - "tagRenderings": [ - { - "id": "building type", - "freeform": { - "key": "building" - }, - "render": "The building type is <b>{building}</b>", - "question": { - "en": "What kind of building is this?" - }, - "mappings": [ - { - "if": "building=house", - "then": "A normal house" - }, - { - "if": "building=detached", - "then": "A house detached from other building" - }, - { - "if": "building=semidetached_house", - "then": "A house sharing only one wall with another house" - }, - { - "if": "building=apartments", - "then": "An apartment building - highrise for living" - }, - { - "if": "building=office", - "then": "An office building - highrise for work" - }, - { - "if": "building=apartments", - "then": "An apartment building" - }, - { - "if": "building=shed", - "then": "A small shed, e.g. in a garden" - }, - { - "if": "building=garage", - "then": "A single garage to park a car" - }, - { - "if": "building=garages", - "then": "A building containing only garages; typically they are all identical" - }, - { - "if": "building=yes", - "then": "A building - no specification" - } - ] - }, - { - "id": "grb-housenumber", - "render": { - "nl": "Het huisnummer is <b>{addr:housenumber}</b>" - }, - "question": { - "nl": "Wat is het huisnummer?" - }, - "freeform": { - "key": "addr:housenumber" - }, - "mappings": [ - { - "if": { - "and": [ - "not:addr:housenumber=yes", - "addr:housenumber=" - ] + if: "building=yes", + then: "#0774f2", }, - "then": { - "nl": "Geen huisnummer" - } - } - ] + ], + }, }, - { - "id": "grb-unit", - "question": "Wat is de wooneenheid-aanduiding?", - "render": { - "nl": "De wooneenheid-aanduiding is <b>{addr:unit}</b> " - }, - "freeform": { - "key": "addr:unit" - }, - "mappings": [ - { - "if": "addr:unit=", - "then": "Geen wooneenheid-nummer" - } - ] - }, - { - "id": "grb-street", - "render": { - "nl": "De straat is <b>{addr:street}</b>" - }, - "freeform": { - "key": "addr:street" - }, - "question": { - "nl": "Wat is de straat?" - } - }, - { - "id": "grb-fixme", - "render": { - "nl": "De fixme is <b>{fixme}</b>" - }, - "question": { - "nl": "Wat zegt de fixme?" - }, - "freeform": { - "key": "fixme" - }, - "mappings": [ - { - "if": { - "and": [ - "fixme=" - ] - }, - "then": { - "nl": "Geen fixme" - } - } - ] - }, - { - "id": "grb-min-level", - "render": { - "nl": "Dit gebouw begint maar op de {building:min_level} verdieping" - }, - "question": { - "nl": "Hoeveel verdiepingen ontbreken?" - }, - "freeform": { - "key": "building:min_level", - "type": "pnat" - } - }, - "all_tags" ], - "filter": [ + title: "OSM-gebouw", + tagRenderings: [ { - "id": "has-fixme", - "options": [ + id: "building type", + freeform: { + key: "building", + }, + render: "The building type is <b>{building}</b>", + question: { + en: "What kind of building is this?", + }, + mappings: [ { - "osmTags": "fixme~*", - "question": "Heeft een FIXME" - } - ] - } - ] + if: "building=house", + then: "A normal house", + }, + { + if: "building=detached", + then: "A house detached from other building", + }, + { + if: "building=semidetached_house", + then: "A house sharing only one wall with another house", + }, + { + if: "building=apartments", + then: "An apartment building - highrise for living", + }, + { + if: "building=office", + then: "An office building - highrise for work", + }, + { + if: "building=apartments", + then: "An apartment building", + }, + { + if: "building=shed", + then: "A small shed, e.g. in a garden", + }, + { + if: "building=garage", + then: "A single garage to park a car", + }, + { + if: "building=garages", + then: "A building containing only garages; typically they are all identical", + }, + { + if: "building=yes", + then: "A building - no specification", + }, + ], + }, + { + id: "grb-housenumber", + render: { + nl: "Het huisnummer is <b>{addr:housenumber}</b>", + }, + question: { + nl: "Wat is het huisnummer?", + }, + freeform: { + key: "addr:housenumber", + }, + mappings: [ + { + if: { + and: ["not:addr:housenumber=yes", "addr:housenumber="], + }, + then: { + nl: "Geen huisnummer", + }, + }, + ], + }, + { + id: "grb-unit", + question: "Wat is de wooneenheid-aanduiding?", + render: { + nl: "De wooneenheid-aanduiding is <b>{addr:unit}</b> ", + }, + freeform: { + key: "addr:unit", + }, + mappings: [ + { + if: "addr:unit=", + then: "Geen wooneenheid-nummer", + }, + ], + }, + { + id: "grb-street", + render: { + nl: "De straat is <b>{addr:street}</b>", + }, + freeform: { + key: "addr:street", + }, + question: { + nl: "Wat is de straat?", + }, + }, + { + id: "grb-fixme", + render: { + nl: "De fixme is <b>{fixme}</b>", + }, + question: { + nl: "Wat zegt de fixme?", + }, + freeform: { + key: "fixme", + }, + mappings: [ + { + if: { + and: ["fixme="], + }, + then: { + nl: "Geen fixme", + }, + }, + ], + }, + { + id: "grb-min-level", + render: { + nl: "Dit gebouw begint maar op de {building:min_level} verdieping", + }, + question: { + nl: "Hoeveel verdiepingen ontbreken?", + }, + freeform: { + key: "building:min_level", + type: "pnat", + }, + }, + "all_tags", + ], + filter: [ + { + id: "has-fixme", + options: [ + { + osmTags: "fixme~*", + question: "Heeft een FIXME", + }, + ], + }, + ], }, { - "id": "grb", - "description": "Geometry which comes from GRB with tools to import them", - "source": { - "osmTags": { - "and": [ - "HUISNR~*", - "man_made!=mast" - ] + id: "grb", + description: "Geometry which comes from GRB with tools to import them", + source: { + osmTags: { + and: ["HUISNR~*", "man_made!=mast"], }, - "geoJson": "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}", - "geoJsonZoomLevel": 18, - "mercatorCrs": true, - "maxCacheAge": 0 + geoJson: + "https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}", + geoJsonZoomLevel: 18, + mercatorCrs: true, + maxCacheAge: 0, }, - "name": "GRB geometries", - "title": "GRB outline", - "calculatedTags": [ + name: "GRB geometries", + title: "GRB outline", + calculatedTags: [ "_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", "_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''", "_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']", @@ -306,70 +288,34 @@ describe("ReplaceGeometryAction", () => { "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date", "_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)", "_building:min_level= feat.properties['fixme']?.startsWith('verdieping, correct the building tag, add building:level and building:min_level before upload in JOSM!') ? '1' : ''", - "_intersects_with_other_features=feat.intersectionsWith('generic_osm_object').map(f => \"<a href='https://osm.org/\"+f.feat.properties.id+\"' target='_blank'>\" + f.feat.properties.id + \"</a>\").join(', ')" + "_intersects_with_other_features=feat.intersectionsWith('generic_osm_object').map(f => \"<a href='https://osm.org/\"+f.feat.properties.id+\"' target='_blank'>\" + f.feat.properties.id + \"</a>\").join(', ')", ], - "tagRenderings": [], - "mapRendering": [ + tagRenderings: [], + mapRendering: [ { - "iconSize": "50,50,center", - "icon": "./assets/themes/grb_import/housenumber_blank.svg", - "location": [ - "point", - "centroid" - ] - } - ] - } - ] + iconSize: "50,50,center", + icon: "./assets/themes/grb_import/housenumber_blank.svg", + location: ["point", "centroid"], + }, + ], + }, + ], } - Minimap.createMiniMap = () => undefined; + Minimap.createMiniMap = () => undefined const coordinates = <[number, number][]>[ - [ - 3.216690793633461, - 51.21474084112525 - ], - [ - 3.2167256623506546, - 51.214696737309964 - ], - [ - 3.2169999182224274, - 51.214768983537674 - ], - [ - 3.2169650495052338, - 51.21480720678671 - ], - [ - 3.2169368863105774, - 51.21480090625335 - ], - [ - 3.2169489562511444, - 51.21478074454077 - ], - [ - 3.216886594891548, - 51.214765203214625 - ], - [ - 3.2168812304735184, - 51.21477192378873 - ], - [ - 3.2168644666671753, - 51.214768983537674 - ], - [ - 3.2168537378311157, - 51.21478746511261 - ], - [ - 3.216690793633461, - 51.21474084112525 - ] + [3.216690793633461, 51.21474084112525], + [3.2167256623506546, 51.214696737309964], + [3.2169999182224274, 51.214768983537674], + [3.2169650495052338, 51.21480720678671], + [3.2169368863105774, 51.21480090625335], + [3.2169489562511444, 51.21478074454077], + [3.216886594891548, 51.214765203214625], + [3.2168812304735184, 51.21477192378873], + [3.2168644666671753, 51.214768983537674], + [3.2168537378311157, 51.21478746511261], + [3.216690793633461, 51.21474084112525], ] const targetFeature = { @@ -377,8 +323,8 @@ describe("ReplaceGeometryAction", () => { properties: {}, geometry: { type: "Polygon", - coordinates: [coordinates] - } + coordinates: [coordinates], + }, } const wayId = "way/160909312" @@ -386,544 +332,604 @@ describe("ReplaceGeometryAction", () => { Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/map.json?bbox=3.2166673243045807,51.21467321525788,3.217007964849472,51.21482442824023", { - "version": "0.6", - "generator": "CGImap 0.8.6 (1549677 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "bounds": {"minlat": 51.2146732, "minlon": 3.2166673, "maxlat": 51.2148244, "maxlon": 3.217008}, - "elements": [{ - "type": "node", - "id": 1612385157, - "lat": 51.2148016, - "lon": 3.2168453, - "timestamp": "2018-04-30T12:26:00Z", - "version": 3, - "changeset": 58553478, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728816256, - "lat": 51.2147111, - "lon": 3.2170233, - "timestamp": "2017-07-18T22:52:44Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728816287, - "lat": 51.2146408, - "lon": 3.2167601, - "timestamp": "2021-10-29T16:24:43Z", - "version": 3, - "changeset": 113131915, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728823481, - "lat": 51.2146968, - "lon": 3.2167242, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728823499, - "lat": 51.2147127, - "lon": 3.2170302, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823501, - "lat": 51.2148696, - "lon": 3.2168941, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823514, - "lat": 51.2147863, - "lon": 3.2168551, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728823522, - "lat": 51.2148489, - "lon": 3.2169012, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823523, - "lat": 51.2147578, - "lon": 3.2169995, - "timestamp": "2017-07-18T22:52:45Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823543, - "lat": 51.2148075, - "lon": 3.2166445, - "timestamp": "2017-07-18T22:52:46Z", - "version": 3, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823544, - "lat": 51.2148553, - "lon": 3.2169315, - "timestamp": "2017-07-18T22:52:46Z", - "version": 2, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 1728823549, - "lat": 51.2147401, - "lon": 3.2168877, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978288376, - "lat": 51.2147306, - "lon": 3.2168928, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978288381, - "lat": 51.2147638, - "lon": 3.2168856, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978288382, - "lat": 51.2148189, - "lon": 3.216912, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978288385, - "lat": 51.2148835, - "lon": 3.2170623, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978288387, - "lat": 51.2148904, - "lon": 3.2171037, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 4978289383, - "lat": 51.2147678, - "lon": 3.2169969, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289384, - "lat": 51.2147684, - "lon": 3.2168674, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289386, - "lat": 51.2147716, - "lon": 3.2168811, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289388, - "lat": 51.2148115, - "lon": 3.216966, - "timestamp": "2021-11-02T23:38:13Z", - "version": 7, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289391, - "lat": 51.2148019, - "lon": 3.2169194, - "timestamp": "2017-07-18T22:52:21Z", - "version": 1, - "changeset": 50391526, - "user": "catweazle67", - "uid": 1976209 - }, { - "type": "node", - "id": 9219974337, - "lat": 51.2148449, - "lon": 3.2171278, - "timestamp": "2021-11-02T23:40:52Z", - "version": 1, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979643, - "lat": 51.2147405, - "lon": 3.216693, - "timestamp": "2021-11-02T23:37:11Z", - "version": 1, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979646, - "lat": 51.2148043, - "lon": 3.2169312, - "timestamp": "2021-11-02T23:38:13Z", - "version": 2, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979647, - "lat": 51.2147792, - "lon": 3.2169466, - "timestamp": "2021-11-02T23:37:11Z", - "version": 1, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "way", - "id": 160909311, - "timestamp": "2021-12-23T12:03:37Z", - "version": 6, - "changeset": 115295690, - "user": "s8evq", - "uid": 3710738, - "nodes": [1728823481, 1728823549, 4978288376, 1728823523, 1728823499, 1728816256, 1728816287, 1728823481], - "tags": { - "addr:city": "Brugge", - "addr:country": "BE", - "addr:housenumber": "106", - "addr:postcode": "8000", - "addr:street": "Ezelstraat", - "building": "house", - "source:geometry:date": "2015-07-09", - "source:geometry:ref": "Gbg/2391617" - } - }, { - "type": "way", - "id": 160909312, - "timestamp": "2021-11-02T23:38:13Z", - "version": 4, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [9219979643, 1728823481, 1728823549, 4978289383, 4978289388, 9219979646, 9219979647, 4978288381, 4978289386, 4978289384, 1728823514, 9219979643], - "tags": { + version: "0.6", + generator: "CGImap 0.8.6 (1549677 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + bounds: { minlat: 51.2146732, minlon: 3.2166673, maxlat: 51.2148244, maxlon: 3.217008 }, + elements: [ + { + type: "node", + id: 1612385157, + lat: 51.2148016, + lon: 3.2168453, + timestamp: "2018-04-30T12:26:00Z", + version: 3, + changeset: 58553478, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728816256, + lat: 51.2147111, + lon: 3.2170233, + timestamp: "2017-07-18T22:52:44Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728816287, + lat: 51.2146408, + lon: 3.2167601, + timestamp: "2021-10-29T16:24:43Z", + version: 3, + changeset: 113131915, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728823481, + lat: 51.2146968, + lon: 3.2167242, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728823499, + lat: 51.2147127, + lon: 3.2170302, + timestamp: "2017-07-18T22:52:45Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823501, + lat: 51.2148696, + lon: 3.2168941, + timestamp: "2017-07-18T22:52:45Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823514, + lat: 51.2147863, + lon: 3.2168551, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728823522, + lat: 51.2148489, + lon: 3.2169012, + timestamp: "2017-07-18T22:52:45Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823523, + lat: 51.2147578, + lon: 3.2169995, + timestamp: "2017-07-18T22:52:45Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823543, + lat: 51.2148075, + lon: 3.2166445, + timestamp: "2017-07-18T22:52:46Z", + version: 3, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823544, + lat: 51.2148553, + lon: 3.2169315, + timestamp: "2017-07-18T22:52:46Z", + version: 2, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 1728823549, + lat: 51.2147401, + lon: 3.2168877, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978288376, + lat: 51.2147306, + lon: 3.2168928, + timestamp: "2017-07-18T22:52:21Z", + version: 1, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 4978288381, + lat: 51.2147638, + lon: 3.2168856, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978288382, + lat: 51.2148189, + lon: 3.216912, + timestamp: "2017-07-18T22:52:21Z", + version: 1, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 4978288385, + lat: 51.2148835, + lon: 3.2170623, + timestamp: "2017-07-18T22:52:21Z", + version: 1, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 4978288387, + lat: 51.2148904, + lon: 3.2171037, + timestamp: "2017-07-18T22:52:21Z", + version: 1, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 4978289383, + lat: 51.2147678, + lon: 3.2169969, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289384, + lat: 51.2147684, + lon: 3.2168674, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289386, + lat: 51.2147716, + lon: 3.2168811, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289388, + lat: 51.2148115, + lon: 3.216966, + timestamp: "2021-11-02T23:38:13Z", + version: 7, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289391, + lat: 51.2148019, + lon: 3.2169194, + timestamp: "2017-07-18T22:52:21Z", + version: 1, + changeset: 50391526, + user: "catweazle67", + uid: 1976209, + }, + { + type: "node", + id: 9219974337, + lat: 51.2148449, + lon: 3.2171278, + timestamp: "2021-11-02T23:40:52Z", + version: 1, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979643, + lat: 51.2147405, + lon: 3.216693, + timestamp: "2021-11-02T23:37:11Z", + version: 1, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979646, + lat: 51.2148043, + lon: 3.2169312, + timestamp: "2021-11-02T23:38:13Z", + version: 2, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979647, + lat: 51.2147792, + lon: 3.2169466, + timestamp: "2021-11-02T23:37:11Z", + version: 1, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "way", + id: 160909311, + timestamp: "2021-12-23T12:03:37Z", + version: 6, + changeset: 115295690, + user: "s8evq", + uid: 3710738, + nodes: [ + 1728823481, 1728823549, 4978288376, 1728823523, 1728823499, 1728816256, + 1728816287, 1728823481, + ], + tags: { + "addr:city": "Brugge", + "addr:country": "BE", + "addr:housenumber": "106", + "addr:postcode": "8000", + "addr:street": "Ezelstraat", + building: "house", + "source:geometry:date": "2015-07-09", + "source:geometry:ref": "Gbg/2391617", + }, + }, + { + type: "way", + id: 160909312, + timestamp: "2021-11-02T23:38:13Z", + version: 4, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [ + 9219979643, 1728823481, 1728823549, 4978289383, 4978289388, 9219979646, + 9219979647, 4978288381, 4978289386, 4978289384, 1728823514, 9219979643, + ], + tags: { + "addr:city": "Brugge", + "addr:country": "BE", + "addr:housenumber": "108", + "addr:postcode": "8000", + "addr:street": "Ezelstraat", + building: "house", + "source:geometry:date": "2018-10-02", + "source:geometry:ref": "Gbg/5926383", + }, + }, + { + type: "way", + id: 160909315, + timestamp: "2021-12-23T12:03:37Z", + version: 8, + changeset: 115295690, + user: "s8evq", + uid: 3710738, + nodes: [ + 1728823543, 1728823501, 1728823522, 4978288382, 1612385157, 1728823514, + 9219979643, 1728823543, + ], + tags: { + "addr:city": "Brugge", + "addr:country": "BE", + "addr:housenumber": "110", + "addr:postcode": "8000", + "addr:street": "Ezelstraat", + building: "house", + name: "La Style", + shop: "hairdresser", + "source:geometry:date": "2015-07-09", + "source:geometry:ref": "Gbg/5260837", + }, + }, + { + type: "way", + id: 508533816, + timestamp: "2021-12-23T12:03:37Z", + version: 7, + changeset: 115295690, + user: "s8evq", + uid: 3710738, + nodes: [ + 4978288387, 4978288385, 1728823544, 1728823522, 4978288382, 4978289391, + 9219979646, 4978289388, 9219974337, 4978288387, + ], + tags: { + building: "yes", + "source:geometry:date": "2015-07-09", + "source:geometry:ref": "Gbg/5260790", + }, + }, + ], + } + ) + + Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/way/160909312/full", { + version: "0.6", + generator: "CGImap 0.8.6 (2407324 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 1728823481, + lat: 51.2146968, + lon: 3.2167242, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728823514, + lat: 51.2147863, + lon: 3.2168551, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 1728823549, + lat: 51.2147401, + lon: 3.2168877, + timestamp: "2021-11-02T23:37:11Z", + version: 5, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978288381, + lat: 51.2147638, + lon: 3.2168856, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289383, + lat: 51.2147678, + lon: 3.2169969, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289384, + lat: 51.2147684, + lon: 3.2168674, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289386, + lat: 51.2147716, + lon: 3.2168811, + timestamp: "2021-11-02T23:37:11Z", + version: 4, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 4978289388, + lat: 51.2148115, + lon: 3.216966, + timestamp: "2021-11-02T23:38:13Z", + version: 7, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979643, + lat: 51.2147405, + lon: 3.216693, + timestamp: "2021-11-02T23:37:11Z", + version: 1, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979646, + lat: 51.2148043, + lon: 3.2169312, + timestamp: "2021-11-02T23:38:13Z", + version: 2, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 9219979647, + lat: 51.2147792, + lon: 3.2169466, + timestamp: "2021-11-02T23:37:11Z", + version: 1, + changeset: 113305401, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "way", + id: 160909312, + timestamp: "2021-11-02T23:38:13Z", + version: 4, + changeset: 113306325, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [ + 9219979643, 1728823481, 1728823549, 4978289383, 4978289388, 9219979646, + 9219979647, 4978288381, 4978289386, 4978289384, 1728823514, 9219979643, + ], + tags: { "addr:city": "Brugge", "addr:country": "BE", "addr:housenumber": "108", "addr:postcode": "8000", "addr:street": "Ezelstraat", - "building": "house", + building: "house", "source:geometry:date": "2018-10-02", - "source:geometry:ref": "Gbg/5926383" - } - }, { - "type": "way", - "id": 160909315, - "timestamp": "2021-12-23T12:03:37Z", - "version": 8, - "changeset": 115295690, - "user": "s8evq", - "uid": 3710738, - "nodes": [1728823543, 1728823501, 1728823522, 4978288382, 1612385157, 1728823514, 9219979643, 1728823543], - "tags": { - "addr:city": "Brugge", - "addr:country": "BE", - "addr:housenumber": "110", - "addr:postcode": "8000", - "addr:street": "Ezelstraat", - "building": "house", - "name": "La Style", - "shop": "hairdresser", - "source:geometry:date": "2015-07-09", - "source:geometry:ref": "Gbg/5260837" - } - }, { - "type": "way", - "id": 508533816, - "timestamp": "2021-12-23T12:03:37Z", - "version": 7, - "changeset": 115295690, - "user": "s8evq", - "uid": 3710738, - "nodes": [4978288387, 4978288385, 1728823544, 1728823522, 4978288382, 4978289391, 9219979646, 4978289388, 9219974337, 4978288387], - "tags": { - "building": "yes", - "source:geometry:date": "2015-07-09", - "source:geometry:ref": "Gbg/5260790" - } - }] - } - ) - - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/way/160909312/full", - { - "version": "0.6", - "generator": "CGImap 0.8.6 (2407324 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 1728823481, - "lat": 51.2146968, - "lon": 3.2167242, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728823514, - "lat": 51.2147863, - "lon": 3.2168551, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 1728823549, - "lat": 51.2147401, - "lon": 3.2168877, - "timestamp": "2021-11-02T23:37:11Z", - "version": 5, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978288381, - "lat": 51.2147638, - "lon": 3.2168856, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289383, - "lat": 51.2147678, - "lon": 3.2169969, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289384, - "lat": 51.2147684, - "lon": 3.2168674, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289386, - "lat": 51.2147716, - "lon": 3.2168811, - "timestamp": "2021-11-02T23:37:11Z", - "version": 4, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 4978289388, - "lat": 51.2148115, - "lon": 3.216966, - "timestamp": "2021-11-02T23:38:13Z", - "version": 7, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979643, - "lat": 51.2147405, - "lon": 3.216693, - "timestamp": "2021-11-02T23:37:11Z", - "version": 1, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979646, - "lat": 51.2148043, - "lon": 3.2169312, - "timestamp": "2021-11-02T23:38:13Z", - "version": 2, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 9219979647, - "lat": 51.2147792, - "lon": 3.2169466, - "timestamp": "2021-11-02T23:37:11Z", - "version": 1, - "changeset": 113305401, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "way", - "id": 160909312, - "timestamp": "2021-11-02T23:38:13Z", - "version": 4, - "changeset": 113306325, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [9219979643, 1728823481, 1728823549, 4978289383, 4978289388, 9219979646, 9219979647, 4978288381, 4978289386, 4978289384, 1728823514, 9219979643], - "tags": { - "addr:city": "Brugge", - "addr:country": "BE", - "addr:housenumber": "108", - "addr:postcode": "8000", - "addr:street": "Ezelstraat", - "building": "house", - "source:geometry:date": "2018-10-02", - "source:geometry:ref": "Gbg/5926383" - } - }] - } - ) - Utils.injectJsonDownloadForTests("https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country/0.0.0.json", "be") - - -it("should move nodes accordingly", async () => { - - - const layout = new LayoutConfig(<any>grbStripped) - ShowDataLayer.actualContstructor = (_) => undefined; - - const state = new State(layout) - State.state = state; - const bbox = new BBox( - [[ - 3.2166673243045807, - 51.21467321525788 + "source:geometry:ref": "Gbg/5926383", + }, + }, ], - [ - 3.217007964849472, - 51.21482442824023 - ] - ]) - const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` - const data = await Utils.downloadJson(url) - - state.featurePipeline.fullNodeDatabase.handleOsmJson(data, 0) - - - const action = new ReplaceGeometryAction(state, targetFeature, wayId, { - theme: "test" - } + }) + Utils.injectJsonDownloadForTests( + "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country/0.0.0.json", + "be" ) - const closestIds = await action.GetClosestIds() - expect(closestIds.closestIds).deep.equal([9219979643, - 1728823481, - 4978289383, - 4978289388, - 9219979646, - 9219979647, - 4978288381, - 4978289386, - 4978289384, - 1728823514, - undefined]) - - expect(closestIds.reprojectedNodes.size).deep.equal(1) - const reproj = closestIds.reprojectedNodes.get(1728823549) - expect(reproj.projectAfterIndex).deep.equal(1) - expect(reproj.newLon).deep.equal(3.2168880864669203) - expect(reproj.newLat).deep.equal(51.214739524104694) - expect(closestIds.detachedNodes.size).deep.equal(0) - const changes = await action.Perform(state.changes) - expect(changes[11].changes["coordinates"]).deep.equal([[3.216690793633461, 51.21474084112525], [3.2167256623506546, 51.214696737309964], [3.2168880864669203, 51.214739524104694], [3.2169999182224274, 51.214768983537674], [3.2169650495052338, 51.21480720678671], [3.2169368863105774, 51.21480090625335], [3.2169489562511444, 51.21478074454077], [3.216886594891548, 51.214765203214625], [3.2168812304735184, 51.21477192378873], [3.2168644666671753, 51.214768983537674], [3.2168537378311157, 51.21478746511261], [3.216690793633461, 51.21474084112525]]) - -}) + it("should move nodes accordingly", async () => { + const layout = new LayoutConfig(<any>grbStripped) + ShowDataLayer.actualContstructor = (_) => undefined + const state = new State(layout) + State.state = state + const bbox = new BBox([ + [3.2166673243045807, 51.21467321525788], + [3.217007964849472, 51.21482442824023], + ]) + const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` + const data = await Utils.downloadJson(url) + state.featurePipeline.fullNodeDatabase.handleOsmJson(data, 0) + + const action = new ReplaceGeometryAction(state, targetFeature, wayId, { + theme: "test", + }) + + const closestIds = await action.GetClosestIds() + expect(closestIds.closestIds).deep.equal([ + 9219979643, + 1728823481, + 4978289383, + 4978289388, + 9219979646, + 9219979647, + 4978288381, + 4978289386, + 4978289384, + 1728823514, + undefined, + ]) + + expect(closestIds.reprojectedNodes.size).deep.equal(1) + const reproj = closestIds.reprojectedNodes.get(1728823549) + expect(reproj.projectAfterIndex).deep.equal(1) + expect(reproj.newLon).deep.equal(3.2168880864669203) + expect(reproj.newLat).deep.equal(51.214739524104694) + expect(closestIds.detachedNodes.size).deep.equal(0) + const changes = await action.Perform(state.changes) + expect(changes[11].changes["coordinates"]).deep.equal([ + [3.216690793633461, 51.21474084112525], + [3.2167256623506546, 51.214696737309964], + [3.2168880864669203, 51.214739524104694], + [3.2169999182224274, 51.214768983537674], + [3.2169650495052338, 51.21480720678671], + [3.2169368863105774, 51.21480090625335], + [3.2169489562511444, 51.21478074454077], + [3.216886594891548, 51.214765203214625], + [3.2168812304735184, 51.21477192378873], + [3.2168644666671753, 51.214768983537674], + [3.2168537378311157, 51.21478746511261], + [3.216690793633461, 51.21474084112525], + ]) + }) }) diff --git a/test/Logic/OSM/Actions/SplitAction.spec.ts b/test/Logic/OSM/Actions/SplitAction.spec.ts index b2f5f9c58..446e636a5 100644 --- a/test/Logic/OSM/Actions/SplitAction.spec.ts +++ b/test/Logic/OSM/Actions/SplitAction.spec.ts @@ -1,2022 +1,2753 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../../../Utils"; -import SplitAction from "../../../../Logic/Osm/Actions/SplitAction"; -import {Changes} from "../../../../Logic/Osm/Changes"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../../../Utils" +import SplitAction from "../../../../Logic/Osm/Actions/SplitAction" +import { Changes } from "../../../../Logic/Osm/Changes" describe("SplitAction", () => { - - { // Setup of download + { + // Setup of download Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/941079939/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (957273 spike-08.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 6490126559, - "lat": 51.2332219, - "lon": 3.1429387, - "timestamp": "2021-05-09T19:04:53Z", - "version": 2, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"highway": "street_lamp", "power": "pole", "support": "pole"} - }, { - "type": "node", - "id": 8715440363, - "lat": 51.2324011, - "lon": 3.1367377, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"fixme": "continue"} - }, { - "type": "node", - "id": 8715440364, - "lat": 51.232455, - "lon": 3.1368759, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440365, - "lat": 51.2325883, - "lon": 3.1373986, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440366, - "lat": 51.232688, - "lon": 3.1379837, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440367, - "lat": 51.2327354, - "lon": 3.1385649, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440368, - "lat": 51.2327042, - "lon": 3.1392187, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"highway": "street_lamp", "power": "pole", "support": "pole"} - }, { - "type": "node", - "id": 8715440369, - "lat": 51.2323902, - "lon": 3.139353, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440370, - "lat": 51.2321027, - "lon": 3.139601, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"highway": "street_lamp", "power": "pole", "ref": "242", "support": "pole"} - }, { - "type": "node", - "id": 8715440371, - "lat": 51.2322614, - "lon": 3.1401564, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440372, - "lat": 51.232378, - "lon": 3.1407909, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440373, - "lat": 51.2325532, - "lon": 3.1413659, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440374, - "lat": 51.2327611, - "lon": 3.1418877, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "node", - "id": 8715440375, - "lat": 51.2330037, - "lon": 3.142418, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "tags": {"power": "pole"} - }, { - "type": "way", - "id": 941079939, - "timestamp": "2021-05-09T19:04:53Z", - "version": 1, - "changeset": 104407928, - "user": "M!dgard", - "uid": 763799, - "nodes": [6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, 8715440370, 8715440369, 8715440368, 8715440367, 8715440366, 8715440365, 8715440364, 8715440363], - "tags": {"power": "minor_line"} - }] + version: "0.6", + generator: "CGImap 0.8.5 (957273 spike-08.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 6490126559, + lat: 51.2332219, + lon: 3.1429387, + timestamp: "2021-05-09T19:04:53Z", + version: 2, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { highway: "street_lamp", power: "pole", support: "pole" }, + }, + { + type: "node", + id: 8715440363, + lat: 51.2324011, + lon: 3.1367377, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { fixme: "continue" }, + }, + { + type: "node", + id: 8715440364, + lat: 51.232455, + lon: 3.1368759, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440365, + lat: 51.2325883, + lon: 3.1373986, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440366, + lat: 51.232688, + lon: 3.1379837, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440367, + lat: 51.2327354, + lon: 3.1385649, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440368, + lat: 51.2327042, + lon: 3.1392187, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { highway: "street_lamp", power: "pole", support: "pole" }, + }, + { + type: "node", + id: 8715440369, + lat: 51.2323902, + lon: 3.139353, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440370, + lat: 51.2321027, + lon: 3.139601, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { + highway: "street_lamp", + power: "pole", + ref: "242", + support: "pole", + }, + }, + { + type: "node", + id: 8715440371, + lat: 51.2322614, + lon: 3.1401564, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440372, + lat: 51.232378, + lon: 3.1407909, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440373, + lat: 51.2325532, + lon: 3.1413659, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440374, + lat: 51.2327611, + lon: 3.1418877, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "node", + id: 8715440375, + lat: 51.2330037, + lon: 3.142418, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + tags: { power: "pole" }, + }, + { + type: "way", + id: 941079939, + timestamp: "2021-05-09T19:04:53Z", + version: 1, + changeset: 104407928, + user: "M!dgard", + uid: 763799, + nodes: [ + 6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, + 8715440370, 8715440369, 8715440368, 8715440367, 8715440366, 8715440365, + 8715440364, 8715440363, + ], + tags: { power: "minor_line" }, + }, + ], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/941079939/relations", { - "version": "0.6", - "generator": "CGImap 0.8.5 (2419440 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [] + version: "0.6", + generator: "CGImap 0.8.5 (2419440 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/295132739/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (3138407 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 170497153, - "lat": 51.1825167, - "lon": 3.2487885, - "timestamp": "2011-11-18T16:33:43Z", - "version": 5, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 170497155, - "lat": 51.1817632, - "lon": 3.2472706, - "timestamp": "2011-11-18T16:33:43Z", - "version": 5, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 170497157, - "lat": 51.1815203, - "lon": 3.2465569, - "timestamp": "2011-11-18T16:33:43Z", - "version": 5, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 170497158, - "lat": 51.1812261, - "lon": 3.2454261, - "timestamp": "2011-11-18T16:33:43Z", - "version": 5, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 170497160, - "lat": 51.1810957, - "lon": 3.2443030, - "timestamp": "2011-11-18T16:33:43Z", - "version": 5, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 1507524573, - "lat": 51.1810778, - "lon": 3.2437148, - "timestamp": "2011-11-18T16:33:36Z", - "version": 1, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 1507524582, - "lat": 51.1821130, - "lon": 3.2481284, - "timestamp": "2011-11-18T16:33:37Z", - "version": 1, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 1507524610, - "lat": 51.1811645, - "lon": 3.2450828, - "timestamp": "2011-11-18T16:33:38Z", - "version": 1, - "changeset": 9865255, - "user": "TripleBee", - "uid": 497177 - }, { - "type": "node", - "id": 1575932830, - "lat": 51.1811153, - "lon": 3.2431503, - "timestamp": "2019-05-04T22:44:13Z", - "version": 2, - "changeset": 69891295, - "user": "Pieter Vander Vennet", - "uid": 3818858 - }, { - "type": "node", - "id": 3208166178, - "lat": 51.1810837, - "lon": 3.2439090, - "timestamp": "2014-11-27T20:23:10Z", - "version": 1, - "changeset": 27076816, - "user": "JanFi", - "uid": 672253 - }, { - "type": "node", - "id": 3208166179, - "lat": 51.1812062, - "lon": 3.2453151, - "timestamp": "2014-11-27T20:23:10Z", - "version": 1, - "changeset": 27076816, - "user": "JanFi", - "uid": 672253 - }, { - "type": "node", - "id": 4524321710, - "lat": 51.1820656, - "lon": 3.2480253, - "timestamp": "2017-12-09T18:56:37Z", - "version": 2, - "changeset": 54493928, - "user": "CacherB", - "uid": 1999108 - }, { - "type": "node", - "id": 5273988967, - "lat": 51.1826590, - "lon": 3.2490040, - "timestamp": "2017-12-09T18:40:21Z", - "version": 1, - "changeset": 54493533, - "user": "CacherB", - "uid": 1999108 - }, { - "type": "node", - "id": 6448669326, - "lat": 51.1811346, - "lon": 3.2428910, - "timestamp": "2019-05-04T22:44:12Z", - "version": 1, - "changeset": 69891295, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "tags": {"barrier": "bollard"} - }, { - "type": "way", - "id": 295132739, - "timestamp": "2021-07-29T21:14:53Z", - "version": 17, - "changeset": 108847202, - "user": "kaart_fietser", - "uid": 11022240, - "nodes": [5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158, 3208166179, 1507524610, 170497160, 3208166178, 1507524573, 1575932830, 6448669326], - "tags": { - "highway": "cycleway", - "name": "Abdijenroute", - "railway": "abandoned", - "surface": "compacted" - } - }] - }) + version: "0.6", + generator: "CGImap 0.8.5 (3138407 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 170497153, + lat: 51.1825167, + lon: 3.2487885, + timestamp: "2011-11-18T16:33:43Z", + version: 5, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 170497155, + lat: 51.1817632, + lon: 3.2472706, + timestamp: "2011-11-18T16:33:43Z", + version: 5, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 170497157, + lat: 51.1815203, + lon: 3.2465569, + timestamp: "2011-11-18T16:33:43Z", + version: 5, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 170497158, + lat: 51.1812261, + lon: 3.2454261, + timestamp: "2011-11-18T16:33:43Z", + version: 5, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 170497160, + lat: 51.1810957, + lon: 3.244303, + timestamp: "2011-11-18T16:33:43Z", + version: 5, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 1507524573, + lat: 51.1810778, + lon: 3.2437148, + timestamp: "2011-11-18T16:33:36Z", + version: 1, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 1507524582, + lat: 51.182113, + lon: 3.2481284, + timestamp: "2011-11-18T16:33:37Z", + version: 1, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 1507524610, + lat: 51.1811645, + lon: 3.2450828, + timestamp: "2011-11-18T16:33:38Z", + version: 1, + changeset: 9865255, + user: "TripleBee", + uid: 497177, + }, + { + type: "node", + id: 1575932830, + lat: 51.1811153, + lon: 3.2431503, + timestamp: "2019-05-04T22:44:13Z", + version: 2, + changeset: 69891295, + user: "Pieter Vander Vennet", + uid: 3818858, + }, + { + type: "node", + id: 3208166178, + lat: 51.1810837, + lon: 3.243909, + timestamp: "2014-11-27T20:23:10Z", + version: 1, + changeset: 27076816, + user: "JanFi", + uid: 672253, + }, + { + type: "node", + id: 3208166179, + lat: 51.1812062, + lon: 3.2453151, + timestamp: "2014-11-27T20:23:10Z", + version: 1, + changeset: 27076816, + user: "JanFi", + uid: 672253, + }, + { + type: "node", + id: 4524321710, + lat: 51.1820656, + lon: 3.2480253, + timestamp: "2017-12-09T18:56:37Z", + version: 2, + changeset: 54493928, + user: "CacherB", + uid: 1999108, + }, + { + type: "node", + id: 5273988967, + lat: 51.182659, + lon: 3.249004, + timestamp: "2017-12-09T18:40:21Z", + version: 1, + changeset: 54493533, + user: "CacherB", + uid: 1999108, + }, + { + type: "node", + id: 6448669326, + lat: 51.1811346, + lon: 3.242891, + timestamp: "2019-05-04T22:44:12Z", + version: 1, + changeset: 69891295, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { barrier: "bollard" }, + }, + { + type: "way", + id: 295132739, + timestamp: "2021-07-29T21:14:53Z", + version: 17, + changeset: 108847202, + user: "kaart_fietser", + uid: 11022240, + nodes: [ + 5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, + 170497158, 3208166179, 1507524610, 170497160, 3208166178, 1507524573, + 1575932830, 6448669326, + ], + tags: { + highway: "cycleway", + name: "Abdijenroute", + railway: "abandoned", + surface: "compacted", + }, + }, + ], + } + ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/295132739/relations", // Mimick that there are no relations relation is missing { - "version": "0.6", - "generator": "CGImap 0.8.5 (2935793 spike-07.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [] + version: "0.6", + generator: "CGImap 0.8.5 (2935793 spike-07.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/61435323/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (53092 spike-08.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 766990983, - "lat": 51.2170219, - "lon": 3.2022337, - "timestamp": "2021-04-26T15:48:22Z", - "version": 6, - "changeset": 103647857, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 766990985, - "lat": 51.2169574, - "lon": 3.2017548, - "timestamp": "2016-07-05T22:41:12Z", - "version": 6, - "changeset": 40511250, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 8669018379, - "lat": 51.2169592, - "lon": 3.2017683, - "timestamp": "2021-04-26T15:48:22Z", - "version": 1, - "changeset": 103647857, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "way", - "id": 61435323, - "timestamp": "2021-08-21T12:24:13Z", - "version": 7, - "changeset": 110026637, - "user": "Thibault Rommel", - "uid": 5846458, - "nodes": [766990983, 8669018379, 766990985], - "tags": { - "bicycle": "yes", - "bridge": "yes", - "cycleway": "shared_lane", - "highway": "unclassified", - "layer": "1", - "maxspeed": "50", - "name": "Houtkaai", - "surface": "asphalt", - "zone:traffic": "BE-VLG:urban" - } - }] + version: "0.6", + generator: "CGImap 0.8.5 (53092 spike-08.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 766990983, + lat: 51.2170219, + lon: 3.2022337, + timestamp: "2021-04-26T15:48:22Z", + version: 6, + changeset: 103647857, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 766990985, + lat: 51.2169574, + lon: 3.2017548, + timestamp: "2016-07-05T22:41:12Z", + version: 6, + changeset: 40511250, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 8669018379, + lat: 51.2169592, + lon: 3.2017683, + timestamp: "2021-04-26T15:48:22Z", + version: 1, + changeset: 103647857, + user: "M!dgard", + uid: 763799, + }, + { + type: "way", + id: 61435323, + timestamp: "2021-08-21T12:24:13Z", + version: 7, + changeset: 110026637, + user: "Thibault Rommel", + uid: 5846458, + nodes: [766990983, 8669018379, 766990985], + tags: { + bicycle: "yes", + bridge: "yes", + cycleway: "shared_lane", + highway: "unclassified", + layer: "1", + maxspeed: "50", + name: "Houtkaai", + surface: "asphalt", + "zone:traffic": "BE-VLG:urban", + }, + }, + ], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/61435323/relations", { - "version": "0.6", - "generator": "CGImap 0.8.5 (3622541 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "relation", - "id": 1723870, - "timestamp": "2021-09-18T06:29:31Z", - "version": 183, - "changeset": 111362343, - "user": "emvee", - "uid": 5211, - "members": [{"type": "way", "ref": 261428947, "role": ""}, { - "type": "way", - "ref": 162774622, - "role": "" - }, {"type": "way", "ref": 317060244, "role": ""}, { - "type": "way", - "ref": 81155378, - "role": "" - }, {"type": "way", "ref": 99749583, "role": ""}, { - "type": "way", - "ref": 131332113, - "role": "" - }, {"type": "way", "ref": 949518831, "role": ""}, { - "type": "way", - "ref": 99749584, - "role": "" - }, {"type": "way", "ref": 129133519, "role": ""}, { - "type": "way", - "ref": 73241312, - "role": "" - }, {"type": "way", "ref": 785514256, "role": ""}, { - "type": "way", - "ref": 58509643, - "role": "" - }, {"type": "way", "ref": 73241332, "role": ""}, { - "type": "way", - "ref": 58509653, - "role": "" - }, {"type": "way", "ref": 100044097, "role": ""}, { - "type": "way", - "ref": 946999067, - "role": "" - }, {"type": "way", "ref": 73241327, "role": ""}, { - "type": "way", - "ref": 58509617, - "role": "" - }, {"type": "way", "ref": 58509627, "role": ""}, { - "type": "way", - "ref": 69990655, - "role": "" - }, {"type": "way", "ref": 73241311, "role": ""}, { - "type": "way", - "ref": 123142336, - "role": "" - }, {"type": "way", "ref": 249671053, "role": ""}, { - "type": "way", - "ref": 73241324, - "role": "" - }, {"type": "way", "ref": 66706953, "role": ""}, { - "type": "way", - "ref": 112679357, - "role": "" - }, {"type": "way", "ref": 112679358, "role": ""}, { - "type": "way", - "ref": 53105113, - "role": "" - }, {"type": "way", "ref": 66706952, "role": ""}, { - "type": "way", - "ref": 64083661, - "role": "" - }, {"type": "way", "ref": 53105162, "role": ""}, { - "type": "way", - "ref": 249671070, - "role": "" - }, {"type": "way", "ref": 249671064, "role": ""}, { - "type": "way", - "ref": 101498587, - "role": "" - }, {"type": "way", "ref": 69001236, "role": ""}, { - "type": "way", - "ref": 101498585, - "role": "" - }, {"type": "way", "ref": 70909444, "role": ""}, { - "type": "way", - "ref": 73241314, - "role": "" - }, {"type": "way", "ref": 69001235, "role": ""}, { - "type": "way", - "ref": 113150200, - "role": "" - }, {"type": "way", "ref": 137305843, "role": ""}, { - "type": "way", - "ref": 936827687, - "role": "" - }, {"type": "way", "ref": 936827688, "role": ""}, { - "type": "way", - "ref": 112952373, - "role": "" - }, {"type": "way", "ref": 930798379, "role": ""}, { - "type": "way", - "ref": 930798378, - "role": "" - }, {"type": "way", "ref": 112951439, "role": ""}, { - "type": "way", - "ref": 445541591, - "role": "" - }, {"type": "way", "ref": 103843896, "role": ""}, { - "type": "way", - "ref": 23734118, - "role": "" - }, {"type": "way", "ref": 103840557, "role": ""}, { - "type": "way", - "ref": 433852210, - "role": "" - }, {"type": "way", "ref": 313604670, "role": ""}, { - "type": "way", - "ref": 103839402, - "role": "" - }, {"type": "way", "ref": 23736061, "role": ""}, { - "type": "way", - "ref": 73241328, - "role": "" - }, {"type": "way", "ref": 295392689, "role": ""}, { - "type": "way", - "ref": 297168171, - "role": "" - }, {"type": "way", "ref": 297168170, "role": ""}, { - "type": "way", - "ref": 433852205, - "role": "" - }, {"type": "way", "ref": 295392695, "role": ""}, { - "type": "way", - "ref": 663268954, - "role": "" - }, {"type": "way", "ref": 663267598, "role": ""}, { - "type": "way", - "ref": 292478843, - "role": "" - }, {"type": "way", "ref": 981853853, "role": ""}, { - "type": "way", - "ref": 663270140, - "role": "" - }, {"type": "way", "ref": 981853854, "role": ""}, { - "type": "way", - "ref": 295392703, - "role": "" - }, {"type": "way", "ref": 663304916, "role": ""}, { - "type": "way", - "ref": 297169116, - "role": "" - }, {"type": "way", "ref": 295400810, "role": ""}, { - "type": "way", - "ref": 981853855, - "role": "" - }, {"type": "way", "ref": 663304806, "role": ""}, { - "type": "way", - "ref": 516452870, - "role": "" - }, {"type": "way", "ref": 66459239, "role": ""}, { - "type": "way", - "ref": 791430504, - "role": "" - }, {"type": "way", "ref": 178926037, "role": ""}, { - "type": "way", - "ref": 864799431, - "role": "" - }, {"type": "way", "ref": 178926107, "role": ""}, { - "type": "way", - "ref": 663320459, - "role": "" - }, {"type": "way", "ref": 62033993, "role": ""}, { - "type": "way", - "ref": 62283023, - "role": "" - }, {"type": "way", "ref": 62283057, "role": ""}, { - "type": "way", - "ref": 62283032, - "role": "" - }, {"type": "way", "ref": 490551085, "role": ""}, { - "type": "way", - "ref": 435318979, - "role": "" - }, {"type": "way", "ref": 371750677, "role": ""}, { - "type": "way", - "ref": 371750670, - "role": "" - }, {"type": "way", "ref": 371750673, "role": ""}, { - "type": "way", - "ref": 371750675, - "role": "" - }, {"type": "way", "ref": 459885691, "role": ""}, { - "type": "way", - "ref": 371750669, - "role": "" - }, {"type": "way", "ref": 371750668, "role": ""}, { - "type": "way", - "ref": 371750667, - "role": "" - }, {"type": "way", "ref": 428848639, "role": ""}, { - "type": "way", - "ref": 371750666, - "role": "" - }, {"type": "way", "ref": 371750665, "role": ""}, { - "type": "way", - "ref": 825496473, - "role": "" - }, {"type": "way", "ref": 371750664, "role": ""}, { - "type": "way", - "ref": 371750662, - "role": "" - }, {"type": "way", "ref": 371750663, "role": ""}, { - "type": "way", - "ref": 371750660, - "role": "" - }, {"type": "way", "ref": 371750658, "role": ""}, { - "type": "way", - "ref": 40507374, - "role": "" - }, {"type": "way", "ref": 165878356, "role": ""}, { - "type": "way", - "ref": 165878355, - "role": "" - }, {"type": "way", "ref": 8494219, "role": ""}, { - "type": "way", - "ref": 5023947, - "role": "" - }, {"type": "way", "ref": 5023939, "role": ""}, { - "type": "way", - "ref": 26718843, - "role": "" - }, {"type": "way", "ref": 79437029, "role": ""}, { - "type": "way", - "ref": 87522151, - "role": "" - }, {"type": "way", "ref": 26718848, "role": ""}, { - "type": "way", - "ref": 233169831, - "role": "" - }, {"type": "way", "ref": 85934460, "role": ""}, { - "type": "way", - "ref": 145892210, - "role": "" - }, {"type": "way", "ref": 79434764, "role": ""}, { - "type": "way", - "ref": 127079185, - "role": "" - }, {"type": "way", "ref": 67794715, "role": ""}, { - "type": "way", - "ref": 85934250, - "role": "" - }, {"type": "way", "ref": 421566302, "role": ""}, { - "type": "way", - "ref": 123445537, - "role": "" - }, {"type": "way", "ref": 308077683, "role": ""}, { - "type": "way", - "ref": 308077684, - "role": "" - }, {"type": "way", "ref": 972955357, "role": ""}, { - "type": "way", - "ref": 308077682, - "role": "" - }, {"type": "way", "ref": 659880052, "role": ""}, { - "type": "way", - "ref": 308077681, - "role": "" - }, {"type": "way", "ref": 66364130, "role": ""}, { - "type": "way", - "ref": 51086959, - "role": "" - }, {"type": "way", "ref": 51086961, "role": ""}, { - "type": "way", - "ref": 102154586, - "role": "" - }, {"type": "way", "ref": 102154589, "role": ""}, { - "type": "way", - "ref": 703008376, - "role": "" - }, {"type": "way", "ref": 703008375, "role": ""}, { - "type": "way", - "ref": 54435150, - "role": "" - }, {"type": "way", "ref": 115913100, "role": ""}, { - "type": "way", - "ref": 79433785, - "role": "" - }, {"type": "way", "ref": 51204355, "role": ""}, { - "type": "way", - "ref": 422395066, - "role": "" - }, {"type": "way", "ref": 116628138, "role": ""}, { - "type": "way", - "ref": 690189323, - "role": "" - }, {"type": "way", "ref": 132068368, "role": ""}, { - "type": "way", - "ref": 690220771, - "role": "" - }, {"type": "way", "ref": 690220772, "role": ""}, { - "type": "way", - "ref": 690226744, - "role": "" - }, {"type": "way", "ref": 690226745, "role": ""}, { - "type": "way", - "ref": 60253953, - "role": "" - }, {"type": "way", "ref": 690195774, "role": ""}, { - "type": "way", - "ref": 688104939, - "role": "" - }, {"type": "way", "ref": 422395064, "role": "forward"}, { - "type": "way", - "ref": 422309497, - "role": "forward" - }, {"type": "way", "ref": 25677204, "role": "forward"}, { - "type": "way", - "ref": 51570941, - "role": "" - }, {"type": "way", "ref": 807329786, "role": ""}, { - "type": "way", - "ref": 165500495, - "role": "" - }, {"type": "way", "ref": 689494106, "role": ""}, { - "type": "way", - "ref": 131476435, - "role": "" - }, {"type": "way", "ref": 689493508, "role": ""}, { - "type": "way", - "ref": 12126873, - "role": "" - }, {"type": "way", "ref": 32789519, "role": ""}, { - "type": "way", - "ref": 27288122, - "role": "" - }, {"type": "way", "ref": 116717060, "role": ""}, { - "type": "way", - "ref": 176380249, - "role": "" - }, {"type": "way", "ref": 116717052, "role": ""}, { - "type": "way", - "ref": 176380250, - "role": "" - }, {"type": "way", "ref": 421998791, "role": ""}, { - "type": "way", - "ref": 34562745, - "role": "" - }, {"type": "way", "ref": 130473931, "role": ""}, { - "type": "way", - "ref": 136487196, - "role": "" - }, {"type": "way", "ref": 23792223, "role": ""}, { - "type": "way", - "ref": 23775021, - "role": "" - }, {"type": "way", "ref": 560506339, "role": ""}, { - "type": "way", - "ref": 337945886, - "role": "" - }, {"type": "way", "ref": 61435332, "role": ""}, { - "type": "way", - "ref": 61435323, - "role": "" - }, {"type": "way", "ref": 509668834, "role": ""}, { - "type": "way", - "ref": 130473917, - "role": "" - }, {"type": "way", "ref": 369929894, "role": ""}, { - "type": "way", - "ref": 805247467, - "role": "forward" - }, {"type": "way", "ref": 840210016, "role": "forward"}, { - "type": "way", - "ref": 539026983, - "role": "forward" - }, {"type": "way", "ref": 539037793, "role": "forward"}, { - "type": "way", - "ref": 244428576, - "role": "forward" - }, {"type": "way", "ref": 243333119, "role": "forward"}, { - "type": "way", - "ref": 243333108, - "role": "forward" - }, {"type": "way", "ref": 243333106, "role": "forward"}, { - "type": "way", - "ref": 243333110, - "role": "forward" - }, {"type": "way", "ref": 230511503, "role": "forward"}, { - "type": "way", - "ref": 510520445, - "role": "forward" - }, {"type": "way", "ref": 688103605, "role": "forward"}, { - "type": "way", - "ref": 668577053, - "role": "forward" - }, {"type": "way", "ref": 4332489, "role": "forward"}, { - "type": "way", - "ref": 668577051, - "role": "forward" - }, {"type": "way", "ref": 185476761, "role": "forward"}, { - "type": "way", - "ref": 100774483, - "role": "forward" - }, {"type": "way", "ref": 668672434, "role": "backward"}, { - "type": "way", - "ref": 488558133, - "role": "backward" - }, {"type": "way", "ref": 13943237, "role": "forward"}, { - "type": "way", - "ref": 840241791, - "role": "forward" - }, {"type": "way", "ref": 805247468, "role": "forward"}, { - "type": "way", - "ref": 539040946, - "role": "forward" - }, {"type": "way", "ref": 539026103, "role": "forward"}, { - "type": "way", - "ref": 539037781, - "role": "forward" - }, {"type": "way", "ref": 28942112, "role": "forward"}, { - "type": "way", - "ref": 699841535, - "role": "forward" - }, {"type": "way", "ref": 635374201, "role": "forward"}, { - "type": "way", - "ref": 28942118, - "role": "forward" - }, {"type": "way", "ref": 185476755, "role": "forward"}, { - "type": "way", - "ref": 78794903, - "role": "forward" - }, {"type": "way", "ref": 688103599, "role": "forward"}, { - "type": "way", - "ref": 688103600, - "role": "backward" - }, {"type": "way", "ref": 32699077, "role": "backward"}, { - "type": "way", - "ref": 249092420, - "role": "backward" - }, {"type": "way", "ref": 540048295, "role": ""}, { - "type": "way", - "ref": 13942938, - "role": "" - }, {"type": "way", "ref": 827705395, "role": ""}, { - "type": "way", - "ref": 72492953, - "role": "" - }, {"type": "way", "ref": 61435342, "role": ""}, { - "type": "way", - "ref": 95106180, - "role": "" - }, {"type": "way", "ref": 182691326, "role": ""}, { - "type": "way", - "ref": 180915274, - "role": "" - }, {"type": "way", "ref": 61435340, "role": ""}, { - "type": "way", - "ref": 95506626, - "role": "" - }, {"type": "way", "ref": 183330864, "role": ""}, { - "type": "way", - "ref": 318631002, - "role": "" - }, {"type": "way", "ref": 4332470, "role": ""}, { - "type": "way", - "ref": 318631014, - "role": "" - }, {"type": "way", "ref": 337969633, "role": ""}, { - "type": "way", - "ref": 668566903, - "role": "" - }, {"type": "way", "ref": 668566904, "role": ""}, { - "type": "way", - "ref": 248228679, - "role": "" - }, {"type": "way", "ref": 419296358, "role": ""}, { - "type": "way", - "ref": 601005356, - "role": "" - }, {"type": "way", "ref": 497802656, "role": ""}, { - "type": "way", - "ref": 948484806, - "role": "" - }, {"type": "way", "ref": 756223825, "role": ""}, { - "type": "way", - "ref": 23206884, - "role": "" - }, {"type": "way", "ref": 157436856, "role": ""}, { - "type": "way", - "ref": 829398288, - "role": "" - }, {"type": "way", "ref": 829398289, "role": ""}, { - "type": "way", - "ref": 674490354, - "role": "" - }, {"type": "way", "ref": 131704173, "role": ""}, { - "type": "way", - "ref": 120976014, - "role": "" - }, {"type": "way", "ref": 38864144, "role": ""}, { - "type": "way", - "ref": 38864143, - "role": "" - }, {"type": "way", "ref": 32147475, "role": ""}, { - "type": "way", - "ref": 962256846, - "role": "" - }, {"type": "way", "ref": 32147479, "role": ""}, { - "type": "way", - "ref": 32147481, - "role": "" - }, {"type": "way", "ref": 49486734, "role": ""}, { - "type": "way", - "ref": 829394351, - "role": "" - }, {"type": "way", "ref": 829394349, "role": ""}, { - "type": "way", - "ref": 235193261, - "role": "" - }, {"type": "way", "ref": 130495866, "role": ""}, { - "type": "way", - "ref": 978366962, - "role": "" - }, {"type": "way", "ref": 39588752, "role": ""}, { - "type": "way", - "ref": 436528651, - "role": "" - }, {"type": "way", "ref": 27370335, "role": ""}, { - "type": "way", - "ref": 157558803, - "role": "" - }, {"type": "way", "ref": 39590466, "role": ""}, { - "type": "way", - "ref": 157558804, - "role": "" - }, {"type": "way", "ref": 27370165, "role": ""}, {"type": "way", "ref": 970841665, "role": ""}], - "tags": { - "name": "Euroroute R1 - part Belgium", - "name:de": "Europaradweg R1 - Abschnitt Belgien", - "name:nl": "Euroroute R1 - deel België", - "network": "icn", - "ref": "R1", - "route": "bicycle", - "type": "route" - } - }, { - "type": "relation", - "id": 1757007, - "timestamp": "2020-10-13T01:31:44Z", - "version": 10, - "changeset": 92380204, - "user": "Diabolix", - "uid": 2123963, - "members": [{"type": "way", "ref": 509668834, "role": ""}, { - "type": "way", - "ref": 61435323, - "role": "" - }, {"type": "way", "ref": 61435332, "role": ""}, { - "type": "way", - "ref": 337945886, - "role": "" - }, {"type": "way", "ref": 560506339, "role": ""}, { - "type": "way", - "ref": 23775021, - "role": "" - }, {"type": "way", "ref": 23792223, "role": ""}], - "tags": { - "network": "rcn", - "network:type": "node_network", - "ref": "4-36", - "route": "bicycle", - "type": "route" - } - }, { - "type": "relation", - "id": 5150189, - "timestamp": "2021-09-09T20:15:58Z", - "version": 44, - "changeset": 110993632, - "user": "JosV", - "uid": 170722, - "members": [{"type": "way", "ref": 13943237, "role": ""}, { - "type": "way", - "ref": 488558133, - "role": "" - }, {"type": "way", "ref": 369929894, "role": ""}, { - "type": "way", - "ref": 130473917, - "role": "" - }, {"type": "way", "ref": 509668834, "role": ""}, { - "type": "way", - "ref": 61435323, - "role": "" - }, {"type": "way", "ref": 61435332, "role": ""}, { - "type": "way", - "ref": 337945886, - "role": "" - }, {"type": "way", "ref": 560506339, "role": ""}, { - "type": "way", - "ref": 23775021, - "role": "" - }, {"type": "way", "ref": 23792223, "role": ""}, { - "type": "way", - "ref": 136487196, - "role": "" - }, {"type": "way", "ref": 130473931, "role": ""}, { - "type": "way", - "ref": 34562745, - "role": "" - }, {"type": "way", "ref": 421998791, "role": ""}, { - "type": "way", - "ref": 126996864, - "role": "" - }, {"type": "way", "ref": 126996861, "role": ""}, { - "type": "way", - "ref": 170989337, - "role": "" - }, {"type": "way", "ref": 72482534, "role": ""}, { - "type": "way", - "ref": 58913500, - "role": "" - }, {"type": "way", "ref": 72482539, "role": ""}, { - "type": "way", - "ref": 246969243, - "role": "" - }, {"type": "way", "ref": 153150902, "role": ""}, { - "type": "way", - "ref": 116748588, - "role": "" - }, {"type": "way", "ref": 72482544, "role": ""}, { - "type": "way", - "ref": 72482542, - "role": "" - }, {"type": "way", "ref": 337013552, "role": ""}, { - "type": "way", - "ref": 132790401, - "role": "" - }, {"type": "way", "ref": 105166767, "role": ""}, { - "type": "way", - "ref": 720356345, - "role": "" - }, {"type": "way", "ref": 197829999, "role": ""}, { - "type": "way", - "ref": 105166552, - "role": "" - }, {"type": "way", "ref": 61979075, "role": ""}, { - "type": "way", - "ref": 197830184, - "role": "" - }, {"type": "way", "ref": 61979070, "role": ""}, { - "type": "way", - "ref": 948826013, - "role": "" - }, {"type": "way", "ref": 197830182, "role": ""}, { - "type": "way", - "ref": 672535497, - "role": "" - }, {"type": "way", "ref": 672535498, "role": ""}, { - "type": "way", - "ref": 948826015, - "role": "" - }, {"type": "way", "ref": 11378674, "role": ""}, { - "type": "way", - "ref": 672535496, - "role": "" - }, {"type": "way", "ref": 70023921, "role": ""}, { - "type": "way", - "ref": 948826017, - "role": "" - }, {"type": "way", "ref": 197830260, "role": ""}, { - "type": "way", - "ref": 152210843, - "role": "" - }, {"type": "way", "ref": 33748055, "role": ""}, { - "type": "way", - "ref": 344701437, - "role": "" - }, {"type": "way", "ref": 422150672, "role": ""}, { - "type": "way", - "ref": 156228338, - "role": "" - }, {"type": "way", "ref": 422150674, "role": ""}, { - "type": "way", - "ref": 223674432, - "role": "" - }, {"type": "way", "ref": 223674437, "role": ""}, { - "type": "way", - "ref": 156228327, - "role": "" - }, {"type": "way", "ref": 223674372, "role": ""}, { - "type": "way", - "ref": 592937889, - "role": "" - }, {"type": "way", "ref": 592937890, "role": ""}, { - "type": "way", - "ref": 422099666, - "role": "" - }, {"type": "way", "ref": 422100304, "role": ""}, { - "type": "way", - "ref": 948826022, - "role": "" - }, {"type": "way", "ref": 15092930, "role": ""}, { - "type": "way", - "ref": 948826024, - "role": "" - }, {"type": "way", "ref": 105182226, "role": ""}, { - "type": "way", - "ref": 133606215, - "role": "" - }, {"type": "way", "ref": 533395656, "role": ""}, { - "type": "way", - "ref": 187115987, - "role": "" - }, {"type": "way", "ref": 105182230, "role": ""}, { - "type": "way", - "ref": 105182232, - "role": "" - }, {"type": "way", "ref": 196011634, "role": ""}, { - "type": "way", - "ref": 153273480, - "role": "" - }, {"type": "way", "ref": 153273481, "role": ""}, { - "type": "way", - "ref": 881767783, - "role": "" - }, {"type": "way", "ref": 153273479, "role": ""}, { - "type": "way", - "ref": 13462242, - "role": "" - }, {"type": "way", "ref": 498093425, "role": ""}, { - "type": "way", - "ref": 70009137, - "role": "" - }, {"type": "way", "ref": 12086805, "role": ""}, { - "type": "way", - "ref": 52523332, - "role": "" - }, {"type": "way", "ref": 70009138, "role": ""}, { - "type": "way", - "ref": 592937884, - "role": "" - }, {"type": "way", "ref": 15071942, "role": ""}, { - "type": "way", - "ref": 180798233, - "role": "" - }, {"type": "way", "ref": 70010670, "role": ""}, { - "type": "way", - "ref": 15802818, - "role": "" - }, {"type": "way", "ref": 15802809, "role": ""}, { - "type": "way", - "ref": 70011254, - "role": "" - }, {"type": "way", "ref": 671368756, "role": ""}, { - "type": "way", - "ref": 840241791, - "role": "" - }, {"type": "way", "ref": 369929367, "role": ""}, { - "type": "way", - "ref": 539038988, - "role": "" - }, {"type": "way", "ref": 80130513, "role": ""}, { - "type": "way", - "ref": 540214122, - "role": "" - }, {"type": "way", "ref": 765795083, "role": ""}, { - "type": "way", - "ref": 13943005, - "role": "" - }, {"type": "way", "ref": 72492950, "role": ""}, { - "type": "way", - "ref": 183330864, - "role": "" - }, {"type": "way", "ref": 318631002, "role": ""}, { - "type": "way", - "ref": 4332470, - "role": "" - }, {"type": "way", "ref": 318631014, "role": ""}, { - "type": "way", - "ref": 337969633, - "role": "" - }, {"type": "way", "ref": 668566903, "role": ""}, { - "type": "way", - "ref": 668566904, - "role": "" - }, {"type": "way", "ref": 248228679, "role": ""}, { - "type": "way", - "ref": 419296358, - "role": "" - }, {"type": "way", "ref": 601005356, "role": ""}, { - "type": "way", - "ref": 497802656, - "role": "" - }, {"type": "way", "ref": 948484806, "role": ""}, { - "type": "way", - "ref": 100323579, - "role": "" - }, {"type": "way", "ref": 100708215, "role": ""}, { - "type": "way", - "ref": 124559834, - "role": "" - }, {"type": "way", "ref": 124559835, "role": ""}, { - "type": "way", - "ref": 239484694, - "role": "" - }, {"type": "way", "ref": 972646812, "role": ""}, { - "type": "way", - "ref": 124559832, - "role": "" - }, {"type": "way", "ref": 361686157, "role": ""}, { - "type": "way", - "ref": 361686155, - "role": "" - }, {"type": "way", "ref": 239484693, "role": ""}, { - "type": "way", - "ref": 19861731, - "role": "" - }, {"type": "way", "ref": 967906429, "role": ""}, { - "type": "way", - "ref": 126402539, - "role": "" - }, {"type": "way", "ref": 94427058, "role": ""}, { - "type": "way", - "ref": 126402541, - "role": "" - }, {"type": "way", "ref": 313693839, "role": ""}, { - "type": "way", - "ref": 313693838, - "role": "" - }, {"type": "way", "ref": 970740536, "role": ""}, { - "type": "way", - "ref": 361719175, - "role": "" - }, {"type": "way", "ref": 663186012, "role": ""}, { - "type": "way", - "ref": 744625794, - "role": "" - }, {"type": "way", "ref": 94569877, "role": ""}, { - "type": "way", - "ref": 188973964, - "role": "" - }, {"type": "way", "ref": 948484822, "role": ""}, { - "type": "way", - "ref": 28857260, - "role": "" - }, {"type": "way", "ref": 948484821, "role": ""}, { - "type": "way", - "ref": 219185860, - "role": "" - }, {"type": "way", "ref": 948484818, "role": ""}, { - "type": "way", - "ref": 219185861, - "role": "" - }, {"type": "way", "ref": 229885580, "role": ""}, { - "type": "way", - "ref": 28857247, - "role": "" - }, {"type": "way", "ref": 128813937, "role": ""}, { - "type": "way", - "ref": 32148201, - "role": "" - }, {"type": "way", "ref": 829398290, "role": ""}, { - "type": "way", - "ref": 829398288, - "role": "" - }, {"type": "way", "ref": 157436856, "role": ""}, { - "type": "way", - "ref": 23206887, - "role": "" - }, {"type": "way", "ref": 657081380, "role": ""}, { - "type": "way", - "ref": 948484817, - "role": "" - }, {"type": "way", "ref": 657081379, "role": ""}, { - "type": "way", - "ref": 657083379, - "role": "" - }, {"type": "way", "ref": 657083378, "role": ""}, { - "type": "way", - "ref": 72492956, - "role": "" - }, {"type": "way", "ref": 183763716, "role": ""}, { - "type": "way", - "ref": 497802654, - "role": "" - }, {"type": "way", "ref": 497802655, "role": ""}, { - "type": "way", - "ref": 348402994, - "role": "" - }, {"type": "way", "ref": 497802653, "role": ""}, { - "type": "way", - "ref": 948484813, - "role": "" - }, {"type": "way", "ref": 272353449, "role": "forward"}, { - "type": "way", - "ref": 497802652, - "role": "forward" - }, {"type": "way", "ref": 948484811, "role": ""}, { - "type": "way", - "ref": 948484810, - "role": "" - }, {"type": "way", "ref": 136564089, "role": ""}, { - "type": "way", - "ref": 970740538, - "role": "" - }, {"type": "way", "ref": 970740539, "role": ""}, { - "type": "way", - "ref": 433455263, - "role": "" - }, {"type": "way", "ref": 23206893, "role": ""}, { - "type": "way", - "ref": 95506626, - "role": "" - }, {"type": "way", "ref": 61435340, "role": ""}, { - "type": "way", - "ref": 180915274, - "role": "" - }, {"type": "way", "ref": 182691326, "role": ""}, { - "type": "way", - "ref": 95106180, - "role": "" - }, {"type": "way", "ref": 61435342, "role": ""}, { - "type": "way", - "ref": 72492953, - "role": "" - }, {"type": "way", "ref": 827705395, "role": ""}, { - "type": "way", - "ref": 13942938, - "role": "" - }, {"type": "way", "ref": 540048295, "role": ""}, { - "type": "way", - "ref": 249092420, - "role": "" - }, {"type": "way", "ref": 32699077, "role": ""}, { - "type": "way", - "ref": 688103600, - "role": "" - }, {"type": "way", "ref": 654338684, "role": "forward"}, { - "type": "way", - "ref": 11018710, - "role": "forward" - }, {"type": "way", "ref": 510825612, "role": "forward"}, { - "type": "way", - "ref": 70011248, - "role": "forward" - }, {"type": "way", "ref": 654338685, "role": "forward"}, { - "type": "way", - "ref": 14626290, - "role": "" - }, {"type": "way", "ref": 70011250, "role": ""}, { - "type": "way", - "ref": 12295471, - "role": "" - }, {"type": "way", "ref": 397097504, "role": ""}, { - "type": "way", - "ref": 12295484, - "role": "" - }, {"type": "way", "ref": 41990436, "role": ""}, { - "type": "way", - "ref": 70011252, - "role": "" - }, {"type": "way", "ref": 61503690, "role": ""}, { - "type": "way", - "ref": 182978284, - "role": "" - }, {"type": "way", "ref": 790820260, "role": "forward"}, { - "type": "way", - "ref": 592937894, - "role": "forward" - }, {"type": "way", "ref": 926028042, "role": "forward"}, { - "type": "way", - "ref": 592937902, - "role": "forward" - }, {"type": "way", "ref": 592937901, "role": "forward"}, { - "type": "way", - "ref": 182978255, - "role": "forward" - }, {"type": "way", "ref": 592937903, "role": "forward"}, { - "type": "way", - "ref": 12123659, - "role": "forward" - }, {"type": "way", "ref": 666877213, "role": "forward"}, { - "type": "way", - "ref": 790820259, - "role": "forward" - }, {"type": "way", "ref": 510825618, "role": ""}, { - "type": "way", - "ref": 13496412, - "role": "" - }, {"type": "way", "ref": 654338689, "role": ""}, { - "type": "way", - "ref": 740935312, - "role": "" - }, {"type": "way", "ref": 52288671, "role": ""}, { - "type": "way", - "ref": 52288667, - "role": "" - }, {"type": "way", "ref": 12123458, "role": ""}, { - "type": "way", - "ref": 508681905, - "role": "" - }, {"type": "way", "ref": 15071314, "role": ""}, { - "type": "way", - "ref": 61503700, - "role": "" - }, {"type": "way", "ref": 41989874, "role": ""}, { - "type": "way", - "ref": 328002077, - "role": "" - }, {"type": "way", "ref": 396377151, "role": ""}, { - "type": "way", - "ref": 396377150, - "role": "" - }, {"type": "way", "ref": 396377125, "role": ""}, { - "type": "way", - "ref": 328985990, - "role": "" - }, {"type": "way", "ref": 328985992, "role": ""}, { - "type": "way", - "ref": 328985993, - "role": "" - }, {"type": "way", "ref": 328985991, "role": ""}, { - "type": "way", - "ref": 632506298, - "role": "" - }, {"type": "way", "ref": 101191104, "role": ""}, { - "type": "way", - "ref": 499129522, - "role": "" - }, {"type": "way", "ref": 15071174, "role": ""}, { - "type": "way", - "ref": 297023609, - "role": "" - }, {"type": "way", "ref": 297023610, "role": ""}, { - "type": "way", - "ref": 297023608, - "role": "" - }, {"type": "way", "ref": 112695115, "role": ""}, { - "type": "way", - "ref": 584024902, - "role": "" - }, {"type": "way", "ref": 243543197, "role": ""}, { - "type": "way", - "ref": 101191119, - "role": "forward" - }, {"type": "way", "ref": 173530022, "role": "forward"}, { - "type": "way", - "ref": 265137637, - "role": "forward" - }, {"type": "way", "ref": 160627684, "role": "forward"}, { - "type": "way", - "ref": 657163351, - "role": "forward" - }, {"type": "way", "ref": 160627682, "role": "forward"}, { - "type": "way", - "ref": 160632906, - "role": "forward" - }, {"type": "way", "ref": 176870850, "role": "forward"}, { - "type": "way", - "ref": 173662701, - "role": "forward" - }, {"type": "way", "ref": 173662702, "role": ""}, { - "type": "way", - "ref": 467606230, - "role": "" - }, {"type": "way", "ref": 126267167, "role": ""}, { - "type": "way", - "ref": 301897426, - "role": "" - }, {"type": "way", "ref": 687866206, "role": ""}, { - "type": "way", - "ref": 295132739, - "role": "" - }, {"type": "way", "ref": 690497698, "role": ""}, { - "type": "way", - "ref": 627893684, - "role": "" - }, {"type": "way", "ref": 295132741, "role": ""}, { - "type": "way", - "ref": 301903120, - "role": "" - }, {"type": "way", "ref": 672541156, "role": ""}, { - "type": "way", - "ref": 126264330, - "role": "" - }, {"type": "way", "ref": 280440853, "role": ""}, { - "type": "way", - "ref": 838499667, - "role": "" - }, {"type": "way", "ref": 838499663, "role": ""}, { - "type": "way", - "ref": 690497623, - "role": "" - }, {"type": "way", "ref": 301902946, "role": ""}, { - "type": "way", - "ref": 280460715, - "role": "" - }, {"type": "way", "ref": 972534369, "role": ""}, { - "type": "way", - "ref": 588764361, - "role": "" - }, {"type": "way", "ref": 981365419, "role": ""}, { - "type": "way", - "ref": 188979882, - "role": "" - }, {"type": "way", "ref": 578030518, "role": ""}, { - "type": "way", - "ref": 124559857, - "role": "" - }, {"type": "way", "ref": 284568605, "role": ""}, { - "type": "way", - "ref": 126405025, - "role": "" - }, {"type": "way", "ref": 188978777, "role": ""}, { - "type": "way", - "ref": 272353445, - "role": "forward" - }, {"type": "way", "ref": 221443952, "role": "forward"}, { - "type": "way", - "ref": 172708119, - "role": "forward" - }, {"type": "way", "ref": 173061662, "role": "forward"}, { - "type": "way", - "ref": 441663456, - "role": "forward" - }, {"type": "way", "ref": 160627680, "role": "forward"}, { - "type": "way", - "ref": 176870852, - "role": "forward" - }, {"type": "way", "ref": 39588762, "role": "forward"}, { - "type": "way", - "ref": 172709466, - "role": "forward" - }, {"type": "way", "ref": 598459103, "role": "forward"}, { - "type": "way", - "ref": 688054392, - "role": "forward" - }, {"type": "way", "ref": 155986859, "role": "forward"}], - "tags": { - "name": "Groene Gordel Brugge", - "network": "lcn", - "ref": "GGB", - "route": "bicycle", - "type": "route" - } - }, { - "type": "relation", - "id": 8369765, - "timestamp": "2021-08-23T14:22:45Z", - "version": 19, - "changeset": 110120188, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "members": [{"type": "way", "ref": 539038988, "role": ""}, { - "type": "way", - "ref": 369929367, - "role": "" - }, {"type": "way", "ref": 840241791, "role": ""}, { - "type": "way", - "ref": 488558133, - "role": "" - }, {"type": "way", "ref": 369929894, "role": ""}, { - "type": "way", - "ref": 130473917, - "role": "" - }, {"type": "way", "ref": 509668834, "role": ""}, { - "type": "way", - "ref": 61435323, - "role": "" - }, {"type": "way", "ref": 61435332, "role": ""}, { - "type": "way", - "ref": 337945886, - "role": "" - }, {"type": "way", "ref": 560506339, "role": ""}, { - "type": "way", - "ref": 23775021, - "role": "" - }, {"type": "way", "ref": 23792223, "role": ""}, { - "type": "way", - "ref": 136487196, - "role": "" - }, {"type": "way", "ref": 130473931, "role": ""}, { - "type": "way", - "ref": 34562745, - "role": "" - }, {"type": "way", "ref": 421998791, "role": ""}, { - "type": "way", - "ref": 176380250, - "role": "" - }, {"type": "way", "ref": 116717052, "role": ""}, { - "type": "way", - "ref": 176380249, - "role": "" - }, {"type": "way", "ref": 116717060, "role": ""}, { - "type": "way", - "ref": 27288122, - "role": "" - }, {"type": "way", "ref": 32789519, "role": ""}, { - "type": "way", - "ref": 12126873, - "role": "" - }, {"type": "way", "ref": 689493508, "role": ""}, { - "type": "way", - "ref": 131476435, - "role": "" - }, {"type": "way", "ref": 689494106, "role": ""}, { - "type": "way", - "ref": 165500495, - "role": "" - }, {"type": "way", "ref": 807329786, "role": ""}, { - "type": "way", - "ref": 51570941, - "role": "" - }, {"type": "way", "ref": 422309497, "role": ""}, { - "type": "way", - "ref": 240869981, - "role": "" - }, {"type": "way", "ref": 240869873, "role": ""}, { - "type": "way", - "ref": 240869980, - "role": "" - }, {"type": "way", "ref": 165503767, "role": ""}, { - "type": "way", - "ref": 165503764, - "role": "" - }, {"type": "way", "ref": 421566315, "role": ""}, { - "type": "way", - "ref": 165503768, - "role": "" - }, {"type": "way", "ref": 245236630, "role": ""}, { - "type": "way", - "ref": 658500046, - "role": "forward" - }, {"type": "way", "ref": 646903393, "role": "forward"}, { - "type": "way", - "ref": 245236632, - "role": "forward" - }, {"type": "way", "ref": 245236633, "role": "forward"}, { - "type": "way", - "ref": 90485426, - "role": "" - }, {"type": "way", "ref": 596073878, "role": ""}, { - "type": "way", - "ref": 10898401, - "role": "backward" - }, {"type": "way", "ref": 658500044, "role": "forward"}, { - "type": "way", - "ref": 474253371, - "role": "forward" - }, {"type": "way", "ref": 474253369, "role": "forward"}, { - "type": "way", - "ref": 474253376, - "role": "forward" - }, {"type": "way", "ref": 165845350, "role": "backward"}, { - "type": "way", - "ref": 130697218, - "role": "" - }, {"type": "way", "ref": 61565721, "role": ""}, { - "type": "way", - "ref": 497202210, - "role": "" - }, {"type": "way", "ref": 130697226, "role": ""}, { - "type": "way", - "ref": 227617858, - "role": "" - }, {"type": "way", "ref": 227617857, "role": ""}, { - "type": "way", - "ref": 681804956, - "role": "" - }, {"type": "way", "ref": 165881675, "role": ""}, { - "type": "way", - "ref": 806146504, - "role": "" - }, {"type": "way", "ref": 806146505, "role": ""}, {"type": "way", "ref": 659762284, "role": ""}], - "tags": { - "alt_name": "Fietssnelweg F30 Brugge - Oostende", - "bicycle:type": "utility", - "cycle_highway": "yes", - "cycle_network": "BE-VLG:cycle_highway", - "name": "F30 Fietssnelweg Brugge - Oostende", - "network": "ncn", - "operator": "Provincie West-Vlaanderen", - "ref": "F30", - "route": "bicycle", - "state": "proposed", - "type": "route", - "website": "https://fietssnelwegen.be/f30", - "wikidata": "Q107485732" - } - }, { - "type": "relation", - "id": 13060733, - "timestamp": "2021-09-19T18:08:57Z", - "version": 5, - "changeset": 111419581, - "user": "L'imaginaire", - "uid": 654234, - "members": [{"type": "way", "ref": 23792223, "role": ""}, { - "type": "way", - "ref": 23775021, - "role": "" - }, {"type": "way", "ref": 560506339, "role": ""}, { - "type": "way", - "ref": 337945886, - "role": "" - }, {"type": "way", "ref": 61435332, "role": ""}, { - "type": "way", - "ref": 61435323, - "role": "" - }, {"type": "way", "ref": 509668834, "role": ""}, { - "type": "way", - "ref": 839596136, - "role": "" - }, {"type": "way", "ref": 840488274, "role": ""}, { - "type": "way", - "ref": 839596137, - "role": "" - }, {"type": "way", "ref": 146172188, "role": ""}, { - "type": "way", - "ref": 749212030, - "role": "" - }, {"type": "way", "ref": 799479035, "role": ""}, { - "type": "way", - "ref": 130473928, - "role": "" - }, {"type": "way", "ref": 61414103, "role": ""}, { - "type": "way", - "ref": 539672618, - "role": "" - }, {"type": "way", "ref": 799479034, "role": ""}, { - "type": "way", - "ref": 539672617, - "role": "" - }, {"type": "way", "ref": 539672616, "role": ""}, { - "type": "way", - "ref": 539671786, - "role": "" - }, {"type": "way", "ref": 172317285, "role": ""}, { - "type": "way", - "ref": 35328157, - "role": "" - }, {"type": "way", "ref": 249119335, "role": ""}, { - "type": "way", - "ref": 584214875, - "role": "" - }, {"type": "way", "ref": 584217798, "role": ""}, { - "type": "way", - "ref": 676801473, - "role": "" - }, {"type": "way", "ref": 456588356, "role": ""}, { - "type": "way", - "ref": 456589109, - "role": "" - }, {"type": "way", "ref": 456588496, "role": ""}, { - "type": "way", - "ref": 487199906, - "role": "" - }, {"type": "way", "ref": 299450868, "role": ""}, { - "type": "way", - "ref": 165548222, - "role": "" - }, {"type": "way", "ref": 4329135, "role": ""}, { - "type": "way", - "ref": 4329771, - "role": "" - }, {"type": "way", "ref": 155149803, "role": ""}, { - "type": "way", - "ref": 305625031, - "role": "" - }, {"type": "way", "ref": 100842624, "role": ""}, { - "type": "way", - "ref": 18102445, - "role": "" - }, {"type": "way", "ref": 541116658, "role": ""}, { - "type": "way", - "ref": 591094005, - "role": "" - }, {"type": "way", "ref": 591094004, "role": ""}, { - "type": "way", - "ref": 184684947, - "role": "" - }, {"type": "way", "ref": 34945088, "role": ""}, { - "type": "way", - "ref": 235195315, - "role": "" - }, {"type": "way", "ref": 497849660, "role": ""}], - "tags": { - "colour": "#e40613", - "cycle_network": "BE-VLG:icoonroutes", - "description": "segment 2 van de Kunststedenroute", - "fixme": "incomplete", - "from": "Oostende", - "name": "Kunststedenroute - 02 - Oostende - Brugge", - "network": "ncn", - "operator": "Toerisme Vlaanderen", - "ref": "Kunst", - "route": "bicycle", - "to": "Brugge", - "type": "route", - "website": "https://www.vlaanderenmetdefiets.be/routes/kunststeden.html", - "wikidata": "Q106529274", - "wikipedia": "nl:LF Kunststedenroute" - } - }] + version: "0.6", + generator: "CGImap 0.8.5 (3622541 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "relation", + id: 1723870, + timestamp: "2021-09-18T06:29:31Z", + version: 183, + changeset: 111362343, + user: "emvee", + uid: 5211, + members: [ + { type: "way", ref: 261428947, role: "" }, + { + type: "way", + ref: 162774622, + role: "", + }, + { type: "way", ref: 317060244, role: "" }, + { + type: "way", + ref: 81155378, + role: "", + }, + { type: "way", ref: 99749583, role: "" }, + { + type: "way", + ref: 131332113, + role: "", + }, + { type: "way", ref: 949518831, role: "" }, + { + type: "way", + ref: 99749584, + role: "", + }, + { type: "way", ref: 129133519, role: "" }, + { + type: "way", + ref: 73241312, + role: "", + }, + { type: "way", ref: 785514256, role: "" }, + { + type: "way", + ref: 58509643, + role: "", + }, + { type: "way", ref: 73241332, role: "" }, + { + type: "way", + ref: 58509653, + role: "", + }, + { type: "way", ref: 100044097, role: "" }, + { + type: "way", + ref: 946999067, + role: "", + }, + { type: "way", ref: 73241327, role: "" }, + { + type: "way", + ref: 58509617, + role: "", + }, + { type: "way", ref: 58509627, role: "" }, + { + type: "way", + ref: 69990655, + role: "", + }, + { type: "way", ref: 73241311, role: "" }, + { + type: "way", + ref: 123142336, + role: "", + }, + { type: "way", ref: 249671053, role: "" }, + { + type: "way", + ref: 73241324, + role: "", + }, + { type: "way", ref: 66706953, role: "" }, + { + type: "way", + ref: 112679357, + role: "", + }, + { type: "way", ref: 112679358, role: "" }, + { + type: "way", + ref: 53105113, + role: "", + }, + { type: "way", ref: 66706952, role: "" }, + { + type: "way", + ref: 64083661, + role: "", + }, + { type: "way", ref: 53105162, role: "" }, + { + type: "way", + ref: 249671070, + role: "", + }, + { type: "way", ref: 249671064, role: "" }, + { + type: "way", + ref: 101498587, + role: "", + }, + { type: "way", ref: 69001236, role: "" }, + { + type: "way", + ref: 101498585, + role: "", + }, + { type: "way", ref: 70909444, role: "" }, + { + type: "way", + ref: 73241314, + role: "", + }, + { type: "way", ref: 69001235, role: "" }, + { + type: "way", + ref: 113150200, + role: "", + }, + { type: "way", ref: 137305843, role: "" }, + { + type: "way", + ref: 936827687, + role: "", + }, + { type: "way", ref: 936827688, role: "" }, + { + type: "way", + ref: 112952373, + role: "", + }, + { type: "way", ref: 930798379, role: "" }, + { + type: "way", + ref: 930798378, + role: "", + }, + { type: "way", ref: 112951439, role: "" }, + { + type: "way", + ref: 445541591, + role: "", + }, + { type: "way", ref: 103843896, role: "" }, + { + type: "way", + ref: 23734118, + role: "", + }, + { type: "way", ref: 103840557, role: "" }, + { + type: "way", + ref: 433852210, + role: "", + }, + { type: "way", ref: 313604670, role: "" }, + { + type: "way", + ref: 103839402, + role: "", + }, + { type: "way", ref: 23736061, role: "" }, + { + type: "way", + ref: 73241328, + role: "", + }, + { type: "way", ref: 295392689, role: "" }, + { + type: "way", + ref: 297168171, + role: "", + }, + { type: "way", ref: 297168170, role: "" }, + { + type: "way", + ref: 433852205, + role: "", + }, + { type: "way", ref: 295392695, role: "" }, + { + type: "way", + ref: 663268954, + role: "", + }, + { type: "way", ref: 663267598, role: "" }, + { + type: "way", + ref: 292478843, + role: "", + }, + { type: "way", ref: 981853853, role: "" }, + { + type: "way", + ref: 663270140, + role: "", + }, + { type: "way", ref: 981853854, role: "" }, + { + type: "way", + ref: 295392703, + role: "", + }, + { type: "way", ref: 663304916, role: "" }, + { + type: "way", + ref: 297169116, + role: "", + }, + { type: "way", ref: 295400810, role: "" }, + { + type: "way", + ref: 981853855, + role: "", + }, + { type: "way", ref: 663304806, role: "" }, + { + type: "way", + ref: 516452870, + role: "", + }, + { type: "way", ref: 66459239, role: "" }, + { + type: "way", + ref: 791430504, + role: "", + }, + { type: "way", ref: 178926037, role: "" }, + { + type: "way", + ref: 864799431, + role: "", + }, + { type: "way", ref: 178926107, role: "" }, + { + type: "way", + ref: 663320459, + role: "", + }, + { type: "way", ref: 62033993, role: "" }, + { + type: "way", + ref: 62283023, + role: "", + }, + { type: "way", ref: 62283057, role: "" }, + { + type: "way", + ref: 62283032, + role: "", + }, + { type: "way", ref: 490551085, role: "" }, + { + type: "way", + ref: 435318979, + role: "", + }, + { type: "way", ref: 371750677, role: "" }, + { + type: "way", + ref: 371750670, + role: "", + }, + { type: "way", ref: 371750673, role: "" }, + { + type: "way", + ref: 371750675, + role: "", + }, + { type: "way", ref: 459885691, role: "" }, + { + type: "way", + ref: 371750669, + role: "", + }, + { type: "way", ref: 371750668, role: "" }, + { + type: "way", + ref: 371750667, + role: "", + }, + { type: "way", ref: 428848639, role: "" }, + { + type: "way", + ref: 371750666, + role: "", + }, + { type: "way", ref: 371750665, role: "" }, + { + type: "way", + ref: 825496473, + role: "", + }, + { type: "way", ref: 371750664, role: "" }, + { + type: "way", + ref: 371750662, + role: "", + }, + { type: "way", ref: 371750663, role: "" }, + { + type: "way", + ref: 371750660, + role: "", + }, + { type: "way", ref: 371750658, role: "" }, + { + type: "way", + ref: 40507374, + role: "", + }, + { type: "way", ref: 165878356, role: "" }, + { + type: "way", + ref: 165878355, + role: "", + }, + { type: "way", ref: 8494219, role: "" }, + { + type: "way", + ref: 5023947, + role: "", + }, + { type: "way", ref: 5023939, role: "" }, + { + type: "way", + ref: 26718843, + role: "", + }, + { type: "way", ref: 79437029, role: "" }, + { + type: "way", + ref: 87522151, + role: "", + }, + { type: "way", ref: 26718848, role: "" }, + { + type: "way", + ref: 233169831, + role: "", + }, + { type: "way", ref: 85934460, role: "" }, + { + type: "way", + ref: 145892210, + role: "", + }, + { type: "way", ref: 79434764, role: "" }, + { + type: "way", + ref: 127079185, + role: "", + }, + { type: "way", ref: 67794715, role: "" }, + { + type: "way", + ref: 85934250, + role: "", + }, + { type: "way", ref: 421566302, role: "" }, + { + type: "way", + ref: 123445537, + role: "", + }, + { type: "way", ref: 308077683, role: "" }, + { + type: "way", + ref: 308077684, + role: "", + }, + { type: "way", ref: 972955357, role: "" }, + { + type: "way", + ref: 308077682, + role: "", + }, + { type: "way", ref: 659880052, role: "" }, + { + type: "way", + ref: 308077681, + role: "", + }, + { type: "way", ref: 66364130, role: "" }, + { + type: "way", + ref: 51086959, + role: "", + }, + { type: "way", ref: 51086961, role: "" }, + { + type: "way", + ref: 102154586, + role: "", + }, + { type: "way", ref: 102154589, role: "" }, + { + type: "way", + ref: 703008376, + role: "", + }, + { type: "way", ref: 703008375, role: "" }, + { + type: "way", + ref: 54435150, + role: "", + }, + { type: "way", ref: 115913100, role: "" }, + { + type: "way", + ref: 79433785, + role: "", + }, + { type: "way", ref: 51204355, role: "" }, + { + type: "way", + ref: 422395066, + role: "", + }, + { type: "way", ref: 116628138, role: "" }, + { + type: "way", + ref: 690189323, + role: "", + }, + { type: "way", ref: 132068368, role: "" }, + { + type: "way", + ref: 690220771, + role: "", + }, + { type: "way", ref: 690220772, role: "" }, + { + type: "way", + ref: 690226744, + role: "", + }, + { type: "way", ref: 690226745, role: "" }, + { + type: "way", + ref: 60253953, + role: "", + }, + { type: "way", ref: 690195774, role: "" }, + { + type: "way", + ref: 688104939, + role: "", + }, + { type: "way", ref: 422395064, role: "forward" }, + { + type: "way", + ref: 422309497, + role: "forward", + }, + { type: "way", ref: 25677204, role: "forward" }, + { + type: "way", + ref: 51570941, + role: "", + }, + { type: "way", ref: 807329786, role: "" }, + { + type: "way", + ref: 165500495, + role: "", + }, + { type: "way", ref: 689494106, role: "" }, + { + type: "way", + ref: 131476435, + role: "", + }, + { type: "way", ref: 689493508, role: "" }, + { + type: "way", + ref: 12126873, + role: "", + }, + { type: "way", ref: 32789519, role: "" }, + { + type: "way", + ref: 27288122, + role: "", + }, + { type: "way", ref: 116717060, role: "" }, + { + type: "way", + ref: 176380249, + role: "", + }, + { type: "way", ref: 116717052, role: "" }, + { + type: "way", + ref: 176380250, + role: "", + }, + { type: "way", ref: 421998791, role: "" }, + { + type: "way", + ref: 34562745, + role: "", + }, + { type: "way", ref: 130473931, role: "" }, + { + type: "way", + ref: 136487196, + role: "", + }, + { type: "way", ref: 23792223, role: "" }, + { + type: "way", + ref: 23775021, + role: "", + }, + { type: "way", ref: 560506339, role: "" }, + { + type: "way", + ref: 337945886, + role: "", + }, + { type: "way", ref: 61435332, role: "" }, + { + type: "way", + ref: 61435323, + role: "", + }, + { type: "way", ref: 509668834, role: "" }, + { + type: "way", + ref: 130473917, + role: "", + }, + { type: "way", ref: 369929894, role: "" }, + { + type: "way", + ref: 805247467, + role: "forward", + }, + { type: "way", ref: 840210016, role: "forward" }, + { + type: "way", + ref: 539026983, + role: "forward", + }, + { type: "way", ref: 539037793, role: "forward" }, + { + type: "way", + ref: 244428576, + role: "forward", + }, + { type: "way", ref: 243333119, role: "forward" }, + { + type: "way", + ref: 243333108, + role: "forward", + }, + { type: "way", ref: 243333106, role: "forward" }, + { + type: "way", + ref: 243333110, + role: "forward", + }, + { type: "way", ref: 230511503, role: "forward" }, + { + type: "way", + ref: 510520445, + role: "forward", + }, + { type: "way", ref: 688103605, role: "forward" }, + { + type: "way", + ref: 668577053, + role: "forward", + }, + { type: "way", ref: 4332489, role: "forward" }, + { + type: "way", + ref: 668577051, + role: "forward", + }, + { type: "way", ref: 185476761, role: "forward" }, + { + type: "way", + ref: 100774483, + role: "forward", + }, + { type: "way", ref: 668672434, role: "backward" }, + { + type: "way", + ref: 488558133, + role: "backward", + }, + { type: "way", ref: 13943237, role: "forward" }, + { + type: "way", + ref: 840241791, + role: "forward", + }, + { type: "way", ref: 805247468, role: "forward" }, + { + type: "way", + ref: 539040946, + role: "forward", + }, + { type: "way", ref: 539026103, role: "forward" }, + { + type: "way", + ref: 539037781, + role: "forward", + }, + { type: "way", ref: 28942112, role: "forward" }, + { + type: "way", + ref: 699841535, + role: "forward", + }, + { type: "way", ref: 635374201, role: "forward" }, + { + type: "way", + ref: 28942118, + role: "forward", + }, + { type: "way", ref: 185476755, role: "forward" }, + { + type: "way", + ref: 78794903, + role: "forward", + }, + { type: "way", ref: 688103599, role: "forward" }, + { + type: "way", + ref: 688103600, + role: "backward", + }, + { type: "way", ref: 32699077, role: "backward" }, + { + type: "way", + ref: 249092420, + role: "backward", + }, + { type: "way", ref: 540048295, role: "" }, + { + type: "way", + ref: 13942938, + role: "", + }, + { type: "way", ref: 827705395, role: "" }, + { + type: "way", + ref: 72492953, + role: "", + }, + { type: "way", ref: 61435342, role: "" }, + { + type: "way", + ref: 95106180, + role: "", + }, + { type: "way", ref: 182691326, role: "" }, + { + type: "way", + ref: 180915274, + role: "", + }, + { type: "way", ref: 61435340, role: "" }, + { + type: "way", + ref: 95506626, + role: "", + }, + { type: "way", ref: 183330864, role: "" }, + { + type: "way", + ref: 318631002, + role: "", + }, + { type: "way", ref: 4332470, role: "" }, + { + type: "way", + ref: 318631014, + role: "", + }, + { type: "way", ref: 337969633, role: "" }, + { + type: "way", + ref: 668566903, + role: "", + }, + { type: "way", ref: 668566904, role: "" }, + { + type: "way", + ref: 248228679, + role: "", + }, + { type: "way", ref: 419296358, role: "" }, + { + type: "way", + ref: 601005356, + role: "", + }, + { type: "way", ref: 497802656, role: "" }, + { + type: "way", + ref: 948484806, + role: "", + }, + { type: "way", ref: 756223825, role: "" }, + { + type: "way", + ref: 23206884, + role: "", + }, + { type: "way", ref: 157436856, role: "" }, + { + type: "way", + ref: 829398288, + role: "", + }, + { type: "way", ref: 829398289, role: "" }, + { + type: "way", + ref: 674490354, + role: "", + }, + { type: "way", ref: 131704173, role: "" }, + { + type: "way", + ref: 120976014, + role: "", + }, + { type: "way", ref: 38864144, role: "" }, + { + type: "way", + ref: 38864143, + role: "", + }, + { type: "way", ref: 32147475, role: "" }, + { + type: "way", + ref: 962256846, + role: "", + }, + { type: "way", ref: 32147479, role: "" }, + { + type: "way", + ref: 32147481, + role: "", + }, + { type: "way", ref: 49486734, role: "" }, + { + type: "way", + ref: 829394351, + role: "", + }, + { type: "way", ref: 829394349, role: "" }, + { + type: "way", + ref: 235193261, + role: "", + }, + { type: "way", ref: 130495866, role: "" }, + { + type: "way", + ref: 978366962, + role: "", + }, + { type: "way", ref: 39588752, role: "" }, + { + type: "way", + ref: 436528651, + role: "", + }, + { type: "way", ref: 27370335, role: "" }, + { + type: "way", + ref: 157558803, + role: "", + }, + { type: "way", ref: 39590466, role: "" }, + { + type: "way", + ref: 157558804, + role: "", + }, + { type: "way", ref: 27370165, role: "" }, + { type: "way", ref: 970841665, role: "" }, + ], + tags: { + name: "Euroroute R1 - part Belgium", + "name:de": "Europaradweg R1 - Abschnitt Belgien", + "name:nl": "Euroroute R1 - deel België", + network: "icn", + ref: "R1", + route: "bicycle", + type: "route", + }, + }, + { + type: "relation", + id: 1757007, + timestamp: "2020-10-13T01:31:44Z", + version: 10, + changeset: 92380204, + user: "Diabolix", + uid: 2123963, + members: [ + { type: "way", ref: 509668834, role: "" }, + { + type: "way", + ref: 61435323, + role: "", + }, + { type: "way", ref: 61435332, role: "" }, + { + type: "way", + ref: 337945886, + role: "", + }, + { type: "way", ref: 560506339, role: "" }, + { + type: "way", + ref: 23775021, + role: "", + }, + { type: "way", ref: 23792223, role: "" }, + ], + tags: { + network: "rcn", + "network:type": "node_network", + ref: "4-36", + route: "bicycle", + type: "route", + }, + }, + { + type: "relation", + id: 5150189, + timestamp: "2021-09-09T20:15:58Z", + version: 44, + changeset: 110993632, + user: "JosV", + uid: 170722, + members: [ + { type: "way", ref: 13943237, role: "" }, + { + type: "way", + ref: 488558133, + role: "", + }, + { type: "way", ref: 369929894, role: "" }, + { + type: "way", + ref: 130473917, + role: "", + }, + { type: "way", ref: 509668834, role: "" }, + { + type: "way", + ref: 61435323, + role: "", + }, + { type: "way", ref: 61435332, role: "" }, + { + type: "way", + ref: 337945886, + role: "", + }, + { type: "way", ref: 560506339, role: "" }, + { + type: "way", + ref: 23775021, + role: "", + }, + { type: "way", ref: 23792223, role: "" }, + { + type: "way", + ref: 136487196, + role: "", + }, + { type: "way", ref: 130473931, role: "" }, + { + type: "way", + ref: 34562745, + role: "", + }, + { type: "way", ref: 421998791, role: "" }, + { + type: "way", + ref: 126996864, + role: "", + }, + { type: "way", ref: 126996861, role: "" }, + { + type: "way", + ref: 170989337, + role: "", + }, + { type: "way", ref: 72482534, role: "" }, + { + type: "way", + ref: 58913500, + role: "", + }, + { type: "way", ref: 72482539, role: "" }, + { + type: "way", + ref: 246969243, + role: "", + }, + { type: "way", ref: 153150902, role: "" }, + { + type: "way", + ref: 116748588, + role: "", + }, + { type: "way", ref: 72482544, role: "" }, + { + type: "way", + ref: 72482542, + role: "", + }, + { type: "way", ref: 337013552, role: "" }, + { + type: "way", + ref: 132790401, + role: "", + }, + { type: "way", ref: 105166767, role: "" }, + { + type: "way", + ref: 720356345, + role: "", + }, + { type: "way", ref: 197829999, role: "" }, + { + type: "way", + ref: 105166552, + role: "", + }, + { type: "way", ref: 61979075, role: "" }, + { + type: "way", + ref: 197830184, + role: "", + }, + { type: "way", ref: 61979070, role: "" }, + { + type: "way", + ref: 948826013, + role: "", + }, + { type: "way", ref: 197830182, role: "" }, + { + type: "way", + ref: 672535497, + role: "", + }, + { type: "way", ref: 672535498, role: "" }, + { + type: "way", + ref: 948826015, + role: "", + }, + { type: "way", ref: 11378674, role: "" }, + { + type: "way", + ref: 672535496, + role: "", + }, + { type: "way", ref: 70023921, role: "" }, + { + type: "way", + ref: 948826017, + role: "", + }, + { type: "way", ref: 197830260, role: "" }, + { + type: "way", + ref: 152210843, + role: "", + }, + { type: "way", ref: 33748055, role: "" }, + { + type: "way", + ref: 344701437, + role: "", + }, + { type: "way", ref: 422150672, role: "" }, + { + type: "way", + ref: 156228338, + role: "", + }, + { type: "way", ref: 422150674, role: "" }, + { + type: "way", + ref: 223674432, + role: "", + }, + { type: "way", ref: 223674437, role: "" }, + { + type: "way", + ref: 156228327, + role: "", + }, + { type: "way", ref: 223674372, role: "" }, + { + type: "way", + ref: 592937889, + role: "", + }, + { type: "way", ref: 592937890, role: "" }, + { + type: "way", + ref: 422099666, + role: "", + }, + { type: "way", ref: 422100304, role: "" }, + { + type: "way", + ref: 948826022, + role: "", + }, + { type: "way", ref: 15092930, role: "" }, + { + type: "way", + ref: 948826024, + role: "", + }, + { type: "way", ref: 105182226, role: "" }, + { + type: "way", + ref: 133606215, + role: "", + }, + { type: "way", ref: 533395656, role: "" }, + { + type: "way", + ref: 187115987, + role: "", + }, + { type: "way", ref: 105182230, role: "" }, + { + type: "way", + ref: 105182232, + role: "", + }, + { type: "way", ref: 196011634, role: "" }, + { + type: "way", + ref: 153273480, + role: "", + }, + { type: "way", ref: 153273481, role: "" }, + { + type: "way", + ref: 881767783, + role: "", + }, + { type: "way", ref: 153273479, role: "" }, + { + type: "way", + ref: 13462242, + role: "", + }, + { type: "way", ref: 498093425, role: "" }, + { + type: "way", + ref: 70009137, + role: "", + }, + { type: "way", ref: 12086805, role: "" }, + { + type: "way", + ref: 52523332, + role: "", + }, + { type: "way", ref: 70009138, role: "" }, + { + type: "way", + ref: 592937884, + role: "", + }, + { type: "way", ref: 15071942, role: "" }, + { + type: "way", + ref: 180798233, + role: "", + }, + { type: "way", ref: 70010670, role: "" }, + { + type: "way", + ref: 15802818, + role: "", + }, + { type: "way", ref: 15802809, role: "" }, + { + type: "way", + ref: 70011254, + role: "", + }, + { type: "way", ref: 671368756, role: "" }, + { + type: "way", + ref: 840241791, + role: "", + }, + { type: "way", ref: 369929367, role: "" }, + { + type: "way", + ref: 539038988, + role: "", + }, + { type: "way", ref: 80130513, role: "" }, + { + type: "way", + ref: 540214122, + role: "", + }, + { type: "way", ref: 765795083, role: "" }, + { + type: "way", + ref: 13943005, + role: "", + }, + { type: "way", ref: 72492950, role: "" }, + { + type: "way", + ref: 183330864, + role: "", + }, + { type: "way", ref: 318631002, role: "" }, + { + type: "way", + ref: 4332470, + role: "", + }, + { type: "way", ref: 318631014, role: "" }, + { + type: "way", + ref: 337969633, + role: "", + }, + { type: "way", ref: 668566903, role: "" }, + { + type: "way", + ref: 668566904, + role: "", + }, + { type: "way", ref: 248228679, role: "" }, + { + type: "way", + ref: 419296358, + role: "", + }, + { type: "way", ref: 601005356, role: "" }, + { + type: "way", + ref: 497802656, + role: "", + }, + { type: "way", ref: 948484806, role: "" }, + { + type: "way", + ref: 100323579, + role: "", + }, + { type: "way", ref: 100708215, role: "" }, + { + type: "way", + ref: 124559834, + role: "", + }, + { type: "way", ref: 124559835, role: "" }, + { + type: "way", + ref: 239484694, + role: "", + }, + { type: "way", ref: 972646812, role: "" }, + { + type: "way", + ref: 124559832, + role: "", + }, + { type: "way", ref: 361686157, role: "" }, + { + type: "way", + ref: 361686155, + role: "", + }, + { type: "way", ref: 239484693, role: "" }, + { + type: "way", + ref: 19861731, + role: "", + }, + { type: "way", ref: 967906429, role: "" }, + { + type: "way", + ref: 126402539, + role: "", + }, + { type: "way", ref: 94427058, role: "" }, + { + type: "way", + ref: 126402541, + role: "", + }, + { type: "way", ref: 313693839, role: "" }, + { + type: "way", + ref: 313693838, + role: "", + }, + { type: "way", ref: 970740536, role: "" }, + { + type: "way", + ref: 361719175, + role: "", + }, + { type: "way", ref: 663186012, role: "" }, + { + type: "way", + ref: 744625794, + role: "", + }, + { type: "way", ref: 94569877, role: "" }, + { + type: "way", + ref: 188973964, + role: "", + }, + { type: "way", ref: 948484822, role: "" }, + { + type: "way", + ref: 28857260, + role: "", + }, + { type: "way", ref: 948484821, role: "" }, + { + type: "way", + ref: 219185860, + role: "", + }, + { type: "way", ref: 948484818, role: "" }, + { + type: "way", + ref: 219185861, + role: "", + }, + { type: "way", ref: 229885580, role: "" }, + { + type: "way", + ref: 28857247, + role: "", + }, + { type: "way", ref: 128813937, role: "" }, + { + type: "way", + ref: 32148201, + role: "", + }, + { type: "way", ref: 829398290, role: "" }, + { + type: "way", + ref: 829398288, + role: "", + }, + { type: "way", ref: 157436856, role: "" }, + { + type: "way", + ref: 23206887, + role: "", + }, + { type: "way", ref: 657081380, role: "" }, + { + type: "way", + ref: 948484817, + role: "", + }, + { type: "way", ref: 657081379, role: "" }, + { + type: "way", + ref: 657083379, + role: "", + }, + { type: "way", ref: 657083378, role: "" }, + { + type: "way", + ref: 72492956, + role: "", + }, + { type: "way", ref: 183763716, role: "" }, + { + type: "way", + ref: 497802654, + role: "", + }, + { type: "way", ref: 497802655, role: "" }, + { + type: "way", + ref: 348402994, + role: "", + }, + { type: "way", ref: 497802653, role: "" }, + { + type: "way", + ref: 948484813, + role: "", + }, + { type: "way", ref: 272353449, role: "forward" }, + { + type: "way", + ref: 497802652, + role: "forward", + }, + { type: "way", ref: 948484811, role: "" }, + { + type: "way", + ref: 948484810, + role: "", + }, + { type: "way", ref: 136564089, role: "" }, + { + type: "way", + ref: 970740538, + role: "", + }, + { type: "way", ref: 970740539, role: "" }, + { + type: "way", + ref: 433455263, + role: "", + }, + { type: "way", ref: 23206893, role: "" }, + { + type: "way", + ref: 95506626, + role: "", + }, + { type: "way", ref: 61435340, role: "" }, + { + type: "way", + ref: 180915274, + role: "", + }, + { type: "way", ref: 182691326, role: "" }, + { + type: "way", + ref: 95106180, + role: "", + }, + { type: "way", ref: 61435342, role: "" }, + { + type: "way", + ref: 72492953, + role: "", + }, + { type: "way", ref: 827705395, role: "" }, + { + type: "way", + ref: 13942938, + role: "", + }, + { type: "way", ref: 540048295, role: "" }, + { + type: "way", + ref: 249092420, + role: "", + }, + { type: "way", ref: 32699077, role: "" }, + { + type: "way", + ref: 688103600, + role: "", + }, + { type: "way", ref: 654338684, role: "forward" }, + { + type: "way", + ref: 11018710, + role: "forward", + }, + { type: "way", ref: 510825612, role: "forward" }, + { + type: "way", + ref: 70011248, + role: "forward", + }, + { type: "way", ref: 654338685, role: "forward" }, + { + type: "way", + ref: 14626290, + role: "", + }, + { type: "way", ref: 70011250, role: "" }, + { + type: "way", + ref: 12295471, + role: "", + }, + { type: "way", ref: 397097504, role: "" }, + { + type: "way", + ref: 12295484, + role: "", + }, + { type: "way", ref: 41990436, role: "" }, + { + type: "way", + ref: 70011252, + role: "", + }, + { type: "way", ref: 61503690, role: "" }, + { + type: "way", + ref: 182978284, + role: "", + }, + { type: "way", ref: 790820260, role: "forward" }, + { + type: "way", + ref: 592937894, + role: "forward", + }, + { type: "way", ref: 926028042, role: "forward" }, + { + type: "way", + ref: 592937902, + role: "forward", + }, + { type: "way", ref: 592937901, role: "forward" }, + { + type: "way", + ref: 182978255, + role: "forward", + }, + { type: "way", ref: 592937903, role: "forward" }, + { + type: "way", + ref: 12123659, + role: "forward", + }, + { type: "way", ref: 666877213, role: "forward" }, + { + type: "way", + ref: 790820259, + role: "forward", + }, + { type: "way", ref: 510825618, role: "" }, + { + type: "way", + ref: 13496412, + role: "", + }, + { type: "way", ref: 654338689, role: "" }, + { + type: "way", + ref: 740935312, + role: "", + }, + { type: "way", ref: 52288671, role: "" }, + { + type: "way", + ref: 52288667, + role: "", + }, + { type: "way", ref: 12123458, role: "" }, + { + type: "way", + ref: 508681905, + role: "", + }, + { type: "way", ref: 15071314, role: "" }, + { + type: "way", + ref: 61503700, + role: "", + }, + { type: "way", ref: 41989874, role: "" }, + { + type: "way", + ref: 328002077, + role: "", + }, + { type: "way", ref: 396377151, role: "" }, + { + type: "way", + ref: 396377150, + role: "", + }, + { type: "way", ref: 396377125, role: "" }, + { + type: "way", + ref: 328985990, + role: "", + }, + { type: "way", ref: 328985992, role: "" }, + { + type: "way", + ref: 328985993, + role: "", + }, + { type: "way", ref: 328985991, role: "" }, + { + type: "way", + ref: 632506298, + role: "", + }, + { type: "way", ref: 101191104, role: "" }, + { + type: "way", + ref: 499129522, + role: "", + }, + { type: "way", ref: 15071174, role: "" }, + { + type: "way", + ref: 297023609, + role: "", + }, + { type: "way", ref: 297023610, role: "" }, + { + type: "way", + ref: 297023608, + role: "", + }, + { type: "way", ref: 112695115, role: "" }, + { + type: "way", + ref: 584024902, + role: "", + }, + { type: "way", ref: 243543197, role: "" }, + { + type: "way", + ref: 101191119, + role: "forward", + }, + { type: "way", ref: 173530022, role: "forward" }, + { + type: "way", + ref: 265137637, + role: "forward", + }, + { type: "way", ref: 160627684, role: "forward" }, + { + type: "way", + ref: 657163351, + role: "forward", + }, + { type: "way", ref: 160627682, role: "forward" }, + { + type: "way", + ref: 160632906, + role: "forward", + }, + { type: "way", ref: 176870850, role: "forward" }, + { + type: "way", + ref: 173662701, + role: "forward", + }, + { type: "way", ref: 173662702, role: "" }, + { + type: "way", + ref: 467606230, + role: "", + }, + { type: "way", ref: 126267167, role: "" }, + { + type: "way", + ref: 301897426, + role: "", + }, + { type: "way", ref: 687866206, role: "" }, + { + type: "way", + ref: 295132739, + role: "", + }, + { type: "way", ref: 690497698, role: "" }, + { + type: "way", + ref: 627893684, + role: "", + }, + { type: "way", ref: 295132741, role: "" }, + { + type: "way", + ref: 301903120, + role: "", + }, + { type: "way", ref: 672541156, role: "" }, + { + type: "way", + ref: 126264330, + role: "", + }, + { type: "way", ref: 280440853, role: "" }, + { + type: "way", + ref: 838499667, + role: "", + }, + { type: "way", ref: 838499663, role: "" }, + { + type: "way", + ref: 690497623, + role: "", + }, + { type: "way", ref: 301902946, role: "" }, + { + type: "way", + ref: 280460715, + role: "", + }, + { type: "way", ref: 972534369, role: "" }, + { + type: "way", + ref: 588764361, + role: "", + }, + { type: "way", ref: 981365419, role: "" }, + { + type: "way", + ref: 188979882, + role: "", + }, + { type: "way", ref: 578030518, role: "" }, + { + type: "way", + ref: 124559857, + role: "", + }, + { type: "way", ref: 284568605, role: "" }, + { + type: "way", + ref: 126405025, + role: "", + }, + { type: "way", ref: 188978777, role: "" }, + { + type: "way", + ref: 272353445, + role: "forward", + }, + { type: "way", ref: 221443952, role: "forward" }, + { + type: "way", + ref: 172708119, + role: "forward", + }, + { type: "way", ref: 173061662, role: "forward" }, + { + type: "way", + ref: 441663456, + role: "forward", + }, + { type: "way", ref: 160627680, role: "forward" }, + { + type: "way", + ref: 176870852, + role: "forward", + }, + { type: "way", ref: 39588762, role: "forward" }, + { + type: "way", + ref: 172709466, + role: "forward", + }, + { type: "way", ref: 598459103, role: "forward" }, + { + type: "way", + ref: 688054392, + role: "forward", + }, + { type: "way", ref: 155986859, role: "forward" }, + ], + tags: { + name: "Groene Gordel Brugge", + network: "lcn", + ref: "GGB", + route: "bicycle", + type: "route", + }, + }, + { + type: "relation", + id: 8369765, + timestamp: "2021-08-23T14:22:45Z", + version: 19, + changeset: 110120188, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 539038988, role: "" }, + { + type: "way", + ref: 369929367, + role: "", + }, + { type: "way", ref: 840241791, role: "" }, + { + type: "way", + ref: 488558133, + role: "", + }, + { type: "way", ref: 369929894, role: "" }, + { + type: "way", + ref: 130473917, + role: "", + }, + { type: "way", ref: 509668834, role: "" }, + { + type: "way", + ref: 61435323, + role: "", + }, + { type: "way", ref: 61435332, role: "" }, + { + type: "way", + ref: 337945886, + role: "", + }, + { type: "way", ref: 560506339, role: "" }, + { + type: "way", + ref: 23775021, + role: "", + }, + { type: "way", ref: 23792223, role: "" }, + { + type: "way", + ref: 136487196, + role: "", + }, + { type: "way", ref: 130473931, role: "" }, + { + type: "way", + ref: 34562745, + role: "", + }, + { type: "way", ref: 421998791, role: "" }, + { + type: "way", + ref: 176380250, + role: "", + }, + { type: "way", ref: 116717052, role: "" }, + { + type: "way", + ref: 176380249, + role: "", + }, + { type: "way", ref: 116717060, role: "" }, + { + type: "way", + ref: 27288122, + role: "", + }, + { type: "way", ref: 32789519, role: "" }, + { + type: "way", + ref: 12126873, + role: "", + }, + { type: "way", ref: 689493508, role: "" }, + { + type: "way", + ref: 131476435, + role: "", + }, + { type: "way", ref: 689494106, role: "" }, + { + type: "way", + ref: 165500495, + role: "", + }, + { type: "way", ref: 807329786, role: "" }, + { + type: "way", + ref: 51570941, + role: "", + }, + { type: "way", ref: 422309497, role: "" }, + { + type: "way", + ref: 240869981, + role: "", + }, + { type: "way", ref: 240869873, role: "" }, + { + type: "way", + ref: 240869980, + role: "", + }, + { type: "way", ref: 165503767, role: "" }, + { + type: "way", + ref: 165503764, + role: "", + }, + { type: "way", ref: 421566315, role: "" }, + { + type: "way", + ref: 165503768, + role: "", + }, + { type: "way", ref: 245236630, role: "" }, + { + type: "way", + ref: 658500046, + role: "forward", + }, + { type: "way", ref: 646903393, role: "forward" }, + { + type: "way", + ref: 245236632, + role: "forward", + }, + { type: "way", ref: 245236633, role: "forward" }, + { + type: "way", + ref: 90485426, + role: "", + }, + { type: "way", ref: 596073878, role: "" }, + { + type: "way", + ref: 10898401, + role: "backward", + }, + { type: "way", ref: 658500044, role: "forward" }, + { + type: "way", + ref: 474253371, + role: "forward", + }, + { type: "way", ref: 474253369, role: "forward" }, + { + type: "way", + ref: 474253376, + role: "forward", + }, + { type: "way", ref: 165845350, role: "backward" }, + { + type: "way", + ref: 130697218, + role: "", + }, + { type: "way", ref: 61565721, role: "" }, + { + type: "way", + ref: 497202210, + role: "", + }, + { type: "way", ref: 130697226, role: "" }, + { + type: "way", + ref: 227617858, + role: "", + }, + { type: "way", ref: 227617857, role: "" }, + { + type: "way", + ref: 681804956, + role: "", + }, + { type: "way", ref: 165881675, role: "" }, + { + type: "way", + ref: 806146504, + role: "", + }, + { type: "way", ref: 806146505, role: "" }, + { type: "way", ref: 659762284, role: "" }, + ], + tags: { + alt_name: "Fietssnelweg F30 Brugge - Oostende", + "bicycle:type": "utility", + cycle_highway: "yes", + cycle_network: "BE-VLG:cycle_highway", + name: "F30 Fietssnelweg Brugge - Oostende", + network: "ncn", + operator: "Provincie West-Vlaanderen", + ref: "F30", + route: "bicycle", + state: "proposed", + type: "route", + website: "https://fietssnelwegen.be/f30", + wikidata: "Q107485732", + }, + }, + { + type: "relation", + id: 13060733, + timestamp: "2021-09-19T18:08:57Z", + version: 5, + changeset: 111419581, + user: "L'imaginaire", + uid: 654234, + members: [ + { type: "way", ref: 23792223, role: "" }, + { + type: "way", + ref: 23775021, + role: "", + }, + { type: "way", ref: 560506339, role: "" }, + { + type: "way", + ref: 337945886, + role: "", + }, + { type: "way", ref: 61435332, role: "" }, + { + type: "way", + ref: 61435323, + role: "", + }, + { type: "way", ref: 509668834, role: "" }, + { + type: "way", + ref: 839596136, + role: "", + }, + { type: "way", ref: 840488274, role: "" }, + { + type: "way", + ref: 839596137, + role: "", + }, + { type: "way", ref: 146172188, role: "" }, + { + type: "way", + ref: 749212030, + role: "", + }, + { type: "way", ref: 799479035, role: "" }, + { + type: "way", + ref: 130473928, + role: "", + }, + { type: "way", ref: 61414103, role: "" }, + { + type: "way", + ref: 539672618, + role: "", + }, + { type: "way", ref: 799479034, role: "" }, + { + type: "way", + ref: 539672617, + role: "", + }, + { type: "way", ref: 539672616, role: "" }, + { + type: "way", + ref: 539671786, + role: "", + }, + { type: "way", ref: 172317285, role: "" }, + { + type: "way", + ref: 35328157, + role: "", + }, + { type: "way", ref: 249119335, role: "" }, + { + type: "way", + ref: 584214875, + role: "", + }, + { type: "way", ref: 584217798, role: "" }, + { + type: "way", + ref: 676801473, + role: "", + }, + { type: "way", ref: 456588356, role: "" }, + { + type: "way", + ref: 456589109, + role: "", + }, + { type: "way", ref: 456588496, role: "" }, + { + type: "way", + ref: 487199906, + role: "", + }, + { type: "way", ref: 299450868, role: "" }, + { + type: "way", + ref: 165548222, + role: "", + }, + { type: "way", ref: 4329135, role: "" }, + { + type: "way", + ref: 4329771, + role: "", + }, + { type: "way", ref: 155149803, role: "" }, + { + type: "way", + ref: 305625031, + role: "", + }, + { type: "way", ref: 100842624, role: "" }, + { + type: "way", + ref: 18102445, + role: "", + }, + { type: "way", ref: 541116658, role: "" }, + { + type: "way", + ref: 591094005, + role: "", + }, + { type: "way", ref: 591094004, role: "" }, + { + type: "way", + ref: 184684947, + role: "", + }, + { type: "way", ref: 34945088, role: "" }, + { + type: "way", + ref: 235195315, + role: "", + }, + { type: "way", ref: 497849660, role: "" }, + ], + tags: { + colour: "#e40613", + cycle_network: "BE-VLG:icoonroutes", + description: "segment 2 van de Kunststedenroute", + fixme: "incomplete", + from: "Oostende", + name: "Kunststedenroute - 02 - Oostende - Brugge", + network: "ncn", + operator: "Toerisme Vlaanderen", + ref: "Kunst", + route: "bicycle", + to: "Brugge", + type: "route", + website: "https://www.vlaanderenmetdefiets.be/routes/kunststeden.html", + wikidata: "Q106529274", + wikipedia: "nl:LF Kunststedenroute", + }, + }, + ], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/61435332/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (3819319 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 766990985, - "lat": 51.2169574, - "lon": 3.2017548, - "timestamp": "2016-07-05T22:41:12Z", - "version": 6, - "changeset": 40511250, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "node", - "id": 3450208876, - "lat": 51.2169482, - "lon": 3.2016802, - "timestamp": "2016-07-05T22:41:11Z", - "version": 2, - "changeset": 40511250, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "way", - "id": 61435332, - "timestamp": "2021-08-21T12:24:13Z", - "version": 8, - "changeset": 110026637, - "user": "Thibault Rommel", - "uid": 5846458, - "nodes": [766990985, 3450208876], - "tags": { - "bicycle": "yes", - "cycleway": "shared_lane", - "highway": "unclassified", - "maxspeed": "50", - "name": "Houtkaai", - "surface": "asphalt", - "zone:traffic": "BE-VLG:urban" - } - }] + version: "0.6", + generator: "CGImap 0.8.5 (3819319 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 766990985, + lat: 51.2169574, + lon: 3.2017548, + timestamp: "2016-07-05T22:41:12Z", + version: 6, + changeset: 40511250, + user: "M!dgard", + uid: 763799, + }, + { + type: "node", + id: 3450208876, + lat: 51.2169482, + lon: 3.2016802, + timestamp: "2016-07-05T22:41:11Z", + version: 2, + changeset: 40511250, + user: "M!dgard", + uid: 763799, + }, + { + type: "way", + id: 61435332, + timestamp: "2021-08-21T12:24:13Z", + version: 8, + changeset: 110026637, + user: "Thibault Rommel", + uid: 5846458, + nodes: [766990985, 3450208876], + tags: { + bicycle: "yes", + cycleway: "shared_lane", + highway: "unclassified", + maxspeed: "50", + name: "Houtkaai", + surface: "asphalt", + "zone:traffic": "BE-VLG:urban", + }, + }, + ], } ) Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/way/509668834/full", { - "version": "0.6", - "generator": "CGImap 0.8.5 (3735280 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "node", - "id": 131917824, - "lat": 51.2170327, - "lon": 3.2023577, - "timestamp": "2019-09-16T09:48:28Z", - "version": 17, - "changeset": 74521581, - "user": "Peter Elderson", - "uid": 7103674, - "tags": {"network:type": "node_network", "rcn_ref": "4", "rcn_region": "Brugse Ommeland"} - }, { - "type": "node", - "id": 766990983, - "lat": 51.2170219, - "lon": 3.2022337, - "timestamp": "2021-04-26T15:48:22Z", - "version": 6, - "changeset": 103647857, - "user": "M!dgard", - "uid": 763799 - }, { - "type": "way", - "id": 509668834, - "timestamp": "2021-08-21T12:24:13Z", - "version": 5, - "changeset": 110026637, - "user": "Thibault Rommel", - "uid": 5846458, - "nodes": [131917824, 766990983], - "tags": { - "bicycle": "yes", - "cycleway": "shared_lane", - "highway": "residential", - "lit": "yes", - "maxspeed": "30", - "name": "Houtkaai", - "sidewalk": "both", - "surface": "paving_stones", - "zone:maxspeed": "BE:30", - "zone:traffic": "BE-VLG:urban" - } - }] + version: "0.6", + generator: "CGImap 0.8.5 (3735280 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "node", + id: 131917824, + lat: 51.2170327, + lon: 3.2023577, + timestamp: "2019-09-16T09:48:28Z", + version: 17, + changeset: 74521581, + user: "Peter Elderson", + uid: 7103674, + tags: { + "network:type": "node_network", + rcn_ref: "4", + rcn_region: "Brugse Ommeland", + }, + }, + { + type: "node", + id: 766990983, + lat: 51.2170219, + lon: 3.2022337, + timestamp: "2021-04-26T15:48:22Z", + version: 6, + changeset: 103647857, + user: "M!dgard", + uid: 763799, + }, + { + type: "way", + id: 509668834, + timestamp: "2021-08-21T12:24:13Z", + version: 5, + changeset: 110026637, + user: "Thibault Rommel", + uid: 5846458, + nodes: [131917824, 766990983], + tags: { + bicycle: "yes", + cycleway: "shared_lane", + highway: "residential", + lit: "yes", + maxspeed: "30", + name: "Houtkaai", + sidewalk: "both", + surface: "paving_stones", + "zone:maxspeed": "BE:30", + "zone:traffic": "BE-VLG:urban", + }, + }, + ], } ) } - - it("split 295132739", - async () => { - // Lets split road https://www.openstreetmap.org/way/295132739 - const id = "way/295132739" - const splitPoint: [number, number] = [3.246733546257019, 51.181710380278176] - const splitter = new SplitAction(id, [splitPoint], { - theme: "test" - }) - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - expect(changeDescription[0].type).eq("node") - expect(changeDescription[0].id).eq( -1) - expect(changeDescription[0].changes["lat"]).eq( 51.181710380278176) - expect(changeDescription[0].changes["lon"]).eq( 3.246733546257019) - expect(changeDescription[1].type).eq( "way") - expect(changeDescription[1].id).eq( -2) - expect(changeDescription[1].changes["coordinates"].length).eq( 6) - expect(changeDescription[1].changes["coordinates"][5][0]).eq( splitPoint[0]) - expect(changeDescription[1].changes["coordinates"][5][1]).eq( splitPoint[1]) - expect(changeDescription[2].type).eq( "way") - expect(changeDescription[2].id,).eq(295132739) - expect(changeDescription[2].changes["coordinates"].length).eq( 10) - expect(changeDescription[2].changes["coordinates"][0][0]).eq( splitPoint[0]) - expect(changeDescription[2].changes["coordinates"][0][1]).eq( splitPoint[1]); + it("split 295132739", async () => { + // Lets split road https://www.openstreetmap.org/way/295132739 + const id = "way/295132739" + const splitPoint: [number, number] = [3.246733546257019, 51.181710380278176] + const splitter = new SplitAction(id, [splitPoint], { + theme: "test", }) - - it("split 295132739 on already existing node", - async () => { - // Lets split road near an already existing point https://www.openstreetmap.org/way/295132739 - const id = "way/295132739" - const splitPoint: [number, number] = [3.2451081275939937, 51.18116898253599] - const splitter = new SplitAction(id, [splitPoint], { - theme: "test" - }) - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - expect(changeDescription.length).eq( 2) - expect(changeDescription[0].type).eq( "way") - expect(changeDescription[1].type).eq( "way") - expect(changeDescription[0].changes["nodes"][changeDescription[0].changes["nodes"].length - 1]).eq( changeDescription[1].changes["nodes"][0]) - expect(changeDescription[1].changes["nodes"][0]).eq( 1507524610); + expect(changeDescription[0].type).eq("node") + expect(changeDescription[0].id).eq(-1) + expect(changeDescription[0].changes["lat"]).eq(51.181710380278176) + expect(changeDescription[0].changes["lon"]).eq(3.246733546257019) + expect(changeDescription[1].type).eq("way") + expect(changeDescription[1].id).eq(-2) + expect(changeDescription[1].changes["coordinates"].length).eq(6) + expect(changeDescription[1].changes["coordinates"][5][0]).eq(splitPoint[0]) + expect(changeDescription[1].changes["coordinates"][5][1]).eq(splitPoint[1]) + expect(changeDescription[2].type).eq("way") + expect(changeDescription[2].id).eq(295132739) + expect(changeDescription[2].changes["coordinates"].length).eq(10) + expect(changeDescription[2].changes["coordinates"][0][0]).eq(splitPoint[0]) + expect(changeDescription[2].changes["coordinates"][0][1]).eq(splitPoint[1]) + }) + + it("split 295132739 on already existing node", async () => { + // Lets split road near an already existing point https://www.openstreetmap.org/way/295132739 + const id = "way/295132739" + const splitPoint: [number, number] = [3.2451081275939937, 51.18116898253599] + const splitter = new SplitAction(id, [splitPoint], { + theme: "test", }) - - it("split 61435323 on already existing node", - async () => { - const id = "way/61435323" - const splitPoint: [number, number] = [3.2021324336528774, 51.2170001600597] - const splitter = new SplitAction(id, [splitPoint], { - theme: "test" - }) - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - // Should be a new node - expect(changeDescription[0].type).eq( "node") - expect(changeDescription[3].type).eq( "relation"); + expect(changeDescription.length).eq(2) + expect(changeDescription[0].type).eq("way") + expect(changeDescription[1].type).eq("way") + expect( + changeDescription[0].changes["nodes"][changeDescription[0].changes["nodes"].length - 1] + ).eq(changeDescription[1].changes["nodes"][0]) + expect(changeDescription[1].changes["nodes"][0]).eq(1507524610) + }) + + it("split 61435323 on already existing node", async () => { + const id = "way/61435323" + const splitPoint: [number, number] = [3.2021324336528774, 51.2170001600597] + const splitter = new SplitAction(id, [splitPoint], { + theme: "test", }) - - it("Split test line", - async () => { - // Split points are lon,lat - const splitPointAroundP3: [number, number] = [3.1392198801040645, 51.232701022376745] - const splitAction = new SplitAction("way/941079939", [splitPointAroundP3], {theme: "test"}) - const changes = await splitAction.Perform(new Changes()) - console.log(changes) - // 8715440368 is the expected point of the split + const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) - /* Nodes are + // Should be a new node + expect(changeDescription[0].type).eq("node") + expect(changeDescription[3].type).eq("relation") + }) + + it("Split test line", async () => { + // Split points are lon,lat + const splitPointAroundP3: [number, number] = [3.1392198801040645, 51.232701022376745] + const splitAction = new SplitAction("way/941079939", [splitPointAroundP3], { + theme: "test", + }) + const changes = await splitAction.Perform(new Changes()) + console.log(changes) + // 8715440368 is the expected point of the split + + /* Nodes are 6490126559 (part of ways 941079941 and 941079940) 8715440375 8715440374 @@ -2033,59 +2764,35 @@ describe("SplitAction", () => { 8715440363 */ - expect(changes[0].changes["nodes"]).deep.equal([6490126559, - 8715440375, - 8715440374, - 8715440373, - 8715440372, - 8715440371, - 8715440370, - 8715440369, - 8715440368]) - expect(changes[1].changes["nodes"]).deep.equal([ - 8715440368, - 8715440367, - 8715440366, - 8715440365, - 8715440364, - 8715440363 - ]) - - }) - - - + expect(changes[0].changes["nodes"]).deep.equal([ + 6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, 8715440370, + 8715440369, 8715440368, + ]) + expect(changes[1].changes["nodes"]).deep.equal([ + 8715440368, 8715440367, 8715440366, 8715440365, 8715440364, 8715440363, + ]) + }) + it("Split minor powerline halfway", async () => { + const splitPointHalfway: [number, number] = [3.1392842531204224, 51.23255322710106] + const splitAction = new SplitAction( + "way/941079939", + [splitPointHalfway], + { theme: "test" }, + 1 + ) + const changes = await splitAction.Perform(new Changes()) + // THe first change is the creation of the new node + expect(changes[0].type).deep.equal("node") + expect(changes[0].id).deep.equal(-1) - const splitPointHalfway: [number, number] = [3.1392842531204224, 51.23255322710106] - const splitAction = new SplitAction("way/941079939", [splitPointHalfway], {theme: "test"}, 1) - const changes = await splitAction.Perform(new Changes()) - - // THe first change is the creation of the new node - expect(changes[0].type).deep.equal("node") - expect(changes[0].id).deep.equal(-1) - - expect(changes[1].changes["nodes"]).deep.equal([6490126559, - 8715440375, - 8715440374, - 8715440373, - 8715440372, - 8715440371, - 8715440370, - 8715440369, - -1]) - expect(changes[2].changes["nodes"]).deep.equal([ - -1, - 8715440368, - 8715440367, - 8715440366, - 8715440365, - 8715440364, - 8715440363 - ]) - - + expect(changes[1].changes["nodes"]).deep.equal([ + 6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, 8715440370, + 8715440369, -1, + ]) + expect(changes[2].changes["nodes"]).deep.equal([ + -1, 8715440368, 8715440367, 8715440366, 8715440365, 8715440364, 8715440363, + ]) }) }) - diff --git a/test/Logic/OSM/Changes.spec.ts b/test/Logic/OSM/Changes.spec.ts index 21b9a7e83..c1b2a8aba 100644 --- a/test/Logic/OSM/Changes.spec.ts +++ b/test/Logic/OSM/Changes.spec.ts @@ -1,6 +1,6 @@ -import {expect} from 'chai' -import {ChangeDescription} from "../../../Logic/Osm/Actions/ChangeDescription"; -import {Changes} from "../../../Logic/Osm/Changes"; +import { expect } from "chai" +import { ChangeDescription } from "../../../Logic/Osm/Actions/ChangeDescription" +import { Changes } from "../../../Logic/Osm/Changes" it("Generate preXML from changeDescriptions", () => { const changeDescrs: ChangeDescription[] = [ @@ -9,29 +9,26 @@ it("Generate preXML from changeDescriptions", () => { id: -1, changes: { lat: 42, - lon: -8 + lon: -8, }, - tags: [{k: "someKey", v: "someValue"}], + tags: [{ k: "someKey", v: "someValue" }], meta: { changeType: "create", - theme: "test" - } + theme: "test", + }, }, { type: "node", id: -1, - tags: [{k: 'foo', v: 'bar'}], + tags: [{ k: "foo", v: "bar" }], meta: { changeType: "answer", - theme: "test" - } - } + theme: "test", + }, + }, ] const c = new Changes() - const descr = c.CreateChangesetObjects( - changeDescrs, - [] - ) + const descr = c.CreateChangesetObjects(changeDescrs, []) expect(descr.modifiedObjects).length(0) expect(descr.deletedObjects).length(0) expect(descr.newObjects).length(1) @@ -39,4 +36,4 @@ it("Generate preXML from changeDescriptions", () => { const ch = descr.newObjects[0] expect(ch.tags["foo"]).eq("bar") expect(ch.tags["someKey"]).eq("someValue") -}) \ No newline at end of file +}) diff --git a/test/Logic/OSM/ChangesetHandler.spec.ts b/test/Logic/OSM/ChangesetHandler.spec.ts index 3d33bdcbe..99820c0d4 100644 --- a/test/Logic/OSM/ChangesetHandler.spec.ts +++ b/test/Logic/OSM/ChangesetHandler.spec.ts @@ -1,62 +1,66 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../../Utils"; -import {ChangesetHandler, ChangesetTag} from "../../../Logic/Osm/ChangesetHandler"; -import {UIEventSource} from "../../../Logic/UIEventSource"; -import {OsmConnection} from "../../../Logic/Osm/OsmConnection"; -import {ElementStorage} from "../../../Logic/ElementStorage"; -import {Changes} from "../../../Logic/Osm/Changes"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../../Utils" +import { ChangesetHandler, ChangesetTag } from "../../../Logic/Osm/ChangesetHandler" +import { UIEventSource } from "../../../Logic/UIEventSource" +import { OsmConnection } from "../../../Logic/Osm/OsmConnection" +import { ElementStorage } from "../../../Logic/ElementStorage" +import { Changes } from "../../../Logic/Osm/Changes" describe("ChangesetHanlder", () => { - describe("RewriteTagsOf", () => { it("should insert new tags", () => { - - const changesetHandler = new ChangesetHandler(new UIEventSource<boolean>(true), + const changesetHandler = new ChangesetHandler( + new UIEventSource<boolean>(true), new OsmConnection({}), new ElementStorage(), new Changes(), new UIEventSource(undefined) - ); + ) const oldChangesetMeta = { - "type": "changeset", - "id": 118443748, - "created_at": "2022-03-13T19:52:10Z", - "closed_at": "2022-03-13T20:54:35Z", - "open": false, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "minlat": 51.0361902, - "minlon": 3.7092939, - "maxlat": 51.0364194, - "maxlon": 3.7099520, - "comments_count": 0, - "changes_count": 3, - "tags": { - "answer": "5", - "comment": "Adding data with #MapComplete for theme #toerisme_vlaanderen", - "created_by": "MapComplete 0.16.6", - "host": "https://mapcomplete.osm.be/toerisme_vlaanderen.html", - "imagery": "osm", - "locale": "nl", - "source": "survey", + type: "changeset", + id: 118443748, + created_at: "2022-03-13T19:52:10Z", + closed_at: "2022-03-13T20:54:35Z", + open: false, + user: "Pieter Vander Vennet", + uid: 3818858, + minlat: 51.0361902, + minlon: 3.7092939, + maxlat: 51.0364194, + maxlon: 3.709952, + comments_count: 0, + changes_count: 3, + tags: { + answer: "5", + comment: "Adding data with #MapComplete for theme #toerisme_vlaanderen", + created_by: "MapComplete 0.16.6", + host: "https://mapcomplete.osm.be/toerisme_vlaanderen.html", + imagery: "osm", + locale: "nl", + source: "survey", "source:node/-1": "note/1234", - "theme": "toerisme_vlaanderen", - } + theme: "toerisme_vlaanderen", + }, } const rewritten = changesetHandler.RewriteTagsOf( - [{ - key: "newTag", - value: "newValue", - aggregate: false - }], + [ + { + key: "newTag", + value: "newValue", + aggregate: false, + }, + ], new Map<string, string>(), - oldChangesetMeta) + oldChangesetMeta + ) const d = Utils.asDict(rewritten) expect(d.size).deep.equal(10) expect(d.get("answer")).deep.equal("5") - expect(d.get("comment")).deep.equal("Adding data with #MapComplete for theme #toerisme_vlaanderen") + expect(d.get("comment")).deep.equal( + "Adding data with #MapComplete for theme #toerisme_vlaanderen" + ) expect(d.get("created_by")).deep.equal("MapComplete 0.16.6") expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html") expect(d.get("imagery")).deep.equal("osm") @@ -64,54 +68,59 @@ describe("ChangesetHanlder", () => { expect(d.get("source:node/-1")).deep.equal("note/1234") expect(d.get("theme")).deep.equal("toerisme_vlaanderen") expect(d.get("newTag")).deep.equal("newValue") - }) it("should aggregate numeric tags", () => { - const changesetHandler = new ChangesetHandler(new UIEventSource<boolean>(true), + const changesetHandler = new ChangesetHandler( + new UIEventSource<boolean>(true), new OsmConnection({}), new ElementStorage(), new Changes(), new UIEventSource(undefined) - ); + ) const oldChangesetMeta = { - "type": "changeset", - "id": 118443748, - "created_at": "2022-03-13T19:52:10Z", - "closed_at": "2022-03-13T20:54:35Z", - "open": false, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "minlat": 51.0361902, - "minlon": 3.7092939, - "maxlat": 51.0364194, - "maxlon": 3.7099520, - "comments_count": 0, - "changes_count": 3, - "tags": { - "answer": "5", - "comment": "Adding data with #MapComplete for theme #toerisme_vlaanderen", - "created_by": "MapComplete 0.16.6", - "host": "https://mapcomplete.osm.be/toerisme_vlaanderen.html", - "imagery": "osm", - "locale": "nl", - "source": "survey", + type: "changeset", + id: 118443748, + created_at: "2022-03-13T19:52:10Z", + closed_at: "2022-03-13T20:54:35Z", + open: false, + user: "Pieter Vander Vennet", + uid: 3818858, + minlat: 51.0361902, + minlon: 3.7092939, + maxlat: 51.0364194, + maxlon: 3.709952, + comments_count: 0, + changes_count: 3, + tags: { + answer: "5", + comment: "Adding data with #MapComplete for theme #toerisme_vlaanderen", + created_by: "MapComplete 0.16.6", + host: "https://mapcomplete.osm.be/toerisme_vlaanderen.html", + imagery: "osm", + locale: "nl", + source: "survey", "source:node/-1": "note/1234", - "theme": "toerisme_vlaanderen", - } + theme: "toerisme_vlaanderen", + }, } const rewritten = changesetHandler.RewriteTagsOf( - [{ - key: "answer", - value: "37", - aggregate: true - }], + [ + { + key: "answer", + value: "37", + aggregate: true, + }, + ], new Map<string, string>(), - oldChangesetMeta) + oldChangesetMeta + ) const d = Utils.asDict(rewritten) expect(d.size).deep.equal(9) expect(d.get("answer")).deep.equal("42") - expect(d.get("comment")).deep.equal("Adding data with #MapComplete for theme #toerisme_vlaanderen") + expect(d.get("comment")).deep.equal( + "Adding data with #MapComplete for theme #toerisme_vlaanderen" + ) expect(d.get("created_by")).deep.equal("MapComplete 0.16.6") expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html") expect(d.get("imagery")).deep.equal("osm") @@ -120,47 +129,51 @@ describe("ChangesetHanlder", () => { expect(d.get("theme")).deep.equal("toerisme_vlaanderen") }) it("should rewrite special reasons with the correct ID", () => { - const changesetHandler = new ChangesetHandler(new UIEventSource<boolean>(true), + const changesetHandler = new ChangesetHandler( + new UIEventSource<boolean>(true), new OsmConnection({}), new ElementStorage(), new Changes(), new UIEventSource(undefined) - ); + ) const oldChangesetMeta = { - "type": "changeset", - "id": 118443748, - "created_at": "2022-03-13T19:52:10Z", - "closed_at": "2022-03-13T20:54:35Z", - "open": false, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "minlat": 51.0361902, - "minlon": 3.7092939, - "maxlat": 51.0364194, - "maxlon": 3.7099520, - "comments_count": 0, - "changes_count": 3, - "tags": { - "answer": "5", - "comment": "Adding data with #MapComplete for theme #toerisme_vlaanderen", - "created_by": "MapComplete 0.16.6", - "host": "https://mapcomplete.osm.be/toerisme_vlaanderen.html", - "imagery": "osm", - "locale": "nl", - "source": "survey", + type: "changeset", + id: 118443748, + created_at: "2022-03-13T19:52:10Z", + closed_at: "2022-03-13T20:54:35Z", + open: false, + user: "Pieter Vander Vennet", + uid: 3818858, + minlat: 51.0361902, + minlon: 3.7092939, + maxlat: 51.0364194, + maxlon: 3.709952, + comments_count: 0, + changes_count: 3, + tags: { + answer: "5", + comment: "Adding data with #MapComplete for theme #toerisme_vlaanderen", + created_by: "MapComplete 0.16.6", + host: "https://mapcomplete.osm.be/toerisme_vlaanderen.html", + imagery: "osm", + locale: "nl", + source: "survey", "source:node/-1": "note/1234", - "theme": "toerisme_vlaanderen", - } + theme: "toerisme_vlaanderen", + }, } const rewritten = changesetHandler.RewriteTagsOf( [], new Map<string, string>([["node/-1", "node/42"]]), - oldChangesetMeta) + oldChangesetMeta + ) const d = Utils.asDict(rewritten) expect(d.size).deep.equal(9) expect(d.get("answer")).deep.equal("5") - expect(d.get("comment")).deep.equal("Adding data with #MapComplete for theme #toerisme_vlaanderen") + expect(d.get("comment")).deep.equal( + "Adding data with #MapComplete for theme #toerisme_vlaanderen" + ) expect(d.get("created_by")).deep.equal("MapComplete 0.16.6") expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html") expect(d.get("imagery")).deep.equal("osm") @@ -169,21 +182,24 @@ describe("ChangesetHanlder", () => { expect(d.get("theme")).deep.equal("toerisme_vlaanderen") }) }) - - describe("rewriteMetaTags" , () => { + + describe("rewriteMetaTags", () => { it("should rewrite special reasons with the correct ID", () => { - const extraMetaTags : ChangesetTag[] = [ + const extraMetaTags: ChangesetTag[] = [ { key: "created_by", - value:"mapcomplete" + value: "mapcomplete", }, { key: "source:node/-1", - value:"note/1234" - } + value: "note/1234", + }, ] - const changes = new Map<string, string>([["node/-1","node/42"]]) - const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(extraMetaTags, changes) + const changes = new Map<string, string>([["node/-1", "node/42"]]) + const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( + extraMetaTags, + changes + ) expect(hasSpecialMotivationChanges, "Special rewrite did not trigger").true // Rewritten inline by rewriteMetaTags expect(extraMetaTags[1].key).deep.equal("source:node/42") @@ -191,5 +207,5 @@ describe("ChangesetHanlder", () => { expect(extraMetaTags[0].key).deep.equal("created_by") expect(extraMetaTags[0].value).deep.equal("mapcomplete") }) -}) + }) }) diff --git a/test/Logic/OSM/OsmObject.spec.ts b/test/Logic/OSM/OsmObject.spec.ts index 7b3c6d831..023603e51 100644 --- a/test/Logic/OSM/OsmObject.spec.ts +++ b/test/Logic/OSM/OsmObject.spec.ts @@ -1,93 +1,99 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {OsmObject} from "../../../Logic/Osm/OsmObject"; -import {Utils} from "../../../Utils"; -import ScriptUtils from "../../../scripts/ScriptUtils"; -import {readFileSync} from "fs"; +import { describe } from "mocha" +import { expect } from "chai" +import { OsmObject } from "../../../Logic/Osm/OsmObject" +import { Utils } from "../../../Utils" +import ScriptUtils from "../../../scripts/ScriptUtils" +import { readFileSync } from "fs" describe("OsmObject", () => { - describe("download referencing ways", () => { - Utils.injectJsonDownloadForTests( - "https://www.openstreetmap.org/api/0.6/node/1124134958/ways", { - "version": "0.6", - "generator": "CGImap 0.8.6 (49805 spike-06.openstreetmap.org)", - "copyright": "OpenStreetMap and contributors", - "attribution": "http://www.openstreetmap.org/copyright", - "license": "http://opendatacommons.org/licenses/odbl/1-0/", - "elements": [{ - "type": "way", - "id": 97038428, - "timestamp": "2019-06-19T12:26:24Z", - "version": 6, - "changeset": 71399984, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [1124134958, 323729212, 323729351, 2542460408, 187073405], - "tags": { - "highway": "residential", - "name": "Brugs-Kerkhofstraat", - "sett:pattern": "arc", - "surface": "sett" - } - }, { - "type": "way", - "id": 97038434, - "timestamp": "2019-06-19T12:26:24Z", - "version": 5, - "changeset": 71399984, - "user": "Pieter Vander Vennet", - "uid": 3818858, - "nodes": [1124134958, 1124135024, 187058607], - "tags": { - "bicycle": "use_sidepath", - "highway": "residential", - "name": "Kerkhofblommenstraat", - "sett:pattern": "arc", - "surface": "sett" - } - }, { - "type": "way", - "id": 97038435, - "timestamp": "2017-12-21T21:41:08Z", - "version": 4, - "changeset": 54826837, - "user": "Jakka", - "uid": 2403313, - "nodes": [1124134958, 2576628889, 1124135035, 5298371485, 5298371495], - "tags": {"bicycle": "use_sidepath", "highway": "residential", "name": "Kerkhofblommenstraat"} - }, { - "type": "way", - "id": 251446313, - "timestamp": "2019-01-07T19:22:47Z", - "version": 4, - "changeset": 66106872, - "user": "M!dgard", - "uid": 763799, - "nodes": [1124134958, 5243143198, 4555715455], - "tags": {"foot": "yes", "highway": "service"} - }] - }) + "https://www.openstreetmap.org/api/0.6/node/1124134958/ways", + { + version: "0.6", + generator: "CGImap 0.8.6 (49805 spike-06.openstreetmap.org)", + copyright: "OpenStreetMap and contributors", + attribution: "http://www.openstreetmap.org/copyright", + license: "http://opendatacommons.org/licenses/odbl/1-0/", + elements: [ + { + type: "way", + id: 97038428, + timestamp: "2019-06-19T12:26:24Z", + version: 6, + changeset: 71399984, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1124134958, 323729212, 323729351, 2542460408, 187073405], + tags: { + highway: "residential", + name: "Brugs-Kerkhofstraat", + "sett:pattern": "arc", + surface: "sett", + }, + }, + { + type: "way", + id: 97038434, + timestamp: "2019-06-19T12:26:24Z", + version: 5, + changeset: 71399984, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1124134958, 1124135024, 187058607], + tags: { + bicycle: "use_sidepath", + highway: "residential", + name: "Kerkhofblommenstraat", + "sett:pattern": "arc", + surface: "sett", + }, + }, + { + type: "way", + id: 97038435, + timestamp: "2017-12-21T21:41:08Z", + version: 4, + changeset: 54826837, + user: "Jakka", + uid: 2403313, + nodes: [1124134958, 2576628889, 1124135035, 5298371485, 5298371495], + tags: { + bicycle: "use_sidepath", + highway: "residential", + name: "Kerkhofblommenstraat", + }, + }, + { + type: "way", + id: 251446313, + timestamp: "2019-01-07T19:22:47Z", + version: 4, + changeset: 66106872, + user: "M!dgard", + uid: 763799, + nodes: [1124134958, 5243143198, 4555715455], + tags: { foot: "yes", highway: "service" }, + }, + ], + } + ) + it("should download referencing ways", async () => { + const ways = await OsmObject.DownloadReferencingWays("node/1124134958") + expect(ways).not.undefined + expect(ways).length(4) + }) - it("should download referencing ways", - async () => { - - - const ways = await OsmObject.DownloadReferencingWays("node/1124134958") - expect(ways).not.undefined - expect(ways).length(4) - }) - - it("should download full OSM-relations", async () => { ScriptUtils.fixUtils() - Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/relation/5759328/full", JSON.parse(readFileSync("./test/data/relation_5759328.json","UTF-8"))) - const r = await OsmObject.DownloadObjectAsync("relation/5759328").then(x => x) - const geojson = r.asGeoJson(); + Utils.injectJsonDownloadForTests( + "https://www.openstreetmap.org/api/0.6/relation/5759328/full", + JSON.parse(readFileSync("./test/data/relation_5759328.json", "UTF-8")) + ) + const r = await OsmObject.DownloadObjectAsync("relation/5759328").then((x) => x) + const geojson = r.asGeoJson() expect(geojson.geometry.type).eq("MultiPolygon") }) }) - }) diff --git a/test/Logic/Tags/LazyMatching.spec.ts b/test/Logic/Tags/LazyMatching.spec.ts index 3d3d5dd19..2e94b50f1 100644 --- a/test/Logic/Tags/LazyMatching.spec.ts +++ b/test/Logic/Tags/LazyMatching.spec.ts @@ -1,11 +1,9 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {TagUtils} from "../../../Logic/Tags/TagUtils"; -import {Tag} from "../../../Logic/Tags/Tag"; +import { describe } from "mocha" +import { expect } from "chai" +import { TagUtils } from "../../../Logic/Tags/TagUtils" +import { Tag } from "../../../Logic/Tags/Tag" describe("Lazy object properties", () => { - - it("should be matche by a normal tag", () => { const properties = {} const key = "_key" @@ -15,26 +13,24 @@ describe("Lazy object properties", () => { delete properties[key] properties[key] = "yes" return "yes" - } + }, }) const filter = new Tag("_key", "yes") expect(filter.matchesProperties(properties)).true - }) - - it("should be matched by a RegexTag", () => { - const properties = {} - const key = "_key" - Object.defineProperty(properties, key, { - configurable: true, - get: function () { - delete properties[key] - properties[key] = "yes" - return "yes" - } - }) - const filter = TagUtils.Tag("_key~*") - expect(filter.matchesProperties(properties)).true; + it("should be matched by a RegexTag", () => { + const properties = {} + const key = "_key" + Object.defineProperty(properties, key, { + configurable: true, + get: function () { + delete properties[key] + properties[key] = "yes" + return "yes" + }, }) + const filter = TagUtils.Tag("_key~*") + expect(filter.matchesProperties(properties)).true + }) }) diff --git a/test/Logic/Tags/OptimizeTags.spec.ts b/test/Logic/Tags/OptimizeTags.spec.ts index 2261cb0eb..928b813d9 100644 --- a/test/Logic/Tags/OptimizeTags.spec.ts +++ b/test/Logic/Tags/OptimizeTags.spec.ts @@ -1,129 +1,91 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; -import {And} from "../../../Logic/Tags/And"; -import {Tag} from "../../../Logic/Tags/Tag"; -import {TagUtils} from "../../../Logic/Tags/TagUtils"; -import {Or} from "../../../Logic/Tags/Or"; -import {RegexTag} from "../../../Logic/Tags/RegexTag"; +import { describe } from "mocha" +import { expect } from "chai" +import { TagsFilter } from "../../../Logic/Tags/TagsFilter" +import { And } from "../../../Logic/Tags/And" +import { Tag } from "../../../Logic/Tags/Tag" +import { TagUtils } from "../../../Logic/Tags/TagUtils" +import { Or } from "../../../Logic/Tags/Or" +import { RegexTag } from "../../../Logic/Tags/RegexTag" describe("Tag optimalization", () => { - describe("And", () => { it("with condition and nested and should be flattened", () => { - const t = new And( - [ - new And([ - new Tag("x", "y") - ]), - new Tag("a", "b") - ] - ) + const t = new And([new And([new Tag("x", "y")]), new Tag("a", "b")]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq(`a=b&x=y`) }) it("should be 'true' if no conditions are given", () => { - const t = new And( - [] - ) + const t = new And([]) const opt = t.optimize() expect(opt).eq(true) }) - + it("should return false on conflicting tags", () => { - const t = new And([new Tag("key","a"), new Tag("key","b")]) + const t = new And([new Tag("key", "a"), new Tag("key", "b")]) const opt = t.optimize() expect(opt).eq(false) }) it("with nested ors and common property should be extracted", () => { - // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d)) const t = new And([ new Tag("foo", "bar"), - new Or([ - new Tag("x", "y"), - new Tag("a", "b") - ]), - new Or([ - new Tag("x", "y"), - new Tag("c", "d") - ]) + new Or([new Tag("x", "y"), new Tag("a", "b")]), + new Or([new Tag("x", "y"), new Tag("c", "d")]), ]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )") }) it("with nested ors and common regextag should be extracted", () => { - // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d)) const t = new And([ new Tag("foo", "bar"), - new Or([ - new RegexTag("x", "y"), - new RegexTag("a", "b") - ]), - new Or([ - new RegexTag("x", "y"), - new RegexTag("c", "d") - ]) + new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), + new Or([new RegexTag("x", "y"), new RegexTag("c", "d")]), ]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("foo=bar& ( (a=b&c=d) |x=y)") }) it("with nested ors and inverted regextags should _not_ be extracted", () => { - // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d)) const t = new And([ new Tag("foo", "bar"), - new Or([ - new RegexTag("x", "y"), - new RegexTag("a", "b") - ]), - new Or([ - new RegexTag("x", "y", true), - new RegexTag("c", "d") - ]) + new Or([new RegexTag("x", "y"), new RegexTag("a", "b")]), + new Or([new RegexTag("x", "y", true), new RegexTag("c", "d")]), ]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("foo=bar& (a=b|x=y) & (c=d|x!=y)") }) it("should move regextag to the end", () => { - const t = new And([ - new RegexTag("x", "y"), - new Tag("a", "b") - ]) + const t = new And([new RegexTag("x", "y"), new Tag("a", "b")]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("a=b&x=y") - }) it("should sort tags by their popularity (least popular first)", () => { - const t = new And([ - new Tag("bicycle", "yes"), - new Tag("amenity", "binoculars") - ]) + const t = new And([new Tag("bicycle", "yes"), new Tag("amenity", "binoculars")]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes") - }) it("should optimize nested ORs", () => { const filter = TagUtils.Tag({ or: [ - "X=Y", "FOO=BAR", + "X=Y", + "FOO=BAR", { - "and": [ + and: [ { - "or": ["X=Y", "FOO=BAR"] + or: ["X=Y", "FOO=BAR"], }, - "bicycle=yes" - ] - } - ] + "bicycle=yes", + ], + }, + ], }) // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) ) // This is equivalent to (X=Y | FOO=BAR) @@ -135,56 +97,60 @@ describe("Tag optimalization", () => { const filter = TagUtils.Tag({ or: [ { - "and": [ + and: [ { - "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] + or: [ + "amenity=charging_station", + "disused:amenity=charging_station", + "planned:amenity=charging_station", + "construction:amenity=charging_station", + ], }, - "bicycle=yes" - ] + "bicycle=yes", + ], }, { - "and": [ + and: [ { - "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] + or: [ + "amenity=charging_station", + "disused:amenity=charging_station", + "planned:amenity=charging_station", + "construction:amenity=charging_station", + ], }, - ] + ], }, "amenity=toilets", "amenity=bench", "leisure=picnic_table", { - "and": [ - "tower:type=observation" - ] + and: ["tower:type=observation"], }, { - "and": [ - "amenity=bicycle_repair_station" - ] + and: ["amenity=bicycle_repair_station"], }, { - "and": [ + and: [ { - "or": [ + or: [ "amenity=bicycle_rental", "bicycle_rental~*", "service:bicycle:rental=yes", - "rental~.*bicycle.*" - ] + "rental~.*bicycle.*", + ], }, - "bicycle_rental!=docking_station" - ] + "bicycle_rental!=docking_station", + ], }, { - "and": [ - "leisure=playground", - "playground!=forest" - ] - } - ] - }); + and: ["leisure=playground", "playground!=forest"], + }, + ], + }) const opt = <TagsFilter>filter.optimize() - const expected = ["amenity=charging_station", + const expected = [ + "amenity=charging_station", "amenity=toilets", "amenity=bench", "amenity=bicycle_repair_station", @@ -194,11 +160,10 @@ describe("Tag optimalization", () => { "planned:amenity=charging_station", "tower:type=observation", "(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!=docking_station", - "leisure=playground&playground!=forest"] + "leisure=playground&playground!=forest", + ] - expect((<Or>opt).or.map(f => TagUtils.toString(f))).deep.eq( - expected - ) + expect((<Or>opt).or.map((f) => TagUtils.toString(f))).deep.eq(expected) }) it("should detect conflicting tags", () => { @@ -210,79 +175,54 @@ describe("Tag optimalization", () => { const q = new And([new Tag("key", "value"), new RegexTag("key", /value/, true)]) expect(q.optimize()).eq(false) }) - }) describe("Or", () => { - - it("with nested And which has a common property should be dropped", () => { - const t = new Or([ new Tag("foo", "bar"), - new And([ - new Tag("foo", "bar"), - new Tag("x", "y"), - ]) + new And([new Tag("foo", "bar"), new Tag("x", "y")]), ]) const opt = <TagsFilter>t.optimize() expect(TagUtils.toString(opt)).eq("foo=bar") - }) it("should flatten nested ors", () => { - const t = new Or([ - new Or([ - new Tag("x", "y") - ]) - ]).optimize() + const t = new Or([new Or([new Tag("x", "y")])]).optimize() expect(t).deep.eq(new Tag("x", "y")) }) it("should flatten nested ors", () => { - const t = new Or([ - new Tag("a", "b"), - new Or([ - new Tag("x", "y") - ]) - ]).optimize() + const t = new Or([new Tag("a", "b"), new Or([new Tag("x", "y")])]).optimize() expect(t).deep.eq(new Or([new Tag("a", "b"), new Tag("x", "y")])) }) - }) it("should not generate a conflict for climbing tags", () => { - const club_tags = TagUtils.Tag( - { - "or": [ - "club=climbing", - { - "and": [ - "sport=climbing", - { - "or": [ - "office~*", - "club~*" - ] - } - ] - } - ] - }) + const club_tags = TagUtils.Tag({ + or: [ + "club=climbing", + { + and: [ + "sport=climbing", + { + or: ["office~*", "club~*"], + }, + ], + }, + ], + }) const gym_tags = TagUtils.Tag({ - "and": [ - "sport=climbing", - "leisure=sports_centre" - ] + and: ["sport=climbing", "leisure=sports_centre"], }) const other_climbing = TagUtils.Tag({ - "and": [ + and: [ "sport=climbing", "climbing!~route", "leisure!~sports_centre", "climbing!=route_top", - "climbing!=route_bottom" - ] + "climbing!=route_bottom", + ], }) const together = new Or([club_tags, gym_tags, other_climbing]) const opt = together.optimize() @@ -319,17 +259,16 @@ describe("Tag optimalization", () => { ) */ - expect(opt).deep.eq( TagUtils.Tag({ or: [ "club=climbing", { - and: ["sport=climbing", - {or: ["club~*", "office~*"]}] + and: ["sport=climbing", { or: ["club~*", "office~*"] }], }, { - and: ["sport=climbing", + and: [ + "sport=climbing", { or: [ "leisure=sports_centre", @@ -338,16 +277,15 @@ describe("Tag optimalization", () => { "climbing!~route", "climbing!=route_top", "climbing!=route_bottom", - "leisure!~sports_centre" - ] - } - ] - }] - } - + "leisure!~sports_centre", + ], + }, + ], + }, + ], + }, ], - }) ) }) -}) \ No newline at end of file +}) diff --git a/test/Logic/Tags/TagUtils.spec.ts b/test/Logic/Tags/TagUtils.spec.ts index 4112fec57..6945154f9 100644 --- a/test/Logic/Tags/TagUtils.spec.ts +++ b/test/Logic/Tags/TagUtils.spec.ts @@ -1,52 +1,49 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {TagUtils} from "../../../Logic/Tags/TagUtils"; -import {equal} from "assert"; +import { describe } from "mocha" +import { expect } from "chai" +import { TagUtils } from "../../../Logic/Tags/TagUtils" +import { equal } from "assert" describe("TagUtils", () => { - describe("ParseTag", () => { - - it("should refuse a key!=* tag", () => { - expect(() => TagUtils.Tag("key!=*")).to.throw(); + expect(() => TagUtils.Tag("key!=*")).to.throw() }) it("should handle compare tag <=5", () => { let compare = TagUtils.Tag("key<=5") - equal(compare.matchesProperties({"key": undefined}), false); - equal(compare.matchesProperties({"key": "6"}), false); - equal(compare.matchesProperties({"key": "5"}), true); - equal(compare.matchesProperties({"key": "4"}), true); + equal(compare.matchesProperties({ key: undefined }), false) + equal(compare.matchesProperties({ key: "6" }), false) + equal(compare.matchesProperties({ key: "5" }), true) + equal(compare.matchesProperties({ key: "4" }), true) }) - + it("should handle compare tag < 5", () => { const compare = TagUtils.Tag("key<5") - equal(compare.matchesProperties({"key": undefined}), false); - equal(compare.matchesProperties({"key": "6"}), false); - equal(compare.matchesProperties({"key": "5"}), false); - equal(compare.matchesProperties({"key": "4.2"}), true); + equal(compare.matchesProperties({ key: undefined }), false) + equal(compare.matchesProperties({ key: "6" }), false) + equal(compare.matchesProperties({ key: "5" }), false) + equal(compare.matchesProperties({ key: "4.2" }), true) }) it("should handle compare tag >5", () => { const compare = TagUtils.Tag("key>5") - equal(compare.matchesProperties({"key": undefined}), false); - equal(compare.matchesProperties({"key": "6"}), true); - equal(compare.matchesProperties({"key": "5"}), false); - equal(compare.matchesProperties({"key": "4.2"}), false); + equal(compare.matchesProperties({ key: undefined }), false) + equal(compare.matchesProperties({ key: "6" }), true) + equal(compare.matchesProperties({ key: "5" }), false) + equal(compare.matchesProperties({ key: "4.2" }), false) }) it("should handle compare tag >=5", () => { const compare = TagUtils.Tag("key>=5") - equal(compare.matchesProperties({"key": undefined}), false); - equal(compare.matchesProperties({"key": "6"}), true); - equal(compare.matchesProperties({"key": "5"}), true); - equal(compare.matchesProperties({"key": "4.2"}), false); + equal(compare.matchesProperties({ key: undefined }), false) + equal(compare.matchesProperties({ key: "6" }), true) + equal(compare.matchesProperties({ key: "5" }), true) + equal(compare.matchesProperties({ key: "4.2" }), false) }) - + it("should handle date comparison tags", () => { const filter = TagUtils.Tag("date_created<2022-01-07") - expect(filter.matchesProperties({"date_created": "2022-01-08"})).false - expect(filter.matchesProperties({"date_created": "2022-01-01"})).true + expect(filter.matchesProperties({ date_created: "2022-01-08" })).false + expect(filter.matchesProperties({ date_created: "2022-01-01" })).true }) }) }) diff --git a/test/Logic/Web/Wikidata.spec.ts b/test/Logic/Web/Wikidata.spec.ts index 23969a501..5fb8fe601 100644 --- a/test/Logic/Web/Wikidata.spec.ts +++ b/test/Logic/Web/Wikidata.spec.ts @@ -1,7499 +1,9484 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../../Utils"; -import Wikidata from "../../../Logic/Web/Wikidata"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../../Utils" +import Wikidata from "../../../Logic/Web/Wikidata" const Q140 = { - "entities": { - "Q140": { - "pageid": 275, - "ns": 0, - "title": "Q140", - "lastrevid": 1503881580, - "modified": "2021-09-26T19:53:55Z", - "type": "item", - "id": "Q140", - "labels": { - "fr": {"language": "fr", "value": "lion"}, - "it": {"language": "it", "value": "leone"}, - "nb": {"language": "nb", "value": "l\u00f8ve"}, - "ru": {"language": "ru", "value": "\u043b\u0435\u0432"}, - "de": {"language": "de", "value": "L\u00f6we"}, - "es": {"language": "es", "value": "le\u00f3n"}, - "nn": {"language": "nn", "value": "l\u00f8ve"}, - "da": {"language": "da", "value": "l\u00f8ve"}, - "af": {"language": "af", "value": "leeu"}, - "ar": {"language": "ar", "value": "\u0623\u0633\u062f"}, - "bg": {"language": "bg", "value": "\u043b\u044a\u0432"}, - "bn": {"language": "bn", "value": "\u09b8\u09bf\u0982\u09b9"}, - "br": {"language": "br", "value": "leon"}, - "bs": {"language": "bs", "value": "lav"}, - "ca": {"language": "ca", "value": "lle\u00f3"}, - "cs": {"language": "cs", "value": "lev"}, - "el": {"language": "el", "value": "\u03bb\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9"}, - "fi": {"language": "fi", "value": "leijona"}, - "ga": {"language": "ga", "value": "leon"}, - "gl": {"language": "gl", "value": "Le\u00f3n"}, - "gu": {"language": "gu", "value": "\u0ab8\u0abf\u0a82\u0ab9"}, - "he": {"language": "he", "value": "\u05d0\u05e8\u05d9\u05d4"}, - "hi": {"language": "hi", "value": "\u0938\u093f\u0902\u0939"}, - "hu": {"language": "hu", "value": "oroszl\u00e1n"}, - "id": {"language": "id", "value": "Singa"}, - "ja": {"language": "ja", "value": "\u30e9\u30a4\u30aa\u30f3"}, - "ko": {"language": "ko", "value": "\uc0ac\uc790"}, - "mk": {"language": "mk", "value": "\u043b\u0430\u0432"}, - "ml": {"language": "ml", "value": "\u0d38\u0d3f\u0d02\u0d39\u0d02"}, - "mr": {"language": "mr", "value": "\u0938\u093f\u0902\u0939"}, - "my": {"language": "my", "value": "\u1001\u103c\u1004\u103a\u1039\u101e\u1031\u1037"}, - "ne": {"language": "ne", "value": "\u0938\u093f\u0902\u0939"}, - "nl": {"language": "nl", "value": "leeuw"}, - "pl": {"language": "pl", "value": "lew afryka\u0144ski"}, - "pt": {"language": "pt", "value": "le\u00e3o"}, - "pt-br": {"language": "pt-br", "value": "le\u00e3o"}, - "scn": {"language": "scn", "value": "liuni"}, - "sq": {"language": "sq", "value": "Luani"}, - "sr": {"language": "sr", "value": "\u043b\u0430\u0432"}, - "sw": {"language": "sw", "value": "simba"}, - "ta": {"language": "ta", "value": "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd"}, - "te": {"language": "te", "value": "\u0c38\u0c3f\u0c02\u0c39\u0c02"}, - "th": {"language": "th", "value": "\u0e2a\u0e34\u0e07\u0e42\u0e15"}, - "tr": {"language": "tr", "value": "aslan"}, - "uk": {"language": "uk", "value": "\u043b\u0435\u0432"}, - "vi": {"language": "vi", "value": "s\u01b0 t\u1eed"}, - "zh": {"language": "zh", "value": "\u7345\u5b50"}, - "sco": {"language": "sco", "value": "lion"}, - "zh-hant": {"language": "zh-hant", "value": "\u7345\u5b50"}, - "fa": {"language": "fa", "value": "\u0634\u06cc\u0631"}, - "zh-hans": {"language": "zh-hans", "value": "\u72ee\u5b50"}, - "ee": {"language": "ee", "value": "Dzata"}, - "ilo": {"language": "ilo", "value": "leon"}, - "ksh": {"language": "ksh", "value": "L\u00f6hv"}, - "zh-hk": {"language": "zh-hk", "value": "\u7345\u5b50"}, - "as": {"language": "as", "value": "\u09b8\u09bf\u0982\u09b9"}, - "zh-cn": {"language": "zh-cn", "value": "\u72ee\u5b50"}, - "zh-mo": {"language": "zh-mo", "value": "\u7345\u5b50"}, - "zh-my": {"language": "zh-my", "value": "\u72ee\u5b50"}, - "zh-sg": {"language": "zh-sg", "value": "\u72ee\u5b50"}, - "zh-tw": {"language": "zh-tw", "value": "\u7345\u5b50"}, - "ast": {"language": "ast", "value": "lle\u00f3n"}, - "sat": {"language": "sat", "value": "\u1c61\u1c5f\u1c74\u1c5f\u1c60\u1c69\u1c5e"}, - "bho": {"language": "bho", "value": "\u0938\u093f\u0902\u0939"}, - "en": {"language": "en", "value": "lion"}, - "ks": {"language": "ks", "value": "\u067e\u0627\u062f\u064e\u0631 \u0633\u0655\u06c1\u06c1"}, - "be-tarask": {"language": "be-tarask", "value": "\u043b\u0435\u045e"}, - "nan": {"language": "nan", "value": "Sai"}, - "la": {"language": "la", "value": "leo"}, - "en-ca": {"language": "en-ca", "value": "Lion"}, - "en-gb": {"language": "en-gb", "value": "lion"}, - "ab": {"language": "ab", "value": "\u0410\u043b\u044b\u043c"}, - "am": {"language": "am", "value": "\u12a0\u1295\u1260\u1233"}, - "an": {"language": "an", "value": "Panthera leo"}, - "ang": {"language": "ang", "value": "L\u0113o"}, - "arc": {"language": "arc", "value": "\u0710\u072a\u071d\u0710"}, - "arz": {"language": "arz", "value": "\u0633\u0628\u0639"}, - "av": {"language": "av", "value": "\u0413\u044a\u0430\u043b\u0431\u0430\u0446\u04c0"}, - "az": {"language": "az", "value": "\u015eir"}, - "ba": {"language": "ba", "value": "\u0410\u0440\u044b\u04ab\u043b\u0430\u043d"}, - "be": {"language": "be", "value": "\u043b\u0435\u045e"}, - "bo": {"language": "bo", "value": "\u0f66\u0f7a\u0f44\u0f0b\u0f42\u0f7a\u0f0d"}, - "bpy": {"language": "bpy", "value": "\u09a8\u0982\u09b8\u09be"}, - "bxr": {"language": "bxr", "value": "\u0410\u0440\u0441\u0430\u043b\u0430\u043d"}, - "ce": {"language": "ce", "value": "\u041b\u043e\u043c"}, - "chr": {"language": "chr", "value": "\u13e2\u13d3\u13e5 \u13a4\u13cd\u13c6\u13b4\u13c2"}, - "chy": {"language": "chy", "value": "P\u00e9hpe'\u00e9nan\u00f3se'hame"}, - "ckb": {"language": "ckb", "value": "\u0634\u06ce\u0631"}, - "co": {"language": "co", "value": "Lionu"}, - "csb": {"language": "csb", "value": "Lew"}, - "cu": {"language": "cu", "value": "\u041b\u044c\u0432\u044a"}, - "cv": {"language": "cv", "value": "\u0410\u0440\u0103\u0441\u043b\u0430\u043d"}, - "cy": {"language": "cy", "value": "Llew"}, - "dsb": {"language": "dsb", "value": "law"}, - "eo": {"language": "eo", "value": "leono"}, - "et": {"language": "et", "value": "l\u00f5vi"}, - "eu": {"language": "eu", "value": "lehoi"}, - "fo": {"language": "fo", "value": "leyvur"}, - "frr": {"language": "frr", "value": "l\u00f6\u00f6w"}, - "gag": {"language": "gag", "value": "aslan"}, - "gd": {"language": "gd", "value": "le\u00f2mhann"}, - "gn": {"language": "gn", "value": "Le\u00f5"}, - "got": {"language": "got", "value": "\ud800\udf3b\ud800\udf39\ud800\udf45\ud800\udf30/Liwa"}, - "ha": {"language": "ha", "value": "Zaki"}, - "hak": {"language": "hak", "value": "S\u1e73\u0302-\u00e9"}, - "haw": {"language": "haw", "value": "Liona"}, - "hif": {"language": "hif", "value": "Ser"}, - "hr": {"language": "hr", "value": "lav"}, - "hsb": {"language": "hsb", "value": "law"}, - "ht": {"language": "ht", "value": "Lyon"}, - "hy": {"language": "hy", "value": "\u0561\u057c\u0575\u0578\u0582\u056e"}, - "ia": {"language": "ia", "value": "Panthera leo"}, - "ig": {"language": "ig", "value": "Od\u00fam"}, - "io": {"language": "io", "value": "leono"}, - "is": {"language": "is", "value": "lj\u00f3n"}, - "jbo": {"language": "jbo", "value": "cinfo"}, - "jv": {"language": "jv", "value": "Singa"}, - "ka": {"language": "ka", "value": "\u10da\u10dd\u10db\u10d8"}, - "kab": {"language": "kab", "value": "Izem"}, - "kbd": {"language": "kbd", "value": "\u0425\u044c\u044d\u0449"}, - "kg": {"language": "kg", "value": "Nkosi"}, - "kk": {"language": "kk", "value": "\u0410\u0440\u044b\u0441\u0442\u0430\u043d"}, - "kn": {"language": "kn", "value": "\u0cb8\u0cbf\u0c82\u0cb9"}, - "ku": {"language": "ku", "value": "\u015e\u00ear"}, - "lb": {"language": "lb", "value": "L\u00e9iw"}, - "lbe": {"language": "lbe", "value": "\u0410\u0441\u043b\u0430\u043d"}, - "lez": {"language": "lez", "value": "\u0410\u0441\u043b\u0430\u043d"}, - "li": {"language": "li", "value": "Liew"}, - "lij": {"language": "lij", "value": "Lion"}, - "ln": {"language": "ln", "value": "Nk\u0254\u0301si"}, - "lt": {"language": "lt", "value": "li\u016btas"}, - "ltg": {"language": "ltg", "value": "\u013bovs"}, - "lv": {"language": "lv", "value": "lauva"}, - "mdf": {"language": "mdf", "value": "\u041e\u0440\u043a\u0441\u043e\u0444\u0442\u0430"}, - "mhr": {"language": "mhr", "value": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d"}, - "mn": {"language": "mn", "value": "\u0410\u0440\u0441\u043b\u0430\u043d"}, - "mrj": {"language": "mrj", "value": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d"}, - "ms": {"language": "ms", "value": "Singa"}, - "mt": {"language": "mt", "value": "iljun"}, - "nah": {"language": "nah", "value": "Cu\u0101miztli"}, - "nrm": {"language": "nrm", "value": "lion"}, - "su": {"language": "su", "value": "Singa"}, - "de-ch": {"language": "de-ch", "value": "L\u00f6we"}, - "ky": {"language": "ky", "value": "\u0410\u0440\u0441\u0442\u0430\u043d"}, - "lmo": {"language": "lmo", "value": "Panthera leo"}, - "ceb": {"language": "ceb", "value": "Panthera leo"}, - "diq": {"language": "diq", "value": "\u015e\u00ear"}, - "new": {"language": "new", "value": "\u0938\u093f\u0902\u0939"}, - "nds": {"language": "nds", "value": "L\u00f6\u00f6w"}, - "ak": {"language": "ak", "value": "Gyata"}, - "cdo": {"language": "cdo", "value": "S\u0103i"}, - "ady": {"language": "ady", "value": "\u0410\u0441\u043b\u044a\u0430\u043d"}, - "azb": {"language": "azb", "value": "\u0622\u0633\u0644\u0627\u0646"}, - "lfn": {"language": "lfn", "value": "Leon"}, - "kbp": {"language": "kbp", "value": "T\u0254\u0254y\u028b\u028b"}, - "gsw": {"language": "gsw", "value": "L\u00f6we"}, - "din": {"language": "din", "value": "K\u00f6r"}, - "inh": {"language": "inh", "value": "\u041b\u043e\u043c"}, - "bm": {"language": "bm", "value": "Waraba"}, - "hyw": {"language": "hyw", "value": "\u0531\u057c\u056b\u0582\u056e"}, - "nds-nl": {"language": "nds-nl", "value": "leeuw"}, - "kw": {"language": "kw", "value": "Lew"}, - "ext": {"language": "ext", "value": "Le\u00f3n"}, - "bcl": {"language": "bcl", "value": "Leon"}, - "mg": {"language": "mg", "value": "Liona"}, - "lld": {"language": "lld", "value": "Lion"}, - "lzh": {"language": "lzh", "value": "\u7345"}, - "ary": {"language": "ary", "value": "\u0633\u0628\u0639"}, - "sv": {"language": "sv", "value": "lejon"}, - "nso": {"language": "nso", "value": "Tau"}, - "nv": { - "language": "nv", - "value": "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed" + entities: { + Q140: { + pageid: 275, + ns: 0, + title: "Q140", + lastrevid: 1503881580, + modified: "2021-09-26T19:53:55Z", + type: "item", + id: "Q140", + labels: { + fr: { language: "fr", value: "lion" }, + it: { language: "it", value: "leone" }, + nb: { language: "nb", value: "l\u00f8ve" }, + ru: { language: "ru", value: "\u043b\u0435\u0432" }, + de: { language: "de", value: "L\u00f6we" }, + es: { language: "es", value: "le\u00f3n" }, + nn: { language: "nn", value: "l\u00f8ve" }, + da: { language: "da", value: "l\u00f8ve" }, + af: { language: "af", value: "leeu" }, + ar: { language: "ar", value: "\u0623\u0633\u062f" }, + bg: { language: "bg", value: "\u043b\u044a\u0432" }, + bn: { language: "bn", value: "\u09b8\u09bf\u0982\u09b9" }, + br: { language: "br", value: "leon" }, + bs: { language: "bs", value: "lav" }, + ca: { language: "ca", value: "lle\u00f3" }, + cs: { language: "cs", value: "lev" }, + el: { language: "el", value: "\u03bb\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9" }, + fi: { language: "fi", value: "leijona" }, + ga: { language: "ga", value: "leon" }, + gl: { language: "gl", value: "Le\u00f3n" }, + gu: { language: "gu", value: "\u0ab8\u0abf\u0a82\u0ab9" }, + he: { language: "he", value: "\u05d0\u05e8\u05d9\u05d4" }, + hi: { language: "hi", value: "\u0938\u093f\u0902\u0939" }, + hu: { language: "hu", value: "oroszl\u00e1n" }, + id: { language: "id", value: "Singa" }, + ja: { language: "ja", value: "\u30e9\u30a4\u30aa\u30f3" }, + ko: { language: "ko", value: "\uc0ac\uc790" }, + mk: { language: "mk", value: "\u043b\u0430\u0432" }, + ml: { language: "ml", value: "\u0d38\u0d3f\u0d02\u0d39\u0d02" }, + mr: { language: "mr", value: "\u0938\u093f\u0902\u0939" }, + my: { language: "my", value: "\u1001\u103c\u1004\u103a\u1039\u101e\u1031\u1037" }, + ne: { language: "ne", value: "\u0938\u093f\u0902\u0939" }, + nl: { language: "nl", value: "leeuw" }, + pl: { language: "pl", value: "lew afryka\u0144ski" }, + pt: { language: "pt", value: "le\u00e3o" }, + "pt-br": { language: "pt-br", value: "le\u00e3o" }, + scn: { language: "scn", value: "liuni" }, + sq: { language: "sq", value: "Luani" }, + sr: { language: "sr", value: "\u043b\u0430\u0432" }, + sw: { language: "sw", value: "simba" }, + ta: { language: "ta", value: "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd" }, + te: { language: "te", value: "\u0c38\u0c3f\u0c02\u0c39\u0c02" }, + th: { language: "th", value: "\u0e2a\u0e34\u0e07\u0e42\u0e15" }, + tr: { language: "tr", value: "aslan" }, + uk: { language: "uk", value: "\u043b\u0435\u0432" }, + vi: { language: "vi", value: "s\u01b0 t\u1eed" }, + zh: { language: "zh", value: "\u7345\u5b50" }, + sco: { language: "sco", value: "lion" }, + "zh-hant": { language: "zh-hant", value: "\u7345\u5b50" }, + fa: { language: "fa", value: "\u0634\u06cc\u0631" }, + "zh-hans": { language: "zh-hans", value: "\u72ee\u5b50" }, + ee: { language: "ee", value: "Dzata" }, + ilo: { language: "ilo", value: "leon" }, + ksh: { language: "ksh", value: "L\u00f6hv" }, + "zh-hk": { language: "zh-hk", value: "\u7345\u5b50" }, + as: { language: "as", value: "\u09b8\u09bf\u0982\u09b9" }, + "zh-cn": { language: "zh-cn", value: "\u72ee\u5b50" }, + "zh-mo": { language: "zh-mo", value: "\u7345\u5b50" }, + "zh-my": { language: "zh-my", value: "\u72ee\u5b50" }, + "zh-sg": { language: "zh-sg", value: "\u72ee\u5b50" }, + "zh-tw": { language: "zh-tw", value: "\u7345\u5b50" }, + ast: { language: "ast", value: "lle\u00f3n" }, + sat: { language: "sat", value: "\u1c61\u1c5f\u1c74\u1c5f\u1c60\u1c69\u1c5e" }, + bho: { language: "bho", value: "\u0938\u093f\u0902\u0939" }, + en: { language: "en", value: "lion" }, + ks: { + language: "ks", + value: "\u067e\u0627\u062f\u064e\u0631 \u0633\u0655\u06c1\u06c1", }, - "oc": {"language": "oc", "value": "panthera leo"}, - "or": {"language": "or", "value": "\u0b38\u0b3f\u0b02\u0b39"}, - "os": {"language": "os", "value": "\u0426\u043e\u043c\u0430\u0445\u044a"}, - "pa": {"language": "pa", "value": "\u0a38\u0a3c\u0a47\u0a30"}, - "pam": {"language": "pam", "value": "Leon"}, - "pcd": {"language": "pcd", "value": "Lion"}, - "pms": {"language": "pms", "value": "Lion"}, - "pnb": {"language": "pnb", "value": "\u0628\u0628\u0631 \u0634\u06cc\u0631"}, - "ps": {"language": "ps", "value": "\u0632\u0645\u0631\u06cc"}, - "qu": {"language": "qu", "value": "Liyun"}, - "rn": {"language": "rn", "value": "Intare"}, - "ro": {"language": "ro", "value": "Leul"}, - "sl": {"language": "sl", "value": "lev"}, - "sn": {"language": "sn", "value": "Shumba"}, - "so": {"language": "so", "value": "Libaax"}, - "ss": {"language": "ss", "value": "Libubesi"}, - "st": {"language": "st", "value": "Tau"}, - "stq": {"language": "stq", "value": "Leeuwe"}, - "sr-ec": {"language": "sr-ec", "value": "\u043b\u0430\u0432"}, - "sr-el": {"language": "sr-el", "value": "lav"}, - "rm": {"language": "rm", "value": "Liun"}, - "sm": {"language": "sm", "value": "Leona"}, - "tcy": {"language": "tcy", "value": "\u0cb8\u0cbf\u0cae\u0ccd\u0cae"}, - "szl": {"language": "szl", "value": "Lew"}, - "rue": {"language": "rue", "value": "\u041b\u0435\u0432"}, - "rw": {"language": "rw", "value": "Intare"}, - "sah": {"language": "sah", "value": "\u0425\u0430\u0445\u0430\u0439"}, - "sh": {"language": "sh", "value": "Lav"}, - "sk": {"language": "sk", "value": "lev p\u00fa\u0161\u0165ov\u00fd"}, - "tg": {"language": "tg", "value": "\u0428\u0435\u0440"}, - "ti": {"language": "ti", "value": "\u12a3\u1295\u1260\u1233"}, - "tl": {"language": "tl", "value": "Leon"}, - "tum": {"language": "tum", "value": "Nkhalamu"}, - "udm": {"language": "udm", "value": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d"}, - "ug": {"language": "ug", "value": "\u0634\u0649\u0631"}, - "ur": {"language": "ur", "value": "\u0628\u0628\u0631"}, - "vec": {"language": "vec", "value": "Leon"}, - "vep": {"language": "vep", "value": "lev"}, - "vls": {"language": "vls", "value": "l\u00eaeuw"}, - "war": {"language": "war", "value": "leon"}, - "wo": {"language": "wo", "value": "gaynde"}, - "xal": {"language": "xal", "value": "\u0410\u0440\u0441\u043b\u04a3"}, - "xmf": {"language": "xmf", "value": "\u10dc\u10ef\u10d8\u10da\u10dd"}, - "yi": {"language": "yi", "value": "\u05dc\u05d9\u05d9\u05d1"}, - "yo": {"language": "yo", "value": "K\u00ecn\u00ec\u00fan"}, - "yue": {"language": "yue", "value": "\u7345\u5b50"}, - "zu": {"language": "zu", "value": "ibhubesi"}, - "tk": {"language": "tk", "value": "\u00ddolbars"}, - "tt": {"language": "tt", "value": "\u0430\u0440\u044b\u0441\u043b\u0430\u043d"}, - "uz": {"language": "uz", "value": "Arslon"}, - "se": {"language": "se", "value": "Ledjon"}, - "si": {"language": "si", "value": "\u0dc3\u0dd2\u0d82\u0dc4\u0dba\u0dcf"}, - "sgs": {"language": "sgs", "value": "Li\u016bts"}, - "vro": {"language": "vro", "value": "L\u00f5vi"}, - "xh": {"language": "xh", "value": "Ingonyama"}, - "sa": {"language": "sa", "value": "\u0938\u093f\u0902\u0939\u0903 \u092a\u0936\u0941\u0903"}, - "za": {"language": "za", "value": "Saeceij"}, - "sd": {"language": "sd", "value": "\u0628\u0628\u0631 \u0634\u064a\u0646\u0647\u0646"}, - "wuu": {"language": "wuu", "value": "\u72ee"}, - "shn": {"language": "shn", "value": "\u101e\u1062\u1004\u103a\u1087\u101e\u102e\u1088"}, - "alt": {"language": "alt", "value": "\u0410\u0440\u0441\u043b\u0430\u043d"}, - "avk": {"language": "avk", "value": "Krapol (Panthera leo)"}, - "dag": {"language": "dag", "value": "Gbu\u0263inli"}, - "shi": {"language": "shi", "value": "Agrzam"}, - "mni": {"language": "mni", "value": "\uabc5\uabe3\uabe1\uabc1\uabe5"} + "be-tarask": { language: "be-tarask", value: "\u043b\u0435\u045e" }, + nan: { language: "nan", value: "Sai" }, + la: { language: "la", value: "leo" }, + "en-ca": { language: "en-ca", value: "Lion" }, + "en-gb": { language: "en-gb", value: "lion" }, + ab: { language: "ab", value: "\u0410\u043b\u044b\u043c" }, + am: { language: "am", value: "\u12a0\u1295\u1260\u1233" }, + an: { language: "an", value: "Panthera leo" }, + ang: { language: "ang", value: "L\u0113o" }, + arc: { language: "arc", value: "\u0710\u072a\u071d\u0710" }, + arz: { language: "arz", value: "\u0633\u0628\u0639" }, + av: { language: "av", value: "\u0413\u044a\u0430\u043b\u0431\u0430\u0446\u04c0" }, + az: { language: "az", value: "\u015eir" }, + ba: { language: "ba", value: "\u0410\u0440\u044b\u04ab\u043b\u0430\u043d" }, + be: { language: "be", value: "\u043b\u0435\u045e" }, + bo: { language: "bo", value: "\u0f66\u0f7a\u0f44\u0f0b\u0f42\u0f7a\u0f0d" }, + bpy: { language: "bpy", value: "\u09a8\u0982\u09b8\u09be" }, + bxr: { language: "bxr", value: "\u0410\u0440\u0441\u0430\u043b\u0430\u043d" }, + ce: { language: "ce", value: "\u041b\u043e\u043c" }, + chr: { + language: "chr", + value: "\u13e2\u13d3\u13e5 \u13a4\u13cd\u13c6\u13b4\u13c2", + }, + chy: { language: "chy", value: "P\u00e9hpe'\u00e9nan\u00f3se'hame" }, + ckb: { language: "ckb", value: "\u0634\u06ce\u0631" }, + co: { language: "co", value: "Lionu" }, + csb: { language: "csb", value: "Lew" }, + cu: { language: "cu", value: "\u041b\u044c\u0432\u044a" }, + cv: { language: "cv", value: "\u0410\u0440\u0103\u0441\u043b\u0430\u043d" }, + cy: { language: "cy", value: "Llew" }, + dsb: { language: "dsb", value: "law" }, + eo: { language: "eo", value: "leono" }, + et: { language: "et", value: "l\u00f5vi" }, + eu: { language: "eu", value: "lehoi" }, + fo: { language: "fo", value: "leyvur" }, + frr: { language: "frr", value: "l\u00f6\u00f6w" }, + gag: { language: "gag", value: "aslan" }, + gd: { language: "gd", value: "le\u00f2mhann" }, + gn: { language: "gn", value: "Le\u00f5" }, + got: { + language: "got", + value: "\ud800\udf3b\ud800\udf39\ud800\udf45\ud800\udf30/Liwa", + }, + ha: { language: "ha", value: "Zaki" }, + hak: { language: "hak", value: "S\u1e73\u0302-\u00e9" }, + haw: { language: "haw", value: "Liona" }, + hif: { language: "hif", value: "Ser" }, + hr: { language: "hr", value: "lav" }, + hsb: { language: "hsb", value: "law" }, + ht: { language: "ht", value: "Lyon" }, + hy: { language: "hy", value: "\u0561\u057c\u0575\u0578\u0582\u056e" }, + ia: { language: "ia", value: "Panthera leo" }, + ig: { language: "ig", value: "Od\u00fam" }, + io: { language: "io", value: "leono" }, + is: { language: "is", value: "lj\u00f3n" }, + jbo: { language: "jbo", value: "cinfo" }, + jv: { language: "jv", value: "Singa" }, + ka: { language: "ka", value: "\u10da\u10dd\u10db\u10d8" }, + kab: { language: "kab", value: "Izem" }, + kbd: { language: "kbd", value: "\u0425\u044c\u044d\u0449" }, + kg: { language: "kg", value: "Nkosi" }, + kk: { language: "kk", value: "\u0410\u0440\u044b\u0441\u0442\u0430\u043d" }, + kn: { language: "kn", value: "\u0cb8\u0cbf\u0c82\u0cb9" }, + ku: { language: "ku", value: "\u015e\u00ear" }, + lb: { language: "lb", value: "L\u00e9iw" }, + lbe: { language: "lbe", value: "\u0410\u0441\u043b\u0430\u043d" }, + lez: { language: "lez", value: "\u0410\u0441\u043b\u0430\u043d" }, + li: { language: "li", value: "Liew" }, + lij: { language: "lij", value: "Lion" }, + ln: { language: "ln", value: "Nk\u0254\u0301si" }, + lt: { language: "lt", value: "li\u016btas" }, + ltg: { language: "ltg", value: "\u013bovs" }, + lv: { language: "lv", value: "lauva" }, + mdf: { language: "mdf", value: "\u041e\u0440\u043a\u0441\u043e\u0444\u0442\u0430" }, + mhr: { language: "mhr", value: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d" }, + mn: { language: "mn", value: "\u0410\u0440\u0441\u043b\u0430\u043d" }, + mrj: { language: "mrj", value: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d" }, + ms: { language: "ms", value: "Singa" }, + mt: { language: "mt", value: "iljun" }, + nah: { language: "nah", value: "Cu\u0101miztli" }, + nrm: { language: "nrm", value: "lion" }, + su: { language: "su", value: "Singa" }, + "de-ch": { language: "de-ch", value: "L\u00f6we" }, + ky: { language: "ky", value: "\u0410\u0440\u0441\u0442\u0430\u043d" }, + lmo: { language: "lmo", value: "Panthera leo" }, + ceb: { language: "ceb", value: "Panthera leo" }, + diq: { language: "diq", value: "\u015e\u00ear" }, + new: { language: "new", value: "\u0938\u093f\u0902\u0939" }, + nds: { language: "nds", value: "L\u00f6\u00f6w" }, + ak: { language: "ak", value: "Gyata" }, + cdo: { language: "cdo", value: "S\u0103i" }, + ady: { language: "ady", value: "\u0410\u0441\u043b\u044a\u0430\u043d" }, + azb: { language: "azb", value: "\u0622\u0633\u0644\u0627\u0646" }, + lfn: { language: "lfn", value: "Leon" }, + kbp: { language: "kbp", value: "T\u0254\u0254y\u028b\u028b" }, + gsw: { language: "gsw", value: "L\u00f6we" }, + din: { language: "din", value: "K\u00f6r" }, + inh: { language: "inh", value: "\u041b\u043e\u043c" }, + bm: { language: "bm", value: "Waraba" }, + hyw: { language: "hyw", value: "\u0531\u057c\u056b\u0582\u056e" }, + "nds-nl": { language: "nds-nl", value: "leeuw" }, + kw: { language: "kw", value: "Lew" }, + ext: { language: "ext", value: "Le\u00f3n" }, + bcl: { language: "bcl", value: "Leon" }, + mg: { language: "mg", value: "Liona" }, + lld: { language: "lld", value: "Lion" }, + lzh: { language: "lzh", value: "\u7345" }, + ary: { language: "ary", value: "\u0633\u0628\u0639" }, + sv: { language: "sv", value: "lejon" }, + nso: { language: "nso", value: "Tau" }, + nv: { + language: "nv", + value: "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed", + }, + oc: { language: "oc", value: "panthera leo" }, + or: { language: "or", value: "\u0b38\u0b3f\u0b02\u0b39" }, + os: { language: "os", value: "\u0426\u043e\u043c\u0430\u0445\u044a" }, + pa: { language: "pa", value: "\u0a38\u0a3c\u0a47\u0a30" }, + pam: { language: "pam", value: "Leon" }, + pcd: { language: "pcd", value: "Lion" }, + pms: { language: "pms", value: "Lion" }, + pnb: { language: "pnb", value: "\u0628\u0628\u0631 \u0634\u06cc\u0631" }, + ps: { language: "ps", value: "\u0632\u0645\u0631\u06cc" }, + qu: { language: "qu", value: "Liyun" }, + rn: { language: "rn", value: "Intare" }, + ro: { language: "ro", value: "Leul" }, + sl: { language: "sl", value: "lev" }, + sn: { language: "sn", value: "Shumba" }, + so: { language: "so", value: "Libaax" }, + ss: { language: "ss", value: "Libubesi" }, + st: { language: "st", value: "Tau" }, + stq: { language: "stq", value: "Leeuwe" }, + "sr-ec": { language: "sr-ec", value: "\u043b\u0430\u0432" }, + "sr-el": { language: "sr-el", value: "lav" }, + rm: { language: "rm", value: "Liun" }, + sm: { language: "sm", value: "Leona" }, + tcy: { language: "tcy", value: "\u0cb8\u0cbf\u0cae\u0ccd\u0cae" }, + szl: { language: "szl", value: "Lew" }, + rue: { language: "rue", value: "\u041b\u0435\u0432" }, + rw: { language: "rw", value: "Intare" }, + sah: { language: "sah", value: "\u0425\u0430\u0445\u0430\u0439" }, + sh: { language: "sh", value: "Lav" }, + sk: { language: "sk", value: "lev p\u00fa\u0161\u0165ov\u00fd" }, + tg: { language: "tg", value: "\u0428\u0435\u0440" }, + ti: { language: "ti", value: "\u12a3\u1295\u1260\u1233" }, + tl: { language: "tl", value: "Leon" }, + tum: { language: "tum", value: "Nkhalamu" }, + udm: { language: "udm", value: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d" }, + ug: { language: "ug", value: "\u0634\u0649\u0631" }, + ur: { language: "ur", value: "\u0628\u0628\u0631" }, + vec: { language: "vec", value: "Leon" }, + vep: { language: "vep", value: "lev" }, + vls: { language: "vls", value: "l\u00eaeuw" }, + war: { language: "war", value: "leon" }, + wo: { language: "wo", value: "gaynde" }, + xal: { language: "xal", value: "\u0410\u0440\u0441\u043b\u04a3" }, + xmf: { language: "xmf", value: "\u10dc\u10ef\u10d8\u10da\u10dd" }, + yi: { language: "yi", value: "\u05dc\u05d9\u05d9\u05d1" }, + yo: { language: "yo", value: "K\u00ecn\u00ec\u00fan" }, + yue: { language: "yue", value: "\u7345\u5b50" }, + zu: { language: "zu", value: "ibhubesi" }, + tk: { language: "tk", value: "\u00ddolbars" }, + tt: { language: "tt", value: "\u0430\u0440\u044b\u0441\u043b\u0430\u043d" }, + uz: { language: "uz", value: "Arslon" }, + se: { language: "se", value: "Ledjon" }, + si: { language: "si", value: "\u0dc3\u0dd2\u0d82\u0dc4\u0dba\u0dcf" }, + sgs: { language: "sgs", value: "Li\u016bts" }, + vro: { language: "vro", value: "L\u00f5vi" }, + xh: { language: "xh", value: "Ingonyama" }, + sa: { + language: "sa", + value: "\u0938\u093f\u0902\u0939\u0903 \u092a\u0936\u0941\u0903", + }, + za: { language: "za", value: "Saeceij" }, + sd: { language: "sd", value: "\u0628\u0628\u0631 \u0634\u064a\u0646\u0647\u0646" }, + wuu: { language: "wuu", value: "\u72ee" }, + shn: { language: "shn", value: "\u101e\u1062\u1004\u103a\u1087\u101e\u102e\u1088" }, + alt: { language: "alt", value: "\u0410\u0440\u0441\u043b\u0430\u043d" }, + avk: { language: "avk", value: "Krapol (Panthera leo)" }, + dag: { language: "dag", value: "Gbu\u0263inli" }, + shi: { language: "shi", value: "Agrzam" }, + mni: { language: "mni", value: "\uabc5\uabe3\uabe1\uabc1\uabe5" }, }, - "descriptions": { - "fr": {"language": "fr", "value": "esp\u00e8ce de mammif\u00e8res carnivores"}, - "it": {"language": "it", "value": "mammifero carnivoro della famiglia dei Felidi"}, - "nb": {"language": "nb", "value": "kattedyr"}, - "ru": { - "language": "ru", - "value": "\u0432\u0438\u0434 \u0445\u0438\u0449\u043d\u044b\u0445 \u043c\u043b\u0435\u043a\u043e\u043f\u0438\u0442\u0430\u044e\u0449\u0438\u0445" + descriptions: { + fr: { language: "fr", value: "esp\u00e8ce de mammif\u00e8res carnivores" }, + it: { language: "it", value: "mammifero carnivoro della famiglia dei Felidi" }, + nb: { language: "nb", value: "kattedyr" }, + ru: { + language: "ru", + value: "\u0432\u0438\u0434 \u0445\u0438\u0449\u043d\u044b\u0445 \u043c\u043b\u0435\u043a\u043e\u043f\u0438\u0442\u0430\u044e\u0449\u0438\u0445", }, - "de": {"language": "de", "value": "Art der Gattung Eigentliche Gro\u00dfkatzen (Panthera)"}, - "es": {"language": "es", "value": "mam\u00edfero carn\u00edvoro de la familia de los f\u00e9lidos"}, - "en": {"language": "en", "value": "species of big cat"}, - "ko": { - "language": "ko", - "value": "\uace0\uc591\uc774\uacfc\uc5d0 \uc18d\ud558\ub294 \uc721\uc2dd\ub3d9\ubb3c" + de: { + language: "de", + value: "Art der Gattung Eigentliche Gro\u00dfkatzen (Panthera)", }, - "ca": {"language": "ca", "value": "mam\u00edfer carn\u00edvor de la fam\u00edlia dels f\u00e8lids"}, - "fi": {"language": "fi", "value": "suuri kissael\u00e4in"}, + es: { + language: "es", + value: "mam\u00edfero carn\u00edvoro de la familia de los f\u00e9lidos", + }, + en: { language: "en", value: "species of big cat" }, + ko: { + language: "ko", + value: "\uace0\uc591\uc774\uacfc\uc5d0 \uc18d\ud558\ub294 \uc721\uc2dd\ub3d9\ubb3c", + }, + ca: { + language: "ca", + value: "mam\u00edfer carn\u00edvor de la fam\u00edlia dels f\u00e8lids", + }, + fi: { language: "fi", value: "suuri kissael\u00e4in" }, "pt-br": { - "language": "pt-br", - "value": "esp\u00e9cie de mam\u00edfero carn\u00edvoro do g\u00eanero Panthera e da fam\u00edlia Felidae" + language: "pt-br", + value: "esp\u00e9cie de mam\u00edfero carn\u00edvoro do g\u00eanero Panthera e da fam\u00edlia Felidae", }, - "ta": {"language": "ta", "value": "\u0bb5\u0bbf\u0bb2\u0b99\u0bcd\u0b95\u0bc1"}, - "nl": {"language": "nl", "value": "groot roofdier uit de familie der katachtigen"}, - "he": { - "language": "he", - "value": "\u05de\u05d9\u05df \u05d1\u05e1\u05d5\u05d2 \u05e4\u05e0\u05ea\u05e8, \u05d8\u05d5\u05e8\u05e3 \u05d2\u05d3\u05d5\u05dc \u05d1\u05de\u05e9\u05e4\u05d7\u05ea \u05d4\u05d7\u05ea\u05d5\u05dc\u05d9\u05d9\u05dd" + ta: { language: "ta", value: "\u0bb5\u0bbf\u0bb2\u0b99\u0bcd\u0b95\u0bc1" }, + nl: { language: "nl", value: "groot roofdier uit de familie der katachtigen" }, + he: { + language: "he", + value: "\u05de\u05d9\u05df \u05d1\u05e1\u05d5\u05d2 \u05e4\u05e0\u05ea\u05e8, \u05d8\u05d5\u05e8\u05e3 \u05d2\u05d3\u05d5\u05dc \u05d1\u05de\u05e9\u05e4\u05d7\u05ea \u05d4\u05d7\u05ea\u05d5\u05dc\u05d9\u05d9\u05dd", }, - "pt": {"language": "pt", "value": "esp\u00e9cie de felino"}, - "sco": {"language": "sco", "value": "species o big cat"}, - "zh-hans": {"language": "zh-hans", "value": "\u5927\u578b\u732b\u79d1\u52a8\u7269"}, - "uk": { - "language": "uk", - "value": "\u0432\u0438\u0434 \u043a\u043b\u0430\u0441\u0443 \u0441\u0441\u0430\u0432\u0446\u0456\u0432, \u0440\u044f\u0434\u0443 \u0445\u0438\u0436\u0438\u0445, \u0440\u043e\u0434\u0438\u043d\u0438 \u043a\u043e\u0442\u044f\u0447\u0438\u0445" + pt: { language: "pt", value: "esp\u00e9cie de felino" }, + sco: { language: "sco", value: "species o big cat" }, + "zh-hans": { language: "zh-hans", value: "\u5927\u578b\u732b\u79d1\u52a8\u7269" }, + uk: { + language: "uk", + value: "\u0432\u0438\u0434 \u043a\u043b\u0430\u0441\u0443 \u0441\u0441\u0430\u0432\u0446\u0456\u0432, \u0440\u044f\u0434\u0443 \u0445\u0438\u0436\u0438\u0445, \u0440\u043e\u0434\u0438\u043d\u0438 \u043a\u043e\u0442\u044f\u0447\u0438\u0445", }, - "hu": { - "language": "hu", - "value": "macskaf\u00e9l\u00e9k csal\u00e1dj\u00e1ba tartoz\u00f3 eml\u0151sfaj" + hu: { + language: "hu", + value: "macskaf\u00e9l\u00e9k csal\u00e1dj\u00e1ba tartoz\u00f3 eml\u0151sfaj", }, - "bn": { - "language": "bn", - "value": "\u099c\u0999\u09cd\u0997\u09b2\u09c7\u09b0 \u09b0\u09be\u099c\u09be" + bn: { + language: "bn", + value: "\u099c\u0999\u09cd\u0997\u09b2\u09c7\u09b0 \u09b0\u09be\u099c\u09be", }, - "hi": {"language": "hi", "value": "\u091c\u0902\u0917\u0932 \u0915\u093e \u0930\u093e\u091c\u093e"}, - "ilo": {"language": "ilo", "value": "sebbangan ti dakkel a pusa"}, - "ksh": { - "language": "ksh", - "value": "et jr\u00fch\u00dfde Kazedier op der \u00c4hd, der K\u00fcnning vun de Diehre" + hi: { + language: "hi", + value: "\u091c\u0902\u0917\u0932 \u0915\u093e \u0930\u093e\u091c\u093e", }, - "fa": { - "language": "fa", - "value": "\u06af\u0631\u0628\u0647\u200c \u0628\u0632\u0631\u06af \u0628\u0648\u0645\u06cc \u0622\u0641\u0631\u06cc\u0642\u0627 \u0648 \u0622\u0633\u06cc\u0627" + ilo: { language: "ilo", value: "sebbangan ti dakkel a pusa" }, + ksh: { + language: "ksh", + value: "et jr\u00fch\u00dfde Kazedier op der \u00c4hd, der K\u00fcnning vun de Diehre", }, - "gl": { - "language": "gl", - "value": "\u00e9 un mam\u00edfero carn\u00edvoro da familia dos f\u00e9lidos e unha das 4 especies do x\u00e9nero Panthera" + fa: { + language: "fa", + value: "\u06af\u0631\u0628\u0647\u200c \u0628\u0632\u0631\u06af \u0628\u0648\u0645\u06cc \u0622\u0641\u0631\u06cc\u0642\u0627 \u0648 \u0622\u0633\u06cc\u0627", }, - "sq": {"language": "sq", "value": "mace e madhe e familjes Felidae"}, - "el": { - "language": "el", - "value": "\u03b5\u03af\u03b4\u03bf\u03c2 \u03c3\u03b1\u03c1\u03ba\u03bf\u03c6\u03ac\u03b3\u03bf \u03b8\u03b7\u03bb\u03b1\u03c3\u03c4\u03b9\u03ba\u03cc" + gl: { + language: "gl", + value: "\u00e9 un mam\u00edfero carn\u00edvoro da familia dos f\u00e9lidos e unha das 4 especies do x\u00e9nero Panthera", }, - "scn": {"language": "scn", "value": "specia di mamm\u00ecfiru"}, - "bg": { - "language": "bg", - "value": "\u0432\u0438\u0434 \u0431\u043e\u0437\u0430\u0439\u043d\u0438\u043a" + sq: { language: "sq", value: "mace e madhe e familjes Felidae" }, + el: { + language: "el", + value: "\u03b5\u03af\u03b4\u03bf\u03c2 \u03c3\u03b1\u03c1\u03ba\u03bf\u03c6\u03ac\u03b3\u03bf \u03b8\u03b7\u03bb\u03b1\u03c3\u03c4\u03b9\u03ba\u03cc", }, - "ne": { - "language": "ne", - "value": "\u0920\u0942\u0932\u094b \u092c\u093f\u0930\u093e\u0932\u094b\u0915\u094b \u092a\u094d\u0930\u091c\u093e\u0924\u093f" + scn: { language: "scn", value: "specia di mamm\u00ecfiru" }, + bg: { + language: "bg", + value: "\u0432\u0438\u0434 \u0431\u043e\u0437\u0430\u0439\u043d\u0438\u043a", }, - "pl": {"language": "pl", "value": "gatunek ssaka z rodziny kotowatych"}, - "af": { - "language": "af", - "value": "Soogdier en roofdier van die familie Felidae, een van die \"groot katte\"" + ne: { + language: "ne", + value: "\u0920\u0942\u0932\u094b \u092c\u093f\u0930\u093e\u0932\u094b\u0915\u094b \u092a\u094d\u0930\u091c\u093e\u0924\u093f", }, - "mk": { - "language": "mk", - "value": "\u0432\u0438\u0434 \u0433\u043e\u043b\u0435\u043c\u0430 \u043c\u0430\u0447\u043a\u0430" + pl: { language: "pl", value: "gatunek ssaka z rodziny kotowatych" }, + af: { + language: "af", + value: 'Soogdier en roofdier van die familie Felidae, een van die "groot katte"', }, - "nn": {"language": "nn", "value": "kattedyr"}, - "zh-hant": {"language": "zh-hant", "value": "\u5927\u578b\u8c93\u79d1\u52d5\u7269"}, - "zh": { - "language": "zh", - "value": "\u4ea7\u81ea\u975e\u6d32\u548c\u4e9a\u6d32\u7684\u5927\u578b\u732b\u79d1\u52a8\u7269" + mk: { + language: "mk", + value: "\u0432\u0438\u0434 \u0433\u043e\u043b\u0435\u043c\u0430 \u043c\u0430\u0447\u043a\u0430", }, - "zh-cn": {"language": "zh-cn", "value": "\u5927\u578b\u732b\u79d1\u52a8\u7269"}, - "zh-hk": {"language": "zh-hk", "value": "\u5927\u578b\u8c93\u79d1\u52d5\u7269"}, - "zh-mo": {"language": "zh-mo", "value": "\u5927\u578b\u8c93\u79d1\u52d5\u7269"}, - "zh-my": {"language": "zh-my", "value": "\u5927\u578b\u732b\u79d1\u52a8\u7269"}, - "zh-sg": {"language": "zh-sg", "value": "\u5927\u578b\u732b\u79d1\u52a8\u7269"}, - "zh-tw": {"language": "zh-tw", "value": "\u5927\u578b\u8c93\u79d1\u52d5\u7269"}, - "sw": {"language": "sw", "value": "mnyama mla nyama kama paka mkubwa"}, - "th": { - "language": "th", - "value": "\u0e0a\u0e37\u0e48\u0e2d\u0e2a\u0e31\u0e15\u0e27\u0e4c\u0e1b\u0e48\u0e32\u0e0a\u0e19\u0e34\u0e14\u0e2b\u0e19\u0e36\u0e48\u0e07 \u0e2d\u0e22\u0e39\u0e48\u0e43\u0e19\u0e2a\u0e32\u0e22\u0e1e\u0e31\u0e19\u0e18\u0e38\u0e4c\u0e02\u0e2d\u0e07\u0e41\u0e21\u0e27\u0e43\u0e2b\u0e0d\u0e48" + nn: { language: "nn", value: "kattedyr" }, + "zh-hant": { language: "zh-hant", value: "\u5927\u578b\u8c93\u79d1\u52d5\u7269" }, + zh: { + language: "zh", + value: "\u4ea7\u81ea\u975e\u6d32\u548c\u4e9a\u6d32\u7684\u5927\u578b\u732b\u79d1\u52a8\u7269", }, - "ar": { - "language": "ar", - "value": "\u062d\u064a\u0648\u0627\u0646 \u0645\u0646 \u0627\u0644\u062b\u062f\u064a\u064a\u0627\u062a \u0645\u0646 \u0641\u0635\u064a\u0644\u0629 \u0627\u0644\u0633\u0646\u0648\u0631\u064a\u0627\u062a \u0648\u0623\u062d\u062f \u0627\u0644\u0633\u0646\u0648\u0631\u064a\u0627\u062a \u0627\u0644\u0623\u0631\u0628\u0639\u0629 \u0627\u0644\u0643\u0628\u064a\u0631\u0629 \u0627\u0644\u0645\u0646\u062a\u0645\u064a\u0629 \u0644\u062c\u0646\u0633 \u0627\u0644\u0646\u0645\u0631" + "zh-cn": { language: "zh-cn", value: "\u5927\u578b\u732b\u79d1\u52a8\u7269" }, + "zh-hk": { language: "zh-hk", value: "\u5927\u578b\u8c93\u79d1\u52d5\u7269" }, + "zh-mo": { language: "zh-mo", value: "\u5927\u578b\u8c93\u79d1\u52d5\u7269" }, + "zh-my": { language: "zh-my", value: "\u5927\u578b\u732b\u79d1\u52a8\u7269" }, + "zh-sg": { language: "zh-sg", value: "\u5927\u578b\u732b\u79d1\u52a8\u7269" }, + "zh-tw": { language: "zh-tw", value: "\u5927\u578b\u8c93\u79d1\u52d5\u7269" }, + sw: { language: "sw", value: "mnyama mla nyama kama paka mkubwa" }, + th: { + language: "th", + value: "\u0e0a\u0e37\u0e48\u0e2d\u0e2a\u0e31\u0e15\u0e27\u0e4c\u0e1b\u0e48\u0e32\u0e0a\u0e19\u0e34\u0e14\u0e2b\u0e19\u0e36\u0e48\u0e07 \u0e2d\u0e22\u0e39\u0e48\u0e43\u0e19\u0e2a\u0e32\u0e22\u0e1e\u0e31\u0e19\u0e18\u0e38\u0e4c\u0e02\u0e2d\u0e07\u0e41\u0e21\u0e27\u0e43\u0e2b\u0e0d\u0e48", }, - "ml": { - "language": "ml", - "value": "\u0d38\u0d38\u0d4d\u0d24\u0d28\u0d3f\u0d15\u0d33\u0d3f\u0d32\u0d46 \u0d2b\u0d46\u0d32\u0d3f\u0d21\u0d47 \u0d15\u0d41\u0d1f\u0d41\u0d02\u0d2c\u0d24\u0d4d\u0d24\u0d3f\u0d32\u0d46 \u0d2a\u0d3e\u0d28\u0d4d\u0d24\u0d31 \u0d1c\u0d28\u0d41\u0d38\u0d4d\u0d38\u0d3f\u0d7d \u0d09\u0d7e\u0d2a\u0d4d\u0d2a\u0d46\u0d1f\u0d4d\u0d1f \u0d12\u0d30\u0d41 \u0d35\u0d28\u0d4d\u0d2f\u0d1c\u0d40\u0d35\u0d3f\u0d2f\u0d3e\u0d23\u0d4d \u0d38\u0d3f\u0d02\u0d39\u0d02" + ar: { + language: "ar", + value: "\u062d\u064a\u0648\u0627\u0646 \u0645\u0646 \u0627\u0644\u062b\u062f\u064a\u064a\u0627\u062a \u0645\u0646 \u0641\u0635\u064a\u0644\u0629 \u0627\u0644\u0633\u0646\u0648\u0631\u064a\u0627\u062a \u0648\u0623\u062d\u062f \u0627\u0644\u0633\u0646\u0648\u0631\u064a\u0627\u062a \u0627\u0644\u0623\u0631\u0628\u0639\u0629 \u0627\u0644\u0643\u0628\u064a\u0631\u0629 \u0627\u0644\u0645\u0646\u062a\u0645\u064a\u0629 \u0644\u062c\u0646\u0633 \u0627\u0644\u0646\u0645\u0631", }, - "cs": {"language": "cs", "value": "ko\u010dkovit\u00e1 \u0161elma"}, - "gu": { - "language": "gu", - "value": "\u0aac\u0abf\u0ab2\u0abe\u0aa1\u0ac0 \u0ab5\u0a82\u0ab6\u0aa8\u0ac1\u0a82 \u0ab8\u0ab8\u0acd\u0aa4\u0aa8 \u0aaa\u0acd\u0ab0\u0abe\u0aa3\u0ac0" + ml: { + language: "ml", + value: "\u0d38\u0d38\u0d4d\u0d24\u0d28\u0d3f\u0d15\u0d33\u0d3f\u0d32\u0d46 \u0d2b\u0d46\u0d32\u0d3f\u0d21\u0d47 \u0d15\u0d41\u0d1f\u0d41\u0d02\u0d2c\u0d24\u0d4d\u0d24\u0d3f\u0d32\u0d46 \u0d2a\u0d3e\u0d28\u0d4d\u0d24\u0d31 \u0d1c\u0d28\u0d41\u0d38\u0d4d\u0d38\u0d3f\u0d7d \u0d09\u0d7e\u0d2a\u0d4d\u0d2a\u0d46\u0d1f\u0d4d\u0d1f \u0d12\u0d30\u0d41 \u0d35\u0d28\u0d4d\u0d2f\u0d1c\u0d40\u0d35\u0d3f\u0d2f\u0d3e\u0d23\u0d4d \u0d38\u0d3f\u0d02\u0d39\u0d02", }, - "mr": { - "language": "mr", - "value": "\u092e\u093e\u0902\u091c\u0930\u093e\u091a\u0940 \u092e\u094b\u0920\u0940 \u091c\u093e\u0924" + cs: { language: "cs", value: "ko\u010dkovit\u00e1 \u0161elma" }, + gu: { + language: "gu", + value: "\u0aac\u0abf\u0ab2\u0abe\u0aa1\u0ac0 \u0ab5\u0a82\u0ab6\u0aa8\u0ac1\u0a82 \u0ab8\u0ab8\u0acd\u0aa4\u0aa8 \u0aaa\u0acd\u0ab0\u0abe\u0aa3\u0ac0", }, - "sr": { - "language": "sr", - "value": "\u0432\u0435\u043b\u0438\u043a\u0438 \u0441\u0438\u0441\u0430\u0440 \u0438\u0437 \u043f\u043e\u0440\u043e\u0434\u0438\u0446\u0435 \u043c\u0430\u0447\u0430\u043a\u0430" + mr: { + language: "mr", + value: "\u092e\u093e\u0902\u091c\u0930\u093e\u091a\u0940 \u092e\u094b\u0920\u0940 \u091c\u093e\u0924", }, - "ast": {"language": "ast", "value": "especie de mam\u00edferu carn\u00edvoru"}, - "te": { - "language": "te", - "value": "\u0c2a\u0c46\u0c26\u0c4d\u0c26 \u0c2a\u0c3f\u0c32\u0c4d\u0c32\u0c3f \u0c1c\u0c24\u0c3f" + sr: { + language: "sr", + value: "\u0432\u0435\u043b\u0438\u043a\u0438 \u0441\u0438\u0441\u0430\u0440 \u0438\u0437 \u043f\u043e\u0440\u043e\u0434\u0438\u0446\u0435 \u043c\u0430\u0447\u0430\u043a\u0430", }, - "bho": { - "language": "bho", - "value": "\u092c\u093f\u0932\u093e\u0930\u092c\u0902\u0938 \u0915\u0947 \u092c\u0921\u093c\u0939\u0928 \u091c\u093e\u0928\u0935\u0930" + ast: { language: "ast", value: "especie de mam\u00edferu carn\u00edvoru" }, + te: { + language: "te", + value: "\u0c2a\u0c46\u0c26\u0c4d\u0c26 \u0c2a\u0c3f\u0c32\u0c4d\u0c32\u0c3f \u0c1c\u0c24\u0c3f", }, - "da": {"language": "da", "value": "en af de fem store katte i sl\u00e6gten Panthera"}, - "vi": {"language": "vi", "value": "M\u1ed9t lo\u00e0i m\u00e8o l\u1edbn thu\u1ed9c chi Panthera"}, - "ja": {"language": "ja", "value": "\u98df\u8089\u76ee\u30cd\u30b3\u79d1\u306e\u52d5\u7269"}, - "ga": {"language": "ga", "value": "speiceas cat"}, - "bs": {"language": "bs", "value": "vrsta velike ma\u010dke"}, - "tr": {"language": "tr", "value": "Afrika ve Asya'ya \u00f6zg\u00fc b\u00fcy\u00fck bir kedi"}, - "as": { - "language": "as", - "value": "\u09b8\u09cd\u09a4\u09a8\u09cd\u09af\u09aa\u09be\u09af\u09bc\u09c0 \u09aa\u09cd\u09f0\u09be\u09a3\u09c0" + bho: { + language: "bho", + value: "\u092c\u093f\u0932\u093e\u0930\u092c\u0902\u0938 \u0915\u0947 \u092c\u0921\u093c\u0939\u0928 \u091c\u093e\u0928\u0935\u0930", }, - "my": { - "language": "my", - "value": "\u1014\u102d\u102f\u1037\u1010\u102d\u102f\u1000\u103a\u101e\u1010\u1039\u1010\u101d\u102b \u1019\u103b\u102d\u102f\u1038\u1005\u102d\u1010\u103a (\u1000\u103c\u1031\u102c\u1004\u103a\u1019\u103b\u102d\u102f\u1038\u101b\u1004\u103a\u1038\u101d\u1004\u103a)" + da: { language: "da", value: "en af de fem store katte i sl\u00e6gten Panthera" }, + vi: { + language: "vi", + value: "M\u1ed9t lo\u00e0i m\u00e8o l\u1edbn thu\u1ed9c chi Panthera", }, - "id": {"language": "id", "value": "spesies hewan keluarga jenis kucing"}, - "ks": { - "language": "ks", - "value": "\u0628\u062c \u0628\u0631\u0627\u0631\u0646 \u06be\u0646\u062f \u06a9\u0633\u0645 \u06cc\u0633 \u0627\u0634\u06cc\u0627 \u062a\u06c1 \u0627\u0641\u0631\u06cc\u06a9\u0627 \u0645\u0646\u0632 \u0645\u0644\u0627\u0646 \u0686\u06be\u06c1" + ja: { + language: "ja", + value: "\u98df\u8089\u76ee\u30cd\u30b3\u79d1\u306e\u52d5\u7269", }, - "br": {"language": "br", "value": "bronneg kigdebrer"}, - "sat": { - "language": "sat", - "value": "\u1c75\u1c64\u1c68 \u1c68\u1c6e\u1c71 \u1c68\u1c5f\u1c61\u1c5f" + ga: { language: "ga", value: "speiceas cat" }, + bs: { language: "bs", value: "vrsta velike ma\u010dke" }, + tr: { + language: "tr", + value: "Afrika ve Asya'ya \u00f6zg\u00fc b\u00fcy\u00fck bir kedi", }, - "mni": { - "language": "mni", - "value": "\uabc2\uabdd\uabc2\uabdb\uabc0\uabe4 \uabc1\uabe5\uabcd\uabe4\uabe1 \uabc6\uabe5\uabd5 \uabc1\uabe5 \uabd1\uabc6\uabe7\uabd5\uabc1\uabe4\uabe1\uabd2\uabe4 \uabc3\uabc5\uabe8\uabe1\uabd7 \uabd1\uabc3" + as: { + language: "as", + value: "\u09b8\u09cd\u09a4\u09a8\u09cd\u09af\u09aa\u09be\u09af\u09bc\u09c0 \u09aa\u09cd\u09f0\u09be\u09a3\u09c0", }, - "ro": {"language": "ro", "value": "mamifer carnivor"} + my: { + language: "my", + value: "\u1014\u102d\u102f\u1037\u1010\u102d\u102f\u1000\u103a\u101e\u1010\u1039\u1010\u101d\u102b \u1019\u103b\u102d\u102f\u1038\u1005\u102d\u1010\u103a (\u1000\u103c\u1031\u102c\u1004\u103a\u1019\u103b\u102d\u102f\u1038\u101b\u1004\u103a\u1038\u101d\u1004\u103a)", + }, + id: { language: "id", value: "spesies hewan keluarga jenis kucing" }, + ks: { + language: "ks", + value: "\u0628\u062c \u0628\u0631\u0627\u0631\u0646 \u06be\u0646\u062f \u06a9\u0633\u0645 \u06cc\u0633 \u0627\u0634\u06cc\u0627 \u062a\u06c1 \u0627\u0641\u0631\u06cc\u06a9\u0627 \u0645\u0646\u0632 \u0645\u0644\u0627\u0646 \u0686\u06be\u06c1", + }, + br: { language: "br", value: "bronneg kigdebrer" }, + sat: { + language: "sat", + value: "\u1c75\u1c64\u1c68 \u1c68\u1c6e\u1c71 \u1c68\u1c5f\u1c61\u1c5f", + }, + mni: { + language: "mni", + value: "\uabc2\uabdd\uabc2\uabdb\uabc0\uabe4 \uabc1\uabe5\uabcd\uabe4\uabe1 \uabc6\uabe5\uabd5 \uabc1\uabe5 \uabd1\uabc6\uabe7\uabd5\uabc1\uabe4\uabe1\uabd2\uabe4 \uabc3\uabc5\uabe8\uabe1\uabd7 \uabd1\uabc3", + }, + ro: { language: "ro", value: "mamifer carnivor" }, }, - "aliases": { - "es": [{"language": "es", "value": "Panthera leo"}, {"language": "es", "value": "leon"}], - "en": [{"language": "en", "value": "Asiatic Lion"}, { - "language": "en", - "value": "Panthera leo" - }, {"language": "en", "value": "African lion"}, { - "language": "en", - "value": "the lion" - }, {"language": "en", "value": "\ud83e\udd81"}], - "pt-br": [{"language": "pt-br", "value": "Panthera leo"}], - "fr": [{"language": "fr", "value": "lionne"}, {"language": "fr", "value": "lionceau"}], - "zh": [{"language": "zh", "value": "\u9b03\u6bdb"}, { - "language": "zh", - "value": "\u72ee\u5b50" - }, {"language": "zh", "value": "\u7345"}, { - "language": "zh", - "value": "\u525b\u679c\u7345" - }, {"language": "zh", "value": "\u975e\u6d32\u72ee"}], - "de": [{"language": "de", "value": "Panthera leo"}], - "ca": [{"language": "ca", "value": "Panthera leo"}], - "sco": [{"language": "sco", "value": "Panthera leo"}], - "hu": [{"language": "hu", "value": "P. leo"}, {"language": "hu", "value": "Panthera leo"}], - "ilo": [{"language": "ilo", "value": "Panthera leo"}, {"language": "ilo", "value": "Felis leo"}], - "ksh": [{"language": "ksh", "value": "L\u00f6hw"}, { - "language": "ksh", - "value": "L\u00f6hf" - }, {"language": "ksh", "value": "L\u00f6v"}], - "gl": [{"language": "gl", "value": "Panthera leo"}], - "ja": [{"language": "ja", "value": "\u767e\u7363\u306e\u738b"}, { - "language": "ja", - "value": "\u7345\u5b50" - }, {"language": "ja", "value": "\u30b7\u30b7"}], - "sq": [{"language": "sq", "value": "Mbreti i Kafsh\u00ebve"}], - "el": [{"language": "el", "value": "\u03c0\u03b1\u03bd\u03b8\u03ae\u03c1"}], - "pl": [{"language": "pl", "value": "Panthera leo"}, {"language": "pl", "value": "lew"}], - "zh-hk": [{"language": "zh-hk", "value": "\u7345"}], - "af": [{"language": "af", "value": "Panthera leo"}], - "mk": [{"language": "mk", "value": "Panthera leo"}], - "ar": [{"language": "ar", "value": "\u0644\u064a\u062b"}], - "zh-hant": [{"language": "zh-hant", "value": "\u7345"}], - "zh-cn": [{"language": "zh-cn", "value": "\u72ee"}], - "zh-hans": [{"language": "zh-hans", "value": "\u72ee"}], - "zh-mo": [{"language": "zh-mo", "value": "\u7345"}], - "zh-my": [{"language": "zh-my", "value": "\u72ee"}], - "zh-sg": [{"language": "zh-sg", "value": "\u72ee"}], - "zh-tw": [{"language": "zh-tw", "value": "\u7345"}], - "gu": [{"language": "gu", "value": "\u0ab5\u0aa8\u0ab0\u0abe\u0a9c"}, { - "language": "gu", - "value": "\u0ab8\u0abe\u0ab5\u0a9c" - }, {"language": "gu", "value": "\u0a95\u0ac7\u0ab8\u0ab0\u0ac0"}], - "ast": [{"language": "ast", "value": "Panthera leo"}, { - "language": "ast", - "value": "lle\u00f3n africanu" - }], - "hi": [{ - "language": "hi", - "value": "\u092a\u0947\u0902\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b" - }], - "te": [{ - "language": "te", - "value": "\u0c2a\u0c3e\u0c02\u0c25\u0c47\u0c30\u0c3e \u0c32\u0c3f\u0c2f\u0c4b" - }], - "nl": [{"language": "nl", "value": "Panthera leo"}], - "bho": [{ - "language": "bho", - "value": "\u092a\u0948\u0928\u094d\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b" - }, { - "language": "bho", - "value": "\u092a\u0948\u0902\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b" - }], - "ru": [{ - "language": "ru", - "value": "\u0430\u0437\u0438\u0430\u0442\u0441\u043a\u0438\u0439 \u043b\u0435\u0432" - }, { - "language": "ru", - "value": "\u0431\u043e\u043b\u044c\u0448\u0430\u044f \u043a\u043e\u0448\u043a\u0430" - }, { - "language": "ru", - "value": "\u0446\u0430\u0440\u044c \u0437\u0432\u0435\u0440\u0435\u0439" - }, { - "language": "ru", - "value": "\u0430\u0444\u0440\u0438\u043a\u0430\u043d\u0441\u043a\u0438\u0439 \u043b\u0435\u0432" - }], - "ga": [{"language": "ga", "value": "Panthera leo"}], - "bg": [{"language": "bg", "value": "Panthera leo"}, { - "language": "bg", - "value": "\u043b\u044a\u0432\u0438\u0446\u0430" - }], - "sat": [{"language": "sat", "value": "\u1c60\u1c69\u1c5e"}], - "nan": [{"language": "nan", "value": "Panthera leo"}], - "la": [{"language": "la", "value": "Panthera leo"}], - "nds-nl": [{"language": "nds-nl", "value": "leywe"}] + aliases: { + es: [ + { language: "es", value: "Panthera leo" }, + { language: "es", value: "leon" }, + ], + en: [ + { language: "en", value: "Asiatic Lion" }, + { + language: "en", + value: "Panthera leo", + }, + { language: "en", value: "African lion" }, + { + language: "en", + value: "the lion", + }, + { language: "en", value: "\ud83e\udd81" }, + ], + "pt-br": [{ language: "pt-br", value: "Panthera leo" }], + fr: [ + { language: "fr", value: "lionne" }, + { language: "fr", value: "lionceau" }, + ], + zh: [ + { language: "zh", value: "\u9b03\u6bdb" }, + { + language: "zh", + value: "\u72ee\u5b50", + }, + { language: "zh", value: "\u7345" }, + { + language: "zh", + value: "\u525b\u679c\u7345", + }, + { language: "zh", value: "\u975e\u6d32\u72ee" }, + ], + de: [{ language: "de", value: "Panthera leo" }], + ca: [{ language: "ca", value: "Panthera leo" }], + sco: [{ language: "sco", value: "Panthera leo" }], + hu: [ + { language: "hu", value: "P. leo" }, + { language: "hu", value: "Panthera leo" }, + ], + ilo: [ + { language: "ilo", value: "Panthera leo" }, + { language: "ilo", value: "Felis leo" }, + ], + ksh: [ + { language: "ksh", value: "L\u00f6hw" }, + { + language: "ksh", + value: "L\u00f6hf", + }, + { language: "ksh", value: "L\u00f6v" }, + ], + gl: [{ language: "gl", value: "Panthera leo" }], + ja: [ + { language: "ja", value: "\u767e\u7363\u306e\u738b" }, + { + language: "ja", + value: "\u7345\u5b50", + }, + { language: "ja", value: "\u30b7\u30b7" }, + ], + sq: [{ language: "sq", value: "Mbreti i Kafsh\u00ebve" }], + el: [{ language: "el", value: "\u03c0\u03b1\u03bd\u03b8\u03ae\u03c1" }], + pl: [ + { language: "pl", value: "Panthera leo" }, + { language: "pl", value: "lew" }, + ], + "zh-hk": [{ language: "zh-hk", value: "\u7345" }], + af: [{ language: "af", value: "Panthera leo" }], + mk: [{ language: "mk", value: "Panthera leo" }], + ar: [{ language: "ar", value: "\u0644\u064a\u062b" }], + "zh-hant": [{ language: "zh-hant", value: "\u7345" }], + "zh-cn": [{ language: "zh-cn", value: "\u72ee" }], + "zh-hans": [{ language: "zh-hans", value: "\u72ee" }], + "zh-mo": [{ language: "zh-mo", value: "\u7345" }], + "zh-my": [{ language: "zh-my", value: "\u72ee" }], + "zh-sg": [{ language: "zh-sg", value: "\u72ee" }], + "zh-tw": [{ language: "zh-tw", value: "\u7345" }], + gu: [ + { language: "gu", value: "\u0ab5\u0aa8\u0ab0\u0abe\u0a9c" }, + { + language: "gu", + value: "\u0ab8\u0abe\u0ab5\u0a9c", + }, + { language: "gu", value: "\u0a95\u0ac7\u0ab8\u0ab0\u0ac0" }, + ], + ast: [ + { language: "ast", value: "Panthera leo" }, + { + language: "ast", + value: "lle\u00f3n africanu", + }, + ], + hi: [ + { + language: "hi", + value: "\u092a\u0947\u0902\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b", + }, + ], + te: [ + { + language: "te", + value: "\u0c2a\u0c3e\u0c02\u0c25\u0c47\u0c30\u0c3e \u0c32\u0c3f\u0c2f\u0c4b", + }, + ], + nl: [{ language: "nl", value: "Panthera leo" }], + bho: [ + { + language: "bho", + value: "\u092a\u0948\u0928\u094d\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b", + }, + { + language: "bho", + value: "\u092a\u0948\u0902\u0925\u0947\u0930\u093e \u0932\u093f\u092f\u094b", + }, + ], + ru: [ + { + language: "ru", + value: "\u0430\u0437\u0438\u0430\u0442\u0441\u043a\u0438\u0439 \u043b\u0435\u0432", + }, + { + language: "ru", + value: "\u0431\u043e\u043b\u044c\u0448\u0430\u044f \u043a\u043e\u0448\u043a\u0430", + }, + { + language: "ru", + value: "\u0446\u0430\u0440\u044c \u0437\u0432\u0435\u0440\u0435\u0439", + }, + { + language: "ru", + value: "\u0430\u0444\u0440\u0438\u043a\u0430\u043d\u0441\u043a\u0438\u0439 \u043b\u0435\u0432", + }, + ], + ga: [{ language: "ga", value: "Panthera leo" }], + bg: [ + { language: "bg", value: "Panthera leo" }, + { + language: "bg", + value: "\u043b\u044a\u0432\u0438\u0446\u0430", + }, + ], + sat: [{ language: "sat", value: "\u1c60\u1c69\u1c5e" }], + nan: [{ language: "nan", value: "Panthera leo" }], + la: [{ language: "la", value: "Panthera leo" }], + "nds-nl": [{ language: "nds-nl", value: "leywe" }], }, - "claims": { - "P225": [{ - "mainsnak": { - "snaktype": "value", - "property": "P225", - "hash": "e2be083a19a0c5e1a3f8341be88c5ec0e347580f", - "datavalue": {"value": "Panthera leo", "type": "string"}, - "datatype": "string" + claims: { + P225: [ + { + mainsnak: { + snaktype: "value", + property: "P225", + hash: "e2be083a19a0c5e1a3f8341be88c5ec0e347580f", + datavalue: { value: "Panthera leo", type: "string" }, + datatype: "string", + }, + type: "statement", + qualifiers: { + P405: [ + { + snaktype: "value", + property: "P405", + hash: "a817d3670bc2f9a3586b6377a65d54fff72ef888", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1043, + id: "Q1043", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P574: [ + { + snaktype: "value", + property: "P574", + hash: "506af9838b7d37b45786395b95170263f1951a31", + datavalue: { + value: { + time: "+1758-01-01T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 9, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + P31: [ + { + snaktype: "value", + property: "P31", + hash: "60a983bb1006c765614eb370c3854e64ec50599f", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 14594740, + id: "Q14594740", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P405", "P574", "P31"], + id: "q140$8CCA0B07-C81F-4456-ABAA-A7348C86C9B4", + rank: "normal", + references: [ + { + hash: "89e96b63b05055cc80c950cf5fea109c7d453658", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c26dbcef1202a7d198982ed24f6ea69b704f95fe", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 82575, + id: "Q82575", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P577: [ + { + snaktype: "value", + property: "P577", + hash: "539fa499b6ea982e64006270bb26f52a57a8e32b", + datavalue: { + value: { + time: "+1996-06-13T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "96dfb8481e184edb40553947f8fe08ce080f1553", + datavalue: { + value: { + time: "+2013-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P577", "P813"], + }, + { + hash: "f2fcc71ba228fd0db2b328c938e601507006fa46", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "603c636b2210e4a74b7d40c9e969b7e503bbe252", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1538807, + id: "Q1538807", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "6892402e621d2b47092e15284d64cdbb395e71f7", + datavalue: { + value: { + time: "+2015-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], }, - "type": "statement", - "qualifiers": { - "P405": [{ - "snaktype": "value", - "property": "P405", - "hash": "a817d3670bc2f9a3586b6377a65d54fff72ef888", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 1043, "id": "Q1043"}, - "type": "wikibase-entityid" + ], + P105: [ + { + mainsnak: { + snaktype: "value", + property: "P105", + hash: "aebf3611b23ed90c7c0fc80f6cd1cb7be110ea59", + datavalue: { + value: { "entity-type": "item", "numeric-id": 7432, id: "Q7432" }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }], - "P574": [{ - "snaktype": "value", - "property": "P574", - "hash": "506af9838b7d37b45786395b95170263f1951a31", - "datavalue": { - "value": { - "time": "+1758-01-01T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 9, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" + datatype: "wikibase-item", + }, + type: "statement", + id: "q140$CD2903E5-743A-4B4F-AE9E-9C0C83426B11", + rank: "normal", + references: [ + { + hash: "89e96b63b05055cc80c950cf5fea109c7d453658", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c26dbcef1202a7d198982ed24f6ea69b704f95fe", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 82575, + id: "Q82575", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P577: [ + { + snaktype: "value", + property: "P577", + hash: "539fa499b6ea982e64006270bb26f52a57a8e32b", + datavalue: { + value: { + time: "+1996-06-13T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "96dfb8481e184edb40553947f8fe08ce080f1553", + datavalue: { + value: { + time: "+2013-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P577", "P813"], }, - "datatype": "time" - }], - "P31": [{ - "snaktype": "value", - "property": "P31", - "hash": "60a983bb1006c765614eb370c3854e64ec50599f", - "datavalue": { - "value": { + { + hash: "f2fcc71ba228fd0db2b328c938e601507006fa46", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "603c636b2210e4a74b7d40c9e969b7e503bbe252", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1538807, + id: "Q1538807", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "6892402e621d2b47092e15284d64cdbb395e71f7", + datavalue: { + value: { + time: "+2015-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P171: [ + { + mainsnak: { + snaktype: "value", + property: "P171", + hash: "cbf0d3943e6cbac8afbec1ff11525c84ee04e442", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 14594740, - "id": "Q14594740" - }, "type": "wikibase-entityid" + "numeric-id": 127960, + id: "Q127960", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P405", "P574", "P31"], - "id": "q140$8CCA0B07-C81F-4456-ABAA-A7348C86C9B4", - "rank": "normal", - "references": [{ - "hash": "89e96b63b05055cc80c950cf5fea109c7d453658", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c26dbcef1202a7d198982ed24f6ea69b704f95fe", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 82575, "id": "Q82575"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P577": [{ - "snaktype": "value", - "property": "P577", - "hash": "539fa499b6ea982e64006270bb26f52a57a8e32b", - "datavalue": { - "value": { - "time": "+1996-06-13T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "96dfb8481e184edb40553947f8fe08ce080f1553", - "datavalue": { - "value": { - "time": "+2013-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] + datatype: "wikibase-item", }, - "snaks-order": ["P248", "P577", "P813"] - }, { - "hash": "f2fcc71ba228fd0db2b328c938e601507006fa46", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "603c636b2210e4a74b7d40c9e969b7e503bbe252", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1538807, - "id": "Q1538807" - }, "type": "wikibase-entityid" + type: "statement", + id: "q140$C1CA40D8-39C3-4DB4-B763-207A22796D85", + rank: "normal", + references: [ + { + hash: "89e96b63b05055cc80c950cf5fea109c7d453658", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c26dbcef1202a7d198982ed24f6ea69b704f95fe", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 82575, + id: "Q82575", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P577: [ + { + snaktype: "value", + property: "P577", + hash: "539fa499b6ea982e64006270bb26f52a57a8e32b", + datavalue: { + value: { + time: "+1996-06-13T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "96dfb8481e184edb40553947f8fe08ce080f1553", + datavalue: { + value: { + time: "+2013-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "6892402e621d2b47092e15284d64cdbb395e71f7", - "datavalue": { - "value": { - "time": "+2015-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P105": [{ - "mainsnak": { - "snaktype": "value", - "property": "P105", - "hash": "aebf3611b23ed90c7c0fc80f6cd1cb7be110ea59", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 7432, "id": "Q7432"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "id": "q140$CD2903E5-743A-4B4F-AE9E-9C0C83426B11", - "rank": "normal", - "references": [{ - "hash": "89e96b63b05055cc80c950cf5fea109c7d453658", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c26dbcef1202a7d198982ed24f6ea69b704f95fe", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 82575, "id": "Q82575"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P577": [{ - "snaktype": "value", - "property": "P577", - "hash": "539fa499b6ea982e64006270bb26f52a57a8e32b", - "datavalue": { - "value": { - "time": "+1996-06-13T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "96dfb8481e184edb40553947f8fe08ce080f1553", - "datavalue": { - "value": { - "time": "+2013-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P577", "P813"] - }, { - "hash": "f2fcc71ba228fd0db2b328c938e601507006fa46", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "603c636b2210e4a74b7d40c9e969b7e503bbe252", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1538807, - "id": "Q1538807" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "6892402e621d2b47092e15284d64cdbb395e71f7", - "datavalue": { - "value": { - "time": "+2015-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P171": [{ - "mainsnak": { - "snaktype": "value", - "property": "P171", - "hash": "cbf0d3943e6cbac8afbec1ff11525c84ee04e442", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 127960, "id": "Q127960"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "id": "q140$C1CA40D8-39C3-4DB4-B763-207A22796D85", - "rank": "normal", - "references": [{ - "hash": "89e96b63b05055cc80c950cf5fea109c7d453658", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c26dbcef1202a7d198982ed24f6ea69b704f95fe", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 82575, "id": "Q82575"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P577": [{ - "snaktype": "value", - "property": "P577", - "hash": "539fa499b6ea982e64006270bb26f52a57a8e32b", - "datavalue": { - "value": { - "time": "+1996-06-13T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "96dfb8481e184edb40553947f8fe08ce080f1553", - "datavalue": { - "value": { - "time": "+2013-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P577", "P813"] - }, { - "hash": "f2fcc71ba228fd0db2b328c938e601507006fa46", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "603c636b2210e4a74b7d40c9e969b7e503bbe252", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1538807, - "id": "Q1538807" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "6892402e621d2b47092e15284d64cdbb395e71f7", - "datavalue": { - "value": { - "time": "+2015-09-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P1403": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1403", - "hash": "baa11a4c668601014a48e2998ab76aa1ea7a5b99", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 15294488, "id": "Q15294488"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$816d2b99-4aa5-5eb9-784b-34e2704d2927", "rank": "normal" - }], - "P141": [{ - "mainsnak": { - "snaktype": "value", - "property": "P141", - "hash": "80026ea5b2066a2538fee5c0897b459bb6770689", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 278113, "id": "Q278113"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "id": "q140$B12A2FD5-692F-4D9A-8FC7-144AA45A16F8", - "rank": "normal", - "references": [{ - "hash": "355df53bb7c6d100219cd2a331afd51719337d88", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "eb153b77c6029ffa1ca09f9128b8e47fe58fce5a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 56011232, - "id": "Q56011232" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P627": [{ - "snaktype": "value", - "property": "P627", - "hash": "3642ac96e05180279c47a035c129d3af38d85027", - "datavalue": {"value": "15951", "type": "string"}, - "datatype": "string" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "76bc602d4f902d015c358223e7c0917bd65095e0", - "datavalue": { - "value": { - "time": "+2018-08-10T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P627", "P813"] - }] - }], - "P181": [{ - "mainsnak": { - "snaktype": "value", - "property": "P181", - "hash": "8467347aac1f01e518c1b94d5bb68c65f9efe84a", - "datavalue": {"value": "Lion distribution.png", "type": "string"}, - "datatype": "commonsMedia" - }, "type": "statement", "id": "q140$12F383DD-D831-4AE9-A0ED-98C27A8C5BA7", "rank": "normal" - }], - "P830": [{ - "mainsnak": { - "snaktype": "value", - "property": "P830", - "hash": "8cafbfe99d80fcfabbd236d4cc01d33cc8a8b41d", - "datavalue": {"value": "328672", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$486d7ab8-4af8-b6e1-85bb-e0749b02c2d9", - "rank": "normal", - "references": [{ - "hash": "7e71b7ede7931e7e2ee9ce54e832816fe948b402", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "6e81987ab11fb1740bd862639411d0700be3b22c", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 82486, "id": "Q82486"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "7c1a33cf9a0bf6cdd57b66f089065ba44b6a8953", - "datavalue": { - "value": { - "time": "+2014-10-30T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P815": [{ - "mainsnak": { - "snaktype": "value", - "property": "P815", - "hash": "27f6bd8fb4504eb79b92e6b63679b83af07d5fed", - "datavalue": {"value": "183803", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$71177A4F-4308-463D-B370-8B354EC2D2C3", - "rank": "normal", - "references": [{ - "hash": "ff0dd9eabf88b0dcefa74b223d065dd644e42050", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c26dbcef1202a7d198982ed24f6ea69b704f95fe", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 82575, "id": "Q82575"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "6b8fcfa6afb3911fecec93ae1dff2b6b6cde5659", - "datavalue": { - "value": { - "time": "+2013-12-07T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P685": [{ - "mainsnak": { - "snaktype": "value", - "property": "P685", - "hash": "c863e255c042b2b9b6a788ebd6e24f38a46dfa88", - "datavalue": {"value": "9689", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$A9F4ABE4-D079-4868-BC18-F685479BB244", - "rank": "normal", - "references": [{ - "hash": "5667273d9f2899620fec2016bb2afd29aa7080ce", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "1851bc60ddfbcf6f76bd45aa7124fc0d5857a379", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 13711410, - "id": "Q13711410" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "6b8fcfa6afb3911fecec93ae1dff2b6b6cde5659", - "datavalue": { - "value": { - "time": "+2013-12-07T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P959": [{ - "mainsnak": { - "snaktype": "value", - "property": "P959", - "hash": "55cab2a9d2af860a89a8d0e2eaefedb64202a3d8", - "datavalue": {"value": "14000228", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$A967D17D-485D-434F-BBF2-E6226E63BA42", - "rank": "normal", - "references": [{ - "hash": "3e398e6df20323ce88e644e5a1e4ec0bc77a5f41", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "603c636b2210e4a74b7d40c9e969b7e503bbe252", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1538807, - "id": "Q1538807" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "d2bace4e146678a5e5f761e9a441b53b95dc2e87", - "datavalue": { - "value": { - "time": "+2014-01-10T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P842": [{ - "mainsnak": { - "snaktype": "value", - "property": "P842", - "hash": "991987fc3fa4d1cfd3a601dcfc9dd1f802255de7", - "datavalue": {"value": "49734", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$3FF45860-DBC3-4629-AAF8-F2899B6C6876", - "rank": "normal", - "references": [{ - "hash": "1111bfc1dc63ee739fb9dd3f5534346c7fd478f0", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "00fe2206a3342fa25c0cfe1d08783c49a1986f12", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 796451, - "id": "Q796451" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "14c5b75e8d3f4c43cb5b570380dd98e421bb9751", - "datavalue": { - "value": { - "time": "+2014-01-30T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P227": [{ - "mainsnak": { - "snaktype": "value", - "property": "P227", - "hash": "3343c5fd594f8f0264332d87ce95e76ffeaebffd", - "datavalue": {"value": "4140572-9", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$0059e08d-4308-8401-58e8-2cb683c03837", "rank": "normal" - }], - "P349": [{ - "mainsnak": { - "snaktype": "value", - "property": "P349", - "hash": "08812c4ef85f397bf00b015d1baf3b00d81cb9bf", - "datavalue": {"value": "00616831", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$B7933772-D27D-49D4-B1BB-AA36ADCA81B0", "rank": "normal" - }], - "P1014": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1014", - "hash": "3d27204feb184f21c042777dc9674150cb07ee92", - "datavalue": {"value": "300310388", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$8e3c9dc3-442e-2e61-8617-f4a41b5be668", "rank": "normal" - }], - "P646": [{ - "mainsnak": { - "snaktype": "value", - "property": "P646", - "hash": "0c053bce57fe07b05c300a09b322d9f89236884b", - "datavalue": {"value": "/m/096mb", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$D94D8A4F-3414-4BE0-82C1-306BD136C017", - "rank": "normal", - "references": [{ - "hash": "2b00cb481cddcac7623114367489b5c194901c4a", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "a94b740202b097dd33355e0e6c00e54b9395e5e0", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 15241312, - "id": "Q15241312" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P577": [{ - "snaktype": "value", - "property": "P577", - "hash": "fde79ecb015112d2f29229ccc1ec514ed3e71fa2", - "datavalue": { - "value": { - "time": "+2013-10-28T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P577"] - }] - }], - "P1036": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1036", - "hash": "02435ba66ab8e5fb26652ae1a84695be24b3e22a", - "datavalue": {"value": "599.757", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$e75ed89a-408d-9bc1-8d99-41663921debd", "rank": "normal" - }], - "P1245": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1245", - "hash": "f3da4ca7d35fc3e02a9ea1662688d8f6c4658df0", - "datavalue": {"value": "5961", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$010e79a0-475e-fcf4-a554-375b64943783", "rank": "normal" - }], - "P910": [{ - "mainsnak": { - "snaktype": "value", - "property": "P910", - "hash": "056367b51cd51edd6c2840134fde01cf40469172", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 6987175, "id": "Q6987175"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$BC4DE2D4-BF45-49AF-A9A6-C0A976F60825", "rank": "normal" - }], - "P373": [{ - "mainsnak": { - "snaktype": "value", - "property": "P373", - "hash": "76c006bc5e2975bcda2e7d60ddcbaaa8c84f69e5", - "datavalue": {"value": "Panthera leo", "type": "string"}, - "datatype": "string" - }, "type": "statement", "id": "q140$939BA4B2-28D3-4C74-B143-A0EA6F423B43", "rank": "normal" - }], - "P846": [{ - "mainsnak": { - "snaktype": "value", - "property": "P846", - "hash": "d0428680cd2b36efde61dc69ccc5a8ff7a735cb5", - "datavalue": {"value": "5219404", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$4CE8E6D4-E9A1-46F1-8EEF-B469E8485F9E", - "rank": "normal", - "references": [{ - "hash": "5b8345ffc93a361b71f5d201a97f587e5e57efe5", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "dbb8dd1efbe0158a5227213bd628eeac27a1da65", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1531570, - "id": "Q1531570" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "3eb17b10ce02d44f47540a6fbdbb3cbb7e77d5f5", - "datavalue": { - "value": { - "time": "+2015-05-15T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P487": [{ - "mainsnak": { - "snaktype": "value", - "property": "P487", - "hash": "5f93415dd33bfde6a546fdd65e5a7013e012c336", - "datavalue": {"value": "\ud83e\udd81", "type": "string"}, - "datatype": "string" - }, "type": "statement", "id": "Q140$da5262fc-4ac5-390b-b424-4f296b2d711d", "rank": "normal" - }], - "P2040": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2040", - "hash": "5b13a3fa0fde6ba09d8e417738c05268bd065e32", - "datavalue": {"value": "6353", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$E97A1A2E-D146-4C62-AE92-5AF5F7E146EF", - "rank": "normal", - "references": [{ - "hash": "348b5187938d682071c94e22f1b30659af715dc7", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "213dc0d84ed983cbb28466ebb0c45bf8b0730ea2", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 20962955, - "id": "Q20962955" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "3d2c713dec9143721ae196af88fee0fde5ae20f2", - "datavalue": { - "value": { - "time": "+2015-09-10T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P935": [{ - "mainsnak": { - "snaktype": "value", - "property": "P935", - "hash": "c3518a9944958337bcce384587f3abc3de6ddf34", - "datavalue": {"value": "Panthera leo", "type": "string"}, - "datatype": "string" - }, "type": "statement", "id": "Q140$F7AAEE1F-4D18-4538-99F0-1A2B5AD7269F", "rank": "normal" - }], - "P1417": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1417", - "hash": "492d3483075b6915990940a4392f5ec035cbe05e", - "datavalue": {"value": "animal/lion", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$FE89C38F-6C79-4F06-8C15-81DCAC8D745F", "rank": "normal" - }], - "P244": [{ - "mainsnak": { - "snaktype": "value", - "property": "P244", - "hash": "2e41780263804dd45d7deaf7955a2d1d221f6096", - "datavalue": {"value": "sh85077276", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$634d86d1-45b1-920d-e9ef-78d5f4023288", - "rank": "normal", - "references": [{ - "hash": "88d810dd1ff791aeb0b5779876b0c9f19acb59b6", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c120f07504c77593a9d734f50361ea829f601960", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 620946, - "id": "Q620946" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "0980c2f2b51e6b2d4c1dd9a77b9fb95dc282bc79", - "datavalue": { - "value": { - "time": "+2016-06-01T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P1843": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "3b1cfb68cc46255ceba7ff7893ac1cabbb4ddd92", - "datavalue": {"value": {"text": "Lion", "language": "en"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "qualifiers": { - "P7018": [{ - "snaktype": "value", - "property": "P7018", - "hash": "40a60b39201df345ffbf5aa724269d5fd61ae028", - "datavalue": { - "value": {"entity-type": "sense", "id": "L17815-S1"}, - "type": "wikibase-entityid" + "snaks-order": ["P248", "P577", "P813"], }, - "datatype": "wikibase-sense" - }] - }, - "qualifiers-order": ["P7018"], - "id": "Q140$6E257597-55C7-4AF3-B3D6-0F2204FAD35C", - "rank": "normal", - "references": [{ - "hash": "eada84c58a38325085267509899037535799e978", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 32059, "id": "Q32059"}, - "type": "wikibase-entityid" + { + hash: "f2fcc71ba228fd0db2b328c938e601507006fa46", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "603c636b2210e4a74b7d40c9e969b7e503bbe252", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1538807, + id: "Q1538807", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "6892402e621d2b47092e15284d64cdbb395e71f7", + datavalue: { + value: { + time: "+2015-09-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "3e51c3c32949f8a45f2c3331f55ea6ae68ecf3fe", - "datavalue": { - "value": { - "time": "+2016-10-21T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }, { - "hash": "cdc389b112247cb50b855fb86e98b7a7892e96f0", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "e17975e5c866df46673c91b2287a82cf23d14f5a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 27310853, - "id": "Q27310853" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P304": [{ - "snaktype": "value", - "property": "P304", - "hash": "ff7ad3502ff7a4a9b0feeb4248a7bed9767a1ec6", - "datavalue": {"value": "166", "type": "string"}, - "datatype": "string" - }] - }, - "snaks-order": ["P248", "P304"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "38a9c57a5c62a707adc86decd2bd00be89eab6f3", - "datavalue": {"value": {"text": "Leeu", "language": "af"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$5E731B05-20D6-491B-97E7-94D90CBB70F0", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "a4455f1ef49d7d17896563760a420031c41d65c1", - "datavalue": {"value": {"text": "Gyata", "language": "ak"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$721B4D81-D948-4002-A13E-0B2567626FD6", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "b955c9239d6ced23c0db577e20219b0417a2dd9b", - "datavalue": {"value": {"text": "Ley\u00f3n", "language": "an"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$E2B52F3D-B12D-48B5-86EA-6A4DCBC091D3", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "e18a8ecb17321c203fcf8f402e82558ce0599b39", - "datavalue": {"value": {"text": "Li\u00f3n", "language": "an"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$339ADC90-41C6-4CDB-B6C3-DA9F952FCC15", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "297bf417fff1510d19b27c08fa9f34e2653b9510", - "datavalue": { - "value": {"text": "\u0623\u064e\u0633\u064e\u062f\u064c", "language": "ar"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$F1849268-0E70-4EC0-A630-EC0D2DCBB298", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "5577ef6920a3ade2365d878740d1d097fcdae399", - "datavalue": {"value": {"text": "L\u00e9we", "language": "bar"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$BDD65B40-7ECB-4725-B33F-417A83AF5102", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "de8fa35eca4e61dfb8fe2df360e734fb1cd37092", - "datavalue": {"value": {"text": "L\u00f6we", "language": "bar"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$486EE5F1-9AB5-4789-98AC-E435D81E784F", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "246c27f44da8bedd2e3313de393fe648b2b40ea9", - "datavalue": { - "value": {"text": "\u041b\u0435\u045e (Lew)", "language": "be"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$47AA6BD4-0B09-4B20-9092-0AEAD8056157", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "64c42db53ef288871161f0a656808f06daae817d", - "datavalue": { - "value": {"text": "\u041b\u044a\u0432 (L\u0103v)", "language": "bg"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$ADF0B08A-9626-4821-8118-0A875CBE5FB9", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "6041e2730af3095f4f0cbf331382e22b596d2305", - "datavalue": { - "value": {"text": "\u09b8\u09bf\u0982\u09b9", "language": "bn"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$8DF5BDCD-B470-46C3-A44A-7375B8A5DCDE", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "8499f437dc8678b0c4b740b40cab41031fce874d", - "datavalue": {"value": {"text": "Lle\u00f3", "language": "ca"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$F55C3E63-DB2C-4F6D-B10B-4C1BB70C06A0", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "b973abb618a6f17b8a9547b852e5817b5c4da00b", - "datavalue": { - "value": {"text": "\u041b\u043e\u044c\u043c", "language": "ce"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$F32B0BFA-3B85-4A26-A888-78FD8F09F943", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "62f53c7229efad1620a5cce4dc5a535d88c4989f", - "datavalue": {"value": {"text": "Lev", "language": "cs"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$1630DAB7-C4D0-4268-A598-8BBB9480221E", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "0df7e23666c947b42aea5572a9f5a987229718d3", - "datavalue": {"value": {"text": "Llew", "language": "cy"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$F33991E8-A532-47F5-B135-A13761DB2E95", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "14942ad0830a0eb7b06704234eea637f99b53a24", - "datavalue": {"value": {"text": "L\u00f8ve", "language": "da"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$478F0603-640A-44BE-9453-700FDD32100F", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "8af089542ef6207b918f656bcf9a96e745970915", - "datavalue": {"value": {"text": "L\u00f6we", "language": "de"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "qualifiers": { - "P7018": [{ - "snaktype": "value", - "property": "P7018", - "hash": "2da239e18a0208847a72fbeab011c8c2fb3b4d99", - "datavalue": { - "value": {"entity-type": "sense", "id": "L41680-S1"}, - "type": "wikibase-entityid" + "snaks-order": ["P248", "P813"], }, - "datatype": "wikibase-sense" - }] - }, - "qualifiers-order": ["P7018"], - "id": "Q140$11F5F498-3688-4F4B-B2FA-7121BE5AA701", - "rank": "normal", - "references": [{ - "hash": "cdc389b112247cb50b855fb86e98b7a7892e96f0", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "e17975e5c866df46673c91b2287a82cf23d14f5a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 27310853, - "id": "Q27310853" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P304": [{ - "snaktype": "value", - "property": "P304", - "hash": "ff7ad3502ff7a4a9b0feeb4248a7bed9767a1ec6", - "datavalue": {"value": "166", "type": "string"}, - "datatype": "string" - }] - }, - "snaks-order": ["P248", "P304"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "c0c8b50001810c1ec643b88479df82ea85c819a2", - "datavalue": {"value": {"text": "Dzata", "language": "ee"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$8F6EC307-A293-4AFC-8154-E3FF187C0D7D", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "57a3384eeb13d1bcffeb3cf0efd0f3e3f511b35d", - "datavalue": { - "value": { - "text": "\u039b\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9 (Liond\u00e1ri)", - "language": "el" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$560B3341-3E06-4D09-8869-FC47C841D14C", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "ee7109a46f8259ae6f52791cfe599b7c4c272831", - "datavalue": {"value": {"text": "Leono", "language": "eo"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$67F2B7A6-1C81-407A-AA61-A1BFF148EC69", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "3b8f4f61c3a18792bfaff5d332f03c80932dce05", - "datavalue": {"value": {"text": "Le\u00f3n", "language": "es"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$DB29EAF7-4405-4030-8056-ED17089B3805", - "rank": "normal", - "references": [{ - "hash": "d3a8e536300044db1d823eae6891b2c7baa49f66", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 32059, "id": "Q32059"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "620d2e76d21bb1d326fc360db5bece2070115240", - "datavalue": { - "value": { - "time": "+2016-10-19T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "41fffb83f35736829d60f782bdce68463f0ab47c", - "datavalue": {"value": {"text": "L\u00f5vi", "language": "et"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$19B76CC4-AA11-443B-BC76-DB2D0DA5B9CB", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "b96549e5ae538fb7e0b48089508333b31aec8fe7", - "datavalue": {"value": {"text": "Lehoi", "language": "eu"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$88F712C1-4EEF-4E42-8C61-84E55CF2DCE0", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "b343c833d8de3dfd5c8b31336afd137380ab42dc", - "datavalue": { - "value": {"text": "\u0634\u06cc\u0631 (\u0160ayr)", "language": "fa"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$B72DB989-EF39-42F5-8FA8-5FC669079DB7", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "51aaf9a4a7c5e77ba931a5280d1fec984c91963b", - "datavalue": {"value": {"text": "Leijona", "language": "fi"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$6861CDE9-707D-43AD-B352-3BCD7B9D4267", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "038249fb112acc26895af45fab412395f999ae11", - "datavalue": {"value": {"text": "Leyva", "language": "fo"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$A044100A-C49F-4AA6-8861-F0300F28126E", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "92ec25b64605d026b07b0cda6e623fbbf2f3dfb4", - "datavalue": {"value": {"text": "Lion", "language": "fr"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$122623FD-3915-49E9-8890-0B6883317507", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "59be091f7839e7a6061c6d1690ed77f3b21b9ff4", - "datavalue": { - "value": {"text": "L\u00f6\u00f6w", "language": "frr"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$76B87E52-A02C-4E99-A4B3-D6105B642521", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "126b0f2c5ed11124233dfefff8bd132a1fe1218a", - "datavalue": { - "value": {"text": "Le\u00f3n-leoa", "language": "gl"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$A4864784-EED3-4898-83FE-A2FCC0C3982E", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "49e0d3858de566edce1a28b0e96f42b2d0df718f", - "datavalue": { - "value": {"text": "\u0ab8\u0abf\u0a82\u0ab9", "language": "gu"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$4EE122CE-7671-480E-86A4-4A4DDABC04BA", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "dff0c422f7403c50d28dd51ca2989d03108b7584", - "datavalue": {"value": {"text": "Liona", "language": "haw"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$FBB6AC65-A224-4C29-8024-079C0687E9FB", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "c174addd56c0f42f6ec3e87c72fb9651e4923a00", - "datavalue": { - "value": {"text": "\u05d0\u05e8\u05d9\u05d4", "language": "he"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$B72D9BDB-A2CC-471D-AF20-8F7FB677D533", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "c38a63a06b569fc8fee3e98c4cf8d5501990811e", - "datavalue": { - "value": {"text": "si\u1e45ha)", "language": "hi"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$9AA9171C-E912-41F2-AB26-643AA538E644", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "94f073519a5b64c48398c73a5f0f135a4f0f4306", - "datavalue": { - "value": {"text": "\u0936\u0947\u0930", "language": "hi"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$0714B97B-03E0-4ACC-80A0-6A17874DDBA8", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "1fbda5b1494db298c698fc28ed0fe68b1c137b2e", - "datavalue": { - "value": {"text": "\u0938\u093f\u0902\u0939", "language": "hi"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$3CE22F68-038C-4A94-9A1F-96B82760DEB9", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "fac41ebd8d1da777acd93720267c7a70016156e4", - "datavalue": {"value": {"text": "Lav", "language": "hr"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$C5351C11-E287-4D3B-A9B2-56716F0E69E5", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "71e0bda709fb17d58f4bd8e12fff7f937a61673c", - "datavalue": { - "value": {"text": "Oroszl\u00e1n", "language": "hu"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$AD06E7D2-3B1F-4D14-B2E9-DD2513BE8B4B", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "05e86680d70a2c0adf9a6e6eb51bbdf8c6ae44bc", - "datavalue": { - "value": {"text": "\u0531\u057c\u0575\u0578\u0582\u056e", "language": "hy"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$3E02B802-8F7B-4C48-A3F9-FBBFDB0D8DB3", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "979f25bee6af37e19471530c6344a0d22a0d594c", - "datavalue": {"value": {"text": "Lj\u00f3n", "language": "is"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$59788C31-A354-4229-AD89-361CB6076EF7", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "789e5f5a7ec6003076bc7fd2996faf8ca8468719", - "datavalue": {"value": {"text": "Leone", "language": "it"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$4901AE59-7749-43D1-BC65-DEEC0DFEB72F", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "4a5bdf9bb40f1cab9a92b7dba1d1d74a8440c7ed", - "datavalue": { - "value": {"text": "\u30e9\u30a4\u30aa\u30f3 (Raion)", "language": "ja"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$4CF2E0D9-5CF3-46A3-A197-938E94270CE2", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "ebba3893211c78dad7ae74a51448e8c7f6e73309", - "datavalue": { - "value": {"text": "\uc0ac\uc790 (saja)", "language": "ko"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$64B6CECD-5FFE-4612-819F-CAB2E726B228", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "ed1fe1812cee80102262dd3b7e170759fbeab86a", - "datavalue": { - "value": {"text": "\u0410\u0440\u0441\u0442\u0430\u043d", "language": "ky"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$3D9597D3-E35F-4EFF-9CAF-E013B45F283F", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "a0868f5f83bb886a408aa9b25b95dbfc59bde4dc", - "datavalue": {"value": {"text": "Leo", "language": "la"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$4D650414-6AFE-430F-892F-B7774AC7AF70", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "054af77b10151632045612df9b96313dfcc3550c", - "datavalue": {"value": {"text": "Liew", "language": "li"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$D5B466A8-AEFB-4083-BF3E-194C5CE45CD3", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "9193a46891a365ee1b0a17dd6e2591babc642811", - "datavalue": {"value": {"text": "Nkosi", "language": "ln"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$55F213DF-5AAB-4490-83CB-B9E5D2B894CD", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "c5952ec6b650f1c66f37194eb88c2889560740b2", - "datavalue": { - "value": {"text": "Li\u016btas", "language": "lt"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$8551F80C-A244-4351-A98A-8A9F37A736A2", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "e3541d0807682631f8fff2d224b2cb1b3d2a4c11", - "datavalue": {"value": {"text": "Lauva", "language": "lv"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$488A2D59-533A-4C02-8AC3-01241FE63D94", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "22e20da399aff10787267691b5211b6fc0bddf38", - "datavalue": { - "value": {"text": "\u041b\u0430\u0432 (lav)", "language": "mk"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$9E2377E9-1D37-4BBC-A409-1C40CDD99A86", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "fe4c9bc3b3cce21a779f72fae808f8ed213d226b", - "datavalue": { - "value": { - "text": "\u0d38\u0d3f\u0d02\u0d39\u0d02 (simham)", - "language": "ml" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$8BEA9E08-4687-434A-9FB4-4B23B2C40838", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "85aa09066722caf2181681a24575ad89ca76210e", - "datavalue": { - "value": {"text": "si\u1e45ha)", "language": "mr"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$46B51EF5-7ADB-4637-B744-89AD1E3B5D19", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "441f3832d6e3c4439c6986075096c7021a0939dd", - "datavalue": { - "value": {"text": "\u0936\u0947\u0930 (\u015a\u0113ra)", "language": "mr"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$12BBC825-32E3-4026-A5E5-0330DEB21D79", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "89b35a359c3891dce190d778e9ae0a9634cfd71f", - "datavalue": { - "value": {"text": "\u0938\u093f\u0902\u0939 (singh", "language": "mr"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$006148E2-658F-4C74-9C3E-26488B7AEB8D", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "5723b45deee51dfe5a2555f2db17bad14acb298a", - "datavalue": {"value": {"text": "Iljun", "language": "mt"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$13D221F5-9763-4550-9CC3-9A697286B785", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "d1ce3ab04f25af38248152eb8caa286b63366c2a", - "datavalue": {"value": {"text": "Leeuw", "language": "nl"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$65E80D17-6F20-4BAE-A2B4-DD934C0BE153", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "12f3384cc32e65dfb501e2fee19ccf709f9df757", - "datavalue": {"value": {"text": "L\u00f8ve", "language": "nn"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$78E95514-1969-4DA3-97CD-0DBADF1223E7", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "a3fedaf780a0d004ba318881f6adbe173750d09e", - "datavalue": {"value": {"text": "L\u00f8ve", "language": "nb"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$809DE1EA-861E-4813-BED7-D9C465341CB3", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "695d2ef10540ba13cf8b3541daa1d39fd720eea0", - "datavalue": { - "value": { - "text": "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed", - "language": "nv" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$E9EDAF16-6650-40ED-B888-C524BD00DF40", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "3c143e8a8cebf92d76d3ae2d7e3bb3f87e963fb4", - "datavalue": { - "value": { - "text": "\u0a2c\u0a71\u0a2c\u0a30 \u0a38\u0a3c\u0a47\u0a30", - "language": "pa" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$13AE1DAB-4B29-49A5-9893-C0014C61D21E", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "195e48d11222aec830fb1d5c2de898c9528abc57", - "datavalue": { - "value": {"text": "lew afryka\u0144ski", "language": "pl"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$6966C1C3-9DD6-48BC-B511-B0827642E41D", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "8bd3ae632e7731ae9e72c50744383006ec6eb73e", - "datavalue": {"value": {"text": "Le\u00e3o", "language": "pt"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$BD454649-347E-4AE5-81B8-360C16C7CDA7", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "79d3336733b7bf4b7dadffd6d6ebabdb892074d1", - "datavalue": {"value": {"text": "Leu", "language": "ro"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$7323EF68-7AA0-4D38-82D8-0A94E61A26F0", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "376b852a92b6472e969ae7b995c4aacea23955eb", - "datavalue": { - "value": {"text": "\u041b\u0435\u0432 (Lev)", "language": "ru"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$A7C413B9-0916-4534-941D-C24BA0334816", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "aa323e0bea79d79900227699b3d42d689a772ca1", - "datavalue": {"value": {"text": "Lioni", "language": "sc"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$2EF83D2C-0DB9-4D3C-8FDD-86237E566260", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "a290fe08983742eac8b5bc479022564fb6b2ce81", - "datavalue": {"value": {"text": "Lev", "language": "sl"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$B276673A-08C1-47E2-99A9-D0861321E157", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "d3b070ff1452d47f87c109a9e0bfa52e61b24a4e", - "datavalue": {"value": {"text": "Libubesi", "language": "ss"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$86BFAB38-1DB8-4903-A17D-A6B8E81819CC", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "3adea59d97f3caf9bb6b1c3d7ae6365f7f656dca", - "datavalue": {"value": {"text": "Tau", "language": "st"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$2FA8893D-2401-42E9-8DC3-288CC1DEDB0C", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "0e73b32fe31a107a95de83706a12f2db419c6909", - "datavalue": {"value": {"text": "Lejon", "language": "sv"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$1A8E006E-CC7B-4066-9DE7-9B82D096779E", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "34550af2fdc48f77cf66cabc5c59d1acf1d8afd0", - "datavalue": {"value": {"text": "Simba", "language": "sw"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$B02CA616-44CF-4AA6-9734-2C05810131EB", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "701f87cf9926c9af2c41434ff130dcb234a6cd95", - "datavalue": { - "value": { - "text": "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd", - "language": "ta" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$DA87A994-A002-45AD-A71F-99FB72F8B92F", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "856fd4809c90e3c34c6876e4410661dc04f5da8d", - "datavalue": { - "value": {"text": "\u0e2a\u0e34\u0e07\u0e42\u0e15", "language": "th"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$BDA8E989-3537-4662-8CC3-33534705A7F1", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "f8598a8369426da0c86bf8bab356a927487eae66", - "datavalue": {"value": {"text": "Aslan", "language": "tr"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$AAE5F227-C0DB-4DF3-B1F4-517699BBDDF1", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "f3c8320bd46913aee164999ab7f68388c1bd9920", - "datavalue": { - "value": {"text": "\u041b\u0435\u0432 (Lev)", "language": "uk"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$494C3503-6016-4539-83AF-6344173C2DCB", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "6cc2d534293320533e15dc713f1d2c07b3811b6a", - "datavalue": {"value": {"text": "Leon", "language": "vec"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$E6F1DA81-9F36-4CC8-B57E-95E3BDC2F5D0", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "32553481e45abf6f5e6292baea486e978c36f8fe", - "datavalue": { - "value": {"text": "S\u01b0 t\u1eed", "language": "vi"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$11D7996C-0492-41CC-AEE7-3C136172DFC7", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "69077fc29f9251d1de124cd3f3c45cd6f0bb6b65", - "datavalue": { - "value": {"text": "\u05dc\u05d9\u05d9\u05d1", "language": "yi"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$969FEF9A-C1C7-41FE-8181-07F6D87B0346", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "e3aaa8cde18be4ea6b4af6ca62b83e7dc23d76e1", - "datavalue": { - "value": {"text": "\u72ee\u5b50 (sh\u012bzi)", "language": "zh"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$3BC22F6C-F460-4354-9BA2-28CEDA9FF170", - "rank": "normal", - "references": [{ - "hash": "2e0c13df5b13edc9b3db9d8129e466c0894710ac", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 13679, "id": "Q13679"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "98f5efae94b2bb9f8ffee6c677ee71f836743ef6", - "datavalue": { - "value": {"text": "Lion d'Afrique", "language": "fr"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$62D09BBF-718A-4139-AF50-DA4185ED67F2", - "rank": "normal", - "references": [{ - "hash": "362e3c5d6de1d193ef97205ba38834ba075191fc", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 32059, "id": "Q32059"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "c7813bad20c2553e26e45c37e3502ce7252312df", - "datavalue": { - "value": { - "time": "+2016-10-20T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "c584bdbd3cdc1215292a4971b920c684d103ea06", - "datavalue": { - "value": {"text": "African Lion", "language": "en"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, - "type": "statement", - "id": "Q140$C871BB58-C689-4DBA-A088-DAC205377979", - "rank": "normal", - "references": [{ - "hash": "eada84c58a38325085267509899037535799e978", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 32059, "id": "Q32059"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "3e51c3c32949f8a45f2c3331f55ea6ae68ecf3fe", - "datavalue": { - "value": { - "time": "+2016-10-21T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "1d03eace9366816c6fda340c0390caac2f3cea8e", - "datavalue": {"value": {"text": "L\u00e9iw", "language": "lb"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$c65c7614-4d6e-3a87-9771-4f8c13618249", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "925c7abced1e89fa7e8000dc9dc78627cdac9769", - "datavalue": { - "value": {"text": "Lle\u00f3n", "language": "ast"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$1024eadb-45dd-7d9a-15f6-8602946ba661", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "0bda7868c3f498ba6fde78d46d0fbcf286e42dd8", - "datavalue": { - "value": {"text": "\u0644\u064e\u064a\u0652\u062b\u064c", "language": "ar"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$c226ff70-48dd-7b4d-00ff-7a683fe510aa", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "28de99c4aa35cc049cf8c9dd18af1791944137d9", - "datavalue": { - "value": {"text": "\u10da\u10dd\u10db\u10d8", "language": "ka"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$8fe60d1a-465a-9614-cbe4-595e22429b0c", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "4550db0f44e21c5eadeaa5a1d8fc614c9eb05f52", - "datavalue": {"value": {"text": "leon", "language": "ga"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$4950cb9c-4f1e-ce27-0d8c-ba3f18096044", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1843", - "hash": "9d268eb76ed921352c205b3f890d1f9428f638f3", - "datavalue": {"value": {"text": "Singa", "language": "ms"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }, "type": "statement", "id": "Q140$67401360-49dd-b13c-8269-e703b30c9a53", "rank": "normal" - }], - "P627": [{ - "mainsnak": { - "snaktype": "value", - "property": "P627", - "hash": "3642ac96e05180279c47a035c129d3af38d85027", - "datavalue": {"value": "15951", "type": "string"}, - "datatype": "string" - }, - "type": "statement", - "id": "Q140$6BE03095-BC68-4CE5-BB99-9F3E33A6F31D", - "rank": "normal", - "references": [{ - "hash": "182efbdb9110d036ca433f3b49bd3a1ae312858b", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 32059, "id": "Q32059"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "8c1c5174f4811115ea8a0def725fdc074c2ef036", - "datavalue": { - "value": { - "time": "+2016-07-10T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P2833": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2833", - "hash": "519877b77b20416af2401e5c0645954c6700d6fd", - "datavalue": {"value": "panthera-leo", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$C0A723AE-ED2E-4FDC-827F-496E4CF29A52", "rank": "normal" - }], - "P3063": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3063", - "hash": "81cdb0273eaf0a0126b62e2ff43b8e09505eea54", - "datavalue": { - "value": { - "amount": "+108", - "unit": "http://www.wikidata.org/entity/Q573", - "upperBound": "+116", - "lowerBound": "+100" - }, "type": "quantity" - }, - "datatype": "quantity" - }, - "type": "statement", - "id": "Q140$878ff87d-40d0-bb2b-c83d-4cef682c2687", - "rank": "normal", - "references": [{ - "hash": "7d748004a43983fabae420123742fda0e9b52840", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "f618501ace3a6524b053661d067b775547f96f58", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 26706243, - "id": "Q26706243" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P478": [{ - "snaktype": "value", - "property": "P478", - "hash": "ca3c5e6054c169ee3d0dfaf660f3eecd77942070", - "datavalue": {"value": "4", "type": "string"}, - "datatype": "string" - }], - "P304": [{ - "snaktype": "value", - "property": "P304", - "hash": "dd1977567f22f4cf510adfaadf5e3574813b3521", - "datavalue": {"value": "46", "type": "string"}, - "datatype": "string" - }] - }, - "snaks-order": ["P248", "P478", "P304"] - }] - }], - "P3031": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3031", - "hash": "e6271e8d12b20c9735d2bbd80eed58581059bf3a", - "datavalue": {"value": "PNTHLE", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$65AD2857-AB65-4CC0-9AB9-9D6C924784FE", "rank": "normal" - }], - "P3151": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3151", - "hash": "e85e5599d303d9a6bb360f3133fb69a76d98d0e2", - "datavalue": {"value": "41964", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$15D7A4EB-F0A3-4C61-8D2B-E557D7BF5CF7", "rank": "normal" - }], - "P3186": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3186", - "hash": "85ec7843064210afdfef6ec565a47f229c6d15e5", - "datavalue": {"value": "644245", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$6903E136-2DB2-42C9-98CB-82F61208FDAD", - "rank": "normal", - "references": [{ - "hash": "5790a745e549ea7e4e6d7ca467148b544529ba96", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "c897ca3efd1604ef7b80a14ac0d2b8d6849c0856", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 26936509, - "id": "Q26936509" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "555ca5385c445e4fd4762281d4873682eff2ce30", - "datavalue": { - "value": { - "time": "+2016-09-24T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }, { - "hash": "3edd37192f877cad0ff97acc3db56ef2cc83945b", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4f7c4fd187630ba8cbb174c2756113983df4ce82", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45029859, - "id": "Q45029859" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "56b6aa0388c9a2711946589902bc195718bb0675", - "datavalue": { - "value": { - "time": "+2017-12-26T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }, { - "hash": "1318ed8ea451b84fe98461305665d8688603bab3", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "0fbeeecce08896108ed797d8ec22c7c10a6015e2", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45029998, - "id": "Q45029998" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "4bac1c0d2ffc45d91b51fc0881eb6bcc7916e854", - "datavalue": { - "value": { - "time": "+2018-01-02T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }, { - "hash": "6f761664a6f331d95bbaa1434447d82afd597a93", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "6c09d1d89e83bd0dfa6c94e01d24a9a47489d83e", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 58035056, - "id": "Q58035056" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "03182012ca72fcd757b8a1fe05ba927cbe9ef374", - "datavalue": { - "value": { - "time": "+2018-11-02T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P3485": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3485", - "hash": "df4e58fc2a196833ab3e33483099e2481e61ba9e", - "datavalue": {"value": {"amount": "+112", "unit": "1"}, "type": "quantity"}, - "datatype": "quantity" - }, - "type": "statement", - "id": "Q140$4B70AA09-AE2F-4F4C-9BAF-09890CDA11B8", - "rank": "normal", - "references": [{ - "hash": "fa278ebfc458360e5aed63d5058cca83c46134f1", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "e4f6d9441d0600513c4533c672b5ab472dc73694", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 328, "id": "Q328"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }], - "P3827": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3827", - "hash": "6bb26d581721d7330c407259d46ab5e25cc4a6b1", - "datavalue": {"value": "lions", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$CCDE6F7D-B4EA-4875-A4D6-5649ACFA8E2F", "rank": "normal" - }], - "P268": [{ - "mainsnak": { - "snaktype": "value", - "property": "P268", - "hash": "a20cdf81e39cd47f4da30073671792380029924c", - "datavalue": {"value": "11932251d", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$000FDB77-C70C-4464-9F00-605787964BBA", - "rank": "normal", - "references": [{ - "hash": "d4bd87b862b12d99d26e86472d44f26858dee639", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "f30cbd35620c4ea6d0633aaf0210a8916130469b", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 8447, "id": "Q8447"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }], - "P3417": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3417", - "hash": "e3b5d21350aef37f27ad8b24142d6b83d9eec0a6", - "datavalue": {"value": "Lions", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$1f96d096-4e4b-06de-740f-7b7215e5ae3f", "rank": "normal" - }], - "P4024": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4024", - "hash": "a698e7dcd6f9b0b00ee8e02846c668db83064833", - "datavalue": {"value": "Panthera_leo", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$F5DC21E8-BF52-4A0D-9A15-63B89297BD70", - "rank": "normal", - "references": [{ - "hash": "d4bd87b862b12d99d26e86472d44f26858dee639", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "f30cbd35620c4ea6d0633aaf0210a8916130469b", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 8447, "id": "Q8447"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }], - "P1225": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1225", - "hash": "9af40267f10f15926877e9a3f78faeab7b0dda82", - "datavalue": {"value": "10665610", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$268074ED-3CD7-46C9-A8FF-8C3679C45547", "rank": "normal" - }], - "P4728": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4728", - "hash": "37eafa980604019b327b1a3552313fb7ae256697", - "datavalue": {"value": "105514", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$50C24ECC-C42C-4A58-8F34-6AF0AC6C4EFE", - "rank": "normal", - "references": [{ - "hash": "d4bd87b862b12d99d26e86472d44f26858dee639", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "f30cbd35620c4ea6d0633aaf0210a8916130469b", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 8447, "id": "Q8447"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P143"] - }] - }], - "P3219": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3219", - "hash": "dedb8825588940caff5a34d04a0e69af296f05dd", - "datavalue": {"value": "lion", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$2A5A2CA3-AB6E-4F68-927F-042D1BD22915", "rank": "normal" - }], - "P1343": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "5b0ef3d5413cd39d887fbe70d2d3b3f4a94ea9d8", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 1138524, "id": "Q1138524"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "f7bf629d348040dd1a59dc5a3199edb50279e8f5", - "datavalue": { - "value": { + ], + }, + ], + P1403: [ + { + mainsnak: { + snaktype: "value", + property: "P1403", + hash: "baa11a4c668601014a48e2998ab76aa1ea7a5b99", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 19997008, - "id": "Q19997008" - }, "type": "wikibase-entityid" + "numeric-id": 15294488, + id: "Q15294488", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$DFE4D4B0-0D84-41F2-B448-4A81AC982927", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "6bc15c6f82feca4f3b173c90209a416f99464cac", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 4086271, "id": "Q4086271"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q140$816d2b99-4aa5-5eb9-784b-34e2704d2927", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "69ace59e966574e4ffb454d26940a58fb45ed7de", - "datavalue": { - "value": { + ], + P141: [ + { + mainsnak: { + snaktype: "value", + property: "P141", + hash: "80026ea5b2066a2538fee5c0897b459bb6770689", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 25295952, - "id": "Q25295952" - }, "type": "wikibase-entityid" + "numeric-id": 278113, + id: "Q278113", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$b82b0461-4ff0-10ac-9825-d4b95fc7a85a", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "ecb04d74140f2ee856c06658b03ec90a21c2edf2", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 1970746, "id": "Q1970746"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "q140$B12A2FD5-692F-4D9A-8FC7-144AA45A16F8", + rank: "normal", + references: [ + { + hash: "355df53bb7c6d100219cd2a331afd51719337d88", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "eb153b77c6029ffa1ca09f9128b8e47fe58fce5a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 56011232, + id: "Q56011232", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P627: [ + { + snaktype: "value", + property: "P627", + hash: "3642ac96e05180279c47a035c129d3af38d85027", + datavalue: { value: "15951", type: "string" }, + datatype: "string", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "76bc602d4f902d015c358223e7c0917bd65095e0", + datavalue: { + value: { + time: "+2018-08-10T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P627", "P813"], + }, + ], }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "169607f1510535f3e1c5e7debce48d1903510f74", - "datavalue": { - "value": { + ], + P181: [ + { + mainsnak: { + snaktype: "value", + property: "P181", + hash: "8467347aac1f01e518c1b94d5bb68c65f9efe84a", + datavalue: { value: "Lion distribution.png", type: "string" }, + datatype: "commonsMedia", + }, + type: "statement", + id: "q140$12F383DD-D831-4AE9-A0ED-98C27A8C5BA7", + rank: "normal", + }, + ], + P830: [ + { + mainsnak: { + snaktype: "value", + property: "P830", + hash: "8cafbfe99d80fcfabbd236d4cc01d33cc8a8b41d", + datavalue: { value: "328672", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$486d7ab8-4af8-b6e1-85bb-e0749b02c2d9", + rank: "normal", + references: [ + { + hash: "7e71b7ede7931e7e2ee9ce54e832816fe948b402", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "6e81987ab11fb1740bd862639411d0700be3b22c", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 82486, + id: "Q82486", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "7c1a33cf9a0bf6cdd57b66f089065ba44b6a8953", + datavalue: { + value: { + time: "+2014-10-30T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P815: [ + { + mainsnak: { + snaktype: "value", + property: "P815", + hash: "27f6bd8fb4504eb79b92e6b63679b83af07d5fed", + datavalue: { value: "183803", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$71177A4F-4308-463D-B370-8B354EC2D2C3", + rank: "normal", + references: [ + { + hash: "ff0dd9eabf88b0dcefa74b223d065dd644e42050", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c26dbcef1202a7d198982ed24f6ea69b704f95fe", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 82575, + id: "Q82575", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "6b8fcfa6afb3911fecec93ae1dff2b6b6cde5659", + datavalue: { + value: { + time: "+2013-12-07T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P685: [ + { + mainsnak: { + snaktype: "value", + property: "P685", + hash: "c863e255c042b2b9b6a788ebd6e24f38a46dfa88", + datavalue: { value: "9689", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$A9F4ABE4-D079-4868-BC18-F685479BB244", + rank: "normal", + references: [ + { + hash: "5667273d9f2899620fec2016bb2afd29aa7080ce", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "1851bc60ddfbcf6f76bd45aa7124fc0d5857a379", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13711410, + id: "Q13711410", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "6b8fcfa6afb3911fecec93ae1dff2b6b6cde5659", + datavalue: { + value: { + time: "+2013-12-07T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P959: [ + { + mainsnak: { + snaktype: "value", + property: "P959", + hash: "55cab2a9d2af860a89a8d0e2eaefedb64202a3d8", + datavalue: { value: "14000228", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$A967D17D-485D-434F-BBF2-E6226E63BA42", + rank: "normal", + references: [ + { + hash: "3e398e6df20323ce88e644e5a1e4ec0bc77a5f41", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "603c636b2210e4a74b7d40c9e969b7e503bbe252", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1538807, + id: "Q1538807", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "d2bace4e146678a5e5f761e9a441b53b95dc2e87", + datavalue: { + value: { + time: "+2014-01-10T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P842: [ + { + mainsnak: { + snaktype: "value", + property: "P842", + hash: "991987fc3fa4d1cfd3a601dcfc9dd1f802255de7", + datavalue: { value: "49734", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$3FF45860-DBC3-4629-AAF8-F2899B6C6876", + rank: "normal", + references: [ + { + hash: "1111bfc1dc63ee739fb9dd3f5534346c7fd478f0", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "00fe2206a3342fa25c0cfe1d08783c49a1986f12", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 796451, + id: "Q796451", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "14c5b75e8d3f4c43cb5b570380dd98e421bb9751", + datavalue: { + value: { + time: "+2014-01-30T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P227: [ + { + mainsnak: { + snaktype: "value", + property: "P227", + hash: "3343c5fd594f8f0264332d87ce95e76ffeaebffd", + datavalue: { value: "4140572-9", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$0059e08d-4308-8401-58e8-2cb683c03837", + rank: "normal", + }, + ], + P349: [ + { + mainsnak: { + snaktype: "value", + property: "P349", + hash: "08812c4ef85f397bf00b015d1baf3b00d81cb9bf", + datavalue: { value: "00616831", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$B7933772-D27D-49D4-B1BB-AA36ADCA81B0", + rank: "normal", + }, + ], + P1014: [ + { + mainsnak: { + snaktype: "value", + property: "P1014", + hash: "3d27204feb184f21c042777dc9674150cb07ee92", + datavalue: { value: "300310388", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$8e3c9dc3-442e-2e61-8617-f4a41b5be668", + rank: "normal", + }, + ], + P646: [ + { + mainsnak: { + snaktype: "value", + property: "P646", + hash: "0c053bce57fe07b05c300a09b322d9f89236884b", + datavalue: { value: "/m/096mb", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$D94D8A4F-3414-4BE0-82C1-306BD136C017", + rank: "normal", + references: [ + { + hash: "2b00cb481cddcac7623114367489b5c194901c4a", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "a94b740202b097dd33355e0e6c00e54b9395e5e0", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 15241312, + id: "Q15241312", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P577: [ + { + snaktype: "value", + property: "P577", + hash: "fde79ecb015112d2f29229ccc1ec514ed3e71fa2", + datavalue: { + value: { + time: "+2013-10-28T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P577"], + }, + ], + }, + ], + P1036: [ + { + mainsnak: { + snaktype: "value", + property: "P1036", + hash: "02435ba66ab8e5fb26652ae1a84695be24b3e22a", + datavalue: { value: "599.757", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$e75ed89a-408d-9bc1-8d99-41663921debd", + rank: "normal", + }, + ], + P1245: [ + { + mainsnak: { + snaktype: "value", + property: "P1245", + hash: "f3da4ca7d35fc3e02a9ea1662688d8f6c4658df0", + datavalue: { value: "5961", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$010e79a0-475e-fcf4-a554-375b64943783", + rank: "normal", + }, + ], + P910: [ + { + mainsnak: { + snaktype: "value", + property: "P910", + hash: "056367b51cd51edd6c2840134fde01cf40469172", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 30202801, - "id": "Q30202801" - }, "type": "wikibase-entityid" + "numeric-id": 6987175, + id: "Q6987175", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$bd49e319-477f-0cd2-a404-642156321081", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "88389772f86dcd7d415ddd029f601412e5cc894a", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 602358, "id": "Q602358"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q140$BC4DE2D4-BF45-49AF-A9A6-C0A976F60825", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "67f2e59eb3f6480bdbaa3954055dfbf8fd045bc4", - "datavalue": { - "value": { + ], + P373: [ + { + mainsnak: { + snaktype: "value", + property: "P373", + hash: "76c006bc5e2975bcda2e7d60ddcbaaa8c84f69e5", + datavalue: { value: "Panthera leo", type: "string" }, + datatype: "string", + }, + type: "statement", + id: "q140$939BA4B2-28D3-4C74-B143-A0EA6F423B43", + rank: "normal", + }, + ], + P846: [ + { + mainsnak: { + snaktype: "value", + property: "P846", + hash: "d0428680cd2b36efde61dc69ccc5a8ff7a735cb5", + datavalue: { value: "5219404", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$4CE8E6D4-E9A1-46F1-8EEF-B469E8485F9E", + rank: "normal", + references: [ + { + hash: "5b8345ffc93a361b71f5d201a97f587e5e57efe5", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "dbb8dd1efbe0158a5227213bd628eeac27a1da65", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1531570, + id: "Q1531570", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "3eb17b10ce02d44f47540a6fbdbb3cbb7e77d5f5", + datavalue: { + value: { + time: "+2015-05-15T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P487: [ + { + mainsnak: { + snaktype: "value", + property: "P487", + hash: "5f93415dd33bfde6a546fdd65e5a7013e012c336", + datavalue: { value: "\ud83e\udd81", type: "string" }, + datatype: "string", + }, + type: "statement", + id: "Q140$da5262fc-4ac5-390b-b424-4f296b2d711d", + rank: "normal", + }, + ], + P2040: [ + { + mainsnak: { + snaktype: "value", + property: "P2040", + hash: "5b13a3fa0fde6ba09d8e417738c05268bd065e32", + datavalue: { value: "6353", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$E97A1A2E-D146-4C62-AE92-5AF5F7E146EF", + rank: "normal", + references: [ + { + hash: "348b5187938d682071c94e22f1b30659af715dc7", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "213dc0d84ed983cbb28466ebb0c45bf8b0730ea2", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 20962955, + id: "Q20962955", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "3d2c713dec9143721ae196af88fee0fde5ae20f2", + datavalue: { + value: { + time: "+2015-09-10T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P935: [ + { + mainsnak: { + snaktype: "value", + property: "P935", + hash: "c3518a9944958337bcce384587f3abc3de6ddf34", + datavalue: { value: "Panthera leo", type: "string" }, + datatype: "string", + }, + type: "statement", + id: "Q140$F7AAEE1F-4D18-4538-99F0-1A2B5AD7269F", + rank: "normal", + }, + ], + P1417: [ + { + mainsnak: { + snaktype: "value", + property: "P1417", + hash: "492d3483075b6915990940a4392f5ec035cbe05e", + datavalue: { value: "animal/lion", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$FE89C38F-6C79-4F06-8C15-81DCAC8D745F", + rank: "normal", + }, + ], + P244: [ + { + mainsnak: { + snaktype: "value", + property: "P244", + hash: "2e41780263804dd45d7deaf7955a2d1d221f6096", + datavalue: { value: "sh85077276", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$634d86d1-45b1-920d-e9ef-78d5f4023288", + rank: "normal", + references: [ + { + hash: "88d810dd1ff791aeb0b5779876b0c9f19acb59b6", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c120f07504c77593a9d734f50361ea829f601960", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 620946, + id: "Q620946", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "0980c2f2b51e6b2d4c1dd9a77b9fb95dc282bc79", + datavalue: { + value: { + time: "+2016-06-01T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P1843: [ + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "3b1cfb68cc46255ceba7ff7893ac1cabbb4ddd92", + datavalue: { + value: { text: "Lion", language: "en" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + qualifiers: { + P7018: [ + { + snaktype: "value", + property: "P7018", + hash: "40a60b39201df345ffbf5aa724269d5fd61ae028", + datavalue: { + value: { "entity-type": "sense", id: "L17815-S1" }, + type: "wikibase-entityid", + }, + datatype: "wikibase-sense", + }, + ], + }, + "qualifiers-order": ["P7018"], + id: "Q140$6E257597-55C7-4AF3-B3D6-0F2204FAD35C", + rank: "normal", + references: [ + { + hash: "eada84c58a38325085267509899037535799e978", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 32059, + id: "Q32059", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "3e51c3c32949f8a45f2c3331f55ea6ae68ecf3fe", + datavalue: { + value: { + time: "+2016-10-21T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + { + hash: "cdc389b112247cb50b855fb86e98b7a7892e96f0", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "e17975e5c866df46673c91b2287a82cf23d14f5a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 27310853, + id: "Q27310853", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P304: [ + { + snaktype: "value", + property: "P304", + hash: "ff7ad3502ff7a4a9b0feeb4248a7bed9767a1ec6", + datavalue: { value: "166", type: "string" }, + datatype: "string", + }, + ], + }, + "snaks-order": ["P248", "P304"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "38a9c57a5c62a707adc86decd2bd00be89eab6f3", + datavalue: { + value: { text: "Leeu", language: "af" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$5E731B05-20D6-491B-97E7-94D90CBB70F0", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "a4455f1ef49d7d17896563760a420031c41d65c1", + datavalue: { + value: { text: "Gyata", language: "ak" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$721B4D81-D948-4002-A13E-0B2567626FD6", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "b955c9239d6ced23c0db577e20219b0417a2dd9b", + datavalue: { + value: { text: "Ley\u00f3n", language: "an" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$E2B52F3D-B12D-48B5-86EA-6A4DCBC091D3", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "e18a8ecb17321c203fcf8f402e82558ce0599b39", + datavalue: { + value: { text: "Li\u00f3n", language: "an" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$339ADC90-41C6-4CDB-B6C3-DA9F952FCC15", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "297bf417fff1510d19b27c08fa9f34e2653b9510", + datavalue: { + value: { + text: "\u0623\u064e\u0633\u064e\u062f\u064c", + language: "ar", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$F1849268-0E70-4EC0-A630-EC0D2DCBB298", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "5577ef6920a3ade2365d878740d1d097fcdae399", + datavalue: { + value: { text: "L\u00e9we", language: "bar" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$BDD65B40-7ECB-4725-B33F-417A83AF5102", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "de8fa35eca4e61dfb8fe2df360e734fb1cd37092", + datavalue: { + value: { text: "L\u00f6we", language: "bar" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$486EE5F1-9AB5-4789-98AC-E435D81E784F", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "246c27f44da8bedd2e3313de393fe648b2b40ea9", + datavalue: { + value: { text: "\u041b\u0435\u045e (Lew)", language: "be" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$47AA6BD4-0B09-4B20-9092-0AEAD8056157", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "64c42db53ef288871161f0a656808f06daae817d", + datavalue: { + value: { text: "\u041b\u044a\u0432 (L\u0103v)", language: "bg" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$ADF0B08A-9626-4821-8118-0A875CBE5FB9", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "6041e2730af3095f4f0cbf331382e22b596d2305", + datavalue: { + value: { text: "\u09b8\u09bf\u0982\u09b9", language: "bn" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$8DF5BDCD-B470-46C3-A44A-7375B8A5DCDE", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "8499f437dc8678b0c4b740b40cab41031fce874d", + datavalue: { + value: { text: "Lle\u00f3", language: "ca" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$F55C3E63-DB2C-4F6D-B10B-4C1BB70C06A0", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "b973abb618a6f17b8a9547b852e5817b5c4da00b", + datavalue: { + value: { text: "\u041b\u043e\u044c\u043c", language: "ce" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$F32B0BFA-3B85-4A26-A888-78FD8F09F943", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "62f53c7229efad1620a5cce4dc5a535d88c4989f", + datavalue: { + value: { text: "Lev", language: "cs" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$1630DAB7-C4D0-4268-A598-8BBB9480221E", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "0df7e23666c947b42aea5572a9f5a987229718d3", + datavalue: { + value: { text: "Llew", language: "cy" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$F33991E8-A532-47F5-B135-A13761DB2E95", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "14942ad0830a0eb7b06704234eea637f99b53a24", + datavalue: { + value: { text: "L\u00f8ve", language: "da" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$478F0603-640A-44BE-9453-700FDD32100F", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "8af089542ef6207b918f656bcf9a96e745970915", + datavalue: { + value: { text: "L\u00f6we", language: "de" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + qualifiers: { + P7018: [ + { + snaktype: "value", + property: "P7018", + hash: "2da239e18a0208847a72fbeab011c8c2fb3b4d99", + datavalue: { + value: { "entity-type": "sense", id: "L41680-S1" }, + type: "wikibase-entityid", + }, + datatype: "wikibase-sense", + }, + ], + }, + "qualifiers-order": ["P7018"], + id: "Q140$11F5F498-3688-4F4B-B2FA-7121BE5AA701", + rank: "normal", + references: [ + { + hash: "cdc389b112247cb50b855fb86e98b7a7892e96f0", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "e17975e5c866df46673c91b2287a82cf23d14f5a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 27310853, + id: "Q27310853", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P304: [ + { + snaktype: "value", + property: "P304", + hash: "ff7ad3502ff7a4a9b0feeb4248a7bed9767a1ec6", + datavalue: { value: "166", type: "string" }, + datatype: "string", + }, + ], + }, + "snaks-order": ["P248", "P304"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "c0c8b50001810c1ec643b88479df82ea85c819a2", + datavalue: { + value: { text: "Dzata", language: "ee" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$8F6EC307-A293-4AFC-8154-E3FF187C0D7D", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "57a3384eeb13d1bcffeb3cf0efd0f3e3f511b35d", + datavalue: { + value: { + text: "\u039b\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9 (Liond\u00e1ri)", + language: "el", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$560B3341-3E06-4D09-8869-FC47C841D14C", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "ee7109a46f8259ae6f52791cfe599b7c4c272831", + datavalue: { + value: { text: "Leono", language: "eo" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$67F2B7A6-1C81-407A-AA61-A1BFF148EC69", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "3b8f4f61c3a18792bfaff5d332f03c80932dce05", + datavalue: { + value: { text: "Le\u00f3n", language: "es" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$DB29EAF7-4405-4030-8056-ED17089B3805", + rank: "normal", + references: [ + { + hash: "d3a8e536300044db1d823eae6891b2c7baa49f66", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 32059, + id: "Q32059", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "620d2e76d21bb1d326fc360db5bece2070115240", + datavalue: { + value: { + time: "+2016-10-19T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "41fffb83f35736829d60f782bdce68463f0ab47c", + datavalue: { + value: { text: "L\u00f5vi", language: "et" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$19B76CC4-AA11-443B-BC76-DB2D0DA5B9CB", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "b96549e5ae538fb7e0b48089508333b31aec8fe7", + datavalue: { + value: { text: "Lehoi", language: "eu" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$88F712C1-4EEF-4E42-8C61-84E55CF2DCE0", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "b343c833d8de3dfd5c8b31336afd137380ab42dc", + datavalue: { + value: { text: "\u0634\u06cc\u0631 (\u0160ayr)", language: "fa" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$B72DB989-EF39-42F5-8FA8-5FC669079DB7", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "51aaf9a4a7c5e77ba931a5280d1fec984c91963b", + datavalue: { + value: { text: "Leijona", language: "fi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$6861CDE9-707D-43AD-B352-3BCD7B9D4267", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "038249fb112acc26895af45fab412395f999ae11", + datavalue: { + value: { text: "Leyva", language: "fo" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$A044100A-C49F-4AA6-8861-F0300F28126E", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "92ec25b64605d026b07b0cda6e623fbbf2f3dfb4", + datavalue: { + value: { text: "Lion", language: "fr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$122623FD-3915-49E9-8890-0B6883317507", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "59be091f7839e7a6061c6d1690ed77f3b21b9ff4", + datavalue: { + value: { text: "L\u00f6\u00f6w", language: "frr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$76B87E52-A02C-4E99-A4B3-D6105B642521", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "126b0f2c5ed11124233dfefff8bd132a1fe1218a", + datavalue: { + value: { text: "Le\u00f3n-leoa", language: "gl" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$A4864784-EED3-4898-83FE-A2FCC0C3982E", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "49e0d3858de566edce1a28b0e96f42b2d0df718f", + datavalue: { + value: { text: "\u0ab8\u0abf\u0a82\u0ab9", language: "gu" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$4EE122CE-7671-480E-86A4-4A4DDABC04BA", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "dff0c422f7403c50d28dd51ca2989d03108b7584", + datavalue: { + value: { text: "Liona", language: "haw" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$FBB6AC65-A224-4C29-8024-079C0687E9FB", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "c174addd56c0f42f6ec3e87c72fb9651e4923a00", + datavalue: { + value: { text: "\u05d0\u05e8\u05d9\u05d4", language: "he" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$B72D9BDB-A2CC-471D-AF20-8F7FB677D533", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "c38a63a06b569fc8fee3e98c4cf8d5501990811e", + datavalue: { + value: { text: "si\u1e45ha)", language: "hi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$9AA9171C-E912-41F2-AB26-643AA538E644", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "94f073519a5b64c48398c73a5f0f135a4f0f4306", + datavalue: { + value: { text: "\u0936\u0947\u0930", language: "hi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$0714B97B-03E0-4ACC-80A0-6A17874DDBA8", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "1fbda5b1494db298c698fc28ed0fe68b1c137b2e", + datavalue: { + value: { text: "\u0938\u093f\u0902\u0939", language: "hi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$3CE22F68-038C-4A94-9A1F-96B82760DEB9", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "fac41ebd8d1da777acd93720267c7a70016156e4", + datavalue: { + value: { text: "Lav", language: "hr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$C5351C11-E287-4D3B-A9B2-56716F0E69E5", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "71e0bda709fb17d58f4bd8e12fff7f937a61673c", + datavalue: { + value: { text: "Oroszl\u00e1n", language: "hu" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$AD06E7D2-3B1F-4D14-B2E9-DD2513BE8B4B", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "05e86680d70a2c0adf9a6e6eb51bbdf8c6ae44bc", + datavalue: { + value: { + text: "\u0531\u057c\u0575\u0578\u0582\u056e", + language: "hy", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$3E02B802-8F7B-4C48-A3F9-FBBFDB0D8DB3", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "979f25bee6af37e19471530c6344a0d22a0d594c", + datavalue: { + value: { text: "Lj\u00f3n", language: "is" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$59788C31-A354-4229-AD89-361CB6076EF7", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "789e5f5a7ec6003076bc7fd2996faf8ca8468719", + datavalue: { + value: { text: "Leone", language: "it" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$4901AE59-7749-43D1-BC65-DEEC0DFEB72F", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "4a5bdf9bb40f1cab9a92b7dba1d1d74a8440c7ed", + datavalue: { + value: { text: "\u30e9\u30a4\u30aa\u30f3 (Raion)", language: "ja" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$4CF2E0D9-5CF3-46A3-A197-938E94270CE2", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "ebba3893211c78dad7ae74a51448e8c7f6e73309", + datavalue: { + value: { text: "\uc0ac\uc790 (saja)", language: "ko" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$64B6CECD-5FFE-4612-819F-CAB2E726B228", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "ed1fe1812cee80102262dd3b7e170759fbeab86a", + datavalue: { + value: { + text: "\u0410\u0440\u0441\u0442\u0430\u043d", + language: "ky", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$3D9597D3-E35F-4EFF-9CAF-E013B45F283F", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "a0868f5f83bb886a408aa9b25b95dbfc59bde4dc", + datavalue: { + value: { text: "Leo", language: "la" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$4D650414-6AFE-430F-892F-B7774AC7AF70", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "054af77b10151632045612df9b96313dfcc3550c", + datavalue: { + value: { text: "Liew", language: "li" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$D5B466A8-AEFB-4083-BF3E-194C5CE45CD3", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "9193a46891a365ee1b0a17dd6e2591babc642811", + datavalue: { + value: { text: "Nkosi", language: "ln" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$55F213DF-5AAB-4490-83CB-B9E5D2B894CD", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "c5952ec6b650f1c66f37194eb88c2889560740b2", + datavalue: { + value: { text: "Li\u016btas", language: "lt" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$8551F80C-A244-4351-A98A-8A9F37A736A2", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "e3541d0807682631f8fff2d224b2cb1b3d2a4c11", + datavalue: { + value: { text: "Lauva", language: "lv" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$488A2D59-533A-4C02-8AC3-01241FE63D94", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "22e20da399aff10787267691b5211b6fc0bddf38", + datavalue: { + value: { text: "\u041b\u0430\u0432 (lav)", language: "mk" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$9E2377E9-1D37-4BBC-A409-1C40CDD99A86", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "fe4c9bc3b3cce21a779f72fae808f8ed213d226b", + datavalue: { + value: { + text: "\u0d38\u0d3f\u0d02\u0d39\u0d02 (simham)", + language: "ml", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$8BEA9E08-4687-434A-9FB4-4B23B2C40838", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "85aa09066722caf2181681a24575ad89ca76210e", + datavalue: { + value: { text: "si\u1e45ha)", language: "mr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$46B51EF5-7ADB-4637-B744-89AD1E3B5D19", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "441f3832d6e3c4439c6986075096c7021a0939dd", + datavalue: { + value: { + text: "\u0936\u0947\u0930 (\u015a\u0113ra)", + language: "mr", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$12BBC825-32E3-4026-A5E5-0330DEB21D79", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "89b35a359c3891dce190d778e9ae0a9634cfd71f", + datavalue: { + value: { text: "\u0938\u093f\u0902\u0939 (singh", language: "mr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$006148E2-658F-4C74-9C3E-26488B7AEB8D", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "5723b45deee51dfe5a2555f2db17bad14acb298a", + datavalue: { + value: { text: "Iljun", language: "mt" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$13D221F5-9763-4550-9CC3-9A697286B785", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "d1ce3ab04f25af38248152eb8caa286b63366c2a", + datavalue: { + value: { text: "Leeuw", language: "nl" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$65E80D17-6F20-4BAE-A2B4-DD934C0BE153", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "12f3384cc32e65dfb501e2fee19ccf709f9df757", + datavalue: { + value: { text: "L\u00f8ve", language: "nn" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$78E95514-1969-4DA3-97CD-0DBADF1223E7", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "a3fedaf780a0d004ba318881f6adbe173750d09e", + datavalue: { + value: { text: "L\u00f8ve", language: "nb" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$809DE1EA-861E-4813-BED7-D9C465341CB3", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "695d2ef10540ba13cf8b3541daa1d39fd720eea0", + datavalue: { + value: { + text: "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed", + language: "nv", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$E9EDAF16-6650-40ED-B888-C524BD00DF40", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "3c143e8a8cebf92d76d3ae2d7e3bb3f87e963fb4", + datavalue: { + value: { + text: "\u0a2c\u0a71\u0a2c\u0a30 \u0a38\u0a3c\u0a47\u0a30", + language: "pa", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$13AE1DAB-4B29-49A5-9893-C0014C61D21E", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "195e48d11222aec830fb1d5c2de898c9528abc57", + datavalue: { + value: { text: "lew afryka\u0144ski", language: "pl" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$6966C1C3-9DD6-48BC-B511-B0827642E41D", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "8bd3ae632e7731ae9e72c50744383006ec6eb73e", + datavalue: { + value: { text: "Le\u00e3o", language: "pt" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$BD454649-347E-4AE5-81B8-360C16C7CDA7", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "79d3336733b7bf4b7dadffd6d6ebabdb892074d1", + datavalue: { + value: { text: "Leu", language: "ro" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$7323EF68-7AA0-4D38-82D8-0A94E61A26F0", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "376b852a92b6472e969ae7b995c4aacea23955eb", + datavalue: { + value: { text: "\u041b\u0435\u0432 (Lev)", language: "ru" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$A7C413B9-0916-4534-941D-C24BA0334816", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "aa323e0bea79d79900227699b3d42d689a772ca1", + datavalue: { + value: { text: "Lioni", language: "sc" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$2EF83D2C-0DB9-4D3C-8FDD-86237E566260", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "a290fe08983742eac8b5bc479022564fb6b2ce81", + datavalue: { + value: { text: "Lev", language: "sl" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$B276673A-08C1-47E2-99A9-D0861321E157", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "d3b070ff1452d47f87c109a9e0bfa52e61b24a4e", + datavalue: { + value: { text: "Libubesi", language: "ss" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$86BFAB38-1DB8-4903-A17D-A6B8E81819CC", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "3adea59d97f3caf9bb6b1c3d7ae6365f7f656dca", + datavalue: { + value: { text: "Tau", language: "st" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$2FA8893D-2401-42E9-8DC3-288CC1DEDB0C", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "0e73b32fe31a107a95de83706a12f2db419c6909", + datavalue: { + value: { text: "Lejon", language: "sv" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$1A8E006E-CC7B-4066-9DE7-9B82D096779E", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "34550af2fdc48f77cf66cabc5c59d1acf1d8afd0", + datavalue: { + value: { text: "Simba", language: "sw" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$B02CA616-44CF-4AA6-9734-2C05810131EB", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "701f87cf9926c9af2c41434ff130dcb234a6cd95", + datavalue: { + value: { + text: "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd", + language: "ta", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$DA87A994-A002-45AD-A71F-99FB72F8B92F", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "856fd4809c90e3c34c6876e4410661dc04f5da8d", + datavalue: { + value: { text: "\u0e2a\u0e34\u0e07\u0e42\u0e15", language: "th" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$BDA8E989-3537-4662-8CC3-33534705A7F1", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "f8598a8369426da0c86bf8bab356a927487eae66", + datavalue: { + value: { text: "Aslan", language: "tr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$AAE5F227-C0DB-4DF3-B1F4-517699BBDDF1", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "f3c8320bd46913aee164999ab7f68388c1bd9920", + datavalue: { + value: { text: "\u041b\u0435\u0432 (Lev)", language: "uk" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$494C3503-6016-4539-83AF-6344173C2DCB", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "6cc2d534293320533e15dc713f1d2c07b3811b6a", + datavalue: { + value: { text: "Leon", language: "vec" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$E6F1DA81-9F36-4CC8-B57E-95E3BDC2F5D0", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "32553481e45abf6f5e6292baea486e978c36f8fe", + datavalue: { + value: { text: "S\u01b0 t\u1eed", language: "vi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$11D7996C-0492-41CC-AEE7-3C136172DFC7", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "69077fc29f9251d1de124cd3f3c45cd6f0bb6b65", + datavalue: { + value: { text: "\u05dc\u05d9\u05d9\u05d1", language: "yi" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$969FEF9A-C1C7-41FE-8181-07F6D87B0346", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "e3aaa8cde18be4ea6b4af6ca62b83e7dc23d76e1", + datavalue: { + value: { text: "\u72ee\u5b50 (sh\u012bzi)", language: "zh" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$3BC22F6C-F460-4354-9BA2-28CEDA9FF170", + rank: "normal", + references: [ + { + hash: "2e0c13df5b13edc9b3db9d8129e466c0894710ac", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "2b1e96d67dc01973d72472f712fd98ce87c6f0d7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 13679, + id: "Q13679", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "98f5efae94b2bb9f8ffee6c677ee71f836743ef6", + datavalue: { + value: { text: "Lion d'Afrique", language: "fr" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$62D09BBF-718A-4139-AF50-DA4185ED67F2", + rank: "normal", + references: [ + { + hash: "362e3c5d6de1d193ef97205ba38834ba075191fc", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 32059, + id: "Q32059", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "c7813bad20c2553e26e45c37e3502ce7252312df", + datavalue: { + value: { + time: "+2016-10-20T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "c584bdbd3cdc1215292a4971b920c684d103ea06", + datavalue: { + value: { text: "African Lion", language: "en" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$C871BB58-C689-4DBA-A088-DAC205377979", + rank: "normal", + references: [ + { + hash: "eada84c58a38325085267509899037535799e978", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 32059, + id: "Q32059", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "3e51c3c32949f8a45f2c3331f55ea6ae68ecf3fe", + datavalue: { + value: { + time: "+2016-10-21T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "1d03eace9366816c6fda340c0390caac2f3cea8e", + datavalue: { + value: { text: "L\u00e9iw", language: "lb" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$c65c7614-4d6e-3a87-9771-4f8c13618249", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "925c7abced1e89fa7e8000dc9dc78627cdac9769", + datavalue: { + value: { text: "Lle\u00f3n", language: "ast" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$1024eadb-45dd-7d9a-15f6-8602946ba661", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "0bda7868c3f498ba6fde78d46d0fbcf286e42dd8", + datavalue: { + value: { + text: "\u0644\u064e\u064a\u0652\u062b\u064c", + language: "ar", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$c226ff70-48dd-7b4d-00ff-7a683fe510aa", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "28de99c4aa35cc049cf8c9dd18af1791944137d9", + datavalue: { + value: { text: "\u10da\u10dd\u10db\u10d8", language: "ka" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$8fe60d1a-465a-9614-cbe4-595e22429b0c", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "4550db0f44e21c5eadeaa5a1d8fc614c9eb05f52", + datavalue: { + value: { text: "leon", language: "ga" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$4950cb9c-4f1e-ce27-0d8c-ba3f18096044", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1843", + hash: "9d268eb76ed921352c205b3f890d1f9428f638f3", + datavalue: { + value: { text: "Singa", language: "ms" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + type: "statement", + id: "Q140$67401360-49dd-b13c-8269-e703b30c9a53", + rank: "normal", + }, + ], + P627: [ + { + mainsnak: { + snaktype: "value", + property: "P627", + hash: "3642ac96e05180279c47a035c129d3af38d85027", + datavalue: { value: "15951", type: "string" }, + datatype: "string", + }, + type: "statement", + id: "Q140$6BE03095-BC68-4CE5-BB99-9F3E33A6F31D", + rank: "normal", + references: [ + { + hash: "182efbdb9110d036ca433f3b49bd3a1ae312858b", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "ba14d022d7e0c8b74595e7b8aaa1bc2451dd806a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 32059, + id: "Q32059", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "8c1c5174f4811115ea8a0def725fdc074c2ef036", + datavalue: { + value: { + time: "+2016-07-10T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P2833: [ + { + mainsnak: { + snaktype: "value", + property: "P2833", + hash: "519877b77b20416af2401e5c0645954c6700d6fd", + datavalue: { value: "panthera-leo", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$C0A723AE-ED2E-4FDC-827F-496E4CF29A52", + rank: "normal", + }, + ], + P3063: [ + { + mainsnak: { + snaktype: "value", + property: "P3063", + hash: "81cdb0273eaf0a0126b62e2ff43b8e09505eea54", + datavalue: { + value: { + amount: "+108", + unit: "http://www.wikidata.org/entity/Q573", + upperBound: "+116", + lowerBound: "+100", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + id: "Q140$878ff87d-40d0-bb2b-c83d-4cef682c2687", + rank: "normal", + references: [ + { + hash: "7d748004a43983fabae420123742fda0e9b52840", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "f618501ace3a6524b053661d067b775547f96f58", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 26706243, + id: "Q26706243", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P478: [ + { + snaktype: "value", + property: "P478", + hash: "ca3c5e6054c169ee3d0dfaf660f3eecd77942070", + datavalue: { value: "4", type: "string" }, + datatype: "string", + }, + ], + P304: [ + { + snaktype: "value", + property: "P304", + hash: "dd1977567f22f4cf510adfaadf5e3574813b3521", + datavalue: { value: "46", type: "string" }, + datatype: "string", + }, + ], + }, + "snaks-order": ["P248", "P478", "P304"], + }, + ], + }, + ], + P3031: [ + { + mainsnak: { + snaktype: "value", + property: "P3031", + hash: "e6271e8d12b20c9735d2bbd80eed58581059bf3a", + datavalue: { value: "PNTHLE", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$65AD2857-AB65-4CC0-9AB9-9D6C924784FE", + rank: "normal", + }, + ], + P3151: [ + { + mainsnak: { + snaktype: "value", + property: "P3151", + hash: "e85e5599d303d9a6bb360f3133fb69a76d98d0e2", + datavalue: { value: "41964", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$15D7A4EB-F0A3-4C61-8D2B-E557D7BF5CF7", + rank: "normal", + }, + ], + P3186: [ + { + mainsnak: { + snaktype: "value", + property: "P3186", + hash: "85ec7843064210afdfef6ec565a47f229c6d15e5", + datavalue: { value: "644245", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$6903E136-2DB2-42C9-98CB-82F61208FDAD", + rank: "normal", + references: [ + { + hash: "5790a745e549ea7e4e6d7ca467148b544529ba96", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "c897ca3efd1604ef7b80a14ac0d2b8d6849c0856", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 26936509, + id: "Q26936509", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "555ca5385c445e4fd4762281d4873682eff2ce30", + datavalue: { + value: { + time: "+2016-09-24T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + { + hash: "3edd37192f877cad0ff97acc3db56ef2cc83945b", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4f7c4fd187630ba8cbb174c2756113983df4ce82", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45029859, + id: "Q45029859", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "56b6aa0388c9a2711946589902bc195718bb0675", + datavalue: { + value: { + time: "+2017-12-26T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + { + hash: "1318ed8ea451b84fe98461305665d8688603bab3", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "0fbeeecce08896108ed797d8ec22c7c10a6015e2", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45029998, + id: "Q45029998", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "4bac1c0d2ffc45d91b51fc0881eb6bcc7916e854", + datavalue: { + value: { + time: "+2018-01-02T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + { + hash: "6f761664a6f331d95bbaa1434447d82afd597a93", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "6c09d1d89e83bd0dfa6c94e01d24a9a47489d83e", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 58035056, + id: "Q58035056", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "03182012ca72fcd757b8a1fe05ba927cbe9ef374", + datavalue: { + value: { + time: "+2018-11-02T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P3485: [ + { + mainsnak: { + snaktype: "value", + property: "P3485", + hash: "df4e58fc2a196833ab3e33483099e2481e61ba9e", + datavalue: { value: { amount: "+112", unit: "1" }, type: "quantity" }, + datatype: "quantity", + }, + type: "statement", + id: "Q140$4B70AA09-AE2F-4F4C-9BAF-09890CDA11B8", + rank: "normal", + references: [ + { + hash: "fa278ebfc458360e5aed63d5058cca83c46134f1", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "e4f6d9441d0600513c4533c672b5ab472dc73694", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 328, + id: "Q328", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P3827: [ + { + mainsnak: { + snaktype: "value", + property: "P3827", + hash: "6bb26d581721d7330c407259d46ab5e25cc4a6b1", + datavalue: { value: "lions", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$CCDE6F7D-B4EA-4875-A4D6-5649ACFA8E2F", + rank: "normal", + }, + ], + P268: [ + { + mainsnak: { + snaktype: "value", + property: "P268", + hash: "a20cdf81e39cd47f4da30073671792380029924c", + datavalue: { value: "11932251d", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$000FDB77-C70C-4464-9F00-605787964BBA", + rank: "normal", + references: [ + { + hash: "d4bd87b862b12d99d26e86472d44f26858dee639", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "f30cbd35620c4ea6d0633aaf0210a8916130469b", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 8447, + id: "Q8447", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P3417: [ + { + mainsnak: { + snaktype: "value", + property: "P3417", + hash: "e3b5d21350aef37f27ad8b24142d6b83d9eec0a6", + datavalue: { value: "Lions", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$1f96d096-4e4b-06de-740f-7b7215e5ae3f", + rank: "normal", + }, + ], + P4024: [ + { + mainsnak: { + snaktype: "value", + property: "P4024", + hash: "a698e7dcd6f9b0b00ee8e02846c668db83064833", + datavalue: { value: "Panthera_leo", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$F5DC21E8-BF52-4A0D-9A15-63B89297BD70", + rank: "normal", + references: [ + { + hash: "d4bd87b862b12d99d26e86472d44f26858dee639", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "f30cbd35620c4ea6d0633aaf0210a8916130469b", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 8447, + id: "Q8447", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P1225: [ + { + mainsnak: { + snaktype: "value", + property: "P1225", + hash: "9af40267f10f15926877e9a3f78faeab7b0dda82", + datavalue: { value: "10665610", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$268074ED-3CD7-46C9-A8FF-8C3679C45547", + rank: "normal", + }, + ], + P4728: [ + { + mainsnak: { + snaktype: "value", + property: "P4728", + hash: "37eafa980604019b327b1a3552313fb7ae256697", + datavalue: { value: "105514", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$50C24ECC-C42C-4A58-8F34-6AF0AC6C4EFE", + rank: "normal", + references: [ + { + hash: "d4bd87b862b12d99d26e86472d44f26858dee639", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "f30cbd35620c4ea6d0633aaf0210a8916130469b", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 8447, + id: "Q8447", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P3219: [ + { + mainsnak: { + snaktype: "value", + property: "P3219", + hash: "dedb8825588940caff5a34d04a0e69af296f05dd", + datavalue: { value: "lion", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$2A5A2CA3-AB6E-4F68-927F-042D1BD22915", + rank: "normal", + }, + ], + P1343: [ + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "5b0ef3d5413cd39d887fbe70d2d3b3f4a94ea9d8", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 24451091, - "id": "Q24451091" - }, "type": "wikibase-entityid" + "numeric-id": 1138524, + id: "Q1138524", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$906ae22e-4c63-d325-c91e-dc3ee6b7504d", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "42346dfe9209b7359c1f5db829a368b38d407797", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 19180675, "id": "Q19180675"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "f7bf629d348040dd1a59dc5a3199edb50279e8f5", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 19997008, + id: "Q19997008", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P805"], + id: "Q140$DFE4D4B0-0D84-41F2-B448-4A81AC982927", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "195bd04166c04364a657fcd18abd1a082dad3cb0", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "6bc15c6f82feca4f3b173c90209a416f99464cac", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 24758519, - "id": "Q24758519" - }, "type": "wikibase-entityid" + "numeric-id": 4086271, + id: "Q4086271", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$92e7eeb1-4a72-9abf-4260-a96abc32bc42", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "7d6f86cef085693a10b0e0663a0960f58d0e15e2", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 4173137, "id": "Q4173137"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "69ace59e966574e4ffb454d26940a58fb45ed7de", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 25295952, + id: "Q25295952", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P805"], + id: "Q140$b82b0461-4ff0-10ac-9825-d4b95fc7a85a", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "75e5bdfbbf8498b195840749ef3a9bd309b796f7", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "ecb04d74140f2ee856c06658b03ec90a21c2edf2", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 25054587, - "id": "Q25054587" - }, "type": "wikibase-entityid" + "numeric-id": 1970746, + id: "Q1970746", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$6c9c319a-4e71-540e-8866-a6017f0e6bae", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "75dd89e79770a3e631dbba27144940f8f1bc1773", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 1768721, "id": "Q1768721"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "169607f1510535f3e1c5e7debce48d1903510f74", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 30202801, + id: "Q30202801", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P805"], + id: "Q140$bd49e319-477f-0cd2-a404-642156321081", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "a1b448ff5f8818a2254835e0816a03a785bac665", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "88389772f86dcd7d415ddd029f601412e5cc894a", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 96599885, - "id": "Q96599885" - }, "type": "wikibase-entityid" + "numeric-id": 602358, + id: "Q602358", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$A0FD93F4-A401-47A1-BC8E-F0D35A8E8BAD", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "4cfd4eb1fe49d401455df557a7d9b1154f22a725", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 3181656, "id": "Q3181656"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", }, - "datatype": "wikibase-item" - }, - "type": "statement", - "qualifiers": { - "P1932": [{ - "snaktype": "value", - "property": "P1932", - "hash": "a3f6e8ce10c4527693415dbc99b5ea285b2f411c", - "datavalue": {"value": "Lion, The", "type": "string"}, - "datatype": "string" - }] - }, - "qualifiers-order": ["P1932"], - "id": "Q140$100f480e-4ad9-b340-8251-4e875d00315d", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "d5011798f92464584d8ccfc5f19f18f3659668bb", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 106727050, "id": "Q106727050"}, - "type": "wikibase-entityid" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "67f2e59eb3f6480bdbaa3954055dfbf8fd045bc4", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 24451091, + id: "Q24451091", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], }, - "datatype": "wikibase-item" + "qualifiers-order": ["P805"], + id: "Q140$906ae22e-4c63-d325-c91e-dc3ee6b7504d", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P1810": [{ - "snaktype": "value", - "property": "P1810", - "hash": "7d78547303d5e9e014a7c8cef6072faee91088ce", - "datavalue": {"value": "Lions", "type": "string"}, - "datatype": "string" - }], - "P585": [{ - "snaktype": "value", - "property": "P585", - "hash": "ffb837135313cad3b2545c4b9ce5ee416deda3e2", - "datavalue": { - "value": { - "time": "+2021-05-07T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "qualifiers-order": ["P1810", "P585"], - "id": "Q140$A4D208BD-6A69-4561-B402-2E17AAE6E028", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P1343", - "hash": "d12a9ecb0df8fce076df898533fea0339e5881bd", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 10886720, "id": "Q10886720"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "qualifiers": { - "P805": [{ - "snaktype": "value", - "property": "P805", - "hash": "52ddab8de77b01303d508a1de615ca13060ec188", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "42346dfe9209b7359c1f5db829a368b38d407797", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 107513600, - "id": "Q107513600" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P805"], - "id": "Q140$07daf548-4c8d-fa7c-16f4-4c7062f7e48a", - "rank": "normal" - }], - "P4733": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4733", - "hash": "fc789f67f6d4d9b5879a8631eefe61f51a60f979", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 3177438, "id": "Q3177438"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "id": "Q140$3773ba15-4723-261a-f9a8-544496938efa", - "rank": "normal", - "references": [{ - "hash": "649ae5511d5389d870d19e83543fa435de796536", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "9931bb1a17358e94590f8fa0b9550de881616d97", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 784031, - "id": "Q784031" - }, "type": "wikibase-entityid" + "numeric-id": 19180675, + id: "Q19180675", }, - "datatype": "wikibase-item" - }] + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "snaks-order": ["P143"] - }] - }], - "P5019": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5019", - "hash": "44aac3d8a2bd240b4bc81741a0980dc48781181b", - "datavalue": {"value": "l\u00f6we", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$2be40b22-49f1-c9e7-1812-8e3fd69d662d", "rank": "normal" - }], - "P2924": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2924", - "hash": "710d75c07e28936461d03b20b2fc7455599301a1", - "datavalue": {"value": "2135124", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$6326B120-CE04-4F02-94CA-D7BBC2589A39", "rank": "normal" - }], - "P5055": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5055", - "hash": "c5264fc372b7e66566d54d73f86c8ab8c43fb033", - "datavalue": {"value": "10196306", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$F8D43B92-CC3A-4967-A28F-C3E6308946F6", - "rank": "normal", - "references": [{ - "hash": "7131076724beb97fed351cb7e7f6ac6d61dd05b9", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "1e3ad3cb9e0170e28b7c7c335fba55cafa6ef789", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 51885189, - "id": "Q51885189" - }, "type": "wikibase-entityid" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "195bd04166c04364a657fcd18abd1a082dad3cb0", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 24758519, + id: "Q24758519", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "2b1446fcfcd471ab6d36521b4ad2ac183ff8bc0d", - "datavalue": { - "value": { - "time": "+2018-06-07T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] + ], }, - "snaks-order": ["P248", "P813"] - }] - }], - "P5221": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5221", - "hash": "623ca9614dd0d8b8720bf35b4d57be91dcef5fe6", - "datavalue": {"value": "123566", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$472fe544-402d-2574-6b2e-98c5b01bb294", "rank": "normal" - }], - "P5698": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5698", - "hash": "e966694183143d709403fae7baabb5fdf98d219a", - "datavalue": {"value": "70719", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$EF3F712D-B0E5-4151-81E4-67804D6241E6", "rank": "normal" - }], - "P5397": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5397", - "hash": "49a827bc1853a3b5612b437dd61eb5c28dc0bab0", - "datavalue": {"value": "12799", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$DE37BF10-A59D-48F1-926A-7303EDEEDDD0", "rank": "normal" - }], - "P6033": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6033", - "hash": "766727ded3adbbfec0bed77affc89ea4e5214d65", - "datavalue": {"value": "panthera-leo", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$A27BADCC-0F72-45A5-814B-BDE62BD7A1B4", "rank": "normal" - }], - "P18": [{ - "mainsnak": { - "snaktype": "value", - "property": "P18", - "hash": "d3ceb5bb683335c91781e4d52906d2fb1cc0c35d", - "datavalue": {"value": "Lion waiting in Namibia.jpg", "type": "string"}, - "datatype": "commonsMedia" + "qualifiers-order": ["P805"], + id: "Q140$92e7eeb1-4a72-9abf-4260-a96abc32bc42", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P21": [{ - "snaktype": "value", - "property": "P21", - "hash": "0576a008261e5b2544d1ff3328c94bd529379536", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 44148, "id": "Q44148"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P2096": [{ - "snaktype": "value", - "property": "P2096", - "hash": "6923fafa02794ae7d0773e565de7dd49a2694b38", - "datavalue": { - "value": {"text": "Lle\u00f3", "language": "ca"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, { - "snaktype": "value", - "property": "P2096", - "hash": "563784f05211416fda8662a0773f52165ccf6c2a", - "datavalue": { - "value": {"text": "Machu de lle\u00f3n en Namibia", "language": "ast"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, { - "snaktype": "value", - "property": "P2096", - "hash": "52722803d98964d77b79d3ed62bd24b4f25e6993", - "datavalue": { - "value": {"text": "\u043b\u044a\u0432", "language": "bg"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }] - }, - "qualifiers-order": ["P21", "P2096"], - "id": "q140$5903FDF3-DBBD-4527-A738-450EAEAA45CB", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P18", - "hash": "6907d4c168377a18d6a5eb390ab32a7da42d8218", - "datavalue": {"value": "Okonjima Lioness.jpg", "type": "string"}, - "datatype": "commonsMedia" - }, - "type": "statement", - "qualifiers": { - "P21": [{ - "snaktype": "value", - "property": "P21", - "hash": "a274865baccd3ff04c28d5ffdcc12e0079f5a201", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 43445, "id": "Q43445"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P2096": [{ - "snaktype": "value", - "property": "P2096", - "hash": "a9d1363e8fc83ba822c45a81de59fe5b8eb434cf", - "datavalue": { - "value": { - "text": "\u043b\u044a\u0432\u0438\u0446\u0430", - "language": "bg" - }, "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, { - "snaktype": "value", - "property": "P2096", - "hash": "b36ab7371664b7b62ee7be65db4e248074a5330c", - "datavalue": { - "value": {"text": "Lleona n'Okonjima Lodge, Namibia", "language": "ast"}, - "type": "monolingualtext" - }, - "datatype": "monolingualtext" - }, { - "snaktype": "value", - "property": "P2096", - "hash": "31c78a574eabc0426d7984aa4988752e35b71f0c", - "datavalue": {"value": {"text": "lwica", "language": "pl"}, "type": "monolingualtext"}, - "datatype": "monolingualtext" - }] - }, - "qualifiers-order": ["P21", "P2096"], - "id": "Q140$4da15225-f7dc-4942-a685-0669e5d3af14", - "rank": "normal" - }], - "P6573": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6573", - "hash": "c27b457b12eeecb053d60af6ecf9b0baa133bef5", - "datavalue": {"value": "L\u00f6we", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$45B1C3EB-E335-4245-A193-8C48B4953E51", "rank": "normal" - }], - "P443": [{ - "mainsnak": { - "snaktype": "value", - "property": "P443", - "hash": "8a9afb9293804f976c415060900bf9afbc2cfdff", - "datavalue": {"value": "LL-Q188 (deu)-Sebastian Wallroth-L\u00f6we.wav", "type": "string"}, - "datatype": "commonsMedia" - }, - "type": "statement", - "qualifiers": { - "P407": [{ - "snaktype": "value", - "property": "P407", - "hash": "46bfd327b830f66f7061ea92d1be430c135fa91f", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 188, "id": "Q188"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P407"], - "id": "Q140$5EC64299-429F-45E8-B18F-19325401189C", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P443", - "hash": "7d058dfd1e8a41f026974faec3dc0588e29c6854", - "datavalue": {"value": "LL-Q150 (fra)-Ash Crow-lion.wav", "type": "string"}, - "datatype": "commonsMedia" - }, - "type": "statement", - "qualifiers": { - "P407": [{ - "snaktype": "value", - "property": "P407", - "hash": "d197d0a5efa4b4c23a302a829dd3ef43684fe002", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 150, "id": "Q150"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P407"], - "id": "Q140$A4575261-6577-4EF6-A0C9-DA5FA523D1C2", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P443", - "hash": "79b9f51c9b4eec305813d5bb697b403d798cf1c5", - "datavalue": { - "value": "LL-Q33965 (sat)-Joy sagar Murmu-\u1c60\u1c69\u1c5e.wav", - "type": "string" - }, - "datatype": "commonsMedia" - }, - "type": "statement", - "qualifiers": { - "P407": [{ - "snaktype": "value", - "property": "P407", - "hash": "58ae6998321952889f733126c11c582eeef20e72", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 33965, "id": "Q33965"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P407"], - "id": "Q140$7eedc8fa-4d1c-7ee9-3c67-0c89ef464d9f", - "rank": "normal", - "references": [{ - "hash": "d0b5c88b6f49dda9160c706291a9b8645825d99c", - "snaks": { - "P854": [{ - "snaktype": "value", - "property": "P854", - "hash": "38c1012cea9eb73cf1bd11eba0c2f745d2463340", - "datavalue": {"value": "https://lingualibre.org/wiki/Q403065", "type": "string"}, - "datatype": "url" - }] - }, - "snaks-order": ["P854"] - }] - }], - "P1296": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1296", - "hash": "c1f872d4cd22219a7315c0198a83c1918ded97ee", - "datavalue": {"value": "0120024", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$6C51384B-2EBF-4E6B-9201-A44F0A145C04", "rank": "normal" - }], - "P486": [{ - "mainsnak": { - "snaktype": "value", - "property": "P486", - "hash": "b7003b0fb28287301200b6b3871a5437d877913b", - "datavalue": {"value": "D008045", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$B2F98DD2-B679-43DD-B731-FA33FB1EE4B9", "rank": "normal" - }], - "P989": [{ - "mainsnak": { - "snaktype": "value", - "property": "P989", - "hash": "132884b2a696a8b56c8b1460e126f745e2fa6d01", - "datavalue": {"value": "Ru-Lion (intro).ogg", "type": "string"}, - "datatype": "commonsMedia" - }, - "type": "statement", - "qualifiers": { - "P407": [{ - "snaktype": "value", - "property": "P407", - "hash": "d291ddb7cd77c94a7bd709a8395934147e0864fc", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 7737, "id": "Q7737"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P407"], - "id": "Q140$857D8831-673B-427E-A182-6A9FFA980424", - "rank": "normal" - }], - "P51": [{ - "mainsnak": { - "snaktype": "value", - "property": "P51", - "hash": "73b0e8c8458ebc27374fd08d8ef5241f2f28e3e9", - "datavalue": {"value": "Lion raring-sound1TamilNadu178.ogg", "type": "string"}, - "datatype": "commonsMedia" - }, "type": "statement", "id": "Q140$1c254aff-48b1-d3c5-930c-b360ce6fe043", "rank": "normal" - }], - "P4212": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4212", - "hash": "e006ce3295d617a4818dc758c28f444446538019", - "datavalue": {"value": "pcrt5TAeZsO7W4", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$AD6CD534-1FD2-4AC7-9CF8-9D2B4C46927C", "rank": "normal" - }], - "P2067": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2067", - "hash": "97a863433c30b47a6175abb95941d185397ea14a", - "datavalue": { - "value": {"amount": "+1.65", "unit": "http://www.wikidata.org/entity/Q11570"}, - "type": "quantity" - }, - "datatype": "quantity" - }, - "type": "statement", - "qualifiers": { - "P642": [{ - "snaktype": "value", - "property": "P642", - "hash": "f5e24bc6ec443d6cb3678e4561bc298090b54f60", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 4128476, "id": "Q4128476"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P642"], - "id": "Q140$198da244-7e66-4258-9434-537e9ce0ffab", - "rank": "normal", - "references": [{ - "hash": "94a79329d5eac70f7ddb005e0d1dc78c53e77797", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45106562, - "id": "Q45106562" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P2067", - "hash": "ba9933059ce368e3afde1e96d78b1217172c954e", - "datavalue": { - "value": {"amount": "+188", "unit": "http://www.wikidata.org/entity/Q11570"}, - "type": "quantity" - }, - "datatype": "quantity" - }, - "type": "statement", - "qualifiers": { - "P642": [{ - "snaktype": "value", - "property": "P642", - "hash": "b388540fc86300a506b3a753ec58dec445525ffa", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "7d6f86cef085693a10b0e0663a0960f58d0e15e2", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 78101716, - "id": "Q78101716" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P21": [{ - "snaktype": "value", - "property": "P21", - "hash": "0576a008261e5b2544d1ff3328c94bd529379536", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 44148, "id": "Q44148"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P642", "P21"], - "id": "Q140$a3092626-4295-efb8-bbb6-eed913d02fc7", - "rank": "normal", - "references": [{ - "hash": "94a79329d5eac70f7ddb005e0d1dc78c53e77797", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45106562, - "id": "Q45106562" - }, "type": "wikibase-entityid" + "numeric-id": 4173137, + id: "Q4173137", }, - "datatype": "wikibase-item" - }] + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "snaks-order": ["P248"] - }] - }, { - "mainsnak": { - "snaktype": "value", - "property": "P2067", - "hash": "6951281811b2a8a3a78044e2003d6c162d5ba1a3", - "datavalue": { - "value": {"amount": "+126", "unit": "http://www.wikidata.org/entity/Q11570"}, - "type": "quantity" + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "75e5bdfbbf8498b195840749ef3a9bd309b796f7", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 25054587, + id: "Q25054587", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], }, - "datatype": "quantity" + "qualifiers-order": ["P805"], + id: "Q140$6c9c319a-4e71-540e-8866-a6017f0e6bae", + rank: "normal", }, - "type": "statement", - "qualifiers": { - "P642": [{ - "snaktype": "value", - "property": "P642", - "hash": "b388540fc86300a506b3a753ec58dec445525ffa", - "datavalue": { - "value": { + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "75dd89e79770a3e631dbba27144940f8f1bc1773", + datavalue: { + value: { "entity-type": "item", - "numeric-id": 78101716, - "id": "Q78101716" - }, "type": "wikibase-entityid" + "numeric-id": 1768721, + id: "Q1768721", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }], - "P21": [{ - "snaktype": "value", - "property": "P21", - "hash": "a274865baccd3ff04c28d5ffdcc12e0079f5a201", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 43445, "id": "Q43445"}, - "type": "wikibase-entityid" + datatype: "wikibase-item", + }, + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "a1b448ff5f8818a2254835e0816a03a785bac665", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 96599885, + id: "Q96599885", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P805"], + id: "Q140$A0FD93F4-A401-47A1-BC8E-F0D35A8E8BAD", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "4cfd4eb1fe49d401455df557a7d9b1154f22a725", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 3181656, + id: "Q3181656", + }, + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P642", "P21"], - "id": "Q140$20d80fe2-4796-23d1-42c2-c103546aa874", - "rank": "normal", - "references": [{ - "hash": "94a79329d5eac70f7ddb005e0d1dc78c53e77797", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45106562, - "id": "Q45106562" - }, "type": "wikibase-entityid" + datatype: "wikibase-item", + }, + type: "statement", + qualifiers: { + P1932: [ + { + snaktype: "value", + property: "P1932", + hash: "a3f6e8ce10c4527693415dbc99b5ea285b2f411c", + datavalue: { value: "Lion, The", type: "string" }, + datatype: "string", }, - "datatype": "wikibase-item" - }] + ], }, - "snaks-order": ["P248"] - }] - }], - "P7725": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7725", - "hash": "e9338e052dfaa9267c2357bec2e167ca625af667", - "datavalue": { - "value": { - "amount": "+2.5", - "unit": "1", - "upperBound": "+4.0", - "lowerBound": "+1.0" - }, "type": "quantity" - }, - "datatype": "quantity" + "qualifiers-order": ["P1932"], + id: "Q140$100f480e-4ad9-b340-8251-4e875d00315d", + rank: "normal", }, - "type": "statement", - "id": "Q140$f1f04a23-0d34-484a-9419-78d12958170c", - "rank": "normal", - "references": [{ - "hash": "94a79329d5eac70f7ddb005e0d1dc78c53e77797", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45106562, - "id": "Q45106562" - }, "type": "wikibase-entityid" + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "d5011798f92464584d8ccfc5f19f18f3659668bb", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 106727050, + id: "Q106727050", }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }] - }], - "P4214": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4214", - "hash": "5a112dbdaed17b1ee3fe7a63b1f978e5fd41008a", - "datavalue": { - "value": {"amount": "+27", "unit": "http://www.wikidata.org/entity/Q577"}, - "type": "quantity" - }, - "datatype": "quantity" - }, - "type": "statement", - "id": "Q140$ec1ccab2-f506-4c81-9179-4625bbbbbe27", - "rank": "normal", - "references": [{ - "hash": "a8ccf5105b0e2623ae145dd8a9b927c9bd957ddf", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "5b45c23ddb076fe9c5accfe4a4bbd1c24c4c87cb", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 83566668, - "id": "Q83566668" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }] - }], - "P7862": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7862", - "hash": "6e74ddb544498b93407179cc9a7f9b8610762ff5", - "datavalue": { - "value": {"amount": "+8", "unit": "http://www.wikidata.org/entity/Q5151"}, - "type": "quantity" - }, - "datatype": "quantity" - }, - "type": "statement", - "id": "Q140$17b64a1e-4a13-9e2a-f8a2-a9317890aa53", - "rank": "normal", - "references": [{ - "hash": "94a79329d5eac70f7ddb005e0d1dc78c53e77797", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 45106562, - "id": "Q45106562" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }] - }], - "P7818": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7818", - "hash": "5c7bac858cf66d079e6c13c88f3f001eb446cdce", - "datavalue": {"value": "Lion", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$C2D3546E-C42A-404A-A288-580F9C705E12", "rank": "normal" - }], - "P7829": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7829", - "hash": "17fbb02db65a7e80691f58be750382d61148406e", - "datavalue": {"value": "Lion", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$74998F51-E783-40CB-A56A-3189647AB3D4", "rank": "normal" - }], - "P7827": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7827", - "hash": "f85db9fe2c187554aefc51e5529d75e0c5af4767", - "datavalue": {"value": "Le\u00f3n", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$5DA64E1B-F1F1-4254-8629-985DFE8672A2", "rank": "normal" - }], - "P7822": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7822", - "hash": "3cd23fddc416227c2ba85d91aa03dc80a8e95836", - "datavalue": {"value": "Leone", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$DF657EF2-67A8-4272-871D-E95B3719A8B6", "rank": "normal" - }], - "P6105": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6105", - "hash": "8bbda0afe53fc428d3a0d9528c97d2145ee41dce", - "datavalue": {"value": "79432", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$2AE92335-1BC6-4B92-BAF3-9AB41608E638", "rank": "normal" - }], - "P6864": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6864", - "hash": "6f87ce0800057dbe88f27748b3077938973eb5c8", - "datavalue": {"value": "85426", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$9089C4B9-59A8-45A6-821B-05C1BB4C107C", "rank": "normal" - }], - "P2347": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2347", - "hash": "41e41b306cdd5e55007ac02da022d9f4ce230b03", - "datavalue": {"value": "7345", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$3CEB44D7-6C0B-4E66-87ED-37D723A1CCC8", - "rank": "normal", - "references": [{ - "hash": "f9bf1a1f034ddd51bd9928ac535e0f57d748e2cf", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "7133f11674741f52cadaae6029068fad9cbb52e3", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 89345680, - "id": "Q89345680" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }] - }], - "P7033": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7033", - "hash": "e31b2e07ae0ce3d3a087d3c818c7bf29c7b04b72", - "datavalue": {"value": "scot/9244", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$F8148EB5-7934-4491-9B40-3378B7D292A6", "rank": "normal" - }], - "P8408": [{ - "mainsnak": { - "snaktype": "value", - "property": "P8408", - "hash": "51c04ed4f03488e8f428256ee41eb20eabe3ff38", - "datavalue": {"value": "Lion", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q140$2855E519-BCD1-4AB3-B3E9-BB53C5CB2E22", - "rank": "normal", - "references": [{ - "hash": "9a681f9dd95c90224547c404e11295f4f7dcf54e", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "9d5780dddffa8746637a9929a936ab6b0f601e24", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 64139102, - "id": "Q64139102" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "622a5a27fa5b25e7e7984974e9db494cf8460990", - "datavalue": { - "value": { - "time": "+2020-07-09T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P813"] - }] - }], - "P8519": [{ - "mainsnak": { - "snaktype": "value", - "property": "P8519", - "hash": "ad8031a668b5310633a04a9223714b3482d388b2", - "datavalue": {"value": "64570", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$ba4fa085-0e54-4226-a46b-770f7d5a995f", "rank": "normal" - }], - "P279": [{ - "mainsnak": { - "snaktype": "value", - "property": "P279", - "hash": "761c3439637add8f8fe3a351d6231333693835f6", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 6667323, "id": "Q6667323"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$cb41b7d3-46f0-e6d9-ced6-c2803e0c06b7", "rank": "normal" - }], - "P2670": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2670", - "hash": "6563f1e596253f1574515891267de01c5c1e688e", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 17611534, "id": "Q17611534"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$24984be4-4813-b6ad-ec83-1a37b7332c8a", "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P2670", - "hash": "684855138cc32d11b487d0178c194f10c63f5f86", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 98520146, "id": "Q98520146"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$56e8c9b3-4892-e95d-f7c5-02b10ffe77e8", "rank": "normal" - }], - "P31": [{ - "mainsnak": { - "snaktype": "value", - "property": "P31", - "hash": "06629d890d7ab0ff85c403d8aadf57ce9809c01f", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 16521, "id": "Q16521"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "q140$8EE98E5B-4A9C-4BF5-B456-FB77E8EE4E69", "rank": "normal" - }], - "P2581": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2581", - "hash": "beca27cf7dd079eb27b7690e13a446d98448ae91", - "datavalue": {"value": "00049156n", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$90562c9a-4e9c-082d-d577-e0869524d9a1", "rank": "normal" - }], - "P7506": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7506", - "hash": "0562f57f9a54c65a2d45711f6dd5dd53ce37f6f8", - "datavalue": {"value": "1107856", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$00d453bf-4786-bde9-63f4-2db9f3610e88", "rank": "normal" - }], - "P5184": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5184", - "hash": "201d8de9b05ae85fe4f917c7e54c4a0218517888", - "datavalue": {"value": "b11s0701a", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$a2b832c9-4c5c-e402-e10d-cf2b08d35a56", "rank": "normal" - }], - "P6900": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6900", - "hash": "f7218c6984cd57078497a62ad595b089bdd97c49", - "datavalue": {"value": "\u30e9\u30a4\u30aa\u30f3", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$d15d143d-4826-4860-9aa3-6a350d6bc36f", "rank": "normal" - }], - "P3553": [{ - "mainsnak": { - "snaktype": "value", - "property": "P3553", - "hash": "c82c6e1156e098d5ef396248c412371b90e0dc56", - "datavalue": {"value": "19563862", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$5edb95fa-4647-2e9a-9dbf-b68e1326eb79", "rank": "normal" - }], - "P5337": [{ - "mainsnak": { - "snaktype": "value", - "property": "P5337", - "hash": "793cfa52df3c5a6747c0cb5db959db944b04dbed", - "datavalue": { - "value": "CAAqIQgKIhtDQkFTRGdvSUwyMHZNRGsyYldJU0FtcGhLQUFQAQ", - "type": "string" - }, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$6137dbc1-4c6a-af60-00ee-2a32c63bfdfa", "rank": "normal" - }], - "P6200": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6200", - "hash": "0ce08dd38017230d41f530f6e97baf484f607235", - "datavalue": {"value": "ce2gz91pyv2t", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$01ed0f16-4a4a-213d-e457-0d4d5d670d49", "rank": "normal" - }], - "P4527": [{ - "mainsnak": { - "snaktype": "value", - "property": "P4527", - "hash": "b07d29aa5112080a9294a7421e46ed0b73ac96c7", - "datavalue": {"value": "430792", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "qualifiers": { - "P1810": [{ - "snaktype": "value", - "property": "P1810", - "hash": "7d78547303d5e9e014a7c8cef6072faee91088ce", - "datavalue": {"value": "Lions", "type": "string"}, - "datatype": "string" - }] - }, - "qualifiers-order": ["P1810"], - "id": "Q140$C37C600C-4929-4203-A06E-8D797BA9B22A", - "rank": "normal" - }], - "P8989": [{ - "mainsnak": { - "snaktype": "value", - "property": "P8989", - "hash": "37087c42c921d83773f62d77e7360dc44504c122", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 104595349, "id": "Q104595349"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$1ece61f5-c008-4750-8b67-e15337f28e86", "rank": "normal" - }], - "P1552": [{ - "mainsnak": { - "snaktype": "value", - "property": "P1552", - "hash": "1aa7db66bfad11e427c40ec79f3295de877967f1", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 120446, "id": "Q120446"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, "type": "statement", "id": "Q140$0bd4b0d9-49ff-b5b4-5c10-9500bc0ce19d", "rank": "normal" - }], - "P9198": [{ - "mainsnak": { - "snaktype": "value", - "property": "P9198", - "hash": "d3ab4ab9d788dc348d16e13fc77164ea71cef2ae", - "datavalue": {"value": "352", "type": "string"}, - "datatype": "external-id" - }, "type": "statement", "id": "Q140$C5D80C89-2862-490F-AA85-C260F32BE30B", "rank": "normal" - }], - "P9566": [{ - "mainsnak": { - "snaktype": "value", - "property": "P9566", - "hash": "053e0b7c15c8e5a61a71077c4cffa73b9d03005b", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 3255068, "id": "Q3255068"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "id": "Q140$C7DAEA4E-B613-48A6-BFCD-88B551D1EF7A", - "rank": "normal", - "references": [{ - "hash": "0eedf63ac49c9b21aa7ff0a5e70b71aa6069a8ed", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "abfcfc68aa085f872d633958be83cba2ab96ce4a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1637051, - "id": "Q1637051" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] - }, - "snaks-order": ["P248"] - }, { - "hash": "6db51e3163554f674ff270c93a2871c8d859a49e", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "abfcfc68aa085f872d633958be83cba2ab96ce4a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1637051, - "id": "Q1637051" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P577": [{ - "snaktype": "value", - "property": "P577", - "hash": "ccd6ea06a2c9c0f54f5b1f45991a659225b5f4ef", - "datavalue": { - "value": { - "time": "+2013-01-01T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 9, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P577"] - }] - }], - "P508": [{ - "mainsnak": { - "snaktype": "value", - "property": "P508", - "hash": "e87c854abf600fb5de7b9b677d94a06e18851333", - "datavalue": {"value": "34922", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "qualifiers": { - "P1810": [{ - "snaktype": "value", - "property": "P1810", - "hash": "137692b9bcc178e7b7d232631cb607d45e2f543d", - "datavalue": {"value": "Leoni", "type": "string"}, - "datatype": "string" - }], - "P4970": [{ - "snaktype": "value", - "property": "P4970", - "hash": "271ec192bf14b9eb639120c60d5961ab8692444d", - "datavalue": {"value": "Panthera leo", "type": "string"}, - "datatype": "string" - }] - }, - "qualifiers-order": ["P1810", "P4970"], - "id": "Q140$52702f98-7843-4e0e-b646-76629e04e555", - "rank": "normal" - }], - "P950": [{ - "mainsnak": { - "snaktype": "value", - "property": "P950", - "hash": "f447323110fd744383394f91c2dfba2fc3187242", - "datavalue": {"value": "XX530613", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "qualifiers": { - "P1810": [{ - "snaktype": "value", - "property": "P1810", - "hash": "e2b2bda5457e0d5f7859e5c54996e1884062dfd1", - "datavalue": {"value": "Leones", "type": "string"}, - "datatype": "string" - }] - }, - "qualifiers-order": ["P1810"], - "id": "Q140$773f47cf-3133-4892-80eb-9d4dc5e97582", - "rank": "normal", - "references": [{ - "hash": "184729506e049d06de85686ede30c92b3e52451d", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "3b090a7bae73c288393b2c8b9846cc7ed9a58f91", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 16583225, - "id": "Q16583225" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }], - "P854": [{ - "snaktype": "value", - "property": "P854", - "hash": "b16c3ffac23bb97abe5d0c4d6ccffe4d010ab71a", - "datavalue": { - "value": "https://thes.bncf.firenze.sbn.it/termine.php?id=34922", - "type": "string" - }, - "datatype": "url" - }], - "P813": [{ - "snaktype": "value", - "property": "P813", - "hash": "7721e97431215c374db84a9df785dc964a16bd17", - "datavalue": { - "value": { - "time": "+2021-06-15T00:00:00Z", - "timezone": 0, - "before": 0, - "after": 0, - "precision": 11, - "calendarmodel": "http://www.wikidata.org/entity/Q1985727" - }, "type": "time" - }, - "datatype": "time" - }] - }, - "snaks-order": ["P248", "P854", "P813"] - }] - }], - "P7603": [{ - "mainsnak": { - "snaktype": "value", - "property": "P7603", - "hash": "c86436e278d690f057cfecc86babf982948015f3", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 2851528, "id": "Q2851528"}, - "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }, - "type": "statement", - "qualifiers": { - "P17": [{ - "snaktype": "value", - "property": "P17", - "hash": "18fb076bdc1c07e578546d1670ba193b768531ac", - "datavalue": { - "value": {"entity-type": "item", "numeric-id": 668, "id": "Q668"}, - "type": "wikibase-entityid" + type: "wikibase-entityid", }, - "datatype": "wikibase-item" - }] - }, - "qualifiers-order": ["P17"], - "id": "Q140$64262d09-4a19-3945-8a09-c2195b7614a7", - "rank": "normal" - }], - "P6800": [{ - "mainsnak": { - "snaktype": "value", - "property": "P6800", - "hash": "1da99908e2ffdf6de901a1b8a2dbab0c62886565", - "datavalue": {"value": "http://www.ensembl.org/Panthera_leo", "type": "string"}, - "datatype": "url" - }, - "type": "statement", - "id": "Q140$87DC0D37-FC9E-4FFE-B92D-1A3A7C019A1D", - "rank": "normal", - "references": [{ - "hash": "53eb51e25c6356d2d4673dc249ea837dd14feca0", - "snaks": { - "P248": [{ - "snaktype": "value", - "property": "P248", - "hash": "4ec639fccc9ddb8e079f7d27ca43220e3c512c20", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 1344256, - "id": "Q1344256" - }, "type": "wikibase-entityid" - }, - "datatype": "wikibase-item" - }] + datatype: "wikibase-item", }, - "snaks-order": ["P248"] - }] - }] + type: "statement", + qualifiers: { + P1810: [ + { + snaktype: "value", + property: "P1810", + hash: "7d78547303d5e9e014a7c8cef6072faee91088ce", + datavalue: { value: "Lions", type: "string" }, + datatype: "string", + }, + ], + P585: [ + { + snaktype: "value", + property: "P585", + hash: "ffb837135313cad3b2545c4b9ce5ee416deda3e2", + datavalue: { + value: { + time: "+2021-05-07T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "qualifiers-order": ["P1810", "P585"], + id: "Q140$A4D208BD-6A69-4561-B402-2E17AAE6E028", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P1343", + hash: "d12a9ecb0df8fce076df898533fea0339e5881bd", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 10886720, + id: "Q10886720", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + qualifiers: { + P805: [ + { + snaktype: "value", + property: "P805", + hash: "52ddab8de77b01303d508a1de615ca13060ec188", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 107513600, + id: "Q107513600", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P805"], + id: "Q140$07daf548-4c8d-fa7c-16f4-4c7062f7e48a", + rank: "normal", + }, + ], + P4733: [ + { + mainsnak: { + snaktype: "value", + property: "P4733", + hash: "fc789f67f6d4d9b5879a8631eefe61f51a60f979", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 3177438, + id: "Q3177438", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$3773ba15-4723-261a-f9a8-544496938efa", + rank: "normal", + references: [ + { + hash: "649ae5511d5389d870d19e83543fa435de796536", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "9931bb1a17358e94590f8fa0b9550de881616d97", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 784031, + id: "Q784031", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P5019: [ + { + mainsnak: { + snaktype: "value", + property: "P5019", + hash: "44aac3d8a2bd240b4bc81741a0980dc48781181b", + datavalue: { value: "l\u00f6we", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$2be40b22-49f1-c9e7-1812-8e3fd69d662d", + rank: "normal", + }, + ], + P2924: [ + { + mainsnak: { + snaktype: "value", + property: "P2924", + hash: "710d75c07e28936461d03b20b2fc7455599301a1", + datavalue: { value: "2135124", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$6326B120-CE04-4F02-94CA-D7BBC2589A39", + rank: "normal", + }, + ], + P5055: [ + { + mainsnak: { + snaktype: "value", + property: "P5055", + hash: "c5264fc372b7e66566d54d73f86c8ab8c43fb033", + datavalue: { value: "10196306", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$F8D43B92-CC3A-4967-A28F-C3E6308946F6", + rank: "normal", + references: [ + { + hash: "7131076724beb97fed351cb7e7f6ac6d61dd05b9", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "1e3ad3cb9e0170e28b7c7c335fba55cafa6ef789", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 51885189, + id: "Q51885189", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "2b1446fcfcd471ab6d36521b4ad2ac183ff8bc0d", + datavalue: { + value: { + time: "+2018-06-07T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P5221: [ + { + mainsnak: { + snaktype: "value", + property: "P5221", + hash: "623ca9614dd0d8b8720bf35b4d57be91dcef5fe6", + datavalue: { value: "123566", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$472fe544-402d-2574-6b2e-98c5b01bb294", + rank: "normal", + }, + ], + P5698: [ + { + mainsnak: { + snaktype: "value", + property: "P5698", + hash: "e966694183143d709403fae7baabb5fdf98d219a", + datavalue: { value: "70719", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$EF3F712D-B0E5-4151-81E4-67804D6241E6", + rank: "normal", + }, + ], + P5397: [ + { + mainsnak: { + snaktype: "value", + property: "P5397", + hash: "49a827bc1853a3b5612b437dd61eb5c28dc0bab0", + datavalue: { value: "12799", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$DE37BF10-A59D-48F1-926A-7303EDEEDDD0", + rank: "normal", + }, + ], + P6033: [ + { + mainsnak: { + snaktype: "value", + property: "P6033", + hash: "766727ded3adbbfec0bed77affc89ea4e5214d65", + datavalue: { value: "panthera-leo", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$A27BADCC-0F72-45A5-814B-BDE62BD7A1B4", + rank: "normal", + }, + ], + P18: [ + { + mainsnak: { + snaktype: "value", + property: "P18", + hash: "d3ceb5bb683335c91781e4d52906d2fb1cc0c35d", + datavalue: { value: "Lion waiting in Namibia.jpg", type: "string" }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P21: [ + { + snaktype: "value", + property: "P21", + hash: "0576a008261e5b2544d1ff3328c94bd529379536", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 44148, + id: "Q44148", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P2096: [ + { + snaktype: "value", + property: "P2096", + hash: "6923fafa02794ae7d0773e565de7dd49a2694b38", + datavalue: { + value: { text: "Lle\u00f3", language: "ca" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + { + snaktype: "value", + property: "P2096", + hash: "563784f05211416fda8662a0773f52165ccf6c2a", + datavalue: { + value: { + text: "Machu de lle\u00f3n en Namibia", + language: "ast", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + { + snaktype: "value", + property: "P2096", + hash: "52722803d98964d77b79d3ed62bd24b4f25e6993", + datavalue: { + value: { text: "\u043b\u044a\u0432", language: "bg" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + ], + }, + "qualifiers-order": ["P21", "P2096"], + id: "q140$5903FDF3-DBBD-4527-A738-450EAEAA45CB", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P18", + hash: "6907d4c168377a18d6a5eb390ab32a7da42d8218", + datavalue: { value: "Okonjima Lioness.jpg", type: "string" }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P21: [ + { + snaktype: "value", + property: "P21", + hash: "a274865baccd3ff04c28d5ffdcc12e0079f5a201", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 43445, + id: "Q43445", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P2096: [ + { + snaktype: "value", + property: "P2096", + hash: "a9d1363e8fc83ba822c45a81de59fe5b8eb434cf", + datavalue: { + value: { + text: "\u043b\u044a\u0432\u0438\u0446\u0430", + language: "bg", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + { + snaktype: "value", + property: "P2096", + hash: "b36ab7371664b7b62ee7be65db4e248074a5330c", + datavalue: { + value: { + text: "Lleona n'Okonjima Lodge, Namibia", + language: "ast", + }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + { + snaktype: "value", + property: "P2096", + hash: "31c78a574eabc0426d7984aa4988752e35b71f0c", + datavalue: { + value: { text: "lwica", language: "pl" }, + type: "monolingualtext", + }, + datatype: "monolingualtext", + }, + ], + }, + "qualifiers-order": ["P21", "P2096"], + id: "Q140$4da15225-f7dc-4942-a685-0669e5d3af14", + rank: "normal", + }, + ], + P6573: [ + { + mainsnak: { + snaktype: "value", + property: "P6573", + hash: "c27b457b12eeecb053d60af6ecf9b0baa133bef5", + datavalue: { value: "L\u00f6we", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$45B1C3EB-E335-4245-A193-8C48B4953E51", + rank: "normal", + }, + ], + P443: [ + { + mainsnak: { + snaktype: "value", + property: "P443", + hash: "8a9afb9293804f976c415060900bf9afbc2cfdff", + datavalue: { + value: "LL-Q188 (deu)-Sebastian Wallroth-L\u00f6we.wav", + type: "string", + }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P407: [ + { + snaktype: "value", + property: "P407", + hash: "46bfd327b830f66f7061ea92d1be430c135fa91f", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 188, + id: "Q188", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P407"], + id: "Q140$5EC64299-429F-45E8-B18F-19325401189C", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P443", + hash: "7d058dfd1e8a41f026974faec3dc0588e29c6854", + datavalue: { value: "LL-Q150 (fra)-Ash Crow-lion.wav", type: "string" }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P407: [ + { + snaktype: "value", + property: "P407", + hash: "d197d0a5efa4b4c23a302a829dd3ef43684fe002", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 150, + id: "Q150", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P407"], + id: "Q140$A4575261-6577-4EF6-A0C9-DA5FA523D1C2", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P443", + hash: "79b9f51c9b4eec305813d5bb697b403d798cf1c5", + datavalue: { + value: "LL-Q33965 (sat)-Joy sagar Murmu-\u1c60\u1c69\u1c5e.wav", + type: "string", + }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P407: [ + { + snaktype: "value", + property: "P407", + hash: "58ae6998321952889f733126c11c582eeef20e72", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 33965, + id: "Q33965", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P407"], + id: "Q140$7eedc8fa-4d1c-7ee9-3c67-0c89ef464d9f", + rank: "normal", + references: [ + { + hash: "d0b5c88b6f49dda9160c706291a9b8645825d99c", + snaks: { + P854: [ + { + snaktype: "value", + property: "P854", + hash: "38c1012cea9eb73cf1bd11eba0c2f745d2463340", + datavalue: { + value: "https://lingualibre.org/wiki/Q403065", + type: "string", + }, + datatype: "url", + }, + ], + }, + "snaks-order": ["P854"], + }, + ], + }, + ], + P1296: [ + { + mainsnak: { + snaktype: "value", + property: "P1296", + hash: "c1f872d4cd22219a7315c0198a83c1918ded97ee", + datavalue: { value: "0120024", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$6C51384B-2EBF-4E6B-9201-A44F0A145C04", + rank: "normal", + }, + ], + P486: [ + { + mainsnak: { + snaktype: "value", + property: "P486", + hash: "b7003b0fb28287301200b6b3871a5437d877913b", + datavalue: { value: "D008045", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$B2F98DD2-B679-43DD-B731-FA33FB1EE4B9", + rank: "normal", + }, + ], + P989: [ + { + mainsnak: { + snaktype: "value", + property: "P989", + hash: "132884b2a696a8b56c8b1460e126f745e2fa6d01", + datavalue: { value: "Ru-Lion (intro).ogg", type: "string" }, + datatype: "commonsMedia", + }, + type: "statement", + qualifiers: { + P407: [ + { + snaktype: "value", + property: "P407", + hash: "d291ddb7cd77c94a7bd709a8395934147e0864fc", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 7737, + id: "Q7737", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P407"], + id: "Q140$857D8831-673B-427E-A182-6A9FFA980424", + rank: "normal", + }, + ], + P51: [ + { + mainsnak: { + snaktype: "value", + property: "P51", + hash: "73b0e8c8458ebc27374fd08d8ef5241f2f28e3e9", + datavalue: { + value: "Lion raring-sound1TamilNadu178.ogg", + type: "string", + }, + datatype: "commonsMedia", + }, + type: "statement", + id: "Q140$1c254aff-48b1-d3c5-930c-b360ce6fe043", + rank: "normal", + }, + ], + P4212: [ + { + mainsnak: { + snaktype: "value", + property: "P4212", + hash: "e006ce3295d617a4818dc758c28f444446538019", + datavalue: { value: "pcrt5TAeZsO7W4", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$AD6CD534-1FD2-4AC7-9CF8-9D2B4C46927C", + rank: "normal", + }, + ], + P2067: [ + { + mainsnak: { + snaktype: "value", + property: "P2067", + hash: "97a863433c30b47a6175abb95941d185397ea14a", + datavalue: { + value: { + amount: "+1.65", + unit: "http://www.wikidata.org/entity/Q11570", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + qualifiers: { + P642: [ + { + snaktype: "value", + property: "P642", + hash: "f5e24bc6ec443d6cb3678e4561bc298090b54f60", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 4128476, + id: "Q4128476", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P642"], + id: "Q140$198da244-7e66-4258-9434-537e9ce0ffab", + rank: "normal", + references: [ + { + hash: "94a79329d5eac70f7ddb005e0d1dc78c53e77797", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45106562, + id: "Q45106562", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P2067", + hash: "ba9933059ce368e3afde1e96d78b1217172c954e", + datavalue: { + value: { + amount: "+188", + unit: "http://www.wikidata.org/entity/Q11570", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + qualifiers: { + P642: [ + { + snaktype: "value", + property: "P642", + hash: "b388540fc86300a506b3a753ec58dec445525ffa", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 78101716, + id: "Q78101716", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P21: [ + { + snaktype: "value", + property: "P21", + hash: "0576a008261e5b2544d1ff3328c94bd529379536", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 44148, + id: "Q44148", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P642", "P21"], + id: "Q140$a3092626-4295-efb8-bbb6-eed913d02fc7", + rank: "normal", + references: [ + { + hash: "94a79329d5eac70f7ddb005e0d1dc78c53e77797", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45106562, + id: "Q45106562", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + { + mainsnak: { + snaktype: "value", + property: "P2067", + hash: "6951281811b2a8a3a78044e2003d6c162d5ba1a3", + datavalue: { + value: { + amount: "+126", + unit: "http://www.wikidata.org/entity/Q11570", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + qualifiers: { + P642: [ + { + snaktype: "value", + property: "P642", + hash: "b388540fc86300a506b3a753ec58dec445525ffa", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 78101716, + id: "Q78101716", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P21: [ + { + snaktype: "value", + property: "P21", + hash: "a274865baccd3ff04c28d5ffdcc12e0079f5a201", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 43445, + id: "Q43445", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P642", "P21"], + id: "Q140$20d80fe2-4796-23d1-42c2-c103546aa874", + rank: "normal", + references: [ + { + hash: "94a79329d5eac70f7ddb005e0d1dc78c53e77797", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45106562, + id: "Q45106562", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], + P7725: [ + { + mainsnak: { + snaktype: "value", + property: "P7725", + hash: "e9338e052dfaa9267c2357bec2e167ca625af667", + datavalue: { + value: { + amount: "+2.5", + unit: "1", + upperBound: "+4.0", + lowerBound: "+1.0", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + id: "Q140$f1f04a23-0d34-484a-9419-78d12958170c", + rank: "normal", + references: [ + { + hash: "94a79329d5eac70f7ddb005e0d1dc78c53e77797", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45106562, + id: "Q45106562", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], + P4214: [ + { + mainsnak: { + snaktype: "value", + property: "P4214", + hash: "5a112dbdaed17b1ee3fe7a63b1f978e5fd41008a", + datavalue: { + value: { + amount: "+27", + unit: "http://www.wikidata.org/entity/Q577", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + id: "Q140$ec1ccab2-f506-4c81-9179-4625bbbbbe27", + rank: "normal", + references: [ + { + hash: "a8ccf5105b0e2623ae145dd8a9b927c9bd957ddf", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "5b45c23ddb076fe9c5accfe4a4bbd1c24c4c87cb", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 83566668, + id: "Q83566668", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], + P7862: [ + { + mainsnak: { + snaktype: "value", + property: "P7862", + hash: "6e74ddb544498b93407179cc9a7f9b8610762ff5", + datavalue: { + value: { + amount: "+8", + unit: "http://www.wikidata.org/entity/Q5151", + }, + type: "quantity", + }, + datatype: "quantity", + }, + type: "statement", + id: "Q140$17b64a1e-4a13-9e2a-f8a2-a9317890aa53", + rank: "normal", + references: [ + { + hash: "94a79329d5eac70f7ddb005e0d1dc78c53e77797", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4a7fef7ea264a7c71765ce60e3d42f4c043c9646", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 45106562, + id: "Q45106562", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], + P7818: [ + { + mainsnak: { + snaktype: "value", + property: "P7818", + hash: "5c7bac858cf66d079e6c13c88f3f001eb446cdce", + datavalue: { value: "Lion", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$C2D3546E-C42A-404A-A288-580F9C705E12", + rank: "normal", + }, + ], + P7829: [ + { + mainsnak: { + snaktype: "value", + property: "P7829", + hash: "17fbb02db65a7e80691f58be750382d61148406e", + datavalue: { value: "Lion", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$74998F51-E783-40CB-A56A-3189647AB3D4", + rank: "normal", + }, + ], + P7827: [ + { + mainsnak: { + snaktype: "value", + property: "P7827", + hash: "f85db9fe2c187554aefc51e5529d75e0c5af4767", + datavalue: { value: "Le\u00f3n", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$5DA64E1B-F1F1-4254-8629-985DFE8672A2", + rank: "normal", + }, + ], + P7822: [ + { + mainsnak: { + snaktype: "value", + property: "P7822", + hash: "3cd23fddc416227c2ba85d91aa03dc80a8e95836", + datavalue: { value: "Leone", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$DF657EF2-67A8-4272-871D-E95B3719A8B6", + rank: "normal", + }, + ], + P6105: [ + { + mainsnak: { + snaktype: "value", + property: "P6105", + hash: "8bbda0afe53fc428d3a0d9528c97d2145ee41dce", + datavalue: { value: "79432", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$2AE92335-1BC6-4B92-BAF3-9AB41608E638", + rank: "normal", + }, + ], + P6864: [ + { + mainsnak: { + snaktype: "value", + property: "P6864", + hash: "6f87ce0800057dbe88f27748b3077938973eb5c8", + datavalue: { value: "85426", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$9089C4B9-59A8-45A6-821B-05C1BB4C107C", + rank: "normal", + }, + ], + P2347: [ + { + mainsnak: { + snaktype: "value", + property: "P2347", + hash: "41e41b306cdd5e55007ac02da022d9f4ce230b03", + datavalue: { value: "7345", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$3CEB44D7-6C0B-4E66-87ED-37D723A1CCC8", + rank: "normal", + references: [ + { + hash: "f9bf1a1f034ddd51bd9928ac535e0f57d748e2cf", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "7133f11674741f52cadaae6029068fad9cbb52e3", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 89345680, + id: "Q89345680", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], + P7033: [ + { + mainsnak: { + snaktype: "value", + property: "P7033", + hash: "e31b2e07ae0ce3d3a087d3c818c7bf29c7b04b72", + datavalue: { value: "scot/9244", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$F8148EB5-7934-4491-9B40-3378B7D292A6", + rank: "normal", + }, + ], + P8408: [ + { + mainsnak: { + snaktype: "value", + property: "P8408", + hash: "51c04ed4f03488e8f428256ee41eb20eabe3ff38", + datavalue: { value: "Lion", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$2855E519-BCD1-4AB3-B3E9-BB53C5CB2E22", + rank: "normal", + references: [ + { + hash: "9a681f9dd95c90224547c404e11295f4f7dcf54e", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "9d5780dddffa8746637a9929a936ab6b0f601e24", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 64139102, + id: "Q64139102", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "622a5a27fa5b25e7e7984974e9db494cf8460990", + datavalue: { + value: { + time: "+2020-07-09T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P813"], + }, + ], + }, + ], + P8519: [ + { + mainsnak: { + snaktype: "value", + property: "P8519", + hash: "ad8031a668b5310633a04a9223714b3482d388b2", + datavalue: { value: "64570", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$ba4fa085-0e54-4226-a46b-770f7d5a995f", + rank: "normal", + }, + ], + P279: [ + { + mainsnak: { + snaktype: "value", + property: "P279", + hash: "761c3439637add8f8fe3a351d6231333693835f6", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 6667323, + id: "Q6667323", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$cb41b7d3-46f0-e6d9-ced6-c2803e0c06b7", + rank: "normal", + }, + ], + P2670: [ + { + mainsnak: { + snaktype: "value", + property: "P2670", + hash: "6563f1e596253f1574515891267de01c5c1e688e", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 17611534, + id: "Q17611534", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$24984be4-4813-b6ad-ec83-1a37b7332c8a", + rank: "normal", + }, + { + mainsnak: { + snaktype: "value", + property: "P2670", + hash: "684855138cc32d11b487d0178c194f10c63f5f86", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 98520146, + id: "Q98520146", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$56e8c9b3-4892-e95d-f7c5-02b10ffe77e8", + rank: "normal", + }, + ], + P31: [ + { + mainsnak: { + snaktype: "value", + property: "P31", + hash: "06629d890d7ab0ff85c403d8aadf57ce9809c01f", + datavalue: { + value: { "entity-type": "item", "numeric-id": 16521, id: "Q16521" }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "q140$8EE98E5B-4A9C-4BF5-B456-FB77E8EE4E69", + rank: "normal", + }, + ], + P2581: [ + { + mainsnak: { + snaktype: "value", + property: "P2581", + hash: "beca27cf7dd079eb27b7690e13a446d98448ae91", + datavalue: { value: "00049156n", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$90562c9a-4e9c-082d-d577-e0869524d9a1", + rank: "normal", + }, + ], + P7506: [ + { + mainsnak: { + snaktype: "value", + property: "P7506", + hash: "0562f57f9a54c65a2d45711f6dd5dd53ce37f6f8", + datavalue: { value: "1107856", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$00d453bf-4786-bde9-63f4-2db9f3610e88", + rank: "normal", + }, + ], + P5184: [ + { + mainsnak: { + snaktype: "value", + property: "P5184", + hash: "201d8de9b05ae85fe4f917c7e54c4a0218517888", + datavalue: { value: "b11s0701a", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$a2b832c9-4c5c-e402-e10d-cf2b08d35a56", + rank: "normal", + }, + ], + P6900: [ + { + mainsnak: { + snaktype: "value", + property: "P6900", + hash: "f7218c6984cd57078497a62ad595b089bdd97c49", + datavalue: { value: "\u30e9\u30a4\u30aa\u30f3", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$d15d143d-4826-4860-9aa3-6a350d6bc36f", + rank: "normal", + }, + ], + P3553: [ + { + mainsnak: { + snaktype: "value", + property: "P3553", + hash: "c82c6e1156e098d5ef396248c412371b90e0dc56", + datavalue: { value: "19563862", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$5edb95fa-4647-2e9a-9dbf-b68e1326eb79", + rank: "normal", + }, + ], + P5337: [ + { + mainsnak: { + snaktype: "value", + property: "P5337", + hash: "793cfa52df3c5a6747c0cb5db959db944b04dbed", + datavalue: { + value: "CAAqIQgKIhtDQkFTRGdvSUwyMHZNRGsyYldJU0FtcGhLQUFQAQ", + type: "string", + }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$6137dbc1-4c6a-af60-00ee-2a32c63bfdfa", + rank: "normal", + }, + ], + P6200: [ + { + mainsnak: { + snaktype: "value", + property: "P6200", + hash: "0ce08dd38017230d41f530f6e97baf484f607235", + datavalue: { value: "ce2gz91pyv2t", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$01ed0f16-4a4a-213d-e457-0d4d5d670d49", + rank: "normal", + }, + ], + P4527: [ + { + mainsnak: { + snaktype: "value", + property: "P4527", + hash: "b07d29aa5112080a9294a7421e46ed0b73ac96c7", + datavalue: { value: "430792", type: "string" }, + datatype: "external-id", + }, + type: "statement", + qualifiers: { + P1810: [ + { + snaktype: "value", + property: "P1810", + hash: "7d78547303d5e9e014a7c8cef6072faee91088ce", + datavalue: { value: "Lions", type: "string" }, + datatype: "string", + }, + ], + }, + "qualifiers-order": ["P1810"], + id: "Q140$C37C600C-4929-4203-A06E-8D797BA9B22A", + rank: "normal", + }, + ], + P8989: [ + { + mainsnak: { + snaktype: "value", + property: "P8989", + hash: "37087c42c921d83773f62d77e7360dc44504c122", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 104595349, + id: "Q104595349", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$1ece61f5-c008-4750-8b67-e15337f28e86", + rank: "normal", + }, + ], + P1552: [ + { + mainsnak: { + snaktype: "value", + property: "P1552", + hash: "1aa7db66bfad11e427c40ec79f3295de877967f1", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 120446, + id: "Q120446", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$0bd4b0d9-49ff-b5b4-5c10-9500bc0ce19d", + rank: "normal", + }, + ], + P9198: [ + { + mainsnak: { + snaktype: "value", + property: "P9198", + hash: "d3ab4ab9d788dc348d16e13fc77164ea71cef2ae", + datavalue: { value: "352", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q140$C5D80C89-2862-490F-AA85-C260F32BE30B", + rank: "normal", + }, + ], + P9566: [ + { + mainsnak: { + snaktype: "value", + property: "P9566", + hash: "053e0b7c15c8e5a61a71077c4cffa73b9d03005b", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 3255068, + id: "Q3255068", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q140$C7DAEA4E-B613-48A6-BFCD-88B551D1EF7A", + rank: "normal", + references: [ + { + hash: "0eedf63ac49c9b21aa7ff0a5e70b71aa6069a8ed", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "abfcfc68aa085f872d633958be83cba2ab96ce4a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1637051, + id: "Q1637051", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + { + hash: "6db51e3163554f674ff270c93a2871c8d859a49e", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "abfcfc68aa085f872d633958be83cba2ab96ce4a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1637051, + id: "Q1637051", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P577: [ + { + snaktype: "value", + property: "P577", + hash: "ccd6ea06a2c9c0f54f5b1f45991a659225b5f4ef", + datavalue: { + value: { + time: "+2013-01-01T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 9, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P577"], + }, + ], + }, + ], + P508: [ + { + mainsnak: { + snaktype: "value", + property: "P508", + hash: "e87c854abf600fb5de7b9b677d94a06e18851333", + datavalue: { value: "34922", type: "string" }, + datatype: "external-id", + }, + type: "statement", + qualifiers: { + P1810: [ + { + snaktype: "value", + property: "P1810", + hash: "137692b9bcc178e7b7d232631cb607d45e2f543d", + datavalue: { value: "Leoni", type: "string" }, + datatype: "string", + }, + ], + P4970: [ + { + snaktype: "value", + property: "P4970", + hash: "271ec192bf14b9eb639120c60d5961ab8692444d", + datavalue: { value: "Panthera leo", type: "string" }, + datatype: "string", + }, + ], + }, + "qualifiers-order": ["P1810", "P4970"], + id: "Q140$52702f98-7843-4e0e-b646-76629e04e555", + rank: "normal", + }, + ], + P950: [ + { + mainsnak: { + snaktype: "value", + property: "P950", + hash: "f447323110fd744383394f91c2dfba2fc3187242", + datavalue: { value: "XX530613", type: "string" }, + datatype: "external-id", + }, + type: "statement", + qualifiers: { + P1810: [ + { + snaktype: "value", + property: "P1810", + hash: "e2b2bda5457e0d5f7859e5c54996e1884062dfd1", + datavalue: { value: "Leones", type: "string" }, + datatype: "string", + }, + ], + }, + "qualifiers-order": ["P1810"], + id: "Q140$773f47cf-3133-4892-80eb-9d4dc5e97582", + rank: "normal", + references: [ + { + hash: "184729506e049d06de85686ede30c92b3e52451d", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "3b090a7bae73c288393b2c8b9846cc7ed9a58f91", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 16583225, + id: "Q16583225", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + P854: [ + { + snaktype: "value", + property: "P854", + hash: "b16c3ffac23bb97abe5d0c4d6ccffe4d010ab71a", + datavalue: { + value: "https://thes.bncf.firenze.sbn.it/termine.php?id=34922", + type: "string", + }, + datatype: "url", + }, + ], + P813: [ + { + snaktype: "value", + property: "P813", + hash: "7721e97431215c374db84a9df785dc964a16bd17", + datavalue: { + value: { + time: "+2021-06-15T00:00:00Z", + timezone: 0, + before: 0, + after: 0, + precision: 11, + calendarmodel: + "http://www.wikidata.org/entity/Q1985727", + }, + type: "time", + }, + datatype: "time", + }, + ], + }, + "snaks-order": ["P248", "P854", "P813"], + }, + ], + }, + ], + P7603: [ + { + mainsnak: { + snaktype: "value", + property: "P7603", + hash: "c86436e278d690f057cfecc86babf982948015f3", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 2851528, + id: "Q2851528", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + qualifiers: { + P17: [ + { + snaktype: "value", + property: "P17", + hash: "18fb076bdc1c07e578546d1670ba193b768531ac", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 668, + id: "Q668", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "qualifiers-order": ["P17"], + id: "Q140$64262d09-4a19-3945-8a09-c2195b7614a7", + rank: "normal", + }, + ], + P6800: [ + { + mainsnak: { + snaktype: "value", + property: "P6800", + hash: "1da99908e2ffdf6de901a1b8a2dbab0c62886565", + datavalue: { + value: "http://www.ensembl.org/Panthera_leo", + type: "string", + }, + datatype: "url", + }, + type: "statement", + id: "Q140$87DC0D37-FC9E-4FFE-B92D-1A3A7C019A1D", + rank: "normal", + references: [ + { + hash: "53eb51e25c6356d2d4673dc249ea837dd14feca0", + snaks: { + P248: [ + { + snaktype: "value", + property: "P248", + hash: "4ec639fccc9ddb8e079f7d27ca43220e3c512c20", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 1344256, + id: "Q1344256", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P248"], + }, + ], + }, + ], }, - "sitelinks": { - "abwiki": { - "site": "abwiki", - "title": "\u0410\u043b\u044b\u043c", - "badges": [], - "url": "https://ab.wikipedia.org/wiki/%D0%90%D0%BB%D1%8B%D0%BC" - }, - "adywiki": { - "site": "adywiki", - "title": "\u0410\u0441\u043b\u044a\u0430\u043d", - "badges": [], - "url": "https://ady.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D1%8A%D0%B0%D0%BD" - }, - "afwiki": { - "site": "afwiki", - "title": "Leeu", - "badges": ["Q17437796"], - "url": "https://af.wikipedia.org/wiki/Leeu" - }, - "alswiki": { - "site": "alswiki", - "title": "L\u00f6we", - "badges": [], - "url": "https://als.wikipedia.org/wiki/L%C3%B6we" - }, - "altwiki": { - "site": "altwiki", - "title": "\u0410\u0440\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://alt.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D0%B0%D0%BD" - }, - "amwiki": { - "site": "amwiki", - "title": "\u12a0\u1295\u1260\u1233", - "badges": [], - "url": "https://am.wikipedia.org/wiki/%E1%8A%A0%E1%8A%95%E1%89%A0%E1%88%B3" - }, - "angwiki": { - "site": "angwiki", - "title": "L\u0113o", - "badges": [], - "url": "https://ang.wikipedia.org/wiki/L%C4%93o" - }, - "anwiki": { - "site": "anwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://an.wikipedia.org/wiki/Panthera_leo" - }, - "arcwiki": { - "site": "arcwiki", - "title": "\u0710\u072a\u071d\u0710", - "badges": [], - "url": "https://arc.wikipedia.org/wiki/%DC%90%DC%AA%DC%9D%DC%90" - }, - "arwiki": { - "site": "arwiki", - "title": "\u0623\u0633\u062f", - "badges": ["Q17437796"], - "url": "https://ar.wikipedia.org/wiki/%D8%A3%D8%B3%D8%AF" - }, - "arywiki": { - "site": "arywiki", - "title": "\u0633\u0628\u0639", - "badges": [], - "url": "https://ary.wikipedia.org/wiki/%D8%B3%D8%A8%D8%B9" - }, - "arzwiki": { - "site": "arzwiki", - "title": "\u0633\u0628\u0639", - "badges": [], - "url": "https://arz.wikipedia.org/wiki/%D8%B3%D8%A8%D8%B9" - }, - "astwiki": { - "site": "astwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://ast.wikipedia.org/wiki/Panthera_leo" - }, - "aswiki": { - "site": "aswiki", - "title": "\u09b8\u09bf\u0982\u09b9", - "badges": [], - "url": "https://as.wikipedia.org/wiki/%E0%A6%B8%E0%A6%BF%E0%A6%82%E0%A6%B9" - }, - "avkwiki": { - "site": "avkwiki", - "title": "Krapol (Panthera leo)", - "badges": [], - "url": "https://avk.wikipedia.org/wiki/Krapol_(Panthera_leo)" - }, - "avwiki": { - "site": "avwiki", - "title": "\u0413\u044a\u0430\u043b\u0431\u0430\u0446\u04c0", - "badges": [], - "url": "https://av.wikipedia.org/wiki/%D0%93%D1%8A%D0%B0%D0%BB%D0%B1%D0%B0%D1%86%D3%80" - }, - "azbwiki": { - "site": "azbwiki", - "title": "\u0622\u0633\u0644\u0627\u0646", - "badges": [], - "url": "https://azb.wikipedia.org/wiki/%D8%A2%D8%B3%D9%84%D8%A7%D9%86" - }, - "azwiki": { - "site": "azwiki", - "title": "\u015eir", - "badges": [], - "url": "https://az.wikipedia.org/wiki/%C5%9Eir" - }, - "bat_smgwiki": { - "site": "bat_smgwiki", - "title": "Li\u016bts", - "badges": [], - "url": "https://bat-smg.wikipedia.org/wiki/Li%C5%ABts" - }, - "bawiki": { - "site": "bawiki", - "title": "\u0410\u0440\u044b\u04ab\u043b\u0430\u043d", - "badges": [], - "url": "https://ba.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D2%AB%D0%BB%D0%B0%D0%BD" - }, - "bclwiki": { - "site": "bclwiki", - "title": "Leon", - "badges": [], - "url": "https://bcl.wikipedia.org/wiki/Leon" - }, - "be_x_oldwiki": { - "site": "be_x_oldwiki", - "title": "\u041b\u0435\u045e", - "badges": [], - "url": "https://be-tarask.wikipedia.org/wiki/%D0%9B%D0%B5%D1%9E" - }, - "bewiki": { - "site": "bewiki", - "title": "\u041b\u0435\u045e", - "badges": [], - "url": "https://be.wikipedia.org/wiki/%D0%9B%D0%B5%D1%9E" - }, - "bgwiki": { - "site": "bgwiki", - "title": "\u041b\u044a\u0432", - "badges": [], - "url": "https://bg.wikipedia.org/wiki/%D0%9B%D1%8A%D0%B2" - }, - "bhwiki": { - "site": "bhwiki", - "title": "\u0938\u093f\u0902\u0939", - "badges": [], - "url": "https://bh.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9" - }, - "bmwiki": { - "site": "bmwiki", - "title": "Waraba", - "badges": [], - "url": "https://bm.wikipedia.org/wiki/Waraba" - }, - "bnwiki": { - "site": "bnwiki", - "title": "\u09b8\u09bf\u0982\u09b9", - "badges": [], - "url": "https://bn.wikipedia.org/wiki/%E0%A6%B8%E0%A6%BF%E0%A6%82%E0%A6%B9" - }, - "bowiki": { - "site": "bowiki", - "title": "\u0f66\u0f7a\u0f44\u0f0b\u0f42\u0f7a\u0f0d", - "badges": [], - "url": "https://bo.wikipedia.org/wiki/%E0%BD%A6%E0%BD%BA%E0%BD%84%E0%BC%8B%E0%BD%82%E0%BD%BA%E0%BC%8D" - }, - "bpywiki": { - "site": "bpywiki", - "title": "\u09a8\u0982\u09b8\u09be", - "badges": [], - "url": "https://bpy.wikipedia.org/wiki/%E0%A6%A8%E0%A6%82%E0%A6%B8%E0%A6%BE" - }, - "brwiki": { - "site": "brwiki", - "title": "Leon (loen)", - "badges": [], - "url": "https://br.wikipedia.org/wiki/Leon_(loen)" - }, - "bswiki": { - "site": "bswiki", - "title": "Lav", - "badges": [], - "url": "https://bs.wikipedia.org/wiki/Lav" - }, - "bswikiquote": { - "site": "bswikiquote", - "title": "Lav", - "badges": [], - "url": "https://bs.wikiquote.org/wiki/Lav" - }, - "bxrwiki": { - "site": "bxrwiki", - "title": "\u0410\u0440\u0441\u0430\u043b\u0430\u043d", - "badges": [], - "url": "https://bxr.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%B0%D0%BB%D0%B0%D0%BD" - }, - "cawiki": { - "site": "cawiki", - "title": "Lle\u00f3", - "badges": ["Q17437796"], - "url": "https://ca.wikipedia.org/wiki/Lle%C3%B3" - }, - "cawikiquote": { - "site": "cawikiquote", - "title": "Lle\u00f3", - "badges": [], - "url": "https://ca.wikiquote.org/wiki/Lle%C3%B3" - }, - "cdowiki": { - "site": "cdowiki", - "title": "S\u0103i (m\u00e0-ku\u014f d\u00f4ng-\u016dk)", - "badges": [], - "url": "https://cdo.wikipedia.org/wiki/S%C4%83i_(m%C3%A0-ku%C5%8F_d%C3%B4ng-%C5%ADk)" - }, - "cebwiki": { - "site": "cebwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://ceb.wikipedia.org/wiki/Panthera_leo" - }, - "cewiki": { - "site": "cewiki", - "title": "\u041b\u043e\u043c", - "badges": [], - "url": "https://ce.wikipedia.org/wiki/%D0%9B%D0%BE%D0%BC" - }, - "chrwiki": { - "site": "chrwiki", - "title": "\u13e2\u13d3\u13e5 \u13a4\u13c3\u13d5\u13be", - "badges": [], - "url": "https://chr.wikipedia.org/wiki/%E1%8F%A2%E1%8F%93%E1%8F%A5_%E1%8E%A4%E1%8F%83%E1%8F%95%E1%8E%BE" - }, - "chywiki": { - "site": "chywiki", - "title": "P\u00e9hpe'\u00e9nan\u00f3se'hame", - "badges": [], - "url": "https://chy.wikipedia.org/wiki/P%C3%A9hpe%27%C3%A9nan%C3%B3se%27hame" - }, - "ckbwiki": { - "site": "ckbwiki", - "title": "\u0634\u06ce\u0631", - "badges": [], - "url": "https://ckb.wikipedia.org/wiki/%D8%B4%DB%8E%D8%B1" - }, - "commonswiki": { - "site": "commonswiki", - "title": "Panthera leo", - "badges": [], - "url": "https://commons.wikimedia.org/wiki/Panthera_leo" - }, - "cowiki": { - "site": "cowiki", - "title": "Lionu", - "badges": [], - "url": "https://co.wikipedia.org/wiki/Lionu" - }, - "csbwiki": { - "site": "csbwiki", - "title": "Lew", - "badges": [], - "url": "https://csb.wikipedia.org/wiki/Lew" - }, - "cswiki": { - "site": "cswiki", - "title": "Lev", - "badges": [], - "url": "https://cs.wikipedia.org/wiki/Lev" - }, - "cswikiquote": { - "site": "cswikiquote", - "title": "Lev", - "badges": [], - "url": "https://cs.wikiquote.org/wiki/Lev" - }, - "cuwiki": { - "site": "cuwiki", - "title": "\u041b\u044c\u0432\u044a", - "badges": [], - "url": "https://cu.wikipedia.org/wiki/%D0%9B%D1%8C%D0%B2%D1%8A" - }, - "cvwiki": { - "site": "cvwiki", - "title": "\u0410\u0440\u0103\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://cv.wikipedia.org/wiki/%D0%90%D1%80%C4%83%D1%81%D0%BB%D0%B0%D0%BD" - }, - "cywiki": { - "site": "cywiki", - "title": "Llew", - "badges": [], - "url": "https://cy.wikipedia.org/wiki/Llew" - }, - "dagwiki": { - "site": "dagwiki", - "title": "Gbu\u0263inli", - "badges": [], - "url": "https://dag.wikipedia.org/wiki/Gbu%C9%A3inli" - }, - "dawiki": { - "site": "dawiki", - "title": "L\u00f8ve", - "badges": ["Q17559452"], - "url": "https://da.wikipedia.org/wiki/L%C3%B8ve" - }, - "dewiki": { - "site": "dewiki", - "title": "L\u00f6we", - "badges": ["Q17437796"], - "url": "https://de.wikipedia.org/wiki/L%C3%B6we" - }, - "dewikiquote": { - "site": "dewikiquote", - "title": "L\u00f6we", - "badges": [], - "url": "https://de.wikiquote.org/wiki/L%C3%B6we" - }, - "dinwiki": { - "site": "dinwiki", - "title": "K\u00f6r", - "badges": [], - "url": "https://din.wikipedia.org/wiki/K%C3%B6r" - }, - "diqwiki": { - "site": "diqwiki", - "title": "\u015e\u00ear", - "badges": [], - "url": "https://diq.wikipedia.org/wiki/%C5%9E%C3%AAr" - }, - "dsbwiki": { - "site": "dsbwiki", - "title": "Law", - "badges": [], - "url": "https://dsb.wikipedia.org/wiki/Law" - }, - "eewiki": { - "site": "eewiki", - "title": "Dzata", - "badges": [], - "url": "https://ee.wikipedia.org/wiki/Dzata" - }, - "elwiki": { - "site": "elwiki", - "title": "\u039b\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9", - "badges": [], - "url": "https://el.wikipedia.org/wiki/%CE%9B%CE%B9%CE%BF%CE%BD%CF%84%CE%AC%CF%81%CE%B9" - }, - "enwiki": { - "site": "enwiki", - "title": "Lion", - "badges": ["Q17437796"], - "url": "https://en.wikipedia.org/wiki/Lion" - }, - "enwikiquote": { - "site": "enwikiquote", - "title": "Lions", - "badges": [], - "url": "https://en.wikiquote.org/wiki/Lions" - }, - "eowiki": { - "site": "eowiki", - "title": "Leono", - "badges": [], - "url": "https://eo.wikipedia.org/wiki/Leono" - }, - "eowikiquote": { - "site": "eowikiquote", - "title": "Leono", - "badges": [], - "url": "https://eo.wikiquote.org/wiki/Leono" - }, - "eswiki": { - "site": "eswiki", - "title": "Panthera leo", - "badges": ["Q17437796"], - "url": "https://es.wikipedia.org/wiki/Panthera_leo" - }, - "eswikiquote": { - "site": "eswikiquote", - "title": "Le\u00f3n", - "badges": [], - "url": "https://es.wikiquote.org/wiki/Le%C3%B3n" - }, - "etwiki": { - "site": "etwiki", - "title": "L\u00f5vi", - "badges": [], - "url": "https://et.wikipedia.org/wiki/L%C3%B5vi" - }, - "etwikiquote": { - "site": "etwikiquote", - "title": "L\u00f5vi", - "badges": [], - "url": "https://et.wikiquote.org/wiki/L%C3%B5vi" - }, - "euwiki": { - "site": "euwiki", - "title": "Lehoi", - "badges": [], - "url": "https://eu.wikipedia.org/wiki/Lehoi" - }, - "extwiki": { - "site": "extwiki", - "title": "Li\u00f3n (animal)", - "badges": [], - "url": "https://ext.wikipedia.org/wiki/Li%C3%B3n_(animal)" - }, - "fawiki": { - "site": "fawiki", - "title": "\u0634\u06cc\u0631 (\u06af\u0631\u0628\u0647\u200c\u0633\u0627\u0646)", - "badges": ["Q17437796"], - "url": "https://fa.wikipedia.org/wiki/%D8%B4%DB%8C%D8%B1_(%DA%AF%D8%B1%D8%A8%D9%87%E2%80%8C%D8%B3%D8%A7%D9%86)" - }, - "fawikiquote": { - "site": "fawikiquote", - "title": "\u0634\u06cc\u0631", - "badges": [], - "url": "https://fa.wikiquote.org/wiki/%D8%B4%DB%8C%D8%B1" - }, - "fiu_vrowiki": { - "site": "fiu_vrowiki", - "title": "L\u00f5vi", - "badges": [], - "url": "https://fiu-vro.wikipedia.org/wiki/L%C3%B5vi" - }, - "fiwiki": { - "site": "fiwiki", - "title": "Leijona", - "badges": ["Q17437796"], - "url": "https://fi.wikipedia.org/wiki/Leijona" - }, - "fowiki": { - "site": "fowiki", - "title": "Leyva", - "badges": [], - "url": "https://fo.wikipedia.org/wiki/Leyva" - }, - "frrwiki": { - "site": "frrwiki", - "title": "L\u00f6\u00f6w", - "badges": [], - "url": "https://frr.wikipedia.org/wiki/L%C3%B6%C3%B6w" - }, - "frwiki": { - "site": "frwiki", - "title": "Lion", - "badges": ["Q17437796"], - "url": "https://fr.wikipedia.org/wiki/Lion" - }, - "frwikiquote": { - "site": "frwikiquote", - "title": "Lion", - "badges": [], - "url": "https://fr.wikiquote.org/wiki/Lion" - }, - "gagwiki": { - "site": "gagwiki", - "title": "Aslan", - "badges": [], - "url": "https://gag.wikipedia.org/wiki/Aslan" - }, - "gawiki": { - "site": "gawiki", - "title": "Leon", - "badges": [], - "url": "https://ga.wikipedia.org/wiki/Leon" - }, - "gdwiki": { - "site": "gdwiki", - "title": "Le\u00f2mhann", - "badges": [], - "url": "https://gd.wikipedia.org/wiki/Le%C3%B2mhann" - }, - "glwiki": { - "site": "glwiki", - "title": "Le\u00f3n", - "badges": [], - "url": "https://gl.wikipedia.org/wiki/Le%C3%B3n" - }, - "gnwiki": { - "site": "gnwiki", - "title": "Le\u00f5", - "badges": [], - "url": "https://gn.wikipedia.org/wiki/Le%C3%B5" - }, - "gotwiki": { - "site": "gotwiki", - "title": "\ud800\udf3b\ud800\udf39\ud800\udf45\ud800\udf30", - "badges": [], - "url": "https://got.wikipedia.org/wiki/%F0%90%8C%BB%F0%90%8C%B9%F0%90%8D%85%F0%90%8C%B0" - }, - "guwiki": { - "site": "guwiki", - "title": "\u0a8f\u0ab6\u0abf\u0aaf\u0abe\u0a87 \u0ab8\u0abf\u0a82\u0ab9", - "badges": [], - "url": "https://gu.wikipedia.org/wiki/%E0%AA%8F%E0%AA%B6%E0%AA%BF%E0%AA%AF%E0%AA%BE%E0%AA%87_%E0%AA%B8%E0%AA%BF%E0%AA%82%E0%AA%B9" - }, - "hakwiki": { - "site": "hakwiki", - "title": "S\u1e73\u0302-\u00e9", - "badges": [], - "url": "https://hak.wikipedia.org/wiki/S%E1%B9%B3%CC%82-%C3%A9" - }, - "hawiki": { - "site": "hawiki", - "title": "Zaki", - "badges": [], - "url": "https://ha.wikipedia.org/wiki/Zaki" - }, - "hawwiki": { - "site": "hawwiki", - "title": "Liona", - "badges": [], - "url": "https://haw.wikipedia.org/wiki/Liona" - }, - "hewiki": { - "site": "hewiki", - "title": "\u05d0\u05e8\u05d9\u05d4", - "badges": [], - "url": "https://he.wikipedia.org/wiki/%D7%90%D7%A8%D7%99%D7%94" - }, - "hewikiquote": { - "site": "hewikiquote", - "title": "\u05d0\u05e8\u05d9\u05d4", - "badges": [], - "url": "https://he.wikiquote.org/wiki/%D7%90%D7%A8%D7%99%D7%94" - }, - "hifwiki": { - "site": "hifwiki", - "title": "Ser", - "badges": [], - "url": "https://hif.wikipedia.org/wiki/Ser" - }, - "hiwiki": { - "site": "hiwiki", - "title": "\u0938\u093f\u0902\u0939 (\u092a\u0936\u0941)", - "badges": [], - "url": "https://hi.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9_(%E0%A4%AA%E0%A4%B6%E0%A5%81)" - }, - "hrwiki": { - "site": "hrwiki", - "title": "Lav", - "badges": [], - "url": "https://hr.wikipedia.org/wiki/Lav" - }, - "hrwikiquote": { - "site": "hrwikiquote", - "title": "Lav", - "badges": [], - "url": "https://hr.wikiquote.org/wiki/Lav" - }, - "hsbwiki": { - "site": "hsbwiki", - "title": "Law", - "badges": [], - "url": "https://hsb.wikipedia.org/wiki/Law" - }, - "htwiki": { - "site": "htwiki", - "title": "Lyon", - "badges": [], - "url": "https://ht.wikipedia.org/wiki/Lyon" - }, - "huwiki": { - "site": "huwiki", - "title": "Oroszl\u00e1n", - "badges": [], - "url": "https://hu.wikipedia.org/wiki/Oroszl%C3%A1n" - }, - "hywiki": { - "site": "hywiki", - "title": "\u0531\u057c\u0575\u0578\u0582\u056e", - "badges": [], - "url": "https://hy.wikipedia.org/wiki/%D4%B1%D5%BC%D5%B5%D5%B8%D6%82%D5%AE" - }, - "hywikiquote": { - "site": "hywikiquote", - "title": "\u0531\u057c\u0575\u0578\u0582\u056e", - "badges": [], - "url": "https://hy.wikiquote.org/wiki/%D4%B1%D5%BC%D5%B5%D5%B8%D6%82%D5%AE" - }, - "hywwiki": { - "site": "hywwiki", - "title": "\u0531\u057c\u056b\u0582\u056e", - "badges": [], - "url": "https://hyw.wikipedia.org/wiki/%D4%B1%D5%BC%D5%AB%D6%82%D5%AE" - }, - "iawiki": { - "site": "iawiki", - "title": "Leon", - "badges": [], - "url": "https://ia.wikipedia.org/wiki/Leon" - }, - "idwiki": { - "site": "idwiki", - "title": "Singa", - "badges": [], - "url": "https://id.wikipedia.org/wiki/Singa" - }, - "igwiki": { - "site": "igwiki", - "title": "Od\u00fam", - "badges": [], - "url": "https://ig.wikipedia.org/wiki/Od%C3%BAm" - }, - "ilowiki": { - "site": "ilowiki", - "title": "Leon", - "badges": [], - "url": "https://ilo.wikipedia.org/wiki/Leon" - }, - "inhwiki": { - "site": "inhwiki", - "title": "\u041b\u043e\u043c", - "badges": [], - "url": "https://inh.wikipedia.org/wiki/%D0%9B%D0%BE%D0%BC" - }, - "iowiki": { - "site": "iowiki", - "title": "Leono (mamifero)", - "badges": [], - "url": "https://io.wikipedia.org/wiki/Leono_(mamifero)" - }, - "iswiki": { - "site": "iswiki", - "title": "Lj\u00f3n", - "badges": [], - "url": "https://is.wikipedia.org/wiki/Lj%C3%B3n" - }, - "itwiki": { - "site": "itwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://it.wikipedia.org/wiki/Panthera_leo" - }, - "itwikiquote": { - "site": "itwikiquote", - "title": "Leone", - "badges": [], - "url": "https://it.wikiquote.org/wiki/Leone" - }, - "jawiki": { - "site": "jawiki", - "title": "\u30e9\u30a4\u30aa\u30f3", - "badges": [], - "url": "https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%82%AA%E3%83%B3" - }, - "jawikiquote": { - "site": "jawikiquote", - "title": "\u7345\u5b50", - "badges": [], - "url": "https://ja.wikiquote.org/wiki/%E7%8D%85%E5%AD%90" - }, - "jbowiki": { - "site": "jbowiki", - "title": "cinfo", - "badges": [], - "url": "https://jbo.wikipedia.org/wiki/cinfo" - }, - "jvwiki": { - "site": "jvwiki", - "title": "Singa", - "badges": [], - "url": "https://jv.wikipedia.org/wiki/Singa" - }, - "kabwiki": { - "site": "kabwiki", - "title": "Izem", - "badges": [], - "url": "https://kab.wikipedia.org/wiki/Izem" - }, - "kawiki": { - "site": "kawiki", - "title": "\u10da\u10dd\u10db\u10d8", - "badges": [], - "url": "https://ka.wikipedia.org/wiki/%E1%83%9A%E1%83%9D%E1%83%9B%E1%83%98" - }, - "kbdwiki": { - "site": "kbdwiki", - "title": "\u0425\u044c\u044d\u0449", - "badges": [], - "url": "https://kbd.wikipedia.org/wiki/%D0%A5%D1%8C%D1%8D%D1%89" - }, - "kbpwiki": { - "site": "kbpwiki", - "title": "T\u0254\u0254y\u028b\u028b", - "badges": [], - "url": "https://kbp.wikipedia.org/wiki/T%C9%94%C9%94y%CA%8B%CA%8B" - }, - "kgwiki": { - "site": "kgwiki", - "title": "Nkosi", - "badges": [], - "url": "https://kg.wikipedia.org/wiki/Nkosi" - }, - "kkwiki": { - "site": "kkwiki", - "title": "\u0410\u0440\u044b\u0441\u0442\u0430\u043d", - "badges": [], - "url": "https://kk.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D1%82%D0%B0%D0%BD" - }, - "knwiki": { - "site": "knwiki", - "title": "\u0cb8\u0cbf\u0c82\u0cb9", - "badges": [], - "url": "https://kn.wikipedia.org/wiki/%E0%B2%B8%E0%B2%BF%E0%B2%82%E0%B2%B9" - }, - "kowiki": { - "site": "kowiki", - "title": "\uc0ac\uc790", - "badges": [], - "url": "https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9E%90" - }, - "kowikiquote": { - "site": "kowikiquote", - "title": "\uc0ac\uc790", - "badges": [], - "url": "https://ko.wikiquote.org/wiki/%EC%82%AC%EC%9E%90" - }, - "kswiki": { - "site": "kswiki", - "title": "\u067e\u0627\u062f\u064e\u0631 \u0633\u0655\u06c1\u06c1", - "badges": [], - "url": "https://ks.wikipedia.org/wiki/%D9%BE%D8%A7%D8%AF%D9%8E%D8%B1_%D8%B3%D9%95%DB%81%DB%81" - }, - "kuwiki": { - "site": "kuwiki", - "title": "\u015e\u00ear", - "badges": [], - "url": "https://ku.wikipedia.org/wiki/%C5%9E%C3%AAr" - }, - "kwwiki": { - "site": "kwwiki", - "title": "Lew", - "badges": [], - "url": "https://kw.wikipedia.org/wiki/Lew" - }, - "kywiki": { - "site": "kywiki", - "title": "\u0410\u0440\u0441\u0442\u0430\u043d", - "badges": [], - "url": "https://ky.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D1%82%D0%B0%D0%BD" - }, - "lawiki": { - "site": "lawiki", - "title": "Leo", - "badges": [], - "url": "https://la.wikipedia.org/wiki/Leo" - }, - "lawikiquote": { - "site": "lawikiquote", - "title": "Leo", - "badges": [], - "url": "https://la.wikiquote.org/wiki/Leo" - }, - "lbewiki": { - "site": "lbewiki", - "title": "\u0410\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://lbe.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D0%B0%D0%BD" - }, - "lbwiki": { - "site": "lbwiki", - "title": "L\u00e9iw", - "badges": [], - "url": "https://lb.wikipedia.org/wiki/L%C3%A9iw" - }, - "lezwiki": { - "site": "lezwiki", - "title": "\u0410\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://lez.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D0%B0%D0%BD" - }, - "lfnwiki": { - "site": "lfnwiki", - "title": "Leon", - "badges": [], - "url": "https://lfn.wikipedia.org/wiki/Leon" - }, - "lijwiki": { - "site": "lijwiki", - "title": "Lion (bestia)", - "badges": [], - "url": "https://lij.wikipedia.org/wiki/Lion_(bestia)" - }, - "liwiki": { - "site": "liwiki", - "title": "Liew", - "badges": ["Q17437796"], - "url": "https://li.wikipedia.org/wiki/Liew" - }, - "lldwiki": { - "site": "lldwiki", - "title": "Lion", - "badges": [], - "url": "https://lld.wikipedia.org/wiki/Lion" - }, - "lmowiki": { - "site": "lmowiki", - "title": "Panthera leo", - "badges": [], - "url": "https://lmo.wikipedia.org/wiki/Panthera_leo" - }, - "lnwiki": { - "site": "lnwiki", - "title": "Nk\u0254\u0301si", - "badges": [], - "url": "https://ln.wikipedia.org/wiki/Nk%C9%94%CC%81si" - }, - "ltgwiki": { - "site": "ltgwiki", - "title": "\u013bovs", - "badges": [], - "url": "https://ltg.wikipedia.org/wiki/%C4%BBovs" - }, - "ltwiki": { - "site": "ltwiki", - "title": "Li\u016btas", - "badges": [], - "url": "https://lt.wikipedia.org/wiki/Li%C5%ABtas" - }, - "ltwikiquote": { - "site": "ltwikiquote", - "title": "Li\u016btas", - "badges": [], - "url": "https://lt.wikiquote.org/wiki/Li%C5%ABtas" - }, - "lvwiki": { - "site": "lvwiki", - "title": "Lauva", - "badges": ["Q17437796"], - "url": "https://lv.wikipedia.org/wiki/Lauva" - }, - "mdfwiki": { - "site": "mdfwiki", - "title": "\u041e\u0440\u043a\u0441\u043e\u0444\u0442\u0430", - "badges": ["Q17437796"], - "url": "https://mdf.wikipedia.org/wiki/%D0%9E%D1%80%D0%BA%D1%81%D0%BE%D1%84%D1%82%D0%B0" - }, - "mgwiki": { - "site": "mgwiki", - "title": "Liona", - "badges": [], - "url": "https://mg.wikipedia.org/wiki/Liona" - }, - "mhrwiki": { - "site": "mhrwiki", - "title": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://mhr.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD" - }, - "mkwiki": { - "site": "mkwiki", - "title": "\u041b\u0430\u0432", - "badges": [], - "url": "https://mk.wikipedia.org/wiki/%D0%9B%D0%B0%D0%B2" - }, - "mlwiki": { - "site": "mlwiki", - "title": "\u0d38\u0d3f\u0d02\u0d39\u0d02", - "badges": [], - "url": "https://ml.wikipedia.org/wiki/%E0%B4%B8%E0%B4%BF%E0%B4%82%E0%B4%B9%E0%B4%82" - }, - "mniwiki": { - "site": "mniwiki", - "title": "\uabc5\uabe3\uabe1\uabc1\uabe5", - "badges": [], - "url": "https://mni.wikipedia.org/wiki/%EA%AF%85%EA%AF%A3%EA%AF%A1%EA%AF%81%EA%AF%A5" - }, - "mnwiki": { - "site": "mnwiki", - "title": "\u0410\u0440\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://mn.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D0%B0%D0%BD" - }, - "mrjwiki": { - "site": "mrjwiki", - "title": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://mrj.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD" - }, - "mrwiki": { - "site": "mrwiki", - "title": "\u0938\u093f\u0902\u0939", - "badges": [], - "url": "https://mr.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9" - }, - "mswiki": { - "site": "mswiki", - "title": "Singa", - "badges": ["Q17437796"], - "url": "https://ms.wikipedia.org/wiki/Singa" - }, - "mtwiki": { - "site": "mtwiki", - "title": "Iljun", - "badges": [], - "url": "https://mt.wikipedia.org/wiki/Iljun" - }, - "mywiki": { - "site": "mywiki", - "title": "\u1001\u103c\u1004\u103a\u1039\u101e\u1031\u1037", - "badges": [], - "url": "https://my.wikipedia.org/wiki/%E1%80%81%E1%80%BC%E1%80%84%E1%80%BA%E1%80%B9%E1%80%9E%E1%80%B1%E1%80%B7" - }, - "newiki": { - "site": "newiki", - "title": "\u0938\u093f\u0902\u0939", - "badges": [], - "url": "https://ne.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9" - }, - "newwiki": { - "site": "newwiki", - "title": "\u0938\u093f\u0902\u0939", - "badges": [], - "url": "https://new.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9" - }, - "nlwiki": { - "site": "nlwiki", - "title": "Leeuw (dier)", - "badges": [], - "url": "https://nl.wikipedia.org/wiki/Leeuw_(dier)" - }, - "nnwiki": { - "site": "nnwiki", - "title": "L\u00f8ve", - "badges": [], - "url": "https://nn.wikipedia.org/wiki/L%C3%B8ve" - }, - "nowiki": { - "site": "nowiki", - "title": "L\u00f8ve", - "badges": [], - "url": "https://no.wikipedia.org/wiki/L%C3%B8ve" - }, - "nrmwiki": { - "site": "nrmwiki", - "title": "Lion", - "badges": [], - "url": "https://nrm.wikipedia.org/wiki/Lion" - }, - "nsowiki": { - "site": "nsowiki", - "title": "Tau", - "badges": [], - "url": "https://nso.wikipedia.org/wiki/Tau" - }, - "nvwiki": { - "site": "nvwiki", - "title": "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed", - "badges": [], - "url": "https://nv.wikipedia.org/wiki/N%C3%A1shd%C3%B3%C3%ADtsoh_bitsiij%C4%AF%CA%BC_dadit%C5%82%CA%BCoo%C3%ADg%C3%AD%C3%AD" - }, - "ocwiki": { - "site": "ocwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://oc.wikipedia.org/wiki/Panthera_leo" - }, - "orwiki": { - "site": "orwiki", - "title": "\u0b38\u0b3f\u0b02\u0b39", - "badges": [], - "url": "https://or.wikipedia.org/wiki/%E0%AC%B8%E0%AC%BF%E0%AC%82%E0%AC%B9" - }, - "oswiki": { - "site": "oswiki", - "title": "\u0426\u043e\u043c\u0430\u0445\u044a", - "badges": [], - "url": "https://os.wikipedia.org/wiki/%D0%A6%D0%BE%D0%BC%D0%B0%D1%85%D1%8A" - }, - "pamwiki": { - "site": "pamwiki", - "title": "Leon (animal)", - "badges": ["Q17437796"], - "url": "https://pam.wikipedia.org/wiki/Leon_(animal)" - }, - "pawiki": { - "site": "pawiki", - "title": "\u0a2c\u0a71\u0a2c\u0a30 \u0a38\u0a3c\u0a47\u0a30", - "badges": [], - "url": "https://pa.wikipedia.org/wiki/%E0%A8%AC%E0%A9%B1%E0%A8%AC%E0%A8%B0_%E0%A8%B8%E0%A8%BC%E0%A9%87%E0%A8%B0" - }, - "pcdwiki": { - "site": "pcdwiki", - "title": "Lion", - "badges": [], - "url": "https://pcd.wikipedia.org/wiki/Lion" - }, - "plwiki": { - "site": "plwiki", - "title": "Lew afryka\u0144ski", - "badges": ["Q17437796"], - "url": "https://pl.wikipedia.org/wiki/Lew_afryka%C5%84ski" - }, - "plwikiquote": { - "site": "plwikiquote", - "title": "Lew", - "badges": [], - "url": "https://pl.wikiquote.org/wiki/Lew" - }, - "pmswiki": { - "site": "pmswiki", - "title": "Lion", - "badges": [], - "url": "https://pms.wikipedia.org/wiki/Lion" - }, - "pnbwiki": { - "site": "pnbwiki", - "title": "\u0628\u0628\u0631 \u0634\u06cc\u0631", - "badges": [], - "url": "https://pnb.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%DB%8C%D8%B1" - }, - "pswiki": { - "site": "pswiki", - "title": "\u0632\u0645\u0631\u06cc", - "badges": [], - "url": "https://ps.wikipedia.org/wiki/%D8%B2%D9%85%D8%B1%DB%8C" - }, - "ptwiki": { - "site": "ptwiki", - "title": "Le\u00e3o", - "badges": [], - "url": "https://pt.wikipedia.org/wiki/Le%C3%A3o" - }, - "ptwikiquote": { - "site": "ptwikiquote", - "title": "Le\u00e3o", - "badges": [], - "url": "https://pt.wikiquote.org/wiki/Le%C3%A3o" - }, - "quwiki": { - "site": "quwiki", - "title": "Liyun", - "badges": [], - "url": "https://qu.wikipedia.org/wiki/Liyun" - }, - "rmwiki": { - "site": "rmwiki", - "title": "Liun", - "badges": [], - "url": "https://rm.wikipedia.org/wiki/Liun" - }, - "rnwiki": { - "site": "rnwiki", - "title": "Intare", - "badges": [], - "url": "https://rn.wikipedia.org/wiki/Intare" - }, - "rowiki": { - "site": "rowiki", - "title": "Leu", - "badges": [], - "url": "https://ro.wikipedia.org/wiki/Leu" - }, - "ruewiki": { - "site": "ruewiki", - "title": "\u041b\u0435\u0432", - "badges": [], - "url": "https://rue.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2" - }, - "ruwiki": { - "site": "ruwiki", - "title": "\u041b\u0435\u0432", - "badges": ["Q17437798"], - "url": "https://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2" - }, - "ruwikinews": { - "site": "ruwikinews", - "title": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f:\u041b\u044c\u0432\u044b", - "badges": [], - "url": "https://ru.wikinews.org/wiki/%D0%9A%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F:%D0%9B%D1%8C%D0%B2%D1%8B" - }, - "rwwiki": { - "site": "rwwiki", - "title": "Intare", - "badges": [], - "url": "https://rw.wikipedia.org/wiki/Intare" - }, - "sahwiki": { - "site": "sahwiki", - "title": "\u0425\u0430\u0445\u0430\u0439", - "badges": [], - "url": "https://sah.wikipedia.org/wiki/%D0%A5%D0%B0%D1%85%D0%B0%D0%B9" - }, - "satwiki": { - "site": "satwiki", - "title": "\u1c61\u1c5f\u1c74\u1c5f\u1c60\u1c69\u1c5e", - "badges": [], - "url": "https://sat.wikipedia.org/wiki/%E1%B1%A1%E1%B1%9F%E1%B1%B4%E1%B1%9F%E1%B1%A0%E1%B1%A9%E1%B1%9E" - }, - "sawiki": { - "site": "sawiki", - "title": "\u0938\u093f\u0902\u0939\u0903 \u092a\u0936\u0941\u0903", - "badges": [], - "url": "https://sa.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9%E0%A4%83_%E0%A4%AA%E0%A4%B6%E0%A5%81%E0%A4%83" - }, - "scnwiki": { - "site": "scnwiki", - "title": "Panthera leo", - "badges": [], - "url": "https://scn.wikipedia.org/wiki/Panthera_leo" - }, - "scowiki": { - "site": "scowiki", - "title": "Lion", - "badges": ["Q17437796"], - "url": "https://sco.wikipedia.org/wiki/Lion" - }, - "sdwiki": { - "site": "sdwiki", - "title": "\u0628\u0628\u0631 \u0634\u064a\u0646\u0647\u0646", - "badges": [], - "url": "https://sd.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%D9%8A%D9%86%D9%87%D9%86" - }, - "sewiki": { - "site": "sewiki", - "title": "Ledjon", - "badges": [], - "url": "https://se.wikipedia.org/wiki/Ledjon" - }, - "shiwiki": { - "site": "shiwiki", - "title": "Agrzam", - "badges": [], - "url": "https://shi.wikipedia.org/wiki/Agrzam" - }, - "shnwiki": { - "site": "shnwiki", - "title": "\u101e\u1062\u1004\u103a\u1087\u101e\u102e\u1088", - "badges": [], - "url": "https://shn.wikipedia.org/wiki/%E1%80%9E%E1%81%A2%E1%80%84%E1%80%BA%E1%82%87%E1%80%9E%E1%80%AE%E1%82%88" - }, - "shwiki": { - "site": "shwiki", - "title": "Lav", - "badges": [], - "url": "https://sh.wikipedia.org/wiki/Lav" - }, - "simplewiki": { - "site": "simplewiki", - "title": "Lion", - "badges": [], - "url": "https://simple.wikipedia.org/wiki/Lion" - }, - "siwiki": { - "site": "siwiki", - "title": "\u0dc3\u0dd2\u0d82\u0dc4\u0dba\u0dcf", - "badges": [], - "url": "https://si.wikipedia.org/wiki/%E0%B7%83%E0%B7%92%E0%B6%82%E0%B7%84%E0%B6%BA%E0%B7%8F" - }, - "skwiki": { - "site": "skwiki", - "title": "Lev p\u00fa\u0161\u0165ov\u00fd", - "badges": [], - "url": "https://sk.wikipedia.org/wiki/Lev_p%C3%BA%C5%A1%C5%A5ov%C3%BD" - }, - "skwikiquote": { - "site": "skwikiquote", - "title": "Lev", - "badges": [], - "url": "https://sk.wikiquote.org/wiki/Lev" - }, - "slwiki": { - "site": "slwiki", - "title": "Lev", - "badges": [], - "url": "https://sl.wikipedia.org/wiki/Lev" - }, - "smwiki": { - "site": "smwiki", - "title": "Leona", - "badges": [], - "url": "https://sm.wikipedia.org/wiki/Leona" - }, - "snwiki": { - "site": "snwiki", - "title": "Shumba", - "badges": [], - "url": "https://sn.wikipedia.org/wiki/Shumba" - }, - "sowiki": { - "site": "sowiki", - "title": "Libaax", - "badges": [], - "url": "https://so.wikipedia.org/wiki/Libaax" - }, - "specieswiki": { - "site": "specieswiki", - "title": "Panthera leo", - "badges": [], - "url": "https://species.wikimedia.org/wiki/Panthera_leo" - }, - "sqwiki": { - "site": "sqwiki", - "title": "Luani", - "badges": [], - "url": "https://sq.wikipedia.org/wiki/Luani" - }, - "srwiki": { - "site": "srwiki", - "title": "\u041b\u0430\u0432", - "badges": [], - "url": "https://sr.wikipedia.org/wiki/%D0%9B%D0%B0%D0%B2" - }, - "sswiki": { - "site": "sswiki", - "title": "Libhubesi", - "badges": [], - "url": "https://ss.wikipedia.org/wiki/Libhubesi" - }, - "stqwiki": { - "site": "stqwiki", - "title": "Leeuwe", - "badges": [], - "url": "https://stq.wikipedia.org/wiki/Leeuwe" - }, - "stwiki": { - "site": "stwiki", - "title": "Tau", - "badges": [], - "url": "https://st.wikipedia.org/wiki/Tau" - }, - "suwiki": { - "site": "suwiki", - "title": "Singa", - "badges": [], - "url": "https://su.wikipedia.org/wiki/Singa" - }, - "svwiki": { - "site": "svwiki", - "title": "Lejon", - "badges": [], - "url": "https://sv.wikipedia.org/wiki/Lejon" - }, - "swwiki": { - "site": "swwiki", - "title": "Simba", - "badges": [], - "url": "https://sw.wikipedia.org/wiki/Simba" - }, - "szlwiki": { - "site": "szlwiki", - "title": "Lew", - "badges": [], - "url": "https://szl.wikipedia.org/wiki/Lew" - }, - "tawiki": { - "site": "tawiki", - "title": "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd", - "badges": [], - "url": "https://ta.wikipedia.org/wiki/%E0%AE%9A%E0%AE%BF%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%AE%E0%AF%8D" - }, - "tcywiki": { - "site": "tcywiki", - "title": "\u0cb8\u0cbf\u0c82\u0cb9", - "badges": [], - "url": "https://tcy.wikipedia.org/wiki/%E0%B2%B8%E0%B2%BF%E0%B2%82%E0%B2%B9" - }, - "tewiki": { - "site": "tewiki", - "title": "\u0c38\u0c3f\u0c02\u0c39\u0c02", - "badges": [], - "url": "https://te.wikipedia.org/wiki/%E0%B0%B8%E0%B0%BF%E0%B0%82%E0%B0%B9%E0%B0%82" - }, - "tgwiki": { - "site": "tgwiki", - "title": "\u0428\u0435\u0440", - "badges": [], - "url": "https://tg.wikipedia.org/wiki/%D0%A8%D0%B5%D1%80" - }, - "thwiki": { - "site": "thwiki", - "title": "\u0e2a\u0e34\u0e07\u0e42\u0e15", - "badges": [], - "url": "https://th.wikipedia.org/wiki/%E0%B8%AA%E0%B8%B4%E0%B8%87%E0%B9%82%E0%B8%95" - }, - "tiwiki": { - "site": "tiwiki", - "title": "\u12a3\u1295\u1260\u1233", - "badges": [], - "url": "https://ti.wikipedia.org/wiki/%E1%8A%A3%E1%8A%95%E1%89%A0%E1%88%B3" - }, - "tkwiki": { - "site": "tkwiki", - "title": "\u00ddolbars", - "badges": [], - "url": "https://tk.wikipedia.org/wiki/%C3%9Dolbars" - }, - "tlwiki": { - "site": "tlwiki", - "title": "Leon", - "badges": [], - "url": "https://tl.wikipedia.org/wiki/Leon" - }, - "trwiki": { - "site": "trwiki", - "title": "Aslan", - "badges": [], - "url": "https://tr.wikipedia.org/wiki/Aslan" - }, - "ttwiki": { - "site": "ttwiki", - "title": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://tt.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD" - }, - "tumwiki": { - "site": "tumwiki", - "title": "Nkhalamu", - "badges": [], - "url": "https://tum.wikipedia.org/wiki/Nkhalamu" - }, - "udmwiki": { - "site": "udmwiki", - "title": "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", - "badges": [], - "url": "https://udm.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD" - }, - "ugwiki": { - "site": "ugwiki", - "title": "\u0634\u0649\u0631", - "badges": [], - "url": "https://ug.wikipedia.org/wiki/%D8%B4%D9%89%D8%B1" - }, - "ukwiki": { - "site": "ukwiki", - "title": "\u041b\u0435\u0432", - "badges": [], - "url": "https://uk.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2" - }, - "ukwikiquote": { - "site": "ukwikiquote", - "title": "\u041b\u0435\u0432", - "badges": [], - "url": "https://uk.wikiquote.org/wiki/%D0%9B%D0%B5%D0%B2" - }, - "urwiki": { - "site": "urwiki", - "title": "\u0628\u0628\u0631 \u0634\u06cc\u0631", - "badges": ["Q17437796"], - "url": "https://ur.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%DB%8C%D8%B1" - }, - "uzwiki": { - "site": "uzwiki", - "title": "Arslon", - "badges": [], - "url": "https://uz.wikipedia.org/wiki/Arslon" - }, - "vecwiki": { - "site": "vecwiki", - "title": "Leon", - "badges": [], - "url": "https://vec.wikipedia.org/wiki/Leon" - }, - "vepwiki": { - "site": "vepwiki", - "title": "Lev", - "badges": [], - "url": "https://vep.wikipedia.org/wiki/Lev" - }, - "viwiki": { - "site": "viwiki", - "title": "S\u01b0 t\u1eed", - "badges": [], - "url": "https://vi.wikipedia.org/wiki/S%C6%B0_t%E1%BB%AD" - }, - "vlswiki": { - "site": "vlswiki", - "title": "L\u00eaeuw (b\u00eaeste)", - "badges": [], - "url": "https://vls.wikipedia.org/wiki/L%C3%AAeuw_(b%C3%AAeste)" - }, - "warwiki": { - "site": "warwiki", - "title": "Leon", - "badges": [], - "url": "https://war.wikipedia.org/wiki/Leon" - }, - "wowiki": { - "site": "wowiki", - "title": "Gaynde", - "badges": [], - "url": "https://wo.wikipedia.org/wiki/Gaynde" - }, - "wuuwiki": { - "site": "wuuwiki", - "title": "\u72ee", - "badges": [], - "url": "https://wuu.wikipedia.org/wiki/%E7%8B%AE" - }, - "xalwiki": { - "site": "xalwiki", - "title": "\u0410\u0440\u0441\u043b\u04a3", - "badges": [], - "url": "https://xal.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D2%A3" - }, - "xhwiki": { - "site": "xhwiki", - "title": "Ingonyama", - "badges": [], - "url": "https://xh.wikipedia.org/wiki/Ingonyama" - }, - "xmfwiki": { - "site": "xmfwiki", - "title": "\u10dc\u10ef\u10d8\u10da\u10dd", - "badges": [], - "url": "https://xmf.wikipedia.org/wiki/%E1%83%9C%E1%83%AF%E1%83%98%E1%83%9A%E1%83%9D" - }, - "yiwiki": { - "site": "yiwiki", - "title": "\u05dc\u05d9\u05d9\u05d1", - "badges": [], - "url": "https://yi.wikipedia.org/wiki/%D7%9C%D7%99%D7%99%D7%91" - }, - "yowiki": { - "site": "yowiki", - "title": "K\u00ecn\u00ec\u00fan", - "badges": [], - "url": "https://yo.wikipedia.org/wiki/K%C3%ACn%C3%AC%C3%BAn" - }, - "zawiki": { - "site": "zawiki", - "title": "Saeceij", - "badges": [], - "url": "https://za.wikipedia.org/wiki/Saeceij" - }, - "zh_min_nanwiki": { - "site": "zh_min_nanwiki", - "title": "Sai", - "badges": [], - "url": "https://zh-min-nan.wikipedia.org/wiki/Sai" - }, - "zh_yuewiki": { - "site": "zh_yuewiki", - "title": "\u7345\u5b50", - "badges": ["Q17437796"], - "url": "https://zh-yue.wikipedia.org/wiki/%E7%8D%85%E5%AD%90" - }, - "zhwiki": { - "site": "zhwiki", - "title": "\u72ee", - "badges": [], - "url": "https://zh.wikipedia.org/wiki/%E7%8B%AE" - }, - "zuwiki": { - "site": "zuwiki", - "title": "Ibhubesi", - "badges": [], - "url": "https://zu.wikipedia.org/wiki/Ibhubesi" - } - } - } - } + sitelinks: { + abwiki: { + site: "abwiki", + title: "\u0410\u043b\u044b\u043c", + badges: [], + url: "https://ab.wikipedia.org/wiki/%D0%90%D0%BB%D1%8B%D0%BC", + }, + adywiki: { + site: "adywiki", + title: "\u0410\u0441\u043b\u044a\u0430\u043d", + badges: [], + url: "https://ady.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D1%8A%D0%B0%D0%BD", + }, + afwiki: { + site: "afwiki", + title: "Leeu", + badges: ["Q17437796"], + url: "https://af.wikipedia.org/wiki/Leeu", + }, + alswiki: { + site: "alswiki", + title: "L\u00f6we", + badges: [], + url: "https://als.wikipedia.org/wiki/L%C3%B6we", + }, + altwiki: { + site: "altwiki", + title: "\u0410\u0440\u0441\u043b\u0430\u043d", + badges: [], + url: "https://alt.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D0%B0%D0%BD", + }, + amwiki: { + site: "amwiki", + title: "\u12a0\u1295\u1260\u1233", + badges: [], + url: "https://am.wikipedia.org/wiki/%E1%8A%A0%E1%8A%95%E1%89%A0%E1%88%B3", + }, + angwiki: { + site: "angwiki", + title: "L\u0113o", + badges: [], + url: "https://ang.wikipedia.org/wiki/L%C4%93o", + }, + anwiki: { + site: "anwiki", + title: "Panthera leo", + badges: [], + url: "https://an.wikipedia.org/wiki/Panthera_leo", + }, + arcwiki: { + site: "arcwiki", + title: "\u0710\u072a\u071d\u0710", + badges: [], + url: "https://arc.wikipedia.org/wiki/%DC%90%DC%AA%DC%9D%DC%90", + }, + arwiki: { + site: "arwiki", + title: "\u0623\u0633\u062f", + badges: ["Q17437796"], + url: "https://ar.wikipedia.org/wiki/%D8%A3%D8%B3%D8%AF", + }, + arywiki: { + site: "arywiki", + title: "\u0633\u0628\u0639", + badges: [], + url: "https://ary.wikipedia.org/wiki/%D8%B3%D8%A8%D8%B9", + }, + arzwiki: { + site: "arzwiki", + title: "\u0633\u0628\u0639", + badges: [], + url: "https://arz.wikipedia.org/wiki/%D8%B3%D8%A8%D8%B9", + }, + astwiki: { + site: "astwiki", + title: "Panthera leo", + badges: [], + url: "https://ast.wikipedia.org/wiki/Panthera_leo", + }, + aswiki: { + site: "aswiki", + title: "\u09b8\u09bf\u0982\u09b9", + badges: [], + url: "https://as.wikipedia.org/wiki/%E0%A6%B8%E0%A6%BF%E0%A6%82%E0%A6%B9", + }, + avkwiki: { + site: "avkwiki", + title: "Krapol (Panthera leo)", + badges: [], + url: "https://avk.wikipedia.org/wiki/Krapol_(Panthera_leo)", + }, + avwiki: { + site: "avwiki", + title: "\u0413\u044a\u0430\u043b\u0431\u0430\u0446\u04c0", + badges: [], + url: "https://av.wikipedia.org/wiki/%D0%93%D1%8A%D0%B0%D0%BB%D0%B1%D0%B0%D1%86%D3%80", + }, + azbwiki: { + site: "azbwiki", + title: "\u0622\u0633\u0644\u0627\u0646", + badges: [], + url: "https://azb.wikipedia.org/wiki/%D8%A2%D8%B3%D9%84%D8%A7%D9%86", + }, + azwiki: { + site: "azwiki", + title: "\u015eir", + badges: [], + url: "https://az.wikipedia.org/wiki/%C5%9Eir", + }, + bat_smgwiki: { + site: "bat_smgwiki", + title: "Li\u016bts", + badges: [], + url: "https://bat-smg.wikipedia.org/wiki/Li%C5%ABts", + }, + bawiki: { + site: "bawiki", + title: "\u0410\u0440\u044b\u04ab\u043b\u0430\u043d", + badges: [], + url: "https://ba.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D2%AB%D0%BB%D0%B0%D0%BD", + }, + bclwiki: { + site: "bclwiki", + title: "Leon", + badges: [], + url: "https://bcl.wikipedia.org/wiki/Leon", + }, + be_x_oldwiki: { + site: "be_x_oldwiki", + title: "\u041b\u0435\u045e", + badges: [], + url: "https://be-tarask.wikipedia.org/wiki/%D0%9B%D0%B5%D1%9E", + }, + bewiki: { + site: "bewiki", + title: "\u041b\u0435\u045e", + badges: [], + url: "https://be.wikipedia.org/wiki/%D0%9B%D0%B5%D1%9E", + }, + bgwiki: { + site: "bgwiki", + title: "\u041b\u044a\u0432", + badges: [], + url: "https://bg.wikipedia.org/wiki/%D0%9B%D1%8A%D0%B2", + }, + bhwiki: { + site: "bhwiki", + title: "\u0938\u093f\u0902\u0939", + badges: [], + url: "https://bh.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9", + }, + bmwiki: { + site: "bmwiki", + title: "Waraba", + badges: [], + url: "https://bm.wikipedia.org/wiki/Waraba", + }, + bnwiki: { + site: "bnwiki", + title: "\u09b8\u09bf\u0982\u09b9", + badges: [], + url: "https://bn.wikipedia.org/wiki/%E0%A6%B8%E0%A6%BF%E0%A6%82%E0%A6%B9", + }, + bowiki: { + site: "bowiki", + title: "\u0f66\u0f7a\u0f44\u0f0b\u0f42\u0f7a\u0f0d", + badges: [], + url: "https://bo.wikipedia.org/wiki/%E0%BD%A6%E0%BD%BA%E0%BD%84%E0%BC%8B%E0%BD%82%E0%BD%BA%E0%BC%8D", + }, + bpywiki: { + site: "bpywiki", + title: "\u09a8\u0982\u09b8\u09be", + badges: [], + url: "https://bpy.wikipedia.org/wiki/%E0%A6%A8%E0%A6%82%E0%A6%B8%E0%A6%BE", + }, + brwiki: { + site: "brwiki", + title: "Leon (loen)", + badges: [], + url: "https://br.wikipedia.org/wiki/Leon_(loen)", + }, + bswiki: { + site: "bswiki", + title: "Lav", + badges: [], + url: "https://bs.wikipedia.org/wiki/Lav", + }, + bswikiquote: { + site: "bswikiquote", + title: "Lav", + badges: [], + url: "https://bs.wikiquote.org/wiki/Lav", + }, + bxrwiki: { + site: "bxrwiki", + title: "\u0410\u0440\u0441\u0430\u043b\u0430\u043d", + badges: [], + url: "https://bxr.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%B0%D0%BB%D0%B0%D0%BD", + }, + cawiki: { + site: "cawiki", + title: "Lle\u00f3", + badges: ["Q17437796"], + url: "https://ca.wikipedia.org/wiki/Lle%C3%B3", + }, + cawikiquote: { + site: "cawikiquote", + title: "Lle\u00f3", + badges: [], + url: "https://ca.wikiquote.org/wiki/Lle%C3%B3", + }, + cdowiki: { + site: "cdowiki", + title: "S\u0103i (m\u00e0-ku\u014f d\u00f4ng-\u016dk)", + badges: [], + url: "https://cdo.wikipedia.org/wiki/S%C4%83i_(m%C3%A0-ku%C5%8F_d%C3%B4ng-%C5%ADk)", + }, + cebwiki: { + site: "cebwiki", + title: "Panthera leo", + badges: [], + url: "https://ceb.wikipedia.org/wiki/Panthera_leo", + }, + cewiki: { + site: "cewiki", + title: "\u041b\u043e\u043c", + badges: [], + url: "https://ce.wikipedia.org/wiki/%D0%9B%D0%BE%D0%BC", + }, + chrwiki: { + site: "chrwiki", + title: "\u13e2\u13d3\u13e5 \u13a4\u13c3\u13d5\u13be", + badges: [], + url: "https://chr.wikipedia.org/wiki/%E1%8F%A2%E1%8F%93%E1%8F%A5_%E1%8E%A4%E1%8F%83%E1%8F%95%E1%8E%BE", + }, + chywiki: { + site: "chywiki", + title: "P\u00e9hpe'\u00e9nan\u00f3se'hame", + badges: [], + url: "https://chy.wikipedia.org/wiki/P%C3%A9hpe%27%C3%A9nan%C3%B3se%27hame", + }, + ckbwiki: { + site: "ckbwiki", + title: "\u0634\u06ce\u0631", + badges: [], + url: "https://ckb.wikipedia.org/wiki/%D8%B4%DB%8E%D8%B1", + }, + commonswiki: { + site: "commonswiki", + title: "Panthera leo", + badges: [], + url: "https://commons.wikimedia.org/wiki/Panthera_leo", + }, + cowiki: { + site: "cowiki", + title: "Lionu", + badges: [], + url: "https://co.wikipedia.org/wiki/Lionu", + }, + csbwiki: { + site: "csbwiki", + title: "Lew", + badges: [], + url: "https://csb.wikipedia.org/wiki/Lew", + }, + cswiki: { + site: "cswiki", + title: "Lev", + badges: [], + url: "https://cs.wikipedia.org/wiki/Lev", + }, + cswikiquote: { + site: "cswikiquote", + title: "Lev", + badges: [], + url: "https://cs.wikiquote.org/wiki/Lev", + }, + cuwiki: { + site: "cuwiki", + title: "\u041b\u044c\u0432\u044a", + badges: [], + url: "https://cu.wikipedia.org/wiki/%D0%9B%D1%8C%D0%B2%D1%8A", + }, + cvwiki: { + site: "cvwiki", + title: "\u0410\u0440\u0103\u0441\u043b\u0430\u043d", + badges: [], + url: "https://cv.wikipedia.org/wiki/%D0%90%D1%80%C4%83%D1%81%D0%BB%D0%B0%D0%BD", + }, + cywiki: { + site: "cywiki", + title: "Llew", + badges: [], + url: "https://cy.wikipedia.org/wiki/Llew", + }, + dagwiki: { + site: "dagwiki", + title: "Gbu\u0263inli", + badges: [], + url: "https://dag.wikipedia.org/wiki/Gbu%C9%A3inli", + }, + dawiki: { + site: "dawiki", + title: "L\u00f8ve", + badges: ["Q17559452"], + url: "https://da.wikipedia.org/wiki/L%C3%B8ve", + }, + dewiki: { + site: "dewiki", + title: "L\u00f6we", + badges: ["Q17437796"], + url: "https://de.wikipedia.org/wiki/L%C3%B6we", + }, + dewikiquote: { + site: "dewikiquote", + title: "L\u00f6we", + badges: [], + url: "https://de.wikiquote.org/wiki/L%C3%B6we", + }, + dinwiki: { + site: "dinwiki", + title: "K\u00f6r", + badges: [], + url: "https://din.wikipedia.org/wiki/K%C3%B6r", + }, + diqwiki: { + site: "diqwiki", + title: "\u015e\u00ear", + badges: [], + url: "https://diq.wikipedia.org/wiki/%C5%9E%C3%AAr", + }, + dsbwiki: { + site: "dsbwiki", + title: "Law", + badges: [], + url: "https://dsb.wikipedia.org/wiki/Law", + }, + eewiki: { + site: "eewiki", + title: "Dzata", + badges: [], + url: "https://ee.wikipedia.org/wiki/Dzata", + }, + elwiki: { + site: "elwiki", + title: "\u039b\u03b9\u03bf\u03bd\u03c4\u03ac\u03c1\u03b9", + badges: [], + url: "https://el.wikipedia.org/wiki/%CE%9B%CE%B9%CE%BF%CE%BD%CF%84%CE%AC%CF%81%CE%B9", + }, + enwiki: { + site: "enwiki", + title: "Lion", + badges: ["Q17437796"], + url: "https://en.wikipedia.org/wiki/Lion", + }, + enwikiquote: { + site: "enwikiquote", + title: "Lions", + badges: [], + url: "https://en.wikiquote.org/wiki/Lions", + }, + eowiki: { + site: "eowiki", + title: "Leono", + badges: [], + url: "https://eo.wikipedia.org/wiki/Leono", + }, + eowikiquote: { + site: "eowikiquote", + title: "Leono", + badges: [], + url: "https://eo.wikiquote.org/wiki/Leono", + }, + eswiki: { + site: "eswiki", + title: "Panthera leo", + badges: ["Q17437796"], + url: "https://es.wikipedia.org/wiki/Panthera_leo", + }, + eswikiquote: { + site: "eswikiquote", + title: "Le\u00f3n", + badges: [], + url: "https://es.wikiquote.org/wiki/Le%C3%B3n", + }, + etwiki: { + site: "etwiki", + title: "L\u00f5vi", + badges: [], + url: "https://et.wikipedia.org/wiki/L%C3%B5vi", + }, + etwikiquote: { + site: "etwikiquote", + title: "L\u00f5vi", + badges: [], + url: "https://et.wikiquote.org/wiki/L%C3%B5vi", + }, + euwiki: { + site: "euwiki", + title: "Lehoi", + badges: [], + url: "https://eu.wikipedia.org/wiki/Lehoi", + }, + extwiki: { + site: "extwiki", + title: "Li\u00f3n (animal)", + badges: [], + url: "https://ext.wikipedia.org/wiki/Li%C3%B3n_(animal)", + }, + fawiki: { + site: "fawiki", + title: "\u0634\u06cc\u0631 (\u06af\u0631\u0628\u0647\u200c\u0633\u0627\u0646)", + badges: ["Q17437796"], + url: "https://fa.wikipedia.org/wiki/%D8%B4%DB%8C%D8%B1_(%DA%AF%D8%B1%D8%A8%D9%87%E2%80%8C%D8%B3%D8%A7%D9%86)", + }, + fawikiquote: { + site: "fawikiquote", + title: "\u0634\u06cc\u0631", + badges: [], + url: "https://fa.wikiquote.org/wiki/%D8%B4%DB%8C%D8%B1", + }, + fiu_vrowiki: { + site: "fiu_vrowiki", + title: "L\u00f5vi", + badges: [], + url: "https://fiu-vro.wikipedia.org/wiki/L%C3%B5vi", + }, + fiwiki: { + site: "fiwiki", + title: "Leijona", + badges: ["Q17437796"], + url: "https://fi.wikipedia.org/wiki/Leijona", + }, + fowiki: { + site: "fowiki", + title: "Leyva", + badges: [], + url: "https://fo.wikipedia.org/wiki/Leyva", + }, + frrwiki: { + site: "frrwiki", + title: "L\u00f6\u00f6w", + badges: [], + url: "https://frr.wikipedia.org/wiki/L%C3%B6%C3%B6w", + }, + frwiki: { + site: "frwiki", + title: "Lion", + badges: ["Q17437796"], + url: "https://fr.wikipedia.org/wiki/Lion", + }, + frwikiquote: { + site: "frwikiquote", + title: "Lion", + badges: [], + url: "https://fr.wikiquote.org/wiki/Lion", + }, + gagwiki: { + site: "gagwiki", + title: "Aslan", + badges: [], + url: "https://gag.wikipedia.org/wiki/Aslan", + }, + gawiki: { + site: "gawiki", + title: "Leon", + badges: [], + url: "https://ga.wikipedia.org/wiki/Leon", + }, + gdwiki: { + site: "gdwiki", + title: "Le\u00f2mhann", + badges: [], + url: "https://gd.wikipedia.org/wiki/Le%C3%B2mhann", + }, + glwiki: { + site: "glwiki", + title: "Le\u00f3n", + badges: [], + url: "https://gl.wikipedia.org/wiki/Le%C3%B3n", + }, + gnwiki: { + site: "gnwiki", + title: "Le\u00f5", + badges: [], + url: "https://gn.wikipedia.org/wiki/Le%C3%B5", + }, + gotwiki: { + site: "gotwiki", + title: "\ud800\udf3b\ud800\udf39\ud800\udf45\ud800\udf30", + badges: [], + url: "https://got.wikipedia.org/wiki/%F0%90%8C%BB%F0%90%8C%B9%F0%90%8D%85%F0%90%8C%B0", + }, + guwiki: { + site: "guwiki", + title: "\u0a8f\u0ab6\u0abf\u0aaf\u0abe\u0a87 \u0ab8\u0abf\u0a82\u0ab9", + badges: [], + url: "https://gu.wikipedia.org/wiki/%E0%AA%8F%E0%AA%B6%E0%AA%BF%E0%AA%AF%E0%AA%BE%E0%AA%87_%E0%AA%B8%E0%AA%BF%E0%AA%82%E0%AA%B9", + }, + hakwiki: { + site: "hakwiki", + title: "S\u1e73\u0302-\u00e9", + badges: [], + url: "https://hak.wikipedia.org/wiki/S%E1%B9%B3%CC%82-%C3%A9", + }, + hawiki: { + site: "hawiki", + title: "Zaki", + badges: [], + url: "https://ha.wikipedia.org/wiki/Zaki", + }, + hawwiki: { + site: "hawwiki", + title: "Liona", + badges: [], + url: "https://haw.wikipedia.org/wiki/Liona", + }, + hewiki: { + site: "hewiki", + title: "\u05d0\u05e8\u05d9\u05d4", + badges: [], + url: "https://he.wikipedia.org/wiki/%D7%90%D7%A8%D7%99%D7%94", + }, + hewikiquote: { + site: "hewikiquote", + title: "\u05d0\u05e8\u05d9\u05d4", + badges: [], + url: "https://he.wikiquote.org/wiki/%D7%90%D7%A8%D7%99%D7%94", + }, + hifwiki: { + site: "hifwiki", + title: "Ser", + badges: [], + url: "https://hif.wikipedia.org/wiki/Ser", + }, + hiwiki: { + site: "hiwiki", + title: "\u0938\u093f\u0902\u0939 (\u092a\u0936\u0941)", + badges: [], + url: "https://hi.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9_(%E0%A4%AA%E0%A4%B6%E0%A5%81)", + }, + hrwiki: { + site: "hrwiki", + title: "Lav", + badges: [], + url: "https://hr.wikipedia.org/wiki/Lav", + }, + hrwikiquote: { + site: "hrwikiquote", + title: "Lav", + badges: [], + url: "https://hr.wikiquote.org/wiki/Lav", + }, + hsbwiki: { + site: "hsbwiki", + title: "Law", + badges: [], + url: "https://hsb.wikipedia.org/wiki/Law", + }, + htwiki: { + site: "htwiki", + title: "Lyon", + badges: [], + url: "https://ht.wikipedia.org/wiki/Lyon", + }, + huwiki: { + site: "huwiki", + title: "Oroszl\u00e1n", + badges: [], + url: "https://hu.wikipedia.org/wiki/Oroszl%C3%A1n", + }, + hywiki: { + site: "hywiki", + title: "\u0531\u057c\u0575\u0578\u0582\u056e", + badges: [], + url: "https://hy.wikipedia.org/wiki/%D4%B1%D5%BC%D5%B5%D5%B8%D6%82%D5%AE", + }, + hywikiquote: { + site: "hywikiquote", + title: "\u0531\u057c\u0575\u0578\u0582\u056e", + badges: [], + url: "https://hy.wikiquote.org/wiki/%D4%B1%D5%BC%D5%B5%D5%B8%D6%82%D5%AE", + }, + hywwiki: { + site: "hywwiki", + title: "\u0531\u057c\u056b\u0582\u056e", + badges: [], + url: "https://hyw.wikipedia.org/wiki/%D4%B1%D5%BC%D5%AB%D6%82%D5%AE", + }, + iawiki: { + site: "iawiki", + title: "Leon", + badges: [], + url: "https://ia.wikipedia.org/wiki/Leon", + }, + idwiki: { + site: "idwiki", + title: "Singa", + badges: [], + url: "https://id.wikipedia.org/wiki/Singa", + }, + igwiki: { + site: "igwiki", + title: "Od\u00fam", + badges: [], + url: "https://ig.wikipedia.org/wiki/Od%C3%BAm", + }, + ilowiki: { + site: "ilowiki", + title: "Leon", + badges: [], + url: "https://ilo.wikipedia.org/wiki/Leon", + }, + inhwiki: { + site: "inhwiki", + title: "\u041b\u043e\u043c", + badges: [], + url: "https://inh.wikipedia.org/wiki/%D0%9B%D0%BE%D0%BC", + }, + iowiki: { + site: "iowiki", + title: "Leono (mamifero)", + badges: [], + url: "https://io.wikipedia.org/wiki/Leono_(mamifero)", + }, + iswiki: { + site: "iswiki", + title: "Lj\u00f3n", + badges: [], + url: "https://is.wikipedia.org/wiki/Lj%C3%B3n", + }, + itwiki: { + site: "itwiki", + title: "Panthera leo", + badges: [], + url: "https://it.wikipedia.org/wiki/Panthera_leo", + }, + itwikiquote: { + site: "itwikiquote", + title: "Leone", + badges: [], + url: "https://it.wikiquote.org/wiki/Leone", + }, + jawiki: { + site: "jawiki", + title: "\u30e9\u30a4\u30aa\u30f3", + badges: [], + url: "https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%82%AA%E3%83%B3", + }, + jawikiquote: { + site: "jawikiquote", + title: "\u7345\u5b50", + badges: [], + url: "https://ja.wikiquote.org/wiki/%E7%8D%85%E5%AD%90", + }, + jbowiki: { + site: "jbowiki", + title: "cinfo", + badges: [], + url: "https://jbo.wikipedia.org/wiki/cinfo", + }, + jvwiki: { + site: "jvwiki", + title: "Singa", + badges: [], + url: "https://jv.wikipedia.org/wiki/Singa", + }, + kabwiki: { + site: "kabwiki", + title: "Izem", + badges: [], + url: "https://kab.wikipedia.org/wiki/Izem", + }, + kawiki: { + site: "kawiki", + title: "\u10da\u10dd\u10db\u10d8", + badges: [], + url: "https://ka.wikipedia.org/wiki/%E1%83%9A%E1%83%9D%E1%83%9B%E1%83%98", + }, + kbdwiki: { + site: "kbdwiki", + title: "\u0425\u044c\u044d\u0449", + badges: [], + url: "https://kbd.wikipedia.org/wiki/%D0%A5%D1%8C%D1%8D%D1%89", + }, + kbpwiki: { + site: "kbpwiki", + title: "T\u0254\u0254y\u028b\u028b", + badges: [], + url: "https://kbp.wikipedia.org/wiki/T%C9%94%C9%94y%CA%8B%CA%8B", + }, + kgwiki: { + site: "kgwiki", + title: "Nkosi", + badges: [], + url: "https://kg.wikipedia.org/wiki/Nkosi", + }, + kkwiki: { + site: "kkwiki", + title: "\u0410\u0440\u044b\u0441\u0442\u0430\u043d", + badges: [], + url: "https://kk.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D1%82%D0%B0%D0%BD", + }, + knwiki: { + site: "knwiki", + title: "\u0cb8\u0cbf\u0c82\u0cb9", + badges: [], + url: "https://kn.wikipedia.org/wiki/%E0%B2%B8%E0%B2%BF%E0%B2%82%E0%B2%B9", + }, + kowiki: { + site: "kowiki", + title: "\uc0ac\uc790", + badges: [], + url: "https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9E%90", + }, + kowikiquote: { + site: "kowikiquote", + title: "\uc0ac\uc790", + badges: [], + url: "https://ko.wikiquote.org/wiki/%EC%82%AC%EC%9E%90", + }, + kswiki: { + site: "kswiki", + title: "\u067e\u0627\u062f\u064e\u0631 \u0633\u0655\u06c1\u06c1", + badges: [], + url: "https://ks.wikipedia.org/wiki/%D9%BE%D8%A7%D8%AF%D9%8E%D8%B1_%D8%B3%D9%95%DB%81%DB%81", + }, + kuwiki: { + site: "kuwiki", + title: "\u015e\u00ear", + badges: [], + url: "https://ku.wikipedia.org/wiki/%C5%9E%C3%AAr", + }, + kwwiki: { + site: "kwwiki", + title: "Lew", + badges: [], + url: "https://kw.wikipedia.org/wiki/Lew", + }, + kywiki: { + site: "kywiki", + title: "\u0410\u0440\u0441\u0442\u0430\u043d", + badges: [], + url: "https://ky.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D1%82%D0%B0%D0%BD", + }, + lawiki: { + site: "lawiki", + title: "Leo", + badges: [], + url: "https://la.wikipedia.org/wiki/Leo", + }, + lawikiquote: { + site: "lawikiquote", + title: "Leo", + badges: [], + url: "https://la.wikiquote.org/wiki/Leo", + }, + lbewiki: { + site: "lbewiki", + title: "\u0410\u0441\u043b\u0430\u043d", + badges: [], + url: "https://lbe.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D0%B0%D0%BD", + }, + lbwiki: { + site: "lbwiki", + title: "L\u00e9iw", + badges: [], + url: "https://lb.wikipedia.org/wiki/L%C3%A9iw", + }, + lezwiki: { + site: "lezwiki", + title: "\u0410\u0441\u043b\u0430\u043d", + badges: [], + url: "https://lez.wikipedia.org/wiki/%D0%90%D1%81%D0%BB%D0%B0%D0%BD", + }, + lfnwiki: { + site: "lfnwiki", + title: "Leon", + badges: [], + url: "https://lfn.wikipedia.org/wiki/Leon", + }, + lijwiki: { + site: "lijwiki", + title: "Lion (bestia)", + badges: [], + url: "https://lij.wikipedia.org/wiki/Lion_(bestia)", + }, + liwiki: { + site: "liwiki", + title: "Liew", + badges: ["Q17437796"], + url: "https://li.wikipedia.org/wiki/Liew", + }, + lldwiki: { + site: "lldwiki", + title: "Lion", + badges: [], + url: "https://lld.wikipedia.org/wiki/Lion", + }, + lmowiki: { + site: "lmowiki", + title: "Panthera leo", + badges: [], + url: "https://lmo.wikipedia.org/wiki/Panthera_leo", + }, + lnwiki: { + site: "lnwiki", + title: "Nk\u0254\u0301si", + badges: [], + url: "https://ln.wikipedia.org/wiki/Nk%C9%94%CC%81si", + }, + ltgwiki: { + site: "ltgwiki", + title: "\u013bovs", + badges: [], + url: "https://ltg.wikipedia.org/wiki/%C4%BBovs", + }, + ltwiki: { + site: "ltwiki", + title: "Li\u016btas", + badges: [], + url: "https://lt.wikipedia.org/wiki/Li%C5%ABtas", + }, + ltwikiquote: { + site: "ltwikiquote", + title: "Li\u016btas", + badges: [], + url: "https://lt.wikiquote.org/wiki/Li%C5%ABtas", + }, + lvwiki: { + site: "lvwiki", + title: "Lauva", + badges: ["Q17437796"], + url: "https://lv.wikipedia.org/wiki/Lauva", + }, + mdfwiki: { + site: "mdfwiki", + title: "\u041e\u0440\u043a\u0441\u043e\u0444\u0442\u0430", + badges: ["Q17437796"], + url: "https://mdf.wikipedia.org/wiki/%D0%9E%D1%80%D0%BA%D1%81%D0%BE%D1%84%D1%82%D0%B0", + }, + mgwiki: { + site: "mgwiki", + title: "Liona", + badges: [], + url: "https://mg.wikipedia.org/wiki/Liona", + }, + mhrwiki: { + site: "mhrwiki", + title: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", + badges: [], + url: "https://mhr.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD", + }, + mkwiki: { + site: "mkwiki", + title: "\u041b\u0430\u0432", + badges: [], + url: "https://mk.wikipedia.org/wiki/%D0%9B%D0%B0%D0%B2", + }, + mlwiki: { + site: "mlwiki", + title: "\u0d38\u0d3f\u0d02\u0d39\u0d02", + badges: [], + url: "https://ml.wikipedia.org/wiki/%E0%B4%B8%E0%B4%BF%E0%B4%82%E0%B4%B9%E0%B4%82", + }, + mniwiki: { + site: "mniwiki", + title: "\uabc5\uabe3\uabe1\uabc1\uabe5", + badges: [], + url: "https://mni.wikipedia.org/wiki/%EA%AF%85%EA%AF%A3%EA%AF%A1%EA%AF%81%EA%AF%A5", + }, + mnwiki: { + site: "mnwiki", + title: "\u0410\u0440\u0441\u043b\u0430\u043d", + badges: [], + url: "https://mn.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D0%B0%D0%BD", + }, + mrjwiki: { + site: "mrjwiki", + title: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", + badges: [], + url: "https://mrj.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD", + }, + mrwiki: { + site: "mrwiki", + title: "\u0938\u093f\u0902\u0939", + badges: [], + url: "https://mr.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9", + }, + mswiki: { + site: "mswiki", + title: "Singa", + badges: ["Q17437796"], + url: "https://ms.wikipedia.org/wiki/Singa", + }, + mtwiki: { + site: "mtwiki", + title: "Iljun", + badges: [], + url: "https://mt.wikipedia.org/wiki/Iljun", + }, + mywiki: { + site: "mywiki", + title: "\u1001\u103c\u1004\u103a\u1039\u101e\u1031\u1037", + badges: [], + url: "https://my.wikipedia.org/wiki/%E1%80%81%E1%80%BC%E1%80%84%E1%80%BA%E1%80%B9%E1%80%9E%E1%80%B1%E1%80%B7", + }, + newiki: { + site: "newiki", + title: "\u0938\u093f\u0902\u0939", + badges: [], + url: "https://ne.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9", + }, + newwiki: { + site: "newwiki", + title: "\u0938\u093f\u0902\u0939", + badges: [], + url: "https://new.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9", + }, + nlwiki: { + site: "nlwiki", + title: "Leeuw (dier)", + badges: [], + url: "https://nl.wikipedia.org/wiki/Leeuw_(dier)", + }, + nnwiki: { + site: "nnwiki", + title: "L\u00f8ve", + badges: [], + url: "https://nn.wikipedia.org/wiki/L%C3%B8ve", + }, + nowiki: { + site: "nowiki", + title: "L\u00f8ve", + badges: [], + url: "https://no.wikipedia.org/wiki/L%C3%B8ve", + }, + nrmwiki: { + site: "nrmwiki", + title: "Lion", + badges: [], + url: "https://nrm.wikipedia.org/wiki/Lion", + }, + nsowiki: { + site: "nsowiki", + title: "Tau", + badges: [], + url: "https://nso.wikipedia.org/wiki/Tau", + }, + nvwiki: { + site: "nvwiki", + title: "N\u00e1shd\u00f3\u00edtsoh bitsiij\u012f\u02bc dadit\u0142\u02bcoo\u00edg\u00ed\u00ed", + badges: [], + url: "https://nv.wikipedia.org/wiki/N%C3%A1shd%C3%B3%C3%ADtsoh_bitsiij%C4%AF%CA%BC_dadit%C5%82%CA%BCoo%C3%ADg%C3%AD%C3%AD", + }, + ocwiki: { + site: "ocwiki", + title: "Panthera leo", + badges: [], + url: "https://oc.wikipedia.org/wiki/Panthera_leo", + }, + orwiki: { + site: "orwiki", + title: "\u0b38\u0b3f\u0b02\u0b39", + badges: [], + url: "https://or.wikipedia.org/wiki/%E0%AC%B8%E0%AC%BF%E0%AC%82%E0%AC%B9", + }, + oswiki: { + site: "oswiki", + title: "\u0426\u043e\u043c\u0430\u0445\u044a", + badges: [], + url: "https://os.wikipedia.org/wiki/%D0%A6%D0%BE%D0%BC%D0%B0%D1%85%D1%8A", + }, + pamwiki: { + site: "pamwiki", + title: "Leon (animal)", + badges: ["Q17437796"], + url: "https://pam.wikipedia.org/wiki/Leon_(animal)", + }, + pawiki: { + site: "pawiki", + title: "\u0a2c\u0a71\u0a2c\u0a30 \u0a38\u0a3c\u0a47\u0a30", + badges: [], + url: "https://pa.wikipedia.org/wiki/%E0%A8%AC%E0%A9%B1%E0%A8%AC%E0%A8%B0_%E0%A8%B8%E0%A8%BC%E0%A9%87%E0%A8%B0", + }, + pcdwiki: { + site: "pcdwiki", + title: "Lion", + badges: [], + url: "https://pcd.wikipedia.org/wiki/Lion", + }, + plwiki: { + site: "plwiki", + title: "Lew afryka\u0144ski", + badges: ["Q17437796"], + url: "https://pl.wikipedia.org/wiki/Lew_afryka%C5%84ski", + }, + plwikiquote: { + site: "plwikiquote", + title: "Lew", + badges: [], + url: "https://pl.wikiquote.org/wiki/Lew", + }, + pmswiki: { + site: "pmswiki", + title: "Lion", + badges: [], + url: "https://pms.wikipedia.org/wiki/Lion", + }, + pnbwiki: { + site: "pnbwiki", + title: "\u0628\u0628\u0631 \u0634\u06cc\u0631", + badges: [], + url: "https://pnb.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%DB%8C%D8%B1", + }, + pswiki: { + site: "pswiki", + title: "\u0632\u0645\u0631\u06cc", + badges: [], + url: "https://ps.wikipedia.org/wiki/%D8%B2%D9%85%D8%B1%DB%8C", + }, + ptwiki: { + site: "ptwiki", + title: "Le\u00e3o", + badges: [], + url: "https://pt.wikipedia.org/wiki/Le%C3%A3o", + }, + ptwikiquote: { + site: "ptwikiquote", + title: "Le\u00e3o", + badges: [], + url: "https://pt.wikiquote.org/wiki/Le%C3%A3o", + }, + quwiki: { + site: "quwiki", + title: "Liyun", + badges: [], + url: "https://qu.wikipedia.org/wiki/Liyun", + }, + rmwiki: { + site: "rmwiki", + title: "Liun", + badges: [], + url: "https://rm.wikipedia.org/wiki/Liun", + }, + rnwiki: { + site: "rnwiki", + title: "Intare", + badges: [], + url: "https://rn.wikipedia.org/wiki/Intare", + }, + rowiki: { + site: "rowiki", + title: "Leu", + badges: [], + url: "https://ro.wikipedia.org/wiki/Leu", + }, + ruewiki: { + site: "ruewiki", + title: "\u041b\u0435\u0432", + badges: [], + url: "https://rue.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2", + }, + ruwiki: { + site: "ruwiki", + title: "\u041b\u0435\u0432", + badges: ["Q17437798"], + url: "https://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2", + }, + ruwikinews: { + site: "ruwikinews", + title: "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f:\u041b\u044c\u0432\u044b", + badges: [], + url: "https://ru.wikinews.org/wiki/%D0%9A%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F:%D0%9B%D1%8C%D0%B2%D1%8B", + }, + rwwiki: { + site: "rwwiki", + title: "Intare", + badges: [], + url: "https://rw.wikipedia.org/wiki/Intare", + }, + sahwiki: { + site: "sahwiki", + title: "\u0425\u0430\u0445\u0430\u0439", + badges: [], + url: "https://sah.wikipedia.org/wiki/%D0%A5%D0%B0%D1%85%D0%B0%D0%B9", + }, + satwiki: { + site: "satwiki", + title: "\u1c61\u1c5f\u1c74\u1c5f\u1c60\u1c69\u1c5e", + badges: [], + url: "https://sat.wikipedia.org/wiki/%E1%B1%A1%E1%B1%9F%E1%B1%B4%E1%B1%9F%E1%B1%A0%E1%B1%A9%E1%B1%9E", + }, + sawiki: { + site: "sawiki", + title: "\u0938\u093f\u0902\u0939\u0903 \u092a\u0936\u0941\u0903", + badges: [], + url: "https://sa.wikipedia.org/wiki/%E0%A4%B8%E0%A4%BF%E0%A4%82%E0%A4%B9%E0%A4%83_%E0%A4%AA%E0%A4%B6%E0%A5%81%E0%A4%83", + }, + scnwiki: { + site: "scnwiki", + title: "Panthera leo", + badges: [], + url: "https://scn.wikipedia.org/wiki/Panthera_leo", + }, + scowiki: { + site: "scowiki", + title: "Lion", + badges: ["Q17437796"], + url: "https://sco.wikipedia.org/wiki/Lion", + }, + sdwiki: { + site: "sdwiki", + title: "\u0628\u0628\u0631 \u0634\u064a\u0646\u0647\u0646", + badges: [], + url: "https://sd.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%D9%8A%D9%86%D9%87%D9%86", + }, + sewiki: { + site: "sewiki", + title: "Ledjon", + badges: [], + url: "https://se.wikipedia.org/wiki/Ledjon", + }, + shiwiki: { + site: "shiwiki", + title: "Agrzam", + badges: [], + url: "https://shi.wikipedia.org/wiki/Agrzam", + }, + shnwiki: { + site: "shnwiki", + title: "\u101e\u1062\u1004\u103a\u1087\u101e\u102e\u1088", + badges: [], + url: "https://shn.wikipedia.org/wiki/%E1%80%9E%E1%81%A2%E1%80%84%E1%80%BA%E1%82%87%E1%80%9E%E1%80%AE%E1%82%88", + }, + shwiki: { + site: "shwiki", + title: "Lav", + badges: [], + url: "https://sh.wikipedia.org/wiki/Lav", + }, + simplewiki: { + site: "simplewiki", + title: "Lion", + badges: [], + url: "https://simple.wikipedia.org/wiki/Lion", + }, + siwiki: { + site: "siwiki", + title: "\u0dc3\u0dd2\u0d82\u0dc4\u0dba\u0dcf", + badges: [], + url: "https://si.wikipedia.org/wiki/%E0%B7%83%E0%B7%92%E0%B6%82%E0%B7%84%E0%B6%BA%E0%B7%8F", + }, + skwiki: { + site: "skwiki", + title: "Lev p\u00fa\u0161\u0165ov\u00fd", + badges: [], + url: "https://sk.wikipedia.org/wiki/Lev_p%C3%BA%C5%A1%C5%A5ov%C3%BD", + }, + skwikiquote: { + site: "skwikiquote", + title: "Lev", + badges: [], + url: "https://sk.wikiquote.org/wiki/Lev", + }, + slwiki: { + site: "slwiki", + title: "Lev", + badges: [], + url: "https://sl.wikipedia.org/wiki/Lev", + }, + smwiki: { + site: "smwiki", + title: "Leona", + badges: [], + url: "https://sm.wikipedia.org/wiki/Leona", + }, + snwiki: { + site: "snwiki", + title: "Shumba", + badges: [], + url: "https://sn.wikipedia.org/wiki/Shumba", + }, + sowiki: { + site: "sowiki", + title: "Libaax", + badges: [], + url: "https://so.wikipedia.org/wiki/Libaax", + }, + specieswiki: { + site: "specieswiki", + title: "Panthera leo", + badges: [], + url: "https://species.wikimedia.org/wiki/Panthera_leo", + }, + sqwiki: { + site: "sqwiki", + title: "Luani", + badges: [], + url: "https://sq.wikipedia.org/wiki/Luani", + }, + srwiki: { + site: "srwiki", + title: "\u041b\u0430\u0432", + badges: [], + url: "https://sr.wikipedia.org/wiki/%D0%9B%D0%B0%D0%B2", + }, + sswiki: { + site: "sswiki", + title: "Libhubesi", + badges: [], + url: "https://ss.wikipedia.org/wiki/Libhubesi", + }, + stqwiki: { + site: "stqwiki", + title: "Leeuwe", + badges: [], + url: "https://stq.wikipedia.org/wiki/Leeuwe", + }, + stwiki: { + site: "stwiki", + title: "Tau", + badges: [], + url: "https://st.wikipedia.org/wiki/Tau", + }, + suwiki: { + site: "suwiki", + title: "Singa", + badges: [], + url: "https://su.wikipedia.org/wiki/Singa", + }, + svwiki: { + site: "svwiki", + title: "Lejon", + badges: [], + url: "https://sv.wikipedia.org/wiki/Lejon", + }, + swwiki: { + site: "swwiki", + title: "Simba", + badges: [], + url: "https://sw.wikipedia.org/wiki/Simba", + }, + szlwiki: { + site: "szlwiki", + title: "Lew", + badges: [], + url: "https://szl.wikipedia.org/wiki/Lew", + }, + tawiki: { + site: "tawiki", + title: "\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0bae\u0bcd", + badges: [], + url: "https://ta.wikipedia.org/wiki/%E0%AE%9A%E0%AE%BF%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%AE%E0%AF%8D", + }, + tcywiki: { + site: "tcywiki", + title: "\u0cb8\u0cbf\u0c82\u0cb9", + badges: [], + url: "https://tcy.wikipedia.org/wiki/%E0%B2%B8%E0%B2%BF%E0%B2%82%E0%B2%B9", + }, + tewiki: { + site: "tewiki", + title: "\u0c38\u0c3f\u0c02\u0c39\u0c02", + badges: [], + url: "https://te.wikipedia.org/wiki/%E0%B0%B8%E0%B0%BF%E0%B0%82%E0%B0%B9%E0%B0%82", + }, + tgwiki: { + site: "tgwiki", + title: "\u0428\u0435\u0440", + badges: [], + url: "https://tg.wikipedia.org/wiki/%D0%A8%D0%B5%D1%80", + }, + thwiki: { + site: "thwiki", + title: "\u0e2a\u0e34\u0e07\u0e42\u0e15", + badges: [], + url: "https://th.wikipedia.org/wiki/%E0%B8%AA%E0%B8%B4%E0%B8%87%E0%B9%82%E0%B8%95", + }, + tiwiki: { + site: "tiwiki", + title: "\u12a3\u1295\u1260\u1233", + badges: [], + url: "https://ti.wikipedia.org/wiki/%E1%8A%A3%E1%8A%95%E1%89%A0%E1%88%B3", + }, + tkwiki: { + site: "tkwiki", + title: "\u00ddolbars", + badges: [], + url: "https://tk.wikipedia.org/wiki/%C3%9Dolbars", + }, + tlwiki: { + site: "tlwiki", + title: "Leon", + badges: [], + url: "https://tl.wikipedia.org/wiki/Leon", + }, + trwiki: { + site: "trwiki", + title: "Aslan", + badges: [], + url: "https://tr.wikipedia.org/wiki/Aslan", + }, + ttwiki: { + site: "ttwiki", + title: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", + badges: [], + url: "https://tt.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD", + }, + tumwiki: { + site: "tumwiki", + title: "Nkhalamu", + badges: [], + url: "https://tum.wikipedia.org/wiki/Nkhalamu", + }, + udmwiki: { + site: "udmwiki", + title: "\u0410\u0440\u044b\u0441\u043b\u0430\u043d", + badges: [], + url: "https://udm.wikipedia.org/wiki/%D0%90%D1%80%D1%8B%D1%81%D0%BB%D0%B0%D0%BD", + }, + ugwiki: { + site: "ugwiki", + title: "\u0634\u0649\u0631", + badges: [], + url: "https://ug.wikipedia.org/wiki/%D8%B4%D9%89%D8%B1", + }, + ukwiki: { + site: "ukwiki", + title: "\u041b\u0435\u0432", + badges: [], + url: "https://uk.wikipedia.org/wiki/%D0%9B%D0%B5%D0%B2", + }, + ukwikiquote: { + site: "ukwikiquote", + title: "\u041b\u0435\u0432", + badges: [], + url: "https://uk.wikiquote.org/wiki/%D0%9B%D0%B5%D0%B2", + }, + urwiki: { + site: "urwiki", + title: "\u0628\u0628\u0631 \u0634\u06cc\u0631", + badges: ["Q17437796"], + url: "https://ur.wikipedia.org/wiki/%D8%A8%D8%A8%D8%B1_%D8%B4%DB%8C%D8%B1", + }, + uzwiki: { + site: "uzwiki", + title: "Arslon", + badges: [], + url: "https://uz.wikipedia.org/wiki/Arslon", + }, + vecwiki: { + site: "vecwiki", + title: "Leon", + badges: [], + url: "https://vec.wikipedia.org/wiki/Leon", + }, + vepwiki: { + site: "vepwiki", + title: "Lev", + badges: [], + url: "https://vep.wikipedia.org/wiki/Lev", + }, + viwiki: { + site: "viwiki", + title: "S\u01b0 t\u1eed", + badges: [], + url: "https://vi.wikipedia.org/wiki/S%C6%B0_t%E1%BB%AD", + }, + vlswiki: { + site: "vlswiki", + title: "L\u00eaeuw (b\u00eaeste)", + badges: [], + url: "https://vls.wikipedia.org/wiki/L%C3%AAeuw_(b%C3%AAeste)", + }, + warwiki: { + site: "warwiki", + title: "Leon", + badges: [], + url: "https://war.wikipedia.org/wiki/Leon", + }, + wowiki: { + site: "wowiki", + title: "Gaynde", + badges: [], + url: "https://wo.wikipedia.org/wiki/Gaynde", + }, + wuuwiki: { + site: "wuuwiki", + title: "\u72ee", + badges: [], + url: "https://wuu.wikipedia.org/wiki/%E7%8B%AE", + }, + xalwiki: { + site: "xalwiki", + title: "\u0410\u0440\u0441\u043b\u04a3", + badges: [], + url: "https://xal.wikipedia.org/wiki/%D0%90%D1%80%D1%81%D0%BB%D2%A3", + }, + xhwiki: { + site: "xhwiki", + title: "Ingonyama", + badges: [], + url: "https://xh.wikipedia.org/wiki/Ingonyama", + }, + xmfwiki: { + site: "xmfwiki", + title: "\u10dc\u10ef\u10d8\u10da\u10dd", + badges: [], + url: "https://xmf.wikipedia.org/wiki/%E1%83%9C%E1%83%AF%E1%83%98%E1%83%9A%E1%83%9D", + }, + yiwiki: { + site: "yiwiki", + title: "\u05dc\u05d9\u05d9\u05d1", + badges: [], + url: "https://yi.wikipedia.org/wiki/%D7%9C%D7%99%D7%99%D7%91", + }, + yowiki: { + site: "yowiki", + title: "K\u00ecn\u00ec\u00fan", + badges: [], + url: "https://yo.wikipedia.org/wiki/K%C3%ACn%C3%AC%C3%BAn", + }, + zawiki: { + site: "zawiki", + title: "Saeceij", + badges: [], + url: "https://za.wikipedia.org/wiki/Saeceij", + }, + zh_min_nanwiki: { + site: "zh_min_nanwiki", + title: "Sai", + badges: [], + url: "https://zh-min-nan.wikipedia.org/wiki/Sai", + }, + zh_yuewiki: { + site: "zh_yuewiki", + title: "\u7345\u5b50", + badges: ["Q17437796"], + url: "https://zh-yue.wikipedia.org/wiki/%E7%8D%85%E5%AD%90", + }, + zhwiki: { + site: "zhwiki", + title: "\u72ee", + badges: [], + url: "https://zh.wikipedia.org/wiki/%E7%8B%AE", + }, + zuwiki: { + site: "zuwiki", + title: "Ibhubesi", + badges: [], + url: "https://zu.wikipedia.org/wiki/Ibhubesi", + }, + }, + }, + }, } -Utils.injectJsonDownloadForTests( "https://www.wikidata.org/wiki/Special:EntityData/Q140.json", Q140) +Utils.injectJsonDownloadForTests("https://www.wikidata.org/wiki/Special:EntityData/Q140.json", Q140) const Q14517013 = { - "entities": { - "Q14517013": { - "pageid": 16187848, - "ns": 0, - "title": "Q14517013", - "lastrevid": 1408823680, - "modified": "2021-04-26T07:35:01Z", - "type": "item", - "id": "Q14517013", - "labels": { - "nl": {"language": "nl", "value": "Vredesmolen"}, - "en": {"language": "en", "value": "Peace Mill"} + entities: { + Q14517013: { + pageid: 16187848, + ns: 0, + title: "Q14517013", + lastrevid: 1408823680, + modified: "2021-04-26T07:35:01Z", + type: "item", + id: "Q14517013", + labels: { + nl: { language: "nl", value: "Vredesmolen" }, + en: { language: "en", value: "Peace Mill" }, }, - "descriptions": {"nl": {"language": "nl", "value": "molen in West-Vlaanderen"}}, - "aliases": {}, - "claims": { - "P625": [{ - "mainsnak": { - "snaktype": "value", - "property": "P625", - "hash": "d86538f14e8cca00bbf30fb029829aacbc6903a0", - "datavalue": { - "value": { - "latitude": 50.99444, - "longitude": 2.92528, - "altitude": null, - "precision": 0.0001, - "globe": "http://www.wikidata.org/entity/Q2" - }, "type": "globecoordinate" - }, - "datatype": "globe-coordinate" - }, - "type": "statement", - "id": "Q14517013$DBFBFD69-F54D-4C92-A7F4-A44F876E5776", - "rank": "normal", - "references": [{ - "hash": "732ec1c90a6f0694c7db9a71bf09fe7f2b674172", - "snaks": { - "P143": [{ - "snaktype": "value", - "property": "P143", - "hash": "9123b0de1cc9c3954366ba797d598e4e1ea4146f", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 10000, - "id": "Q10000" - }, "type": "wikibase-entityid" + descriptions: { nl: { language: "nl", value: "molen in West-Vlaanderen" } }, + aliases: {}, + claims: { + P625: [ + { + mainsnak: { + snaktype: "value", + property: "P625", + hash: "d86538f14e8cca00bbf30fb029829aacbc6903a0", + datavalue: { + value: { + latitude: 50.99444, + longitude: 2.92528, + altitude: null, + precision: 0.0001, + globe: "http://www.wikidata.org/entity/Q2", }, - "datatype": "wikibase-item" - }] + type: "globecoordinate", + }, + datatype: "globe-coordinate", }, - "snaks-order": ["P143"] - }] - }], - "P17": [{ - "mainsnak": { - "snaktype": "value", - "property": "P17", - "hash": "c2859f311753176d6bdfa7da54ceeeac7acb52c8", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 31, - "id": "Q31" - }, "type": "wikibase-entityid" + type: "statement", + id: "Q14517013$DBFBFD69-F54D-4C92-A7F4-A44F876E5776", + rank: "normal", + references: [ + { + hash: "732ec1c90a6f0694c7db9a71bf09fe7f2b674172", + snaks: { + P143: [ + { + snaktype: "value", + property: "P143", + hash: "9123b0de1cc9c3954366ba797d598e4e1ea4146f", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 10000, + id: "Q10000", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + ], + }, + "snaks-order": ["P143"], + }, + ], + }, + ], + P17: [ + { + mainsnak: { + snaktype: "value", + property: "P17", + hash: "c2859f311753176d6bdfa7da54ceeeac7acb52c8", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 31, + id: "Q31", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q14517013$C12E4DA5-44E1-41ED-BF3D-C84381246429", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$C12E4DA5-44E1-41ED-BF3D-C84381246429", - "rank": "normal" - }], - "P18": [{ - "mainsnak": { - "snaktype": "value", - "property": "P18", - "hash": "af765166ecaa7d01ea800812b5b356886b8849a0", - "datavalue": { - "value": "Klerken Vredesmolen R01.jpg", - "type": "string" + ], + P18: [ + { + mainsnak: { + snaktype: "value", + property: "P18", + hash: "af765166ecaa7d01ea800812b5b356886b8849a0", + datavalue: { + value: "Klerken Vredesmolen R01.jpg", + type: "string", + }, + datatype: "commonsMedia", }, - "datatype": "commonsMedia" + type: "statement", + id: "Q14517013$5291801E-11BE-4CE7-8F42-D0D6A120F390", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$5291801E-11BE-4CE7-8F42-D0D6A120F390", - "rank": "normal" - }], - "P2867": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2867", - "hash": "b1c627972ba2cc71e3567d2fb56cb5f90dd64007", - "datavalue": {"value": "893", "type": "string"}, - "datatype": "external-id" - }, - "type": "statement", - "id": "Q14517013$2aff9dcd-4d24-cd92-b5af-f6268425695f", - "rank": "normal" - }], - "P31": [{ - "mainsnak": { - "snaktype": "value", - "property": "P31", - "hash": "9b48263bb51c506553aac2281ae331353b5c9002", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 38720, - "id": "Q38720" - }, "type": "wikibase-entityid" + ], + P2867: [ + { + mainsnak: { + snaktype: "value", + property: "P2867", + hash: "b1c627972ba2cc71e3567d2fb56cb5f90dd64007", + datavalue: { value: "893", type: "string" }, + datatype: "external-id", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q14517013$2aff9dcd-4d24-cd92-b5af-f6268425695f", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$46dd9d89-4999-eee6-20a4-c4f6650b1d9c", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P31", - "hash": "a1d6f3409c57de0361c68263c9397a99dabe19ea", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 3851468, - "id": "Q3851468" - }, "type": "wikibase-entityid" + ], + P31: [ + { + mainsnak: { + snaktype: "value", + property: "P31", + hash: "9b48263bb51c506553aac2281ae331353b5c9002", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 38720, + id: "Q38720", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q14517013$46dd9d89-4999-eee6-20a4-c4f6650b1d9c", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$C83A8B1F-7798-493A-86C9-EC0EFEE356B3", - "rank": "normal" - }, { - "mainsnak": { - "snaktype": "value", - "property": "P31", - "hash": "ee5ba9185bdf9f0eb80b52e1cdc70c5883fac95a", - "datavalue": { - "value": { - "entity-type": "item", - "numeric-id": 623605, - "id": "Q623605" - }, "type": "wikibase-entityid" + { + mainsnak: { + snaktype: "value", + property: "P31", + hash: "a1d6f3409c57de0361c68263c9397a99dabe19ea", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 3851468, + id: "Q3851468", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", }, - "datatype": "wikibase-item" + type: "statement", + id: "Q14517013$C83A8B1F-7798-493A-86C9-EC0EFEE356B3", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$CF74DC2E-6814-4755-9BAD-6EE9FEF637DD", - "rank": "normal" - }], - "P2671": [{ - "mainsnak": { - "snaktype": "value", - "property": "P2671", - "hash": "83fb38a3c6407f7d0d7bb051d1c31cea8ae26975", - "datavalue": {"value": "/g/121cb15z", "type": "string"}, - "datatype": "external-id" + { + mainsnak: { + snaktype: "value", + property: "P31", + hash: "ee5ba9185bdf9f0eb80b52e1cdc70c5883fac95a", + datavalue: { + value: { + "entity-type": "item", + "numeric-id": 623605, + id: "Q623605", + }, + type: "wikibase-entityid", + }, + datatype: "wikibase-item", + }, + type: "statement", + id: "Q14517013$CF74DC2E-6814-4755-9BAD-6EE9FEF637DD", + rank: "normal", }, - "type": "statement", - "id": "Q14517013$E6FFEF32-0131-42FD-9C66-1A406B68059A", - "rank": "normal" - }] + ], + P2671: [ + { + mainsnak: { + snaktype: "value", + property: "P2671", + hash: "83fb38a3c6407f7d0d7bb051d1c31cea8ae26975", + datavalue: { value: "/g/121cb15z", type: "string" }, + datatype: "external-id", + }, + type: "statement", + id: "Q14517013$E6FFEF32-0131-42FD-9C66-1A406B68059A", + rank: "normal", + }, + ], }, - "sitelinks": { - "commonswiki": { - "site": "commonswiki", - "title": "Category:Vredesmolen, Klerken", - "badges": [], - "url": "https://commons.wikimedia.org/wiki/Category:Vredesmolen,_Klerken" + sitelinks: { + commonswiki: { + site: "commonswiki", + title: "Category:Vredesmolen, Klerken", + badges: [], + url: "https://commons.wikimedia.org/wiki/Category:Vredesmolen,_Klerken", }, - "nlwiki": { - "site": "nlwiki", - "title": "Vredesmolen", - "badges": [], - "url": "https://nl.wikipedia.org/wiki/Vredesmolen" - } - } - } - } + nlwiki: { + site: "nlwiki", + title: "Vredesmolen", + badges: [], + url: "https://nl.wikipedia.org/wiki/Vredesmolen", + }, + }, + }, + }, } -Utils.injectJsonDownloadForTests( "https://www.wikidata.org/wiki/Special:EntityData/Q14517013.json", Q14517013 ) +Utils.injectJsonDownloadForTests( + "https://www.wikidata.org/wiki/Special:EntityData/Q14517013.json", + Q14517013 +) -const L614072={ - "entities": { - "L614072": { - "pageid": 104085278, - "ns": 146, - "title": "Lexeme:L614072", - "lastrevid": 1509989280, - "modified": "2021-10-09T18:43:52Z", - "type": "lexeme", - "id": "L614072", - "lemmas": {"nl": {"language": "nl", "value": "Groen"}}, - "lexicalCategory": "Q34698", - "language": "Q7411", - "claims": {}, - "forms": [], - "senses": [{ - "id": "L614072-S1", - "glosses": {"nl": {"language": "nl", "value": "Nieuw"}}, - "claims": {} - }, { - "id": "L614072-S2", - "glosses": {"nl": {"language": "nl", "value": "Jong"}}, - "claims": {} - }, { - "id": "L614072-S3", - "glosses": {"nl": {"language": "nl", "value": "Pril"}}, - "claims": {} - }] - } - } +const L614072 = { + entities: { + L614072: { + pageid: 104085278, + ns: 146, + title: "Lexeme:L614072", + lastrevid: 1509989280, + modified: "2021-10-09T18:43:52Z", + type: "lexeme", + id: "L614072", + lemmas: { nl: { language: "nl", value: "Groen" } }, + lexicalCategory: "Q34698", + language: "Q7411", + claims: {}, + forms: [], + senses: [ + { + id: "L614072-S1", + glosses: { nl: { language: "nl", value: "Nieuw" } }, + claims: {}, + }, + { + id: "L614072-S2", + glosses: { nl: { language: "nl", value: "Jong" } }, + claims: {}, + }, + { + id: "L614072-S3", + glosses: { nl: { language: "nl", value: "Pril" } }, + claims: {}, + }, + ], + }, + }, } -Utils.injectJsonDownloadForTests( "https://www.wikidata.org/wiki/Special:EntityData/L614072.json",L614072) +Utils.injectJsonDownloadForTests( + "https://www.wikidata.org/wiki/Special:EntityData/L614072.json", + L614072 +) describe("Wikidata", () => { + it("should download Q140 (lion)", async () => { + const wikidata = await Wikidata.LoadWikidataEntryAsync("Q140") + expect(wikidata.claims.get("P18")).length(2) + }) + + it("should download wikidata", async () => { + const wdata = await Wikidata.LoadWikidataEntryAsync(14517013) + expect(wdata.wikisites).to.have.key("nl") + expect(wdata.wikisites.get("nl")).eq("Vredesmolen") + }) - it("should download Q140 (lion)", - async () => { - - const wikidata = await Wikidata.LoadWikidataEntryAsync("Q140") - expect(wikidata.claims.get("P18")).length(2) - } - ) - - it("should download wikidata", - async () => { - const wdata = await Wikidata.LoadWikidataEntryAsync(14517013) - expect(wdata.wikisites).to.have.key("nl") - expect(wdata.wikisites.get("nl")).eq("Vredesmolen") - }) - it("should download a lexeme", async () => { - - const response = await Wikidata.LoadWikidataEntryAsync("https://www.wikidata.org/wiki/Lexeme:L614072") + const response = await Wikidata.LoadWikidataEntryAsync( + "https://www.wikidata.org/wiki/Lexeme:L614072" + ) expect(response).not.undefined expect(response.labels).to.have.key("nl") expect(response.labels).to.contains("Groen") }) - }) diff --git a/test/Models/ThemeConfig/Conversion/CreateNoteImportLayer.spec.ts b/test/Models/ThemeConfig/Conversion/CreateNoteImportLayer.spec.ts index 6cf14a4ef..2c29b9a3d 100644 --- a/test/Models/ThemeConfig/Conversion/CreateNoteImportLayer.spec.ts +++ b/test/Models/ThemeConfig/Conversion/CreateNoteImportLayer.spec.ts @@ -1,32 +1,41 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../../../Utils"; -import {DesugaringContext} from "../../../../Models/ThemeConfig/Conversion/Conversion"; -import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson"; -import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import {PrepareLayer} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer"; -import * as bookcases from "../../../../assets/layers/public_bookcase/public_bookcase.json"; -import CreateNoteImportLayer from "../../../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../../../Utils" +import { DesugaringContext } from "../../../../Models/ThemeConfig/Conversion/Conversion" +import { LayerConfigJson } from "../../../../Models/ThemeConfig/Json/LayerConfigJson" +import { TagRenderingConfigJson } from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson" +import { PrepareLayer } from "../../../../Models/ThemeConfig/Conversion/PrepareLayer" +import * as bookcases from "../../../../assets/layers/public_bookcase/public_bookcase.json" +import CreateNoteImportLayer from "../../../../Models/ThemeConfig/Conversion/CreateNoteImportLayer" describe("CreateNoteImportLayer", () => { - - it("should generate a layerconfig", () => { - const desugaringState: DesugaringContext = { - sharedLayers: new Map<string, LayerConfigJson>(), - tagRenderings: new Map<string, TagRenderingConfigJson>() - - } - const layerPrepare = new PrepareLayer(desugaringState) - const layer = layerPrepare.convertStrict(bookcases, "ImportLayerGeneratorTest:Parse bookcases") - const generator = new CreateNoteImportLayer() - const generatedLayer: LayerConfigJson = generator.convertStrict(layer, "ImportLayerGeneratorTest: convert") - expect(generatedLayer.isShown["and"][1].or[0].and[0]).deep.equal("_tags~(^|.*;)amenity=public_bookcase($|;.*)") - expect(generatedLayer.minzoom <= layer.minzoom, "Zoomlevel is to high").true - let renderings = Utils.NoNull(Utils.NoNull(generatedLayer.tagRenderings - .map(tr => (<TagRenderingConfigJson>tr).render)) - .map(render => render["en"])) - expect(renderings.some(r => r.indexOf("import_button") > 0), "no import button found").true - - - }) + it("should generate a layerconfig", () => { + const desugaringState: DesugaringContext = { + sharedLayers: new Map<string, LayerConfigJson>(), + tagRenderings: new Map<string, TagRenderingConfigJson>(), + } + const layerPrepare = new PrepareLayer(desugaringState) + const layer = layerPrepare.convertStrict( + bookcases, + "ImportLayerGeneratorTest:Parse bookcases" + ) + const generator = new CreateNoteImportLayer() + const generatedLayer: LayerConfigJson = generator.convertStrict( + layer, + "ImportLayerGeneratorTest: convert" + ) + expect(generatedLayer.isShown["and"][1].or[0].and[0]).deep.equal( + "_tags~(^|.*;)amenity=public_bookcase($|;.*)" + ) + expect(generatedLayer.minzoom <= layer.minzoom, "Zoomlevel is to high").true + let renderings = Utils.NoNull( + Utils.NoNull( + generatedLayer.tagRenderings.map((tr) => (<TagRenderingConfigJson>tr).render) + ).map((render) => render["en"]) + ) + expect( + renderings.some((r) => r.indexOf("import_button") > 0), + "no import button found" + ).true + }) }) diff --git a/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts b/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts index 0fbe04cca..8ca660dd8 100644 --- a/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts +++ b/test/Models/ThemeConfig/Conversion/FixLegacyTheme.spec.ts @@ -1,153 +1,142 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig"; -import {FixLegacyTheme} from "../../../../Models/ThemeConfig/Conversion/LegacyJsonConvert"; - +import { describe } from "mocha" +import { expect } from "chai" +import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig" +import { FixLegacyTheme } from "../../../../Models/ThemeConfig/Conversion/LegacyJsonConvert" describe("FixLegacyTheme", () => { - it("should create a working theme config", () => { - const walking_node_theme = { - "id": "walkingnodenetworks", - "title": { - "en": "Walking node networks" - }, - "maintainer": "L'imaginaire", - "icon": "https://upload.wikimedia.org/wikipedia/commons/3/30/Man_walking_icon_1410105361.svg", - "description": { - "en": "This map shows walking node networks" - }, - "language": [ - "en" - ], - socialImage: "img.jpg", - "version": "2021-10-02", - "startLat": 51.1599, - "startLon": 3.34750, - "startZoom": 12, - "clustering": { - "maxZoom": 12 - }, - "layers": [ - { - "id": "node2node", - "name": { - "en": "node to node links" - }, - "source": { - "osmTags": { - "and": [ - "network=rwn", - "network:type=node_network" - ] - } - }, - "minzoom": 12, - "title": { - "render": { - "en": "node to node link" - }, - "mappings": [ - { - "if": "ref~*", - "then": { - "en": "node to node link <strong>{ref}</strong>" - } - } - ] - }, - "width": { - "render": "4" - }, - "color": { - "render": "#8b1e20" - }, - "tagRenderings": [ - { - "question": { - "en": "When was this node to node link last surveyed?" - }, - "render": { - "en": "This node to node link was last surveyed on {survey:date}" - }, - "freeform": { - "key": "survey:date", - "type": "date" - }, - "mappings": [ - { - "if": "survey:date:={_now:date}", - "then": "Surveyed today!" - } - ] - } - ] + const walking_node_theme = { + id: "walkingnodenetworks", + title: { + en: "Walking node networks", + }, + maintainer: "L'imaginaire", + icon: "https://upload.wikimedia.org/wikipedia/commons/3/30/Man_walking_icon_1410105361.svg", + description: { + en: "This map shows walking node networks", + }, + language: ["en"], + socialImage: "img.jpg", + version: "2021-10-02", + startLat: 51.1599, + startLon: 3.3475, + startZoom: 12, + clustering: { + maxZoom: 12, + }, + layers: [ + { + id: "node2node", + name: { + en: "node to node links", }, - { - "id": "node", - "name": { - "en": "nodes" + source: { + osmTags: { + and: ["network=rwn", "network:type=node_network"], }, - "source": { - "osmTags": "rwn_ref~*" + }, + minzoom: 12, + title: { + render: { + en: "node to node link", }, - "minzoom": 12, - "title": { - "render": { - "en": "walking node <strong>{rwn_ref}</strong>" - } - }, - "label": { - "mappings": [ + mappings: [ + { + if: "ref~*", + then: { + en: "node to node link <strong>{ref}</strong>", + }, + }, + ], + }, + width: { + render: "4", + }, + color: { + render: "#8b1e20", + }, + tagRenderings: [ + { + question: { + en: "When was this node to node link last surveyed?", + }, + render: { + en: "This node to node link was last surveyed on {survey:date}", + }, + freeform: { + key: "survey:date", + type: "date", + }, + mappings: [ { - "if": "rwn_ref~*", - "then": "<div style='position: absolute; top: 10px; right: 10px; color: white; background-color: #8b1e20; width: 20px; height: 20px; border-radius: 100%'>{rwn_ref}</div>" - } - ] + if: "survey:date:={_now:date}", + then: "Surveyed today!", + }, + ], }, - "tagRenderings": [ + ], + }, + { + id: "node", + name: { + en: "nodes", + }, + source: { + osmTags: "rwn_ref~*", + }, + minzoom: 12, + title: { + render: { + en: "walking node <strong>{rwn_ref}</strong>", + }, + }, + label: { + mappings: [ { - "question": { - "en": "When was this walking node last surveyed?" - }, - "render": { - "en": "This walking node was last surveyed on {survey:date}" - }, - "freeform": { - "key": "survey:date", - "type": "date" - }, - "mappings": [ - { - "if": "survey:date:={_now:date}", - "then": "Surveyed today!" - } - ] + if: "rwn_ref~*", + then: "<div style='position: absolute; top: 10px; right: 10px; color: white; background-color: #8b1e20; width: 20px; height: 20px; border-radius: 100%'>{rwn_ref}</div>", }, - { - "question": { - "en": "How many other walking nodes does this node link to?" - }, - "render": { - "en": "This node links to {expected_rwn_route_relations} other walking nodes." - }, - "freeform": { - "key": "expected_rwn_route_relations", - "type": "int" - } + ], + }, + tagRenderings: [ + { + question: { + en: "When was this walking node last surveyed?", }, - "images" - ] - } - ] - } - const fixed = new FixLegacyTheme().convert( - <any> walking_node_theme, - "While testing") - expect(fixed.errors, "Could not fix the legacy theme").empty - const theme = new LayoutConfig(fixed.result, false) - expect(theme).not.undefined - + render: { + en: "This walking node was last surveyed on {survey:date}", + }, + freeform: { + key: "survey:date", + type: "date", + }, + mappings: [ + { + if: "survey:date:={_now:date}", + then: "Surveyed today!", + }, + ], + }, + { + question: { + en: "How many other walking nodes does this node link to?", + }, + render: { + en: "This node links to {expected_rwn_route_relations} other walking nodes.", + }, + freeform: { + key: "expected_rwn_route_relations", + type: "int", + }, + }, + "images", + ], + }, + ], + } + const fixed = new FixLegacyTheme().convert(<any>walking_node_theme, "While testing") + expect(fixed.errors, "Could not fix the legacy theme").empty + const theme = new LayoutConfig(fixed.result, false) + expect(theme).not.undefined }) }) - diff --git a/test/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts b/test/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts index 116006a62..1dcc695d3 100644 --- a/test/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts +++ b/test/Models/ThemeConfig/Conversion/PrepareLayer.spec.ts @@ -1,24 +1,24 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson"; -import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import LineRenderingConfigJson from "../../../../Models/ThemeConfig/Json/LineRenderingConfigJson"; -import {ExpandRewrite, PrepareLayer, RewriteSpecial} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer"; +import { describe } from "mocha" +import { expect } from "chai" +import { LayerConfigJson } from "../../../../Models/ThemeConfig/Json/LayerConfigJson" +import { TagRenderingConfigJson } from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson" +import LineRenderingConfigJson from "../../../../Models/ThemeConfig/Json/LineRenderingConfigJson" import { - QuestionableTagRenderingConfigJson -} from "../../../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; -import RewritableConfigJson from "../../../../Models/ThemeConfig/Json/RewritableConfigJson"; - + ExpandRewrite, + PrepareLayer, + RewriteSpecial, +} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer" +import { QuestionableTagRenderingConfigJson } from "../../../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import RewritableConfigJson from "../../../../Models/ThemeConfig/Json/RewritableConfigJson" describe("ExpandRewrite", () => { - it("should not allow overlapping keys", () => { const spec = <RewritableConfigJson<string>>{ rewrite: { sourceString: ["xyz", "longer_xyz"], into: [["a", "b"], ["A, B"]], }, - renderings: "The value of xyz is longer_xyz" + renderings: "The value of xyz is longer_xyz", } const rewrite = new ExpandRewrite() expect(() => rewrite.convert(spec, "test")).to.throw @@ -26,105 +26,111 @@ describe("ExpandRewrite", () => { }) describe("PrepareLayer", () => { - it("should expand rewrites in map renderings", () => { const exampleLayer: LayerConfigJson = { id: "testlayer", source: { - osmTags: "key=value" + osmTags: "key=value", }, mapRendering: [ { - "rewrite": { + rewrite: { sourceString: ["left|right", "lr_offset"], into: [ ["left", -6], - [ "right", +6], - ] + ["right", +6], + ], }, renderings: <LineRenderingConfigJson>{ - "color": { - "render": "#888", - "mappings": [ + color: { + render: "#888", + mappings: [ { - "if": "parking:condition:left|right=free", - "then": "#299921" + if: "parking:condition:left|right=free", + then: "#299921", }, { - "if": "parking:condition:left|right=disc", - "then": "#219991" - } - ] + if: "parking:condition:left|right=disc", + then: "#219991", + }, + ], }, - "offset": "lr_offset" - } - } - ] + offset: "lr_offset", + }, + }, + ], } const prep = new PrepareLayer({ tagRenderings: new Map<string, TagRenderingConfigJson>(), - sharedLayers: new Map<string, LayerConfigJson>() + sharedLayers: new Map<string, LayerConfigJson>(), }) const result = prep.convertStrict(exampleLayer, "test") const expected = { - "id": "testlayer", - "source": {"osmTags": "key=value"}, - "mapRendering": [{ - "color": { - "render": "#888", - "mappings": [{ - "if": "parking:condition:left=free", - "then": "#299921" + id: "testlayer", + source: { osmTags: "key=value" }, + mapRendering: [ + { + color: { + render: "#888", + mappings: [ + { + if: "parking:condition:left=free", + then: "#299921", + }, + { + if: "parking:condition:left=disc", + then: "#219991", + }, + ], }, - { - "if": "parking:condition:left=disc", - "then": "#219991" - }] + offset: -6, }, - "offset": -6 - }, { - "color": { - "render": "#888", - "mappings": [{ - "if": "parking:condition:right=free", - "then": "#299921" + { + color: { + render: "#888", + mappings: [ + { + if: "parking:condition:right=free", + then: "#299921", + }, + { + if: "parking:condition:right=disc", + then: "#219991", + }, + ], }, - { - "if": "parking:condition:right=disc", - "then": "#219991" - }] + offset: 6, }, - "offset": 6 - }], - "titleIcons": [{"render": "defaults", "id": "defaults"}] + ], + titleIcons: [{ render: "defaults", id: "defaults" }], } - expect(result).deep.eq(expected) }) }) -describe('RewriteSpecial', function () { +describe("RewriteSpecial", function () { it("should rewrite the UK import button", () => { const tr = <QuestionableTagRenderingConfigJson>{ - "id": "uk_addresses_import_button", - "render": { - "special": { - "type": "import_button", - "targetLayer": "address", - "tags": "urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$", - "text": "Add this address", - "icon": "./assets/themes/uk_addresses/housenumber_add.svg", - "location_picker": "none" - } - } + id: "uk_addresses_import_button", + render: { + special: { + type: "import_button", + targetLayer: "address", + tags: "urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$", + text: "Add this address", + icon: "./assets/themes/uk_addresses/housenumber_add.svg", + location_picker: "none", + }, + }, } const r = new RewriteSpecial().convert(tr, "test").result expect(r).to.deep.eq({ - "id": "uk_addresses_import_button", - "render": {'*': "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$,Add this address,./assets/themes/uk_addresses/housenumber_add.svg,,,,none,)}"} + id: "uk_addresses_import_button", + render: { + "*": "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$,Add this address,./assets/themes/uk_addresses/housenumber_add.svg,,,,none,)}", + }, }) }) -}); - +}) diff --git a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts index 9ef2996b7..8dbc989a1 100644 --- a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts +++ b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts @@ -1,21 +1,19 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {LayoutConfigJson} from "../../../../Models/ThemeConfig/Json/LayoutConfigJson"; -import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson"; -import {PrepareTheme} from "../../../../Models/ThemeConfig/Conversion/PrepareTheme"; -import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig"; +import { describe } from "mocha" +import { expect } from "chai" +import { LayoutConfigJson } from "../../../../Models/ThemeConfig/Json/LayoutConfigJson" +import { LayerConfigJson } from "../../../../Models/ThemeConfig/Json/LayerConfigJson" +import { PrepareTheme } from "../../../../Models/ThemeConfig/Conversion/PrepareTheme" +import { TagRenderingConfigJson } from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson" +import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig" import * as bookcaseLayer from "../../../../assets/generated/layers/public_bookcase.json" -import LayerConfig from "../../../../Models/ThemeConfig/LayerConfig"; -import {ExtractImages} from "../../../../Models/ThemeConfig/Conversion/FixImages"; +import LayerConfig from "../../../../Models/ThemeConfig/LayerConfig" +import { ExtractImages } from "../../../../Models/ThemeConfig/Conversion/FixImages" import * as cyclofix from "../../../../assets/generated/themes/cyclofix.json" -import {Tag} from "../../../../Logic/Tags/Tag"; -import {DesugaringContext} from "../../../../Models/ThemeConfig/Conversion/Conversion"; -import {And} from "../../../../Logic/Tags/And"; - +import { Tag } from "../../../../Logic/Tags/Tag" +import { DesugaringContext } from "../../../../Models/ThemeConfig/Conversion/Conversion" +import { And } from "../../../../Logic/Tags/And" const themeConfigJson: LayoutConfigJson = { - description: "Descr", icon: "", layers: [ @@ -23,137 +21,144 @@ const themeConfigJson: LayoutConfigJson = { builtin: "public_bookcase", override: { source: { - geoJson: "xyz" - } - } - } + geoJson: "xyz", + }, + }, + }, ], startLat: 0, startLon: 0, startZoom: 0, title: { - en: "Title" + en: "Title", }, - id: "test" + id: "test", } describe("PrepareTheme", () => { - it("should substitute layers", () => { - const sharedLayers = new Map<string, LayerConfigJson>() sharedLayers.set("public_bookcase", bookcaseLayer["default"]) - const theme ={...themeConfigJson, layers: ["public_bookcase"]} - const prepareStep = new PrepareTheme({ + const theme = { ...themeConfigJson, layers: ["public_bookcase"] } + const prepareStep = new PrepareTheme({ tagRenderings: new Map<string, TagRenderingConfigJson>(), - sharedLayers: sharedLayers + sharedLayers: sharedLayers, }) let themeConfigJsonPrepared = prepareStep.convert(theme, "test").result - const themeConfig = new LayoutConfig(themeConfigJsonPrepared); - const layerUnderTest = <LayerConfig> themeConfig.layers.find(l => l.id === "public_bookcase") - expect(layerUnderTest.source.osmTags).deep.eq(new And([new Tag("amenity","public_bookcase")])) - + const themeConfig = new LayoutConfig(themeConfigJsonPrepared) + const layerUnderTest = <LayerConfig>( + themeConfig.layers.find((l) => l.id === "public_bookcase") + ) + expect(layerUnderTest.source.osmTags).deep.eq( + new And([new Tag("amenity", "public_bookcase")]) + ) }) - - it("should apply override", () => { + it("should apply override", () => { const sharedLayers = new Map<string, LayerConfigJson>() sharedLayers.set("public_bookcase", bookcaseLayer["default"]) let themeConfigJsonPrepared = new PrepareTheme({ tagRenderings: new Map<string, TagRenderingConfigJson>(), - sharedLayers: sharedLayers - }).convert( themeConfigJson, "test").result - const themeConfig = new LayoutConfig(themeConfigJsonPrepared); - const layerUnderTest = <LayerConfig> themeConfig.layers.find(l => l.id === "public_bookcase") + sharedLayers: sharedLayers, + }).convert(themeConfigJson, "test").result + const themeConfig = new LayoutConfig(themeConfigJsonPrepared) + const layerUnderTest = <LayerConfig>( + themeConfig.layers.find((l) => l.id === "public_bookcase") + ) expect(layerUnderTest.source.geojsonSource).eq("xyz") - }) it("should apply override", () => { - const sharedLayers = new Map<string, LayerConfigJson>() sharedLayers.set("public_bookcase", bookcaseLayer["default"]) let themeConfigJsonPrepared = new PrepareTheme({ tagRenderings: new Map<string, TagRenderingConfigJson>(), - sharedLayers: sharedLayers - }).convert({...themeConfigJson, overrideAll: {source: {geoJson: "https://example.com/data.geojson"}}}, "test").result - const themeConfig = new LayoutConfig(themeConfigJsonPrepared); - const layerUnderTest = <LayerConfig> themeConfig.layers.find(l => l.id === "public_bookcase") + sharedLayers: sharedLayers, + }).convert( + { + ...themeConfigJson, + overrideAll: { source: { geoJson: "https://example.com/data.geojson" } }, + }, + "test" + ).result + const themeConfig = new LayoutConfig(themeConfigJsonPrepared) + const layerUnderTest = <LayerConfig>( + themeConfig.layers.find((l) => l.id === "public_bookcase") + ) expect(layerUnderTest.source.geojsonSource).eq("https://example.com/data.geojson") }) - + it("should remove names which are overriden with null", () => { const testLayer: LayerConfigJson = { source: { - osmTags: "x=y" + osmTags: "x=y", }, id: "layer-example", name: { - en: "Test layer - please ignore" + en: "Test layer - please ignore", }, - titleIcons : [], - mapRendering: null + titleIcons: [], + mapRendering: null, + } + const ctx: DesugaringContext = { + sharedLayers: new Map<string, LayerConfigJson>([["layer-example", testLayer]]), + tagRenderings: new Map<string, TagRenderingConfigJson>(), + } + const layout: LayoutConfigJson = { + description: "A testing theme", + icon: "", + id: "", + layers: [ + "layer-example", + { + builtin: "layer-example", + override: { + name: null, + minzoom: 18, + }, + }, + ], + startLat: 0, + startLon: 0, + startZoom: 0, + title: "Test theme", } - const ctx: DesugaringContext = { - sharedLayers: new Map<string, LayerConfigJson>([[ - "layer-example", testLayer - ]]), - tagRenderings: new Map<string, TagRenderingConfigJson>() - - } - const layout : LayoutConfigJson= - { - description: "A testing theme", - icon: "", - id: "", - layers: [ - "layer-example", - { - "builtin": "layer-example", - "override": { - "name": null, - "minzoom": 18 - } - } - ], - startLat: 0, - startLon: 0, - startZoom: 0, - title: "Test theme", - } const rewritten = new PrepareTheme(ctx, { - skipDefaultLayers: true + skipDefaultLayers: true, }).convertStrict(layout, "test") expect(rewritten.layers[0]).deep.eq(testLayer) expect(rewritten.layers[1]).deep.eq({ source: { - osmTags: "x=y" + osmTags: "x=y", }, id: "layer-example", - name:null, + name: null, minzoom: 18, mapRendering: null, - titleIcons: [] + titleIcons: [], }) }) }) describe("ExtractImages", () => { it("should find all images in a themefile", () => { - const images = new Set(new ExtractImages(true, new Map<string, any>()).convertStrict(<any> cyclofix, "test")) - const expectedValues = [ - './assets/layers/bike_repair_station/repair_station.svg', - './assets/layers/bike_repair_station/repair_station_pump.svg', - './assets/layers/bike_repair_station/broken_pump.svg', - './assets/layers/bike_repair_station/pump.svg', - './assets/themes/cyclofix/fietsambassade_gent_logo_small.svg', - './assets/layers/bike_repair_station/pump_example_manual.jpg', - './assets/layers/bike_repair_station/pump_example.png', - './assets/layers/bike_repair_station/pump_example_round.jpg', - './assets/layers/bike_repair_station/repair_station_example_2.jpg', - 'close'] - for (const expected of expectedValues) { - expect(images).contains(expected) - } + const images = new Set( + new ExtractImages(true, new Map<string, any>()).convertStrict(<any>cyclofix, "test") + ) + const expectedValues = [ + "./assets/layers/bike_repair_station/repair_station.svg", + "./assets/layers/bike_repair_station/repair_station_pump.svg", + "./assets/layers/bike_repair_station/broken_pump.svg", + "./assets/layers/bike_repair_station/pump.svg", + "./assets/themes/cyclofix/fietsambassade_gent_logo_small.svg", + "./assets/layers/bike_repair_station/pump_example_manual.jpg", + "./assets/layers/bike_repair_station/pump_example.png", + "./assets/layers/bike_repair_station/pump_example_round.jpg", + "./assets/layers/bike_repair_station/repair_station_example_2.jpg", + "close", + ] + for (const expected of expectedValues) { + expect(images).contains(expected) + } }) -}) \ No newline at end of file +}) diff --git a/test/Models/ThemeConfig/SourceConfig.spec.ts b/test/Models/ThemeConfig/SourceConfig.spec.ts index 97c590e87..e5b2601ad 100644 --- a/test/Models/ThemeConfig/SourceConfig.spec.ts +++ b/test/Models/ThemeConfig/SourceConfig.spec.ts @@ -1,18 +1,18 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import SourceConfig from "../../../Models/ThemeConfig/SourceConfig"; -import {TagUtils} from "../../../Logic/Tags/TagUtils"; +import { describe } from "mocha" +import { expect } from "chai" +import SourceConfig from "../../../Models/ThemeConfig/SourceConfig" +import { TagUtils } from "../../../Logic/Tags/TagUtils" describe("SourceConfig", () => { - it("should throw an error on conflicting tags", () => { expect(() => { new SourceConfig( { osmTags: TagUtils.Tag({ - and: ["x=y", "a=b", "x!=y"] - }) - }, false + and: ["x=y", "a=b", "x!=y"], + }), + }, + false ) }).to.throw(/tags are conflicting/) }) diff --git a/test/Models/ThemeConfig/TagRenderingConfig.spec.ts b/test/Models/ThemeConfig/TagRenderingConfig.spec.ts index aa38aaf87..9e4cd3ad0 100644 --- a/test/Models/ThemeConfig/TagRenderingConfig.spec.ts +++ b/test/Models/ThemeConfig/TagRenderingConfig.spec.ts @@ -1,71 +1,70 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; -import Locale from "../../../UI/i18n/Locale"; +import { describe } from "mocha" +import { expect } from "chai" +import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" +import Locale from "../../../UI/i18n/Locale" describe("TagRenderingConfig", () => { - describe("isKnown", () => { - it("should give correct render values", () => { - Locale.language.setData("nl"); - const tr = new TagRenderingConfig({ - render: ({"en": "Name is {name}", "nl": "Ook een {name}"} as any), - question: "Wat is de naam van dit object?", - freeform: { - key: "name", + Locale.language.setData("nl") + const tr = new TagRenderingConfig( + { + render: { en: "Name is {name}", nl: "Ook een {name}" } as any, + question: "Wat is de naam van dit object?", + freeform: { + key: "name", + }, + + mappings: [ + { + if: "noname=yes", + then: "Has no name", + }, + ], + condition: "x=", }, + "Tests" + ) - mappings: [ - { - if: "noname=yes", - "then": "Has no name" - } - ], - condition: "x=" - }, "Tests"); - - expect(tr.GetRenderValue({"foo": "bar"})).undefined - - expect (tr.GetRenderValue({"noname": "yes"})?.textFor("nl")).eq("Has no name") - expect( tr.GetRenderValue({"name": "xyz"})?.textFor("nl")).eq("Ook een {name}") - expect( tr.GetRenderValue({"foo": "bar"})).undefined + expect(tr.GetRenderValue({ foo: "bar" })).undefined + expect(tr.GetRenderValue({ noname: "yes" })?.textFor("nl")).eq("Has no name") + expect(tr.GetRenderValue({ name: "xyz" })?.textFor("nl")).eq("Ook een {name}") + expect(tr.GetRenderValue({ foo: "bar" })).undefined }) - + it("should give a correct indication", () => { // tests a regression in parsing const config = { "#": "Bottle refill", - "question": { - "en": "How easy is it to fill water bottles?", - "nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?", - "de": "Wie einfach ist es, Wasserflaschen zu füllen?" + question: { + en: "How easy is it to fill water bottles?", + nl: "Hoe gemakkelijk is het om drinkbussen bij te vullen?", + de: "Wie einfach ist es, Wasserflaschen zu füllen?", }, - "mappings": [ + mappings: [ { - "if": "bottle=yes", - "then": { - "en": "It is easy to refill water bottles", - "nl": "Een drinkbus bijvullen gaat makkelijk", - "de": "Es ist einfach, Wasserflaschen nachzufüllen" - } + if: "bottle=yes", + then: { + en: "It is easy to refill water bottles", + nl: "Een drinkbus bijvullen gaat makkelijk", + de: "Es ist einfach, Wasserflaschen nachzufüllen", + }, }, { - "if": "bottle=no", - "then": { - "en": "Water bottles may not fit", - "nl": "Een drinkbus past moeilijk", - "de": "Wasserflaschen passen möglicherweise nicht" - } - } - ] - }; + if: "bottle=no", + then: { + en: "Water bottles may not fit", + nl: "Een drinkbus past moeilijk", + de: "Wasserflaschen passen möglicherweise nicht", + }, + }, + ], + } - const tagRendering = new TagRenderingConfig(config, "test"); - expect(tagRendering.IsKnown({bottle: "yes"})).true + const tagRendering = new TagRenderingConfig(config, "test") + expect(tagRendering.IsKnown({ bottle: "yes" })).true expect(tagRendering.IsKnown({})).false }) }) }) - diff --git a/test/Models/Units.spec.ts b/test/Models/Units.spec.ts index e5045acb0..1490ba6a1 100644 --- a/test/Models/Units.spec.ts +++ b/test/Models/Units.spec.ts @@ -1,29 +1,27 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Unit} from "../../Models/Unit"; -import {Denomination} from "../../Models/Denomination"; +import { describe } from "mocha" +import { expect } from "chai" +import { Unit } from "../../Models/Unit" +import { Denomination } from "../../Models/Denomination" describe("Unit", () => { - - it("should convert a value back and forth", () => { - - const denomintion = new Denomination({ - "canonicalDenomination": "MW", - "alternativeDenomination": ["megawatts", "megawatt"], - "human": { - "en": " megawatts", - "nl": " megawatt" + it("should convert a value back and forth", () => { + const denomintion = new Denomination( + { + canonicalDenomination: "MW", + alternativeDenomination: ["megawatts", "megawatt"], + human: { + en: " megawatts", + nl: " megawatt", }, - }, "test"); + }, + "test" + ) - const canonical = denomintion.canonicalValue("5", true) - expect(canonical).eq( "5 MW") - const units = new Unit(["key"], [denomintion], false) - const [detected, detectedDenom] = units.findDenomination("5 MW", () => "be") - expect(detected).eq( "5") - expect(detectedDenom).eq( denomintion) - } - ) + const canonical = denomintion.canonicalValue("5", true) + expect(canonical).eq("5 MW") + const units = new Unit(["key"], [denomintion], false) + const [detected, detectedDenom] = units.findDenomination("5 MW", () => "be") + expect(detected).eq("5") + expect(detectedDenom).eq(denomintion) + }) }) - - diff --git a/test/UI/Popup/TagRenderingQuestion.spec.ts b/test/UI/Popup/TagRenderingQuestion.spec.ts index c4bdd453f..db3a3dfe6 100644 --- a/test/UI/Popup/TagRenderingQuestion.spec.ts +++ b/test/UI/Popup/TagRenderingQuestion.spec.ts @@ -1,15 +1,13 @@ -import {describe} from 'mocha' -import {TagRenderingConfigJson} from "../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; -import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; -import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion"; -import {UIEventSource} from "../../../Logic/UIEventSource"; -import { expect } from 'chai'; -import Locale from "../../../UI/i18n/Locale"; +import { describe } from "mocha" +import { TagRenderingConfigJson } from "../../../Models/ThemeConfig/Json/TagRenderingConfigJson" +import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" +import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion" +import { UIEventSource } from "../../../Logic/UIEventSource" +import { expect } from "chai" +import Locale from "../../../UI/i18n/Locale" describe("TagRenderingQuestion", () => { - it("should have a freeform text field with the user defined placeholder", () => { - const configJson = <TagRenderingConfigJson>{ id: "test-tag-rendering", question: "Question?", @@ -17,17 +15,18 @@ describe("TagRenderingQuestion", () => { freeform: { key: "capacity", type: "pnat", - placeholder: "Some user defined placeholder" - } + placeholder: "Some user defined placeholder", + }, } const config = new TagRenderingConfig(configJson, "test") - const ui = new TagRenderingQuestion( new UIEventSource<any>({}), config) + const ui = new TagRenderingQuestion(new UIEventSource<any>({}), config) const html = ui.ConstructElement() - expect(html.getElementsByTagName("input")[0]["placeholder"]).eq("Some user defined placeholder") + expect(html.getElementsByTagName("input")[0]["placeholder"]).eq( + "Some user defined placeholder" + ) }) it("should have a freeform text field with a type explanation", () => { - Locale.language.setData("en") const configJson = <TagRenderingConfigJson>{ id: "test-tag-rendering", @@ -36,12 +35,13 @@ describe("TagRenderingQuestion", () => { freeform: { key: "capacity", type: "pnat", - } + }, } const config = new TagRenderingConfig(configJson, "test") - const ui = new TagRenderingQuestion( new UIEventSource<any>({}), config) + const ui = new TagRenderingQuestion(new UIEventSource<any>({}), config) const html = ui.ConstructElement() - expect(html.getElementsByTagName("input")[0]["placeholder"]) - .eq("capacity (a positive, whole number)") + expect(html.getElementsByTagName("input")[0]["placeholder"]).eq( + "capacity (a positive, whole number)" + ) }) }) diff --git a/test/UI/SpecialVisualisations.spec.ts b/test/UI/SpecialVisualisations.spec.ts index b68339309..e5ac1c4a8 100644 --- a/test/UI/SpecialVisualisations.spec.ts +++ b/test/UI/SpecialVisualisations.spec.ts @@ -1,19 +1,23 @@ -import {describe} from 'mocha' -import SpecialVisualizations from "../../UI/SpecialVisualizations"; -import {expect} from "chai"; +import { describe } from "mocha" +import SpecialVisualizations from "../../UI/SpecialVisualizations" +import { expect } from "chai" describe("SpecialVisualisations", () => { - describe("predifined special visualisations", () => { it("should not have an argument called 'type'", () => { const specials = SpecialVisualizations.specialVisualizations for (const special of specials) { - expect(special.funcName).not.eq('type', "A special visualisation is not allowed to be named 'type', as this will conflict with the 'special'-blocks") + expect(special.funcName).not.eq( + "type", + "A special visualisation is not allowed to be named 'type', as this will conflict with the 'special'-blocks" + ) for (const arg of special.args) { - expect(arg.name).not.eq('type', "An argument is not allowed to be called 'type', as this will conflict with the 'special'-blocks") + expect(arg.name).not.eq( + "type", + "An argument is not allowed to be called 'type', as this will conflict with the 'special'-blocks" + ) } - } }) }) -}) \ No newline at end of file +}) diff --git a/test/UI/ValidatedTextFieldTranslations.ts b/test/UI/ValidatedTextFieldTranslations.ts index 48234d2cd..489a3a521 100644 --- a/test/UI/ValidatedTextFieldTranslations.ts +++ b/test/UI/ValidatedTextFieldTranslations.ts @@ -1,17 +1,20 @@ -import {describe} from 'mocha' -import ValidatedTextField from "../../UI/Input/ValidatedTextField"; -import {fail} from "assert"; -import Translations from "../../UI/i18n/Translations"; +import { describe } from "mocha" +import ValidatedTextField from "../../UI/Input/ValidatedTextField" +import { fail } from "assert" +import Translations from "../../UI/i18n/Translations" describe("ValidatedTextFields", () => { - it("should all have description in the translations", () => { - const ts = Translations.t.validation; + const ts = Translations.t.validation const missingTranslations = Array.from(ValidatedTextField.allTypes.keys()) - .filter(key => ts[key] === undefined || ts[key].description === undefined) - .filter(key => key !== "distance") + .filter((key) => ts[key] === undefined || ts[key].description === undefined) + .filter((key) => key !== "distance") if (missingTranslations.length > 0) { - fail("The validated text fields don't have a description defined in en.json for "+missingTranslations.join(", ")+". (Did you just add one? Run `npm run generate:translations`)") + fail( + "The validated text fields don't have a description defined in en.json for " + + missingTranslations.join(", ") + + ". (Did you just add one? Run `npm run generate:translations`)" + ) } }) }) diff --git a/test/Utils.MinifyJson.spec.ts b/test/Utils.MinifyJson.spec.ts index e86d442e6..4e39574dc 100644 --- a/test/Utils.MinifyJson.spec.ts +++ b/test/Utils.MinifyJson.spec.ts @@ -1,75 +1,60 @@ -import {Utils} from "../Utils"; -import LZString from "lz-string"; -import {describe} from 'mocha' -import {expect} from 'chai' +import { Utils } from "../Utils" +import LZString from "lz-string" +import { describe } from "mocha" +import { expect } from "chai" const example = { - "id": "bookcases", - "maintainer": "MapComplete", - "version": "2020-08-29", - "language": [ - "en", - "nl", - "de", - "fr" - ], - "title": { - "en": "Open Bookcase Map", - "nl": "Open Boekenruilkastenkaart", - "de": "Öffentliche Bücherschränke Karte", - "fr": "Carte des microbibliothèques" + id: "bookcases", + maintainer: "MapComplete", + version: "2020-08-29", + language: ["en", "nl", "de", "fr"], + title: { + en: "Open Bookcase Map", + nl: "Open Boekenruilkastenkaart", + de: "Öffentliche Bücherschränke Karte", + fr: "Carte des microbibliothèques", }, - "description": { - "en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.", - "nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren", - "de": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen.", - "fr": "Une microbibliothèques, également appelée boite à livre, est un élément de mobilier urbain (étagère, armoire, etc) dans lequel sont stockés des livres et autres objets en accès libre. Découvrez les boites à livres prêt de chez vous, ou ajouter en une nouvelle à l'aide de votre compte OpenStreetMap." + description: { + en: "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.", + nl: "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren", + de: "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen.", + fr: "Une microbibliothèques, également appelée boite à livre, est un élément de mobilier urbain (étagère, armoire, etc) dans lequel sont stockés des livres et autres objets en accès libre. Découvrez les boites à livres prêt de chez vous, ou ajouter en une nouvelle à l'aide de votre compte OpenStreetMap.", }, - "icon": "./assets/themes/bookcases/bookcase.svg", - "socialImage": null, - "startLat": 0, - "startLon": 0, - "startZoom": 1, - "widenFactor": 0.05, - "roamingRenderings": [], - "layers": [ - "public_bookcase" - ] + icon: "./assets/themes/bookcases/bookcase.svg", + socialImage: null, + startLat: 0, + startLon: 0, + startZoom: 1, + widenFactor: 0.05, + roamingRenderings: [], + layers: ["public_bookcase"], } describe("Utils", () => { describe("Minify-json", () => { - - it("should work in two directions", () => { - const str = JSON.stringify({title: "abc", "and": "xyz", "render": "somevalue"}); - const minified = Utils.MinifyJSON(str); + const str = JSON.stringify({ title: "abc", and: "xyz", render: "somevalue" }) + const minified = Utils.MinifyJSON(str) const restored = Utils.UnMinify(minified) expect(str).eq(restored) }) it("should minify and restore the bookcase example", () => { const str = JSON.stringify(example, null, 0) - const minified = Utils.MinifyJSON(str); + const minified = Utils.MinifyJSON(str) const restored = Utils.UnMinify(minified) expect(str).eq(restored) - }) it("should LZ-compress a theme", () => { const str = JSON.stringify(example, null, 0) - const minified = LZString.compressToBase64(Utils.MinifyJSON(str)); + const minified = LZString.compressToBase64(Utils.MinifyJSON(str)) const restored = Utils.UnMinify(LZString.decompressFromBase64(minified)) expect(str).eq(restored) - }) it("shoud be able to decode the LZ-compression of a theme", () => { const str = JSON.stringify(example, null, 0) - const minified = LZString.compressToBase64(str); + const minified = LZString.compressToBase64(str) const restored = LZString.decompressFromBase64(minified) expect(str).eq(restored) - }) - }) - }) - \ No newline at end of file diff --git a/test/scripts/GenerateCache.spec.ts b/test/scripts/GenerateCache.spec.ts index dec2721b3..8dd702d4e 100644 --- a/test/scripts/GenerateCache.spec.ts +++ b/test/scripts/GenerateCache.spec.ts @@ -1,56 +1,7620 @@ -import {describe} from 'mocha' -import {expect} from 'chai' -import {Utils} from "../../Utils"; -import {existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync} from "fs"; -import ScriptUtils from "../../scripts/ScriptUtils"; -import {main} from "../../scripts/generateCache"; +import { describe } from "mocha" +import { expect } from "chai" +import { Utils } from "../../Utils" +import { existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync } from "fs" +import ScriptUtils from "../../scripts/ScriptUtils" +import { main } from "../../scripts/generateCache" +function initDownloads(query: string) { + const d = { + version: 0.6, + generator: "Overpass API 0.7.57 93a4d346", + osm3s: { + timestamp_osm_base: "2022-02-13T23:54:06Z", + copyright: + "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.", + }, + elements: [ + { + type: "node", + id: 518224450, + lat: 51.1548065, + lon: 3.1880118, + tags: { access: "yes", amenity: "parking", fee: "no", parking: "street_side" }, + }, + { + type: "node", + id: 665418924, + lat: 51.1575547, + lon: 3.20522, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 1168727903, + lat: 51.1299141, + lon: 3.1776123, + tags: { + amenity: "drinking_water", + mapillary: + "https://www.mapillary.com/app/?lat=51.129853685131906&lng=3.177603984688602&z=17&pKey=SEyKzIMUeKssni1ZLVe-9A&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01&x=0.5168826751181941&y=0.6114877557873634&zoom=0", + }, + }, + { + type: "node", + id: 1168728245, + lat: 51.1290938, + lon: 3.1767502, + tags: { + amenity: "drinking_water", + mapillary: + "https://www.mapillary.com/app/?lat=51.129104406662464&lng=3.176675795895676&z=17&pKey=vSP3D_hWv3XCBtH75GnYUQ&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01", + }, + }, + { + type: "node", + id: 1725842653, + lat: 51.153364, + lon: 3.2352655, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1744641290, + lat: 51.1389321, + lon: 3.2385407, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 1746891135, + lat: 51.1598841, + lon: 3.2361425, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1810326078, + lat: 51.1550855, + lon: 3.2349358, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1810326092, + lat: 51.1552302, + lon: 3.234968, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2325437742, + lat: 51.1770052, + lon: 3.1967794, + tags: { + board_type: "board", + information: "board", + name: "Tillegembos", + tourism: "information", + }, + }, + { + type: "node", + id: 2325437743, + lat: 51.1787363, + lon: 3.1949036, + tags: { + board_type: "board", + information: "board", + name: "Tillegembos", + tourism: "information", + }, + }, + { + type: "node", + id: 2325437813, + lat: 51.1733102, + lon: 3.1895672, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437839, + lat: 51.1763436, + lon: 3.1984985, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437848, + lat: 51.1770966, + lon: 3.1963507, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437862, + lat: 51.1773439, + lon: 3.1948779, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437867, + lat: 51.1775994, + lon: 3.1888088, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437873, + lat: 51.1778384, + lon: 3.1913802, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2732486257, + lat: 51.129741, + lon: 3.1907419, + tags: { + board_type: "nature", + information: "board", + name: "Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 3774054068, + lat: 51.1586662, + lon: 3.2271102, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 4769106605, + lat: 51.138264, + lon: 3.1798655, + tags: { backrest: "yes", leisure: "picnic_table" }, + }, + { + type: "node", + id: 4912238707, + lat: 51.1448634, + lon: 3.2455986, + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Oostkamp", + parking: "Carpool", + }, + }, + { + type: "node", + id: 5637212235, + lat: 51.1305439, + lon: 3.1866873, + tags: { + board_type: "nature", + image: "https://i.imgur.com/HehOQL9.jpg", + information: "board", + name: "Welkom Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 5637224573, + lat: 51.1281084, + lon: 3.1881726, + tags: { + board_type: "nature", + information: "board", + name: "Welkom Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 5637230107, + lat: 51.1280884, + lon: 3.1889798, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 5637743026, + lat: 51.1295973, + lon: 3.1751122, + tags: { + information: "board", + name: "Doeveren Wandelroute", + tourism: "information", + }, + }, + { + type: "node", + id: 5716130103, + lat: 51.1767183, + lon: 3.1947867, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 5745783208, + lat: 51.1782581, + lon: 3.2410111, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745807545, + lat: 51.1784037, + lon: 3.2369439, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745807551, + lat: 51.1783278, + lon: 3.236678, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241426, + lat: 51.1693142, + lon: 3.1673093, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241427, + lat: 51.169265, + lon: 3.1673159, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241428, + lat: 51.1692199, + lon: 3.1673224, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241430, + lat: 51.1685726, + lon: 3.1678225, + tags: { bench: "yes", leisure: "picnic_table", material: "wood" }, + }, + { + type: "node", + id: 6536026827, + lat: 51.1703142, + lon: 3.1691109, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6536026828, + lat: 51.1702795, + lon: 3.1691552, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6712112244, + lat: 51.1595064, + lon: 3.2021482, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7304050040, + lat: 51.1560908, + lon: 3.1748919, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7304050041, + lat: 51.1560141, + lon: 3.1749533, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7304050042, + lat: 51.156032, + lon: 3.1749379, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7439979218, + lat: 51.1780402, + lon: 3.2178666, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 7439979219, + lat: 51.1780508, + lon: 3.2179033, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 7529262982, + lat: 51.1585566, + lon: 3.1715528, + tags: { board_type: "map", information: "board", tourism: "information" }, + }, + { + type: "node", + id: 7529262984, + lat: 51.1585786, + lon: 3.1715385, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7554879668, + lat: 51.1573713, + lon: 3.2043731, + tags: { + access: "yes", + amenity: "toilets", + fee: "no", + "toilets:disposal": "flush", + unisex: "yes", + wheelchair: "yes", + }, + }, + { + type: "node", + id: 7554879669, + lat: 51.1594855, + lon: 3.2021507, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7556988723, + lat: 51.1330234, + lon: 3.1839944, + tags: { amenity: "bench", material: "wood" }, + }, + { + type: "node", + id: 7575825326, + lat: 51.1386553, + lon: 3.1797358, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7575825327, + lat: 51.1382456, + lon: 3.1797422, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 8109498958, + lat: 51.1332267, + lon: 3.2341272, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 8109498959, + lat: 51.1335011, + lon: 3.2343954, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 8198894646, + lat: 51.125688, + lon: 3.1856217, + tags: { + image: "https://i.imgur.com/O5kX20u.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 8199012519, + lat: 51.1262245, + lon: 3.1802429, + tags: { + image: "https://i.imgur.com/tomw9p5.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 8199244816, + lat: 51.1252874, + lon: 3.1837622, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 8199301617, + lat: 51.1256827, + lon: 3.1853543, + tags: { amenity: "bench", backrest: "no" }, + }, + { + type: "node", + id: 8255488518, + lat: 51.1406698, + lon: 3.235178, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 9316104741, + lat: 51.1330984, + lon: 3.2335257, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 9442532340, + lat: 51.1763651, + lon: 3.1947952, + tags: { + amenity: "bench", + backrest: "yes", + image: "https://i.imgur.com/eZ0Loii.jpg", + }, + }, + { + type: "way", + id: 15242261, + nodes: [ + 150996092, 150996093, 6754312552, 6754312553, 6754312550, 6754312551, 150996094, + 150996095, 150996097, 150996098, 6754312560, 6754312559, 6754312558, 150996099, + 6754312557, 150996100, 6754312555, 6754312556, 150996101, 6754312554, 150996092, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 16514228, + nodes: [170464837, 170464839, 170464840, 170464841, 170464837], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + maxstay: "4 hours", + parking: "surface", + }, + }, + { + type: "way", + id: 76706071, + nodes: [ + 903903386, 1038557094, 1038557233, 1038557143, 1038557075, 903903387, + 1038557195, 903903388, 903903390, 903904576, 903903386, + ], + tags: { access: "permissive", amenity: "parking" }, + }, + { + type: "way", + id: 89601157, + nodes: [ + 1038557083, 1038557078, 1038557104, 1038557072, 1038557108, 1038557230, + 1038557227, 1038557102, 1038557137, 1038575040, 1038557191, 1038557014, + 6960473080, 1038557035, 1038557012, 1038557083, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 89604999, + nodes: [ + 1038583404, 1038583491, 1038583375, 1038583483, 1038583479, 1038583398, + 1038583459, 1038583456, 1038583446, 1038583441, 1038583425, 1038583501, + 1038583451, 1038583463, 1038583476, 1038583404, + ], + tags: { + access: "yes", + amenity: "parking", + capacity: "57", + carpool: "yes", + description: "carpoolparking", + fee: "no", + name: "Loppem", + parking: "surface", + }, + }, + { + type: "way", + id: 92035679, + nodes: [ + 1069177920, 6853179264, 1069177925, 1069177919, 6853179269, 6853179268, + 6853179267, 6853179215, 6853179213, 6853179214, 1069178133, 1069177984, + 6853179230, 6853179228, 6853179229, 6853179224, 6853179225, 6853179227, + 6853179226, 6853179216, 6853179220, 6853179219, 6853179218, 6853179217, + 6853179223, 6853179221, 6853179222, 1069177967, 1069177852, 6853179211, + 6853179212, 6853179210, 6853179327, 6853179208, 6853179209, 6853179203, + 1069177976, 6853179207, 6853179206, 6853179205, 6853179204, 6853179202, + 1069177849, 6852012579, 6852012578, 6852012580, 6852012577, 6852012581, + 6852012582, 6852012583, 1069177845, 1759437085, 1519342743, 1519342742, + 1069178166, 1069177853, 1069177915, 6853179235, 6853179234, 6853179236, + 1069177933, 6853179237, 6853179238, 1069178021, 6853179246, 6853179244, + 6853179245, 6853179240, 6853179243, 6853179241, 6853179242, 6853179239, + 6853179248, 6853179249, 6853179250, 6853179247, 1069177873, 6853179262, + 6853179263, 6853179260, 6853179261, 6853179256, 6853179259, 6853179257, + 6853179258, 1069177920, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 101248451, + nodes: [1168728158, 1168728325, 1168728159, 5637669355, 1168728109, 1168728158], + tags: { access: "private", amenity: "parking", name: "Parking Merkenveld" }, + }, + { + type: "way", + id: 101248462, + nodes: [1168727876, 1168728288, 1168728412, 1168728208, 1168727876], + tags: { + amenity: "toilets", + building: "yes", + "source:geometry:date": "2019-03-14", + "source:geometry:ref": "Gbg/6588148", + }, + }, + { + type: "way", + id: 131622387, + nodes: [1448421093, 1448421099, 1448421091, 1448421081, 1448421093], + tags: { amenity: "parking", name: "Tudor - Zeeweg", parking: "surface" }, + }, + { + type: "way", + id: 145691934, + nodes: [1590642859, 1590642860, 1590642858, 1590642849, 1590642859], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 145691937, + nodes: [1590642829, 1590642828, 1590642830, 1590642832, 1590642829], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 158901716, + nodes: [ + 1710245713, 1710245718, 1710245707, 1710245705, 1710245703, 1710245715, + 1710245711, 1710245709, 1710245701, 1710245713, + ], + tags: { + access: "yes", + amenity: "parking", + capacity: "14", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 158904558, + nodes: [ + 1710262742, 1710262745, 1710262735, 1710262733, 1710262732, 1710262743, + 1710262741, 1710262739, 1710262744, 1710262737, 1710262736, 1710262731, + 1710262738, 1710262742, + ], + tags: { + access: "yes", + alt_name: "Schoolparking", + amenity: "parking", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 158906028, + nodes: [ + 1710276259, 1710276251, 1810330766, 1710276255, 1710276261, 1710276240, + 1710276232, 1710276257, 1710276243, 1710276253, 1810347217, 1710276242, + 1710276259, + ], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Parking Sportcentrum De Valkaart", + parking: "surface", + }, + }, + { + type: "way", + id: 160825858, + nodes: [ + 1728421375, 1728421374, 1728421379, 1728421377, 1728421376, 1728421378, + 1728421375, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 162602213, + nodes: [ + 1920143232, 7393009684, 7393048385, 1744641293, 1523513488, 1744641292, + 1920143232, + ], + tags: { amenity: "parking", capacity: "15" }, + }, + { + type: "way", + id: 165489167, + nodes: [ + 4912197370, 4912197365, 4912197373, 4912197364, 4912197372, 1770289505, + 4912197362, 4912197371, 4912197374, 4912197363, 4912197368, 4912197366, + 4912197369, 4912197367, 4912197370, + ], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Bad Neuheimplein", + parking: "surface", + }, + }, + { + type: "way", + id: 168291852, + nodes: [ + 1795793399, 4979389763, 1795793409, 1795793395, 1795793393, 1795793397, + 1795793407, 1795793406, 1795793408, 1795793405, 1795793399, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 169875513, + nodes: [1810345951, 1810345955, 1810345947, 1810345944, 1810345951], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 170015605, + nodes: [1811673425, 1811673421, 1811673418, 1811673423, 1811673425], + tags: { + access: "private", + amenity: "parking", + name: "gheeraert E40", + operator: "Gheeraert", + }, + }, + { + type: "way", + id: 170018487, + nodes: [1811699779, 1811699778, 1811699776, 1811699777, 1811699779], + tags: { access: "private", amenity: "parking", name: "Gheeraert vooraan" }, + }, + { + type: "way", + id: 170559194, + nodes: [1817319304, 1817319302, 1817319297, 1817319301, 1817319304], + tags: { access: "private", amenity: "parking", name: "Gheeraert laadkade" }, + }, + { + type: "way", + id: 170559195, + nodes: [1817319299, 1817319303, 1817319300, 1817319292, 1817319299], + tags: { + access: "private", + amenity: "parking", + name: "Gheeraert spoorweg trailers", + }, + }, + { + type: "way", + id: 170559196, + nodes: [1817319293, 1817319289, 1817319291, 1817319296, 1817319293], + tags: { access: "private", amenity: "parking", name: "Gheeraert spoorweg trucks" }, + }, + { + type: "way", + id: 170559197, + nodes: [1817319294, 1817319298, 1817319295, 1817319290, 1817319294], + tags: { access: "private", amenity: "parking", name: "Gheeraert zijkant" }, + }, + { + type: "way", + id: 170559292, + nodes: [1817320496, 1817320494, 1817320493, 1817320495, 1817320496], + tags: { access: "private", amenity: "parking", name: "Gheeraert vooraan gebouw" }, + }, + { + type: "way", + id: 170559832, + nodes: [ + 1817324515, 1817324509, 1817324491, 6397031888, 4044172008, 4044172003, + 4044171997, 4044171962, 4044171957, 4044171976, 1817324489, 1817324488, + 3550860268, 1817324505, 3550860269, 1817324513, 1817324515, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 223674368, + nodes: [ + 2325437844, 8191691971, 8191691973, 8191691972, 2325437840, 8191691970, + 414025563, 2325437844, + ], + tags: { amenity: "parking", name: "Tillegembos", parking: "surface" }, + }, + { + type: "way", + id: 237214948, + nodes: [2451574741, 2451574742, 2451574744, 1015583939, 2451574741], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 237214949, + nodes: [2451574748, 2451574746, 2451574747, 2451574749, 2451574748], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 237214950, + nodes: [2451569392, 1015567837, 2451574745, 2451574743, 2451578121, 2451569392], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 325909586, + nodes: [ + 3325315243, 111759500, 3325315247, 3325315232, 3325315230, 3325315226, + 1169056712, 3325315243, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 326834162, + nodes: [ + 3335137807, 3335137692, 3335137795, 9163493632, 3335137802, 3335137812, + 9163486855, 9163486856, 3335137823, 3335137807, + ], + tags: { + access: "customers", + amenity: "parking", + operator: "Best Western Weinebrugge", + parking: "surface", + }, + }, + { + type: "way", + id: 327849054, + nodes: [ + 3346575929, 3346575873, 3346575876, 3346575843, 3346575845, 3346575891, + 3346575901, 3346575923, 3346575928, 3346575950, 3346575929, + ], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 327849055, + nodes: [ + 3346575945, 3346575946, 3346575943, 3346575933, 3346575925, 3346575917, + 3346575903, 3346575908, 3346575889, 3346575886, 3346575945, + ], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 327849056, + nodes: [ + 3346575865, 3346575853, 3346575855, 3346575846, 3346575840, 3346575858, + 3346575865, + ], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 374677860, + nodes: [3780611504, 3780611502, 3780611500, 3780611503, 3780611504], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 374677861, + nodes: [3780611495, 3780611493, 3780611492, 3780611494, 3780611495], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 374677862, + nodes: [3780611498, 3780611497, 3780611496, 3780611499, 3780611501, 3780611498], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 389912371, + nodes: [3930713440, 3930713451, 3930713447, 3930713437, 3930713440], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 389912372, + nodes: [ + 3930713454, 3930713442, 3930713435, 3930713429, 5826811614, 3930713426, + 3930713430, 6982605752, 6982605754, 3930713438, 6982605769, 6982605766, + 6982605767, 6982605762, 6982605764, 3930713446, 3930713454, + ], + tags: { access: "customers", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 401995684, + nodes: [4044171963, 4044171954, 4044171924, 4044171936, 4044171941, 4044171963], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067820, + nodes: [ + 4912203166, 4912203165, 4912203164, 4912203163, 4912203162, 4912203161, + 4912203160, 4912203166, + ], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067821, + nodes: [4912203170, 4912203169, 4912203168, 4912203167, 4912203170], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067822, + nodes: [4912203174, 4912203173, 4912203172, 4912203171, 4912203174], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067823, + nodes: [4912203179, 4912203178, 4912203177, 4912203176, 4912203179], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500069613, + nodes: [ + 4912214695, 4912214685, 4912214694, 4912214693, 4912214692, 4912214680, + 4912214695, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071452, + nodes: [ + 4912225068, 4912225062, 4912225063, 4912225053, 4912225064, 4912214694, + 4912214685, 4912225068, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071455, + nodes: [ + 4912225070, 4912214681, 4912214686, 4912225052, 4912225051, 4912225067, + 4912225062, 4912225068, 4912225070, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071458, + nodes: [ + 4912214695, 4912214680, 4912225069, 4912225050, 1525460846, 4912214681, + 4912225070, 4912214695, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 533276307, + nodes: [5173881316, 5173881317, 5173881318, 7054196467, 5173881316], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 533276308, + nodes: [5173881320, 5173881621, 5173881622, 5173881623, 5173881320], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 533276309, + nodes: [5173881624, 5173881625, 5173881626, 5173881627, 5173881624], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 579848581, + nodes: [ + 4043782112, 5825400688, 5552466020, 6997096756, 6997096759, 6997096758, + 5552466018, 5552466013, 5552466014, 6997076567, 4043782112, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 584455569, + nodes: [5586765933, 5586765934, 5586765935, 5586765936, 5586765933], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 585977870, + nodes: [ + 5587844460, 5599314384, 5599314385, 1659850846, 6870850178, 5587844462, + 5587844461, 6870863414, 5587844460, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 587014342, + nodes: [ + 5607796820, 5607798725, 5607798721, 5607798722, 5607796819, 5607798723, + 5607796820, + ], + tags: { + access: "permissive", + amenity: "parking", + park_ride: "no", + parking: "surface", + surface: "paved", + }, + }, + { + type: "way", + id: 590167103, + nodes: [5635001277, 5635001274, 5635001275, 5635001276, 5635001277], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167113, + nodes: [ + 5635001312, 5635001306, 7767137237, 7767137235, 7767137236, 7767137234, + 5635001312, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167134, + nodes: [5635001374, 5635001373, 5635001372, 5635001371, 5635001374], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167135, + nodes: [5635001378, 5635001377, 5635001376, 5635001375, 5635001378], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167136, + nodes: [5635001382, 5635001381, 5635001380, 5635001379, 5635001382], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167137, + nodes: [ + 5635001386, 5882873336, 5882873337, 5882873338, 5882873335, 6593340582, + 6593340583, 5882873334, 6593340584, 5635001385, 5635001384, 5635001383, + 5635001386, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167147, + nodes: [5635001417, 5635001414, 5635001415, 5635001416, 5635001417], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 601406079, + nodes: [5716136617, 5716136618, 5716136619, 5716136620, 5716136617], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 632813009, + nodes: [ + 5974489618, 1810326044, 1810326087, 5974489617, 5972179348, 5972179347, + 5972179346, 5972179345, 5972179344, 5972179343, 5972179331, 5974489616, + 5974489615, 5974489614, 5974489618, + ], + tags: { amenity: "parking", name: "Gemeenteplein", parking: "surface" }, + }, + { + type: "way", + id: 668043297, + nodes: [6255587424, 6255587425, 6255587426, 6255587427, 6255587424], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 670104236, + nodes: [6275462768, 6275462769, 6275462770, 6275462771, 6275462768], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104238, + nodes: [6275462772, 6275462989, 6275462773, 6275462774, 6275462775, 6275462772], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104239, + nodes: [6275462776, 6275462777, 6275462778, 6275462779, 6275462776], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104241, + nodes: [6275462780, 6275462781, 6275462782, 6275462783, 6275462780], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104242, + nodes: [6275462784, 6275462985, 6275462988, 6275462986, 6275462987, 6275462784], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 671840055, + nodes: [ + 6291339827, 6291339828, 6291339816, 6291339815, 6291339822, 6291339821, + 6291339829, 6291339830, 6291339827, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 695825223, + nodes: [ + 1519476746, 6533893620, 6533893621, 6533893622, 1519476797, 1519476620, + 1519476746, + ], + tags: { access: "yes", amenity: "parking" }, + }, + { + type: "way", + id: 695825224, + nodes: [ + 1519476744, 6533893624, 6533893625, 6533893626, 1519476698, 1519476635, + 1519476744, + ], + tags: { access: "yes", amenity: "parking" }, + }, + { + type: "way", + id: 696040917, + nodes: [6536026850, 6536026851, 6536026852, 6536026853, 6536026850], + tags: { amenity: "parking", name: "Kasteel Tudor" }, + }, + { + type: "way", + id: 696043218, + nodes: [6536038234, 6536038235, 6536038236, 6536038237, 6536038234], + tags: { access: "customers", amenity: "parking" }, + }, + { + type: "way", + id: 700675991, + nodes: [6579962064, 6579962065, 6579962066, 6579962068, 6579962067, 6579962064], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 705278707, + nodes: [ + 6625037999, 6625038000, 6625038001, 6625038002, 6625038003, 6004375826, + 6625038005, 6625038006, 6625037999, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 719482833, + nodes: [6754312544, 6754312543, 6754312542, 6754312541, 6754312544], + tags: { access: "yes", amenity: "parking", capacity: "5" }, + }, + { + type: "way", + id: 719482834, + nodes: [6754312565, 6754312564, 6754312563, 6754312562, 6754312565], + tags: { access: "yes", amenity: "parking", capacity: "12" }, + }, + { + type: "way", + id: 737054013, + nodes: [5826811496, 5826811497, 5826811494, 5826811495, 5826811496], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737054014, + nodes: [5826810676, 5826810673, 5826810674, 5826810675, 5826810676], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737054015, + nodes: [5826810681, 5826810678, 5826810679, 5826810680, 5826810681], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737093410, + nodes: [5826811559, 5826811536, 5826811535, 5826811561, 5826811560, 5826811559], + tags: { + access: "yes", + amenity: "parking", + capacity: "4", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 737093411, + nodes: [5826811551, 5826811547, 5826811548, 5826811549, 5826811550, 5826811551], + tags: { + access: "yes", + amenity: "parking", + capacity: "4", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 739652949, + nodes: [6925536542, 6925536541, 6925536540, 6925536539, 6925536542], + tags: { amenity: "parking", capacity: "6", parking: "surface" }, + }, + { + type: "way", + id: 741675236, + nodes: [ + 6943148207, 6943148206, 6943148205, 6943148204, 1637742821, 6943148203, + 6943148202, 6943148201, 6943148200, 6943148199, 6943148198, 6943148197, + 6943148207, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 742295526, + nodes: [6949357909, 6949357908, 6949357907, 6949357906, 6949357909], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295527, + nodes: [6949357913, 6949357912, 6949357911, 6949357910, 6949357913], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295528, + nodes: [6949357917, 6949357916, 6949357915, 6949357914, 6949357917], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295529, + nodes: [6949357921, 6949357920, 6949357919, 6949357918, 6949357921], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 746170866, + nodes: [ + 6982906547, 6982906546, 6982906545, 6982906544, 6982906543, 6982906542, + 6982906547, + ], + tags: { access: "customers", amenity: "parking" }, + }, + { + type: "way", + id: 747880657, + nodes: [ + 3325315397, 6997076566, 6997076565, 6997076563, 6997076562, 6997076564, + 3325315395, 3325315397, + ], + tags: { access: "customers", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 763977465, + nodes: [7137343680, 7137343681, 7137343682, 7137343683, 7137343680], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 763977466, + nodes: [7137343684, 7137383185, 7137383186, 7137383187, 7137343684], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821090, + nodes: [7519058290, 7519058289, 7519058288, 7519058287, 7519058290], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821091, + nodes: [7519058294, 7519058293, 7519058292, 7519058291, 7519058294], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821092, + nodes: [7519058298, 7519058297, 7519058296, 7519058295, 7519058298], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821093, + nodes: [7519058302, 7519058301, 7519058300, 7519058299, 7519058302], + tags: { access: "private", amenity: "parking", capacity: "6", parking: "surface" }, + }, + { + type: "way", + id: 804963962, + nodes: [ + 7529417225, 7529417226, 7529417227, 7529417228, 7529417229, 7529417230, + 7529417232, 7529417225, + ], + tags: { + access: "customers", + amenity: "parking", + fee: "no", + operator: "’t Kiekekot", + parking: "surface", + surface: "unpaved", + }, + }, + { + type: "way", + id: 806875503, + nodes: [4042671969, 7545532512, 7545532514, 7545532513, 3359977305, 4042671969], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 806963547, + nodes: [7546193222, 7546193221, 7546193220, 7546193219, 7546193222], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 865357121, + nodes: [8065883228, 8065883227, 8065883226, 8065883225, 8065883228], + tags: { amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 865357122, + nodes: [8065883233, 8065883230, 8065883229, 8065883232, 8065883233], + tags: { amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 879281221, + nodes: [ + 8179735269, 8179735268, 8179735267, 8179735266, 8179735265, 8179735264, + 8179735224, 8179735269, + ], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 881770201, + nodes: [8200275847, 8200275848, 8200275844, 8200275853, 8200275849, 8200275847], + tags: { amenity: "parking", parking: "surface", surface: "grass" }, + }, + { + type: "way", + id: 978360549, + nodes: [5761770202, 5761770204, 9052878229, 9052878228, 5761770202], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 1009692722, + nodes: [ + 9316118540, 9316118536, 9316118531, 9316118535, 9316118534, 9316118533, + 9316118530, 9316118532, 3311835478, 9316118540, + ], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "relation", + id: 8188853, + members: [ + { type: "way", ref: 577572397, role: "outer" }, + { type: "way", ref: 577572399, role: "outer" }, + ], + tags: { + access: "guided", + curator: "Luc Maene;Geert De Clercq", + email: "lucmaene@hotmail.com;geert.de.clercq1@pandora.be", + landuse: "meadow", + leisure: "nature_reserve", + name: "De Wulgenbroeken", + natural: "wetland", + operator: "Natuurpunt Brugge", + type: "multipolygon", + website: "https://natuurpuntbrugge.be/wulgenbroeken/", + wetland: "wet_meadow", + wikidata: "Q60061498", + wikipedia: "nl:Wulgenbroeken", + }, + }, + { + type: "relation", + id: 11163488, + members: [ + { type: "way", ref: 810604915, role: "outer" }, + { type: "way", ref: 989393316, role: "outer" }, + { type: "way", ref: 389026405, role: "inner" }, + { type: "way", ref: 810607458, role: "outer" }, + ], + tags: { + access: "yes", + curator: "Kris Lesage", + description: + "Wat Doeveren zo uniek maakt, zijn zijn kleine heidegebiedjes met soorten die erg verschillen van de Kempense heide. Doeveren en omstreken was vroeger één groot heidegebied, maar bestaat nu grotendeels uit bossen.", + dog: "leashed", + email: "doeveren@natuurpuntzedelgem.be", + image: "https://i.imgur.com/NEAsQZG.jpg", + "image:0": "https://i.imgur.com/Dq71hyQ.jpg", + "image:1": "https://i.imgur.com/mAIiT4f.jpg", + "image:2": "https://i.imgur.com/dELZU97.jpg", + "image:3": "https://i.imgur.com/Bso57JC.jpg", + "image:4": "https://i.imgur.com/9DtcfXo.jpg", + "image:5": "https://i.imgur.com/0R6eBfk.jpg", + "image:6": "https://i.imgur.com/b0JpvbR.jpg", + leisure: "nature_reserve", + name: "Doeveren", + operator: "Natuurpunt Zedelgem", + phone: "+32 486 25 25 30", + type: "multipolygon", + website: "https://www.natuurpuntzedelgem.be/gebieden/doeveren/", + wikidata: "Q56395754", + wikipedia: "nl:Doeveren (natuurgebied)", + }, + }, + { + type: "relation", + id: 11790117, + members: [ + { type: "way", ref: 863373849, role: "outer" }, + { type: "way", ref: 777280458, role: "outer" }, + ], + tags: { + access: "no", + "description:0": "In gebruik als waterbuffering", + leisure: "nature_reserve", + name: "Kerkebeek", + operator: "Natuurpunt Brugge", + type: "multipolygon", + }, + }, + { + type: "node", + id: 518224450, + lat: 51.1548065, + lon: 3.1880118, + timestamp: "2021-08-14T21:49:28Z", + version: 5, + changeset: 109683837, + user: "effem", + uid: 16437, + tags: { access: "yes", amenity: "parking", fee: "no", parking: "street_side" }, + }, + { + type: "node", + id: 665418924, + lat: 51.1575547, + lon: 3.20522, + timestamp: "2012-05-12T20:13:39Z", + version: 2, + changeset: 11580224, + user: "martino260", + uid: 655442, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 1168727903, + lat: 51.1299141, + lon: 3.1776123, + timestamp: "2017-04-03T08:34:05Z", + version: 2, + changeset: 47403889, + user: "philippec", + uid: 76884, + tags: { + amenity: "drinking_water", + mapillary: + "https://www.mapillary.com/app/?lat=51.129853685131906&lng=3.177603984688602&z=17&pKey=SEyKzIMUeKssni1ZLVe-9A&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01&x=0.5168826751181941&y=0.6114877557873634&zoom=0", + }, + }, + { + type: "node", + id: 1168728245, + lat: 51.1290938, + lon: 3.1767502, + timestamp: "2019-10-07T11:06:57Z", + version: 3, + changeset: 75370316, + user: "Hopperpop", + uid: 3664604, + tags: { + amenity: "drinking_water", + mapillary: + "https://www.mapillary.com/app/?lat=51.129104406662464&lng=3.176675795895676&z=17&pKey=vSP3D_hWv3XCBtH75GnYUQ&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01", + }, + }, + { + type: "node", + id: 1725842653, + lat: 51.153364, + lon: 3.2352655, + timestamp: "2012-07-02T17:33:00Z", + version: 2, + changeset: 12090625, + user: "martino260", + uid: 655442, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1744641290, + lat: 51.1389321, + lon: 3.2385407, + timestamp: "2012-09-18T13:29:52Z", + version: 3, + changeset: 13156159, + user: "martino260", + uid: 655442, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 1746891135, + lat: 51.1598841, + lon: 3.2361425, + timestamp: "2012-05-09T18:22:11Z", + version: 1, + changeset: 11551825, + user: "martino260", + uid: 655442, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1810326078, + lat: 51.1550855, + lon: 3.2349358, + timestamp: "2012-07-02T19:50:15Z", + version: 1, + changeset: 12093439, + user: "martino260", + uid: 655442, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 1810326092, + lat: 51.1552302, + lon: 3.234968, + timestamp: "2012-07-02T19:50:16Z", + version: 1, + changeset: 12093439, + user: "martino260", + uid: 655442, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2325437742, + lat: 51.1770052, + lon: 3.1967794, + timestamp: "2013-05-30T12:19:08Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { + board_type: "board", + information: "board", + name: "Tillegembos", + tourism: "information", + }, + }, + { + type: "node", + id: 2325437743, + lat: 51.1787363, + lon: 3.1949036, + timestamp: "2013-05-30T12:19:08Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { + board_type: "board", + information: "board", + name: "Tillegembos", + tourism: "information", + }, + }, + { + type: "node", + id: 2325437813, + lat: 51.1733102, + lon: 3.1895672, + timestamp: "2013-05-30T12:19:09Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437839, + lat: 51.1763436, + lon: 3.1984985, + timestamp: "2013-05-30T12:19:10Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437848, + lat: 51.1770966, + lon: 3.1963507, + timestamp: "2013-05-30T12:19:10Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437862, + lat: 51.1773439, + lon: 3.1948779, + timestamp: "2013-05-30T12:19:10Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437867, + lat: 51.1775994, + lon: 3.1888088, + timestamp: "2013-05-30T12:19:11Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2325437873, + lat: 51.1778384, + lon: 3.1913802, + timestamp: "2013-05-30T12:19:11Z", + version: 1, + changeset: 16350909, + user: "peeweeke", + uid: 494726, + tags: { amenity: "bench", backrest: "yes", material: "wood" }, + }, + { + type: "node", + id: 2732486257, + lat: 51.129741, + lon: 3.1907419, + timestamp: "2014-03-21T21:15:28Z", + version: 1, + changeset: 21234491, + user: "meannder", + uid: 149496, + tags: { + board_type: "nature", + information: "board", + name: "Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 3774054068, + lat: 51.1586662, + lon: 3.2271102, + timestamp: "2015-10-05T20:34:04Z", + version: 1, + changeset: 34456387, + user: "TripleBee", + uid: 497177, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 4769106605, + lat: 51.138264, + lon: 3.1798655, + timestamp: "2020-05-31T19:49:45Z", + version: 3, + changeset: 86019474, + user: "Hopperpop", + uid: 3664604, + tags: { backrest: "yes", leisure: "picnic_table" }, + }, + { + type: "node", + id: 4912238707, + lat: 51.1448634, + lon: 3.2455986, + timestamp: "2017-06-13T08:12:04Z", + version: 1, + changeset: 49491753, + user: "Jakka", + uid: 2403313, + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Oostkamp", + parking: "Carpool", + }, + }, + { + type: "node", + id: 5637212235, + lat: 51.1305439, + lon: 3.1866873, + timestamp: "2021-11-22T11:54:45Z", + version: 4, + changeset: 114095475, + user: "L'imaginaire", + uid: 654234, + tags: { + board_type: "nature", + image: "https://i.imgur.com/HehOQL9.jpg", + information: "board", + name: "Welkom Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 5637224573, + lat: 51.1281084, + lon: 3.1881726, + timestamp: "2020-06-01T22:39:30Z", + version: 2, + changeset: 86065716, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + board_type: "nature", + information: "board", + name: "Welkom Doeveren", + tourism: "information", + }, + }, + { + type: "node", + id: 5637230107, + lat: 51.1280884, + lon: 3.1889798, + timestamp: "2018-05-23T11:55:01Z", + version: 1, + changeset: 59208628, + user: "Jakka", + uid: 2403313, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 5637743026, + lat: 51.1295973, + lon: 3.1751122, + timestamp: "2021-10-08T08:53:14Z", + version: 2, + changeset: 112251989, + user: "DieterWesttoer", + uid: 13062237, + tags: { + information: "board", + name: "Doeveren Wandelroute", + tourism: "information", + }, + }, + { + type: "node", + id: 5716130103, + lat: 51.1767183, + lon: 3.1947867, + timestamp: "2018-06-24T22:04:21Z", + version: 1, + changeset: 60130942, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 5745783208, + lat: 51.1782581, + lon: 3.2410111, + timestamp: "2018-07-07T18:42:23Z", + version: 1, + changeset: 60494990, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745807545, + lat: 51.1784037, + lon: 3.2369439, + timestamp: "2018-07-07T18:58:25Z", + version: 1, + changeset: 60495307, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745807551, + lat: 51.1783278, + lon: 3.236678, + timestamp: "2018-07-07T18:58:25Z", + version: 1, + changeset: 60495307, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241426, + lat: 51.1693142, + lon: 3.1673093, + timestamp: "2019-06-09T13:50:19Z", + version: 1, + changeset: 71071874, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241427, + lat: 51.169265, + lon: 3.1673159, + timestamp: "2019-06-09T13:50:19Z", + version: 1, + changeset: 71071874, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241428, + lat: 51.1692199, + lon: 3.1673224, + timestamp: "2019-06-09T13:50:19Z", + version: 1, + changeset: 71071874, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6535241430, + lat: 51.1685726, + lon: 3.1678225, + timestamp: "2019-06-09T13:50:19Z", + version: 1, + changeset: 71071874, + user: "Hopperpop", + uid: 3664604, + tags: { bench: "yes", leisure: "picnic_table", material: "wood" }, + }, + { + type: "node", + id: 6536026827, + lat: 51.1703142, + lon: 3.1691109, + timestamp: "2019-06-09T22:54:45Z", + version: 1, + changeset: 71082671, + user: "Hopperpop", + uid: 3664604, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6536026828, + lat: 51.1702795, + lon: 3.1691552, + timestamp: "2019-06-09T22:54:45Z", + version: 1, + changeset: 71082671, + user: "Hopperpop", + uid: 3664604, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6712112244, + lat: 51.1595064, + lon: 3.2021482, + timestamp: "2020-05-24T21:35:50Z", + version: 2, + changeset: 85695537, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7304050040, + lat: 51.1560908, + lon: 3.1748919, + timestamp: "2020-03-17T19:11:00Z", + version: 1, + changeset: 82315744, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7304050041, + lat: 51.1560141, + lon: 3.1749533, + timestamp: "2020-03-17T19:11:00Z", + version: 1, + changeset: 82315744, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7304050042, + lat: 51.156032, + lon: 3.1749379, + timestamp: "2020-03-17T19:11:00Z", + version: 1, + changeset: 82315744, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7439979218, + lat: 51.1780402, + lon: 3.2178666, + timestamp: "2020-04-24T00:56:14Z", + version: 1, + changeset: 84027933, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 7439979219, + lat: 51.1780508, + lon: 3.2179033, + timestamp: "2020-04-24T00:56:14Z", + version: 1, + changeset: 84027933, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 7529262982, + lat: 51.1585566, + lon: 3.1715528, + timestamp: "2020-05-17T16:12:04Z", + version: 1, + changeset: 85340264, + user: "Hopperpop", + uid: 3664604, + tags: { board_type: "map", information: "board", tourism: "information" }, + }, + { + type: "node", + id: 7529262984, + lat: 51.1585786, + lon: 3.1715385, + timestamp: "2020-05-17T16:12:04Z", + version: 1, + changeset: 85340264, + user: "Hopperpop", + uid: 3664604, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7554879668, + lat: 51.1573713, + lon: 3.2043731, + timestamp: "2020-05-24T21:35:50Z", + version: 1, + changeset: 85695537, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + access: "yes", + amenity: "toilets", + fee: "no", + "toilets:disposal": "flush", + unisex: "yes", + wheelchair: "yes", + }, + }, + { + type: "node", + id: 7554879669, + lat: 51.1594855, + lon: 3.2021507, + timestamp: "2020-05-24T21:35:50Z", + version: 1, + changeset: 85695537, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7556988723, + lat: 51.1330234, + lon: 3.1839944, + timestamp: "2020-05-25T19:19:56Z", + version: 1, + changeset: 85730259, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench", material: "wood" }, + }, + { + type: "node", + id: 7575825326, + lat: 51.1386553, + lon: 3.1797358, + timestamp: "2020-05-31T19:49:45Z", + version: 1, + changeset: 86019474, + user: "Hopperpop", + uid: 3664604, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7575825327, + lat: 51.1382456, + lon: 3.1797422, + timestamp: "2020-05-31T19:49:45Z", + version: 1, + changeset: 86019474, + user: "Hopperpop", + uid: 3664604, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 8109498958, + lat: 51.1332267, + lon: 3.2341272, + timestamp: "2020-11-11T20:42:45Z", + version: 1, + changeset: 93951029, + user: "L'imaginaire", + uid: 654234, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 8109498959, + lat: 51.1335011, + lon: 3.2343954, + timestamp: "2020-11-11T20:42:45Z", + version: 1, + changeset: 93951029, + user: "L'imaginaire", + uid: 654234, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 8198894646, + lat: 51.125688, + lon: 3.1856217, + timestamp: "2020-12-06T23:39:34Z", + version: 3, + changeset: 95384686, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + image: "https://i.imgur.com/O5kX20u.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 8199012519, + lat: 51.1262245, + lon: 3.1802429, + timestamp: "2020-12-07T00:12:14Z", + version: 3, + changeset: 95385124, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + image: "https://i.imgur.com/tomw9p5.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 8199244816, + lat: 51.1252874, + lon: 3.1837622, + timestamp: "2020-12-06T17:14:52Z", + version: 1, + changeset: 95374174, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 8199301617, + lat: 51.1256827, + lon: 3.1853543, + timestamp: "2020-12-06T17:14:52Z", + version: 1, + changeset: 95374174, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench", backrest: "no" }, + }, + { + type: "node", + id: 8255488518, + lat: 51.1406698, + lon: 3.235178, + timestamp: "2020-12-23T18:20:35Z", + version: 1, + changeset: 96342158, + user: "L'imaginaire", + uid: 654234, + tags: { amenity: "bench", backrest: "yes" }, + }, + { + type: "node", + id: 9316104741, + lat: 51.1330984, + lon: 3.2335257, + timestamp: "2021-12-06T18:31:00Z", + version: 1, + changeset: 114629890, + user: "L'imaginaire", + uid: 654234, + tags: { information: "board", tourism: "information" }, + }, + { + type: "node", + id: 9442532340, + lat: 51.1763651, + lon: 3.1947952, + timestamp: "2022-01-23T16:26:28Z", + version: 2, + changeset: 116506336, + user: "L'imaginaire", + uid: 654234, + tags: { + amenity: "bench", + backrest: "yes", + image: "https://i.imgur.com/eZ0Loii.jpg", + }, + }, + { + type: "way", + id: 15242261, + timestamp: "2020-04-05T07:08:45Z", + version: 8, + changeset: 83089516, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 150996092, 150996093, 6754312552, 6754312553, 6754312550, 6754312551, 150996094, + 150996095, 150996097, 150996098, 6754312560, 6754312559, 6754312558, 150996099, + 6754312557, 150996100, 6754312555, 6754312556, 150996101, 6754312554, 150996092, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 16514228, + timestamp: "2017-06-13T07:14:23Z", + version: 9, + changeset: 49490318, + user: "Jakka", + uid: 2403313, + nodes: [170464837, 170464839, 170464840, 170464841, 170464837], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + maxstay: "4 hours", + parking: "surface", + }, + }, + { + type: "way", + id: 76706071, + timestamp: "2017-06-17T07:51:31Z", + version: 4, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [ + 903903386, 1038557094, 1038557233, 1038557143, 1038557075, 903903387, + 1038557195, 903903388, 903903390, 903904576, 903903386, + ], + tags: { access: "permissive", amenity: "parking" }, + }, + { + type: "way", + id: 89601157, + timestamp: "2019-11-09T18:40:50Z", + version: 3, + changeset: 76851428, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 1038557083, 1038557078, 1038557104, 1038557072, 1038557108, 1038557230, + 1038557227, 1038557102, 1038557137, 1038575040, 1038557191, 1038557014, + 6960473080, 1038557035, 1038557012, 1038557083, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 89604999, + timestamp: "2020-01-16T22:01:28Z", + version: 5, + changeset: 79667667, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 1038583404, 1038583491, 1038583375, 1038583483, 1038583479, 1038583398, + 1038583459, 1038583456, 1038583446, 1038583441, 1038583425, 1038583501, + 1038583451, 1038583463, 1038583476, 1038583404, + ], + tags: { + access: "yes", + amenity: "parking", + capacity: "57", + carpool: "yes", + description: "carpoolparking", + fee: "no", + name: "Loppem", + parking: "surface", + }, + }, + { + type: "way", + id: 92035679, + timestamp: "2019-10-05T08:05:33Z", + version: 7, + changeset: 75311122, + user: "skyman81", + uid: 955688, + nodes: [ + 1069177920, 6853179264, 1069177925, 1069177919, 6853179269, 6853179268, + 6853179267, 6853179215, 6853179213, 6853179214, 1069178133, 1069177984, + 6853179230, 6853179228, 6853179229, 6853179224, 6853179225, 6853179227, + 6853179226, 6853179216, 6853179220, 6853179219, 6853179218, 6853179217, + 6853179223, 6853179221, 6853179222, 1069177967, 1069177852, 6853179211, + 6853179212, 6853179210, 6853179327, 6853179208, 6853179209, 6853179203, + 1069177976, 6853179207, 6853179206, 6853179205, 6853179204, 6853179202, + 1069177849, 6852012579, 6852012578, 6852012580, 6852012577, 6852012581, + 6852012582, 6852012583, 1069177845, 1759437085, 1519342743, 1519342742, + 1069178166, 1069177853, 1069177915, 6853179235, 6853179234, 6853179236, + 1069177933, 6853179237, 6853179238, 1069178021, 6853179246, 6853179244, + 6853179245, 6853179240, 6853179243, 6853179241, 6853179242, 6853179239, + 6853179248, 6853179249, 6853179250, 6853179247, 1069177873, 6853179262, + 6853179263, 6853179260, 6853179261, 6853179256, 6853179259, 6853179257, + 6853179258, 1069177920, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 101248451, + timestamp: "2018-05-23T14:18:27Z", + version: 2, + changeset: 59213416, + user: "Jakka", + uid: 2403313, + nodes: [1168728158, 1168728325, 1168728159, 5637669355, 1168728109, 1168728158], + tags: { access: "private", amenity: "parking", name: "Parking Merkenveld" }, + }, + { + type: "way", + id: 101248462, + timestamp: "2020-05-25T13:53:02Z", + version: 3, + changeset: 85720081, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1168727876, 1168728288, 1168728412, 1168728208, 1168727876], + tags: { + amenity: "toilets", + building: "yes", + "source:geometry:date": "2019-03-14", + "source:geometry:ref": "Gbg/6588148", + }, + }, + { + type: "way", + id: 131622387, + timestamp: "2015-01-09T12:06:51Z", + version: 2, + changeset: 28017707, + user: "TripleBee", + uid: 497177, + nodes: [1448421093, 1448421099, 1448421091, 1448421081, 1448421093], + tags: { amenity: "parking", name: "Tudor - Zeeweg", parking: "surface" }, + }, + { + type: "way", + id: 145691934, + timestamp: "2012-01-15T12:43:37Z", + version: 1, + changeset: 10397429, + user: "Sanderd17", + uid: 253266, + nodes: [1590642859, 1590642860, 1590642858, 1590642849, 1590642859], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 145691937, + timestamp: "2012-01-15T12:43:37Z", + version: 1, + changeset: 10397429, + user: "Sanderd17", + uid: 253266, + nodes: [1590642829, 1590642828, 1590642830, 1590642832, 1590642829], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 158901716, + timestamp: "2017-06-13T07:12:20Z", + version: 2, + changeset: 49490264, + user: "Jakka", + uid: 2403313, + nodes: [ + 1710245713, 1710245718, 1710245707, 1710245705, 1710245703, 1710245715, + 1710245711, 1710245709, 1710245701, 1710245713, + ], + tags: { + access: "yes", + amenity: "parking", + capacity: "14", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 158904558, + timestamp: "2020-12-22T22:39:41Z", + version: 4, + changeset: 96283379, + user: "M!dgard", + uid: 763799, + nodes: [ + 1710262742, 1710262745, 1710262735, 1710262733, 1710262732, 1710262743, + 1710262741, 1710262739, 1710262744, 1710262737, 1710262736, 1710262731, + 1710262738, 1710262742, + ], + tags: { + access: "yes", + alt_name: "Schoolparking", + amenity: "parking", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 158906028, + timestamp: "2017-06-13T07:27:19Z", + version: 5, + changeset: 49490646, + user: "Jakka", + uid: 2403313, + nodes: [ + 1710276259, 1710276251, 1810330766, 1710276255, 1710276261, 1710276240, + 1710276232, 1710276257, 1710276243, 1710276253, 1810347217, 1710276242, + 1710276259, + ], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Parking Sportcentrum De Valkaart", + parking: "surface", + }, + }, + { + type: "way", + id: 160825858, + timestamp: "2012-04-23T20:35:52Z", + version: 1, + changeset: 11399632, + user: "martino260", + uid: 655442, + nodes: [ + 1728421375, 1728421374, 1728421379, 1728421377, 1728421376, 1728421378, + 1728421375, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 162602213, + timestamp: "2020-04-11T18:12:16Z", + version: 8, + changeset: 83407731, + user: "JanFi", + uid: 672253, + nodes: [ + 1920143232, 7393009684, 7393048385, 1744641293, 1523513488, 1744641292, + 1920143232, + ], + tags: { amenity: "parking", capacity: "15" }, + }, + { + type: "way", + id: 165489167, + timestamp: "2020-10-12T19:06:29Z", + version: 3, + changeset: 92371840, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 4912197370, 4912197365, 4912197373, 4912197364, 4912197372, 1770289505, + 4912197362, 4912197371, 4912197374, 4912197363, 4912197368, 4912197366, + 4912197369, 4912197367, 4912197370, + ], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + name: "Bad Neuheimplein", + parking: "surface", + }, + }, + { + type: "way", + id: 168291852, + timestamp: "2017-07-19T11:17:33Z", + version: 3, + changeset: 50402298, + user: "martino260", + uid: 655442, + nodes: [ + 1795793399, 4979389763, 1795793409, 1795793395, 1795793393, 1795793397, + 1795793407, 1795793406, 1795793408, 1795793405, 1795793399, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 169875513, + timestamp: "2017-06-13T07:26:09Z", + version: 2, + changeset: 49490613, + user: "Jakka", + uid: 2403313, + nodes: [1810345951, 1810345955, 1810345947, 1810345944, 1810345951], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 170015605, + timestamp: "2017-06-17T07:51:33Z", + version: 4, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1811673425, 1811673421, 1811673418, 1811673423, 1811673425], + tags: { + access: "private", + amenity: "parking", + name: "gheeraert E40", + operator: "Gheeraert", + }, + }, + { + type: "way", + id: 170018487, + timestamp: "2017-06-17T07:51:33Z", + version: 3, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1811699779, 1811699778, 1811699776, 1811699777, 1811699779], + tags: { access: "private", amenity: "parking", name: "Gheeraert vooraan" }, + }, + { + type: "way", + id: 170559194, + timestamp: "2017-06-17T07:51:33Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1817319304, 1817319302, 1817319297, 1817319301, 1817319304], + tags: { access: "private", amenity: "parking", name: "Gheeraert laadkade" }, + }, + { + type: "way", + id: 170559195, + timestamp: "2017-06-17T07:51:33Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1817319299, 1817319303, 1817319300, 1817319292, 1817319299], + tags: { + access: "private", + amenity: "parking", + name: "Gheeraert spoorweg trailers", + }, + }, + { + type: "way", + id: 170559196, + timestamp: "2017-06-17T07:51:33Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1817319293, 1817319289, 1817319291, 1817319296, 1817319293], + tags: { access: "private", amenity: "parking", name: "Gheeraert spoorweg trucks" }, + }, + { + type: "way", + id: 170559197, + timestamp: "2017-06-17T07:51:33Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1817319294, 1817319298, 1817319295, 1817319290, 1817319294], + tags: { access: "private", amenity: "parking", name: "Gheeraert zijkant" }, + }, + { + type: "way", + id: 170559292, + timestamp: "2017-06-17T07:51:33Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [1817320496, 1817320494, 1817320493, 1817320495, 1817320496], + tags: { access: "private", amenity: "parking", name: "Gheeraert vooraan gebouw" }, + }, + { + type: "way", + id: 170559832, + timestamp: "2020-10-07T10:51:41Z", + version: 6, + changeset: 92105788, + user: "effem", + uid: 16437, + nodes: [ + 1817324515, 1817324509, 1817324491, 6397031888, 4044172008, 4044172003, + 4044171997, 4044171962, 4044171957, 4044171976, 1817324489, 1817324488, + 3550860268, 1817324505, 3550860269, 1817324513, 1817324515, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 223674368, + timestamp: "2020-12-03T18:47:28Z", + version: 3, + changeset: 95244226, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 2325437844, 8191691971, 8191691973, 8191691972, 2325437840, 8191691970, + 414025563, 2325437844, + ], + tags: { amenity: "parking", name: "Tillegembos", parking: "surface" }, + }, + { + type: "way", + id: 237214948, + timestamp: "2017-06-17T07:51:35Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [2451574741, 2451574742, 2451574744, 1015583939, 2451574741], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 237214949, + timestamp: "2017-06-17T07:51:35Z", + version: 2, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [2451574748, 2451574746, 2451574747, 2451574749, 2451574748], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 237214950, + timestamp: "2017-06-17T07:51:35Z", + version: 3, + changeset: 49608752, + user: "rowers2", + uid: 2445224, + nodes: [2451569392, 1015567837, 2451574745, 2451574743, 2451578121, 2451569392], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 325909586, + timestamp: "2016-01-05T18:51:49Z", + version: 2, + changeset: 36387330, + user: "JanFi", + uid: 672253, + nodes: [ + 3325315243, 111759500, 3325315247, 3325315232, 3325315230, 3325315226, + 1169056712, 3325315243, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 326834162, + timestamp: "2021-10-10T21:49:11Z", + version: 4, + changeset: 112350014, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [ + 3335137807, 3335137692, 3335137795, 9163493632, 3335137802, 3335137812, + 9163486855, 9163486856, 3335137823, 3335137807, + ], + tags: { + access: "customers", + amenity: "parking", + operator: "Best Western Weinebrugge", + parking: "surface", + }, + }, + { + type: "way", + id: 327849054, + timestamp: "2015-02-12T20:26:22Z", + version: 1, + changeset: 28807613, + user: "escada", + uid: 436365, + nodes: [ + 3346575929, 3346575873, 3346575876, 3346575843, 3346575845, 3346575891, + 3346575901, 3346575923, 3346575928, 3346575950, 3346575929, + ], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 327849055, + timestamp: "2016-10-08T21:24:46Z", + version: 2, + changeset: 42742859, + user: "maggot27", + uid: 118021, + nodes: [ + 3346575945, 3346575946, 3346575943, 3346575933, 3346575925, 3346575917, + 3346575903, 3346575908, 3346575889, 3346575886, 3346575945, + ], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 327849056, + timestamp: "2015-02-12T20:26:22Z", + version: 1, + changeset: 28807613, + user: "escada", + uid: 436365, + nodes: [ + 3346575865, 3346575853, 3346575855, 3346575846, 3346575840, 3346575858, + 3346575865, + ], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 374677860, + timestamp: "2015-10-10T09:08:20Z", + version: 1, + changeset: 34545536, + user: "Jakka", + uid: 2403313, + nodes: [3780611504, 3780611502, 3780611500, 3780611503, 3780611504], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 374677861, + timestamp: "2015-10-10T09:08:20Z", + version: 1, + changeset: 34545536, + user: "Jakka", + uid: 2403313, + nodes: [3780611495, 3780611493, 3780611492, 3780611494, 3780611495], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 374677862, + timestamp: "2015-10-10T09:08:20Z", + version: 1, + changeset: 34545536, + user: "Jakka", + uid: 2403313, + nodes: [3780611498, 3780611497, 3780611496, 3780611499, 3780611501, 3780611498], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 389912371, + timestamp: "2016-01-06T16:51:45Z", + version: 1, + changeset: 36407948, + user: "Spectrokid", + uid: 19775, + nodes: [3930713440, 3930713451, 3930713447, 3930713437, 3930713440], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 389912372, + timestamp: "2019-11-16T13:17:09Z", + version: 4, + changeset: 77164376, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 3930713454, 3930713442, 3930713435, 3930713429, 5826811614, 3930713426, + 3930713430, 6982605752, 6982605754, 3930713438, 6982605769, 6982605766, + 6982605767, 6982605762, 6982605764, 3930713446, 3930713454, + ], + tags: { access: "customers", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 401995684, + timestamp: "2020-10-07T10:52:00Z", + version: 3, + changeset: 92105972, + user: "effem", + uid: 16437, + nodes: [4044171963, 4044171954, 4044171924, 4044171936, 4044171941, 4044171963], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067820, + timestamp: "2017-06-13T07:49:12Z", + version: 1, + changeset: 49491219, + user: "Jakka", + uid: 2403313, + nodes: [ + 4912203166, 4912203165, 4912203164, 4912203163, 4912203162, 4912203161, + 4912203160, 4912203166, + ], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067821, + timestamp: "2017-06-13T07:49:12Z", + version: 1, + changeset: 49491219, + user: "Jakka", + uid: 2403313, + nodes: [4912203170, 4912203169, 4912203168, 4912203167, 4912203170], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067822, + timestamp: "2017-06-13T07:49:12Z", + version: 1, + changeset: 49491219, + user: "Jakka", + uid: 2403313, + nodes: [4912203174, 4912203173, 4912203172, 4912203171, 4912203174], + tags: { access: "yes", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 500067823, + timestamp: "2017-06-13T07:49:12Z", + version: 1, + changeset: 49491219, + user: "Jakka", + uid: 2403313, + nodes: [4912203179, 4912203178, 4912203177, 4912203176, 4912203179], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500069613, + timestamp: "2018-10-11T09:30:48Z", + version: 2, + changeset: 63409550, + user: "Jakka", + uid: 2403313, + nodes: [ + 4912214695, 4912214685, 4912214694, 4912214693, 4912214692, 4912214680, + 4912214695, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071452, + timestamp: "2018-10-11T09:30:48Z", + version: 2, + changeset: 63409550, + user: "Jakka", + uid: 2403313, + nodes: [ + 4912225068, 4912225062, 4912225063, 4912225053, 4912225064, 4912214694, + 4912214685, 4912225068, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071455, + timestamp: "2018-10-11T09:30:48Z", + version: 2, + changeset: 63409550, + user: "Jakka", + uid: 2403313, + nodes: [ + 4912225070, 4912214681, 4912214686, 4912225052, 4912225051, 4912225067, + 4912225062, 4912225068, 4912225070, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 500071458, + timestamp: "2018-10-11T09:30:48Z", + version: 2, + changeset: 63409550, + user: "Jakka", + uid: 2403313, + nodes: [ + 4912214695, 4912214680, 4912225069, 4912225050, 1525460846, 4912214681, + 4912225070, 4912214695, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 533276307, + timestamp: "2019-12-13T10:02:12Z", + version: 2, + changeset: 78364882, + user: "skyman81", + uid: 955688, + nodes: [5173881316, 5173881317, 5173881318, 7054196467, 5173881316], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 533276308, + timestamp: "2017-10-17T23:36:18Z", + version: 1, + changeset: 53027174, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [5173881320, 5173881621, 5173881622, 5173881623, 5173881320], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 533276309, + timestamp: "2017-10-17T23:36:18Z", + version: 1, + changeset: 53027174, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [5173881624, 5173881625, 5173881626, 5173881627, 5173881624], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 579848581, + timestamp: "2020-04-05T07:08:57Z", + version: 6, + changeset: 83089516, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 4043782112, 5825400688, 5552466020, 6997096756, 6997096759, 6997096758, + 5552466018, 5552466013, 5552466014, 6997076567, 4043782112, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 584455569, + timestamp: "2019-10-31T19:49:18Z", + version: 2, + changeset: 76465627, + user: "Hopperpop", + uid: 3664604, + nodes: [5586765933, 5586765934, 5586765935, 5586765936, 5586765933], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 585977870, + timestamp: "2019-10-11T13:28:22Z", + version: 2, + changeset: 75566739, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 5587844460, 5599314384, 5599314385, 1659850846, 6870850178, 5587844462, + 5587844461, 6870863414, 5587844460, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 587014342, + timestamp: "2018-05-09T19:13:29Z", + version: 1, + changeset: 58829130, + user: "Sille Van Landschoot", + uid: 4852501, + nodes: [ + 5607796820, 5607798725, 5607798721, 5607798722, 5607796819, 5607798723, + 5607796820, + ], + tags: { + access: "permissive", + amenity: "parking", + park_ride: "no", + parking: "surface", + surface: "paved", + }, + }, + { + type: "way", + id: 590167103, + timestamp: "2018-05-22T12:37:36Z", + version: 1, + changeset: 59179114, + user: "ForstEK", + uid: 1737608, + nodes: [5635001277, 5635001274, 5635001275, 5635001276, 5635001277], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167113, + timestamp: "2020-07-29T17:45:20Z", + version: 2, + changeset: 88691835, + user: "JanFi", + uid: 672253, + nodes: [ + 5635001312, 5635001306, 7767137237, 7767137235, 7767137236, 7767137234, + 5635001312, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167134, + timestamp: "2018-05-22T12:37:37Z", + version: 1, + changeset: 59179114, + user: "ForstEK", + uid: 1737608, + nodes: [5635001374, 5635001373, 5635001372, 5635001371, 5635001374], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167135, + timestamp: "2018-05-22T12:37:37Z", + version: 1, + changeset: 59179114, + user: "ForstEK", + uid: 1737608, + nodes: [5635001378, 5635001377, 5635001376, 5635001375, 5635001378], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167136, + timestamp: "2018-05-22T12:37:37Z", + version: 1, + changeset: 59179114, + user: "ForstEK", + uid: 1737608, + nodes: [5635001382, 5635001381, 5635001380, 5635001379, 5635001382], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167137, + timestamp: "2019-07-06T15:58:19Z", + version: 3, + changeset: 71962591, + user: "gjosch", + uid: 1776978, + nodes: [ + 5635001386, 5882873336, 5882873337, 5882873338, 5882873335, 6593340582, + 6593340583, 5882873334, 6593340584, 5635001385, 5635001384, 5635001383, + 5635001386, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 590167147, + timestamp: "2018-05-22T12:37:38Z", + version: 1, + changeset: 59179114, + user: "ForstEK", + uid: 1737608, + nodes: [5635001417, 5635001414, 5635001415, 5635001416, 5635001417], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 601406079, + timestamp: "2018-06-24T22:15:06Z", + version: 1, + changeset: 60131072, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [5716136617, 5716136618, 5716136619, 5716136620, 5716136617], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 632813009, + timestamp: "2018-10-11T09:24:42Z", + version: 1, + changeset: 63409297, + user: "Jakka", + uid: 2403313, + nodes: [ + 5974489618, 1810326044, 1810326087, 5974489617, 5972179348, 5972179347, + 5972179346, 5972179345, 5972179344, 5972179343, 5972179331, 5974489616, + 5974489615, 5974489614, 5974489618, + ], + tags: { amenity: "parking", name: "Gemeenteplein", parking: "surface" }, + }, + { + type: "way", + id: 668043297, + timestamp: "2019-04-10T18:34:27Z", + version: 2, + changeset: 69093378, + user: "RudolpheDeer", + uid: 9408828, + nodes: [6255587424, 6255587425, 6255587426, 6255587427, 6255587424], + tags: { access: "private", amenity: "parking" }, + }, + { + type: "way", + id: 670104236, + timestamp: "2019-02-13T00:05:02Z", + version: 1, + changeset: 67147239, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [6275462768, 6275462769, 6275462770, 6275462771, 6275462768], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104238, + timestamp: "2019-02-13T00:05:02Z", + version: 1, + changeset: 67147239, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [6275462772, 6275462989, 6275462773, 6275462774, 6275462775, 6275462772], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104239, + timestamp: "2019-02-13T00:05:02Z", + version: 1, + changeset: 67147239, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [6275462776, 6275462777, 6275462778, 6275462779, 6275462776], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104241, + timestamp: "2019-02-13T00:05:02Z", + version: 1, + changeset: 67147239, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [6275462780, 6275462781, 6275462782, 6275462783, 6275462780], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 670104242, + timestamp: "2019-02-13T00:05:02Z", + version: 1, + changeset: 67147239, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [6275462784, 6275462985, 6275462988, 6275462986, 6275462987, 6275462784], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 671840055, + timestamp: "2019-02-20T15:15:45Z", + version: 1, + changeset: 67395313, + user: "Tim Couwelier", + uid: 7246683, + nodes: [ + 6291339827, 6291339828, 6291339816, 6291339815, 6291339822, 6291339821, + 6291339829, 6291339830, 6291339827, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 695825223, + timestamp: "2019-06-08T15:19:24Z", + version: 1, + changeset: 71053301, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 1519476746, 6533893620, 6533893621, 6533893622, 1519476797, 1519476620, + 1519476746, + ], + tags: { access: "yes", amenity: "parking" }, + }, + { + type: "way", + id: 695825224, + timestamp: "2019-06-08T15:19:24Z", + version: 1, + changeset: 71053301, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 1519476744, 6533893624, 6533893625, 6533893626, 1519476698, 1519476635, + 1519476744, + ], + tags: { access: "yes", amenity: "parking" }, + }, + { + type: "way", + id: 696040917, + timestamp: "2019-06-09T23:24:59Z", + version: 2, + changeset: 71082992, + user: "Hopperpop", + uid: 3664604, + nodes: [6536026850, 6536026851, 6536026852, 6536026853, 6536026850], + tags: { amenity: "parking", name: "Kasteel Tudor" }, + }, + { + type: "way", + id: 696043218, + timestamp: "2019-06-09T23:24:59Z", + version: 1, + changeset: 71082992, + user: "Hopperpop", + uid: 3664604, + nodes: [6536038234, 6536038235, 6536038236, 6536038237, 6536038234], + tags: { access: "customers", amenity: "parking" }, + }, + { + type: "way", + id: 700675991, + timestamp: "2020-12-18T10:48:20Z", + version: 2, + changeset: 96062619, + user: "Hopperpop", + uid: 3664604, + nodes: [6579962064, 6579962065, 6579962066, 6579962068, 6579962067, 6579962064], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 705278707, + timestamp: "2020-09-30T20:36:55Z", + version: 2, + changeset: 91787895, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 6625037999, 6625038000, 6625038001, 6625038002, 6625038003, 6004375826, + 6625038005, 6625038006, 6625037999, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 719482833, + timestamp: "2019-08-28T21:18:49Z", + version: 1, + changeset: 73857967, + user: "Hopperpop", + uid: 3664604, + nodes: [6754312544, 6754312543, 6754312542, 6754312541, 6754312544], + tags: { access: "yes", amenity: "parking", capacity: "5" }, + }, + { + type: "way", + id: 719482834, + timestamp: "2019-08-28T21:18:49Z", + version: 1, + changeset: 73857967, + user: "Hopperpop", + uid: 3664604, + nodes: [6754312565, 6754312564, 6754312563, 6754312562, 6754312565], + tags: { access: "yes", amenity: "parking", capacity: "12" }, + }, + { + type: "way", + id: 737054013, + timestamp: "2019-10-20T15:39:32Z", + version: 1, + changeset: 75957554, + user: "Hopperpop", + uid: 3664604, + nodes: [5826811496, 5826811497, 5826811494, 5826811495, 5826811496], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737054014, + timestamp: "2019-10-20T15:39:32Z", + version: 1, + changeset: 75957554, + user: "Hopperpop", + uid: 3664604, + nodes: [5826810676, 5826810673, 5826810674, 5826810675, 5826810676], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737054015, + timestamp: "2019-10-20T15:39:32Z", + version: 1, + changeset: 75957554, + user: "Hopperpop", + uid: 3664604, + nodes: [5826810681, 5826810678, 5826810679, 5826810680, 5826810681], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 737093410, + timestamp: "2021-08-14T21:52:07Z", + version: 2, + changeset: 109683899, + user: "effem", + uid: 16437, + nodes: [5826811559, 5826811536, 5826811535, 5826811561, 5826811560, 5826811559], + tags: { + access: "yes", + amenity: "parking", + capacity: "4", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 737093411, + timestamp: "2021-08-14T21:52:03Z", + version: 2, + changeset: 109683899, + user: "effem", + uid: 16437, + nodes: [5826811551, 5826811547, 5826811548, 5826811549, 5826811550, 5826811551], + tags: { + access: "yes", + amenity: "parking", + capacity: "4", + fee: "no", + parking: "surface", + }, + }, + { + type: "way", + id: 739652949, + timestamp: "2019-10-28T20:18:16Z", + version: 1, + changeset: 76314556, + user: "Hopperpop", + uid: 3664604, + nodes: [6925536542, 6925536541, 6925536540, 6925536539, 6925536542], + tags: { amenity: "parking", capacity: "6", parking: "surface" }, + }, + { + type: "way", + id: 741675236, + timestamp: "2020-12-17T22:07:55Z", + version: 4, + changeset: 96029554, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 6943148207, 6943148206, 6943148205, 6943148204, 1637742821, 6943148203, + 6943148202, 6943148201, 6943148200, 6943148199, 6943148198, 6943148197, + 6943148207, + ], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 742295526, + timestamp: "2019-11-05T19:27:15Z", + version: 1, + changeset: 76664875, + user: "Hopperpop", + uid: 3664604, + nodes: [6949357909, 6949357908, 6949357907, 6949357906, 6949357909], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295527, + timestamp: "2019-11-05T19:27:15Z", + version: 1, + changeset: 76664875, + user: "Hopperpop", + uid: 3664604, + nodes: [6949357913, 6949357912, 6949357911, 6949357910, 6949357913], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295528, + timestamp: "2019-11-05T19:27:15Z", + version: 1, + changeset: 76664875, + user: "Hopperpop", + uid: 3664604, + nodes: [6949357917, 6949357916, 6949357915, 6949357914, 6949357917], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 742295529, + timestamp: "2019-11-05T19:27:15Z", + version: 1, + changeset: 76664875, + user: "Hopperpop", + uid: 3664604, + nodes: [6949357921, 6949357920, 6949357919, 6949357918, 6949357921], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 746170866, + timestamp: "2019-11-16T15:27:02Z", + version: 1, + changeset: 77167609, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 6982906547, 6982906546, 6982906545, 6982906544, 6982906543, 6982906542, + 6982906547, + ], + tags: { access: "customers", amenity: "parking" }, + }, + { + type: "way", + id: 747880657, + timestamp: "2021-09-19T12:40:45Z", + version: 2, + changeset: 111407274, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 3325315397, 6997076566, 6997076565, 6997076563, 6997076562, 6997076564, + 3325315395, 3325315397, + ], + tags: { access: "customers", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 763977465, + timestamp: "2020-12-17T21:59:46Z", + version: 2, + changeset: 96029554, + user: "Hopperpop", + uid: 3664604, + nodes: [7137343680, 7137343681, 7137343682, 7137343683, 7137343680], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 763977466, + timestamp: "2020-12-17T21:59:40Z", + version: 2, + changeset: 96029554, + user: "Hopperpop", + uid: 3664604, + nodes: [7137343684, 7137383185, 7137383186, 7137383187, 7137343684], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821090, + timestamp: "2020-05-14T17:37:10Z", + version: 1, + changeset: 85215781, + user: "Hopperpop", + uid: 3664604, + nodes: [7519058290, 7519058289, 7519058288, 7519058287, 7519058290], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821091, + timestamp: "2020-05-14T17:37:10Z", + version: 1, + changeset: 85215781, + user: "Hopperpop", + uid: 3664604, + nodes: [7519058294, 7519058293, 7519058292, 7519058291, 7519058294], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821092, + timestamp: "2020-05-14T17:37:10Z", + version: 1, + changeset: 85215781, + user: "Hopperpop", + uid: 3664604, + nodes: [7519058298, 7519058297, 7519058296, 7519058295, 7519058298], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 803821093, + timestamp: "2020-05-14T17:37:10Z", + version: 1, + changeset: 85215781, + user: "Hopperpop", + uid: 3664604, + nodes: [7519058302, 7519058301, 7519058300, 7519058299, 7519058302], + tags: { access: "private", amenity: "parking", capacity: "6", parking: "surface" }, + }, + { + type: "way", + id: 804963962, + timestamp: "2020-05-17T17:09:31Z", + version: 1, + changeset: 85342199, + user: "Hopperpop", + uid: 3664604, + nodes: [ + 7529417225, 7529417226, 7529417227, 7529417228, 7529417229, 7529417230, + 7529417232, 7529417225, + ], + tags: { + access: "customers", + amenity: "parking", + fee: "no", + operator: "’t Kiekekot", + parking: "surface", + surface: "unpaved", + }, + }, + { + type: "way", + id: 806875503, + timestamp: "2020-05-21T15:48:37Z", + version: 1, + changeset: 85563652, + user: "Hopperpop", + uid: 3664604, + nodes: [4042671969, 7545532512, 7545532514, 7545532513, 3359977305, 4042671969], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 806963547, + timestamp: "2020-05-21T21:16:34Z", + version: 1, + changeset: 85574048, + user: "Hopperpop", + uid: 3664604, + nodes: [7546193222, 7546193221, 7546193220, 7546193219, 7546193222], + tags: { access: "private", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 865357121, + timestamp: "2020-10-30T14:45:23Z", + version: 1, + changeset: 93296961, + user: "L'imaginaire", + uid: 654234, + nodes: [8065883228, 8065883227, 8065883226, 8065883225, 8065883228], + tags: { amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 865357122, + timestamp: "2020-10-30T14:45:23Z", + version: 1, + changeset: 93296961, + user: "L'imaginaire", + uid: 654234, + nodes: [8065883233, 8065883230, 8065883229, 8065883232, 8065883233], + tags: { amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 879281221, + timestamp: "2020-11-30T14:18:18Z", + version: 1, + changeset: 95050429, + user: "M!dgard", + uid: 763799, + nodes: [ + 8179735269, 8179735268, 8179735267, 8179735266, 8179735265, 8179735264, + 8179735224, 8179735269, + ], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 881770201, + timestamp: "2020-12-07T00:46:56Z", + version: 1, + changeset: 95385582, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [8200275847, 8200275848, 8200275844, 8200275853, 8200275849, 8200275847], + tags: { amenity: "parking", parking: "surface", surface: "grass" }, + }, + { + type: "way", + id: 978360549, + timestamp: "2021-09-01T08:53:08Z", + version: 1, + changeset: 110553113, + user: "JanFi", + uid: 672253, + nodes: [5761770202, 5761770204, 9052878229, 9052878228, 5761770202], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 1009692722, + timestamp: "2021-12-06T18:34:20Z", + version: 1, + changeset: 114629990, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 9316118540, 9316118536, 9316118531, 9316118535, 9316118534, 9316118533, + 9316118530, 9316118532, 3311835478, 9316118540, + ], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "relation", + id: 8188853, + timestamp: "2020-07-05T12:38:48Z", + version: 13, + changeset: 87554177, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 577572397, role: "outer" }, + { type: "way", ref: 577572399, role: "outer" }, + ], + tags: { + access: "guided", + curator: "Luc Maene;Geert De Clercq", + email: "lucmaene@hotmail.com;geert.de.clercq1@pandora.be", + landuse: "meadow", + leisure: "nature_reserve", + name: "De Wulgenbroeken", + natural: "wetland", + operator: "Natuurpunt Brugge", + type: "multipolygon", + website: "https://natuurpuntbrugge.be/wulgenbroeken/", + wetland: "wet_meadow", + wikidata: "Q60061498", + wikipedia: "nl:Wulgenbroeken", + }, + }, + { + type: "relation", + id: 11163488, + timestamp: "2021-10-04T14:09:47Z", + version: 15, + changeset: 112079863, + user: "DieterWesttoer", + uid: 13062237, + members: [ + { type: "way", ref: 810604915, role: "outer" }, + { type: "way", ref: 989393316, role: "outer" }, + { type: "way", ref: 389026405, role: "inner" }, + { type: "way", ref: 810607458, role: "outer" }, + ], + tags: { + access: "yes", + curator: "Kris Lesage", + description: + "Wat Doeveren zo uniek maakt, zijn zijn kleine heidegebiedjes met soorten die erg verschillen van de Kempense heide. Doeveren en omstreken was vroeger één groot heidegebied, maar bestaat nu grotendeels uit bossen.", + dog: "leashed", + email: "doeveren@natuurpuntzedelgem.be", + image: "https://i.imgur.com/NEAsQZG.jpg", + "image:0": "https://i.imgur.com/Dq71hyQ.jpg", + "image:1": "https://i.imgur.com/mAIiT4f.jpg", + "image:2": "https://i.imgur.com/dELZU97.jpg", + "image:3": "https://i.imgur.com/Bso57JC.jpg", + "image:4": "https://i.imgur.com/9DtcfXo.jpg", + "image:5": "https://i.imgur.com/0R6eBfk.jpg", + "image:6": "https://i.imgur.com/b0JpvbR.jpg", + leisure: "nature_reserve", + name: "Doeveren", + operator: "Natuurpunt Zedelgem", + phone: "+32 486 25 25 30", + type: "multipolygon", + website: "https://www.natuurpuntzedelgem.be/gebieden/doeveren/", + wikidata: "Q56395754", + wikipedia: "nl:Doeveren (natuurgebied)", + }, + }, + { + type: "relation", + id: 11790117, + timestamp: "2020-10-24T19:11:01Z", + version: 1, + changeset: 92997462, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 863373849, role: "outer" }, + { type: "way", ref: 777280458, role: "outer" }, + ], + tags: { + access: "no", + "description:0": "In gebruik als waterbuffering", + leisure: "nature_reserve", + name: "Kerkebeek", + operator: "Natuurpunt Brugge", + type: "multipolygon", + }, + }, + { type: "node", id: 1038638696, lat: 51.1197075, lon: 3.1827007 }, + { type: "node", id: 7578975029, lat: 51.1199041, lon: 3.1828945 }, + { type: "node", id: 7578975030, lat: 51.1201514, lon: 3.1831942 }, + { type: "node", id: 1038638743, lat: 51.1202445, lon: 3.1894878 }, + { type: "node", id: 7578975002, lat: 51.1202598, lon: 3.1897155 }, + { type: "node", id: 7578975007, lat: 51.1199771, lon: 3.1863015 }, + { type: "node", id: 7578975008, lat: 51.1199523, lon: 3.1863031 }, + { type: "node", id: 7578975009, lat: 51.1199279, lon: 3.1859524 }, + { type: "node", id: 1168728109, lat: 51.1275839, lon: 3.1765505 }, + { type: "node", id: 1168728158, lat: 51.1278835, lon: 3.1763986 }, + { type: "node", id: 1168728159, lat: 51.1276298, lon: 3.1767808 }, + { type: "node", id: 5637669355, lat: 51.1276039, lon: 3.1766509 }, + { type: "node", id: 1038638723, lat: 51.1272818, lon: 3.184601 }, + { type: "node", id: 7554434438, lat: 51.1225298, lon: 3.1847624 }, + { type: "node", id: 7578865273, lat: 51.122084, lon: 3.1846859 }, + { type: "node", id: 7578975032, lat: 51.1215762, lon: 3.1842866 }, + { type: "node", id: 1168727876, lat: 51.1290569, lon: 3.1766033 }, + { type: "node", id: 1168728208, lat: 51.1289763, lon: 3.1767696 }, + { type: "node", id: 1168728288, lat: 51.1291195, lon: 3.1766799 }, + { type: "node", id: 1168728325, lat: 51.1279295, lon: 3.1766288 }, + { type: "node", id: 1168728412, lat: 51.1290389, lon: 3.1768463 }, + { type: "node", id: 1038638712, lat: 51.1282367, lon: 3.1840296 }, + { type: "node", id: 1168727824, lat: 51.1312478, lon: 3.182233 }, + { type: "node", id: 3922375256, lat: 51.1301155, lon: 3.1848042 }, + { type: "node", id: 3922380071, lat: 51.1304048, lon: 3.1838954 }, + { type: "node", id: 3922380081, lat: 51.1305694, lon: 3.1845093 }, + { type: "node", id: 3922380086, lat: 51.1306445, lon: 3.1848152 }, + { type: "node", id: 3922380092, lat: 51.1307378, lon: 3.1849591 }, + { type: "node", id: 7577430793, lat: 51.1289492, lon: 3.1836032 }, + { type: "node", id: 7578975024, lat: 51.1299598, lon: 3.1841704 }, + { type: "node", id: 7578975028, lat: 51.1322547, lon: 3.1833542 }, + { type: "node", id: 7578975049, lat: 51.1313772, lon: 3.1838431 }, + { type: "node", id: 9167054153, lat: 51.1310258, lon: 3.1823668 }, + { type: "node", id: 9167054154, lat: 51.13145, lon: 3.1841492 }, + { type: "node", id: 9167054156, lat: 51.1316731, lon: 3.1850331 }, + { type: "node", id: 9274761589, lat: 51.1297088, lon: 3.1831312 }, + { type: "node", id: 9274761596, lat: 51.1296735, lon: 3.1831518 }, + { type: "node", id: 929120698, lat: 51.1276376, lon: 3.1903134 }, + { type: "node", id: 1038638592, lat: 51.1264889, lon: 3.19027 }, + { type: "node", id: 1038638661, lat: 51.1258582, lon: 3.1854626 }, + { type: "node", id: 1038638721, lat: 51.125636, lon: 3.1855855 }, + { type: "node", id: 1038638753, lat: 51.123845, lon: 3.1880289 }, + { type: "node", id: 3921878998, lat: 51.1255719, lon: 3.1902463 }, + { type: "node", id: 3921879004, lat: 51.1275463, lon: 3.188843 }, + { type: "node", id: 3921879011, lat: 51.1271626, lon: 3.1872368 }, + { type: "node", id: 3921879019, lat: 51.1277666, lon: 3.1868505 }, + { type: "node", id: 7554434436, lat: 51.1252645, lon: 3.1852941 }, + { type: "node", id: 7578865274, lat: 51.1230564, lon: 3.187978 }, + { type: "node", id: 7578865275, lat: 51.1226417, lon: 3.188075 }, + { type: "node", id: 7578904489, lat: 51.1247504, lon: 3.1900249 }, + { type: "node", id: 7578974988, lat: 51.1223221, lon: 3.1906513 }, + { type: "node", id: 7578974989, lat: 51.1224255, lon: 3.1905646 }, + { type: "node", id: 7578974990, lat: 51.1224672, lon: 3.1905195 }, + { type: "node", id: 7578974991, lat: 51.1228709, lon: 3.1901867 }, + { type: "node", id: 7578974992, lat: 51.1229568, lon: 3.1901459 }, + { type: "node", id: 7578974995, lat: 51.123814, lon: 3.1899138 }, + { type: "node", id: 7578974996, lat: 51.1239199, lon: 3.189925 }, + { type: "node", id: 7578974997, lat: 51.1244111, lon: 3.1899686 }, + { type: "node", id: 7578974998, lat: 51.1248503, lon: 3.190215 }, + { type: "node", id: 7578974999, lat: 51.1247917, lon: 3.1900516 }, + { type: "node", id: 7578975000, lat: 51.1248293, lon: 3.1900978 }, + { type: "node", id: 7578975001, lat: 51.1248444, lon: 3.1901483 }, + { type: "node", id: 7578975035, lat: 51.1250798, lon: 3.1851611 }, + { type: "node", id: 7578975045, lat: 51.1278881, lon: 3.1882941 }, + { type: "node", id: 9199177059, lat: 51.1256647, lon: 3.1855696 }, + { type: "node", id: 7578987409, lat: 51.1232707, lon: 3.1920382 }, + { type: "node", id: 7578987413, lat: 51.1260416, lon: 3.1973652 }, + { type: "node", id: 7578987414, lat: 51.1263443, lon: 3.1973775 }, + { type: "node", id: 7578987416, lat: 51.1278708, lon: 3.1974134 }, + { type: "node", id: 7578987417, lat: 51.126749, lon: 3.197258 }, + { type: "node", id: 1675648152, lat: 51.1281877, lon: 3.1903323 }, + { type: "node", id: 2732486274, lat: 51.1302553, lon: 3.1886305 }, + { type: "node", id: 3921879018, lat: 51.1280809, lon: 3.188102 }, + { type: "node", id: 3922380061, lat: 51.1301929, lon: 3.1895402 }, + { type: "node", id: 3922380083, lat: 51.1305788, lon: 3.1884337 }, + { type: "node", id: 3922380095, lat: 51.130784, lon: 3.1852632 }, + { type: "node", id: 7578865281, lat: 51.1283938, lon: 3.1903716 }, + { type: "node", id: 7578865283, lat: 51.1288414, lon: 3.1904911 }, + { type: "node", id: 7578960079, lat: 51.1303025, lon: 3.1855669 }, + { type: "node", id: 7578975012, lat: 51.1294792, lon: 3.1906231 }, + { type: "node", id: 7578975015, lat: 51.1297374, lon: 3.1907018 }, + { type: "node", id: 7578975016, lat: 51.1300451, lon: 3.1907679 }, + { type: "node", id: 7578975018, lat: 51.1305785, lon: 3.186668 }, + { type: "node", id: 7578975019, lat: 51.130956, lon: 3.1881852 }, + { type: "node", id: 7578975021, lat: 51.1303082, lon: 3.1855908 }, + { type: "node", id: 7578975026, lat: 51.1310167, lon: 3.1861981 }, + { type: "node", id: 7578975027, lat: 51.1318642, lon: 3.1857905 }, + { type: "node", id: 7578975040, lat: 51.1280348, lon: 3.1889503 }, + { type: "node", id: 7578975044, lat: 51.1279308, lon: 3.1875031 }, + { type: "node", id: 7578975046, lat: 51.1279329, lon: 3.1881992 }, + { type: "node", id: 9167054157, lat: 51.1308023, lon: 3.1852517 }, + { type: "node", id: 9274761591, lat: 51.1310994, lon: 3.1862668 }, + { type: "node", id: 9274761592, lat: 51.1310643, lon: 3.1862655 }, + { type: "node", id: 9274761593, lat: 51.1310386, lon: 3.1862393 }, + { type: "node", id: 7578987418, lat: 51.128003, lon: 3.1959031 }, + { type: "node", id: 7578987419, lat: 51.1282167, lon: 3.193723 }, + { type: "node", id: 5745833239, lat: 51.1258572, lon: 3.1996713 }, + { type: "node", id: 5745833240, lat: 51.1257519, lon: 3.1995939 }, + { type: "node", id: 5745833241, lat: 51.1253365, lon: 3.1991234 }, + { type: "node", id: 7578987410, lat: 51.1243814, lon: 3.1988516 }, + { type: "node", id: 7578987411, lat: 51.1243992, lon: 3.1989686 }, + { type: "node", id: 7578987412, lat: 51.1259883, lon: 3.1997074 }, + { type: "node", id: 7578987415, lat: 51.1260745, lon: 3.1997142 }, + { type: "node", id: 1590642829, lat: 51.1333867, lon: 3.2308055 }, + { type: "node", id: 1590642832, lat: 51.1334371, lon: 3.2308262 }, + { type: "node", id: 1590642849, lat: 51.1336392, lon: 3.2305316 }, + { type: "node", id: 1590642858, lat: 51.133659, lon: 3.2303991 }, + { type: "node", id: 1590642859, lat: 51.1336899, lon: 3.2305508 }, + { type: "node", id: 1590642860, lat: 51.1337096, lon: 3.2304183 }, + { type: "node", id: 1590642828, lat: 51.1333653, lon: 3.2309374 }, + { type: "node", id: 1590642830, lat: 51.1334157, lon: 3.2309581 }, + { type: "node", id: 3311835478, lat: 51.133195, lon: 3.2334351 }, + { type: "node", id: 9316118530, lat: 51.1331607, lon: 3.2333604 }, + { type: "node", id: 9316118531, lat: 51.1330927, lon: 3.2334241 }, + { type: "node", id: 9316118532, lat: 51.1331767, lon: 3.233347 }, + { type: "node", id: 9316118533, lat: 51.133147, lon: 3.2333808 }, + { type: "node", id: 9316118534, lat: 51.1331302, lon: 3.2334006 }, + { type: "node", id: 9316118535, lat: 51.1331127, lon: 3.2334144 }, + { type: "node", id: 9316118536, lat: 51.1330784, lon: 3.2334281 }, + { type: "node", id: 9316118540, lat: 51.1330946, lon: 3.2335103 }, + { type: "node", id: 6579962064, lat: 51.1367061, lon: 3.1640546 }, + { type: "node", id: 6579962065, lat: 51.1361156, lon: 3.1646435 }, + { type: "node", id: 6579962066, lat: 51.1357413, lon: 3.1637334 }, + { type: "node", id: 6579962067, lat: 51.1359511, lon: 3.1637408 }, + { type: "node", id: 6579962068, lat: 51.1359389, lon: 3.1638093 }, + { type: "node", id: 7546193219, lat: 51.1456739, lon: 3.1637073 }, + { type: "node", id: 7546193220, lat: 51.1455706, lon: 3.1643444 }, + { type: "node", id: 7546193221, lat: 51.1456623, lon: 3.1643821 }, + { type: "node", id: 7546193222, lat: 51.1457656, lon: 3.1637451 }, + { type: "node", id: 3359977305, lat: 51.1464982, lon: 3.1689911 }, + { type: "node", id: 4042671969, lat: 51.1461398, lon: 3.169233 }, + { type: "node", id: 7545532512, lat: 51.1463103, lon: 3.169498 }, + { type: "node", id: 7545532513, lat: 51.1466008, lon: 3.1695349 }, + { type: "node", id: 7545532514, lat: 51.1464331, lon: 3.16962 }, + { type: "node", id: 8200275848, lat: 51.1409105, lon: 3.1800288 }, + { type: "node", id: 8200275844, lat: 51.1416967, lon: 3.1797728 }, + { type: "node", id: 8200275847, lat: 51.1411211, lon: 3.1805153 }, + { type: "node", id: 8200275849, lat: 51.1417397, lon: 3.1802115 }, + { type: "node", id: 8200275853, lat: 51.1417351, lon: 3.1801651 }, + { type: "node", id: 111759500, lat: 51.1483444, lon: 3.1886487 }, + { type: "node", id: 1169056712, lat: 51.1482974, lon: 3.1884805 }, + { type: "node", id: 3325315226, lat: 51.147926, lon: 3.1887015 }, + { type: "node", id: 3325315230, lat: 51.1479856, lon: 3.1889124 }, + { type: "node", id: 3325315232, lat: 51.1480454, lon: 3.1891027 }, + { type: "node", id: 3325315243, lat: 51.1483285, lon: 3.1885919 }, + { type: "node", id: 3325315247, lat: 51.1484007, lon: 3.1888131 }, + { type: "node", id: 5826810673, lat: 51.1512507, lon: 3.1914885 }, + { type: "node", id: 5826810674, lat: 51.15126, lon: 3.1914533 }, + { type: "node", id: 5826810675, lat: 51.1510883, lon: 3.1912776 }, + { type: "node", id: 5826810676, lat: 51.151057, lon: 3.1912906 }, + { type: "node", id: 5826810678, lat: 51.1509897, lon: 3.1911759 }, + { type: "node", id: 5826810679, lat: 51.1510025, lon: 3.1911334 }, + { type: "node", id: 5826810680, lat: 51.1509352, lon: 3.1908874 }, + { type: "node", id: 5826810681, lat: 51.1509074, lon: 3.1908689 }, + { type: "node", id: 5826811494, lat: 51.1512125, lon: 3.1911315 }, + { type: "node", id: 5826811495, lat: 51.1512055, lon: 3.191187 }, + { type: "node", id: 5826811496, lat: 51.151296, lon: 3.1914182 }, + { type: "node", id: 5826811497, lat: 51.1513261, lon: 3.1914182 }, + { type: "node", id: 5826811535, lat: 51.1517839, lon: 3.1959375 }, + { type: "node", id: 5826811536, lat: 51.1518581, lon: 3.1959708 }, + { type: "node", id: 5826811547, lat: 51.1515913, lon: 3.196115 }, + { type: "node", id: 5826811548, lat: 51.1516307, lon: 3.1961465 }, + { type: "node", id: 5826811549, lat: 51.1516435, lon: 3.1961076 }, + { type: "node", id: 5826811550, lat: 51.1516087, lon: 3.1959541 }, + { type: "node", id: 5826811551, lat: 51.1515437, lon: 3.1959024 }, + { type: "node", id: 5826811559, lat: 51.1517896, lon: 3.1957717 }, + { type: "node", id: 5826811560, lat: 51.1517417, lon: 3.1957528 }, + { type: "node", id: 5826811561, lat: 51.1517365, lon: 3.1957872 }, + { type: "node", id: 3325315395, lat: 51.1544187, lon: 3.1856685 }, + { type: "node", id: 3325315397, lat: 51.1545358, lon: 3.1860117 }, + { type: "node", id: 3930713426, lat: 51.1571474, lon: 3.1889324 }, + { type: "node", id: 3930713429, lat: 51.1573669, lon: 3.1883265 }, + { type: "node", id: 3930713430, lat: 51.1573202, lon: 3.1890776 }, + { type: "node", id: 3930713435, lat: 51.1574561, lon: 3.1883916 }, + { type: "node", id: 3930713437, lat: 51.1574971, lon: 3.1893349 }, + { type: "node", id: 3930713438, lat: 51.1574907, lon: 3.1884223 }, + { type: "node", id: 3930713440, lat: 51.1575693, lon: 3.1890951 }, + { type: "node", id: 3930713442, lat: 51.1575932, lon: 3.1879961 }, + { type: "node", id: 3930713446, lat: 51.1577118, lon: 3.1885611 }, + { type: "node", id: 3930713447, lat: 51.157698, lon: 3.1894886 }, + { type: "node", id: 3930713451, lat: 51.1577702, lon: 3.1892488 }, + { type: "node", id: 3930713454, lat: 51.1577969, lon: 3.1882567 }, + { type: "node", id: 4043782112, lat: 51.1549447, lon: 3.1864571 }, + { type: "node", id: 5552466013, lat: 51.1545783, lon: 3.1874672 }, + { type: "node", id: 5552466014, lat: 51.1543143, lon: 3.1873045 }, + { type: "node", id: 5552466018, lat: 51.1545999, lon: 3.1873846 }, + { type: "node", id: 5552466020, lat: 51.1548079, lon: 3.1868846 }, + { type: "node", id: 5825400688, lat: 51.1549303, lon: 3.1867969 }, + { type: "node", id: 5826811614, lat: 51.1572659, lon: 3.1885288 }, + { type: "node", id: 6949357906, lat: 51.1572911, lon: 3.1915323 }, + { type: "node", id: 6949357909, lat: 51.1573109, lon: 3.1914572 }, + { type: "node", id: 6949357910, lat: 51.1574373, lon: 3.1913744 }, + { type: "node", id: 6949357911, lat: 51.1574179, lon: 3.1914502 }, + { type: "node", id: 6949357912, lat: 51.1575109, lon: 3.1915112 }, + { type: "node", id: 6949357913, lat: 51.1575304, lon: 3.1914354 }, + { type: "node", id: 6949357914, lat: 51.1574638, lon: 3.1912712 }, + { type: "node", id: 6949357915, lat: 51.1574444, lon: 3.1913473 }, + { type: "node", id: 6949357916, lat: 51.1575599, lon: 3.191423 }, + { type: "node", id: 6949357917, lat: 51.1575803, lon: 3.1913473 }, + { type: "node", id: 6949357918, lat: 51.157501, lon: 3.1911303 }, + { type: "node", id: 6949357919, lat: 51.1574808, lon: 3.1912058 }, + { type: "node", id: 6949357920, lat: 51.1577374, lon: 3.1913724 }, + { type: "node", id: 6949357921, lat: 51.1577563, lon: 3.1912987 }, + { type: "node", id: 6982605752, lat: 51.1574664, lon: 3.1885975 }, + { type: "node", id: 6982605754, lat: 51.1574428, lon: 3.1885793 }, + { type: "node", id: 6982605762, lat: 51.1576066, lon: 3.1884793 }, + { type: "node", id: 6982605764, lat: 51.1576853, lon: 3.18854 }, + { type: "node", id: 6982605766, lat: 51.1575125, lon: 3.1884502 }, + { type: "node", id: 6982605767, lat: 51.1575959, lon: 3.1885145 }, + { type: "node", id: 6982605769, lat: 51.1575152, lon: 3.1884412 }, + { type: "node", id: 6982906542, lat: 51.1591649, lon: 3.1904238 }, + { type: "node", id: 6982906543, lat: 51.1592937, lon: 3.1905221 }, + { type: "node", id: 6982906544, lat: 51.1593438, lon: 3.1903659 }, + { type: "node", id: 6982906545, lat: 51.1593158, lon: 3.1903453 }, + { type: "node", id: 6982906546, lat: 51.1593351, lon: 3.1902852 }, + { type: "node", id: 6982906547, lat: 51.1592385, lon: 3.1902052 }, + { type: "node", id: 6997076562, lat: 51.1546633, lon: 3.1858333 }, + { type: "node", id: 6997076563, lat: 51.1546293, lon: 3.1858642 }, + { type: "node", id: 6997076564, lat: 51.154594, lon: 3.1856715 }, + { type: "node", id: 6997076565, lat: 51.1546727, lon: 3.1859889 }, + { type: "node", id: 6997076566, lat: 51.1545545, lon: 3.1860687 }, + { type: "node", id: 6997076567, lat: 51.1543063, lon: 3.1869337 }, + { type: "node", id: 6997096756, lat: 51.1547387, lon: 3.1868376 }, + { type: "node", id: 6997096758, lat: 51.1546899, lon: 3.1870707 }, + { type: "node", id: 6997096759, lat: 51.1546751, lon: 3.1870601 }, + { type: "node", id: 7137343680, lat: 51.1558646, lon: 3.1876808 }, + { type: "node", id: 7137343681, lat: 51.1558466, lon: 3.1877456 }, + { type: "node", id: 7137343682, lat: 51.1555824, lon: 3.187559 }, + { type: "node", id: 7137343683, lat: 51.1556004, lon: 3.1874941 }, + { type: "node", id: 7137343684, lat: 51.1555549, lon: 3.1874612 }, + { type: "node", id: 7137383185, lat: 51.1555369, lon: 3.1875268 }, + { type: "node", id: 7137383186, lat: 51.1553925, lon: 3.1874261 }, + { type: "node", id: 7137383187, lat: 51.1554105, lon: 3.1873604 }, + { type: "node", id: 7519058287, lat: 51.1555296, lon: 3.1858152 }, + { type: "node", id: 7519058288, lat: 51.1555027, lon: 3.1858752 }, + { type: "node", id: 7519058289, lat: 51.1556329, lon: 3.1860234 }, + { type: "node", id: 7519058290, lat: 51.1556597, lon: 3.1859634 }, + { type: "node", id: 7519058291, lat: 51.1557039, lon: 3.1860155 }, + { type: "node", id: 7519058292, lat: 51.1556789, lon: 3.1860736 }, + { type: "node", id: 7519058293, lat: 51.1558105, lon: 3.1862177 }, + { type: "node", id: 7519058294, lat: 51.1558355, lon: 3.1861597 }, + { type: "node", id: 7519058295, lat: 51.1557209, lon: 3.185828 }, + { type: "node", id: 7519058296, lat: 51.1556932, lon: 3.1858902 }, + { type: "node", id: 7519058297, lat: 51.1558686, lon: 3.1860888 }, + { type: "node", id: 7519058298, lat: 51.1558963, lon: 3.1860265 }, + { type: "node", id: 7519058299, lat: 51.1555421, lon: 3.1856365 }, + { type: "node", id: 7519058300, lat: 51.1555168, lon: 3.1856923 }, + { type: "node", id: 7519058301, lat: 51.1556479, lon: 3.1858437 }, + { type: "node", id: 7519058302, lat: 51.1556732, lon: 3.1857879 }, + { type: "node", id: 150996092, lat: 51.1554945, lon: 3.1954639 }, + { type: "node", id: 150996093, lat: 51.155531, lon: 3.1953168 }, + { type: "node", id: 150996094, lat: 51.1554912, lon: 3.1943936 }, + { type: "node", id: 150996095, lat: 51.155456, lon: 3.1942488 }, + { type: "node", id: 150996097, lat: 51.155432, lon: 3.1942095 }, + { type: "node", id: 150996098, lat: 51.155397, lon: 3.1941906 }, + { type: "node", id: 150996099, lat: 51.1552938, lon: 3.1945134 }, + { type: "node", id: 150996100, lat: 51.1552701, lon: 3.1949002 }, + { type: "node", id: 150996101, lat: 51.1553901, lon: 3.1953415 }, + { type: "node", id: 1015567837, lat: 51.1603457, lon: 3.1926746 }, + { type: "node", id: 1015583939, lat: 51.1598982, lon: 3.1934595 }, + { type: "node", id: 1659850846, lat: 51.1562462, lon: 3.1925019 }, + { type: "node", id: 1811699776, lat: 51.1604859, lon: 3.1920734 }, + { type: "node", id: 1811699777, lat: 51.1605587, lon: 3.1917984 }, + { type: "node", id: 1817319289, lat: 51.1599853, lon: 3.1935159 }, + { type: "node", id: 1817319290, lat: 51.1600476, lon: 3.1933385 }, + { type: "node", id: 1817319291, lat: 51.1600745, lon: 3.1934309 }, + { type: "node", id: 1817319292, lat: 51.1602073, lon: 3.1941751 }, + { type: "node", id: 1817319293, lat: 51.160208, lon: 3.1941098 }, + { type: "node", id: 1817319294, lat: 51.1602178, lon: 3.1935425 }, + { type: "node", id: 1817319295, lat: 51.1602385, lon: 3.19297 }, + { type: "node", id: 1817319296, lat: 51.1602972, lon: 3.1940248 }, + { type: "node", id: 1817319297, lat: 51.1603313, lon: 3.193809 }, + { type: "node", id: 1817319298, lat: 51.1603427, lon: 3.1930326 }, + { type: "node", id: 1817319299, lat: 51.1603716, lon: 3.1940192 }, + { type: "node", id: 1817319300, lat: 51.1603777, lon: 3.1946319 }, + { type: "node", id: 1817319301, lat: 51.1604665, lon: 3.193304 }, + { type: "node", id: 1817319302, lat: 51.1604758, lon: 3.1939077 }, + { type: "node", id: 1817319303, lat: 51.1605421, lon: 3.194476 }, + { type: "node", id: 1817319304, lat: 51.1606037, lon: 3.1933895 }, + { type: "node", id: 1817320493, lat: 51.1604248, lon: 3.1927398 }, + { type: "node", id: 1817320494, lat: 51.1604851, lon: 3.192495 }, + { type: "node", id: 2451569392, lat: 51.1598651, lon: 3.1919974 }, + { type: "node", id: 2451574741, lat: 51.1594995, lon: 3.1924908 }, + { type: "node", id: 2451574742, lat: 51.1596217, lon: 3.1923259 }, + { type: "node", id: 2451574743, lat: 51.1597161, lon: 3.1922023 }, + { type: "node", id: 2451574744, lat: 51.1600391, lon: 3.1932123 }, + { type: "node", id: 2451574745, lat: 51.1601904, lon: 3.192947 }, + { type: "node", id: 2451574746, lat: 51.1603862, lon: 3.1925461 }, + { type: "node", id: 2451574747, lat: 51.1604272, lon: 3.19257 }, + { type: "node", id: 2451574748, lat: 51.1604837, lon: 3.1921535 }, + { type: "node", id: 2451574749, lat: 51.1605266, lon: 3.1921769 }, + { type: "node", id: 2451578121, lat: 51.159758, lon: 3.1921448 }, + { type: "node", id: 5587844460, lat: 51.1562664, lon: 3.1919544 }, + { type: "node", id: 5587844461, lat: 51.15612, lon: 3.1920681 }, + { type: "node", id: 5587844462, lat: 51.1561808, lon: 3.1922614 }, + { type: "node", id: 5599314384, lat: 51.1563882, lon: 3.1923707 }, + { type: "node", id: 5599314385, lat: 51.1563587, lon: 3.1924465 }, + { type: "node", id: 6754312541, lat: 51.154716, lon: 3.1952084 }, + { type: "node", id: 6754312542, lat: 51.1547723, lon: 3.1953372 }, + { type: "node", id: 6754312543, lat: 51.1548078, lon: 3.1952983 }, + { type: "node", id: 6754312544, lat: 51.1547519, lon: 3.1951702 }, + { type: "node", id: 6754312550, lat: 51.1555879, lon: 3.1950341 }, + { type: "node", id: 6754312551, lat: 51.1556025, lon: 3.194956 }, + { type: "node", id: 6754312552, lat: 51.1555664, lon: 3.1952408 }, + { type: "node", id: 6754312553, lat: 51.1556188, lon: 3.1951751 }, + { type: "node", id: 6754312554, lat: 51.1554565, lon: 3.195427 }, + { type: "node", id: 6754312555, lat: 51.1552894, lon: 3.1950649 }, + { type: "node", id: 6754312556, lat: 51.1553277, lon: 3.1951991 }, + { type: "node", id: 6754312557, lat: 51.1552774, lon: 3.1947027 }, + { type: "node", id: 6754312558, lat: 51.1553131, lon: 3.1943816 }, + { type: "node", id: 6754312559, lat: 51.1553397, lon: 3.1942577 }, + { type: "node", id: 6754312560, lat: 51.1553697, lon: 3.1942084 }, + { type: "node", id: 6754312562, lat: 51.1548064, lon: 3.1954209 }, + { type: "node", id: 6754312563, lat: 51.1548266, lon: 3.1954893 }, + { type: "node", id: 6754312564, lat: 51.1550417, lon: 3.1953175 }, + { type: "node", id: 6754312565, lat: 51.1550193, lon: 3.1952538 }, + { type: "node", id: 6870850178, lat: 51.1561935, lon: 3.1923019 }, + { type: "node", id: 6870863414, lat: 51.1562489, lon: 3.1919675 }, + { type: "node", id: 6949357907, lat: 51.1575239, lon: 3.1916859 }, + { type: "node", id: 6949357908, lat: 51.1575439, lon: 3.1916101 }, + { type: "node", id: 1448421081, lat: 51.167491, lon: 3.1713256 }, + { type: "node", id: 1448421091, lat: 51.1677042, lon: 3.1714548 }, + { type: "node", id: 1448421093, lat: 51.1677455, lon: 3.1702529 }, + { type: "node", id: 1448421099, lat: 51.1679647, lon: 3.1703977 }, + { type: "node", id: 6536026850, lat: 51.1711159, lon: 3.1669401 }, + { type: "node", id: 6536026851, lat: 51.1712692, lon: 3.167296 }, + { type: "node", id: 6536026852, lat: 51.1711296, lon: 3.1674712 }, + { type: "node", id: 6536026853, lat: 51.1709602, lon: 3.1671189 }, + { type: "node", id: 6536038234, lat: 51.1711913, lon: 3.165543 }, + { type: "node", id: 6536038235, lat: 51.1711301, lon: 3.1656263 }, + { type: "node", id: 6536038236, lat: 51.1712318, lon: 3.1658161 }, + { type: "node", id: 6536038237, lat: 51.171293, lon: 3.1657327 }, + { type: "node", id: 1038583451, lat: 51.1625071, lon: 3.191602 }, + { type: "node", id: 4044171936, lat: 51.1609177, lon: 3.1911926 }, + { type: "node", id: 4044171941, lat: 51.1609801, lon: 3.191229 }, + { type: "node", id: 4044171963, lat: 51.1611962, lon: 3.1913534 }, + { type: "node", id: 903903386, lat: 51.1618084, lon: 3.1932127 }, + { type: "node", id: 903903387, lat: 51.1623536, lon: 3.1936011 }, + { type: "node", id: 903903388, lat: 51.1624429, lon: 3.1931156 }, + { type: "node", id: 903903390, lat: 51.1618641, lon: 3.1930197 }, + { type: "node", id: 903904576, lat: 51.1618211, lon: 3.1931711 }, + { type: "node", id: 1038557012, lat: 51.16155, lon: 3.1931121 }, + { type: "node", id: 1038557014, lat: 51.1613774, lon: 3.1930711 }, + { type: "node", id: 1038557035, lat: 51.1615349, lon: 3.1931712 }, + { type: "node", id: 1038557072, lat: 51.1616138, lon: 3.1927483 }, + { type: "node", id: 1038557075, lat: 51.162286, lon: 3.1935989 }, + { type: "node", id: 1038557078, lat: 51.1616517, lon: 3.1928439 }, + { type: "node", id: 1038557083, lat: 51.1616055, lon: 3.1930249 }, + { type: "node", id: 1038557094, lat: 51.1618993, lon: 3.193299 }, + { type: "node", id: 1038557102, lat: 51.1613235, lon: 3.1927138 }, + { type: "node", id: 1038557104, lat: 51.1615987, lon: 3.1928077 }, + { type: "node", id: 1038557108, lat: 51.1615113, lon: 3.1926816 }, + { type: "node", id: 1038557137, lat: 51.1608873, lon: 3.1924416 }, + { type: "node", id: 1038557143, lat: 51.1622248, lon: 3.193662 }, + { type: "node", id: 1038557191, lat: 51.1608171, lon: 3.1926951 }, + { type: "node", id: 1038557195, lat: 51.162409, lon: 3.1933428 }, + { type: "node", id: 1038557227, lat: 51.1613767, lon: 3.1925113 }, + { type: "node", id: 1038557230, lat: 51.1615281, lon: 3.1926132 }, + { type: "node", id: 1038557233, lat: 51.1619765, lon: 3.1933723 }, + { type: "node", id: 1038575040, lat: 51.1608523, lon: 3.1925679 }, + { type: "node", id: 1038583375, lat: 51.1625113, lon: 3.1926776 }, + { type: "node", id: 1038583398, lat: 51.1624305, lon: 3.1925743 }, + { type: "node", id: 1038583404, lat: 51.1627055, lon: 3.191826 }, + { type: "node", id: 1038583425, lat: 51.1624272, lon: 3.1916637 }, + { type: "node", id: 1038583441, lat: 51.1621479, lon: 3.1921546 }, + { type: "node", id: 1038583446, lat: 51.1622018, lon: 3.1921814 }, + { type: "node", id: 1038583456, lat: 51.162174, lon: 3.1922806 }, + { type: "node", id: 1038583459, lat: 51.162259, lon: 3.1924885 }, + { type: "node", id: 1038583463, lat: 51.162661, lon: 3.1917442 }, + { type: "node", id: 1038583476, lat: 51.1626425, lon: 3.1918448 }, + { type: "node", id: 1038583479, lat: 51.1624196, lon: 3.1926561 }, + { type: "node", id: 1038583483, lat: 51.1625037, lon: 3.1927232 }, + { type: "node", id: 1038583491, lat: 51.1625701, lon: 3.1926387 }, + { type: "node", id: 1038583501, lat: 51.1624928, lon: 3.1917013 }, + { type: "node", id: 1811673418, lat: 51.1618484, lon: 3.1950253 }, + { type: "node", id: 1811673421, lat: 51.1619875, lon: 3.1951427 }, + { type: "node", id: 1811673423, lat: 51.162144, lon: 3.193835 }, + { type: "node", id: 1811673425, lat: 51.1622452, lon: 3.1939149 }, + { type: "node", id: 1811699778, lat: 51.1608187, lon: 3.1922973 }, + { type: "node", id: 1811699779, lat: 51.1608915, lon: 3.1920223 }, + { type: "node", id: 1817320495, lat: 51.1607269, lon: 3.192929 }, + { type: "node", id: 1817320496, lat: 51.1607872, lon: 3.1926841 }, + { type: "node", id: 1817324488, lat: 51.1615575, lon: 3.1921423 }, + { type: "node", id: 1817324489, lat: 51.1612682, lon: 3.1920196 }, + { type: "node", id: 1817324491, lat: 51.1617501, lon: 3.1918468 }, + { type: "node", id: 1817324505, lat: 51.1618272, lon: 3.1921231 }, + { type: "node", id: 1817324509, lat: 51.1618658, lon: 3.191793 }, + { type: "node", id: 1817324513, lat: 51.1619814, lon: 3.1920002 }, + { type: "node", id: 1817324515, lat: 51.161985, lon: 3.191793 }, + { type: "node", id: 3550860268, lat: 51.1617349, lon: 3.1921922 }, + { type: "node", id: 3550860269, lat: 51.1619403, lon: 3.1920686 }, + { type: "node", id: 4044171924, lat: 51.1608199, lon: 3.1916126 }, + { type: "node", id: 4044171954, lat: 51.1610853, lon: 3.191781 }, + { type: "node", id: 4044171957, lat: 51.1611433, lon: 3.1918701 }, + { type: "node", id: 4044171962, lat: 51.1611926, lon: 3.1916807 }, + { type: "node", id: 4044171976, lat: 51.1612826, lon: 3.1919582 }, + { type: "node", id: 4044171997, lat: 51.1613568, lon: 3.1917787 }, + { type: "node", id: 4044172003, lat: 51.1613248, lon: 3.1919027 }, + { type: "node", id: 4044172008, lat: 51.1614345, lon: 3.1919767 }, + { type: "node", id: 6397031888, lat: 51.1615029, lon: 3.1919851 }, + { type: "node", id: 6960473080, lat: 51.1614227, lon: 3.1931004 }, + { type: "node", id: 3335137692, lat: 51.1698302, lon: 3.1965654 }, + { type: "node", id: 3335137795, lat: 51.1698788, lon: 3.1970728 }, + { type: "node", id: 3335137802, lat: 51.1700082, lon: 3.1971734 }, + { type: "node", id: 3335137807, lat: 51.1701139, lon: 3.1965001 }, + { type: "node", id: 3335137812, lat: 51.1703247, lon: 3.197352 }, + { type: "node", id: 3335137823, lat: 51.1703133, lon: 3.1966232 }, + { type: "node", id: 6255587427, lat: 51.1694775, lon: 3.1980047 }, + { type: "node", id: 9163486855, lat: 51.1703854, lon: 3.1970901 }, + { type: "node", id: 9163486856, lat: 51.1702114, lon: 3.1969688 }, + { type: "node", id: 9163493632, lat: 51.1699473, lon: 3.197063 }, + { type: "node", id: 414025563, lat: 51.1768198, lon: 3.1839265 }, + { type: "node", id: 2325437840, lat: 51.1765747, lon: 3.1839745 }, + { type: "node", id: 2325437844, lat: 51.1768286, lon: 3.1825946 }, + { type: "node", id: 8191691970, lat: 51.1767611, lon: 3.1839766 }, + { type: "node", id: 8191691971, lat: 51.1767812, lon: 3.1826167 }, + { type: "node", id: 8191691972, lat: 51.1765804, lon: 3.1826583 }, + { type: "node", id: 8191691973, lat: 51.1766324, lon: 3.182616 }, + { type: "node", id: 5716136617, lat: 51.1773127, lon: 3.1969033 }, + { type: "node", id: 5716136618, lat: 51.1771859, lon: 3.1969078 }, + { type: "node", id: 5716136619, lat: 51.177203, lon: 3.1981369 }, + { type: "node", id: 5716136620, lat: 51.1773298, lon: 3.1981324 }, + { type: "node", id: 6004375826, lat: 51.1786839, lon: 3.1952389 }, + { type: "node", id: 6625037999, lat: 51.1788016, lon: 3.1950645 }, + { type: "node", id: 6625038000, lat: 51.1789916, lon: 3.1966631 }, + { type: "node", id: 6625038001, lat: 51.1789003, lon: 3.1966838 }, + { type: "node", id: 6625038002, lat: 51.1788554, lon: 3.1963547 }, + { type: "node", id: 6625038003, lat: 51.1788165, lon: 3.1963622 }, + { type: "node", id: 6625038005, lat: 51.1785124, lon: 3.1952362 }, + { type: "node", id: 6625038006, lat: 51.1784779, lon: 3.1950618 }, + { type: "node", id: 6925536539, lat: 51.1522475, lon: 3.1985416 }, + { type: "node", id: 6925536540, lat: 51.1522635, lon: 3.1986187 }, + { type: "node", id: 6925536541, lat: 51.1523922, lon: 3.1985517 }, + { type: "node", id: 6925536542, lat: 51.1523766, lon: 3.1984746 }, + { type: "node", id: 1637742821, lat: 51.158524, lon: 3.199021 }, + { type: "node", id: 5586765933, lat: 51.1566667, lon: 3.2008881 }, + { type: "node", id: 5586765934, lat: 51.1564286, lon: 3.2012575 }, + { type: "node", id: 5586765935, lat: 51.1562496, lon: 3.2008579 }, + { type: "node", id: 5586765936, lat: 51.1564982, lon: 3.200522 }, + { type: "node", id: 6943148197, lat: 51.1581916, lon: 3.1989071 }, + { type: "node", id: 6943148198, lat: 51.1582377, lon: 3.1989899 }, + { type: "node", id: 6943148199, lat: 51.1581859, lon: 3.1990728 }, + { type: "node", id: 6943148200, lat: 51.1583257, lon: 3.199295 }, + { type: "node", id: 6943148201, lat: 51.1583563, lon: 3.199246 }, + { type: "node", id: 6943148202, lat: 51.1583988, lon: 3.1993135 }, + { type: "node", id: 6943148203, lat: 51.1585529, lon: 3.1990669 }, + { type: "node", id: 6943148204, lat: 51.1586554, lon: 3.1988109 }, + { type: "node", id: 6943148205, lat: 51.1585617, lon: 3.1986619 }, + { type: "node", id: 6943148206, lat: 51.1586782, lon: 3.1984755 }, + { type: "node", id: 6943148207, lat: 51.1585692, lon: 3.1982996 }, + { type: "node", id: 8065883225, lat: 51.1366029, lon: 3.233506 }, + { type: "node", id: 8065883226, lat: 51.1364627, lon: 3.2335338 }, + { type: "node", id: 8065883227, lat: 51.1363922, lon: 3.2326314 }, + { type: "node", id: 8065883228, lat: 51.1365324, lon: 3.2326036 }, + { type: "node", id: 8065883229, lat: 51.1374344, lon: 3.2333338 }, + { type: "node", id: 8065883230, lat: 51.1373632, lon: 3.2324534 }, + { type: "node", id: 8065883232, lat: 51.1375819, lon: 3.2333035 }, + { type: "node", id: 8065883233, lat: 51.1375107, lon: 3.232423 }, + { type: "node", id: 1795793393, lat: 51.1475704, lon: 3.233832 }, + { type: "node", id: 3346575840, lat: 51.146485, lon: 3.2349284 }, + { type: "node", id: 3346575846, lat: 51.1465127, lon: 3.2355341 }, + { type: "node", id: 3346575853, lat: 51.1466617, lon: 3.2349708 }, + { type: "node", id: 3346575855, lat: 51.1466787, lon: 3.2355171 }, + { type: "node", id: 3346575858, lat: 51.1466968, lon: 3.2342532 }, + { type: "node", id: 3346575865, lat: 51.1468756, lon: 3.2344143 }, + { type: "node", id: 3346575873, lat: 51.1469706, lon: 3.2366779 }, + { type: "node", id: 3346575929, lat: 51.1474307, lon: 3.2366434 }, + { type: "node", id: 1523513488, lat: 51.1398248, lon: 3.2392608 }, + { type: "node", id: 1744641292, lat: 51.1395814, lon: 3.2390365 }, + { type: "node", id: 1744641293, lat: 51.1397822, lon: 3.2393867 }, + { type: "node", id: 1920143232, lat: 51.1394724, lon: 3.2390121 }, + { type: "node", id: 7393009684, lat: 51.1394307, lon: 3.2390001 }, + { type: "node", id: 7393048385, lat: 51.1394135, lon: 3.2390524 }, + { type: "node", id: 3346575843, lat: 51.1465041, lon: 3.2376453 }, + { type: "node", id: 3346575845, lat: 51.146508, lon: 3.2378517 }, + { type: "node", id: 3346575876, lat: 51.1470052, lon: 3.237604 }, + { type: "node", id: 3346575886, lat: 51.147098, lon: 3.2412975 }, + { type: "node", id: 3346575889, lat: 51.1471585, lon: 3.2428761 }, + { type: "node", id: 3346575891, lat: 51.1471715, lon: 3.2377865 }, + { type: "node", id: 3346575901, lat: 51.1472492, lon: 3.2398591 }, + { type: "node", id: 3346575903, lat: 51.1472654, lon: 3.242623 }, + { type: "node", id: 3346575908, lat: 51.1472751, lon: 3.2428571 }, + { type: "node", id: 3346575917, lat: 51.1473518, lon: 3.2426093 }, + { type: "node", id: 3346575923, lat: 51.1473906, lon: 3.2398205 }, + { type: "node", id: 3346575925, lat: 51.147408, lon: 3.2424474 }, + { type: "node", id: 3346575928, lat: 51.1474263, lon: 3.24062 }, + { type: "node", id: 3346575933, lat: 51.1474728, lon: 3.2421565 }, + { type: "node", id: 3346575943, lat: 51.1475354, lon: 3.2418174 }, + { type: "node", id: 3346575945, lat: 51.1475494, lon: 3.2412786 }, + { type: "node", id: 3346575946, lat: 51.1475667, lon: 3.2415454 }, + { type: "node", id: 6291339815, lat: 51.1434669, lon: 3.2496811 }, + { type: "node", id: 6291339816, lat: 51.1434103, lon: 3.2497287 }, + { type: "node", id: 6291339821, lat: 51.1435685, lon: 3.2496576 }, + { type: "node", id: 6291339822, lat: 51.1434825, lon: 3.2497283 }, + { type: "node", id: 6291339827, lat: 51.1437401, lon: 3.2490327 }, + { type: "node", id: 6291339828, lat: 51.1433037, lon: 3.2494123 }, + { type: "node", id: 6291339829, lat: 51.1436099, lon: 3.2497855 }, + { type: "node", id: 6291339830, lat: 51.1439041, lon: 3.249535 }, + { type: "node", id: 170464837, lat: 51.1533286, lon: 3.236239 }, + { type: "node", id: 170464839, lat: 51.1532379, lon: 3.2364578 }, + { type: "node", id: 1710262731, lat: 51.1513274, lon: 3.2373199 }, + { type: "node", id: 1710262732, lat: 51.1508759, lon: 3.2370371 }, + { type: "node", id: 1710262733, lat: 51.1509316, lon: 3.2370599 }, + { type: "node", id: 1710262735, lat: 51.1510166, lon: 3.2370123 }, + { type: "node", id: 1710262738, lat: 51.1512751, lon: 3.2372984 }, + { type: "node", id: 1710262742, lat: 51.1513127, lon: 3.237065 }, + { type: "node", id: 1710262743, lat: 51.1508201, lon: 3.2373832 }, + { type: "node", id: 1710262745, lat: 51.151027, lon: 3.2369479 }, + { type: "node", id: 1795793395, lat: 51.1476158, lon: 3.2338163 }, + { type: "node", id: 1795793397, lat: 51.1475842, lon: 3.2344427 }, + { type: "node", id: 1795793399, lat: 51.1477641, lon: 3.233033 }, + { type: "node", id: 1795793405, lat: 51.1477717, lon: 3.2338665 }, + { type: "node", id: 1795793406, lat: 51.1480775, lon: 3.2344787 }, + { type: "node", id: 1795793407, lat: 51.1478893, lon: 3.2344203 }, + { type: "node", id: 1795793408, lat: 51.1481719, lon: 3.2342485 }, + { type: "node", id: 1795793409, lat: 51.147607, lon: 3.2330256 }, + { type: "node", id: 4979389763, lat: 51.1476223, lon: 3.2330288 }, + { type: "node", id: 8179735224, lat: 51.156047, lon: 3.2252534 }, + { type: "node", id: 8179735264, lat: 51.1560464, lon: 3.2252785 }, + { type: "node", id: 8179735265, lat: 51.1558544, lon: 3.2252597 }, + { type: "node", id: 8179735266, lat: 51.1556789, lon: 3.2252522 }, + { type: "node", id: 8179735267, lat: 51.1555253, lon: 3.2252547 }, + { type: "node", id: 8179735268, lat: 51.1555747, lon: 3.2250024 }, + { type: "node", id: 8179735269, lat: 51.1560527, lon: 3.2250198 }, + { type: "node", id: 1525460846, lat: 51.1550489, lon: 3.2347709 }, + { type: "node", id: 1810326044, lat: 51.1547625, lon: 3.2355098 }, + { type: "node", id: 1810326087, lat: 51.1547809, lon: 3.2353708 }, + { type: "node", id: 4912203160, lat: 51.1554941, lon: 3.2364781 }, + { type: "node", id: 4912203161, lat: 51.1554192, lon: 3.2369649 }, + { type: "node", id: 4912203162, lat: 51.1554175, lon: 3.2371071 }, + { type: "node", id: 4912203163, lat: 51.1554301, lon: 3.2372063 }, + { type: "node", id: 4912203164, lat: 51.1553847, lon: 3.2372197 }, + { type: "node", id: 4912203165, lat: 51.1553763, lon: 3.2369502 }, + { type: "node", id: 4912203166, lat: 51.1554512, lon: 3.236462 }, + { type: "node", id: 4912203167, lat: 51.1555218, lon: 3.2366455 }, + { type: "node", id: 4912203168, lat: 51.1554742, lon: 3.2369592 }, + { type: "node", id: 4912203169, lat: 51.155516, lon: 3.2369752 }, + { type: "node", id: 4912203170, lat: 51.1555635, lon: 3.2366616 }, + { type: "node", id: 4912203171, lat: 51.1556125, lon: 3.2360775 }, + { type: "node", id: 4912203172, lat: 51.155547, lon: 3.2364923 }, + { type: "node", id: 4912203173, lat: 51.1555893, lon: 3.2365092 }, + { type: "node", id: 4912203174, lat: 51.1556547, lon: 3.2360945 }, + { type: "node", id: 4912203176, lat: 51.1554841, lon: 3.2362949 }, + { type: "node", id: 4912203177, lat: 51.1553752, lon: 3.2362591 }, + { type: "node", id: 4912203178, lat: 51.1553968, lon: 3.2360924 }, + { type: "node", id: 4912203179, lat: 51.1555056, lon: 3.2361281 }, + { type: "node", id: 4912214680, lat: 51.1553544, lon: 3.2348128 }, + { type: "node", id: 4912214681, lat: 51.1550186, lon: 3.2346814 }, + { type: "node", id: 4912214685, lat: 51.1554486, lon: 3.2338965 }, + { type: "node", id: 4912214686, lat: 51.1550098, lon: 3.2346405 }, + { type: "node", id: 4912214692, lat: 51.155386, lon: 3.2347722 }, + { type: "node", id: 4912214693, lat: 51.1555155, lon: 3.2338415 }, + { type: "node", id: 4912214694, lat: 51.1554587, lon: 3.2338214 }, + { type: "node", id: 4912214695, lat: 51.1553292, lon: 3.2347521 }, + { type: "node", id: 4912225050, lat: 51.1553136, lon: 3.234866 }, + { type: "node", id: 4912225051, lat: 51.1551036, lon: 3.2337751 }, + { type: "node", id: 4912225052, lat: 51.1549825, lon: 3.2346307 }, + { type: "node", id: 4912225053, lat: 51.1551894, lon: 3.2336789 }, + { type: "node", id: 4912225062, lat: 51.1551528, lon: 3.233747 }, + { type: "node", id: 4912225063, lat: 51.1551831, lon: 3.2337249 }, + { type: "node", id: 4912225064, lat: 51.1554653, lon: 3.2337755 }, + { type: "node", id: 4912225067, lat: 51.1551309, lon: 3.2337849 }, + { type: "node", id: 4912225068, lat: 51.1551727, lon: 3.2337999 }, + { type: "node", id: 4912225069, lat: 51.1553182, lon: 3.2348322 }, + { type: "node", id: 4912225070, lat: 51.1550516, lon: 3.2346556 }, + { type: "node", id: 5972179331, lat: 51.154488, lon: 3.2350802 }, + { type: "node", id: 5972179343, lat: 51.1545781, lon: 3.2351138 }, + { type: "node", id: 5972179344, lat: 51.1546444, lon: 3.2351409 }, + { type: "node", id: 5972179345, lat: 51.1546431, lon: 3.2351485 }, + { type: "node", id: 5972179346, lat: 51.1547126, lon: 3.2351739 }, + { type: "node", id: 5972179347, lat: 51.154714, lon: 3.2351659 }, + { type: "node", id: 5972179348, lat: 51.1547795, lon: 3.235188 }, + { type: "node", id: 5974489614, lat: 51.1546386, lon: 3.2355125 }, + { type: "node", id: 5974489615, lat: 51.1544914, lon: 3.2353516 }, + { type: "node", id: 5974489616, lat: 51.1544813, lon: 3.2351384 }, + { type: "node", id: 5974489617, lat: 51.1548024, lon: 3.2352003 }, + { type: "node", id: 5974489618, lat: 51.154732, lon: 3.2356091 }, + { type: "node", id: 7529417225, lat: 51.159095, lon: 3.2366844 }, + { type: "node", id: 7529417226, lat: 51.1589926, lon: 3.2372825 }, + { type: "node", id: 7529417227, lat: 51.1588687, lon: 3.2372286 }, + { type: "node", id: 7529417228, lat: 51.1589264, lon: 3.2368918 }, + { type: "node", id: 7529417229, lat: 51.1588879, lon: 3.236875 }, + { type: "node", id: 7529417230, lat: 51.1589326, lon: 3.2366138 }, + { type: "node", id: 7529417232, lat: 51.159019, lon: 3.2366513 }, + { type: "node", id: 170464840, lat: 51.1531619, lon: 3.2376814 }, + { type: "node", id: 170464841, lat: 51.1532306, lon: 3.2376888 }, + { type: "node", id: 1710245701, lat: 51.1528676, lon: 3.2390068 }, + { type: "node", id: 1710245703, lat: 51.1527703, lon: 3.239403 }, + { type: "node", id: 1710245705, lat: 51.1527888, lon: 3.2390966 }, + { type: "node", id: 1710245707, lat: 51.1526357, lon: 3.2390543 }, + { type: "node", id: 1710245709, lat: 51.1528763, lon: 3.2390382 }, + { type: "node", id: 1710245711, lat: 51.1528928, lon: 3.2390631 }, + { type: "node", id: 1710245713, lat: 51.1528729, lon: 3.2389738 }, + { type: "node", id: 1710245715, lat: 51.1528645, lon: 3.2394083 }, + { type: "node", id: 1710245718, lat: 51.1526407, lon: 3.2389524 }, + { type: "node", id: 1710262736, lat: 51.1512857, lon: 3.2375783 }, + { type: "node", id: 1710262737, lat: 51.151234, lon: 3.2375571 }, + { type: "node", id: 1710262739, lat: 51.151183, lon: 3.2375939 }, + { type: "node", id: 1710262741, lat: 51.1511924, lon: 3.2375358 }, + { type: "node", id: 1710262744, lat: 51.1512252, lon: 3.2376112 }, + { type: "node", id: 3346575950, lat: 51.1475926, lon: 3.2406028 }, + { type: "node", id: 1728421374, lat: 51.1554436, lon: 3.2438233 }, + { type: "node", id: 1728421375, lat: 51.15594, lon: 3.2438649 }, + { type: "node", id: 1728421377, lat: 51.15559, lon: 3.2439695 }, + { type: "node", id: 1728421379, lat: 51.1554335, lon: 3.243944 }, + { type: "node", id: 1770289505, lat: 51.1565437, lon: 3.2437924 }, + { type: "node", id: 4912197362, lat: 51.1565535, lon: 3.2438327 }, + { type: "node", id: 4912197363, lat: 51.1562818, lon: 3.2438374 }, + { type: "node", id: 4912197364, lat: 51.1565321, lon: 3.2435757 }, + { type: "node", id: 4912197365, lat: 51.1565279, lon: 3.2433181 }, + { type: "node", id: 4912197366, lat: 51.1562804, lon: 3.2435833 }, + { type: "node", id: 4912197367, lat: 51.1562798, lon: 3.2431702 }, + { type: "node", id: 4912197368, lat: 51.1562813, lon: 3.2437497 }, + { type: "node", id: 4912197369, lat: 51.1562802, lon: 3.243488 }, + { type: "node", id: 4912197370, lat: 51.1565257, lon: 3.2431702 }, + { type: "node", id: 4912197371, lat: 51.1565542, lon: 3.2439044 }, + { type: "node", id: 4912197372, lat: 51.1565308, lon: 3.2437482 }, + { type: "node", id: 4912197373, lat: 51.1565294, lon: 3.2434893 }, + { type: "node", id: 4912197374, lat: 51.1562835, lon: 3.2439005 }, + { type: "node", id: 1710276232, lat: 51.1572435, lon: 3.2451269 }, + { type: "node", id: 1710276240, lat: 51.156984, lon: 3.2453481 }, + { type: "node", id: 1710276242, lat: 51.1567167, lon: 3.2452913 }, + { type: "node", id: 1710276243, lat: 51.1570484, lon: 3.2451 }, + { type: "node", id: 1710276251, lat: 51.1562241, lon: 3.2457572 }, + { type: "node", id: 1710276253, lat: 51.156868, lon: 3.2451689 }, + { type: "node", id: 1710276255, lat: 51.1563873, lon: 3.2456553 }, + { type: "node", id: 1710276257, lat: 51.1572402, lon: 3.2450303 }, + { type: "node", id: 1710276259, lat: 51.1561703, lon: 3.2454299 }, + { type: "node", id: 1710276261, lat: 51.1564411, lon: 3.2457304 }, + { type: "node", id: 1728421376, lat: 51.1555858, lon: 3.2441572 }, + { type: "node", id: 1728421378, lat: 51.1559348, lon: 3.2441264 }, + { type: "node", id: 1810330766, lat: 51.1562599, lon: 3.2457349 }, + { type: "node", id: 1810345944, lat: 51.1572859, lon: 3.2458614 }, + { type: "node", id: 1810345947, lat: 51.1568366, lon: 3.2461909 }, + { type: "node", id: 1810345951, lat: 51.1572074, lon: 3.2455895 }, + { type: "node", id: 1810345955, lat: 51.1567582, lon: 3.245919 }, + { type: "node", id: 1810347217, lat: 51.1568783, lon: 3.2452387 }, + { type: "node", id: 6255587424, lat: 51.1699788, lon: 3.1988617 }, + { type: "node", id: 6255587425, lat: 51.1695322, lon: 3.1995447 }, + { type: "node", id: 6255587426, lat: 51.1690026, lon: 3.1986489 }, + { type: "node", id: 5173881316, lat: 51.1728811, lon: 3.210692 }, + { type: "node", id: 5173881317, lat: 51.1728829, lon: 3.21077 }, + { type: "node", id: 5173881318, lat: 51.1726197, lon: 3.2107914 }, + { type: "node", id: 5173881320, lat: 51.1728794, lon: 3.2108575 }, + { type: "node", id: 5173881621, lat: 51.1728791, lon: 3.2109364 }, + { type: "node", id: 5173881622, lat: 51.1727133, lon: 3.2109488 }, + { type: "node", id: 5173881623, lat: 51.1727117, lon: 3.2108703 }, + { type: "node", id: 5173881624, lat: 51.1728613, lon: 3.211069 }, + { type: "node", id: 5173881625, lat: 51.1728601, lon: 3.2111406 }, + { type: "node", id: 5173881626, lat: 51.1727019, lon: 3.2111316 }, + { type: "node", id: 5173881627, lat: 51.1727041, lon: 3.2110597 }, + { type: "node", id: 6275462772, lat: 51.1733576, lon: 3.2110928 }, + { type: "node", id: 6275462773, lat: 51.1733528, lon: 3.2112295 }, + { type: "node", id: 6275462774, lat: 51.1735278, lon: 3.2112449 }, + { type: "node", id: 6275462775, lat: 51.1735325, lon: 3.2111082 }, + { type: "node", id: 6275462776, lat: 51.1736659, lon: 3.2112568 }, + { type: "node", id: 6275462777, lat: 51.1736113, lon: 3.2112529 }, + { type: "node", id: 6275462784, lat: 51.1735598, lon: 3.2109277 }, + { type: "node", id: 6275462985, lat: 51.1734626, lon: 3.2109229 }, + { type: "node", id: 6275462986, lat: 51.1734599, lon: 3.2110592 }, + { type: "node", id: 6275462987, lat: 51.1735572, lon: 3.211064 }, + { type: "node", id: 6275462988, lat: 51.1734613, lon: 3.2109904 }, + { type: "node", id: 6275462989, lat: 51.173357, lon: 3.2111476 }, + { type: "node", id: 7054196467, lat: 51.1726209, lon: 3.2107131 }, + { type: "node", id: 8042845810, lat: 51.1737696, lon: 3.206708 }, + { type: "node", id: 8042845811, lat: 51.1734466, lon: 3.2056148 }, + { type: "node", id: 5952389321, lat: 51.1668901, lon: 3.2166736 }, + { type: "node", id: 5952389322, lat: 51.1664592, lon: 3.2166337 }, + { type: "node", id: 5952389323, lat: 51.1659941, lon: 3.2166018 }, + { type: "node", id: 5172938444, lat: 51.1667283, lon: 3.2192609 }, + { type: "node", id: 5536609426, lat: 51.1664884, lon: 3.2184803 }, + { type: "node", id: 5536620510, lat: 51.1668363, lon: 3.2197448 }, + { type: "node", id: 5536620511, lat: 51.1671911, lon: 3.2210959 }, + { type: "node", id: 5952389320, lat: 51.1665059, lon: 3.2183884 }, + { type: "node", id: 5536620506, lat: 51.1675299, lon: 3.2170283 }, + { type: "node", id: 5536620507, lat: 51.1672585, lon: 3.2168071 }, + { type: "node", id: 6275462778, lat: 51.1736042, lon: 3.2115044 }, + { type: "node", id: 6275462779, lat: 51.1736588, lon: 3.2115083 }, + { type: "node", id: 6275462780, lat: 51.1735687, lon: 3.2112964 }, + { type: "node", id: 6275462781, lat: 51.1735211, lon: 3.2112937 }, + { type: "node", id: 6275462782, lat: 51.1735156, lon: 3.2115423 }, + { type: "node", id: 6275462783, lat: 51.1735632, lon: 3.211545 }, + { type: "node", id: 5536620505, lat: 51.1683944, lon: 3.2180288 }, + { type: "node", id: 5536620512, lat: 51.1673641, lon: 3.2217199 }, + { type: "node", id: 5536620513, lat: 51.1675007, lon: 3.2221772 }, + { type: "node", id: 5536620514, lat: 51.1679104, lon: 3.2236675 }, + { type: "node", id: 5536620516, lat: 51.1679955, lon: 3.224005 }, + { type: "node", id: 5536620824, lat: 51.1700823, lon: 3.2236258 }, + { type: "node", id: 5536620826, lat: 51.171324, lon: 3.2230929 }, + { type: "node", id: 5536620827, lat: 51.1717731, lon: 3.2213846 }, + { type: "node", id: 5536620828, lat: 51.170726, lon: 3.2202369 }, + { type: "node", id: 5536620829, lat: 51.1706206, lon: 3.2201178 }, + { type: "node", id: 5536620830, lat: 51.170049, lon: 3.2215441 }, + { type: "node", id: 5536620831, lat: 51.1699442, lon: 3.2215294 }, + { type: "node", id: 5536620832, lat: 51.1683757, lon: 3.2194636 }, + { type: "node", id: 6067483781, lat: 51.1675243, lon: 3.222207 }, + { type: "node", id: 6067483782, lat: 51.1713888, lon: 3.2231705 }, + { type: "node", id: 7794736251, lat: 51.1688401, lon: 3.2184325 }, + { type: "node", id: 1069177852, lat: 51.1789546, lon: 3.2021791 }, + { type: "node", id: 1069177853, lat: 51.1801636, lon: 3.2031369 }, + { type: "node", id: 1069177873, lat: 51.1791663, lon: 3.2034541 }, + { type: "node", id: 1069177915, lat: 51.1799187, lon: 3.2029579 }, + { type: "node", id: 1069177919, lat: 51.1786053, lon: 3.2030903 }, + { type: "node", id: 1069177920, lat: 51.1790417, lon: 3.2033998 }, + { type: "node", id: 1069177925, lat: 51.1788415, lon: 3.2032442 }, + { type: "node", id: 1069177933, lat: 51.1798479, lon: 3.2029364 }, + { type: "node", id: 1069177967, lat: 51.178467, lon: 3.2025563 }, + { type: "node", id: 1069177976, lat: 51.1791427, lon: 3.2026954 }, + { type: "node", id: 1069177984, lat: 51.1783718, lon: 3.2028231 }, + { type: "node", id: 1069178021, lat: 51.1793534, lon: 3.2033094 }, + { type: "node", id: 1069178133, lat: 51.1784113, lon: 3.2028156 }, + { type: "node", id: 6853179202, lat: 51.1792037, lon: 3.2026994 }, + { type: "node", id: 6853179203, lat: 51.1790601, lon: 3.2026755 }, + { type: "node", id: 6853179204, lat: 51.1791861, lon: 3.2027108 }, + { type: "node", id: 6853179205, lat: 51.1791741, lon: 3.2027122 }, + { type: "node", id: 6853179206, lat: 51.1791634, lon: 3.2027102 }, + { type: "node", id: 6853179207, lat: 51.1791526, lon: 3.2027048 }, + { type: "node", id: 6853179208, lat: 51.1790128, lon: 3.202488 }, + { type: "node", id: 6853179209, lat: 51.1790434, lon: 3.2026225 }, + { type: "node", id: 6853179210, lat: 51.1789811, lon: 3.2023837 }, + { type: "node", id: 6853179211, lat: 51.1789744, lon: 3.2022415 }, + { type: "node", id: 6853179212, lat: 51.1789438, lon: 3.2022643 }, + { type: "node", id: 6853179213, lat: 51.1784216, lon: 3.2028567 }, + { type: "node", id: 6853179214, lat: 51.1784157, lon: 3.2028375 }, + { type: "node", id: 6853179215, lat: 51.1784506, lon: 3.2029294 }, + { type: "node", id: 6853179216, lat: 51.1783884, lon: 3.2026704 }, + { type: "node", id: 6853179217, lat: 51.178422, lon: 3.2026034 }, + { type: "node", id: 6853179218, lat: 51.1784117, lon: 3.2026185 }, + { type: "node", id: 6853179219, lat: 51.1784021, lon: 3.2026354 }, + { type: "node", id: 6853179220, lat: 51.1783957, lon: 3.2026505 }, + { type: "node", id: 6853179221, lat: 51.178443, lon: 3.2025785 }, + { type: "node", id: 6853179222, lat: 51.1784546, lon: 3.202567 }, + { type: "node", id: 6853179223, lat: 51.1784321, lon: 3.2025906 }, + { type: "node", id: 6853179224, lat: 51.1783719, lon: 3.2027464 }, + { type: "node", id: 6853179225, lat: 51.1783749, lon: 3.2027238 }, + { type: "node", id: 6853179226, lat: 51.1783827, lon: 3.2026894 }, + { type: "node", id: 6853179227, lat: 51.1783784, lon: 3.2027066 }, + { type: "node", id: 6853179228, lat: 51.1783697, lon: 3.2027864 }, + { type: "node", id: 6853179229, lat: 51.1783704, lon: 3.2027651 }, + { type: "node", id: 6853179230, lat: 51.1783703, lon: 3.2028048 }, + { type: "node", id: 6853179234, lat: 51.1798837, lon: 3.2029379 }, + { type: "node", id: 6853179235, lat: 51.1798988, lon: 3.2029445 }, + { type: "node", id: 6853179236, lat: 51.1798661, lon: 3.2029354 }, + { type: "node", id: 6853179237, lat: 51.1798345, lon: 3.2029407 }, + { type: "node", id: 6853179238, lat: 51.1798207, lon: 3.2029479 }, + { type: "node", id: 6853179239, lat: 51.1792634, lon: 3.2033377 }, + { type: "node", id: 6853179240, lat: 51.1793019, lon: 3.2033359 }, + { type: "node", id: 6853179241, lat: 51.1792828, lon: 3.20334 }, + { type: "node", id: 6853179242, lat: 51.1792732, lon: 3.2033393 }, + { type: "node", id: 6853179243, lat: 51.1792921, lon: 3.2033388 }, + { type: "node", id: 6853179244, lat: 51.1793279, lon: 3.2033257 }, + { type: "node", id: 6853179245, lat: 51.1793153, lon: 3.2033313 }, + { type: "node", id: 6853179246, lat: 51.1793413, lon: 3.2033182 }, + { type: "node", id: 6853179247, lat: 51.1792384, lon: 3.2033981 }, + { type: "node", id: 6853179248, lat: 51.1792572, lon: 3.2033605 }, + { type: "node", id: 6853179249, lat: 51.1792512, lon: 3.2033774 }, + { type: "node", id: 6853179250, lat: 51.1792456, lon: 3.2033888 }, + { type: "node", id: 6853179256, lat: 51.1790996, lon: 3.2034514 }, + { type: "node", id: 6853179257, lat: 51.1790731, lon: 3.2034317 }, + { type: "node", id: 6853179258, lat: 51.1790581, lon: 3.2034168 }, + { type: "node", id: 6853179259, lat: 51.1790862, lon: 3.2034422 }, + { type: "node", id: 6853179260, lat: 51.179133, lon: 3.2034648 }, + { type: "node", id: 6853179261, lat: 51.1791191, lon: 3.203461 }, + { type: "node", id: 6853179262, lat: 51.1791546, lon: 3.2034608 }, + { type: "node", id: 6853179263, lat: 51.1791443, lon: 3.2034639 }, + { type: "node", id: 6853179264, lat: 51.1789437, lon: 3.2033089 }, + { type: "node", id: 6853179267, lat: 51.1785146, lon: 3.2030128 }, + { type: "node", id: 6853179268, lat: 51.1785517, lon: 3.2030489 }, + { type: "node", id: 6853179269, lat: 51.1785863, lon: 3.2030773 }, + { type: "node", id: 6853179327, lat: 51.1789936, lon: 3.2024248 }, + { type: "node", id: 7252820961, lat: 51.175521, lon: 3.2045972 }, + { type: "node", id: 7252863798, lat: 51.1754304, lon: 3.2044959 }, + { type: "node", id: 8042845806, lat: 51.1753353, lon: 3.2041851 }, + { type: "node", id: 8042845807, lat: 51.175363, lon: 3.2043314 }, + { type: "node", id: 8042845812, lat: 51.1752711, lon: 3.2040127 }, + { type: "node", id: 4036885076, lat: 51.1740632, lon: 3.2050437 }, + { type: "node", id: 4036899624, lat: 51.1767493, lon: 3.2082945 }, + { type: "node", id: 5607796819, lat: 51.1782483, lon: 3.2091883 }, + { type: "node", id: 5607796820, lat: 51.1785128, lon: 3.2086016 }, + { type: "node", id: 5607798721, lat: 51.1786474, lon: 3.2090453 }, + { type: "node", id: 5607798722, lat: 51.1782874, lon: 3.2093115 }, + { type: "node", id: 5607798723, lat: 51.178141, lon: 3.2088491 }, + { type: "node", id: 5607798725, lat: 51.1785713, lon: 3.2087945 }, + { type: "node", id: 5728443539, lat: 51.1753294, lon: 3.2097039 }, + { type: "node", id: 5728443540, lat: 51.1752216, lon: 3.2089278 }, + { type: "node", id: 6275462768, lat: 51.174424, lon: 3.2105467 }, + { type: "node", id: 6275462769, lat: 51.1743524, lon: 3.2105548 }, + { type: "node", id: 6275462770, lat: 51.1743644, lon: 3.2108257 }, + { type: "node", id: 6275462771, lat: 51.1744361, lon: 3.2108176 }, + { type: "node", id: 7252820962, lat: 51.1756015, lon: 3.204854 }, + { type: "node", id: 7252820963, lat: 51.1755802, lon: 3.204928 }, + { type: "node", id: 7252820964, lat: 51.1755132, lon: 3.2049422 }, + { type: "node", id: 7252820965, lat: 51.1754719, lon: 3.2050156 }, + { type: "node", id: 7252820966, lat: 51.1754575, lon: 3.2051212 }, + { type: "node", id: 7252820967, lat: 51.1755143, lon: 3.2052892 }, + { type: "node", id: 7252820968, lat: 51.1755533, lon: 3.2055086 }, + { type: "node", id: 7252820969, lat: 51.1755563, lon: 3.2060065 }, + { type: "node", id: 7252820970, lat: 51.175491, lon: 3.2064409 }, + { type: "node", id: 7252820971, lat: 51.1753674, lon: 3.2068348 }, + { type: "node", id: 7252820972, lat: 51.1751944, lon: 3.2070531 }, + { type: "node", id: 7252820973, lat: 51.1751195, lon: 3.2071478 }, + { type: "node", id: 7252820974, lat: 51.1750834, lon: 3.2072467 }, + { type: "node", id: 7252820975, lat: 51.1750963, lon: 3.2073579 }, + { type: "node", id: 7252820976, lat: 51.1751376, lon: 3.2074032 }, + { type: "node", id: 7252820977, lat: 51.175215, lon: 3.2073826 }, + { type: "node", id: 7252820978, lat: 51.1752848, lon: 3.2073785 }, + { type: "node", id: 7252820979, lat: 51.1754252, lon: 3.2073858 }, + { type: "node", id: 7252820980, lat: 51.1754615, lon: 3.2074926 }, + { type: "node", id: 7252820981, lat: 51.1754259, lon: 3.20756 }, + { type: "node", id: 7252820982, lat: 51.17537, lon: 3.2076668 }, + { type: "node", id: 7252820983, lat: 51.1753304, lon: 3.2078901 }, + { type: "node", id: 7252820984, lat: 51.1753152, lon: 3.2079319 }, + { type: "node", id: 7252874885, lat: 51.1754423, lon: 3.2080951 }, + { type: "node", id: 7252874886, lat: 51.1754991, lon: 3.2083134 }, + { type: "node", id: 7252874887, lat: 51.1755307, lon: 3.2084864 }, + { type: "node", id: 7252874888, lat: 51.1755729, lon: 3.2087064 }, + { type: "node", id: 7252874889, lat: 51.1753248, lon: 3.2088635 }, + { type: "node", id: 7252874890, lat: 51.1752645, lon: 3.2092365 }, + { type: "node", id: 7252874891, lat: 51.1747746, lon: 3.2093558 }, + { type: "node", id: 8042845789, lat: 51.1748587, lon: 3.209526 }, + { type: "node", id: 8042845790, lat: 51.1749489, lon: 3.2096774 }, + { type: "node", id: 8042845791, lat: 51.1750595, lon: 3.2097458 }, + { type: "node", id: 8042845792, lat: 51.1753557, lon: 3.2077924 }, + { type: "node", id: 8042845793, lat: 51.1754621, lon: 3.2074425 }, + { type: "node", id: 8042845794, lat: 51.1754531, lon: 3.2074092 }, + { type: "node", id: 8042845795, lat: 51.1754729, lon: 3.2051839 }, + { type: "node", id: 8042845796, lat: 51.1754907, lon: 3.2052089 }, + { type: "node", id: 8042845797, lat: 51.1755084, lon: 3.2052355 }, + { type: "node", id: 8042845798, lat: 51.1755235, lon: 3.2053482 }, + { type: "node", id: 8042845799, lat: 51.1755387, lon: 3.2053805 }, + { type: "node", id: 8042845800, lat: 51.1755584, lon: 3.2057251 }, + { type: "node", id: 8042845801, lat: 51.1755536, lon: 3.205762 }, + { type: "node", id: 8042845802, lat: 51.1755492, lon: 3.2061312 }, + { type: "node", id: 8042845803, lat: 51.1755305, lon: 3.2062755 }, + { type: "node", id: 8042845804, lat: 51.1754335, lon: 3.2066603 }, + { type: "node", id: 8042845805, lat: 51.1755929, lon: 3.2047843 }, + { type: "node", id: 8042845808, lat: 51.1746278, lon: 3.2090183 }, + { type: "node", id: 8042845809, lat: 51.1740796, lon: 3.2076268 }, + { type: "node", id: 8042845844, lat: 51.1768218, lon: 3.20861 }, + { type: "node", id: 8042845845, lat: 51.1767935, lon: 3.2085031 }, + { type: "node", id: 8042845846, lat: 51.1769413, lon: 3.2089936 }, + { type: "node", id: 8042845847, lat: 51.1757541, lon: 3.2096988 }, + { type: "node", id: 8042845848, lat: 51.1757421, lon: 3.2096812 }, + { type: "node", id: 8042845849, lat: 51.1757312, lon: 3.2096924 }, + { type: "node", id: 8042845850, lat: 51.1757202, lon: 3.2096478 }, + { type: "node", id: 8042845851, lat: 51.1756902, lon: 3.2096207 }, + { type: "node", id: 8042845852, lat: 51.1756712, lon: 3.2096143 }, + { type: "node", id: 8042845853, lat: 51.1756602, lon: 3.2095745 }, + { type: "node", id: 8042845854, lat: 51.1756552, lon: 3.2095537 }, + { type: "node", id: 8042845855, lat: 51.1756657, lon: 3.2095174 }, + { type: "node", id: 8042845856, lat: 51.175658, lon: 3.20908 }, + { type: "node", id: 8042845857, lat: 51.1756525, lon: 3.2093366 }, + { type: "node", id: 8042845858, lat: 51.1756466, lon: 3.2088282 }, + { type: "node", id: 8042845859, lat: 51.1756582, lon: 3.2089151 }, + { type: "node", id: 8042845860, lat: 51.1765521, lon: 3.20839 }, + { type: "node", id: 1069177845, lat: 51.1809357, lon: 3.2035366 }, + { type: "node", id: 1069177849, lat: 51.1803975, lon: 3.2017749 }, + { type: "node", id: 1069178166, lat: 51.1804195, lon: 3.2033098 }, + { type: "node", id: 1519342742, lat: 51.1805239, lon: 3.2032684 }, + { type: "node", id: 1519342743, lat: 51.18064, lon: 3.2036951 }, + { type: "node", id: 1759437085, lat: 51.1806986, lon: 3.2036647 }, + { type: "node", id: 6852012577, lat: 51.1804541, lon: 3.2017867 }, + { type: "node", id: 6852012578, lat: 51.1804124, lon: 3.2018177 }, + { type: "node", id: 6852012579, lat: 51.1804106, lon: 3.2018165 }, + { type: "node", id: 6852012580, lat: 51.1804143, lon: 3.2018177 }, + { type: "node", id: 6852012581, lat: 51.1808363, lon: 3.2030295 }, + { type: "node", id: 6852012582, lat: 51.1807955, lon: 3.2030595 }, + { type: "node", id: 6852012583, lat: 51.180798, lon: 3.2030712 }, + { type: "node", id: 1519476620, lat: 51.1786696, lon: 3.2199463 }, + { type: "node", id: 1519476635, lat: 51.179306, lon: 3.2193119 }, + { type: "node", id: 1519476698, lat: 51.1795485, lon: 3.2192221 }, + { type: "node", id: 1519476744, lat: 51.1791125, lon: 3.2194529 }, + { type: "node", id: 1519476746, lat: 51.178483, lon: 3.2203218 }, + { type: "node", id: 1519476797, lat: 51.1788731, lon: 3.2196593 }, + { type: "node", id: 3780611492, lat: 51.1761568, lon: 3.2238485 }, + { type: "node", id: 3780611493, lat: 51.1762213, lon: 3.223901 }, + { type: "node", id: 3780611494, lat: 51.1762626, lon: 3.2237172 }, + { type: "node", id: 3780611495, lat: 51.1763208, lon: 3.2237628 }, + { type: "node", id: 3780611496, lat: 51.1763248, lon: 3.2236414 }, + { type: "node", id: 3780611497, lat: 51.1763881, lon: 3.2236926 }, + { type: "node", id: 3780611498, lat: 51.1764876, lon: 3.2235544 }, + { type: "node", id: 3780611499, lat: 51.1766551, lon: 3.2232337 }, + { type: "node", id: 3780611500, lat: 51.176687, lon: 3.2231945 }, + { type: "node", id: 3780611501, lat: 51.1767105, lon: 3.2232776 }, + { type: "node", id: 3780611502, lat: 51.176751, lon: 3.2232465 }, + { type: "node", id: 3780611503, lat: 51.1767812, lon: 3.2230729 }, + { type: "node", id: 3780611504, lat: 51.1768505, lon: 3.2231083 }, + { type: "node", id: 6533893620, lat: 51.178521, lon: 3.2203687 }, + { type: "node", id: 6533893621, lat: 51.1786845, lon: 3.220025 }, + { type: "node", id: 6533893622, lat: 51.1789011, lon: 3.2197183 }, + { type: "node", id: 6533893624, lat: 51.1791343, lon: 3.2195235 }, + { type: "node", id: 6533893625, lat: 51.1793269, lon: 3.2193854 }, + { type: "node", id: 6533893626, lat: 51.1795596, lon: 3.219299 }, + { type: "node", id: 5536620518, lat: 51.1683264, lon: 3.224863 }, + { type: "node", id: 5536620519, lat: 51.1684352, lon: 3.2251117 }, + { type: "node", id: 5536620520, lat: 51.1685675, lon: 3.2254022 }, + { type: "node", id: 5536620821, lat: 51.1687379, lon: 3.2258223 }, + { type: "node", id: 5536620822, lat: 51.1693682, lon: 3.2250177 }, + { type: "node", id: 5536620823, lat: 51.1693734, lon: 3.225049 }, + { type: "node", id: 5536620825, lat: 51.1707605, lon: 3.2244639 }, + { type: "node", id: 5536620837, lat: 51.1697793, lon: 3.2260181 }, + { type: "node", id: 5536620838, lat: 51.1699712, lon: 3.2262338 }, + { type: "node", id: 5536620839, lat: 51.1701247, lon: 3.2263242 }, + { type: "node", id: 5536620840, lat: 51.1704719, lon: 3.2266478 }, + { type: "node", id: 5536620841, lat: 51.1701028, lon: 3.2281081 }, + { type: "node", id: 5536620842, lat: 51.1698158, lon: 3.2276446 }, + { type: "node", id: 5536620843, lat: 51.1696441, lon: 3.2273837 }, + { type: "node", id: 5536620844, lat: 51.1695154, lon: 3.2272009 }, + { type: "node", id: 5536620845, lat: 51.169536, lon: 3.2271664 }, + { type: "node", id: 5536620846, lat: 51.1694515, lon: 3.2270181 }, + { type: "node", id: 5635001306, lat: 51.1737078, lon: 3.2354437 }, + { type: "node", id: 5635001371, lat: 51.1722128, lon: 3.2340273 }, + { type: "node", id: 5635001372, lat: 51.1723921, lon: 3.2343394 }, + { type: "node", id: 5635001373, lat: 51.1724213, lon: 3.2342967 }, + { type: "node", id: 5635001374, lat: 51.1722421, lon: 3.2339846 }, + { type: "node", id: 5635001375, lat: 51.1728995, lon: 3.2339319 }, + { type: "node", id: 5635001376, lat: 51.1729253, lon: 3.2339922 }, + { type: "node", id: 5635001377, lat: 51.1723583, lon: 3.2340816 }, + { type: "node", id: 5635001378, lat: 51.1723268, lon: 3.2340173 }, + { type: "node", id: 5635001379, lat: 51.172885, lon: 3.2337993 }, + { type: "node", id: 5635001380, lat: 51.1728611, lon: 3.2338706 }, + { type: "node", id: 5635001381, lat: 51.1723325, lon: 3.2339419 }, + { type: "node", id: 5635001382, lat: 51.1723464, lon: 3.2338696 }, + { type: "node", id: 5882873334, lat: 51.1736186, lon: 3.2330966 }, + { type: "node", id: 5882873335, lat: 51.1735451, lon: 3.2327633 }, + { type: "node", id: 5882873336, lat: 51.1737001, lon: 3.2327438 }, + { type: "node", id: 5882873337, lat: 51.1736796, lon: 3.2318764 }, + { type: "node", id: 5882873338, lat: 51.1735265, lon: 3.2318782 }, + { type: "node", id: 6593340582, lat: 51.1727872, lon: 3.2328745 }, + { type: "node", id: 6593340583, lat: 51.1728013, lon: 3.2332051 }, + { type: "node", id: 6593340584, lat: 51.1736743, lon: 3.2331435 }, + { type: "node", id: 7767137235, lat: 51.1735198, lon: 3.2355568 }, + { type: "node", id: 7767137236, lat: 51.1735366, lon: 3.2355246 }, + { type: "node", id: 7767137237, lat: 51.1735198, lon: 3.2356399 }, + { type: "node", id: 5635001274, lat: 51.1751425, lon: 3.2346144 }, + { type: "node", id: 5635001275, lat: 51.1751696, lon: 3.2347601 }, + { type: "node", id: 5635001276, lat: 51.1750553, lon: 3.2348141 }, + { type: "node", id: 5635001277, lat: 51.1750282, lon: 3.2346684 }, + { type: "node", id: 5635001312, lat: 51.174002, lon: 3.2349367 }, + { type: "node", id: 5635001383, lat: 51.1740709, lon: 3.233056 }, + { type: "node", id: 5635001384, lat: 51.1740249, lon: 3.2330598 }, + { type: "node", id: 5635001385, lat: 51.1740265, lon: 3.2331313 }, + { type: "node", id: 5635001386, lat: 51.1740597, lon: 3.2327202 }, + { type: "node", id: 5635001414, lat: 51.174281, lon: 3.2336147 }, + { type: "node", id: 5635001415, lat: 51.174081, lon: 3.2338914 }, + { type: "node", id: 5635001416, lat: 51.1740489, lon: 3.2338323 }, + { type: "node", id: 5635001417, lat: 51.1742489, lon: 3.2335556 }, + { type: "node", id: 5761770202, lat: 51.1783111, lon: 3.2342484 }, + { type: "node", id: 5761770204, lat: 51.1782819, lon: 3.2339616 }, + { type: "node", id: 7767137234, lat: 51.1739713, lon: 3.2348766 }, + { type: "node", id: 9052878228, lat: 51.1781206, lon: 3.234323 }, + { type: "node", id: 9052878229, lat: 51.1781054, lon: 3.2339448 }, + { + type: "way", + id: 810604915, + nodes: [ + 1168727824, 9167054153, 9274761589, 9274761596, 7577430793, 1038638712, + 1038638723, 1038638661, 9199177059, 1038638721, 7554434436, 7578975035, + 7554434438, 7578865273, 7578975032, 7578975030, 7578975029, 1038638696, + 7578975009, 7578975008, 7578975007, 1038638743, 7578975002, 7578974988, + 7578974989, 7578974990, 7578974991, 7578974992, 7578865275, 7578865274, + 1038638753, 7578974995, 7578974996, 7578974997, 7578904489, 7578974999, + 7578975000, 7578975001, 7578974998, 3921878998, 1038638592, 929120698, + 1675648152, 7578865281, 7578865283, 7578975012, 7578975015, 7578975016, + 3922380061, 2732486274, 3922380083, 7578975019, 7578975018, 7578975021, + 7578960079, 3922375256, 7578975024, 3922380071, + ], + }, + { + type: "way", + id: 989393316, + nodes: [ + 3922380071, 3922380081, 3922380086, 3922380092, 3922380095, 9167054157, + 7578975026, 9274761593, 9274761592, 9274761591, 7578975027, 9167054156, + 9167054154, 7578975049, 7578975028, 1168727824, + ], + }, + { + type: "way", + id: 389026405, + nodes: [ + 3921879019, 7578975044, 3921879018, 7578975046, 7578975045, 7578975040, + 3921879004, 3921879011, 3921879019, + ], + }, + { + type: "way", + id: 810607458, + nodes: [ + 7578987409, 7578987410, 7578987411, 5745833241, 5745833240, 5745833239, + 7578987412, 7578987415, 7578987413, 7578987414, 7578987417, 7578987416, + 7578987418, 7578987419, 7578987409, + ], + }, + { + type: "way", + id: 777280458, + nodes: [ + 8042845812, 8042845806, 8042845807, 7252863798, 7252820961, 8042845805, + 7252820962, 7252820963, 7252820964, 7252820965, 7252820966, 8042845795, + 8042845796, 8042845797, 7252820967, 8042845798, 8042845799, 7252820968, + 8042845800, 8042845801, 7252820969, 8042845802, 8042845803, 7252820970, + 8042845804, 7252820971, 7252820972, 7252820973, 7252820974, 7252820975, + 7252820976, 7252820977, 7252820978, 7252820979, 8042845794, 8042845793, + 7252820980, 7252820981, 7252820982, 8042845792, 7252820983, 7252820984, + 7252874885, 7252874886, 7252874887, 7252874888, 7252874889, 5728443540, + 7252874890, 5728443539, 8042845791, 8042845790, 8042845789, 7252874891, + 8042845808, 8042845809, 8042845810, 8042845811, 4036885076, 8042845812, + ], + }, + { + type: "way", + id: 577572397, + nodes: [ + 5536620518, 5536620519, 5536620520, 5536620821, 5536620822, 5536620823, + 5536620824, 5536620825, 5536620826, 6067483782, 5536620827, 5536620828, + 5536620829, 5536620830, 5536620831, 5536620832, 7794736251, 5536620505, + 5536620506, 5536620507, 5952389321, 5952389322, 5952389323, 5536609426, + 5952389320, 5172938444, 5536620510, 5536620511, 5536620512, 5536620513, + 6067483781, 5536620514, 5536620516, 5536620518, + ], + }, + { + type: "way", + id: 863373849, + nodes: [ + 4036899624, 8042845845, 8042845844, 8042845846, 8042845847, 8042845848, + 8042845849, 8042845850, 8042845851, 8042845852, 8042845853, 8042845854, + 8042845855, 8042845857, 8042845856, 8042845859, 8042845858, 8042845860, + 4036899624, + ], + }, + { + type: "way", + id: 577572399, + nodes: [ + 5536620837, 5536620838, 5536620839, 5536620840, 5536620841, 5536620842, + 5536620843, 5536620844, 5536620845, 5536620846, 5536620837, + ], + }, + ], + } -function initDownloads(query: string){ - - - const d = {"version":0.6,"generator":"Overpass API 0.7.57 93a4d346","osm3s":{"timestamp_osm_base":"2022-02-13T23:54:06Z","copyright":"The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."},"elements":[{"type":"node","id":518224450,"lat":51.1548065,"lon":3.1880118,"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"street_side"}},{"type":"node","id":665418924,"lat":51.1575547,"lon":3.20522,"tags":{"amenity":"parking"}},{"type":"node","id":1168727903,"lat":51.1299141,"lon":3.1776123,"tags":{"amenity":"drinking_water","mapillary":"https://www.mapillary.com/app/?lat=51.129853685131906&lng=3.177603984688602&z=17&pKey=SEyKzIMUeKssni1ZLVe-9A&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01&x=0.5168826751181941&y=0.6114877557873634&zoom=0"}},{"type":"node","id":1168728245,"lat":51.1290938,"lon":3.1767502,"tags":{"amenity":"drinking_water","mapillary":"https://www.mapillary.com/app/?lat=51.129104406662464&lng=3.176675795895676&z=17&pKey=vSP3D_hWv3XCBtH75GnYUQ&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01"}},{"type":"node","id":1725842653,"lat":51.153364,"lon":3.2352655,"tags":{"amenity":"bench"}},{"type":"node","id":1744641290,"lat":51.1389321,"lon":3.2385407,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":1746891135,"lat":51.1598841,"lon":3.2361425,"tags":{"amenity":"bench"}},{"type":"node","id":1810326078,"lat":51.1550855,"lon":3.2349358,"tags":{"amenity":"bench"}},{"type":"node","id":1810326092,"lat":51.1552302,"lon":3.234968,"tags":{"amenity":"bench"}},{"type":"node","id":2325437742,"lat":51.1770052,"lon":3.1967794,"tags":{"board_type":"board","information":"board","name":"Tillegembos","tourism":"information"}},{"type":"node","id":2325437743,"lat":51.1787363,"lon":3.1949036,"tags":{"board_type":"board","information":"board","name":"Tillegembos","tourism":"information"}},{"type":"node","id":2325437813,"lat":51.1733102,"lon":3.1895672,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437839,"lat":51.1763436,"lon":3.1984985,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437848,"lat":51.1770966,"lon":3.1963507,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437862,"lat":51.1773439,"lon":3.1948779,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437867,"lat":51.1775994,"lon":3.1888088,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437873,"lat":51.1778384,"lon":3.1913802,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2732486257,"lat":51.129741,"lon":3.1907419,"tags":{"board_type":"nature","information":"board","name":"Doeveren","tourism":"information"}},{"type":"node","id":3774054068,"lat":51.1586662,"lon":3.2271102,"tags":{"amenity":"bench"}},{"type":"node","id":4769106605,"lat":51.138264,"lon":3.1798655,"tags":{"backrest":"yes","leisure":"picnic_table"}},{"type":"node","id":4912238707,"lat":51.1448634,"lon":3.2455986,"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Oostkamp","parking":"Carpool"}},{"type":"node","id":5637212235,"lat":51.1305439,"lon":3.1866873,"tags":{"board_type":"nature","image":"https://i.imgur.com/HehOQL9.jpg","information":"board","name":"Welkom Doeveren","tourism":"information"}},{"type":"node","id":5637224573,"lat":51.1281084,"lon":3.1881726,"tags":{"board_type":"nature","information":"board","name":"Welkom Doeveren","tourism":"information"}},{"type":"node","id":5637230107,"lat":51.1280884,"lon":3.1889798,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":5637743026,"lat":51.1295973,"lon":3.1751122,"tags":{"information":"board","name":"Doeveren Wandelroute","tourism":"information"}},{"type":"node","id":5716130103,"lat":51.1767183,"lon":3.1947867,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":5745783208,"lat":51.1782581,"lon":3.2410111,"tags":{"amenity":"bench"}},{"type":"node","id":5745807545,"lat":51.1784037,"lon":3.2369439,"tags":{"amenity":"bench"}},{"type":"node","id":5745807551,"lat":51.1783278,"lon":3.236678,"tags":{"amenity":"bench"}},{"type":"node","id":6535241426,"lat":51.1693142,"lon":3.1673093,"tags":{"amenity":"bench"}},{"type":"node","id":6535241427,"lat":51.169265,"lon":3.1673159,"tags":{"amenity":"bench"}},{"type":"node","id":6535241428,"lat":51.1692199,"lon":3.1673224,"tags":{"amenity":"bench"}},{"type":"node","id":6535241430,"lat":51.1685726,"lon":3.1678225,"tags":{"bench":"yes","leisure":"picnic_table","material":"wood"}},{"type":"node","id":6536026827,"lat":51.1703142,"lon":3.1691109,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6536026828,"lat":51.1702795,"lon":3.1691552,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6712112244,"lat":51.1595064,"lon":3.2021482,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7304050040,"lat":51.1560908,"lon":3.1748919,"tags":{"amenity":"bench"}},{"type":"node","id":7304050041,"lat":51.1560141,"lon":3.1749533,"tags":{"amenity":"bench"}},{"type":"node","id":7304050042,"lat":51.156032,"lon":3.1749379,"tags":{"amenity":"bench"}},{"type":"node","id":7439979218,"lat":51.1780402,"lon":3.2178666,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":7439979219,"lat":51.1780508,"lon":3.2179033,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":7529262982,"lat":51.1585566,"lon":3.1715528,"tags":{"board_type":"map","information":"board","tourism":"information"}},{"type":"node","id":7529262984,"lat":51.1585786,"lon":3.1715385,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7554879668,"lat":51.1573713,"lon":3.2043731,"tags":{"access":"yes","amenity":"toilets","fee":"no","toilets:disposal":"flush","unisex":"yes","wheelchair":"yes"}},{"type":"node","id":7554879669,"lat":51.1594855,"lon":3.2021507,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7556988723,"lat":51.1330234,"lon":3.1839944,"tags":{"amenity":"bench","material":"wood"}},{"type":"node","id":7575825326,"lat":51.1386553,"lon":3.1797358,"tags":{"amenity":"bench"}},{"type":"node","id":7575825327,"lat":51.1382456,"lon":3.1797422,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":8109498958,"lat":51.1332267,"lon":3.2341272,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":8109498959,"lat":51.1335011,"lon":3.2343954,"tags":{"leisure":"picnic_table"}},{"type":"node","id":8198894646,"lat":51.125688,"lon":3.1856217,"tags":{"image":"https://i.imgur.com/O5kX20u.jpg","information":"board","tourism":"information"}},{"type":"node","id":8199012519,"lat":51.1262245,"lon":3.1802429,"tags":{"image":"https://i.imgur.com/tomw9p5.jpg","information":"board","tourism":"information"}},{"type":"node","id":8199244816,"lat":51.1252874,"lon":3.1837622,"tags":{"amenity":"bench"}},{"type":"node","id":8199301617,"lat":51.1256827,"lon":3.1853543,"tags":{"amenity":"bench","backrest":"no"}},{"type":"node","id":8255488518,"lat":51.1406698,"lon":3.235178,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":9316104741,"lat":51.1330984,"lon":3.2335257,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":9442532340,"lat":51.1763651,"lon":3.1947952,"tags":{"amenity":"bench","backrest":"yes","image":"https://i.imgur.com/eZ0Loii.jpg"}},{"type":"way","id":15242261,"nodes":[150996092,150996093,6754312552,6754312553,6754312550,6754312551,150996094,150996095,150996097,150996098,6754312560,6754312559,6754312558,150996099,6754312557,150996100,6754312555,6754312556,150996101,6754312554,150996092],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":16514228,"nodes":[170464837,170464839,170464840,170464841,170464837],"tags":{"access":"yes","amenity":"parking","fee":"no","maxstay":"4 hours","parking":"surface"}},{"type":"way","id":76706071,"nodes":[903903386,1038557094,1038557233,1038557143,1038557075,903903387,1038557195,903903388,903903390,903904576,903903386],"tags":{"access":"permissive","amenity":"parking"}},{"type":"way","id":89601157,"nodes":[1038557083,1038557078,1038557104,1038557072,1038557108,1038557230,1038557227,1038557102,1038557137,1038575040,1038557191,1038557014,6960473080,1038557035,1038557012,1038557083],"tags":{"amenity":"parking"}},{"type":"way","id":89604999,"nodes":[1038583404,1038583491,1038583375,1038583483,1038583479,1038583398,1038583459,1038583456,1038583446,1038583441,1038583425,1038583501,1038583451,1038583463,1038583476,1038583404],"tags":{"access":"yes","amenity":"parking","capacity":"57","carpool":"yes","description":"carpoolparking","fee":"no","name":"Loppem","parking":"surface"}},{"type":"way","id":92035679,"nodes":[1069177920,6853179264,1069177925,1069177919,6853179269,6853179268,6853179267,6853179215,6853179213,6853179214,1069178133,1069177984,6853179230,6853179228,6853179229,6853179224,6853179225,6853179227,6853179226,6853179216,6853179220,6853179219,6853179218,6853179217,6853179223,6853179221,6853179222,1069177967,1069177852,6853179211,6853179212,6853179210,6853179327,6853179208,6853179209,6853179203,1069177976,6853179207,6853179206,6853179205,6853179204,6853179202,1069177849,6852012579,6852012578,6852012580,6852012577,6852012581,6852012582,6852012583,1069177845,1759437085,1519342743,1519342742,1069178166,1069177853,1069177915,6853179235,6853179234,6853179236,1069177933,6853179237,6853179238,1069178021,6853179246,6853179244,6853179245,6853179240,6853179243,6853179241,6853179242,6853179239,6853179248,6853179249,6853179250,6853179247,1069177873,6853179262,6853179263,6853179260,6853179261,6853179256,6853179259,6853179257,6853179258,1069177920],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":101248451,"nodes":[1168728158,1168728325,1168728159,5637669355,1168728109,1168728158],"tags":{"access":"private","amenity":"parking","name":"Parking Merkenveld"}},{"type":"way","id":101248462,"nodes":[1168727876,1168728288,1168728412,1168728208,1168727876],"tags":{"amenity":"toilets","building":"yes","source:geometry:date":"2019-03-14","source:geometry:ref":"Gbg/6588148"}},{"type":"way","id":131622387,"nodes":[1448421093,1448421099,1448421091,1448421081,1448421093],"tags":{"amenity":"parking","name":"Tudor - Zeeweg","parking":"surface"}},{"type":"way","id":145691934,"nodes":[1590642859,1590642860,1590642858,1590642849,1590642859],"tags":{"amenity":"parking"}},{"type":"way","id":145691937,"nodes":[1590642829,1590642828,1590642830,1590642832,1590642829],"tags":{"amenity":"parking"}},{"type":"way","id":158901716,"nodes":[1710245713,1710245718,1710245707,1710245705,1710245703,1710245715,1710245711,1710245709,1710245701,1710245713],"tags":{"access":"yes","amenity":"parking","capacity":"14","fee":"no","parking":"surface"}},{"type":"way","id":158904558,"nodes":[1710262742,1710262745,1710262735,1710262733,1710262732,1710262743,1710262741,1710262739,1710262744,1710262737,1710262736,1710262731,1710262738,1710262742],"tags":{"access":"yes","alt_name":"Schoolparking","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":158906028,"nodes":[1710276259,1710276251,1810330766,1710276255,1710276261,1710276240,1710276232,1710276257,1710276243,1710276253,1810347217,1710276242,1710276259],"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Parking Sportcentrum De Valkaart","parking":"surface"}},{"type":"way","id":160825858,"nodes":[1728421375,1728421374,1728421379,1728421377,1728421376,1728421378,1728421375],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":162602213,"nodes":[1920143232,7393009684,7393048385,1744641293,1523513488,1744641292,1920143232],"tags":{"amenity":"parking","capacity":"15"}},{"type":"way","id":165489167,"nodes":[4912197370,4912197365,4912197373,4912197364,4912197372,1770289505,4912197362,4912197371,4912197374,4912197363,4912197368,4912197366,4912197369,4912197367,4912197370],"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Bad Neuheimplein","parking":"surface"}},{"type":"way","id":168291852,"nodes":[1795793399,4979389763,1795793409,1795793395,1795793393,1795793397,1795793407,1795793406,1795793408,1795793405,1795793399],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":169875513,"nodes":[1810345951,1810345955,1810345947,1810345944,1810345951],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":170015605,"nodes":[1811673425,1811673421,1811673418,1811673423,1811673425],"tags":{"access":"private","amenity":"parking","name":"gheeraert E40","operator":"Gheeraert"}},{"type":"way","id":170018487,"nodes":[1811699779,1811699778,1811699776,1811699777,1811699779],"tags":{"access":"private","amenity":"parking","name":"Gheeraert vooraan"}},{"type":"way","id":170559194,"nodes":[1817319304,1817319302,1817319297,1817319301,1817319304],"tags":{"access":"private","amenity":"parking","name":"Gheeraert laadkade"}},{"type":"way","id":170559195,"nodes":[1817319299,1817319303,1817319300,1817319292,1817319299],"tags":{"access":"private","amenity":"parking","name":"Gheeraert spoorweg trailers"}},{"type":"way","id":170559196,"nodes":[1817319293,1817319289,1817319291,1817319296,1817319293],"tags":{"access":"private","amenity":"parking","name":"Gheeraert spoorweg trucks"}},{"type":"way","id":170559197,"nodes":[1817319294,1817319298,1817319295,1817319290,1817319294],"tags":{"access":"private","amenity":"parking","name":"Gheeraert zijkant"}},{"type":"way","id":170559292,"nodes":[1817320496,1817320494,1817320493,1817320495,1817320496],"tags":{"access":"private","amenity":"parking","name":"Gheeraert vooraan gebouw"}},{"type":"way","id":170559832,"nodes":[1817324515,1817324509,1817324491,6397031888,4044172008,4044172003,4044171997,4044171962,4044171957,4044171976,1817324489,1817324488,3550860268,1817324505,3550860269,1817324513,1817324515],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":223674368,"nodes":[2325437844,8191691971,8191691973,8191691972,2325437840,8191691970,414025563,2325437844],"tags":{"amenity":"parking","name":"Tillegembos","parking":"surface"}},{"type":"way","id":237214948,"nodes":[2451574741,2451574742,2451574744,1015583939,2451574741],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":237214949,"nodes":[2451574748,2451574746,2451574747,2451574749,2451574748],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":237214950,"nodes":[2451569392,1015567837,2451574745,2451574743,2451578121,2451569392],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":325909586,"nodes":[3325315243,111759500,3325315247,3325315232,3325315230,3325315226,1169056712,3325315243],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":326834162,"nodes":[3335137807,3335137692,3335137795,9163493632,3335137802,3335137812,9163486855,9163486856,3335137823,3335137807],"tags":{"access":"customers","amenity":"parking","operator":"Best Western Weinebrugge","parking":"surface"}},{"type":"way","id":327849054,"nodes":[3346575929,3346575873,3346575876,3346575843,3346575845,3346575891,3346575901,3346575923,3346575928,3346575950,3346575929],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":327849055,"nodes":[3346575945,3346575946,3346575943,3346575933,3346575925,3346575917,3346575903,3346575908,3346575889,3346575886,3346575945],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"way","id":327849056,"nodes":[3346575865,3346575853,3346575855,3346575846,3346575840,3346575858,3346575865],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":374677860,"nodes":[3780611504,3780611502,3780611500,3780611503,3780611504],"tags":{"amenity":"parking"}},{"type":"way","id":374677861,"nodes":[3780611495,3780611493,3780611492,3780611494,3780611495],"tags":{"amenity":"parking"}},{"type":"way","id":374677862,"nodes":[3780611498,3780611497,3780611496,3780611499,3780611501,3780611498],"tags":{"amenity":"parking"}},{"type":"way","id":389912371,"nodes":[3930713440,3930713451,3930713447,3930713437,3930713440],"tags":{"amenity":"parking"}},{"type":"way","id":389912372,"nodes":[3930713454,3930713442,3930713435,3930713429,5826811614,3930713426,3930713430,6982605752,6982605754,3930713438,6982605769,6982605766,6982605767,6982605762,6982605764,3930713446,3930713454],"tags":{"access":"customers","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":401995684,"nodes":[4044171963,4044171954,4044171924,4044171936,4044171941,4044171963],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":500067820,"nodes":[4912203166,4912203165,4912203164,4912203163,4912203162,4912203161,4912203160,4912203166],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067821,"nodes":[4912203170,4912203169,4912203168,4912203167,4912203170],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067822,"nodes":[4912203174,4912203173,4912203172,4912203171,4912203174],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067823,"nodes":[4912203179,4912203178,4912203177,4912203176,4912203179],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500069613,"nodes":[4912214695,4912214685,4912214694,4912214693,4912214692,4912214680,4912214695],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071452,"nodes":[4912225068,4912225062,4912225063,4912225053,4912225064,4912214694,4912214685,4912225068],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071455,"nodes":[4912225070,4912214681,4912214686,4912225052,4912225051,4912225067,4912225062,4912225068,4912225070],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071458,"nodes":[4912214695,4912214680,4912225069,4912225050,1525460846,4912214681,4912225070,4912214695],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":533276307,"nodes":[5173881316,5173881317,5173881318,7054196467,5173881316],"tags":{"amenity":"parking"}},{"type":"way","id":533276308,"nodes":[5173881320,5173881621,5173881622,5173881623,5173881320],"tags":{"amenity":"parking"}},{"type":"way","id":533276309,"nodes":[5173881624,5173881625,5173881626,5173881627,5173881624],"tags":{"amenity":"parking"}},{"type":"way","id":579848581,"nodes":[4043782112,5825400688,5552466020,6997096756,6997096759,6997096758,5552466018,5552466013,5552466014,6997076567,4043782112],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":584455569,"nodes":[5586765933,5586765934,5586765935,5586765936,5586765933],"tags":{"amenity":"parking"}},{"type":"way","id":585977870,"nodes":[5587844460,5599314384,5599314385,1659850846,6870850178,5587844462,5587844461,6870863414,5587844460],"tags":{"amenity":"parking"}},{"type":"way","id":587014342,"nodes":[5607796820,5607798725,5607798721,5607798722,5607796819,5607798723,5607796820],"tags":{"access":"permissive","amenity":"parking","park_ride":"no","parking":"surface","surface":"paved"}},{"type":"way","id":590167103,"nodes":[5635001277,5635001274,5635001275,5635001276,5635001277],"tags":{"amenity":"parking"}},{"type":"way","id":590167113,"nodes":[5635001312,5635001306,7767137237,7767137235,7767137236,7767137234,5635001312],"tags":{"amenity":"parking"}},{"type":"way","id":590167134,"nodes":[5635001374,5635001373,5635001372,5635001371,5635001374],"tags":{"amenity":"parking"}},{"type":"way","id":590167135,"nodes":[5635001378,5635001377,5635001376,5635001375,5635001378],"tags":{"amenity":"parking"}},{"type":"way","id":590167136,"nodes":[5635001382,5635001381,5635001380,5635001379,5635001382],"tags":{"amenity":"parking"}},{"type":"way","id":590167137,"nodes":[5635001386,5882873336,5882873337,5882873338,5882873335,6593340582,6593340583,5882873334,6593340584,5635001385,5635001384,5635001383,5635001386],"tags":{"amenity":"parking"}},{"type":"way","id":590167147,"nodes":[5635001417,5635001414,5635001415,5635001416,5635001417],"tags":{"amenity":"parking"}},{"type":"way","id":601406079,"nodes":[5716136617,5716136618,5716136619,5716136620,5716136617],"tags":{"amenity":"parking"}},{"type":"way","id":632813009,"nodes":[5974489618,1810326044,1810326087,5974489617,5972179348,5972179347,5972179346,5972179345,5972179344,5972179343,5972179331,5974489616,5974489615,5974489614,5974489618],"tags":{"amenity":"parking","name":"Gemeenteplein","parking":"surface"}},{"type":"way","id":668043297,"nodes":[6255587424,6255587425,6255587426,6255587427,6255587424],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":670104236,"nodes":[6275462768,6275462769,6275462770,6275462771,6275462768],"tags":{"amenity":"parking"}},{"type":"way","id":670104238,"nodes":[6275462772,6275462989,6275462773,6275462774,6275462775,6275462772],"tags":{"amenity":"parking"}},{"type":"way","id":670104239,"nodes":[6275462776,6275462777,6275462778,6275462779,6275462776],"tags":{"amenity":"parking"}},{"type":"way","id":670104241,"nodes":[6275462780,6275462781,6275462782,6275462783,6275462780],"tags":{"amenity":"parking"}},{"type":"way","id":670104242,"nodes":[6275462784,6275462985,6275462988,6275462986,6275462987,6275462784],"tags":{"amenity":"parking"}},{"type":"way","id":671840055,"nodes":[6291339827,6291339828,6291339816,6291339815,6291339822,6291339821,6291339829,6291339830,6291339827],"tags":{"amenity":"parking"}},{"type":"way","id":695825223,"nodes":[1519476746,6533893620,6533893621,6533893622,1519476797,1519476620,1519476746],"tags":{"access":"yes","amenity":"parking"}},{"type":"way","id":695825224,"nodes":[1519476744,6533893624,6533893625,6533893626,1519476698,1519476635,1519476744],"tags":{"access":"yes","amenity":"parking"}},{"type":"way","id":696040917,"nodes":[6536026850,6536026851,6536026852,6536026853,6536026850],"tags":{"amenity":"parking","name":"Kasteel Tudor"}},{"type":"way","id":696043218,"nodes":[6536038234,6536038235,6536038236,6536038237,6536038234],"tags":{"access":"customers","amenity":"parking"}},{"type":"way","id":700675991,"nodes":[6579962064,6579962065,6579962066,6579962068,6579962067,6579962064],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":705278707,"nodes":[6625037999,6625038000,6625038001,6625038002,6625038003,6004375826,6625038005,6625038006,6625037999],"tags":{"amenity":"parking"}},{"type":"way","id":719482833,"nodes":[6754312544,6754312543,6754312542,6754312541,6754312544],"tags":{"access":"yes","amenity":"parking","capacity":"5"}},{"type":"way","id":719482834,"nodes":[6754312565,6754312564,6754312563,6754312562,6754312565],"tags":{"access":"yes","amenity":"parking","capacity":"12"}},{"type":"way","id":737054013,"nodes":[5826811496,5826811497,5826811494,5826811495,5826811496],"tags":{"amenity":"parking"}},{"type":"way","id":737054014,"nodes":[5826810676,5826810673,5826810674,5826810675,5826810676],"tags":{"amenity":"parking"}},{"type":"way","id":737054015,"nodes":[5826810681,5826810678,5826810679,5826810680,5826810681],"tags":{"amenity":"parking"}},{"type":"way","id":737093410,"nodes":[5826811559,5826811536,5826811535,5826811561,5826811560,5826811559],"tags":{"access":"yes","amenity":"parking","capacity":"4","fee":"no","parking":"surface"}},{"type":"way","id":737093411,"nodes":[5826811551,5826811547,5826811548,5826811549,5826811550,5826811551],"tags":{"access":"yes","amenity":"parking","capacity":"4","fee":"no","parking":"surface"}},{"type":"way","id":739652949,"nodes":[6925536542,6925536541,6925536540,6925536539,6925536542],"tags":{"amenity":"parking","capacity":"6","parking":"surface"}},{"type":"way","id":741675236,"nodes":[6943148207,6943148206,6943148205,6943148204,1637742821,6943148203,6943148202,6943148201,6943148200,6943148199,6943148198,6943148197,6943148207],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":742295526,"nodes":[6949357909,6949357908,6949357907,6949357906,6949357909],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295527,"nodes":[6949357913,6949357912,6949357911,6949357910,6949357913],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295528,"nodes":[6949357917,6949357916,6949357915,6949357914,6949357917],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295529,"nodes":[6949357921,6949357920,6949357919,6949357918,6949357921],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":746170866,"nodes":[6982906547,6982906546,6982906545,6982906544,6982906543,6982906542,6982906547],"tags":{"access":"customers","amenity":"parking"}},{"type":"way","id":747880657,"nodes":[3325315397,6997076566,6997076565,6997076563,6997076562,6997076564,3325315395,3325315397],"tags":{"access":"customers","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":763977465,"nodes":[7137343680,7137343681,7137343682,7137343683,7137343680],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":763977466,"nodes":[7137343684,7137383185,7137383186,7137383187,7137343684],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821090,"nodes":[7519058290,7519058289,7519058288,7519058287,7519058290],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821091,"nodes":[7519058294,7519058293,7519058292,7519058291,7519058294],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821092,"nodes":[7519058298,7519058297,7519058296,7519058295,7519058298],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821093,"nodes":[7519058302,7519058301,7519058300,7519058299,7519058302],"tags":{"access":"private","amenity":"parking","capacity":"6","parking":"surface"}},{"type":"way","id":804963962,"nodes":[7529417225,7529417226,7529417227,7529417228,7529417229,7529417230,7529417232,7529417225],"tags":{"access":"customers","amenity":"parking","fee":"no","operator":"’t Kiekekot","parking":"surface","surface":"unpaved"}},{"type":"way","id":806875503,"nodes":[4042671969,7545532512,7545532514,7545532513,3359977305,4042671969],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":806963547,"nodes":[7546193222,7546193221,7546193220,7546193219,7546193222],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":865357121,"nodes":[8065883228,8065883227,8065883226,8065883225,8065883228],"tags":{"amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":865357122,"nodes":[8065883233,8065883230,8065883229,8065883232,8065883233],"tags":{"amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":879281221,"nodes":[8179735269,8179735268,8179735267,8179735266,8179735265,8179735264,8179735224,8179735269],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"way","id":881770201,"nodes":[8200275847,8200275848,8200275844,8200275853,8200275849,8200275847],"tags":{"amenity":"parking","parking":"surface","surface":"grass"}},{"type":"way","id":978360549,"nodes":[5761770202,5761770204,9052878229,9052878228,5761770202],"tags":{"amenity":"parking"}},{"type":"way","id":1009692722,"nodes":[9316118540,9316118536,9316118531,9316118535,9316118534,9316118533,9316118530,9316118532,3311835478,9316118540],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"relation","id":8188853,"members":[{"type":"way","ref":577572397,"role":"outer"},{"type":"way","ref":577572399,"role":"outer"}],"tags":{"access":"guided","curator":"Luc Maene;Geert De Clercq","email":"lucmaene@hotmail.com;geert.de.clercq1@pandora.be","landuse":"meadow","leisure":"nature_reserve","name":"De Wulgenbroeken","natural":"wetland","operator":"Natuurpunt Brugge","type":"multipolygon","website":"https://natuurpuntbrugge.be/wulgenbroeken/","wetland":"wet_meadow","wikidata":"Q60061498","wikipedia":"nl:Wulgenbroeken"}},{"type":"relation","id":11163488,"members":[{"type":"way","ref":810604915,"role":"outer"},{"type":"way","ref":989393316,"role":"outer"},{"type":"way","ref":389026405,"role":"inner"},{"type":"way","ref":810607458,"role":"outer"}],"tags":{"access":"yes","curator":"Kris Lesage","description":"Wat Doeveren zo uniek maakt, zijn zijn kleine heidegebiedjes met soorten die erg verschillen van de Kempense heide. Doeveren en omstreken was vroeger één groot heidegebied, maar bestaat nu grotendeels uit bossen.","dog":"leashed","email":"doeveren@natuurpuntzedelgem.be","image":"https://i.imgur.com/NEAsQZG.jpg","image:0":"https://i.imgur.com/Dq71hyQ.jpg","image:1":"https://i.imgur.com/mAIiT4f.jpg","image:2":"https://i.imgur.com/dELZU97.jpg","image:3":"https://i.imgur.com/Bso57JC.jpg","image:4":"https://i.imgur.com/9DtcfXo.jpg","image:5":"https://i.imgur.com/0R6eBfk.jpg","image:6":"https://i.imgur.com/b0JpvbR.jpg","leisure":"nature_reserve","name":"Doeveren","operator":"Natuurpunt Zedelgem","phone":"+32 486 25 25 30","type":"multipolygon","website":"https://www.natuurpuntzedelgem.be/gebieden/doeveren/","wikidata":"Q56395754","wikipedia":"nl:Doeveren (natuurgebied)"}},{"type":"relation","id":11790117,"members":[{"type":"way","ref":863373849,"role":"outer"},{"type":"way","ref":777280458,"role":"outer"}],"tags":{"access":"no","description:0":"In gebruik als waterbuffering","leisure":"nature_reserve","name":"Kerkebeek","operator":"Natuurpunt Brugge","type":"multipolygon"}},{"type":"node","id":518224450,"lat":51.1548065,"lon":3.1880118,"timestamp":"2021-08-14T21:49:28Z","version":5,"changeset":109683837,"user":"effem","uid":16437,"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"street_side"}},{"type":"node","id":665418924,"lat":51.1575547,"lon":3.20522,"timestamp":"2012-05-12T20:13:39Z","version":2,"changeset":11580224,"user":"martino260","uid":655442,"tags":{"amenity":"parking"}},{"type":"node","id":1168727903,"lat":51.1299141,"lon":3.1776123,"timestamp":"2017-04-03T08:34:05Z","version":2,"changeset":47403889,"user":"philippec","uid":76884,"tags":{"amenity":"drinking_water","mapillary":"https://www.mapillary.com/app/?lat=51.129853685131906&lng=3.177603984688602&z=17&pKey=SEyKzIMUeKssni1ZLVe-9A&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01&x=0.5168826751181941&y=0.6114877557873634&zoom=0"}},{"type":"node","id":1168728245,"lat":51.1290938,"lon":3.1767502,"timestamp":"2019-10-07T11:06:57Z","version":3,"changeset":75370316,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"drinking_water","mapillary":"https://www.mapillary.com/app/?lat=51.129104406662464&lng=3.176675795895676&z=17&pKey=vSP3D_hWv3XCBtH75GnYUQ&focus=photo&dateTo=2017-04-02&dateFrom=2017-04-01"}},{"type":"node","id":1725842653,"lat":51.153364,"lon":3.2352655,"timestamp":"2012-07-02T17:33:00Z","version":2,"changeset":12090625,"user":"martino260","uid":655442,"tags":{"amenity":"bench"}},{"type":"node","id":1744641290,"lat":51.1389321,"lon":3.2385407,"timestamp":"2012-09-18T13:29:52Z","version":3,"changeset":13156159,"user":"martino260","uid":655442,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":1746891135,"lat":51.1598841,"lon":3.2361425,"timestamp":"2012-05-09T18:22:11Z","version":1,"changeset":11551825,"user":"martino260","uid":655442,"tags":{"amenity":"bench"}},{"type":"node","id":1810326078,"lat":51.1550855,"lon":3.2349358,"timestamp":"2012-07-02T19:50:15Z","version":1,"changeset":12093439,"user":"martino260","uid":655442,"tags":{"amenity":"bench"}},{"type":"node","id":1810326092,"lat":51.1552302,"lon":3.234968,"timestamp":"2012-07-02T19:50:16Z","version":1,"changeset":12093439,"user":"martino260","uid":655442,"tags":{"amenity":"bench"}},{"type":"node","id":2325437742,"lat":51.1770052,"lon":3.1967794,"timestamp":"2013-05-30T12:19:08Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"board_type":"board","information":"board","name":"Tillegembos","tourism":"information"}},{"type":"node","id":2325437743,"lat":51.1787363,"lon":3.1949036,"timestamp":"2013-05-30T12:19:08Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"board_type":"board","information":"board","name":"Tillegembos","tourism":"information"}},{"type":"node","id":2325437813,"lat":51.1733102,"lon":3.1895672,"timestamp":"2013-05-30T12:19:09Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437839,"lat":51.1763436,"lon":3.1984985,"timestamp":"2013-05-30T12:19:10Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437848,"lat":51.1770966,"lon":3.1963507,"timestamp":"2013-05-30T12:19:10Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437862,"lat":51.1773439,"lon":3.1948779,"timestamp":"2013-05-30T12:19:10Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437867,"lat":51.1775994,"lon":3.1888088,"timestamp":"2013-05-30T12:19:11Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2325437873,"lat":51.1778384,"lon":3.1913802,"timestamp":"2013-05-30T12:19:11Z","version":1,"changeset":16350909,"user":"peeweeke","uid":494726,"tags":{"amenity":"bench","backrest":"yes","material":"wood"}},{"type":"node","id":2732486257,"lat":51.129741,"lon":3.1907419,"timestamp":"2014-03-21T21:15:28Z","version":1,"changeset":21234491,"user":"meannder","uid":149496,"tags":{"board_type":"nature","information":"board","name":"Doeveren","tourism":"information"}},{"type":"node","id":3774054068,"lat":51.1586662,"lon":3.2271102,"timestamp":"2015-10-05T20:34:04Z","version":1,"changeset":34456387,"user":"TripleBee","uid":497177,"tags":{"amenity":"bench"}},{"type":"node","id":4769106605,"lat":51.138264,"lon":3.1798655,"timestamp":"2020-05-31T19:49:45Z","version":3,"changeset":86019474,"user":"Hopperpop","uid":3664604,"tags":{"backrest":"yes","leisure":"picnic_table"}},{"type":"node","id":4912238707,"lat":51.1448634,"lon":3.2455986,"timestamp":"2017-06-13T08:12:04Z","version":1,"changeset":49491753,"user":"Jakka","uid":2403313,"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Oostkamp","parking":"Carpool"}},{"type":"node","id":5637212235,"lat":51.1305439,"lon":3.1866873,"timestamp":"2021-11-22T11:54:45Z","version":4,"changeset":114095475,"user":"L'imaginaire","uid":654234,"tags":{"board_type":"nature","image":"https://i.imgur.com/HehOQL9.jpg","information":"board","name":"Welkom Doeveren","tourism":"information"}},{"type":"node","id":5637224573,"lat":51.1281084,"lon":3.1881726,"timestamp":"2020-06-01T22:39:30Z","version":2,"changeset":86065716,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"board_type":"nature","information":"board","name":"Welkom Doeveren","tourism":"information"}},{"type":"node","id":5637230107,"lat":51.1280884,"lon":3.1889798,"timestamp":"2018-05-23T11:55:01Z","version":1,"changeset":59208628,"user":"Jakka","uid":2403313,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":5637743026,"lat":51.1295973,"lon":3.1751122,"timestamp":"2021-10-08T08:53:14Z","version":2,"changeset":112251989,"user":"DieterWesttoer","uid":13062237,"tags":{"information":"board","name":"Doeveren Wandelroute","tourism":"information"}},{"type":"node","id":5716130103,"lat":51.1767183,"lon":3.1947867,"timestamp":"2018-06-24T22:04:21Z","version":1,"changeset":60130942,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":5745783208,"lat":51.1782581,"lon":3.2410111,"timestamp":"2018-07-07T18:42:23Z","version":1,"changeset":60494990,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench"}},{"type":"node","id":5745807545,"lat":51.1784037,"lon":3.2369439,"timestamp":"2018-07-07T18:58:25Z","version":1,"changeset":60495307,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench"}},{"type":"node","id":5745807551,"lat":51.1783278,"lon":3.236678,"timestamp":"2018-07-07T18:58:25Z","version":1,"changeset":60495307,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench"}},{"type":"node","id":6535241426,"lat":51.1693142,"lon":3.1673093,"timestamp":"2019-06-09T13:50:19Z","version":1,"changeset":71071874,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":6535241427,"lat":51.169265,"lon":3.1673159,"timestamp":"2019-06-09T13:50:19Z","version":1,"changeset":71071874,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":6535241428,"lat":51.1692199,"lon":3.1673224,"timestamp":"2019-06-09T13:50:19Z","version":1,"changeset":71071874,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":6535241430,"lat":51.1685726,"lon":3.1678225,"timestamp":"2019-06-09T13:50:19Z","version":1,"changeset":71071874,"user":"Hopperpop","uid":3664604,"tags":{"bench":"yes","leisure":"picnic_table","material":"wood"}},{"type":"node","id":6536026827,"lat":51.1703142,"lon":3.1691109,"timestamp":"2019-06-09T22:54:45Z","version":1,"changeset":71082671,"user":"Hopperpop","uid":3664604,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6536026828,"lat":51.1702795,"lon":3.1691552,"timestamp":"2019-06-09T22:54:45Z","version":1,"changeset":71082671,"user":"Hopperpop","uid":3664604,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6712112244,"lat":51.1595064,"lon":3.2021482,"timestamp":"2020-05-24T21:35:50Z","version":2,"changeset":85695537,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7304050040,"lat":51.1560908,"lon":3.1748919,"timestamp":"2020-03-17T19:11:00Z","version":1,"changeset":82315744,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":7304050041,"lat":51.1560141,"lon":3.1749533,"timestamp":"2020-03-17T19:11:00Z","version":1,"changeset":82315744,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":7304050042,"lat":51.156032,"lon":3.1749379,"timestamp":"2020-03-17T19:11:00Z","version":1,"changeset":82315744,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":7439979218,"lat":51.1780402,"lon":3.2178666,"timestamp":"2020-04-24T00:56:14Z","version":1,"changeset":84027933,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":7439979219,"lat":51.1780508,"lon":3.2179033,"timestamp":"2020-04-24T00:56:14Z","version":1,"changeset":84027933,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":7529262982,"lat":51.1585566,"lon":3.1715528,"timestamp":"2020-05-17T16:12:04Z","version":1,"changeset":85340264,"user":"Hopperpop","uid":3664604,"tags":{"board_type":"map","information":"board","tourism":"information"}},{"type":"node","id":7529262984,"lat":51.1585786,"lon":3.1715385,"timestamp":"2020-05-17T16:12:04Z","version":1,"changeset":85340264,"user":"Hopperpop","uid":3664604,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7554879668,"lat":51.1573713,"lon":3.2043731,"timestamp":"2020-05-24T21:35:50Z","version":1,"changeset":85695537,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"access":"yes","amenity":"toilets","fee":"no","toilets:disposal":"flush","unisex":"yes","wheelchair":"yes"}},{"type":"node","id":7554879669,"lat":51.1594855,"lon":3.2021507,"timestamp":"2020-05-24T21:35:50Z","version":1,"changeset":85695537,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7556988723,"lat":51.1330234,"lon":3.1839944,"timestamp":"2020-05-25T19:19:56Z","version":1,"changeset":85730259,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench","material":"wood"}},{"type":"node","id":7575825326,"lat":51.1386553,"lon":3.1797358,"timestamp":"2020-05-31T19:49:45Z","version":1,"changeset":86019474,"user":"Hopperpop","uid":3664604,"tags":{"amenity":"bench"}},{"type":"node","id":7575825327,"lat":51.1382456,"lon":3.1797422,"timestamp":"2020-05-31T19:49:45Z","version":1,"changeset":86019474,"user":"Hopperpop","uid":3664604,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":8109498958,"lat":51.1332267,"lon":3.2341272,"timestamp":"2020-11-11T20:42:45Z","version":1,"changeset":93951029,"user":"L'imaginaire","uid":654234,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":8109498959,"lat":51.1335011,"lon":3.2343954,"timestamp":"2020-11-11T20:42:45Z","version":1,"changeset":93951029,"user":"L'imaginaire","uid":654234,"tags":{"leisure":"picnic_table"}},{"type":"node","id":8198894646,"lat":51.125688,"lon":3.1856217,"timestamp":"2020-12-06T23:39:34Z","version":3,"changeset":95384686,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"image":"https://i.imgur.com/O5kX20u.jpg","information":"board","tourism":"information"}},{"type":"node","id":8199012519,"lat":51.1262245,"lon":3.1802429,"timestamp":"2020-12-07T00:12:14Z","version":3,"changeset":95385124,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"image":"https://i.imgur.com/tomw9p5.jpg","information":"board","tourism":"information"}},{"type":"node","id":8199244816,"lat":51.1252874,"lon":3.1837622,"timestamp":"2020-12-06T17:14:52Z","version":1,"changeset":95374174,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench"}},{"type":"node","id":8199301617,"lat":51.1256827,"lon":3.1853543,"timestamp":"2020-12-06T17:14:52Z","version":1,"changeset":95374174,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench","backrest":"no"}},{"type":"node","id":8255488518,"lat":51.1406698,"lon":3.235178,"timestamp":"2020-12-23T18:20:35Z","version":1,"changeset":96342158,"user":"L'imaginaire","uid":654234,"tags":{"amenity":"bench","backrest":"yes"}},{"type":"node","id":9316104741,"lat":51.1330984,"lon":3.2335257,"timestamp":"2021-12-06T18:31:00Z","version":1,"changeset":114629890,"user":"L'imaginaire","uid":654234,"tags":{"information":"board","tourism":"information"}},{"type":"node","id":9442532340,"lat":51.1763651,"lon":3.1947952,"timestamp":"2022-01-23T16:26:28Z","version":2,"changeset":116506336,"user":"L'imaginaire","uid":654234,"tags":{"amenity":"bench","backrest":"yes","image":"https://i.imgur.com/eZ0Loii.jpg"}},{"type":"way","id":15242261,"timestamp":"2020-04-05T07:08:45Z","version":8,"changeset":83089516,"user":"Hopperpop","uid":3664604,"nodes":[150996092,150996093,6754312552,6754312553,6754312550,6754312551,150996094,150996095,150996097,150996098,6754312560,6754312559,6754312558,150996099,6754312557,150996100,6754312555,6754312556,150996101,6754312554,150996092],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":16514228,"timestamp":"2017-06-13T07:14:23Z","version":9,"changeset":49490318,"user":"Jakka","uid":2403313,"nodes":[170464837,170464839,170464840,170464841,170464837],"tags":{"access":"yes","amenity":"parking","fee":"no","maxstay":"4 hours","parking":"surface"}},{"type":"way","id":76706071,"timestamp":"2017-06-17T07:51:31Z","version":4,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[903903386,1038557094,1038557233,1038557143,1038557075,903903387,1038557195,903903388,903903390,903904576,903903386],"tags":{"access":"permissive","amenity":"parking"}},{"type":"way","id":89601157,"timestamp":"2019-11-09T18:40:50Z","version":3,"changeset":76851428,"user":"Hopperpop","uid":3664604,"nodes":[1038557083,1038557078,1038557104,1038557072,1038557108,1038557230,1038557227,1038557102,1038557137,1038575040,1038557191,1038557014,6960473080,1038557035,1038557012,1038557083],"tags":{"amenity":"parking"}},{"type":"way","id":89604999,"timestamp":"2020-01-16T22:01:28Z","version":5,"changeset":79667667,"user":"Hopperpop","uid":3664604,"nodes":[1038583404,1038583491,1038583375,1038583483,1038583479,1038583398,1038583459,1038583456,1038583446,1038583441,1038583425,1038583501,1038583451,1038583463,1038583476,1038583404],"tags":{"access":"yes","amenity":"parking","capacity":"57","carpool":"yes","description":"carpoolparking","fee":"no","name":"Loppem","parking":"surface"}},{"type":"way","id":92035679,"timestamp":"2019-10-05T08:05:33Z","version":7,"changeset":75311122,"user":"skyman81","uid":955688,"nodes":[1069177920,6853179264,1069177925,1069177919,6853179269,6853179268,6853179267,6853179215,6853179213,6853179214,1069178133,1069177984,6853179230,6853179228,6853179229,6853179224,6853179225,6853179227,6853179226,6853179216,6853179220,6853179219,6853179218,6853179217,6853179223,6853179221,6853179222,1069177967,1069177852,6853179211,6853179212,6853179210,6853179327,6853179208,6853179209,6853179203,1069177976,6853179207,6853179206,6853179205,6853179204,6853179202,1069177849,6852012579,6852012578,6852012580,6852012577,6852012581,6852012582,6852012583,1069177845,1759437085,1519342743,1519342742,1069178166,1069177853,1069177915,6853179235,6853179234,6853179236,1069177933,6853179237,6853179238,1069178021,6853179246,6853179244,6853179245,6853179240,6853179243,6853179241,6853179242,6853179239,6853179248,6853179249,6853179250,6853179247,1069177873,6853179262,6853179263,6853179260,6853179261,6853179256,6853179259,6853179257,6853179258,1069177920],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":101248451,"timestamp":"2018-05-23T14:18:27Z","version":2,"changeset":59213416,"user":"Jakka","uid":2403313,"nodes":[1168728158,1168728325,1168728159,5637669355,1168728109,1168728158],"tags":{"access":"private","amenity":"parking","name":"Parking Merkenveld"}},{"type":"way","id":101248462,"timestamp":"2020-05-25T13:53:02Z","version":3,"changeset":85720081,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[1168727876,1168728288,1168728412,1168728208,1168727876],"tags":{"amenity":"toilets","building":"yes","source:geometry:date":"2019-03-14","source:geometry:ref":"Gbg/6588148"}},{"type":"way","id":131622387,"timestamp":"2015-01-09T12:06:51Z","version":2,"changeset":28017707,"user":"TripleBee","uid":497177,"nodes":[1448421093,1448421099,1448421091,1448421081,1448421093],"tags":{"amenity":"parking","name":"Tudor - Zeeweg","parking":"surface"}},{"type":"way","id":145691934,"timestamp":"2012-01-15T12:43:37Z","version":1,"changeset":10397429,"user":"Sanderd17","uid":253266,"nodes":[1590642859,1590642860,1590642858,1590642849,1590642859],"tags":{"amenity":"parking"}},{"type":"way","id":145691937,"timestamp":"2012-01-15T12:43:37Z","version":1,"changeset":10397429,"user":"Sanderd17","uid":253266,"nodes":[1590642829,1590642828,1590642830,1590642832,1590642829],"tags":{"amenity":"parking"}},{"type":"way","id":158901716,"timestamp":"2017-06-13T07:12:20Z","version":2,"changeset":49490264,"user":"Jakka","uid":2403313,"nodes":[1710245713,1710245718,1710245707,1710245705,1710245703,1710245715,1710245711,1710245709,1710245701,1710245713],"tags":{"access":"yes","amenity":"parking","capacity":"14","fee":"no","parking":"surface"}},{"type":"way","id":158904558,"timestamp":"2020-12-22T22:39:41Z","version":4,"changeset":96283379,"user":"M!dgard","uid":763799,"nodes":[1710262742,1710262745,1710262735,1710262733,1710262732,1710262743,1710262741,1710262739,1710262744,1710262737,1710262736,1710262731,1710262738,1710262742],"tags":{"access":"yes","alt_name":"Schoolparking","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":158906028,"timestamp":"2017-06-13T07:27:19Z","version":5,"changeset":49490646,"user":"Jakka","uid":2403313,"nodes":[1710276259,1710276251,1810330766,1710276255,1710276261,1710276240,1710276232,1710276257,1710276243,1710276253,1810347217,1710276242,1710276259],"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Parking Sportcentrum De Valkaart","parking":"surface"}},{"type":"way","id":160825858,"timestamp":"2012-04-23T20:35:52Z","version":1,"changeset":11399632,"user":"martino260","uid":655442,"nodes":[1728421375,1728421374,1728421379,1728421377,1728421376,1728421378,1728421375],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":162602213,"timestamp":"2020-04-11T18:12:16Z","version":8,"changeset":83407731,"user":"JanFi","uid":672253,"nodes":[1920143232,7393009684,7393048385,1744641293,1523513488,1744641292,1920143232],"tags":{"amenity":"parking","capacity":"15"}},{"type":"way","id":165489167,"timestamp":"2020-10-12T19:06:29Z","version":3,"changeset":92371840,"user":"L'imaginaire","uid":654234,"nodes":[4912197370,4912197365,4912197373,4912197364,4912197372,1770289505,4912197362,4912197371,4912197374,4912197363,4912197368,4912197366,4912197369,4912197367,4912197370],"tags":{"access":"yes","amenity":"parking","fee":"no","name":"Bad Neuheimplein","parking":"surface"}},{"type":"way","id":168291852,"timestamp":"2017-07-19T11:17:33Z","version":3,"changeset":50402298,"user":"martino260","uid":655442,"nodes":[1795793399,4979389763,1795793409,1795793395,1795793393,1795793397,1795793407,1795793406,1795793408,1795793405,1795793399],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":169875513,"timestamp":"2017-06-13T07:26:09Z","version":2,"changeset":49490613,"user":"Jakka","uid":2403313,"nodes":[1810345951,1810345955,1810345947,1810345944,1810345951],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":170015605,"timestamp":"2017-06-17T07:51:33Z","version":4,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1811673425,1811673421,1811673418,1811673423,1811673425],"tags":{"access":"private","amenity":"parking","name":"gheeraert E40","operator":"Gheeraert"}},{"type":"way","id":170018487,"timestamp":"2017-06-17T07:51:33Z","version":3,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1811699779,1811699778,1811699776,1811699777,1811699779],"tags":{"access":"private","amenity":"parking","name":"Gheeraert vooraan"}},{"type":"way","id":170559194,"timestamp":"2017-06-17T07:51:33Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1817319304,1817319302,1817319297,1817319301,1817319304],"tags":{"access":"private","amenity":"parking","name":"Gheeraert laadkade"}},{"type":"way","id":170559195,"timestamp":"2017-06-17T07:51:33Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1817319299,1817319303,1817319300,1817319292,1817319299],"tags":{"access":"private","amenity":"parking","name":"Gheeraert spoorweg trailers"}},{"type":"way","id":170559196,"timestamp":"2017-06-17T07:51:33Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1817319293,1817319289,1817319291,1817319296,1817319293],"tags":{"access":"private","amenity":"parking","name":"Gheeraert spoorweg trucks"}},{"type":"way","id":170559197,"timestamp":"2017-06-17T07:51:33Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1817319294,1817319298,1817319295,1817319290,1817319294],"tags":{"access":"private","amenity":"parking","name":"Gheeraert zijkant"}},{"type":"way","id":170559292,"timestamp":"2017-06-17T07:51:33Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[1817320496,1817320494,1817320493,1817320495,1817320496],"tags":{"access":"private","amenity":"parking","name":"Gheeraert vooraan gebouw"}},{"type":"way","id":170559832,"timestamp":"2020-10-07T10:51:41Z","version":6,"changeset":92105788,"user":"effem","uid":16437,"nodes":[1817324515,1817324509,1817324491,6397031888,4044172008,4044172003,4044171997,4044171962,4044171957,4044171976,1817324489,1817324488,3550860268,1817324505,3550860269,1817324513,1817324515],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":223674368,"timestamp":"2020-12-03T18:47:28Z","version":3,"changeset":95244226,"user":"L'imaginaire","uid":654234,"nodes":[2325437844,8191691971,8191691973,8191691972,2325437840,8191691970,414025563,2325437844],"tags":{"amenity":"parking","name":"Tillegembos","parking":"surface"}},{"type":"way","id":237214948,"timestamp":"2017-06-17T07:51:35Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[2451574741,2451574742,2451574744,1015583939,2451574741],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":237214949,"timestamp":"2017-06-17T07:51:35Z","version":2,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[2451574748,2451574746,2451574747,2451574749,2451574748],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":237214950,"timestamp":"2017-06-17T07:51:35Z","version":3,"changeset":49608752,"user":"rowers2","uid":2445224,"nodes":[2451569392,1015567837,2451574745,2451574743,2451578121,2451569392],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":325909586,"timestamp":"2016-01-05T18:51:49Z","version":2,"changeset":36387330,"user":"JanFi","uid":672253,"nodes":[3325315243,111759500,3325315247,3325315232,3325315230,3325315226,1169056712,3325315243],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":326834162,"timestamp":"2021-10-10T21:49:11Z","version":4,"changeset":112350014,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[3335137807,3335137692,3335137795,9163493632,3335137802,3335137812,9163486855,9163486856,3335137823,3335137807],"tags":{"access":"customers","amenity":"parking","operator":"Best Western Weinebrugge","parking":"surface"}},{"type":"way","id":327849054,"timestamp":"2015-02-12T20:26:22Z","version":1,"changeset":28807613,"user":"escada","uid":436365,"nodes":[3346575929,3346575873,3346575876,3346575843,3346575845,3346575891,3346575901,3346575923,3346575928,3346575950,3346575929],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":327849055,"timestamp":"2016-10-08T21:24:46Z","version":2,"changeset":42742859,"user":"maggot27","uid":118021,"nodes":[3346575945,3346575946,3346575943,3346575933,3346575925,3346575917,3346575903,3346575908,3346575889,3346575886,3346575945],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"way","id":327849056,"timestamp":"2015-02-12T20:26:22Z","version":1,"changeset":28807613,"user":"escada","uid":436365,"nodes":[3346575865,3346575853,3346575855,3346575846,3346575840,3346575858,3346575865],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":374677860,"timestamp":"2015-10-10T09:08:20Z","version":1,"changeset":34545536,"user":"Jakka","uid":2403313,"nodes":[3780611504,3780611502,3780611500,3780611503,3780611504],"tags":{"amenity":"parking"}},{"type":"way","id":374677861,"timestamp":"2015-10-10T09:08:20Z","version":1,"changeset":34545536,"user":"Jakka","uid":2403313,"nodes":[3780611495,3780611493,3780611492,3780611494,3780611495],"tags":{"amenity":"parking"}},{"type":"way","id":374677862,"timestamp":"2015-10-10T09:08:20Z","version":1,"changeset":34545536,"user":"Jakka","uid":2403313,"nodes":[3780611498,3780611497,3780611496,3780611499,3780611501,3780611498],"tags":{"amenity":"parking"}},{"type":"way","id":389912371,"timestamp":"2016-01-06T16:51:45Z","version":1,"changeset":36407948,"user":"Spectrokid","uid":19775,"nodes":[3930713440,3930713451,3930713447,3930713437,3930713440],"tags":{"amenity":"parking"}},{"type":"way","id":389912372,"timestamp":"2019-11-16T13:17:09Z","version":4,"changeset":77164376,"user":"Hopperpop","uid":3664604,"nodes":[3930713454,3930713442,3930713435,3930713429,5826811614,3930713426,3930713430,6982605752,6982605754,3930713438,6982605769,6982605766,6982605767,6982605762,6982605764,3930713446,3930713454],"tags":{"access":"customers","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":401995684,"timestamp":"2020-10-07T10:52:00Z","version":3,"changeset":92105972,"user":"effem","uid":16437,"nodes":[4044171963,4044171954,4044171924,4044171936,4044171941,4044171963],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":500067820,"timestamp":"2017-06-13T07:49:12Z","version":1,"changeset":49491219,"user":"Jakka","uid":2403313,"nodes":[4912203166,4912203165,4912203164,4912203163,4912203162,4912203161,4912203160,4912203166],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067821,"timestamp":"2017-06-13T07:49:12Z","version":1,"changeset":49491219,"user":"Jakka","uid":2403313,"nodes":[4912203170,4912203169,4912203168,4912203167,4912203170],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067822,"timestamp":"2017-06-13T07:49:12Z","version":1,"changeset":49491219,"user":"Jakka","uid":2403313,"nodes":[4912203174,4912203173,4912203172,4912203171,4912203174],"tags":{"access":"yes","amenity":"parking","parking":"surface"}},{"type":"way","id":500067823,"timestamp":"2017-06-13T07:49:12Z","version":1,"changeset":49491219,"user":"Jakka","uid":2403313,"nodes":[4912203179,4912203178,4912203177,4912203176,4912203179],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500069613,"timestamp":"2018-10-11T09:30:48Z","version":2,"changeset":63409550,"user":"Jakka","uid":2403313,"nodes":[4912214695,4912214685,4912214694,4912214693,4912214692,4912214680,4912214695],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071452,"timestamp":"2018-10-11T09:30:48Z","version":2,"changeset":63409550,"user":"Jakka","uid":2403313,"nodes":[4912225068,4912225062,4912225063,4912225053,4912225064,4912214694,4912214685,4912225068],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071455,"timestamp":"2018-10-11T09:30:48Z","version":2,"changeset":63409550,"user":"Jakka","uid":2403313,"nodes":[4912225070,4912214681,4912214686,4912225052,4912225051,4912225067,4912225062,4912225068,4912225070],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":500071458,"timestamp":"2018-10-11T09:30:48Z","version":2,"changeset":63409550,"user":"Jakka","uid":2403313,"nodes":[4912214695,4912214680,4912225069,4912225050,1525460846,4912214681,4912225070,4912214695],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":533276307,"timestamp":"2019-12-13T10:02:12Z","version":2,"changeset":78364882,"user":"skyman81","uid":955688,"nodes":[5173881316,5173881317,5173881318,7054196467,5173881316],"tags":{"amenity":"parking"}},{"type":"way","id":533276308,"timestamp":"2017-10-17T23:36:18Z","version":1,"changeset":53027174,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[5173881320,5173881621,5173881622,5173881623,5173881320],"tags":{"amenity":"parking"}},{"type":"way","id":533276309,"timestamp":"2017-10-17T23:36:18Z","version":1,"changeset":53027174,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[5173881624,5173881625,5173881626,5173881627,5173881624],"tags":{"amenity":"parking"}},{"type":"way","id":579848581,"timestamp":"2020-04-05T07:08:57Z","version":6,"changeset":83089516,"user":"Hopperpop","uid":3664604,"nodes":[4043782112,5825400688,5552466020,6997096756,6997096759,6997096758,5552466018,5552466013,5552466014,6997076567,4043782112],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":584455569,"timestamp":"2019-10-31T19:49:18Z","version":2,"changeset":76465627,"user":"Hopperpop","uid":3664604,"nodes":[5586765933,5586765934,5586765935,5586765936,5586765933],"tags":{"amenity":"parking"}},{"type":"way","id":585977870,"timestamp":"2019-10-11T13:28:22Z","version":2,"changeset":75566739,"user":"Hopperpop","uid":3664604,"nodes":[5587844460,5599314384,5599314385,1659850846,6870850178,5587844462,5587844461,6870863414,5587844460],"tags":{"amenity":"parking"}},{"type":"way","id":587014342,"timestamp":"2018-05-09T19:13:29Z","version":1,"changeset":58829130,"user":"Sille Van Landschoot","uid":4852501,"nodes":[5607796820,5607798725,5607798721,5607798722,5607796819,5607798723,5607796820],"tags":{"access":"permissive","amenity":"parking","park_ride":"no","parking":"surface","surface":"paved"}},{"type":"way","id":590167103,"timestamp":"2018-05-22T12:37:36Z","version":1,"changeset":59179114,"user":"ForstEK","uid":1737608,"nodes":[5635001277,5635001274,5635001275,5635001276,5635001277],"tags":{"amenity":"parking"}},{"type":"way","id":590167113,"timestamp":"2020-07-29T17:45:20Z","version":2,"changeset":88691835,"user":"JanFi","uid":672253,"nodes":[5635001312,5635001306,7767137237,7767137235,7767137236,7767137234,5635001312],"tags":{"amenity":"parking"}},{"type":"way","id":590167134,"timestamp":"2018-05-22T12:37:37Z","version":1,"changeset":59179114,"user":"ForstEK","uid":1737608,"nodes":[5635001374,5635001373,5635001372,5635001371,5635001374],"tags":{"amenity":"parking"}},{"type":"way","id":590167135,"timestamp":"2018-05-22T12:37:37Z","version":1,"changeset":59179114,"user":"ForstEK","uid":1737608,"nodes":[5635001378,5635001377,5635001376,5635001375,5635001378],"tags":{"amenity":"parking"}},{"type":"way","id":590167136,"timestamp":"2018-05-22T12:37:37Z","version":1,"changeset":59179114,"user":"ForstEK","uid":1737608,"nodes":[5635001382,5635001381,5635001380,5635001379,5635001382],"tags":{"amenity":"parking"}},{"type":"way","id":590167137,"timestamp":"2019-07-06T15:58:19Z","version":3,"changeset":71962591,"user":"gjosch","uid":1776978,"nodes":[5635001386,5882873336,5882873337,5882873338,5882873335,6593340582,6593340583,5882873334,6593340584,5635001385,5635001384,5635001383,5635001386],"tags":{"amenity":"parking"}},{"type":"way","id":590167147,"timestamp":"2018-05-22T12:37:38Z","version":1,"changeset":59179114,"user":"ForstEK","uid":1737608,"nodes":[5635001417,5635001414,5635001415,5635001416,5635001417],"tags":{"amenity":"parking"}},{"type":"way","id":601406079,"timestamp":"2018-06-24T22:15:06Z","version":1,"changeset":60131072,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[5716136617,5716136618,5716136619,5716136620,5716136617],"tags":{"amenity":"parking"}},{"type":"way","id":632813009,"timestamp":"2018-10-11T09:24:42Z","version":1,"changeset":63409297,"user":"Jakka","uid":2403313,"nodes":[5974489618,1810326044,1810326087,5974489617,5972179348,5972179347,5972179346,5972179345,5972179344,5972179343,5972179331,5974489616,5974489615,5974489614,5974489618],"tags":{"amenity":"parking","name":"Gemeenteplein","parking":"surface"}},{"type":"way","id":668043297,"timestamp":"2019-04-10T18:34:27Z","version":2,"changeset":69093378,"user":"RudolpheDeer","uid":9408828,"nodes":[6255587424,6255587425,6255587426,6255587427,6255587424],"tags":{"access":"private","amenity":"parking"}},{"type":"way","id":670104236,"timestamp":"2019-02-13T00:05:02Z","version":1,"changeset":67147239,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[6275462768,6275462769,6275462770,6275462771,6275462768],"tags":{"amenity":"parking"}},{"type":"way","id":670104238,"timestamp":"2019-02-13T00:05:02Z","version":1,"changeset":67147239,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[6275462772,6275462989,6275462773,6275462774,6275462775,6275462772],"tags":{"amenity":"parking"}},{"type":"way","id":670104239,"timestamp":"2019-02-13T00:05:02Z","version":1,"changeset":67147239,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[6275462776,6275462777,6275462778,6275462779,6275462776],"tags":{"amenity":"parking"}},{"type":"way","id":670104241,"timestamp":"2019-02-13T00:05:02Z","version":1,"changeset":67147239,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[6275462780,6275462781,6275462782,6275462783,6275462780],"tags":{"amenity":"parking"}},{"type":"way","id":670104242,"timestamp":"2019-02-13T00:05:02Z","version":1,"changeset":67147239,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[6275462784,6275462985,6275462988,6275462986,6275462987,6275462784],"tags":{"amenity":"parking"}},{"type":"way","id":671840055,"timestamp":"2019-02-20T15:15:45Z","version":1,"changeset":67395313,"user":"Tim Couwelier","uid":7246683,"nodes":[6291339827,6291339828,6291339816,6291339815,6291339822,6291339821,6291339829,6291339830,6291339827],"tags":{"amenity":"parking"}},{"type":"way","id":695825223,"timestamp":"2019-06-08T15:19:24Z","version":1,"changeset":71053301,"user":"Hopperpop","uid":3664604,"nodes":[1519476746,6533893620,6533893621,6533893622,1519476797,1519476620,1519476746],"tags":{"access":"yes","amenity":"parking"}},{"type":"way","id":695825224,"timestamp":"2019-06-08T15:19:24Z","version":1,"changeset":71053301,"user":"Hopperpop","uid":3664604,"nodes":[1519476744,6533893624,6533893625,6533893626,1519476698,1519476635,1519476744],"tags":{"access":"yes","amenity":"parking"}},{"type":"way","id":696040917,"timestamp":"2019-06-09T23:24:59Z","version":2,"changeset":71082992,"user":"Hopperpop","uid":3664604,"nodes":[6536026850,6536026851,6536026852,6536026853,6536026850],"tags":{"amenity":"parking","name":"Kasteel Tudor"}},{"type":"way","id":696043218,"timestamp":"2019-06-09T23:24:59Z","version":1,"changeset":71082992,"user":"Hopperpop","uid":3664604,"nodes":[6536038234,6536038235,6536038236,6536038237,6536038234],"tags":{"access":"customers","amenity":"parking"}},{"type":"way","id":700675991,"timestamp":"2020-12-18T10:48:20Z","version":2,"changeset":96062619,"user":"Hopperpop","uid":3664604,"nodes":[6579962064,6579962065,6579962066,6579962068,6579962067,6579962064],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":705278707,"timestamp":"2020-09-30T20:36:55Z","version":2,"changeset":91787895,"user":"L'imaginaire","uid":654234,"nodes":[6625037999,6625038000,6625038001,6625038002,6625038003,6004375826,6625038005,6625038006,6625037999],"tags":{"amenity":"parking"}},{"type":"way","id":719482833,"timestamp":"2019-08-28T21:18:49Z","version":1,"changeset":73857967,"user":"Hopperpop","uid":3664604,"nodes":[6754312544,6754312543,6754312542,6754312541,6754312544],"tags":{"access":"yes","amenity":"parking","capacity":"5"}},{"type":"way","id":719482834,"timestamp":"2019-08-28T21:18:49Z","version":1,"changeset":73857967,"user":"Hopperpop","uid":3664604,"nodes":[6754312565,6754312564,6754312563,6754312562,6754312565],"tags":{"access":"yes","amenity":"parking","capacity":"12"}},{"type":"way","id":737054013,"timestamp":"2019-10-20T15:39:32Z","version":1,"changeset":75957554,"user":"Hopperpop","uid":3664604,"nodes":[5826811496,5826811497,5826811494,5826811495,5826811496],"tags":{"amenity":"parking"}},{"type":"way","id":737054014,"timestamp":"2019-10-20T15:39:32Z","version":1,"changeset":75957554,"user":"Hopperpop","uid":3664604,"nodes":[5826810676,5826810673,5826810674,5826810675,5826810676],"tags":{"amenity":"parking"}},{"type":"way","id":737054015,"timestamp":"2019-10-20T15:39:32Z","version":1,"changeset":75957554,"user":"Hopperpop","uid":3664604,"nodes":[5826810681,5826810678,5826810679,5826810680,5826810681],"tags":{"amenity":"parking"}},{"type":"way","id":737093410,"timestamp":"2021-08-14T21:52:07Z","version":2,"changeset":109683899,"user":"effem","uid":16437,"nodes":[5826811559,5826811536,5826811535,5826811561,5826811560,5826811559],"tags":{"access":"yes","amenity":"parking","capacity":"4","fee":"no","parking":"surface"}},{"type":"way","id":737093411,"timestamp":"2021-08-14T21:52:03Z","version":2,"changeset":109683899,"user":"effem","uid":16437,"nodes":[5826811551,5826811547,5826811548,5826811549,5826811550,5826811551],"tags":{"access":"yes","amenity":"parking","capacity":"4","fee":"no","parking":"surface"}},{"type":"way","id":739652949,"timestamp":"2019-10-28T20:18:16Z","version":1,"changeset":76314556,"user":"Hopperpop","uid":3664604,"nodes":[6925536542,6925536541,6925536540,6925536539,6925536542],"tags":{"amenity":"parking","capacity":"6","parking":"surface"}},{"type":"way","id":741675236,"timestamp":"2020-12-17T22:07:55Z","version":4,"changeset":96029554,"user":"Hopperpop","uid":3664604,"nodes":[6943148207,6943148206,6943148205,6943148204,1637742821,6943148203,6943148202,6943148201,6943148200,6943148199,6943148198,6943148197,6943148207],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":742295526,"timestamp":"2019-11-05T19:27:15Z","version":1,"changeset":76664875,"user":"Hopperpop","uid":3664604,"nodes":[6949357909,6949357908,6949357907,6949357906,6949357909],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295527,"timestamp":"2019-11-05T19:27:15Z","version":1,"changeset":76664875,"user":"Hopperpop","uid":3664604,"nodes":[6949357913,6949357912,6949357911,6949357910,6949357913],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295528,"timestamp":"2019-11-05T19:27:15Z","version":1,"changeset":76664875,"user":"Hopperpop","uid":3664604,"nodes":[6949357917,6949357916,6949357915,6949357914,6949357917],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":742295529,"timestamp":"2019-11-05T19:27:15Z","version":1,"changeset":76664875,"user":"Hopperpop","uid":3664604,"nodes":[6949357921,6949357920,6949357919,6949357918,6949357921],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":746170866,"timestamp":"2019-11-16T15:27:02Z","version":1,"changeset":77167609,"user":"Hopperpop","uid":3664604,"nodes":[6982906547,6982906546,6982906545,6982906544,6982906543,6982906542,6982906547],"tags":{"access":"customers","amenity":"parking"}},{"type":"way","id":747880657,"timestamp":"2021-09-19T12:40:45Z","version":2,"changeset":111407274,"user":"Hopperpop","uid":3664604,"nodes":[3325315397,6997076566,6997076565,6997076563,6997076562,6997076564,3325315395,3325315397],"tags":{"access":"customers","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":763977465,"timestamp":"2020-12-17T21:59:46Z","version":2,"changeset":96029554,"user":"Hopperpop","uid":3664604,"nodes":[7137343680,7137343681,7137343682,7137343683,7137343680],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":763977466,"timestamp":"2020-12-17T21:59:40Z","version":2,"changeset":96029554,"user":"Hopperpop","uid":3664604,"nodes":[7137343684,7137383185,7137383186,7137383187,7137343684],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821090,"timestamp":"2020-05-14T17:37:10Z","version":1,"changeset":85215781,"user":"Hopperpop","uid":3664604,"nodes":[7519058290,7519058289,7519058288,7519058287,7519058290],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821091,"timestamp":"2020-05-14T17:37:10Z","version":1,"changeset":85215781,"user":"Hopperpop","uid":3664604,"nodes":[7519058294,7519058293,7519058292,7519058291,7519058294],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821092,"timestamp":"2020-05-14T17:37:10Z","version":1,"changeset":85215781,"user":"Hopperpop","uid":3664604,"nodes":[7519058298,7519058297,7519058296,7519058295,7519058298],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":803821093,"timestamp":"2020-05-14T17:37:10Z","version":1,"changeset":85215781,"user":"Hopperpop","uid":3664604,"nodes":[7519058302,7519058301,7519058300,7519058299,7519058302],"tags":{"access":"private","amenity":"parking","capacity":"6","parking":"surface"}},{"type":"way","id":804963962,"timestamp":"2020-05-17T17:09:31Z","version":1,"changeset":85342199,"user":"Hopperpop","uid":3664604,"nodes":[7529417225,7529417226,7529417227,7529417228,7529417229,7529417230,7529417232,7529417225],"tags":{"access":"customers","amenity":"parking","fee":"no","operator":"’t Kiekekot","parking":"surface","surface":"unpaved"}},{"type":"way","id":806875503,"timestamp":"2020-05-21T15:48:37Z","version":1,"changeset":85563652,"user":"Hopperpop","uid":3664604,"nodes":[4042671969,7545532512,7545532514,7545532513,3359977305,4042671969],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":806963547,"timestamp":"2020-05-21T21:16:34Z","version":1,"changeset":85574048,"user":"Hopperpop","uid":3664604,"nodes":[7546193222,7546193221,7546193220,7546193219,7546193222],"tags":{"access":"private","amenity":"parking","parking":"surface"}},{"type":"way","id":865357121,"timestamp":"2020-10-30T14:45:23Z","version":1,"changeset":93296961,"user":"L'imaginaire","uid":654234,"nodes":[8065883228,8065883227,8065883226,8065883225,8065883228],"tags":{"amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":865357122,"timestamp":"2020-10-30T14:45:23Z","version":1,"changeset":93296961,"user":"L'imaginaire","uid":654234,"nodes":[8065883233,8065883230,8065883229,8065883232,8065883233],"tags":{"amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":879281221,"timestamp":"2020-11-30T14:18:18Z","version":1,"changeset":95050429,"user":"M!dgard","uid":763799,"nodes":[8179735269,8179735268,8179735267,8179735266,8179735265,8179735264,8179735224,8179735269],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"way","id":881770201,"timestamp":"2020-12-07T00:46:56Z","version":1,"changeset":95385582,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[8200275847,8200275848,8200275844,8200275853,8200275849,8200275847],"tags":{"amenity":"parking","parking":"surface","surface":"grass"}},{"type":"way","id":978360549,"timestamp":"2021-09-01T08:53:08Z","version":1,"changeset":110553113,"user":"JanFi","uid":672253,"nodes":[5761770202,5761770204,9052878229,9052878228,5761770202],"tags":{"amenity":"parking"}},{"type":"way","id":1009692722,"timestamp":"2021-12-06T18:34:20Z","version":1,"changeset":114629990,"user":"L'imaginaire","uid":654234,"nodes":[9316118540,9316118536,9316118531,9316118535,9316118534,9316118533,9316118530,9316118532,3311835478,9316118540],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"relation","id":8188853,"timestamp":"2020-07-05T12:38:48Z","version":13,"changeset":87554177,"user":"Pieter Vander Vennet","uid":3818858,"members":[{"type":"way","ref":577572397,"role":"outer"},{"type":"way","ref":577572399,"role":"outer"}],"tags":{"access":"guided","curator":"Luc Maene;Geert De Clercq","email":"lucmaene@hotmail.com;geert.de.clercq1@pandora.be","landuse":"meadow","leisure":"nature_reserve","name":"De Wulgenbroeken","natural":"wetland","operator":"Natuurpunt Brugge","type":"multipolygon","website":"https://natuurpuntbrugge.be/wulgenbroeken/","wetland":"wet_meadow","wikidata":"Q60061498","wikipedia":"nl:Wulgenbroeken"}},{"type":"relation","id":11163488,"timestamp":"2021-10-04T14:09:47Z","version":15,"changeset":112079863,"user":"DieterWesttoer","uid":13062237,"members":[{"type":"way","ref":810604915,"role":"outer"},{"type":"way","ref":989393316,"role":"outer"},{"type":"way","ref":389026405,"role":"inner"},{"type":"way","ref":810607458,"role":"outer"}],"tags":{"access":"yes","curator":"Kris Lesage","description":"Wat Doeveren zo uniek maakt, zijn zijn kleine heidegebiedjes met soorten die erg verschillen van de Kempense heide. Doeveren en omstreken was vroeger één groot heidegebied, maar bestaat nu grotendeels uit bossen.","dog":"leashed","email":"doeveren@natuurpuntzedelgem.be","image":"https://i.imgur.com/NEAsQZG.jpg","image:0":"https://i.imgur.com/Dq71hyQ.jpg","image:1":"https://i.imgur.com/mAIiT4f.jpg","image:2":"https://i.imgur.com/dELZU97.jpg","image:3":"https://i.imgur.com/Bso57JC.jpg","image:4":"https://i.imgur.com/9DtcfXo.jpg","image:5":"https://i.imgur.com/0R6eBfk.jpg","image:6":"https://i.imgur.com/b0JpvbR.jpg","leisure":"nature_reserve","name":"Doeveren","operator":"Natuurpunt Zedelgem","phone":"+32 486 25 25 30","type":"multipolygon","website":"https://www.natuurpuntzedelgem.be/gebieden/doeveren/","wikidata":"Q56395754","wikipedia":"nl:Doeveren (natuurgebied)"}},{"type":"relation","id":11790117,"timestamp":"2020-10-24T19:11:01Z","version":1,"changeset":92997462,"user":"Pieter Vander Vennet","uid":3818858,"members":[{"type":"way","ref":863373849,"role":"outer"},{"type":"way","ref":777280458,"role":"outer"}],"tags":{"access":"no","description:0":"In gebruik als waterbuffering","leisure":"nature_reserve","name":"Kerkebeek","operator":"Natuurpunt Brugge","type":"multipolygon"}},{"type":"node","id":1038638696,"lat":51.1197075,"lon":3.1827007},{"type":"node","id":7578975029,"lat":51.1199041,"lon":3.1828945},{"type":"node","id":7578975030,"lat":51.1201514,"lon":3.1831942},{"type":"node","id":1038638743,"lat":51.1202445,"lon":3.1894878},{"type":"node","id":7578975002,"lat":51.1202598,"lon":3.1897155},{"type":"node","id":7578975007,"lat":51.1199771,"lon":3.1863015},{"type":"node","id":7578975008,"lat":51.1199523,"lon":3.1863031},{"type":"node","id":7578975009,"lat":51.1199279,"lon":3.1859524},{"type":"node","id":1168728109,"lat":51.1275839,"lon":3.1765505},{"type":"node","id":1168728158,"lat":51.1278835,"lon":3.1763986},{"type":"node","id":1168728159,"lat":51.1276298,"lon":3.1767808},{"type":"node","id":5637669355,"lat":51.1276039,"lon":3.1766509},{"type":"node","id":1038638723,"lat":51.1272818,"lon":3.184601},{"type":"node","id":7554434438,"lat":51.1225298,"lon":3.1847624},{"type":"node","id":7578865273,"lat":51.122084,"lon":3.1846859},{"type":"node","id":7578975032,"lat":51.1215762,"lon":3.1842866},{"type":"node","id":1168727876,"lat":51.1290569,"lon":3.1766033},{"type":"node","id":1168728208,"lat":51.1289763,"lon":3.1767696},{"type":"node","id":1168728288,"lat":51.1291195,"lon":3.1766799},{"type":"node","id":1168728325,"lat":51.1279295,"lon":3.1766288},{"type":"node","id":1168728412,"lat":51.1290389,"lon":3.1768463},{"type":"node","id":1038638712,"lat":51.1282367,"lon":3.1840296},{"type":"node","id":1168727824,"lat":51.1312478,"lon":3.182233},{"type":"node","id":3922375256,"lat":51.1301155,"lon":3.1848042},{"type":"node","id":3922380071,"lat":51.1304048,"lon":3.1838954},{"type":"node","id":3922380081,"lat":51.1305694,"lon":3.1845093},{"type":"node","id":3922380086,"lat":51.1306445,"lon":3.1848152},{"type":"node","id":3922380092,"lat":51.1307378,"lon":3.1849591},{"type":"node","id":7577430793,"lat":51.1289492,"lon":3.1836032},{"type":"node","id":7578975024,"lat":51.1299598,"lon":3.1841704},{"type":"node","id":7578975028,"lat":51.1322547,"lon":3.1833542},{"type":"node","id":7578975049,"lat":51.1313772,"lon":3.1838431},{"type":"node","id":9167054153,"lat":51.1310258,"lon":3.1823668},{"type":"node","id":9167054154,"lat":51.13145,"lon":3.1841492},{"type":"node","id":9167054156,"lat":51.1316731,"lon":3.1850331},{"type":"node","id":9274761589,"lat":51.1297088,"lon":3.1831312},{"type":"node","id":9274761596,"lat":51.1296735,"lon":3.1831518},{"type":"node","id":929120698,"lat":51.1276376,"lon":3.1903134},{"type":"node","id":1038638592,"lat":51.1264889,"lon":3.19027},{"type":"node","id":1038638661,"lat":51.1258582,"lon":3.1854626},{"type":"node","id":1038638721,"lat":51.125636,"lon":3.1855855},{"type":"node","id":1038638753,"lat":51.123845,"lon":3.1880289},{"type":"node","id":3921878998,"lat":51.1255719,"lon":3.1902463},{"type":"node","id":3921879004,"lat":51.1275463,"lon":3.188843},{"type":"node","id":3921879011,"lat":51.1271626,"lon":3.1872368},{"type":"node","id":3921879019,"lat":51.1277666,"lon":3.1868505},{"type":"node","id":7554434436,"lat":51.1252645,"lon":3.1852941},{"type":"node","id":7578865274,"lat":51.1230564,"lon":3.187978},{"type":"node","id":7578865275,"lat":51.1226417,"lon":3.188075},{"type":"node","id":7578904489,"lat":51.1247504,"lon":3.1900249},{"type":"node","id":7578974988,"lat":51.1223221,"lon":3.1906513},{"type":"node","id":7578974989,"lat":51.1224255,"lon":3.1905646},{"type":"node","id":7578974990,"lat":51.1224672,"lon":3.1905195},{"type":"node","id":7578974991,"lat":51.1228709,"lon":3.1901867},{"type":"node","id":7578974992,"lat":51.1229568,"lon":3.1901459},{"type":"node","id":7578974995,"lat":51.123814,"lon":3.1899138},{"type":"node","id":7578974996,"lat":51.1239199,"lon":3.189925},{"type":"node","id":7578974997,"lat":51.1244111,"lon":3.1899686},{"type":"node","id":7578974998,"lat":51.1248503,"lon":3.190215},{"type":"node","id":7578974999,"lat":51.1247917,"lon":3.1900516},{"type":"node","id":7578975000,"lat":51.1248293,"lon":3.1900978},{"type":"node","id":7578975001,"lat":51.1248444,"lon":3.1901483},{"type":"node","id":7578975035,"lat":51.1250798,"lon":3.1851611},{"type":"node","id":7578975045,"lat":51.1278881,"lon":3.1882941},{"type":"node","id":9199177059,"lat":51.1256647,"lon":3.1855696},{"type":"node","id":7578987409,"lat":51.1232707,"lon":3.1920382},{"type":"node","id":7578987413,"lat":51.1260416,"lon":3.1973652},{"type":"node","id":7578987414,"lat":51.1263443,"lon":3.1973775},{"type":"node","id":7578987416,"lat":51.1278708,"lon":3.1974134},{"type":"node","id":7578987417,"lat":51.126749,"lon":3.197258},{"type":"node","id":1675648152,"lat":51.1281877,"lon":3.1903323},{"type":"node","id":2732486274,"lat":51.1302553,"lon":3.1886305},{"type":"node","id":3921879018,"lat":51.1280809,"lon":3.188102},{"type":"node","id":3922380061,"lat":51.1301929,"lon":3.1895402},{"type":"node","id":3922380083,"lat":51.1305788,"lon":3.1884337},{"type":"node","id":3922380095,"lat":51.130784,"lon":3.1852632},{"type":"node","id":7578865281,"lat":51.1283938,"lon":3.1903716},{"type":"node","id":7578865283,"lat":51.1288414,"lon":3.1904911},{"type":"node","id":7578960079,"lat":51.1303025,"lon":3.1855669},{"type":"node","id":7578975012,"lat":51.1294792,"lon":3.1906231},{"type":"node","id":7578975015,"lat":51.1297374,"lon":3.1907018},{"type":"node","id":7578975016,"lat":51.1300451,"lon":3.1907679},{"type":"node","id":7578975018,"lat":51.1305785,"lon":3.186668},{"type":"node","id":7578975019,"lat":51.130956,"lon":3.1881852},{"type":"node","id":7578975021,"lat":51.1303082,"lon":3.1855908},{"type":"node","id":7578975026,"lat":51.1310167,"lon":3.1861981},{"type":"node","id":7578975027,"lat":51.1318642,"lon":3.1857905},{"type":"node","id":7578975040,"lat":51.1280348,"lon":3.1889503},{"type":"node","id":7578975044,"lat":51.1279308,"lon":3.1875031},{"type":"node","id":7578975046,"lat":51.1279329,"lon":3.1881992},{"type":"node","id":9167054157,"lat":51.1308023,"lon":3.1852517},{"type":"node","id":9274761591,"lat":51.1310994,"lon":3.1862668},{"type":"node","id":9274761592,"lat":51.1310643,"lon":3.1862655},{"type":"node","id":9274761593,"lat":51.1310386,"lon":3.1862393},{"type":"node","id":7578987418,"lat":51.128003,"lon":3.1959031},{"type":"node","id":7578987419,"lat":51.1282167,"lon":3.193723},{"type":"node","id":5745833239,"lat":51.1258572,"lon":3.1996713},{"type":"node","id":5745833240,"lat":51.1257519,"lon":3.1995939},{"type":"node","id":5745833241,"lat":51.1253365,"lon":3.1991234},{"type":"node","id":7578987410,"lat":51.1243814,"lon":3.1988516},{"type":"node","id":7578987411,"lat":51.1243992,"lon":3.1989686},{"type":"node","id":7578987412,"lat":51.1259883,"lon":3.1997074},{"type":"node","id":7578987415,"lat":51.1260745,"lon":3.1997142},{"type":"node","id":1590642829,"lat":51.1333867,"lon":3.2308055},{"type":"node","id":1590642832,"lat":51.1334371,"lon":3.2308262},{"type":"node","id":1590642849,"lat":51.1336392,"lon":3.2305316},{"type":"node","id":1590642858,"lat":51.133659,"lon":3.2303991},{"type":"node","id":1590642859,"lat":51.1336899,"lon":3.2305508},{"type":"node","id":1590642860,"lat":51.1337096,"lon":3.2304183},{"type":"node","id":1590642828,"lat":51.1333653,"lon":3.2309374},{"type":"node","id":1590642830,"lat":51.1334157,"lon":3.2309581},{"type":"node","id":3311835478,"lat":51.133195,"lon":3.2334351},{"type":"node","id":9316118530,"lat":51.1331607,"lon":3.2333604},{"type":"node","id":9316118531,"lat":51.1330927,"lon":3.2334241},{"type":"node","id":9316118532,"lat":51.1331767,"lon":3.233347},{"type":"node","id":9316118533,"lat":51.133147,"lon":3.2333808},{"type":"node","id":9316118534,"lat":51.1331302,"lon":3.2334006},{"type":"node","id":9316118535,"lat":51.1331127,"lon":3.2334144},{"type":"node","id":9316118536,"lat":51.1330784,"lon":3.2334281},{"type":"node","id":9316118540,"lat":51.1330946,"lon":3.2335103},{"type":"node","id":6579962064,"lat":51.1367061,"lon":3.1640546},{"type":"node","id":6579962065,"lat":51.1361156,"lon":3.1646435},{"type":"node","id":6579962066,"lat":51.1357413,"lon":3.1637334},{"type":"node","id":6579962067,"lat":51.1359511,"lon":3.1637408},{"type":"node","id":6579962068,"lat":51.1359389,"lon":3.1638093},{"type":"node","id":7546193219,"lat":51.1456739,"lon":3.1637073},{"type":"node","id":7546193220,"lat":51.1455706,"lon":3.1643444},{"type":"node","id":7546193221,"lat":51.1456623,"lon":3.1643821},{"type":"node","id":7546193222,"lat":51.1457656,"lon":3.1637451},{"type":"node","id":3359977305,"lat":51.1464982,"lon":3.1689911},{"type":"node","id":4042671969,"lat":51.1461398,"lon":3.169233},{"type":"node","id":7545532512,"lat":51.1463103,"lon":3.169498},{"type":"node","id":7545532513,"lat":51.1466008,"lon":3.1695349},{"type":"node","id":7545532514,"lat":51.1464331,"lon":3.16962},{"type":"node","id":8200275848,"lat":51.1409105,"lon":3.1800288},{"type":"node","id":8200275844,"lat":51.1416967,"lon":3.1797728},{"type":"node","id":8200275847,"lat":51.1411211,"lon":3.1805153},{"type":"node","id":8200275849,"lat":51.1417397,"lon":3.1802115},{"type":"node","id":8200275853,"lat":51.1417351,"lon":3.1801651},{"type":"node","id":111759500,"lat":51.1483444,"lon":3.1886487},{"type":"node","id":1169056712,"lat":51.1482974,"lon":3.1884805},{"type":"node","id":3325315226,"lat":51.147926,"lon":3.1887015},{"type":"node","id":3325315230,"lat":51.1479856,"lon":3.1889124},{"type":"node","id":3325315232,"lat":51.1480454,"lon":3.1891027},{"type":"node","id":3325315243,"lat":51.1483285,"lon":3.1885919},{"type":"node","id":3325315247,"lat":51.1484007,"lon":3.1888131},{"type":"node","id":5826810673,"lat":51.1512507,"lon":3.1914885},{"type":"node","id":5826810674,"lat":51.15126,"lon":3.1914533},{"type":"node","id":5826810675,"lat":51.1510883,"lon":3.1912776},{"type":"node","id":5826810676,"lat":51.151057,"lon":3.1912906},{"type":"node","id":5826810678,"lat":51.1509897,"lon":3.1911759},{"type":"node","id":5826810679,"lat":51.1510025,"lon":3.1911334},{"type":"node","id":5826810680,"lat":51.1509352,"lon":3.1908874},{"type":"node","id":5826810681,"lat":51.1509074,"lon":3.1908689},{"type":"node","id":5826811494,"lat":51.1512125,"lon":3.1911315},{"type":"node","id":5826811495,"lat":51.1512055,"lon":3.191187},{"type":"node","id":5826811496,"lat":51.151296,"lon":3.1914182},{"type":"node","id":5826811497,"lat":51.1513261,"lon":3.1914182},{"type":"node","id":5826811535,"lat":51.1517839,"lon":3.1959375},{"type":"node","id":5826811536,"lat":51.1518581,"lon":3.1959708},{"type":"node","id":5826811547,"lat":51.1515913,"lon":3.196115},{"type":"node","id":5826811548,"lat":51.1516307,"lon":3.1961465},{"type":"node","id":5826811549,"lat":51.1516435,"lon":3.1961076},{"type":"node","id":5826811550,"lat":51.1516087,"lon":3.1959541},{"type":"node","id":5826811551,"lat":51.1515437,"lon":3.1959024},{"type":"node","id":5826811559,"lat":51.1517896,"lon":3.1957717},{"type":"node","id":5826811560,"lat":51.1517417,"lon":3.1957528},{"type":"node","id":5826811561,"lat":51.1517365,"lon":3.1957872},{"type":"node","id":3325315395,"lat":51.1544187,"lon":3.1856685},{"type":"node","id":3325315397,"lat":51.1545358,"lon":3.1860117},{"type":"node","id":3930713426,"lat":51.1571474,"lon":3.1889324},{"type":"node","id":3930713429,"lat":51.1573669,"lon":3.1883265},{"type":"node","id":3930713430,"lat":51.1573202,"lon":3.1890776},{"type":"node","id":3930713435,"lat":51.1574561,"lon":3.1883916},{"type":"node","id":3930713437,"lat":51.1574971,"lon":3.1893349},{"type":"node","id":3930713438,"lat":51.1574907,"lon":3.1884223},{"type":"node","id":3930713440,"lat":51.1575693,"lon":3.1890951},{"type":"node","id":3930713442,"lat":51.1575932,"lon":3.1879961},{"type":"node","id":3930713446,"lat":51.1577118,"lon":3.1885611},{"type":"node","id":3930713447,"lat":51.157698,"lon":3.1894886},{"type":"node","id":3930713451,"lat":51.1577702,"lon":3.1892488},{"type":"node","id":3930713454,"lat":51.1577969,"lon":3.1882567},{"type":"node","id":4043782112,"lat":51.1549447,"lon":3.1864571},{"type":"node","id":5552466013,"lat":51.1545783,"lon":3.1874672},{"type":"node","id":5552466014,"lat":51.1543143,"lon":3.1873045},{"type":"node","id":5552466018,"lat":51.1545999,"lon":3.1873846},{"type":"node","id":5552466020,"lat":51.1548079,"lon":3.1868846},{"type":"node","id":5825400688,"lat":51.1549303,"lon":3.1867969},{"type":"node","id":5826811614,"lat":51.1572659,"lon":3.1885288},{"type":"node","id":6949357906,"lat":51.1572911,"lon":3.1915323},{"type":"node","id":6949357909,"lat":51.1573109,"lon":3.1914572},{"type":"node","id":6949357910,"lat":51.1574373,"lon":3.1913744},{"type":"node","id":6949357911,"lat":51.1574179,"lon":3.1914502},{"type":"node","id":6949357912,"lat":51.1575109,"lon":3.1915112},{"type":"node","id":6949357913,"lat":51.1575304,"lon":3.1914354},{"type":"node","id":6949357914,"lat":51.1574638,"lon":3.1912712},{"type":"node","id":6949357915,"lat":51.1574444,"lon":3.1913473},{"type":"node","id":6949357916,"lat":51.1575599,"lon":3.191423},{"type":"node","id":6949357917,"lat":51.1575803,"lon":3.1913473},{"type":"node","id":6949357918,"lat":51.157501,"lon":3.1911303},{"type":"node","id":6949357919,"lat":51.1574808,"lon":3.1912058},{"type":"node","id":6949357920,"lat":51.1577374,"lon":3.1913724},{"type":"node","id":6949357921,"lat":51.1577563,"lon":3.1912987},{"type":"node","id":6982605752,"lat":51.1574664,"lon":3.1885975},{"type":"node","id":6982605754,"lat":51.1574428,"lon":3.1885793},{"type":"node","id":6982605762,"lat":51.1576066,"lon":3.1884793},{"type":"node","id":6982605764,"lat":51.1576853,"lon":3.18854},{"type":"node","id":6982605766,"lat":51.1575125,"lon":3.1884502},{"type":"node","id":6982605767,"lat":51.1575959,"lon":3.1885145},{"type":"node","id":6982605769,"lat":51.1575152,"lon":3.1884412},{"type":"node","id":6982906542,"lat":51.1591649,"lon":3.1904238},{"type":"node","id":6982906543,"lat":51.1592937,"lon":3.1905221},{"type":"node","id":6982906544,"lat":51.1593438,"lon":3.1903659},{"type":"node","id":6982906545,"lat":51.1593158,"lon":3.1903453},{"type":"node","id":6982906546,"lat":51.1593351,"lon":3.1902852},{"type":"node","id":6982906547,"lat":51.1592385,"lon":3.1902052},{"type":"node","id":6997076562,"lat":51.1546633,"lon":3.1858333},{"type":"node","id":6997076563,"lat":51.1546293,"lon":3.1858642},{"type":"node","id":6997076564,"lat":51.154594,"lon":3.1856715},{"type":"node","id":6997076565,"lat":51.1546727,"lon":3.1859889},{"type":"node","id":6997076566,"lat":51.1545545,"lon":3.1860687},{"type":"node","id":6997076567,"lat":51.1543063,"lon":3.1869337},{"type":"node","id":6997096756,"lat":51.1547387,"lon":3.1868376},{"type":"node","id":6997096758,"lat":51.1546899,"lon":3.1870707},{"type":"node","id":6997096759,"lat":51.1546751,"lon":3.1870601},{"type":"node","id":7137343680,"lat":51.1558646,"lon":3.1876808},{"type":"node","id":7137343681,"lat":51.1558466,"lon":3.1877456},{"type":"node","id":7137343682,"lat":51.1555824,"lon":3.187559},{"type":"node","id":7137343683,"lat":51.1556004,"lon":3.1874941},{"type":"node","id":7137343684,"lat":51.1555549,"lon":3.1874612},{"type":"node","id":7137383185,"lat":51.1555369,"lon":3.1875268},{"type":"node","id":7137383186,"lat":51.1553925,"lon":3.1874261},{"type":"node","id":7137383187,"lat":51.1554105,"lon":3.1873604},{"type":"node","id":7519058287,"lat":51.1555296,"lon":3.1858152},{"type":"node","id":7519058288,"lat":51.1555027,"lon":3.1858752},{"type":"node","id":7519058289,"lat":51.1556329,"lon":3.1860234},{"type":"node","id":7519058290,"lat":51.1556597,"lon":3.1859634},{"type":"node","id":7519058291,"lat":51.1557039,"lon":3.1860155},{"type":"node","id":7519058292,"lat":51.1556789,"lon":3.1860736},{"type":"node","id":7519058293,"lat":51.1558105,"lon":3.1862177},{"type":"node","id":7519058294,"lat":51.1558355,"lon":3.1861597},{"type":"node","id":7519058295,"lat":51.1557209,"lon":3.185828},{"type":"node","id":7519058296,"lat":51.1556932,"lon":3.1858902},{"type":"node","id":7519058297,"lat":51.1558686,"lon":3.1860888},{"type":"node","id":7519058298,"lat":51.1558963,"lon":3.1860265},{"type":"node","id":7519058299,"lat":51.1555421,"lon":3.1856365},{"type":"node","id":7519058300,"lat":51.1555168,"lon":3.1856923},{"type":"node","id":7519058301,"lat":51.1556479,"lon":3.1858437},{"type":"node","id":7519058302,"lat":51.1556732,"lon":3.1857879},{"type":"node","id":150996092,"lat":51.1554945,"lon":3.1954639},{"type":"node","id":150996093,"lat":51.155531,"lon":3.1953168},{"type":"node","id":150996094,"lat":51.1554912,"lon":3.1943936},{"type":"node","id":150996095,"lat":51.155456,"lon":3.1942488},{"type":"node","id":150996097,"lat":51.155432,"lon":3.1942095},{"type":"node","id":150996098,"lat":51.155397,"lon":3.1941906},{"type":"node","id":150996099,"lat":51.1552938,"lon":3.1945134},{"type":"node","id":150996100,"lat":51.1552701,"lon":3.1949002},{"type":"node","id":150996101,"lat":51.1553901,"lon":3.1953415},{"type":"node","id":1015567837,"lat":51.1603457,"lon":3.1926746},{"type":"node","id":1015583939,"lat":51.1598982,"lon":3.1934595},{"type":"node","id":1659850846,"lat":51.1562462,"lon":3.1925019},{"type":"node","id":1811699776,"lat":51.1604859,"lon":3.1920734},{"type":"node","id":1811699777,"lat":51.1605587,"lon":3.1917984},{"type":"node","id":1817319289,"lat":51.1599853,"lon":3.1935159},{"type":"node","id":1817319290,"lat":51.1600476,"lon":3.1933385},{"type":"node","id":1817319291,"lat":51.1600745,"lon":3.1934309},{"type":"node","id":1817319292,"lat":51.1602073,"lon":3.1941751},{"type":"node","id":1817319293,"lat":51.160208,"lon":3.1941098},{"type":"node","id":1817319294,"lat":51.1602178,"lon":3.1935425},{"type":"node","id":1817319295,"lat":51.1602385,"lon":3.19297},{"type":"node","id":1817319296,"lat":51.1602972,"lon":3.1940248},{"type":"node","id":1817319297,"lat":51.1603313,"lon":3.193809},{"type":"node","id":1817319298,"lat":51.1603427,"lon":3.1930326},{"type":"node","id":1817319299,"lat":51.1603716,"lon":3.1940192},{"type":"node","id":1817319300,"lat":51.1603777,"lon":3.1946319},{"type":"node","id":1817319301,"lat":51.1604665,"lon":3.193304},{"type":"node","id":1817319302,"lat":51.1604758,"lon":3.1939077},{"type":"node","id":1817319303,"lat":51.1605421,"lon":3.194476},{"type":"node","id":1817319304,"lat":51.1606037,"lon":3.1933895},{"type":"node","id":1817320493,"lat":51.1604248,"lon":3.1927398},{"type":"node","id":1817320494,"lat":51.1604851,"lon":3.192495},{"type":"node","id":2451569392,"lat":51.1598651,"lon":3.1919974},{"type":"node","id":2451574741,"lat":51.1594995,"lon":3.1924908},{"type":"node","id":2451574742,"lat":51.1596217,"lon":3.1923259},{"type":"node","id":2451574743,"lat":51.1597161,"lon":3.1922023},{"type":"node","id":2451574744,"lat":51.1600391,"lon":3.1932123},{"type":"node","id":2451574745,"lat":51.1601904,"lon":3.192947},{"type":"node","id":2451574746,"lat":51.1603862,"lon":3.1925461},{"type":"node","id":2451574747,"lat":51.1604272,"lon":3.19257},{"type":"node","id":2451574748,"lat":51.1604837,"lon":3.1921535},{"type":"node","id":2451574749,"lat":51.1605266,"lon":3.1921769},{"type":"node","id":2451578121,"lat":51.159758,"lon":3.1921448},{"type":"node","id":5587844460,"lat":51.1562664,"lon":3.1919544},{"type":"node","id":5587844461,"lat":51.15612,"lon":3.1920681},{"type":"node","id":5587844462,"lat":51.1561808,"lon":3.1922614},{"type":"node","id":5599314384,"lat":51.1563882,"lon":3.1923707},{"type":"node","id":5599314385,"lat":51.1563587,"lon":3.1924465},{"type":"node","id":6754312541,"lat":51.154716,"lon":3.1952084},{"type":"node","id":6754312542,"lat":51.1547723,"lon":3.1953372},{"type":"node","id":6754312543,"lat":51.1548078,"lon":3.1952983},{"type":"node","id":6754312544,"lat":51.1547519,"lon":3.1951702},{"type":"node","id":6754312550,"lat":51.1555879,"lon":3.1950341},{"type":"node","id":6754312551,"lat":51.1556025,"lon":3.194956},{"type":"node","id":6754312552,"lat":51.1555664,"lon":3.1952408},{"type":"node","id":6754312553,"lat":51.1556188,"lon":3.1951751},{"type":"node","id":6754312554,"lat":51.1554565,"lon":3.195427},{"type":"node","id":6754312555,"lat":51.1552894,"lon":3.1950649},{"type":"node","id":6754312556,"lat":51.1553277,"lon":3.1951991},{"type":"node","id":6754312557,"lat":51.1552774,"lon":3.1947027},{"type":"node","id":6754312558,"lat":51.1553131,"lon":3.1943816},{"type":"node","id":6754312559,"lat":51.1553397,"lon":3.1942577},{"type":"node","id":6754312560,"lat":51.1553697,"lon":3.1942084},{"type":"node","id":6754312562,"lat":51.1548064,"lon":3.1954209},{"type":"node","id":6754312563,"lat":51.1548266,"lon":3.1954893},{"type":"node","id":6754312564,"lat":51.1550417,"lon":3.1953175},{"type":"node","id":6754312565,"lat":51.1550193,"lon":3.1952538},{"type":"node","id":6870850178,"lat":51.1561935,"lon":3.1923019},{"type":"node","id":6870863414,"lat":51.1562489,"lon":3.1919675},{"type":"node","id":6949357907,"lat":51.1575239,"lon":3.1916859},{"type":"node","id":6949357908,"lat":51.1575439,"lon":3.1916101},{"type":"node","id":1448421081,"lat":51.167491,"lon":3.1713256},{"type":"node","id":1448421091,"lat":51.1677042,"lon":3.1714548},{"type":"node","id":1448421093,"lat":51.1677455,"lon":3.1702529},{"type":"node","id":1448421099,"lat":51.1679647,"lon":3.1703977},{"type":"node","id":6536026850,"lat":51.1711159,"lon":3.1669401},{"type":"node","id":6536026851,"lat":51.1712692,"lon":3.167296},{"type":"node","id":6536026852,"lat":51.1711296,"lon":3.1674712},{"type":"node","id":6536026853,"lat":51.1709602,"lon":3.1671189},{"type":"node","id":6536038234,"lat":51.1711913,"lon":3.165543},{"type":"node","id":6536038235,"lat":51.1711301,"lon":3.1656263},{"type":"node","id":6536038236,"lat":51.1712318,"lon":3.1658161},{"type":"node","id":6536038237,"lat":51.171293,"lon":3.1657327},{"type":"node","id":1038583451,"lat":51.1625071,"lon":3.191602},{"type":"node","id":4044171936,"lat":51.1609177,"lon":3.1911926},{"type":"node","id":4044171941,"lat":51.1609801,"lon":3.191229},{"type":"node","id":4044171963,"lat":51.1611962,"lon":3.1913534},{"type":"node","id":903903386,"lat":51.1618084,"lon":3.1932127},{"type":"node","id":903903387,"lat":51.1623536,"lon":3.1936011},{"type":"node","id":903903388,"lat":51.1624429,"lon":3.1931156},{"type":"node","id":903903390,"lat":51.1618641,"lon":3.1930197},{"type":"node","id":903904576,"lat":51.1618211,"lon":3.1931711},{"type":"node","id":1038557012,"lat":51.16155,"lon":3.1931121},{"type":"node","id":1038557014,"lat":51.1613774,"lon":3.1930711},{"type":"node","id":1038557035,"lat":51.1615349,"lon":3.1931712},{"type":"node","id":1038557072,"lat":51.1616138,"lon":3.1927483},{"type":"node","id":1038557075,"lat":51.162286,"lon":3.1935989},{"type":"node","id":1038557078,"lat":51.1616517,"lon":3.1928439},{"type":"node","id":1038557083,"lat":51.1616055,"lon":3.1930249},{"type":"node","id":1038557094,"lat":51.1618993,"lon":3.193299},{"type":"node","id":1038557102,"lat":51.1613235,"lon":3.1927138},{"type":"node","id":1038557104,"lat":51.1615987,"lon":3.1928077},{"type":"node","id":1038557108,"lat":51.1615113,"lon":3.1926816},{"type":"node","id":1038557137,"lat":51.1608873,"lon":3.1924416},{"type":"node","id":1038557143,"lat":51.1622248,"lon":3.193662},{"type":"node","id":1038557191,"lat":51.1608171,"lon":3.1926951},{"type":"node","id":1038557195,"lat":51.162409,"lon":3.1933428},{"type":"node","id":1038557227,"lat":51.1613767,"lon":3.1925113},{"type":"node","id":1038557230,"lat":51.1615281,"lon":3.1926132},{"type":"node","id":1038557233,"lat":51.1619765,"lon":3.1933723},{"type":"node","id":1038575040,"lat":51.1608523,"lon":3.1925679},{"type":"node","id":1038583375,"lat":51.1625113,"lon":3.1926776},{"type":"node","id":1038583398,"lat":51.1624305,"lon":3.1925743},{"type":"node","id":1038583404,"lat":51.1627055,"lon":3.191826},{"type":"node","id":1038583425,"lat":51.1624272,"lon":3.1916637},{"type":"node","id":1038583441,"lat":51.1621479,"lon":3.1921546},{"type":"node","id":1038583446,"lat":51.1622018,"lon":3.1921814},{"type":"node","id":1038583456,"lat":51.162174,"lon":3.1922806},{"type":"node","id":1038583459,"lat":51.162259,"lon":3.1924885},{"type":"node","id":1038583463,"lat":51.162661,"lon":3.1917442},{"type":"node","id":1038583476,"lat":51.1626425,"lon":3.1918448},{"type":"node","id":1038583479,"lat":51.1624196,"lon":3.1926561},{"type":"node","id":1038583483,"lat":51.1625037,"lon":3.1927232},{"type":"node","id":1038583491,"lat":51.1625701,"lon":3.1926387},{"type":"node","id":1038583501,"lat":51.1624928,"lon":3.1917013},{"type":"node","id":1811673418,"lat":51.1618484,"lon":3.1950253},{"type":"node","id":1811673421,"lat":51.1619875,"lon":3.1951427},{"type":"node","id":1811673423,"lat":51.162144,"lon":3.193835},{"type":"node","id":1811673425,"lat":51.1622452,"lon":3.1939149},{"type":"node","id":1811699778,"lat":51.1608187,"lon":3.1922973},{"type":"node","id":1811699779,"lat":51.1608915,"lon":3.1920223},{"type":"node","id":1817320495,"lat":51.1607269,"lon":3.192929},{"type":"node","id":1817320496,"lat":51.1607872,"lon":3.1926841},{"type":"node","id":1817324488,"lat":51.1615575,"lon":3.1921423},{"type":"node","id":1817324489,"lat":51.1612682,"lon":3.1920196},{"type":"node","id":1817324491,"lat":51.1617501,"lon":3.1918468},{"type":"node","id":1817324505,"lat":51.1618272,"lon":3.1921231},{"type":"node","id":1817324509,"lat":51.1618658,"lon":3.191793},{"type":"node","id":1817324513,"lat":51.1619814,"lon":3.1920002},{"type":"node","id":1817324515,"lat":51.161985,"lon":3.191793},{"type":"node","id":3550860268,"lat":51.1617349,"lon":3.1921922},{"type":"node","id":3550860269,"lat":51.1619403,"lon":3.1920686},{"type":"node","id":4044171924,"lat":51.1608199,"lon":3.1916126},{"type":"node","id":4044171954,"lat":51.1610853,"lon":3.191781},{"type":"node","id":4044171957,"lat":51.1611433,"lon":3.1918701},{"type":"node","id":4044171962,"lat":51.1611926,"lon":3.1916807},{"type":"node","id":4044171976,"lat":51.1612826,"lon":3.1919582},{"type":"node","id":4044171997,"lat":51.1613568,"lon":3.1917787},{"type":"node","id":4044172003,"lat":51.1613248,"lon":3.1919027},{"type":"node","id":4044172008,"lat":51.1614345,"lon":3.1919767},{"type":"node","id":6397031888,"lat":51.1615029,"lon":3.1919851},{"type":"node","id":6960473080,"lat":51.1614227,"lon":3.1931004},{"type":"node","id":3335137692,"lat":51.1698302,"lon":3.1965654},{"type":"node","id":3335137795,"lat":51.1698788,"lon":3.1970728},{"type":"node","id":3335137802,"lat":51.1700082,"lon":3.1971734},{"type":"node","id":3335137807,"lat":51.1701139,"lon":3.1965001},{"type":"node","id":3335137812,"lat":51.1703247,"lon":3.197352},{"type":"node","id":3335137823,"lat":51.1703133,"lon":3.1966232},{"type":"node","id":6255587427,"lat":51.1694775,"lon":3.1980047},{"type":"node","id":9163486855,"lat":51.1703854,"lon":3.1970901},{"type":"node","id":9163486856,"lat":51.1702114,"lon":3.1969688},{"type":"node","id":9163493632,"lat":51.1699473,"lon":3.197063},{"type":"node","id":414025563,"lat":51.1768198,"lon":3.1839265},{"type":"node","id":2325437840,"lat":51.1765747,"lon":3.1839745},{"type":"node","id":2325437844,"lat":51.1768286,"lon":3.1825946},{"type":"node","id":8191691970,"lat":51.1767611,"lon":3.1839766},{"type":"node","id":8191691971,"lat":51.1767812,"lon":3.1826167},{"type":"node","id":8191691972,"lat":51.1765804,"lon":3.1826583},{"type":"node","id":8191691973,"lat":51.1766324,"lon":3.182616},{"type":"node","id":5716136617,"lat":51.1773127,"lon":3.1969033},{"type":"node","id":5716136618,"lat":51.1771859,"lon":3.1969078},{"type":"node","id":5716136619,"lat":51.177203,"lon":3.1981369},{"type":"node","id":5716136620,"lat":51.1773298,"lon":3.1981324},{"type":"node","id":6004375826,"lat":51.1786839,"lon":3.1952389},{"type":"node","id":6625037999,"lat":51.1788016,"lon":3.1950645},{"type":"node","id":6625038000,"lat":51.1789916,"lon":3.1966631},{"type":"node","id":6625038001,"lat":51.1789003,"lon":3.1966838},{"type":"node","id":6625038002,"lat":51.1788554,"lon":3.1963547},{"type":"node","id":6625038003,"lat":51.1788165,"lon":3.1963622},{"type":"node","id":6625038005,"lat":51.1785124,"lon":3.1952362},{"type":"node","id":6625038006,"lat":51.1784779,"lon":3.1950618},{"type":"node","id":6925536539,"lat":51.1522475,"lon":3.1985416},{"type":"node","id":6925536540,"lat":51.1522635,"lon":3.1986187},{"type":"node","id":6925536541,"lat":51.1523922,"lon":3.1985517},{"type":"node","id":6925536542,"lat":51.1523766,"lon":3.1984746},{"type":"node","id":1637742821,"lat":51.158524,"lon":3.199021},{"type":"node","id":5586765933,"lat":51.1566667,"lon":3.2008881},{"type":"node","id":5586765934,"lat":51.1564286,"lon":3.2012575},{"type":"node","id":5586765935,"lat":51.1562496,"lon":3.2008579},{"type":"node","id":5586765936,"lat":51.1564982,"lon":3.200522},{"type":"node","id":6943148197,"lat":51.1581916,"lon":3.1989071},{"type":"node","id":6943148198,"lat":51.1582377,"lon":3.1989899},{"type":"node","id":6943148199,"lat":51.1581859,"lon":3.1990728},{"type":"node","id":6943148200,"lat":51.1583257,"lon":3.199295},{"type":"node","id":6943148201,"lat":51.1583563,"lon":3.199246},{"type":"node","id":6943148202,"lat":51.1583988,"lon":3.1993135},{"type":"node","id":6943148203,"lat":51.1585529,"lon":3.1990669},{"type":"node","id":6943148204,"lat":51.1586554,"lon":3.1988109},{"type":"node","id":6943148205,"lat":51.1585617,"lon":3.1986619},{"type":"node","id":6943148206,"lat":51.1586782,"lon":3.1984755},{"type":"node","id":6943148207,"lat":51.1585692,"lon":3.1982996},{"type":"node","id":8065883225,"lat":51.1366029,"lon":3.233506},{"type":"node","id":8065883226,"lat":51.1364627,"lon":3.2335338},{"type":"node","id":8065883227,"lat":51.1363922,"lon":3.2326314},{"type":"node","id":8065883228,"lat":51.1365324,"lon":3.2326036},{"type":"node","id":8065883229,"lat":51.1374344,"lon":3.2333338},{"type":"node","id":8065883230,"lat":51.1373632,"lon":3.2324534},{"type":"node","id":8065883232,"lat":51.1375819,"lon":3.2333035},{"type":"node","id":8065883233,"lat":51.1375107,"lon":3.232423},{"type":"node","id":1795793393,"lat":51.1475704,"lon":3.233832},{"type":"node","id":3346575840,"lat":51.146485,"lon":3.2349284},{"type":"node","id":3346575846,"lat":51.1465127,"lon":3.2355341},{"type":"node","id":3346575853,"lat":51.1466617,"lon":3.2349708},{"type":"node","id":3346575855,"lat":51.1466787,"lon":3.2355171},{"type":"node","id":3346575858,"lat":51.1466968,"lon":3.2342532},{"type":"node","id":3346575865,"lat":51.1468756,"lon":3.2344143},{"type":"node","id":3346575873,"lat":51.1469706,"lon":3.2366779},{"type":"node","id":3346575929,"lat":51.1474307,"lon":3.2366434},{"type":"node","id":1523513488,"lat":51.1398248,"lon":3.2392608},{"type":"node","id":1744641292,"lat":51.1395814,"lon":3.2390365},{"type":"node","id":1744641293,"lat":51.1397822,"lon":3.2393867},{"type":"node","id":1920143232,"lat":51.1394724,"lon":3.2390121},{"type":"node","id":7393009684,"lat":51.1394307,"lon":3.2390001},{"type":"node","id":7393048385,"lat":51.1394135,"lon":3.2390524},{"type":"node","id":3346575843,"lat":51.1465041,"lon":3.2376453},{"type":"node","id":3346575845,"lat":51.146508,"lon":3.2378517},{"type":"node","id":3346575876,"lat":51.1470052,"lon":3.237604},{"type":"node","id":3346575886,"lat":51.147098,"lon":3.2412975},{"type":"node","id":3346575889,"lat":51.1471585,"lon":3.2428761},{"type":"node","id":3346575891,"lat":51.1471715,"lon":3.2377865},{"type":"node","id":3346575901,"lat":51.1472492,"lon":3.2398591},{"type":"node","id":3346575903,"lat":51.1472654,"lon":3.242623},{"type":"node","id":3346575908,"lat":51.1472751,"lon":3.2428571},{"type":"node","id":3346575917,"lat":51.1473518,"lon":3.2426093},{"type":"node","id":3346575923,"lat":51.1473906,"lon":3.2398205},{"type":"node","id":3346575925,"lat":51.147408,"lon":3.2424474},{"type":"node","id":3346575928,"lat":51.1474263,"lon":3.24062},{"type":"node","id":3346575933,"lat":51.1474728,"lon":3.2421565},{"type":"node","id":3346575943,"lat":51.1475354,"lon":3.2418174},{"type":"node","id":3346575945,"lat":51.1475494,"lon":3.2412786},{"type":"node","id":3346575946,"lat":51.1475667,"lon":3.2415454},{"type":"node","id":6291339815,"lat":51.1434669,"lon":3.2496811},{"type":"node","id":6291339816,"lat":51.1434103,"lon":3.2497287},{"type":"node","id":6291339821,"lat":51.1435685,"lon":3.2496576},{"type":"node","id":6291339822,"lat":51.1434825,"lon":3.2497283},{"type":"node","id":6291339827,"lat":51.1437401,"lon":3.2490327},{"type":"node","id":6291339828,"lat":51.1433037,"lon":3.2494123},{"type":"node","id":6291339829,"lat":51.1436099,"lon":3.2497855},{"type":"node","id":6291339830,"lat":51.1439041,"lon":3.249535},{"type":"node","id":170464837,"lat":51.1533286,"lon":3.236239},{"type":"node","id":170464839,"lat":51.1532379,"lon":3.2364578},{"type":"node","id":1710262731,"lat":51.1513274,"lon":3.2373199},{"type":"node","id":1710262732,"lat":51.1508759,"lon":3.2370371},{"type":"node","id":1710262733,"lat":51.1509316,"lon":3.2370599},{"type":"node","id":1710262735,"lat":51.1510166,"lon":3.2370123},{"type":"node","id":1710262738,"lat":51.1512751,"lon":3.2372984},{"type":"node","id":1710262742,"lat":51.1513127,"lon":3.237065},{"type":"node","id":1710262743,"lat":51.1508201,"lon":3.2373832},{"type":"node","id":1710262745,"lat":51.151027,"lon":3.2369479},{"type":"node","id":1795793395,"lat":51.1476158,"lon":3.2338163},{"type":"node","id":1795793397,"lat":51.1475842,"lon":3.2344427},{"type":"node","id":1795793399,"lat":51.1477641,"lon":3.233033},{"type":"node","id":1795793405,"lat":51.1477717,"lon":3.2338665},{"type":"node","id":1795793406,"lat":51.1480775,"lon":3.2344787},{"type":"node","id":1795793407,"lat":51.1478893,"lon":3.2344203},{"type":"node","id":1795793408,"lat":51.1481719,"lon":3.2342485},{"type":"node","id":1795793409,"lat":51.147607,"lon":3.2330256},{"type":"node","id":4979389763,"lat":51.1476223,"lon":3.2330288},{"type":"node","id":8179735224,"lat":51.156047,"lon":3.2252534},{"type":"node","id":8179735264,"lat":51.1560464,"lon":3.2252785},{"type":"node","id":8179735265,"lat":51.1558544,"lon":3.2252597},{"type":"node","id":8179735266,"lat":51.1556789,"lon":3.2252522},{"type":"node","id":8179735267,"lat":51.1555253,"lon":3.2252547},{"type":"node","id":8179735268,"lat":51.1555747,"lon":3.2250024},{"type":"node","id":8179735269,"lat":51.1560527,"lon":3.2250198},{"type":"node","id":1525460846,"lat":51.1550489,"lon":3.2347709},{"type":"node","id":1810326044,"lat":51.1547625,"lon":3.2355098},{"type":"node","id":1810326087,"lat":51.1547809,"lon":3.2353708},{"type":"node","id":4912203160,"lat":51.1554941,"lon":3.2364781},{"type":"node","id":4912203161,"lat":51.1554192,"lon":3.2369649},{"type":"node","id":4912203162,"lat":51.1554175,"lon":3.2371071},{"type":"node","id":4912203163,"lat":51.1554301,"lon":3.2372063},{"type":"node","id":4912203164,"lat":51.1553847,"lon":3.2372197},{"type":"node","id":4912203165,"lat":51.1553763,"lon":3.2369502},{"type":"node","id":4912203166,"lat":51.1554512,"lon":3.236462},{"type":"node","id":4912203167,"lat":51.1555218,"lon":3.2366455},{"type":"node","id":4912203168,"lat":51.1554742,"lon":3.2369592},{"type":"node","id":4912203169,"lat":51.155516,"lon":3.2369752},{"type":"node","id":4912203170,"lat":51.1555635,"lon":3.2366616},{"type":"node","id":4912203171,"lat":51.1556125,"lon":3.2360775},{"type":"node","id":4912203172,"lat":51.155547,"lon":3.2364923},{"type":"node","id":4912203173,"lat":51.1555893,"lon":3.2365092},{"type":"node","id":4912203174,"lat":51.1556547,"lon":3.2360945},{"type":"node","id":4912203176,"lat":51.1554841,"lon":3.2362949},{"type":"node","id":4912203177,"lat":51.1553752,"lon":3.2362591},{"type":"node","id":4912203178,"lat":51.1553968,"lon":3.2360924},{"type":"node","id":4912203179,"lat":51.1555056,"lon":3.2361281},{"type":"node","id":4912214680,"lat":51.1553544,"lon":3.2348128},{"type":"node","id":4912214681,"lat":51.1550186,"lon":3.2346814},{"type":"node","id":4912214685,"lat":51.1554486,"lon":3.2338965},{"type":"node","id":4912214686,"lat":51.1550098,"lon":3.2346405},{"type":"node","id":4912214692,"lat":51.155386,"lon":3.2347722},{"type":"node","id":4912214693,"lat":51.1555155,"lon":3.2338415},{"type":"node","id":4912214694,"lat":51.1554587,"lon":3.2338214},{"type":"node","id":4912214695,"lat":51.1553292,"lon":3.2347521},{"type":"node","id":4912225050,"lat":51.1553136,"lon":3.234866},{"type":"node","id":4912225051,"lat":51.1551036,"lon":3.2337751},{"type":"node","id":4912225052,"lat":51.1549825,"lon":3.2346307},{"type":"node","id":4912225053,"lat":51.1551894,"lon":3.2336789},{"type":"node","id":4912225062,"lat":51.1551528,"lon":3.233747},{"type":"node","id":4912225063,"lat":51.1551831,"lon":3.2337249},{"type":"node","id":4912225064,"lat":51.1554653,"lon":3.2337755},{"type":"node","id":4912225067,"lat":51.1551309,"lon":3.2337849},{"type":"node","id":4912225068,"lat":51.1551727,"lon":3.2337999},{"type":"node","id":4912225069,"lat":51.1553182,"lon":3.2348322},{"type":"node","id":4912225070,"lat":51.1550516,"lon":3.2346556},{"type":"node","id":5972179331,"lat":51.154488,"lon":3.2350802},{"type":"node","id":5972179343,"lat":51.1545781,"lon":3.2351138},{"type":"node","id":5972179344,"lat":51.1546444,"lon":3.2351409},{"type":"node","id":5972179345,"lat":51.1546431,"lon":3.2351485},{"type":"node","id":5972179346,"lat":51.1547126,"lon":3.2351739},{"type":"node","id":5972179347,"lat":51.154714,"lon":3.2351659},{"type":"node","id":5972179348,"lat":51.1547795,"lon":3.235188},{"type":"node","id":5974489614,"lat":51.1546386,"lon":3.2355125},{"type":"node","id":5974489615,"lat":51.1544914,"lon":3.2353516},{"type":"node","id":5974489616,"lat":51.1544813,"lon":3.2351384},{"type":"node","id":5974489617,"lat":51.1548024,"lon":3.2352003},{"type":"node","id":5974489618,"lat":51.154732,"lon":3.2356091},{"type":"node","id":7529417225,"lat":51.159095,"lon":3.2366844},{"type":"node","id":7529417226,"lat":51.1589926,"lon":3.2372825},{"type":"node","id":7529417227,"lat":51.1588687,"lon":3.2372286},{"type":"node","id":7529417228,"lat":51.1589264,"lon":3.2368918},{"type":"node","id":7529417229,"lat":51.1588879,"lon":3.236875},{"type":"node","id":7529417230,"lat":51.1589326,"lon":3.2366138},{"type":"node","id":7529417232,"lat":51.159019,"lon":3.2366513},{"type":"node","id":170464840,"lat":51.1531619,"lon":3.2376814},{"type":"node","id":170464841,"lat":51.1532306,"lon":3.2376888},{"type":"node","id":1710245701,"lat":51.1528676,"lon":3.2390068},{"type":"node","id":1710245703,"lat":51.1527703,"lon":3.239403},{"type":"node","id":1710245705,"lat":51.1527888,"lon":3.2390966},{"type":"node","id":1710245707,"lat":51.1526357,"lon":3.2390543},{"type":"node","id":1710245709,"lat":51.1528763,"lon":3.2390382},{"type":"node","id":1710245711,"lat":51.1528928,"lon":3.2390631},{"type":"node","id":1710245713,"lat":51.1528729,"lon":3.2389738},{"type":"node","id":1710245715,"lat":51.1528645,"lon":3.2394083},{"type":"node","id":1710245718,"lat":51.1526407,"lon":3.2389524},{"type":"node","id":1710262736,"lat":51.1512857,"lon":3.2375783},{"type":"node","id":1710262737,"lat":51.151234,"lon":3.2375571},{"type":"node","id":1710262739,"lat":51.151183,"lon":3.2375939},{"type":"node","id":1710262741,"lat":51.1511924,"lon":3.2375358},{"type":"node","id":1710262744,"lat":51.1512252,"lon":3.2376112},{"type":"node","id":3346575950,"lat":51.1475926,"lon":3.2406028},{"type":"node","id":1728421374,"lat":51.1554436,"lon":3.2438233},{"type":"node","id":1728421375,"lat":51.15594,"lon":3.2438649},{"type":"node","id":1728421377,"lat":51.15559,"lon":3.2439695},{"type":"node","id":1728421379,"lat":51.1554335,"lon":3.243944},{"type":"node","id":1770289505,"lat":51.1565437,"lon":3.2437924},{"type":"node","id":4912197362,"lat":51.1565535,"lon":3.2438327},{"type":"node","id":4912197363,"lat":51.1562818,"lon":3.2438374},{"type":"node","id":4912197364,"lat":51.1565321,"lon":3.2435757},{"type":"node","id":4912197365,"lat":51.1565279,"lon":3.2433181},{"type":"node","id":4912197366,"lat":51.1562804,"lon":3.2435833},{"type":"node","id":4912197367,"lat":51.1562798,"lon":3.2431702},{"type":"node","id":4912197368,"lat":51.1562813,"lon":3.2437497},{"type":"node","id":4912197369,"lat":51.1562802,"lon":3.243488},{"type":"node","id":4912197370,"lat":51.1565257,"lon":3.2431702},{"type":"node","id":4912197371,"lat":51.1565542,"lon":3.2439044},{"type":"node","id":4912197372,"lat":51.1565308,"lon":3.2437482},{"type":"node","id":4912197373,"lat":51.1565294,"lon":3.2434893},{"type":"node","id":4912197374,"lat":51.1562835,"lon":3.2439005},{"type":"node","id":1710276232,"lat":51.1572435,"lon":3.2451269},{"type":"node","id":1710276240,"lat":51.156984,"lon":3.2453481},{"type":"node","id":1710276242,"lat":51.1567167,"lon":3.2452913},{"type":"node","id":1710276243,"lat":51.1570484,"lon":3.2451},{"type":"node","id":1710276251,"lat":51.1562241,"lon":3.2457572},{"type":"node","id":1710276253,"lat":51.156868,"lon":3.2451689},{"type":"node","id":1710276255,"lat":51.1563873,"lon":3.2456553},{"type":"node","id":1710276257,"lat":51.1572402,"lon":3.2450303},{"type":"node","id":1710276259,"lat":51.1561703,"lon":3.2454299},{"type":"node","id":1710276261,"lat":51.1564411,"lon":3.2457304},{"type":"node","id":1728421376,"lat":51.1555858,"lon":3.2441572},{"type":"node","id":1728421378,"lat":51.1559348,"lon":3.2441264},{"type":"node","id":1810330766,"lat":51.1562599,"lon":3.2457349},{"type":"node","id":1810345944,"lat":51.1572859,"lon":3.2458614},{"type":"node","id":1810345947,"lat":51.1568366,"lon":3.2461909},{"type":"node","id":1810345951,"lat":51.1572074,"lon":3.2455895},{"type":"node","id":1810345955,"lat":51.1567582,"lon":3.245919},{"type":"node","id":1810347217,"lat":51.1568783,"lon":3.2452387},{"type":"node","id":6255587424,"lat":51.1699788,"lon":3.1988617},{"type":"node","id":6255587425,"lat":51.1695322,"lon":3.1995447},{"type":"node","id":6255587426,"lat":51.1690026,"lon":3.1986489},{"type":"node","id":5173881316,"lat":51.1728811,"lon":3.210692},{"type":"node","id":5173881317,"lat":51.1728829,"lon":3.21077},{"type":"node","id":5173881318,"lat":51.1726197,"lon":3.2107914},{"type":"node","id":5173881320,"lat":51.1728794,"lon":3.2108575},{"type":"node","id":5173881621,"lat":51.1728791,"lon":3.2109364},{"type":"node","id":5173881622,"lat":51.1727133,"lon":3.2109488},{"type":"node","id":5173881623,"lat":51.1727117,"lon":3.2108703},{"type":"node","id":5173881624,"lat":51.1728613,"lon":3.211069},{"type":"node","id":5173881625,"lat":51.1728601,"lon":3.2111406},{"type":"node","id":5173881626,"lat":51.1727019,"lon":3.2111316},{"type":"node","id":5173881627,"lat":51.1727041,"lon":3.2110597},{"type":"node","id":6275462772,"lat":51.1733576,"lon":3.2110928},{"type":"node","id":6275462773,"lat":51.1733528,"lon":3.2112295},{"type":"node","id":6275462774,"lat":51.1735278,"lon":3.2112449},{"type":"node","id":6275462775,"lat":51.1735325,"lon":3.2111082},{"type":"node","id":6275462776,"lat":51.1736659,"lon":3.2112568},{"type":"node","id":6275462777,"lat":51.1736113,"lon":3.2112529},{"type":"node","id":6275462784,"lat":51.1735598,"lon":3.2109277},{"type":"node","id":6275462985,"lat":51.1734626,"lon":3.2109229},{"type":"node","id":6275462986,"lat":51.1734599,"lon":3.2110592},{"type":"node","id":6275462987,"lat":51.1735572,"lon":3.211064},{"type":"node","id":6275462988,"lat":51.1734613,"lon":3.2109904},{"type":"node","id":6275462989,"lat":51.173357,"lon":3.2111476},{"type":"node","id":7054196467,"lat":51.1726209,"lon":3.2107131},{"type":"node","id":8042845810,"lat":51.1737696,"lon":3.206708},{"type":"node","id":8042845811,"lat":51.1734466,"lon":3.2056148},{"type":"node","id":5952389321,"lat":51.1668901,"lon":3.2166736},{"type":"node","id":5952389322,"lat":51.1664592,"lon":3.2166337},{"type":"node","id":5952389323,"lat":51.1659941,"lon":3.2166018},{"type":"node","id":5172938444,"lat":51.1667283,"lon":3.2192609},{"type":"node","id":5536609426,"lat":51.1664884,"lon":3.2184803},{"type":"node","id":5536620510,"lat":51.1668363,"lon":3.2197448},{"type":"node","id":5536620511,"lat":51.1671911,"lon":3.2210959},{"type":"node","id":5952389320,"lat":51.1665059,"lon":3.2183884},{"type":"node","id":5536620506,"lat":51.1675299,"lon":3.2170283},{"type":"node","id":5536620507,"lat":51.1672585,"lon":3.2168071},{"type":"node","id":6275462778,"lat":51.1736042,"lon":3.2115044},{"type":"node","id":6275462779,"lat":51.1736588,"lon":3.2115083},{"type":"node","id":6275462780,"lat":51.1735687,"lon":3.2112964},{"type":"node","id":6275462781,"lat":51.1735211,"lon":3.2112937},{"type":"node","id":6275462782,"lat":51.1735156,"lon":3.2115423},{"type":"node","id":6275462783,"lat":51.1735632,"lon":3.211545},{"type":"node","id":5536620505,"lat":51.1683944,"lon":3.2180288},{"type":"node","id":5536620512,"lat":51.1673641,"lon":3.2217199},{"type":"node","id":5536620513,"lat":51.1675007,"lon":3.2221772},{"type":"node","id":5536620514,"lat":51.1679104,"lon":3.2236675},{"type":"node","id":5536620516,"lat":51.1679955,"lon":3.224005},{"type":"node","id":5536620824,"lat":51.1700823,"lon":3.2236258},{"type":"node","id":5536620826,"lat":51.171324,"lon":3.2230929},{"type":"node","id":5536620827,"lat":51.1717731,"lon":3.2213846},{"type":"node","id":5536620828,"lat":51.170726,"lon":3.2202369},{"type":"node","id":5536620829,"lat":51.1706206,"lon":3.2201178},{"type":"node","id":5536620830,"lat":51.170049,"lon":3.2215441},{"type":"node","id":5536620831,"lat":51.1699442,"lon":3.2215294},{"type":"node","id":5536620832,"lat":51.1683757,"lon":3.2194636},{"type":"node","id":6067483781,"lat":51.1675243,"lon":3.222207},{"type":"node","id":6067483782,"lat":51.1713888,"lon":3.2231705},{"type":"node","id":7794736251,"lat":51.1688401,"lon":3.2184325},{"type":"node","id":1069177852,"lat":51.1789546,"lon":3.2021791},{"type":"node","id":1069177853,"lat":51.1801636,"lon":3.2031369},{"type":"node","id":1069177873,"lat":51.1791663,"lon":3.2034541},{"type":"node","id":1069177915,"lat":51.1799187,"lon":3.2029579},{"type":"node","id":1069177919,"lat":51.1786053,"lon":3.2030903},{"type":"node","id":1069177920,"lat":51.1790417,"lon":3.2033998},{"type":"node","id":1069177925,"lat":51.1788415,"lon":3.2032442},{"type":"node","id":1069177933,"lat":51.1798479,"lon":3.2029364},{"type":"node","id":1069177967,"lat":51.178467,"lon":3.2025563},{"type":"node","id":1069177976,"lat":51.1791427,"lon":3.2026954},{"type":"node","id":1069177984,"lat":51.1783718,"lon":3.2028231},{"type":"node","id":1069178021,"lat":51.1793534,"lon":3.2033094},{"type":"node","id":1069178133,"lat":51.1784113,"lon":3.2028156},{"type":"node","id":6853179202,"lat":51.1792037,"lon":3.2026994},{"type":"node","id":6853179203,"lat":51.1790601,"lon":3.2026755},{"type":"node","id":6853179204,"lat":51.1791861,"lon":3.2027108},{"type":"node","id":6853179205,"lat":51.1791741,"lon":3.2027122},{"type":"node","id":6853179206,"lat":51.1791634,"lon":3.2027102},{"type":"node","id":6853179207,"lat":51.1791526,"lon":3.2027048},{"type":"node","id":6853179208,"lat":51.1790128,"lon":3.202488},{"type":"node","id":6853179209,"lat":51.1790434,"lon":3.2026225},{"type":"node","id":6853179210,"lat":51.1789811,"lon":3.2023837},{"type":"node","id":6853179211,"lat":51.1789744,"lon":3.2022415},{"type":"node","id":6853179212,"lat":51.1789438,"lon":3.2022643},{"type":"node","id":6853179213,"lat":51.1784216,"lon":3.2028567},{"type":"node","id":6853179214,"lat":51.1784157,"lon":3.2028375},{"type":"node","id":6853179215,"lat":51.1784506,"lon":3.2029294},{"type":"node","id":6853179216,"lat":51.1783884,"lon":3.2026704},{"type":"node","id":6853179217,"lat":51.178422,"lon":3.2026034},{"type":"node","id":6853179218,"lat":51.1784117,"lon":3.2026185},{"type":"node","id":6853179219,"lat":51.1784021,"lon":3.2026354},{"type":"node","id":6853179220,"lat":51.1783957,"lon":3.2026505},{"type":"node","id":6853179221,"lat":51.178443,"lon":3.2025785},{"type":"node","id":6853179222,"lat":51.1784546,"lon":3.202567},{"type":"node","id":6853179223,"lat":51.1784321,"lon":3.2025906},{"type":"node","id":6853179224,"lat":51.1783719,"lon":3.2027464},{"type":"node","id":6853179225,"lat":51.1783749,"lon":3.2027238},{"type":"node","id":6853179226,"lat":51.1783827,"lon":3.2026894},{"type":"node","id":6853179227,"lat":51.1783784,"lon":3.2027066},{"type":"node","id":6853179228,"lat":51.1783697,"lon":3.2027864},{"type":"node","id":6853179229,"lat":51.1783704,"lon":3.2027651},{"type":"node","id":6853179230,"lat":51.1783703,"lon":3.2028048},{"type":"node","id":6853179234,"lat":51.1798837,"lon":3.2029379},{"type":"node","id":6853179235,"lat":51.1798988,"lon":3.2029445},{"type":"node","id":6853179236,"lat":51.1798661,"lon":3.2029354},{"type":"node","id":6853179237,"lat":51.1798345,"lon":3.2029407},{"type":"node","id":6853179238,"lat":51.1798207,"lon":3.2029479},{"type":"node","id":6853179239,"lat":51.1792634,"lon":3.2033377},{"type":"node","id":6853179240,"lat":51.1793019,"lon":3.2033359},{"type":"node","id":6853179241,"lat":51.1792828,"lon":3.20334},{"type":"node","id":6853179242,"lat":51.1792732,"lon":3.2033393},{"type":"node","id":6853179243,"lat":51.1792921,"lon":3.2033388},{"type":"node","id":6853179244,"lat":51.1793279,"lon":3.2033257},{"type":"node","id":6853179245,"lat":51.1793153,"lon":3.2033313},{"type":"node","id":6853179246,"lat":51.1793413,"lon":3.2033182},{"type":"node","id":6853179247,"lat":51.1792384,"lon":3.2033981},{"type":"node","id":6853179248,"lat":51.1792572,"lon":3.2033605},{"type":"node","id":6853179249,"lat":51.1792512,"lon":3.2033774},{"type":"node","id":6853179250,"lat":51.1792456,"lon":3.2033888},{"type":"node","id":6853179256,"lat":51.1790996,"lon":3.2034514},{"type":"node","id":6853179257,"lat":51.1790731,"lon":3.2034317},{"type":"node","id":6853179258,"lat":51.1790581,"lon":3.2034168},{"type":"node","id":6853179259,"lat":51.1790862,"lon":3.2034422},{"type":"node","id":6853179260,"lat":51.179133,"lon":3.2034648},{"type":"node","id":6853179261,"lat":51.1791191,"lon":3.203461},{"type":"node","id":6853179262,"lat":51.1791546,"lon":3.2034608},{"type":"node","id":6853179263,"lat":51.1791443,"lon":3.2034639},{"type":"node","id":6853179264,"lat":51.1789437,"lon":3.2033089},{"type":"node","id":6853179267,"lat":51.1785146,"lon":3.2030128},{"type":"node","id":6853179268,"lat":51.1785517,"lon":3.2030489},{"type":"node","id":6853179269,"lat":51.1785863,"lon":3.2030773},{"type":"node","id":6853179327,"lat":51.1789936,"lon":3.2024248},{"type":"node","id":7252820961,"lat":51.175521,"lon":3.2045972},{"type":"node","id":7252863798,"lat":51.1754304,"lon":3.2044959},{"type":"node","id":8042845806,"lat":51.1753353,"lon":3.2041851},{"type":"node","id":8042845807,"lat":51.175363,"lon":3.2043314},{"type":"node","id":8042845812,"lat":51.1752711,"lon":3.2040127},{"type":"node","id":4036885076,"lat":51.1740632,"lon":3.2050437},{"type":"node","id":4036899624,"lat":51.1767493,"lon":3.2082945},{"type":"node","id":5607796819,"lat":51.1782483,"lon":3.2091883},{"type":"node","id":5607796820,"lat":51.1785128,"lon":3.2086016},{"type":"node","id":5607798721,"lat":51.1786474,"lon":3.2090453},{"type":"node","id":5607798722,"lat":51.1782874,"lon":3.2093115},{"type":"node","id":5607798723,"lat":51.178141,"lon":3.2088491},{"type":"node","id":5607798725,"lat":51.1785713,"lon":3.2087945},{"type":"node","id":5728443539,"lat":51.1753294,"lon":3.2097039},{"type":"node","id":5728443540,"lat":51.1752216,"lon":3.2089278},{"type":"node","id":6275462768,"lat":51.174424,"lon":3.2105467},{"type":"node","id":6275462769,"lat":51.1743524,"lon":3.2105548},{"type":"node","id":6275462770,"lat":51.1743644,"lon":3.2108257},{"type":"node","id":6275462771,"lat":51.1744361,"lon":3.2108176},{"type":"node","id":7252820962,"lat":51.1756015,"lon":3.204854},{"type":"node","id":7252820963,"lat":51.1755802,"lon":3.204928},{"type":"node","id":7252820964,"lat":51.1755132,"lon":3.2049422},{"type":"node","id":7252820965,"lat":51.1754719,"lon":3.2050156},{"type":"node","id":7252820966,"lat":51.1754575,"lon":3.2051212},{"type":"node","id":7252820967,"lat":51.1755143,"lon":3.2052892},{"type":"node","id":7252820968,"lat":51.1755533,"lon":3.2055086},{"type":"node","id":7252820969,"lat":51.1755563,"lon":3.2060065},{"type":"node","id":7252820970,"lat":51.175491,"lon":3.2064409},{"type":"node","id":7252820971,"lat":51.1753674,"lon":3.2068348},{"type":"node","id":7252820972,"lat":51.1751944,"lon":3.2070531},{"type":"node","id":7252820973,"lat":51.1751195,"lon":3.2071478},{"type":"node","id":7252820974,"lat":51.1750834,"lon":3.2072467},{"type":"node","id":7252820975,"lat":51.1750963,"lon":3.2073579},{"type":"node","id":7252820976,"lat":51.1751376,"lon":3.2074032},{"type":"node","id":7252820977,"lat":51.175215,"lon":3.2073826},{"type":"node","id":7252820978,"lat":51.1752848,"lon":3.2073785},{"type":"node","id":7252820979,"lat":51.1754252,"lon":3.2073858},{"type":"node","id":7252820980,"lat":51.1754615,"lon":3.2074926},{"type":"node","id":7252820981,"lat":51.1754259,"lon":3.20756},{"type":"node","id":7252820982,"lat":51.17537,"lon":3.2076668},{"type":"node","id":7252820983,"lat":51.1753304,"lon":3.2078901},{"type":"node","id":7252820984,"lat":51.1753152,"lon":3.2079319},{"type":"node","id":7252874885,"lat":51.1754423,"lon":3.2080951},{"type":"node","id":7252874886,"lat":51.1754991,"lon":3.2083134},{"type":"node","id":7252874887,"lat":51.1755307,"lon":3.2084864},{"type":"node","id":7252874888,"lat":51.1755729,"lon":3.2087064},{"type":"node","id":7252874889,"lat":51.1753248,"lon":3.2088635},{"type":"node","id":7252874890,"lat":51.1752645,"lon":3.2092365},{"type":"node","id":7252874891,"lat":51.1747746,"lon":3.2093558},{"type":"node","id":8042845789,"lat":51.1748587,"lon":3.209526},{"type":"node","id":8042845790,"lat":51.1749489,"lon":3.2096774},{"type":"node","id":8042845791,"lat":51.1750595,"lon":3.2097458},{"type":"node","id":8042845792,"lat":51.1753557,"lon":3.2077924},{"type":"node","id":8042845793,"lat":51.1754621,"lon":3.2074425},{"type":"node","id":8042845794,"lat":51.1754531,"lon":3.2074092},{"type":"node","id":8042845795,"lat":51.1754729,"lon":3.2051839},{"type":"node","id":8042845796,"lat":51.1754907,"lon":3.2052089},{"type":"node","id":8042845797,"lat":51.1755084,"lon":3.2052355},{"type":"node","id":8042845798,"lat":51.1755235,"lon":3.2053482},{"type":"node","id":8042845799,"lat":51.1755387,"lon":3.2053805},{"type":"node","id":8042845800,"lat":51.1755584,"lon":3.2057251},{"type":"node","id":8042845801,"lat":51.1755536,"lon":3.205762},{"type":"node","id":8042845802,"lat":51.1755492,"lon":3.2061312},{"type":"node","id":8042845803,"lat":51.1755305,"lon":3.2062755},{"type":"node","id":8042845804,"lat":51.1754335,"lon":3.2066603},{"type":"node","id":8042845805,"lat":51.1755929,"lon":3.2047843},{"type":"node","id":8042845808,"lat":51.1746278,"lon":3.2090183},{"type":"node","id":8042845809,"lat":51.1740796,"lon":3.2076268},{"type":"node","id":8042845844,"lat":51.1768218,"lon":3.20861},{"type":"node","id":8042845845,"lat":51.1767935,"lon":3.2085031},{"type":"node","id":8042845846,"lat":51.1769413,"lon":3.2089936},{"type":"node","id":8042845847,"lat":51.1757541,"lon":3.2096988},{"type":"node","id":8042845848,"lat":51.1757421,"lon":3.2096812},{"type":"node","id":8042845849,"lat":51.1757312,"lon":3.2096924},{"type":"node","id":8042845850,"lat":51.1757202,"lon":3.2096478},{"type":"node","id":8042845851,"lat":51.1756902,"lon":3.2096207},{"type":"node","id":8042845852,"lat":51.1756712,"lon":3.2096143},{"type":"node","id":8042845853,"lat":51.1756602,"lon":3.2095745},{"type":"node","id":8042845854,"lat":51.1756552,"lon":3.2095537},{"type":"node","id":8042845855,"lat":51.1756657,"lon":3.2095174},{"type":"node","id":8042845856,"lat":51.175658,"lon":3.20908},{"type":"node","id":8042845857,"lat":51.1756525,"lon":3.2093366},{"type":"node","id":8042845858,"lat":51.1756466,"lon":3.2088282},{"type":"node","id":8042845859,"lat":51.1756582,"lon":3.2089151},{"type":"node","id":8042845860,"lat":51.1765521,"lon":3.20839},{"type":"node","id":1069177845,"lat":51.1809357,"lon":3.2035366},{"type":"node","id":1069177849,"lat":51.1803975,"lon":3.2017749},{"type":"node","id":1069178166,"lat":51.1804195,"lon":3.2033098},{"type":"node","id":1519342742,"lat":51.1805239,"lon":3.2032684},{"type":"node","id":1519342743,"lat":51.18064,"lon":3.2036951},{"type":"node","id":1759437085,"lat":51.1806986,"lon":3.2036647},{"type":"node","id":6852012577,"lat":51.1804541,"lon":3.2017867},{"type":"node","id":6852012578,"lat":51.1804124,"lon":3.2018177},{"type":"node","id":6852012579,"lat":51.1804106,"lon":3.2018165},{"type":"node","id":6852012580,"lat":51.1804143,"lon":3.2018177},{"type":"node","id":6852012581,"lat":51.1808363,"lon":3.2030295},{"type":"node","id":6852012582,"lat":51.1807955,"lon":3.2030595},{"type":"node","id":6852012583,"lat":51.180798,"lon":3.2030712},{"type":"node","id":1519476620,"lat":51.1786696,"lon":3.2199463},{"type":"node","id":1519476635,"lat":51.179306,"lon":3.2193119},{"type":"node","id":1519476698,"lat":51.1795485,"lon":3.2192221},{"type":"node","id":1519476744,"lat":51.1791125,"lon":3.2194529},{"type":"node","id":1519476746,"lat":51.178483,"lon":3.2203218},{"type":"node","id":1519476797,"lat":51.1788731,"lon":3.2196593},{"type":"node","id":3780611492,"lat":51.1761568,"lon":3.2238485},{"type":"node","id":3780611493,"lat":51.1762213,"lon":3.223901},{"type":"node","id":3780611494,"lat":51.1762626,"lon":3.2237172},{"type":"node","id":3780611495,"lat":51.1763208,"lon":3.2237628},{"type":"node","id":3780611496,"lat":51.1763248,"lon":3.2236414},{"type":"node","id":3780611497,"lat":51.1763881,"lon":3.2236926},{"type":"node","id":3780611498,"lat":51.1764876,"lon":3.2235544},{"type":"node","id":3780611499,"lat":51.1766551,"lon":3.2232337},{"type":"node","id":3780611500,"lat":51.176687,"lon":3.2231945},{"type":"node","id":3780611501,"lat":51.1767105,"lon":3.2232776},{"type":"node","id":3780611502,"lat":51.176751,"lon":3.2232465},{"type":"node","id":3780611503,"lat":51.1767812,"lon":3.2230729},{"type":"node","id":3780611504,"lat":51.1768505,"lon":3.2231083},{"type":"node","id":6533893620,"lat":51.178521,"lon":3.2203687},{"type":"node","id":6533893621,"lat":51.1786845,"lon":3.220025},{"type":"node","id":6533893622,"lat":51.1789011,"lon":3.2197183},{"type":"node","id":6533893624,"lat":51.1791343,"lon":3.2195235},{"type":"node","id":6533893625,"lat":51.1793269,"lon":3.2193854},{"type":"node","id":6533893626,"lat":51.1795596,"lon":3.219299},{"type":"node","id":5536620518,"lat":51.1683264,"lon":3.224863},{"type":"node","id":5536620519,"lat":51.1684352,"lon":3.2251117},{"type":"node","id":5536620520,"lat":51.1685675,"lon":3.2254022},{"type":"node","id":5536620821,"lat":51.1687379,"lon":3.2258223},{"type":"node","id":5536620822,"lat":51.1693682,"lon":3.2250177},{"type":"node","id":5536620823,"lat":51.1693734,"lon":3.225049},{"type":"node","id":5536620825,"lat":51.1707605,"lon":3.2244639},{"type":"node","id":5536620837,"lat":51.1697793,"lon":3.2260181},{"type":"node","id":5536620838,"lat":51.1699712,"lon":3.2262338},{"type":"node","id":5536620839,"lat":51.1701247,"lon":3.2263242},{"type":"node","id":5536620840,"lat":51.1704719,"lon":3.2266478},{"type":"node","id":5536620841,"lat":51.1701028,"lon":3.2281081},{"type":"node","id":5536620842,"lat":51.1698158,"lon":3.2276446},{"type":"node","id":5536620843,"lat":51.1696441,"lon":3.2273837},{"type":"node","id":5536620844,"lat":51.1695154,"lon":3.2272009},{"type":"node","id":5536620845,"lat":51.169536,"lon":3.2271664},{"type":"node","id":5536620846,"lat":51.1694515,"lon":3.2270181},{"type":"node","id":5635001306,"lat":51.1737078,"lon":3.2354437},{"type":"node","id":5635001371,"lat":51.1722128,"lon":3.2340273},{"type":"node","id":5635001372,"lat":51.1723921,"lon":3.2343394},{"type":"node","id":5635001373,"lat":51.1724213,"lon":3.2342967},{"type":"node","id":5635001374,"lat":51.1722421,"lon":3.2339846},{"type":"node","id":5635001375,"lat":51.1728995,"lon":3.2339319},{"type":"node","id":5635001376,"lat":51.1729253,"lon":3.2339922},{"type":"node","id":5635001377,"lat":51.1723583,"lon":3.2340816},{"type":"node","id":5635001378,"lat":51.1723268,"lon":3.2340173},{"type":"node","id":5635001379,"lat":51.172885,"lon":3.2337993},{"type":"node","id":5635001380,"lat":51.1728611,"lon":3.2338706},{"type":"node","id":5635001381,"lat":51.1723325,"lon":3.2339419},{"type":"node","id":5635001382,"lat":51.1723464,"lon":3.2338696},{"type":"node","id":5882873334,"lat":51.1736186,"lon":3.2330966},{"type":"node","id":5882873335,"lat":51.1735451,"lon":3.2327633},{"type":"node","id":5882873336,"lat":51.1737001,"lon":3.2327438},{"type":"node","id":5882873337,"lat":51.1736796,"lon":3.2318764},{"type":"node","id":5882873338,"lat":51.1735265,"lon":3.2318782},{"type":"node","id":6593340582,"lat":51.1727872,"lon":3.2328745},{"type":"node","id":6593340583,"lat":51.1728013,"lon":3.2332051},{"type":"node","id":6593340584,"lat":51.1736743,"lon":3.2331435},{"type":"node","id":7767137235,"lat":51.1735198,"lon":3.2355568},{"type":"node","id":7767137236,"lat":51.1735366,"lon":3.2355246},{"type":"node","id":7767137237,"lat":51.1735198,"lon":3.2356399},{"type":"node","id":5635001274,"lat":51.1751425,"lon":3.2346144},{"type":"node","id":5635001275,"lat":51.1751696,"lon":3.2347601},{"type":"node","id":5635001276,"lat":51.1750553,"lon":3.2348141},{"type":"node","id":5635001277,"lat":51.1750282,"lon":3.2346684},{"type":"node","id":5635001312,"lat":51.174002,"lon":3.2349367},{"type":"node","id":5635001383,"lat":51.1740709,"lon":3.233056},{"type":"node","id":5635001384,"lat":51.1740249,"lon":3.2330598},{"type":"node","id":5635001385,"lat":51.1740265,"lon":3.2331313},{"type":"node","id":5635001386,"lat":51.1740597,"lon":3.2327202},{"type":"node","id":5635001414,"lat":51.174281,"lon":3.2336147},{"type":"node","id":5635001415,"lat":51.174081,"lon":3.2338914},{"type":"node","id":5635001416,"lat":51.1740489,"lon":3.2338323},{"type":"node","id":5635001417,"lat":51.1742489,"lon":3.2335556},{"type":"node","id":5761770202,"lat":51.1783111,"lon":3.2342484},{"type":"node","id":5761770204,"lat":51.1782819,"lon":3.2339616},{"type":"node","id":7767137234,"lat":51.1739713,"lon":3.2348766},{"type":"node","id":9052878228,"lat":51.1781206,"lon":3.234323},{"type":"node","id":9052878229,"lat":51.1781054,"lon":3.2339448},{"type":"way","id":810604915,"nodes":[1168727824,9167054153,9274761589,9274761596,7577430793,1038638712,1038638723,1038638661,9199177059,1038638721,7554434436,7578975035,7554434438,7578865273,7578975032,7578975030,7578975029,1038638696,7578975009,7578975008,7578975007,1038638743,7578975002,7578974988,7578974989,7578974990,7578974991,7578974992,7578865275,7578865274,1038638753,7578974995,7578974996,7578974997,7578904489,7578974999,7578975000,7578975001,7578974998,3921878998,1038638592,929120698,1675648152,7578865281,7578865283,7578975012,7578975015,7578975016,3922380061,2732486274,3922380083,7578975019,7578975018,7578975021,7578960079,3922375256,7578975024,3922380071]},{"type":"way","id":989393316,"nodes":[3922380071,3922380081,3922380086,3922380092,3922380095,9167054157,7578975026,9274761593,9274761592,9274761591,7578975027,9167054156,9167054154,7578975049,7578975028,1168727824]},{"type":"way","id":389026405,"nodes":[3921879019,7578975044,3921879018,7578975046,7578975045,7578975040,3921879004,3921879011,3921879019]},{"type":"way","id":810607458,"nodes":[7578987409,7578987410,7578987411,5745833241,5745833240,5745833239,7578987412,7578987415,7578987413,7578987414,7578987417,7578987416,7578987418,7578987419,7578987409]},{"type":"way","id":777280458,"nodes":[8042845812,8042845806,8042845807,7252863798,7252820961,8042845805,7252820962,7252820963,7252820964,7252820965,7252820966,8042845795,8042845796,8042845797,7252820967,8042845798,8042845799,7252820968,8042845800,8042845801,7252820969,8042845802,8042845803,7252820970,8042845804,7252820971,7252820972,7252820973,7252820974,7252820975,7252820976,7252820977,7252820978,7252820979,8042845794,8042845793,7252820980,7252820981,7252820982,8042845792,7252820983,7252820984,7252874885,7252874886,7252874887,7252874888,7252874889,5728443540,7252874890,5728443539,8042845791,8042845790,8042845789,7252874891,8042845808,8042845809,8042845810,8042845811,4036885076,8042845812]},{"type":"way","id":577572397,"nodes":[5536620518,5536620519,5536620520,5536620821,5536620822,5536620823,5536620824,5536620825,5536620826,6067483782,5536620827,5536620828,5536620829,5536620830,5536620831,5536620832,7794736251,5536620505,5536620506,5536620507,5952389321,5952389322,5952389323,5536609426,5952389320,5172938444,5536620510,5536620511,5536620512,5536620513,6067483781,5536620514,5536620516,5536620518]},{"type":"way","id":863373849,"nodes":[4036899624,8042845845,8042845844,8042845846,8042845847,8042845848,8042845849,8042845850,8042845851,8042845852,8042845853,8042845854,8042845855,8042845857,8042845856,8042845859,8042845858,8042845860,4036899624]},{"type":"way","id":577572399,"nodes":[5536620837,5536620838,5536620839,5536620840,5536620841,5536620842,5536620843,5536620844,5536620845,5536620846,5536620837]}]}; - Utils.injectJsonDownloadForTests( - "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A60%5D%5Bbbox%3A51.124212757826875%2C3.1640625%2C51.17934297928927%2C3.251953125%5D%3B"+query , d + "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A60%5D%5Bbbox%3A51.124212757826875%2C3.1640625%2C51.17934297928927%2C3.251953125%5D%3B" + + query, + d ) Utils.injectJsonDownloadForTests( - "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A60%5D%5Bbbox%3A51.124212757826875%2C3.251953125%2C51.17934297928927%2C3.33984375%5D%3B"+query , - {"version":0.6,"generator":"Overpass API 0.7.57 93a4d346","osm3s":{"timestamp_osm_base":"2022-02-14T00:02:14Z","copyright":"The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."},"elements":[{"type":"node","id":668981602,"lat":51.1588243,"lon":3.2558654,"tags":{"amenity":"bench"}},{"type":"node","id":668981622,"lat":51.1565636,"lon":3.2549888,"tags":{"leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"no"}},{"type":"node","id":1580339675,"lat":51.1395949,"lon":3.3332507,"tags":{"amenity":"parking"}},{"type":"node","id":1764571836,"lat":51.1701118,"lon":3.3363371,"tags":{"amenity":"parking"}},{"type":"node","id":2121652779,"lat":51.1268536,"lon":3.3239607,"tags":{"amenity":"parking"}},{"type":"node","id":2386053906,"lat":51.162161,"lon":3.263065,"tags":{"amenity":"toilets"}},{"type":"node","id":2978180520,"lat":51.1329149,"lon":3.3362322,"tags":{"amenity":"bench"}},{"type":"node","id":2978183271,"lat":51.1324243,"lon":3.3373735,"tags":{"amenity":"bench"}},{"type":"node","id":2978184471,"lat":51.1436385,"lon":3.2916539,"tags":{"amenity":"bench","backrest":"yes","check_date":"2021-02-26"}},{"type":"node","id":3925976407,"lat":51.1787486,"lon":3.2831866,"tags":{"leisure":"picnic_table"}},{"type":"node","id":5158056232,"lat":51.1592067,"lon":3.2567111,"tags":{"leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"no"}},{"type":"node","id":5718776382,"lat":51.1609023,"lon":3.2582509,"tags":{"check_date":"2021-02-26","covered":"no","leisure":"picnic_table"}},{"type":"node","id":5718776383,"lat":51.1609488,"lon":3.2581877,"tags":{"check_date":"2021-02-26","covered":"no","leisure":"picnic_table"}},{"type":"node","id":5745727100,"lat":51.1594639,"lon":3.2604304,"tags":{"amenity":"bench"}},{"type":"node","id":5745739587,"lat":51.1580397,"lon":3.263101,"tags":{"check_date":"2021-02-26","leisure":"picnic_table"}},{"type":"node","id":5745739588,"lat":51.1580631,"lon":3.2630345,"tags":{"check_date":"2021-02-26","leisure":"picnic_table"}},{"type":"node","id":5961596093,"lat":51.1588103,"lon":3.2633933,"tags":{"leisure":"picnic_table"}},{"type":"node","id":5964032193,"lat":51.1514821,"lon":3.2723766,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034563379,"lat":51.1421689,"lon":3.3022271,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034564191,"lat":51.1722186,"lon":3.2823584,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034565298,"lat":51.1722796,"lon":3.282329,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6145151111,"lat":51.1690435,"lon":3.3388676,"tags":{"amenity":"bench"}},{"type":"node","id":6145151112,"lat":51.1690023,"lon":3.3388636,"tags":{"amenity":"bench"}},{"type":"node","id":6216549651,"lat":51.1292813,"lon":3.332369,"tags":{"amenity":"bench"}},{"type":"node","id":6216549652,"lat":51.1292768,"lon":3.3324259,"tags":{"amenity":"bench"}},{"type":"node","id":7204447030,"lat":51.1791769,"lon":3.283116,"tags":{"board_type":"nature","information":"board","mapillary":"0BHVgU1XCyTMM9cjvidUqk","name":"De Assebroekse Meersen","tourism":"information"}},{"type":"node","id":7468175778,"lat":51.1344104,"lon":3.3348246,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7602473480,"lat":51.1503874,"lon":3.2836867,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7602473482,"lat":51.150244,"lon":3.2842925,"tags":{"board_type":"wildlife","information":"board","name":"Waterbeestjes","operator":"Natuurpunt Vallei van de Zuidleie","tourism":"information"}},{"type":"node","id":7602699080,"lat":51.1367031,"lon":3.3320712,"tags":{"amenity":"bench","backrest":"yes","material":"metal"}},{"type":"node","id":7680940369,"lat":51.1380074,"lon":3.3369928,"tags":{"amenity":"bench"}},{"type":"node","id":7726850522,"lat":51.1418585,"lon":3.3064234,"tags":{"image:0":"https://i.imgur.com/Bh6UjYy.jpg","information":"board","tourism":"information"}},{"type":"node","id":7727071212,"lat":51.1501173,"lon":3.2845352,"tags":{"board_type":"wildlife","image:0":"https://i.imgur.com/mFEQJWd.jpg","information":"board","name":"Vleermuizen","operator":"Natuurpunt Vallei van de Zuidleie","tourism":"information"}},{"type":"node","id":9122376662,"lat":51.1720505,"lon":3.3308524,"tags":{"amenity":"bench"}},{"type":"node","id":9425818876,"lat":51.1325315,"lon":3.3371616,"tags":{"leisure":"picnic_table","mapillary":"101961548889238"}},{"type":"way","id":149408639,"nodes":[1623924235,1623924236,1623924238,1864750831,1623924241,1623924235],"tags":{"amenity":"parking"}},{"type":"way","id":149553206,"nodes":[1625199938,1625199951,8377691836,8378081366,8378081429,8378081386,1625199950,6414383775,1625199827,1625199938],"tags":{"amenity":"parking"}},{"type":"way","id":184402308,"nodes":[1948836195,1948836194,1948836193,1948836189,1948836192,1948836195],"tags":{"building":"yes","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","wheelchair":"yes"}},{"type":"way","id":184402309,"nodes":[1948836029,1948836038,1948836032,1948836025,1948836023,1948836029],"tags":{"building":"yes","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","source:geometry:date":"2011-11-07","source:geometry:ref":"Gbg/3489204"}},{"type":"way","id":184402331,"nodes":[1948836104,1948836072,1948836068,1948836093,1948836104],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":236979353,"nodes":[2449323191,7962681624,7962681623,7962681621,7962681622,2449323193,7962681620,7962681619,8360787098,4350143592,6794421028,6794421027,6794421041,7962681614,2123461969,2449323198,7962681615,7962681616,6794421042,2449323191],"tags":{"amenity":"parking"}},{"type":"way","id":251590503,"nodes":[2577910543,2577910530,2577910542,2577910520,6418533335,2577910526,2577910545,2577910543],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":313090489,"nodes":[3190107423,3190107435,3190107442,3190107439,3190107432,3190107428,3190107424,3190107400,3190107393,3190107377,3190107371,3190107374,3190107408,3190107407,3190107415,3190107416,3190107420,3190107423],"tags":{"amenity":"parking"}},{"type":"way","id":314531418,"nodes":[7468171455,7468171451,7727393212,7727393211,7727393210,7727393208,7727393209,8781449489,7727393207,7727393206,7727393205,7727393204,7727393203,7727393202,7727393201,7727393200,7390697550,7390697534,7727393199,7727393198,7727393197,7390697549,7727393196,7727393195,7727393194,7727393193,7727393192,7727393191,7727393190,7727393189,7727393188,7727393187,7727393186,7727393185,7727339384,7727339383,7727339382,1553169911,1553169836,1493821433,1493821422,3185248088,7727339364,7727339365,7727339366,7727339367,7727339368,7727339369,7727339370,7727339371,7727339372,7727339373,7727339374,7727339375,7727339376,7727339377,3185248049,3185248048,3185248042,3185248040,7727339378,7727339379,7727339380,7727339381,7468171438,7468171442,7468171430,7468171432,7468171446,7468171404,7468171405,7468171422,7468171426,7468171433,7468171423,7468171428,7468171431,7468171429,7468171406,7468171407,7468171444,7468171408,7468171409,7468171410,7468171411,7468171412,7468171413,7190927792,7190927791,7190927793,7190927787,7190927788,7190927789,7190927790,7190927786,7602692242,7190927785,7190873584,7468171450,7190873582,7190873576,7190873578,7190873577,7468171455],"tags":{"access":"yes","dog":"leashed","dogs":"leashed","image":"https://i.imgur.com/cOfwWTj.jpg","image:0":"https://i.imgur.com/RliQdyi.jpg","image:1":"https://i.imgur.com/IeKHahz.jpg","image:2":"https://i.imgur.com/1K0IORH.jpg","image:3":"https://i.imgur.com/jojP09s.jpg","image:4":"https://i.imgur.com/DK6kT51.jpg","image:5":"https://i.imgur.com/RizbGM1.jpg","image:6":"https://i.imgur.com/hyoY6Cl.jpg","image:7":"https://i.imgur.com/xDd7Wrq.jpg","leisure":"nature_reserve","name":"Miseriebocht","operator":"Natuurpunt Beernem","website":"https://www.natuurpunt.be/natuurgebied/miseriebocht","wikidata":"Q97060915"}},{"type":"way","id":366318480,"nodes":[3702926557,3702926558,3702926559,3702926560,3702926557],"tags":{"amenity":"parking"}},{"type":"way","id":366318481,"nodes":[3702878648,3702926561,3702926562,3702926563,3702926564,3702926565,3702926566,3702926567,3702878648],"tags":{"amenity":"parking"}},{"type":"way","id":366318482,"nodes":[3702926568,8292789053,8292789054,3702878654,3702926568],"tags":{"amenity":"parking"}},{"type":"way","id":366320440,"nodes":[3702956173,3702956174,3702956175,3702956176,3702956173],"tags":{"amenity":"parking","name":"Kleine Beer"}},{"type":"way","id":366321706,"nodes":[3702969714,3702969715,3702969716,3702969717,3702969714],"tags":{"amenity":"parking"}},{"type":"way","id":480267681,"nodes":[4732689641,4732689640,4732689639,4732689638,4732689637,4732689636,4732689635,4732689634,4732689633,4732689632,4732689631,4732689630,4732689629,4732689628,4732689627,4732689626,8294875888,4732689641],"tags":{"amenity":"parking"}},{"type":"way","id":554341620,"nodes":[5349884603,5349884602,5349884601,5349884600,5349884603],"tags":{"amenity":"parking"}},{"type":"way","id":554341621,"nodes":[5349884607,5349884606,5349884605,5349884604,5349884607],"tags":{"amenity":"parking"}},{"type":"way","id":561902092,"nodes":[5417516023,5417515520,5417516021,5417516022,5417516023],"tags":{"building":"yes","image":"https://i.imgur.com/WmViSbL.jpg","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","wheelchair":"no"}},{"type":"way","id":605915064,"nodes":[5745739924,5745739925,5745739926,5745739927,5745739924],"tags":{"amenity":"parking","surface":"fine2"}},{"type":"way","id":650285088,"nodes":[3645188881,6100803131,6100803130,6100803129,6100803124,6100803125,6100803128,6100803127,6100803126,3645188881],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":655944574,"nodes":[6145151107,6145151108,6145151109,6145151115,6145151114,6145151110,6145151107],"tags":{"amenity":"parking"}},{"type":"way","id":664171069,"nodes":[6216549610,6216549611,6216549612,6216549613,1413470849,1413470848,6216549605,6216549604,6216549610],"tags":{"amenity":"parking"}},{"type":"way","id":664171076,"nodes":[6216549656,6216549655,8307316294,6216549661,6216549657,6216549658,6216549659,6216549660,6216549656],"tags":{"amenity":"parking","capacity":"50"}},{"type":"way","id":665330334,"nodes":[6227395993,6227395991,6227395992,6227395997,6227395993],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface","surface":"asphalt"}},{"type":"way","id":684598363,"nodes":[3227068565,6416001161,6414352054,6414352055,7274233390,7274233391,7274233397,7274233395,7274233396,6414352053,3227068568,3227068565],"tags":{"amenity":"parking","fee":"no"}},{"type":"way","id":684599810,"nodes":[1317838331,8279842668,1384096112,1317838328,6414374315,3227068446,6414374316,6414374317,6414374318,3227068456,6414374319,6414374320,6414374321,1317838317,1317838331],"tags":{"access":"no","amenity":"parking","operator":"Politie"}},{"type":"way","id":761474468,"nodes":[7114502201,7114502203,7114502200,7114502202,3170562439,3170562437,3170562431,7114502240,7114502211,7114502212,7114502214,7114502215,7114502228,7114502234,7114502235,7114502236,7114502237,7114502238,7114502239,7114502233,7114502232,7114502231,7114502229,7114502230,7114502227,7114502226,7114502225,7114502216,7114502217,7114502224,3170562392,7114502218,3170562394,7114502219,7114502220,7114502221,7114502222,7114502223,3170562395,3170562396,3170562397,3170562402,3170562410,7114502209,7114502208,7114502207,7114502205,7114502206,3170562436,1475188519,1475188516,6627605025,8294886142,7114502201],"tags":{"image":"http://valleivandezuidleie.be/wp-content/uploads/2011/12/2011-03-24_G12_088_1_1.JPG","leisure":"nature_reserve","name":"Merlebeek-Meerberg","natural":"wetland","operator":"Natuurpunt Vallei van de Zuidleie","start_date":"2011","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/merlebeek/","wetland":"wet_meadow"}},{"type":"way","id":813859435,"nodes":[7602479690,7459257985,7602479691,7459154784,7459154782,7602479692,5482441357,7602479693,7602479694,7602479695,7602479696,7602479690],"tags":{"access":"yes","image:0":"https://i.imgur.com/nb9nawa.jpg","landuse":"grass","leisure":"nature_reserve","name:signed":"no","natural":"grass","operator":"Natuurpunt Vallei van de Zuidleie"}},{"type":"way","id":826103452,"nodes":[7713176912,7713176911,7713176910,7713176909,7713176912],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":893176022,"nodes":[1927235214,8301349336,8301349335,8301349337,1927235214],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943476,"nodes":[8328251887,8328251886,8328251885,8328251884,8328251887],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943477,"nodes":[8328251891,8328251890,8328251889,8328251888,8328251891],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943478,"nodes":[8328251895,8328251894,8328251893,8328251892,8328251895],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943479,"nodes":[8328251897,8328251896,8328251901,8328251900,8328251897],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943480,"nodes":[8328251898,8328251899,8328251903,8328251902,8328251898],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943481,"nodes":[8328251907,8328251906,8328251905,8328251904,8328251907],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943482,"nodes":[8328251911,8328251910,8328251909,8328251908,8328251911],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":901952767,"nodes":[8378097127,8378097126,8378097125,8378097124,8378097127],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"way","id":901952768,"nodes":[8378097134,8378097133,8378097132,8378097131,8378097130,8378097129,8378097128,8378097134],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"way","id":947325182,"nodes":[8497007549,8768981525,8768981522,8768981524,8768981521,8768981523,8768981520,8768981519,6206789709,8768981533,8497007549],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":966827815,"nodes":[8945026757,8945026756,8945026755,8945026754,8945026757],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"relation","id":2589413,"members":[{"type":"way","ref":663050030,"role":"outer"},{"type":"way","ref":184402334,"role":"outer"},{"type":"way","ref":184402332,"role":"outer"},{"type":"way","ref":184402325,"role":"outer"},{"type":"way","ref":184402326,"role":"outer"},{"type":"way","ref":314899865,"role":"outer"},{"type":"way","ref":314956402,"role":"outer"}],"tags":{"access":"yes","image":"https://i.imgur.com/Yu4qHh5.jpg","leisure":"nature_reserve","name":"Warandeputten","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","wikipedia":"nl:Warandeputten"}},{"type":"relation","id":8782624,"members":[{"type":"way","ref":315041273,"role":"outer"},{"type":"way","ref":631377343,"role":"outer"},{"type":"way","ref":631371237,"role":"outer"},{"type":"way","ref":631371236,"role":"outer"},{"type":"way","ref":631371234,"role":"outer"},{"type":"way","ref":631377344,"role":"outer"},{"type":"way","ref":631371232,"role":"outer"},{"type":"way","ref":631371231,"role":"outer"},{"type":"way","ref":315041263,"role":"outer"},{"type":"way","ref":631371228,"role":"outer"},{"type":"way","ref":631377341,"role":"outer"},{"type":"way","ref":315041261,"role":"outer"},{"type":"way","ref":631371223,"role":"outer"}],"tags":{"access":"yes","image:0":"https://i.imgur.com/VuzX5jW.jpg","image:1":"https://i.imgur.com/tPppmJG.jpg","image:2":"https://i.imgur.com/ecY3RER.jpg","image:3":"https://i.imgur.com/lr4FK6j.jpg","image:5":"https://i.imgur.com/uufEeE6.jpg","leisure":"nature_reserve","name":"Leiemeersen Noord","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen-noord/"}},{"type":"relation","id":8890076,"members":[{"type":"way","ref":640979982,"role":"outer"},{"type":"way","ref":640979978,"role":"outer"}],"tags":{"access":"yes","image:0":"https://i.imgur.com/SAAaKBH.jpg","image:1":"https://i.imgur.com/DGK9iBN.jpg","image:2":"https://i.imgur.com/bte1KJx.jpg","image:3":"https://i.imgur.com/f75Gxnx.jpg","leisure":"nature_reserve","name":"Gevaerts Noord","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon"}},{"type":"relation","id":9118029,"members":[{"type":"way","ref":606165130,"role":"outer"},{"type":"way","ref":655652319,"role":"outer"},{"type":"way","ref":655652321,"role":"outer"}],"tags":{"access":"no","image:0":"https://i.imgur.com/eBufo0v.jpg","image:1":"https://i.imgur.com/kBej2Nk.jpg","image:2":"https://i.imgur.com/QKoyIRl.jpg","leisure":"nature_reserve","name":"De Leiemeersen","note":"Door de hoge kwetsbaarheid van het gebied zijn De Leiemeersen enkel te bezoeken onder begeleiding van een gids","note:mapping":"NIET VOOR BEGINNENDE MAPPERS! Dit gebied is met relaties als multipolygonen gemapt (zo'n 50 stuks). Als je niet weet hoe dit werkt, vraag hulp.","note_1":"wetland=marsh is het veenmoerasgebied","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen/"}},{"type":"node","id":668981602,"lat":51.1588243,"lon":3.2558654,"timestamp":"2012-07-06T17:58:39Z","version":2,"changeset":12133044,"user":"martino260","uid":655442,"tags":{"amenity":"bench"}},{"type":"node","id":668981622,"lat":51.1565636,"lon":3.2549888,"timestamp":"2020-03-23T23:54:26Z","version":4,"changeset":82544029,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"no"}},{"type":"node","id":1580339675,"lat":51.1395949,"lon":3.3332507,"timestamp":"2012-01-07T00:44:42Z","version":1,"changeset":10317754,"user":"popaultje","uid":519184,"tags":{"amenity":"parking"}},{"type":"node","id":1764571836,"lat":51.1701118,"lon":3.3363371,"timestamp":"2012-05-24T21:06:50Z","version":1,"changeset":11693640,"user":"popaultje","uid":519184,"tags":{"amenity":"parking"}},{"type":"node","id":2121652779,"lat":51.1268536,"lon":3.3239607,"timestamp":"2013-01-20T20:12:50Z","version":1,"changeset":14725799,"user":"tomvdb","uid":437764,"tags":{"amenity":"parking"}},{"type":"node","id":2386053906,"lat":51.162161,"lon":3.263065,"timestamp":"2018-01-19T21:31:30Z","version":2,"changeset":55589374,"user":"L'imaginaire","uid":654234,"tags":{"amenity":"toilets"}},{"type":"node","id":2978180520,"lat":51.1329149,"lon":3.3362322,"timestamp":"2014-07-24T21:29:38Z","version":1,"changeset":24338416,"user":"pieterjanheyse","uid":254767,"tags":{"amenity":"bench"}},{"type":"node","id":2978183271,"lat":51.1324243,"lon":3.3373735,"timestamp":"2019-09-14T05:02:25Z","version":2,"changeset":74462201,"user":"JanFi","uid":672253,"tags":{"amenity":"bench"}},{"type":"node","id":2978184471,"lat":51.1436385,"lon":3.2916539,"timestamp":"2021-02-26T10:22:54Z","version":3,"changeset":100041319,"user":"s8evq","uid":3710738,"tags":{"amenity":"bench","backrest":"yes","check_date":"2021-02-26"}},{"type":"node","id":3925976407,"lat":51.1787486,"lon":3.2831866,"timestamp":"2019-08-12T19:48:31Z","version":2,"changeset":73281516,"user":"s8evq","uid":3710738,"tags":{"leisure":"picnic_table"}},{"type":"node","id":5158056232,"lat":51.1592067,"lon":3.2567111,"timestamp":"2021-04-17T16:21:52Z","version":5,"changeset":103110072,"user":"L'imaginaire","uid":654234,"tags":{"leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"no"}},{"type":"node","id":5718776382,"lat":51.1609023,"lon":3.2582509,"timestamp":"2021-10-18T10:27:50Z","version":3,"changeset":112646117,"user":"s8evq","uid":3710738,"tags":{"check_date":"2021-02-26","covered":"no","leisure":"picnic_table"}},{"type":"node","id":5718776383,"lat":51.1609488,"lon":3.2581877,"timestamp":"2021-10-18T10:27:50Z","version":3,"changeset":112646117,"user":"s8evq","uid":3710738,"tags":{"check_date":"2021-02-26","covered":"no","leisure":"picnic_table"}},{"type":"node","id":5745727100,"lat":51.1594639,"lon":3.2604304,"timestamp":"2018-07-07T18:00:29Z","version":1,"changeset":60494261,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench"}},{"type":"node","id":5745739587,"lat":51.1580397,"lon":3.263101,"timestamp":"2021-02-26T10:07:56Z","version":2,"changeset":100039706,"user":"s8evq","uid":3710738,"tags":{"check_date":"2021-02-26","leisure":"picnic_table"}},{"type":"node","id":5745739588,"lat":51.1580631,"lon":3.2630345,"timestamp":"2021-02-26T10:07:41Z","version":2,"changeset":100039706,"user":"s8evq","uid":3710738,"tags":{"check_date":"2021-02-26","leisure":"picnic_table"}},{"type":"node","id":5961596093,"lat":51.1588103,"lon":3.2633933,"timestamp":"2018-10-06T14:41:12Z","version":1,"changeset":63258667,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":5964032193,"lat":51.1514821,"lon":3.2723766,"timestamp":"2018-10-07T12:15:53Z","version":1,"changeset":63277533,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034563379,"lat":51.1421689,"lon":3.3022271,"timestamp":"2020-02-11T20:10:26Z","version":2,"changeset":80868434,"user":"s8evq","uid":3710738,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034564191,"lat":51.1722186,"lon":3.2823584,"timestamp":"2018-11-04T20:27:50Z","version":2,"changeset":64177431,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6034565298,"lat":51.1722796,"lon":3.282329,"timestamp":"2018-11-04T20:27:50Z","version":2,"changeset":64177431,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":6145151111,"lat":51.1690435,"lon":3.3388676,"timestamp":"2018-12-18T13:13:36Z","version":1,"changeset":65580728,"user":"Siel Nollet","uid":3292414,"tags":{"amenity":"bench"}},{"type":"node","id":6145151112,"lat":51.1690023,"lon":3.3388636,"timestamp":"2018-12-18T13:13:36Z","version":1,"changeset":65580728,"user":"Siel Nollet","uid":3292414,"tags":{"amenity":"bench"}},{"type":"node","id":6216549651,"lat":51.1292813,"lon":3.332369,"timestamp":"2019-01-17T16:29:02Z","version":1,"changeset":66400781,"user":"Nilsnn","uid":4652000,"tags":{"amenity":"bench"}},{"type":"node","id":6216549652,"lat":51.1292768,"lon":3.3324259,"timestamp":"2019-01-17T16:29:03Z","version":1,"changeset":66400781,"user":"Nilsnn","uid":4652000,"tags":{"amenity":"bench"}},{"type":"node","id":7204447030,"lat":51.1791769,"lon":3.283116,"timestamp":"2021-01-01T12:18:32Z","version":3,"changeset":96768203,"user":"L'imaginaire","uid":654234,"tags":{"board_type":"nature","information":"board","mapillary":"0BHVgU1XCyTMM9cjvidUqk","name":"De Assebroekse Meersen","tourism":"information"}},{"type":"node","id":7468175778,"lat":51.1344104,"lon":3.3348246,"timestamp":"2020-04-30T19:07:49Z","version":1,"changeset":84433940,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7602473480,"lat":51.1503874,"lon":3.2836867,"timestamp":"2020-06-08T10:39:07Z","version":1,"changeset":86349440,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"leisure":"picnic_table"}},{"type":"node","id":7602473482,"lat":51.150244,"lon":3.2842925,"timestamp":"2021-02-26T10:18:19Z","version":4,"changeset":100040859,"user":"s8evq","uid":3710738,"tags":{"board_type":"wildlife","information":"board","name":"Waterbeestjes","operator":"Natuurpunt Vallei van de Zuidleie","tourism":"information"}},{"type":"node","id":7602699080,"lat":51.1367031,"lon":3.3320712,"timestamp":"2020-06-08T12:08:11Z","version":1,"changeset":86353903,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"amenity":"bench","backrest":"yes","material":"metal"}},{"type":"node","id":7680940369,"lat":51.1380074,"lon":3.3369928,"timestamp":"2020-07-03T17:54:31Z","version":1,"changeset":87513827,"user":"L'imaginaire","uid":654234,"tags":{"amenity":"bench"}},{"type":"node","id":7726850522,"lat":51.1418585,"lon":3.3064234,"timestamp":"2020-07-18T15:57:43Z","version":1,"changeset":88179934,"user":"Pieter Vander Vennet","uid":3818858,"tags":{"image:0":"https://i.imgur.com/Bh6UjYy.jpg","information":"board","tourism":"information"}},{"type":"node","id":7727071212,"lat":51.1501173,"lon":3.2845352,"timestamp":"2021-02-26T10:17:39Z","version":2,"changeset":100040859,"user":"s8evq","uid":3710738,"tags":{"board_type":"wildlife","image:0":"https://i.imgur.com/mFEQJWd.jpg","information":"board","name":"Vleermuizen","operator":"Natuurpunt Vallei van de Zuidleie","tourism":"information"}},{"type":"node","id":9122376662,"lat":51.1720505,"lon":3.3308524,"timestamp":"2021-09-25T13:32:42Z","version":1,"changeset":111688164,"user":"TeamP8","uid":718373,"tags":{"amenity":"bench"}},{"type":"node","id":9425818876,"lat":51.1325315,"lon":3.3371616,"timestamp":"2022-01-28T20:13:29Z","version":3,"changeset":116721474,"user":"L'imaginaire","uid":654234,"tags":{"leisure":"picnic_table","mapillary":"101961548889238"}},{"type":"way","id":149408639,"timestamp":"2012-08-15T19:09:59Z","version":4,"changeset":12742790,"user":"tomvdb","uid":437764,"nodes":[1623924235,1623924236,1623924238,1864750831,1623924241,1623924235],"tags":{"amenity":"parking"}},{"type":"way","id":149553206,"timestamp":"2021-01-30T12:11:53Z","version":4,"changeset":98411765,"user":"L'imaginaire","uid":654234,"nodes":[1625199938,1625199951,8377691836,8378081366,8378081429,8378081386,1625199950,6414383775,1625199827,1625199938],"tags":{"amenity":"parking"}},{"type":"way","id":184402308,"timestamp":"2020-03-23T23:54:26Z","version":6,"changeset":82544029,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[1948836195,1948836194,1948836193,1948836189,1948836192,1948836195],"tags":{"building":"yes","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","wheelchair":"yes"}},{"type":"way","id":184402309,"timestamp":"2021-12-20T11:33:54Z","version":8,"changeset":115161990,"user":"s8evq","uid":3710738,"nodes":[1948836029,1948836038,1948836032,1948836025,1948836023,1948836029],"tags":{"building":"yes","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","source:geometry:date":"2011-11-07","source:geometry:ref":"Gbg/3489204"}},{"type":"way","id":184402331,"timestamp":"2021-02-26T10:03:17Z","version":4,"changeset":100039746,"user":"s8evq","uid":3710738,"nodes":[1948836104,1948836072,1948836068,1948836093,1948836104],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface"}},{"type":"way","id":236979353,"timestamp":"2021-01-25T12:38:22Z","version":4,"changeset":98122705,"user":"JosV","uid":170722,"nodes":[2449323191,7962681624,7962681623,7962681621,7962681622,2449323193,7962681620,7962681619,8360787098,4350143592,6794421028,6794421027,6794421041,7962681614,2123461969,2449323198,7962681615,7962681616,6794421042,2449323191],"tags":{"amenity":"parking"}},{"type":"way","id":251590503,"timestamp":"2019-04-20T22:04:54Z","version":2,"changeset":69412561,"user":"L'imaginaire","uid":654234,"nodes":[2577910543,2577910530,2577910542,2577910520,6418533335,2577910526,2577910545,2577910543],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":313090489,"timestamp":"2014-11-16T14:59:12Z","version":1,"changeset":26823403,"user":"JanFi","uid":672253,"nodes":[3190107423,3190107435,3190107442,3190107439,3190107432,3190107428,3190107424,3190107400,3190107393,3190107377,3190107371,3190107374,3190107408,3190107407,3190107415,3190107416,3190107420,3190107423],"tags":{"amenity":"parking"}},{"type":"way","id":314531418,"timestamp":"2021-05-30T17:02:49Z","version":21,"changeset":105576255,"user":"s8evq","uid":3710738,"nodes":[7468171455,7468171451,7727393212,7727393211,7727393210,7727393208,7727393209,8781449489,7727393207,7727393206,7727393205,7727393204,7727393203,7727393202,7727393201,7727393200,7390697550,7390697534,7727393199,7727393198,7727393197,7390697549,7727393196,7727393195,7727393194,7727393193,7727393192,7727393191,7727393190,7727393189,7727393188,7727393187,7727393186,7727393185,7727339384,7727339383,7727339382,1553169911,1553169836,1493821433,1493821422,3185248088,7727339364,7727339365,7727339366,7727339367,7727339368,7727339369,7727339370,7727339371,7727339372,7727339373,7727339374,7727339375,7727339376,7727339377,3185248049,3185248048,3185248042,3185248040,7727339378,7727339379,7727339380,7727339381,7468171438,7468171442,7468171430,7468171432,7468171446,7468171404,7468171405,7468171422,7468171426,7468171433,7468171423,7468171428,7468171431,7468171429,7468171406,7468171407,7468171444,7468171408,7468171409,7468171410,7468171411,7468171412,7468171413,7190927792,7190927791,7190927793,7190927787,7190927788,7190927789,7190927790,7190927786,7602692242,7190927785,7190873584,7468171450,7190873582,7190873576,7190873578,7190873577,7468171455],"tags":{"access":"yes","dog":"leashed","dogs":"leashed","image":"https://i.imgur.com/cOfwWTj.jpg","image:0":"https://i.imgur.com/RliQdyi.jpg","image:1":"https://i.imgur.com/IeKHahz.jpg","image:2":"https://i.imgur.com/1K0IORH.jpg","image:3":"https://i.imgur.com/jojP09s.jpg","image:4":"https://i.imgur.com/DK6kT51.jpg","image:5":"https://i.imgur.com/RizbGM1.jpg","image:6":"https://i.imgur.com/hyoY6Cl.jpg","image:7":"https://i.imgur.com/xDd7Wrq.jpg","leisure":"nature_reserve","name":"Miseriebocht","operator":"Natuurpunt Beernem","website":"https://www.natuurpunt.be/natuurgebied/miseriebocht","wikidata":"Q97060915"}},{"type":"way","id":366318480,"timestamp":"2015-08-18T13:50:59Z","version":1,"changeset":33415758,"user":"xras3r","uid":323672,"nodes":[3702926557,3702926558,3702926559,3702926560,3702926557],"tags":{"amenity":"parking"}},{"type":"way","id":366318481,"timestamp":"2015-08-18T13:50:59Z","version":1,"changeset":33415758,"user":"xras3r","uid":323672,"nodes":[3702878648,3702926561,3702926562,3702926563,3702926564,3702926565,3702926566,3702926567,3702878648],"tags":{"amenity":"parking"}},{"type":"way","id":366318482,"timestamp":"2021-01-05T09:04:32Z","version":2,"changeset":96964072,"user":"JosV","uid":170722,"nodes":[3702926568,8292789053,8292789054,3702878654,3702926568],"tags":{"amenity":"parking"}},{"type":"way","id":366320440,"timestamp":"2015-08-18T14:02:00Z","version":1,"changeset":33415981,"user":"xras3r","uid":323672,"nodes":[3702956173,3702956174,3702956175,3702956176,3702956173],"tags":{"amenity":"parking","name":"Kleine Beer"}},{"type":"way","id":366321706,"timestamp":"2015-08-18T14:15:30Z","version":1,"changeset":33416303,"user":"xras3r","uid":323672,"nodes":[3702969714,3702969715,3702969716,3702969717,3702969714],"tags":{"amenity":"parking"}},{"type":"way","id":480267681,"timestamp":"2021-01-05T21:32:23Z","version":2,"changeset":97006454,"user":"JosV","uid":170722,"nodes":[4732689641,4732689640,4732689639,4732689638,4732689637,4732689636,4732689635,4732689634,4732689633,4732689632,4732689631,4732689630,4732689629,4732689628,4732689627,4732689626,8294875888,4732689641],"tags":{"amenity":"parking"}},{"type":"way","id":554341620,"timestamp":"2018-01-19T21:31:30Z","version":1,"changeset":55589374,"user":"L'imaginaire","uid":654234,"nodes":[5349884603,5349884602,5349884601,5349884600,5349884603],"tags":{"amenity":"parking"}},{"type":"way","id":554341621,"timestamp":"2018-01-19T21:31:30Z","version":1,"changeset":55589374,"user":"L'imaginaire","uid":654234,"nodes":[5349884607,5349884606,5349884605,5349884604,5349884607],"tags":{"amenity":"parking"}},{"type":"way","id":561902092,"timestamp":"2021-01-17T14:52:46Z","version":6,"changeset":97640804,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[5417516023,5417515520,5417516021,5417516022,5417516023],"tags":{"building":"yes","image":"https://i.imgur.com/WmViSbL.jpg","leisure":"bird_hide","operator":"Natuurpunt Vallei van de Zuidleie","shelter":"yes","wheelchair":"no"}},{"type":"way","id":605915064,"timestamp":"2018-07-07T18:07:31Z","version":1,"changeset":60494380,"user":"Pieter Vander Vennet","uid":3818858,"nodes":[5745739924,5745739925,5745739926,5745739927,5745739924],"tags":{"amenity":"parking","surface":"fine2"}},{"type":"way","id":650285088,"timestamp":"2021-01-29T09:37:56Z","version":2,"changeset":98352073,"user":"JosV","uid":170722,"nodes":[3645188881,6100803131,6100803130,6100803129,6100803124,6100803125,6100803128,6100803127,6100803126,3645188881],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":655944574,"timestamp":"2020-02-23T22:05:54Z","version":2,"changeset":81377451,"user":"L'imaginaire","uid":654234,"nodes":[6145151107,6145151108,6145151109,6145151115,6145151114,6145151110,6145151107],"tags":{"amenity":"parking"}},{"type":"way","id":664171069,"timestamp":"2019-01-17T16:29:08Z","version":1,"changeset":66400781,"user":"Nilsnn","uid":4652000,"nodes":[6216549610,6216549611,6216549612,6216549613,1413470849,1413470848,6216549605,6216549604,6216549610],"tags":{"amenity":"parking"}},{"type":"way","id":664171076,"timestamp":"2021-01-09T21:56:37Z","version":3,"changeset":97230316,"user":"JosV","uid":170722,"nodes":[6216549656,6216549655,8307316294,6216549661,6216549657,6216549658,6216549659,6216549660,6216549656],"tags":{"amenity":"parking","capacity":"50"}},{"type":"way","id":665330334,"timestamp":"2019-01-22T14:20:51Z","version":1,"changeset":66539354,"user":"Nilsnn","uid":4652000,"nodes":[6227395993,6227395991,6227395992,6227395997,6227395993],"tags":{"access":"yes","amenity":"parking","fee":"no","parking":"surface","surface":"asphalt"}},{"type":"way","id":684598363,"timestamp":"2020-03-07T14:21:22Z","version":3,"changeset":81900556,"user":"L'imaginaire","uid":654234,"nodes":[3227068565,6416001161,6414352054,6414352055,7274233390,7274233391,7274233397,7274233395,7274233396,6414352053,3227068568,3227068565],"tags":{"amenity":"parking","fee":"no"}},{"type":"way","id":684599810,"timestamp":"2020-12-31T20:58:28Z","version":3,"changeset":96751036,"user":"JosV","uid":170722,"nodes":[1317838331,8279842668,1384096112,1317838328,6414374315,3227068446,6414374316,6414374317,6414374318,3227068456,6414374319,6414374320,6414374321,1317838317,1317838331],"tags":{"access":"no","amenity":"parking","operator":"Politie"}},{"type":"way","id":761474468,"timestamp":"2021-01-05T21:32:23Z","version":3,"changeset":97006454,"user":"JosV","uid":170722,"nodes":[7114502201,7114502203,7114502200,7114502202,3170562439,3170562437,3170562431,7114502240,7114502211,7114502212,7114502214,7114502215,7114502228,7114502234,7114502235,7114502236,7114502237,7114502238,7114502239,7114502233,7114502232,7114502231,7114502229,7114502230,7114502227,7114502226,7114502225,7114502216,7114502217,7114502224,3170562392,7114502218,3170562394,7114502219,7114502220,7114502221,7114502222,7114502223,3170562395,3170562396,3170562397,3170562402,3170562410,7114502209,7114502208,7114502207,7114502205,7114502206,3170562436,1475188519,1475188516,6627605025,8294886142,7114502201],"tags":{"image":"http://valleivandezuidleie.be/wp-content/uploads/2011/12/2011-03-24_G12_088_1_1.JPG","leisure":"nature_reserve","name":"Merlebeek-Meerberg","natural":"wetland","operator":"Natuurpunt Vallei van de Zuidleie","start_date":"2011","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/merlebeek/","wetland":"wet_meadow"}},{"type":"way","id":813859435,"timestamp":"2021-02-26T10:18:26Z","version":3,"changeset":100040879,"user":"s8evq","uid":3710738,"nodes":[7602479690,7459257985,7602479691,7459154784,7459154782,7602479692,5482441357,7602479693,7602479694,7602479695,7602479696,7602479690],"tags":{"access":"yes","image:0":"https://i.imgur.com/nb9nawa.jpg","landuse":"grass","leisure":"nature_reserve","name:signed":"no","natural":"grass","operator":"Natuurpunt Vallei van de Zuidleie"}},{"type":"way","id":826103452,"timestamp":"2020-07-14T09:05:14Z","version":1,"changeset":87965884,"user":"L'imaginaire","uid":654234,"nodes":[7713176912,7713176911,7713176910,7713176909,7713176912],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":893176022,"timestamp":"2021-01-07T23:02:07Z","version":1,"changeset":97133621,"user":"JosV","uid":170722,"nodes":[1927235214,8301349336,8301349335,8301349337,1927235214],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943476,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251887,8328251886,8328251885,8328251884,8328251887],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943477,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251891,8328251890,8328251889,8328251888,8328251891],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943478,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251895,8328251894,8328251893,8328251892,8328251895],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943479,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251897,8328251896,8328251901,8328251900,8328251897],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943480,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251898,8328251899,8328251903,8328251902,8328251898],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943481,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251907,8328251906,8328251905,8328251904,8328251907],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":895943482,"timestamp":"2021-01-16T17:56:50Z","version":1,"changeset":97614669,"user":"JosV","uid":170722,"nodes":[8328251911,8328251910,8328251909,8328251908,8328251911],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":901952767,"timestamp":"2021-01-30T12:19:15Z","version":1,"changeset":98412028,"user":"L'imaginaire","uid":654234,"nodes":[8378097127,8378097126,8378097125,8378097124,8378097127],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"way","id":901952768,"timestamp":"2021-01-30T12:19:15Z","version":1,"changeset":98412028,"user":"L'imaginaire","uid":654234,"nodes":[8378097134,8378097133,8378097132,8378097131,8378097130,8378097129,8378097128,8378097134],"tags":{"amenity":"parking","parking":"street_side"}},{"type":"way","id":947325182,"timestamp":"2021-05-26T15:16:06Z","version":1,"changeset":105366812,"user":"L'imaginaire","uid":654234,"nodes":[8497007549,8768981525,8768981522,8768981524,8768981521,8768981523,8768981520,8768981519,6206789709,8768981533,8497007549],"tags":{"amenity":"parking","parking":"surface"}},{"type":"way","id":966827815,"timestamp":"2021-07-23T20:01:09Z","version":1,"changeset":108511361,"user":"L'imaginaire","uid":654234,"nodes":[8945026757,8945026756,8945026755,8945026754,8945026757],"tags":{"access":"customers","amenity":"parking","parking":"surface"}},{"type":"relation","id":2589413,"timestamp":"2021-03-06T15:13:01Z","version":7,"changeset":100540347,"user":"Pieter Vander Vennet","uid":3818858,"members":[{"type":"way","ref":663050030,"role":"outer"},{"type":"way","ref":184402334,"role":"outer"},{"type":"way","ref":184402332,"role":"outer"},{"type":"way","ref":184402325,"role":"outer"},{"type":"way","ref":184402326,"role":"outer"},{"type":"way","ref":314899865,"role":"outer"},{"type":"way","ref":314956402,"role":"outer"}],"tags":{"access":"yes","image":"https://i.imgur.com/Yu4qHh5.jpg","leisure":"nature_reserve","name":"Warandeputten","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","wikipedia":"nl:Warandeputten"}},{"type":"relation","id":8782624,"timestamp":"2021-03-08T13:15:15Z","version":14,"changeset":100640534,"user":"M!dgard","uid":763799,"members":[{"type":"way","ref":315041273,"role":"outer"},{"type":"way","ref":631377343,"role":"outer"},{"type":"way","ref":631371237,"role":"outer"},{"type":"way","ref":631371236,"role":"outer"},{"type":"way","ref":631371234,"role":"outer"},{"type":"way","ref":631377344,"role":"outer"},{"type":"way","ref":631371232,"role":"outer"},{"type":"way","ref":631371231,"role":"outer"},{"type":"way","ref":315041263,"role":"outer"},{"type":"way","ref":631371228,"role":"outer"},{"type":"way","ref":631377341,"role":"outer"},{"type":"way","ref":315041261,"role":"outer"},{"type":"way","ref":631371223,"role":"outer"}],"tags":{"access":"yes","image:0":"https://i.imgur.com/VuzX5jW.jpg","image:1":"https://i.imgur.com/tPppmJG.jpg","image:2":"https://i.imgur.com/ecY3RER.jpg","image:3":"https://i.imgur.com/lr4FK6j.jpg","image:5":"https://i.imgur.com/uufEeE6.jpg","leisure":"nature_reserve","name":"Leiemeersen Noord","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen-noord/"}},{"type":"relation","id":8890076,"timestamp":"2020-07-18T15:56:50Z","version":6,"changeset":88179905,"user":"Pieter Vander Vennet","uid":3818858,"members":[{"type":"way","ref":640979982,"role":"outer"},{"type":"way","ref":640979978,"role":"outer"}],"tags":{"access":"yes","image:0":"https://i.imgur.com/SAAaKBH.jpg","image:1":"https://i.imgur.com/DGK9iBN.jpg","image:2":"https://i.imgur.com/bte1KJx.jpg","image:3":"https://i.imgur.com/f75Gxnx.jpg","leisure":"nature_reserve","name":"Gevaerts Noord","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon"}},{"type":"relation","id":9118029,"timestamp":"2020-07-11T12:04:16Z","version":4,"changeset":87852045,"user":"Pieter Vander Vennet","uid":3818858,"members":[{"type":"way","ref":606165130,"role":"outer"},{"type":"way","ref":655652319,"role":"outer"},{"type":"way","ref":655652321,"role":"outer"}],"tags":{"access":"no","image:0":"https://i.imgur.com/eBufo0v.jpg","image:1":"https://i.imgur.com/kBej2Nk.jpg","image:2":"https://i.imgur.com/QKoyIRl.jpg","leisure":"nature_reserve","name":"De Leiemeersen","note":"Door de hoge kwetsbaarheid van het gebied zijn De Leiemeersen enkel te bezoeken onder begeleiding van een gids","note:mapping":"NIET VOOR BEGINNENDE MAPPERS! Dit gebied is met relaties als multipolygonen gemapt (zo'n 50 stuks). Als je niet weet hoe dit werkt, vraag hulp.","note_1":"wetland=marsh is het veenmoerasgebied","operator":"Natuurpunt Vallei van de Zuidleie","type":"multipolygon","website":"http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen/"}},{"type":"node","id":7114502229,"lat":51.1340508,"lon":3.2949303},{"type":"node","id":7114502231,"lat":51.1340428,"lon":3.2959613},{"type":"node","id":7114502232,"lat":51.134096,"lon":3.2971973},{"type":"node","id":1927235214,"lat":51.1267391,"lon":3.3222921},{"type":"node","id":8301349336,"lat":51.1266966,"lon":3.3223356},{"type":"node","id":1413470848,"lat":51.1276558,"lon":3.3278194},{"type":"node","id":1413470849,"lat":51.1276852,"lon":3.3274627},{"type":"node","id":6216549605,"lat":51.1276578,"lon":3.3279376},{"type":"node","id":6216549611,"lat":51.1277354,"lon":3.3273608},{"type":"node","id":6216549612,"lat":51.1277281,"lon":3.3274215},{"type":"node","id":6216549613,"lat":51.1277062,"lon":3.3274249},{"type":"node","id":8301349335,"lat":51.1269096,"lon":3.3228882},{"type":"node","id":8301349337,"lat":51.126955,"lon":3.3228466},{"type":"node","id":8328251884,"lat":51.1278701,"lon":3.3253392},{"type":"node","id":8328251885,"lat":51.127857,"lon":3.3254078},{"type":"node","id":8328251888,"lat":51.1278797,"lon":3.325168},{"type":"node","id":8328251889,"lat":51.1278665,"lon":3.3252369},{"type":"node","id":8328251892,"lat":51.1271569,"lon":3.3248958},{"type":"node","id":8328251893,"lat":51.1272208,"lon":3.3252329},{"type":"node","id":8328251894,"lat":51.1272677,"lon":3.3252111},{"type":"node","id":8328251895,"lat":51.1272033,"lon":3.3248728},{"type":"node","id":8328251896,"lat":51.1271015,"lon":3.3257575},{"type":"node","id":8328251897,"lat":51.1270872,"lon":3.3256877},{"type":"node","id":8328251898,"lat":51.1271271,"lon":3.325744},{"type":"node","id":8328251899,"lat":51.1271128,"lon":3.3256743},{"type":"node","id":8328251900,"lat":51.1270462,"lon":3.3257067},{"type":"node","id":8328251901,"lat":51.1270609,"lon":3.3257756},{"type":"node","id":8328251902,"lat":51.1272095,"lon":3.3257043},{"type":"node","id":8328251903,"lat":51.1271957,"lon":3.3256351},{"type":"node","id":8328251904,"lat":51.1269631,"lon":3.3253915},{"type":"node","id":8328251905,"lat":51.1269756,"lon":3.3254617},{"type":"node","id":8328251906,"lat":51.1271678,"lon":3.3253747},{"type":"node","id":8328251907,"lat":51.1271557,"lon":3.3253045},{"type":"node","id":8328251908,"lat":51.1270115,"lon":3.3255467},{"type":"node","id":8328251909,"lat":51.1270235,"lon":3.3256138},{"type":"node","id":8328251910,"lat":51.1271099,"lon":3.3255749},{"type":"node","id":8328251911,"lat":51.1270975,"lon":3.3255072},{"type":"node","id":2449323191,"lat":51.1340437,"lon":3.3216558},{"type":"node","id":2449323193,"lat":51.134285,"lon":3.3221086},{"type":"node","id":2449323198,"lat":51.1334724,"lon":3.3223472},{"type":"node","id":4350143592,"lat":51.1344661,"lon":3.3226292},{"type":"node","id":4732689626,"lat":51.1301038,"lon":3.3223672},{"type":"node","id":4732689627,"lat":51.1301351,"lon":3.3223533},{"type":"node","id":4732689628,"lat":51.130161,"lon":3.3224805},{"type":"node","id":4732689629,"lat":51.1301379,"lon":3.3224524},{"type":"node","id":4732689630,"lat":51.130151,"lon":3.3225204},{"type":"node","id":4732689631,"lat":51.1301375,"lon":3.3225274},{"type":"node","id":4732689632,"lat":51.1301505,"lon":3.322593},{"type":"node","id":4732689634,"lat":51.1297694,"lon":3.3223954},{"type":"node","id":4732689635,"lat":51.1298034,"lon":3.3223641},{"type":"node","id":4732689636,"lat":51.1297546,"lon":3.3221883},{"type":"node","id":4732689637,"lat":51.1297088,"lon":3.3219708},{"type":"node","id":4732689638,"lat":51.1297683,"lon":3.3219402},{"type":"node","id":4732689639,"lat":51.1297781,"lon":3.3219034},{"type":"node","id":4732689640,"lat":51.1297694,"lon":3.3218478},{"type":"node","id":4732689641,"lat":51.1300088,"lon":3.32173},{"type":"node","id":6794421027,"lat":51.1341852,"lon":3.3223337},{"type":"node","id":6794421042,"lat":51.1336271,"lon":3.3220973},{"type":"node","id":7962681614,"lat":51.1336656,"lon":3.322624},{"type":"node","id":7962681615,"lat":51.1335301,"lon":3.322287},{"type":"node","id":7962681616,"lat":51.1335065,"lon":3.3222317},{"type":"node","id":7962681619,"lat":51.134306,"lon":3.3222136},{"type":"node","id":7962681620,"lat":51.1343186,"lon":3.3221995},{"type":"node","id":7962681621,"lat":51.1341453,"lon":3.321775},{"type":"node","id":7962681622,"lat":51.1341537,"lon":3.321769},{"type":"node","id":7962681623,"lat":51.1341067,"lon":3.3216811},{"type":"node","id":7962681624,"lat":51.1340675,"lon":3.3217193},{"type":"node","id":8294875888,"lat":51.1300197,"lon":3.3219403},{"type":"node","id":8360787098,"lat":51.1344583,"lon":3.3225918},{"type":"node","id":2123461969,"lat":51.1336123,"lon":3.3226786},{"type":"node","id":4732689633,"lat":51.1298776,"lon":3.3227393},{"type":"node","id":6216549604,"lat":51.1280095,"lon":3.328024},{"type":"node","id":6216549610,"lat":51.128134,"lon":3.3274542},{"type":"node","id":6794421028,"lat":51.1343476,"lon":3.3227429},{"type":"node","id":6794421041,"lat":51.1337382,"lon":3.3227794},{"type":"node","id":8328251886,"lat":51.128048,"lon":3.3255015},{"type":"node","id":8328251887,"lat":51.1280616,"lon":3.3254329},{"type":"node","id":8328251890,"lat":51.1280791,"lon":3.3253407},{"type":"node","id":8328251891,"lat":51.1280922,"lon":3.3252721},{"type":"node","id":1623924235,"lat":51.131361,"lon":3.333018},{"type":"node","id":1623924236,"lat":51.1311039,"lon":3.3325764},{"type":"node","id":1623924238,"lat":51.1310489,"lon":3.3326527},{"type":"node","id":1623924241,"lat":51.1314273,"lon":3.3331479},{"type":"node","id":1864750831,"lat":51.1313615,"lon":3.3332222},{"type":"node","id":6216549655,"lat":51.134416,"lon":3.3347284},{"type":"node","id":6216549656,"lat":51.1343187,"lon":3.3349542},{"type":"node","id":6216549657,"lat":51.1334196,"lon":3.335723},{"type":"node","id":6216549658,"lat":51.1334902,"lon":3.3357377},{"type":"node","id":6216549659,"lat":51.133716,"lon":3.335546},{"type":"node","id":6216549660,"lat":51.133883,"lon":3.3353885},{"type":"node","id":6216549661,"lat":51.133662,"lon":3.3355049},{"type":"node","id":8307316294,"lat":51.1338503,"lon":3.3353095},{"type":"node","id":1493821422,"lat":51.1320567,"lon":3.3398517},{"type":"node","id":1493821433,"lat":51.1316132,"lon":3.3408892},{"type":"node","id":1553169836,"lat":51.1311998,"lon":3.3415993},{"type":"node","id":3185248088,"lat":51.1323359,"lon":3.3389757},{"type":"node","id":7727339364,"lat":51.1321819,"lon":3.3388758},{"type":"node","id":7727339365,"lat":51.1319823,"lon":3.339559},{"type":"node","id":7727339366,"lat":51.1316011,"lon":3.3405485},{"type":"node","id":7727339367,"lat":51.1312764,"lon":3.3411848},{"type":"node","id":7727339368,"lat":51.1310889,"lon":3.3414676},{"type":"node","id":7727339369,"lat":51.1304838,"lon":3.3422395},{"type":"node","id":3185248048,"lat":51.1268693,"lon":3.3480331},{"type":"node","id":3185248049,"lat":51.1269831,"lon":3.3475504},{"type":"node","id":7727339376,"lat":51.1274147,"lon":3.3464515},{"type":"node","id":7727339377,"lat":51.1271283,"lon":3.3469817},{"type":"node","id":3185248040,"lat":51.1266778,"lon":3.3491476},{"type":"node","id":3185248042,"lat":51.126712,"lon":3.3489485},{"type":"node","id":7468171404,"lat":51.1277325,"lon":3.3553578},{"type":"node","id":7468171430,"lat":51.1270326,"lon":3.353594},{"type":"node","id":7468171432,"lat":51.1271802,"lon":3.3540454},{"type":"node","id":7468171438,"lat":51.1268064,"lon":3.3526001},{"type":"node","id":7468171442,"lat":51.1268303,"lon":3.3527746},{"type":"node","id":7468171446,"lat":51.1273659,"lon":3.3546333},{"type":"node","id":7727339378,"lat":51.1265656,"lon":3.3505324},{"type":"node","id":7727339379,"lat":51.1266463,"lon":3.350873},{"type":"node","id":7727339380,"lat":51.1266302,"lon":3.3514032},{"type":"node","id":7727339381,"lat":51.1267774,"lon":3.3523929},{"type":"node","id":7727393190,"lat":51.1276264,"lon":3.3489611},{"type":"node","id":7727393191,"lat":51.1274853,"lon":3.3497484},{"type":"node","id":7727393192,"lat":51.127332,"lon":3.350571},{"type":"node","id":7727393193,"lat":51.1273663,"lon":3.3514386},{"type":"node","id":7727393194,"lat":51.1274853,"lon":3.3519206},{"type":"node","id":7727393195,"lat":51.1276889,"lon":3.3531674},{"type":"node","id":1553169911,"lat":51.1313314,"lon":3.3425771},{"type":"node","id":7727339370,"lat":51.1300705,"lon":3.3427304},{"type":"node","id":7727339371,"lat":51.1293083,"lon":3.343643},{"type":"node","id":7727339372,"lat":51.1285642,"lon":3.3445074},{"type":"node","id":7727339373,"lat":51.1285702,"lon":3.3445781},{"type":"node","id":7727339374,"lat":51.1283181,"lon":3.3448801},{"type":"node","id":7727339375,"lat":51.1281023,"lon":3.3451404},{"type":"node","id":7727339382,"lat":51.1311196,"lon":3.3430952},{"type":"node","id":7727339383,"lat":51.1309343,"lon":3.3434996},{"type":"node","id":7727339384,"lat":51.1306234,"lon":3.343745},{"type":"node","id":7727393185,"lat":51.1300873,"lon":3.3443449},{"type":"node","id":7727393186,"lat":51.129401,"lon":3.3453846},{"type":"node","id":7727393187,"lat":51.1290865,"lon":3.345963},{"type":"node","id":7727393188,"lat":51.1285581,"lon":3.3468788},{"type":"node","id":7727393189,"lat":51.1280217,"lon":3.3479457},{"type":"node","id":7390697549,"lat":51.128429,"lon":3.354427},{"type":"node","id":7727393196,"lat":51.1279813,"lon":3.353749},{"type":"node","id":3209560114,"lat":51.1538874,"lon":3.2531858},{"type":"node","id":3209560115,"lat":51.1539649,"lon":3.2531836},{"type":"node","id":3209560116,"lat":51.1541197,"lon":3.2537986},{"type":"node","id":3209560117,"lat":51.1541021,"lon":3.253149},{"type":"node","id":3210389695,"lat":51.1539646,"lon":3.2534427},{"type":"node","id":416917618,"lat":51.1565737,"lon":3.2549365},{"type":"node","id":668981667,"lat":51.1576026,"lon":3.2555383},{"type":"node","id":668981668,"lat":51.1564046,"lon":3.2547045},{"type":"node","id":1815081998,"lat":51.1575578,"lon":3.2555439},{"type":"node","id":1948835662,"lat":51.155957,"lon":3.2562889},{"type":"node","id":1948835742,"lat":51.156182,"lon":3.2540291},{"type":"node","id":1948835950,"lat":51.1591027,"lon":3.2559292},{"type":"node","id":1948836096,"lat":51.1600831,"lon":3.2562989},{"type":"node","id":1948836149,"lat":51.1605378,"lon":3.2568906},{"type":"node","id":2026541837,"lat":51.1543652,"lon":3.2542901},{"type":"node","id":2026541841,"lat":51.1545903,"lon":3.2546809},{"type":"node","id":2026541843,"lat":51.1548384,"lon":3.2550342},{"type":"node","id":2026541846,"lat":51.155086,"lon":3.2553429},{"type":"node","id":2026541851,"lat":51.15565,"lon":3.2542965},{"type":"node","id":3209560118,"lat":51.1542244,"lon":3.2530815},{"type":"node","id":3209560119,"lat":51.1544741,"lon":3.252803},{"type":"node","id":3209560121,"lat":51.1546028,"lon":3.2526667},{"type":"node","id":3209560124,"lat":51.1547447,"lon":3.2525904},{"type":"node","id":3209560126,"lat":51.1550296,"lon":3.2525253},{"type":"node","id":3209560127,"lat":51.1551915,"lon":3.2525297},{"type":"node","id":3209560131,"lat":51.1553436,"lon":3.2525627},{"type":"node","id":3209560132,"lat":51.1554055,"lon":3.2526006},{"type":"node","id":3209560134,"lat":51.155479,"lon":3.2526651},{"type":"node","id":3209560135,"lat":51.155558,"lon":3.2527647},{"type":"node","id":3209560136,"lat":51.1556482,"lon":3.2528805},{"type":"node","id":3209560138,"lat":51.1559652,"lon":3.2534517},{"type":"node","id":3209560139,"lat":51.1560327,"lon":3.2535705},{"type":"node","id":5417515520,"lat":51.1545998,"lon":3.2545767},{"type":"node","id":5417516021,"lat":51.1545476,"lon":3.2544989},{"type":"node","id":5417516022,"lat":51.1545856,"lon":3.2544342},{"type":"node","id":5417516023,"lat":51.1546378,"lon":3.2545119},{"type":"node","id":6206789688,"lat":51.1553578,"lon":3.255668},{"type":"node","id":416917603,"lat":51.1591104,"lon":3.2595563},{"type":"node","id":416917605,"lat":51.1592829,"lon":3.2591123},{"type":"node","id":416917609,"lat":51.1597764,"lon":3.2597849},{"type":"node","id":1554514806,"lat":51.1586314,"lon":3.2636074},{"type":"node","id":1948835842,"lat":51.1570345,"lon":3.2574053},{"type":"node","id":1948835900,"lat":51.1583428,"lon":3.258761},{"type":"node","id":1948835986,"lat":51.1595419,"lon":3.2593891},{"type":"node","id":1948836023,"lat":51.1597284,"lon":3.2585312},{"type":"node","id":1948836025,"lat":51.1597477,"lon":3.2585581},{"type":"node","id":1948836029,"lat":51.1597506,"lon":3.2584887},{"type":"node","id":1948836032,"lat":51.159778,"lon":3.2585905},{"type":"node","id":1948836038,"lat":51.1597992,"lon":3.2585462},{"type":"node","id":1948836068,"lat":51.1598265,"lon":3.2595499},{"type":"node","id":1948836072,"lat":51.1598725,"lon":3.2596329},{"type":"node","id":1948836093,"lat":51.1600505,"lon":3.2592835},{"type":"node","id":1948836104,"lat":51.1601026,"lon":3.2593298},{"type":"node","id":1948836189,"lat":51.1606847,"lon":3.2577063},{"type":"node","id":5745739926,"lat":51.160615,"lon":3.2602336},{"type":"node","id":5747937642,"lat":51.1580603,"lon":3.2636348},{"type":"node","id":5962340003,"lat":51.1587851,"lon":3.2632739},{"type":"node","id":5962414276,"lat":51.158949,"lon":3.2635109},{"type":"node","id":5962415498,"lat":51.1589617,"lon":3.2635523},{"type":"node","id":5962415499,"lat":51.1589623,"lon":3.2635339},{"type":"node","id":5962415500,"lat":51.1589568,"lon":3.2635203},{"type":"node","id":5962415538,"lat":51.1589195,"lon":3.2636407},{"type":"node","id":6206789709,"lat":51.1545829,"lon":3.2585784},{"type":"node","id":8497007481,"lat":51.1588349,"lon":3.2633671},{"type":"node","id":8497007549,"lat":51.1548977,"lon":3.257909},{"type":"node","id":8768981519,"lat":51.1545715,"lon":3.2585859},{"type":"node","id":8768981520,"lat":51.1544691,"lon":3.2584642},{"type":"node","id":8768981521,"lat":51.1543652,"lon":3.2584253},{"type":"node","id":8768981522,"lat":51.1543648,"lon":3.2583401},{"type":"node","id":8768981523,"lat":51.1544439,"lon":3.2585178},{"type":"node","id":8768981524,"lat":51.1543892,"lon":3.258369},{"type":"node","id":8768981525,"lat":51.1547185,"lon":3.2575482},{"type":"node","id":8768981533,"lat":51.1545902,"lon":3.2585725},{"type":"node","id":3162627482,"lat":51.1513402,"lon":3.2701364},{"type":"node","id":3162627509,"lat":51.1517878,"lon":3.2696503},{"type":"node","id":5745963944,"lat":51.1524768,"lon":3.2700312},{"type":"node","id":5745963946,"lat":51.1519527,"lon":3.2698278},{"type":"node","id":5745963947,"lat":51.1513495,"lon":3.2697903},{"type":"node","id":5747937657,"lat":51.1538472,"lon":3.269262},{"type":"node","id":5747937658,"lat":51.1536592,"lon":3.2698471},{"type":"node","id":5747937668,"lat":51.1514068,"lon":3.2699149},{"type":"node","id":5747937669,"lat":51.1514278,"lon":3.26999},{"type":"node","id":5747937670,"lat":51.1514329,"lon":3.2701134},{"type":"node","id":5747937671,"lat":51.1514387,"lon":3.2702207},{"type":"node","id":5747937672,"lat":51.1515532,"lon":3.2702006},{"type":"node","id":5747937673,"lat":51.1515986,"lon":3.2701456},{"type":"node","id":5747937674,"lat":51.151872,"lon":3.2701201},{"type":"node","id":6142725039,"lat":51.1513841,"lon":3.2701799},{"type":"node","id":6142725060,"lat":51.1499168,"lon":3.2700268},{"type":"node","id":6142725061,"lat":51.1497438,"lon":3.2697269},{"type":"node","id":6142725064,"lat":51.1499256,"lon":3.2692437},{"type":"node","id":6142727594,"lat":51.1500369,"lon":3.2694178},{"type":"node","id":6142727597,"lat":51.1513713,"lon":3.2701053},{"type":"node","id":6143075014,"lat":51.1512852,"lon":3.2702041},{"type":"node","id":1554514655,"lat":51.1533288,"lon":3.2766885},{"type":"node","id":1554514811,"lat":51.1537898,"lon":3.2754789},{"type":"node","id":3211247019,"lat":51.1538788,"lon":3.2763248},{"type":"node","id":3211247021,"lat":51.1538471,"lon":3.2764673},{"type":"node","id":5745963922,"lat":51.1499683,"lon":3.2708447},{"type":"node","id":5745963942,"lat":51.1530892,"lon":3.2704581},{"type":"node","id":5745963943,"lat":51.1529125,"lon":3.2703349},{"type":"node","id":5747535101,"lat":51.1533021,"lon":3.2705247},{"type":"node","id":5747937659,"lat":51.1534063,"lon":3.2705828},{"type":"node","id":5747937675,"lat":51.1521092,"lon":3.2702998},{"type":"node","id":5747937676,"lat":51.1524591,"lon":3.2704822},{"type":"node","id":5747937677,"lat":51.1527839,"lon":3.2706727},{"type":"node","id":5747937678,"lat":51.1529841,"lon":3.2707276},{"type":"node","id":5747937679,"lat":51.15326,"lon":3.2708913},{"type":"node","id":5747937680,"lat":51.1535477,"lon":3.2712989},{"type":"node","id":5747937681,"lat":51.1536831,"lon":3.2717402},{"type":"node","id":5747937682,"lat":51.1536839,"lon":3.2721747},{"type":"node","id":5747937683,"lat":51.1536469,"lon":3.2724161},{"type":"node","id":5747937684,"lat":51.1534635,"lon":3.2730652},{"type":"node","id":5747937685,"lat":51.1540625,"lon":3.2735735},{"type":"node","id":5962339921,"lat":51.1535454,"lon":3.2761201},{"type":"node","id":5962496725,"lat":51.1538672,"lon":3.276374},{"type":"node","id":6142725040,"lat":51.1514014,"lon":3.2704388},{"type":"node","id":6142725041,"lat":51.1514255,"lon":3.2722713},{"type":"node","id":6142725042,"lat":51.1514044,"lon":3.2722821},{"type":"node","id":6142725043,"lat":51.1513617,"lon":3.2722639},{"type":"node","id":6142725044,"lat":51.1513141,"lon":3.2722165},{"type":"node","id":6142725045,"lat":51.1512346,"lon":3.2721372},{"type":"node","id":6142725046,"lat":51.1511314,"lon":3.2720304},{"type":"node","id":6142725047,"lat":51.1509926,"lon":3.2718816},{"type":"node","id":6142725048,"lat":51.1508222,"lon":3.2716946},{"type":"node","id":6142725049,"lat":51.1507329,"lon":3.2715909},{"type":"node","id":6142725050,"lat":51.1506561,"lon":3.2714915},{"type":"node","id":6142725051,"lat":51.1506027,"lon":3.2714102},{"type":"node","id":6142725052,"lat":51.1505293,"lon":3.2712712},{"type":"node","id":6142725053,"lat":51.1504912,"lon":3.2711826},{"type":"node","id":6142725054,"lat":51.1504464,"lon":3.2710615},{"type":"node","id":6142725055,"lat":51.1503588,"lon":3.2707712},{"type":"node","id":6142725056,"lat":51.1503432,"lon":3.2707336},{"type":"node","id":6142725057,"lat":51.1503035,"lon":3.2706647},{"type":"node","id":6142725058,"lat":51.1501763,"lon":3.2704663},{"type":"node","id":6142725059,"lat":51.1500465,"lon":3.270256},{"type":"node","id":6142725065,"lat":51.1511856,"lon":3.2703268},{"type":"node","id":6142725066,"lat":51.1510244,"lon":3.2705705},{"type":"node","id":6142725067,"lat":51.1509355,"lon":3.270823},{"type":"node","id":6142725074,"lat":51.149723,"lon":3.271666},{"type":"node","id":6142725076,"lat":51.1498322,"lon":3.2716555},{"type":"node","id":6142725077,"lat":51.1499403,"lon":3.2715209},{"type":"node","id":6142725078,"lat":51.1500262,"lon":3.2714548},{"type":"node","id":6142725079,"lat":51.1501462,"lon":3.2714359},{"type":"node","id":6142725080,"lat":51.1502306,"lon":3.2714572},{"type":"node","id":6142725081,"lat":51.1502676,"lon":3.2714288},{"type":"node","id":6142725084,"lat":51.1496118,"lon":3.2714056},{"type":"node","id":6142727585,"lat":51.1497709,"lon":3.271258},{"type":"node","id":6142727586,"lat":51.1496767,"lon":3.2713056},{"type":"node","id":6142727587,"lat":51.1498468,"lon":3.2711798},{"type":"node","id":6142727588,"lat":51.1500514,"lon":3.2703954},{"type":"node","id":6142727589,"lat":51.1503033,"lon":3.270777},{"type":"node","id":6142727590,"lat":51.1503141,"lon":3.2713705},{"type":"node","id":6142727591,"lat":51.1498992,"lon":3.2710354},{"type":"node","id":6142727592,"lat":51.1499461,"lon":3.270906},{"type":"node","id":6142727595,"lat":51.1505973,"lon":3.2702942},{"type":"node","id":6142727596,"lat":51.1508515,"lon":3.2706917},{"type":"node","id":6142727598,"lat":51.1504337,"lon":3.2712326},{"type":"node","id":6143074993,"lat":51.1501845,"lon":3.2706959},{"type":"node","id":6143075018,"lat":51.1508744,"lon":3.2707275},{"type":"node","id":8638721230,"lat":51.151458,"lon":3.2721756},{"type":"node","id":8638721239,"lat":51.1514715,"lon":3.2719839},{"type":"node","id":1554514618,"lat":51.1581789,"lon":3.2646401},{"type":"node","id":1554514658,"lat":51.1578742,"lon":3.2653982},{"type":"node","id":1554514750,"lat":51.1568056,"lon":3.2677508},{"type":"node","id":1554514755,"lat":51.1573435,"lon":3.2665694},{"type":"node","id":1554514831,"lat":51.1561729,"lon":3.2691965},{"type":"node","id":3211247042,"lat":51.1557516,"lon":3.2701591},{"type":"node","id":3211247054,"lat":51.1570321,"lon":3.2690614},{"type":"node","id":3211247058,"lat":51.1571866,"lon":3.2685962},{"type":"node","id":3211247059,"lat":51.1572111,"lon":3.2673633},{"type":"node","id":3211247060,"lat":51.1572915,"lon":3.2701354},{"type":"node","id":3211247261,"lat":51.157332,"lon":3.2685775},{"type":"node","id":3211247265,"lat":51.1575267,"lon":3.2694937},{"type":"node","id":3211247266,"lat":51.1576636,"lon":3.2677588},{"type":"node","id":3211247291,"lat":51.15873,"lon":3.2640973},{"type":"node","id":3211247328,"lat":51.1594831,"lon":3.2648765},{"type":"node","id":3211247331,"lat":51.1596428,"lon":3.2643669},{"type":"node","id":5747937641,"lat":51.1581621,"lon":3.2637783},{"type":"node","id":5747937643,"lat":51.1576288,"lon":3.2640519},{"type":"node","id":5747937644,"lat":51.1572672,"lon":3.2645347},{"type":"node","id":5747937645,"lat":51.1568533,"lon":3.2650349},{"type":"node","id":5747937646,"lat":51.1564933,"lon":3.2652213},{"type":"node","id":5747937647,"lat":51.1562275,"lon":3.2654708},{"type":"node","id":5747937648,"lat":51.1560046,"lon":3.2656894},{"type":"node","id":5747937649,"lat":51.1556867,"lon":3.2659965},{"type":"node","id":5747937650,"lat":51.1551778,"lon":3.2664082},{"type":"node","id":5747937651,"lat":51.1550608,"lon":3.2664739},{"type":"node","id":5747937652,"lat":51.1547446,"lon":3.2666805},{"type":"node","id":5747937653,"lat":51.1546747,"lon":3.2667583},{"type":"node","id":5747937654,"lat":51.1546066,"lon":3.266895},{"type":"node","id":5747937655,"lat":51.1544363,"lon":3.2672456},{"type":"node","id":5747937656,"lat":51.1543612,"lon":3.2677126},{"type":"node","id":5747937688,"lat":51.1553145,"lon":3.2701637},{"type":"node","id":5747937689,"lat":51.1564724,"lon":3.2675566},{"type":"node","id":5747937690,"lat":51.1580352,"lon":3.2640939},{"type":"node","id":5747937691,"lat":51.1581075,"lon":3.2639222},{"type":"node","id":5962338038,"lat":51.1580663,"lon":3.2649607},{"type":"node","id":5962340009,"lat":51.1585904,"lon":3.2644244},{"type":"node","id":5962340010,"lat":51.1579212,"lon":3.2658517},{"type":"node","id":5962340011,"lat":51.1575197,"lon":3.2676156},{"type":"node","id":5962340012,"lat":51.1573374,"lon":3.2674543},{"type":"node","id":5962340013,"lat":51.1574265,"lon":3.2698457},{"type":"node","id":5962415543,"lat":51.1591427,"lon":3.2638615},{"type":"node","id":5962415564,"lat":51.1563757,"lon":3.269274},{"type":"node","id":5962444841,"lat":51.1584746,"lon":3.2639653},{"type":"node","id":5962444846,"lat":51.1585428,"lon":3.2645259},{"type":"node","id":5962444849,"lat":51.1586783,"lon":3.2642185},{"type":"node","id":5962444850,"lat":51.1586559,"lon":3.2642708},{"type":"node","id":5962496715,"lat":51.1577157,"lon":3.2657479},{"type":"node","id":5962496718,"lat":51.1572172,"lon":3.2685923},{"type":"node","id":5962496719,"lat":51.1571618,"lon":3.2686709},{"type":"node","id":1554514640,"lat":51.1556541,"lon":3.2703864},{"type":"node","id":1554514735,"lat":51.1546949,"lon":3.273022},{"type":"node","id":1554514739,"lat":51.1552715,"lon":3.271391},{"type":"node","id":1554514744,"lat":51.154165,"lon":3.2744194},{"type":"node","id":3211247024,"lat":51.1541505,"lon":3.2756042},{"type":"node","id":3211247029,"lat":51.154764,"lon":3.2739293},{"type":"node","id":3211247032,"lat":51.1550284,"lon":3.2731777},{"type":"node","id":3211247034,"lat":51.1553184,"lon":3.2723493},{"type":"node","id":3211247036,"lat":51.1554324,"lon":3.2715569},{"type":"node","id":3211247037,"lat":51.1555577,"lon":3.2717139},{"type":"node","id":3211247039,"lat":51.1556614,"lon":3.2709968},{"type":"node","id":5747937686,"lat":51.1543123,"lon":3.272868},{"type":"node","id":5747937687,"lat":51.1551484,"lon":3.2706069},{"type":"node","id":5962415565,"lat":51.1558985,"lon":3.2703959},{"type":"node","id":3170562436,"lat":51.138924,"lon":3.2898032},{"type":"node","id":7114502205,"lat":51.1385818,"lon":3.2899019},{"type":"node","id":7114502206,"lat":51.1387244,"lon":3.289738},{"type":"node","id":1475188516,"lat":51.1392568,"lon":3.2899836},{"type":"node","id":1475188519,"lat":51.1391868,"lon":3.2900136},{"type":"node","id":3170562392,"lat":51.1364433,"lon":3.2910119},{"type":"node","id":3170562394,"lat":51.1368907,"lon":3.291708},{"type":"node","id":3170562395,"lat":51.137106,"lon":3.2924864},{"type":"node","id":3170562396,"lat":51.137265,"lon":3.2919875},{"type":"node","id":3170562397,"lat":51.1374825,"lon":3.2913695},{"type":"node","id":3170562402,"lat":51.1378658,"lon":3.2906394},{"type":"node","id":3170562410,"lat":51.1378512,"lon":3.2905949},{"type":"node","id":3170562431,"lat":51.138431,"lon":3.2910415},{"type":"node","id":3170562437,"lat":51.1389596,"lon":3.2905649},{"type":"node","id":3170562439,"lat":51.1391839,"lon":3.2903042},{"type":"node","id":6627605025,"lat":51.1393014,"lon":3.2899729},{"type":"node","id":7114502200,"lat":51.139377,"lon":3.2901873},{"type":"node","id":7114502201,"lat":51.1395801,"lon":3.2901445},{"type":"node","id":7114502202,"lat":51.1393183,"lon":3.290151},{"type":"node","id":7114502203,"lat":51.1395636,"lon":3.2902055},{"type":"node","id":7114502207,"lat":51.13835,"lon":3.2901225},{"type":"node","id":7114502208,"lat":51.138259,"lon":3.2902362},{"type":"node","id":7114502209,"lat":51.1381351,"lon":3.2903372},{"type":"node","id":7114502211,"lat":51.1376987,"lon":3.2921954},{"type":"node","id":7114502212,"lat":51.1373598,"lon":3.2927952},{"type":"node","id":7114502214,"lat":51.1364782,"lon":3.2933157},{"type":"node","id":7114502215,"lat":51.1356558,"lon":3.2938122},{"type":"node","id":7114502216,"lat":51.1364577,"lon":3.2922382},{"type":"node","id":7114502217,"lat":51.1365055,"lon":3.2917901},{"type":"node","id":7114502218,"lat":51.1369977,"lon":3.2911659},{"type":"node","id":7114502219,"lat":51.1367691,"lon":3.2924051},{"type":"node","id":7114502220,"lat":51.1367022,"lon":3.2927526},{"type":"node","id":7114502221,"lat":51.1367354,"lon":3.2929091},{"type":"node","id":7114502222,"lat":51.1367962,"lon":3.2930156},{"type":"node","id":7114502223,"lat":51.1369436,"lon":3.2928838},{"type":"node","id":7114502224,"lat":51.1365124,"lon":3.2914123},{"type":"node","id":7114502225,"lat":51.135804,"lon":3.2926995},{"type":"node","id":7114502226,"lat":51.1354385,"lon":3.2929897},{"type":"node","id":7114502227,"lat":51.135128,"lon":3.2935318},{"type":"node","id":7114502228,"lat":51.1352851,"lon":3.2947114},{"type":"node","id":7114502230,"lat":51.135254,"lon":3.2946241},{"type":"node","id":7114502234,"lat":51.1354035,"lon":3.2959064},{"type":"node","id":7114502235,"lat":51.1362234,"lon":3.2955042},{"type":"node","id":7114502236,"lat":51.1362859,"lon":3.2964578},{"type":"node","id":7114502240,"lat":51.1379417,"lon":3.2917937},{"type":"node","id":8294886142,"lat":51.1394294,"lon":3.290127},{"type":"node","id":7114502233,"lat":51.1354361,"lon":3.2966386},{"type":"node","id":7114502237,"lat":51.1362025,"lon":3.2964851},{"type":"node","id":7114502238,"lat":51.1362303,"lon":3.2969415},{"type":"node","id":7114502239,"lat":51.1355032,"lon":3.297292},{"type":"node","id":1494027348,"lat":51.1419125,"lon":3.3028989},{"type":"node","id":1494027349,"lat":51.1419929,"lon":3.3024731},{"type":"node","id":1951759298,"lat":51.1421456,"lon":3.3024503},{"type":"node","id":6037556099,"lat":51.1420961,"lon":3.3023333},{"type":"node","id":6037556100,"lat":51.142124,"lon":3.302398},{"type":"node","id":6037556101,"lat":51.1421476,"lon":3.3023664},{"type":"node","id":6037556102,"lat":51.1421686,"lon":3.3023966},{"type":"node","id":6037556103,"lat":51.1421392,"lon":3.3024395},{"type":"node","id":6037556104,"lat":51.1419363,"lon":3.3027154},{"type":"node","id":6037556128,"lat":51.1421612,"lon":3.3024776},{"type":"node","id":6037556129,"lat":51.1421598,"lon":3.3025191},{"type":"node","id":1554514647,"lat":51.1510862,"lon":3.2830051},{"type":"node","id":1554514679,"lat":51.1528746,"lon":3.2778124},{"type":"node","id":1554514683,"lat":51.152459,"lon":3.2789175},{"type":"node","id":1554514730,"lat":51.1514294,"lon":3.2817821},{"type":"node","id":1554514747,"lat":51.1515758,"lon":3.2812966},{"type":"node","id":1554514765,"lat":51.1518248,"lon":3.2805268},{"type":"node","id":1554514773,"lat":51.15167,"lon":3.280964},{"type":"node","id":1554514775,"lat":51.1517205,"lon":3.2808218},{"type":"node","id":3211246555,"lat":51.1516922,"lon":3.2825734},{"type":"node","id":3211246995,"lat":51.1531241,"lon":3.2787307},{"type":"node","id":3211246997,"lat":51.1532256,"lon":3.2781298},{"type":"node","id":3211247004,"lat":51.1532551,"lon":3.2780324},{"type":"node","id":3211247008,"lat":51.1532769,"lon":3.2780262},{"type":"node","id":3211247010,"lat":51.1533547,"lon":3.2780182},{"type":"node","id":3211247013,"lat":51.1534072,"lon":3.2778679},{"type":"node","id":5962338069,"lat":51.1531224,"lon":3.2785269},{"type":"node","id":5962338070,"lat":51.1532454,"lon":3.2781435},{"type":"node","id":5962338071,"lat":51.1531839,"lon":3.2785716},{"type":"node","id":9284562682,"lat":51.153154,"lon":3.2784189},{"type":"node","id":1554514649,"lat":51.1502146,"lon":3.285505},{"type":"node","id":1554514661,"lat":51.1499067,"lon":3.2861781},{"type":"node","id":1554514682,"lat":51.150038,"lon":3.2859179},{"type":"node","id":1554514693,"lat":51.1498077,"lon":3.2862946},{"type":"node","id":1554514703,"lat":51.1497368,"lon":3.2863685},{"type":"node","id":1554514726,"lat":51.1498748,"lon":3.2862129},{"type":"node","id":1554514783,"lat":51.1497118,"lon":3.2864368},{"type":"node","id":1554514787,"lat":51.1499524,"lon":3.2860908},{"type":"node","id":1554514789,"lat":51.1506254,"lon":3.2845029},{"type":"node","id":3203029856,"lat":51.1498994,"lon":3.2867119},{"type":"node","id":3211246542,"lat":51.1505988,"lon":3.2851597},{"type":"node","id":5482441357,"lat":51.1504615,"lon":3.2836968},{"type":"node","id":7459154782,"lat":51.1501541,"lon":3.2834044},{"type":"node","id":7459154784,"lat":51.14999,"lon":3.2838049},{"type":"node","id":7459257985,"lat":51.1496077,"lon":3.2846412},{"type":"node","id":7602479690,"lat":51.150018,"lon":3.2848987},{"type":"node","id":7602479691,"lat":51.149806,"lon":3.2842251},{"type":"node","id":7602479692,"lat":51.1504686,"lon":3.2836785},{"type":"node","id":7602479693,"lat":51.1504309,"lon":3.2838118},{"type":"node","id":7602479694,"lat":51.1504255,"lon":3.2838072},{"type":"node","id":7602479695,"lat":51.1503423,"lon":3.2840392},{"type":"node","id":7602479696,"lat":51.1501698,"lon":3.2845196},{"type":"node","id":7727406921,"lat":51.1497539,"lon":3.2864986},{"type":"node","id":8945026754,"lat":51.1518736,"lon":3.2973911},{"type":"node","id":8945026755,"lat":51.1521646,"lon":3.297599},{"type":"node","id":8945026756,"lat":51.1522042,"lon":3.2974333},{"type":"node","id":8945026757,"lat":51.1519287,"lon":3.2972449},{"type":"node","id":416917612,"lat":51.1610842,"lon":3.2579973},{"type":"node","id":1948836192,"lat":51.1607104,"lon":3.2576119},{"type":"node","id":1948836193,"lat":51.1607138,"lon":3.2577265},{"type":"node","id":1948836194,"lat":51.1607261,"lon":3.2576817},{"type":"node","id":1948836195,"lat":51.1607396,"lon":3.2576322},{"type":"node","id":1948836202,"lat":51.1608376,"lon":3.2582648},{"type":"node","id":1948836209,"lat":51.1608764,"lon":3.2583297},{"type":"node","id":1948836218,"lat":51.1609914,"lon":3.2580679},{"type":"node","id":1948836237,"lat":51.1610295,"lon":3.2581432},{"type":"node","id":1948836277,"lat":51.1610699,"lon":3.2580833},{"type":"node","id":5349884600,"lat":51.1623355,"lon":3.2627383},{"type":"node","id":5349884601,"lat":51.1621681,"lon":3.2630327},{"type":"node","id":5349884602,"lat":51.1622043,"lon":3.2630816},{"type":"node","id":5349884603,"lat":51.162375,"lon":3.2627913},{"type":"node","id":5349884604,"lat":51.1622594,"lon":3.2633478},{"type":"node","id":5349884605,"lat":51.1621955,"lon":3.2632528},{"type":"node","id":5349884606,"lat":51.1623824,"lon":3.2629335},{"type":"node","id":5349884607,"lat":51.1624462,"lon":3.2630285},{"type":"node","id":5745739924,"lat":51.1608677,"lon":3.2604028},{"type":"node","id":5745739925,"lat":51.160749,"lon":3.2605301},{"type":"node","id":5745739927,"lat":51.160719,"lon":3.2600865},{"type":"node","id":1494027341,"lat":51.1418334,"lon":3.3035292},{"type":"node","id":1494027359,"lat":51.1413757,"lon":3.3074828},{"type":"node","id":1494027374,"lat":51.14132,"lon":3.3086261},{"type":"node","id":1494027376,"lat":51.1415625,"lon":3.3055864},{"type":"node","id":1494027385,"lat":51.1414514,"lon":3.3065628},{"type":"node","id":1494027386,"lat":51.1416927,"lon":3.3044972},{"type":"node","id":1494027389,"lat":51.1412677,"lon":3.3093509},{"type":"node","id":6037556130,"lat":51.1420599,"lon":3.303249},{"type":"node","id":6037556131,"lat":51.1419859,"lon":3.3038109},{"type":"node","id":6037556132,"lat":51.1418857,"lon":3.3046747},{"type":"node","id":6037556133,"lat":51.1418916,"lon":3.3050562},{"type":"node","id":6037556134,"lat":51.141927,"lon":3.3054665},{"type":"node","id":6037556135,"lat":51.1419362,"lon":3.3056543},{"type":"node","id":6037556136,"lat":51.1419369,"lon":3.3060001},{"type":"node","id":6037556137,"lat":51.1419328,"lon":3.3061768},{"type":"node","id":6037556138,"lat":51.1419139,"lon":3.3062337},{"type":"node","id":6037556139,"lat":51.1419032,"lon":3.3062932},{"type":"node","id":6037556140,"lat":51.1419001,"lon":3.3065944},{"type":"node","id":6037556141,"lat":51.1419262,"lon":3.3066872},{"type":"node","id":6037556142,"lat":51.1419119,"lon":3.3074563},{"type":"node","id":6037556143,"lat":51.1419027,"lon":3.3079359},{"type":"node","id":6037556144,"lat":51.1418924,"lon":3.3090764},{"type":"node","id":6037556145,"lat":51.1418843,"lon":3.3094015},{"type":"node","id":1494027342,"lat":51.1411958,"lon":3.3123073},{"type":"node","id":1494027346,"lat":51.1411509,"lon":3.3144776},{"type":"node","id":1494027353,"lat":51.141193,"lon":3.3127209},{"type":"node","id":1494027361,"lat":51.1411944,"lon":3.3118627},{"type":"node","id":1494027366,"lat":51.1411788,"lon":3.3132011},{"type":"node","id":1494027368,"lat":51.1411457,"lon":3.3148322},{"type":"node","id":1494027372,"lat":51.1412147,"lon":3.3112199},{"type":"node","id":1494027373,"lat":51.1411641,"lon":3.3142758},{"type":"node","id":1494027378,"lat":51.1412256,"lon":3.3114828},{"type":"node","id":1494027382,"lat":51.1412174,"lon":3.3105548},{"type":"node","id":6037556098,"lat":51.1411339,"lon":3.315984},{"type":"node","id":6037556146,"lat":51.1418276,"lon":3.309789},{"type":"node","id":6037556148,"lat":51.1418583,"lon":3.3096011},{"type":"node","id":6037556149,"lat":51.1418039,"lon":3.3098357},{"type":"node","id":6037556150,"lat":51.1417743,"lon":3.3098805},{"type":"node","id":6037556151,"lat":51.1416977,"lon":3.3101207},{"type":"node","id":6037556152,"lat":51.1416834,"lon":3.3102232},{"type":"node","id":6037556153,"lat":51.1416823,"lon":3.3102705},{"type":"node","id":6037556154,"lat":51.1416302,"lon":3.3105557},{"type":"node","id":6037556155,"lat":51.1416042,"lon":3.3107844},{"type":"node","id":6037556156,"lat":51.1415929,"lon":3.3110547},{"type":"node","id":6037556157,"lat":51.1415904,"lon":3.3118741},{"type":"node","id":6037556158,"lat":51.1415817,"lon":3.3145778},{"type":"node","id":6037556159,"lat":51.14157,"lon":3.3146421},{"type":"node","id":6037556160,"lat":51.1415557,"lon":3.3146975},{"type":"node","id":6037556161,"lat":51.1415541,"lon":3.3148416},{"type":"node","id":6037556162,"lat":51.1415551,"lon":3.3149739},{"type":"node","id":6037556163,"lat":51.1415802,"lon":3.3150635},{"type":"node","id":1494027343,"lat":51.1410607,"lon":3.3204128},{"type":"node","id":1494027365,"lat":51.141078,"lon":3.3193848},{"type":"node","id":1494027369,"lat":51.1410588,"lon":3.3212214},{"type":"node","id":1494027380,"lat":51.1410813,"lon":3.317924},{"type":"node","id":6037556105,"lat":51.141131,"lon":3.31628},{"type":"node","id":6037556106,"lat":51.1411581,"lon":3.3165005},{"type":"node","id":6037556107,"lat":51.141174,"lon":3.3167222},{"type":"node","id":6037556108,"lat":51.1411155,"lon":3.3169939},{"type":"node","id":6037556109,"lat":51.1410508,"lon":3.3221866},{"type":"node","id":6037556110,"lat":51.1410676,"lon":3.3223383},{"type":"node","id":6037556164,"lat":51.1415792,"lon":3.3164565},{"type":"node","id":6037556165,"lat":51.141573,"lon":3.3172357},{"type":"node","id":6037556166,"lat":51.1415771,"lon":3.3182023},{"type":"node","id":6037556167,"lat":51.1415495,"lon":3.3182625},{"type":"node","id":6037556168,"lat":51.1415495,"lon":3.3185605},{"type":"node","id":6037556169,"lat":51.1415684,"lon":3.3186069},{"type":"node","id":6037556170,"lat":51.1415633,"lon":3.3207654},{"type":"node","id":6037556171,"lat":51.1415444,"lon":3.3214131},{"type":"node","id":6037556172,"lat":51.1415097,"lon":3.3218544},{"type":"node","id":6037556173,"lat":51.1414642,"lon":3.3225703},{"type":"node","id":1493821404,"lat":51.1410663,"lon":3.3233361},{"type":"node","id":1494027355,"lat":51.1410465,"lon":3.3227899},{"type":"node","id":1801373604,"lat":51.1410713,"lon":3.3237592},{"type":"node","id":1951759314,"lat":51.1411466,"lon":3.3245554},{"type":"node","id":2474247506,"lat":51.1411643,"lon":3.3245589},{"type":"node","id":6037556111,"lat":51.1410551,"lon":3.3241916},{"type":"node","id":6037556112,"lat":51.1410504,"lon":3.3243751},{"type":"node","id":6037556118,"lat":51.1410331,"lon":3.3245183},{"type":"node","id":6037556120,"lat":51.1411823,"lon":3.3245635},{"type":"node","id":6037556174,"lat":51.1414121,"lon":3.3230419},{"type":"node","id":6037556175,"lat":51.1413953,"lon":3.3230745},{"type":"node","id":6037556176,"lat":51.1413682,"lon":3.323164},{"type":"node","id":6037556177,"lat":51.1413426,"lon":3.3233187},{"type":"node","id":6037556178,"lat":51.1413353,"lon":3.3234317},{"type":"node","id":6037556179,"lat":51.1413527,"lon":3.3235128},{"type":"node","id":6037556180,"lat":51.141294,"lon":3.3239374},{"type":"node","id":3702878648,"lat":51.1401344,"lon":3.3337214},{"type":"node","id":3702878654,"lat":51.1395918,"lon":3.3338166},{"type":"node","id":3702926557,"lat":51.1404241,"lon":3.333592},{"type":"node","id":3702926558,"lat":51.1406229,"lon":3.3335608},{"type":"node","id":3702926559,"lat":51.1406259,"lon":3.3336496},{"type":"node","id":3702926560,"lat":51.1404301,"lon":3.333676},{"type":"node","id":3702926561,"lat":51.1403789,"lon":3.3336784},{"type":"node","id":3702926562,"lat":51.1403729,"lon":3.3335752},{"type":"node","id":3702926563,"lat":51.1403021,"lon":3.333592},{"type":"node","id":3702926564,"lat":51.1402991,"lon":3.3335295},{"type":"node","id":3702926565,"lat":51.1402253,"lon":3.3335392},{"type":"node","id":3702926566,"lat":51.1402283,"lon":3.3336112},{"type":"node","id":3702926567,"lat":51.1401304,"lon":3.3336285},{"type":"node","id":3702926568,"lat":51.1400111,"lon":3.3337421},{"type":"node","id":3702956173,"lat":51.1406298,"lon":3.3321023},{"type":"node","id":3702956174,"lat":51.140115,"lon":3.3325162},{"type":"node","id":3702956175,"lat":51.1400702,"lon":3.3323734},{"type":"node","id":3702956176,"lat":51.1405896,"lon":3.3319784},{"type":"node","id":3702969714,"lat":51.1404452,"lon":3.3324432},{"type":"node","id":3702969715,"lat":51.1402175,"lon":3.3326312},{"type":"node","id":3702969716,"lat":51.1401965,"lon":3.3325686},{"type":"node","id":3702969717,"lat":51.1404269,"lon":3.3323835},{"type":"node","id":7713176909,"lat":51.1387744,"lon":3.3335489},{"type":"node","id":7713176910,"lat":51.1389364,"lon":3.3334306},{"type":"node","id":7713176911,"lat":51.1387319,"lon":3.3327211},{"type":"node","id":7713176912,"lat":51.1385501,"lon":3.332864},{"type":"node","id":8292789053,"lat":51.1400165,"lon":3.3338082},{"type":"node","id":8292789054,"lat":51.1396286,"lon":3.333874},{"type":"node","id":8378097124,"lat":51.140324,"lon":3.3355884},{"type":"node","id":8378097125,"lat":51.1403179,"lon":3.3355055},{"type":"node","id":8378097126,"lat":51.1406041,"lon":3.3354515},{"type":"node","id":8378097127,"lat":51.1406102,"lon":3.3355344},{"type":"node","id":8378097131,"lat":51.14074,"lon":3.3354785},{"type":"node","id":8378097132,"lat":51.1407879,"lon":3.3354717},{"type":"node","id":8378097133,"lat":51.1407993,"lon":3.3356206},{"type":"node","id":1317838317,"lat":51.1372361,"lon":3.338639},{"type":"node","id":1317838328,"lat":51.1369619,"lon":3.3386232},{"type":"node","id":1317838331,"lat":51.1371806,"lon":3.3384037},{"type":"node","id":1384096112,"lat":51.1371794,"lon":3.3384971},{"type":"node","id":1625199827,"lat":51.1408446,"lon":3.3375722},{"type":"node","id":1625199950,"lat":51.1407563,"lon":3.3373232},{"type":"node","id":3227068446,"lat":51.1369433,"lon":3.3387663},{"type":"node","id":3227068456,"lat":51.1371252,"lon":3.3388226},{"type":"node","id":3227068565,"lat":51.1371943,"lon":3.3389942},{"type":"node","id":3227068568,"lat":51.1372698,"lon":3.3389467},{"type":"node","id":6227395991,"lat":51.1392741,"lon":3.3367141},{"type":"node","id":6227395992,"lat":51.1392236,"lon":3.3367356},{"type":"node","id":6227395993,"lat":51.139313,"lon":3.3369193},{"type":"node","id":6227395997,"lat":51.1392584,"lon":3.3369405},{"type":"node","id":6414352053,"lat":51.1374056,"lon":3.3395184},{"type":"node","id":6414352054,"lat":51.1371278,"lon":3.3390236},{"type":"node","id":6414352055,"lat":51.1372589,"lon":3.3395769},{"type":"node","id":6414374315,"lat":51.1369166,"lon":3.338648},{"type":"node","id":6414374316,"lat":51.1369683,"lon":3.3388767},{"type":"node","id":6414374317,"lat":51.1370053,"lon":3.3388543},{"type":"node","id":6414374318,"lat":51.1371169,"lon":3.3387872},{"type":"node","id":6414374319,"lat":51.1371271,"lon":3.3388309},{"type":"node","id":6414374320,"lat":51.137238,"lon":3.3387628},{"type":"node","id":6414374321,"lat":51.1372074,"lon":3.338651},{"type":"node","id":6414383775,"lat":51.1407766,"lon":3.3373855},{"type":"node","id":6416001161,"lat":51.1371631,"lon":3.3390024},{"type":"node","id":7274233390,"lat":51.1372936,"lon":3.3395562},{"type":"node","id":7274233391,"lat":51.137309,"lon":3.3396231},{"type":"node","id":7274233395,"lat":51.1374325,"lon":3.3396124},{"type":"node","id":7274233396,"lat":51.137422,"lon":3.3395803},{"type":"node","id":7274233397,"lat":51.1373776,"lon":3.3396587},{"type":"node","id":8279842668,"lat":51.1371603,"lon":3.3384139},{"type":"node","id":8377691836,"lat":51.1409346,"lon":3.337164},{"type":"node","id":8378081366,"lat":51.140887,"lon":3.3372065},{"type":"node","id":8378081386,"lat":51.1407701,"lon":3.3373106},{"type":"node","id":8378081429,"lat":51.1408204,"lon":3.3372657},{"type":"node","id":8378097128,"lat":51.1407837,"lon":3.3359331},{"type":"node","id":8378097129,"lat":51.1407736,"lon":3.3358962},{"type":"node","id":8378097130,"lat":51.1407627,"lon":3.3358224},{"type":"node","id":8378097134,"lat":51.1408355,"lon":3.3359157},{"type":"node","id":1625199938,"lat":51.1411535,"lon":3.3372917},{"type":"node","id":1625199951,"lat":51.1410675,"lon":3.3370397},{"type":"node","id":6100803125,"lat":51.1672329,"lon":3.3331872},{"type":"node","id":6100803128,"lat":51.1672258,"lon":3.3333568},{"type":"node","id":2577910530,"lat":51.1685713,"lon":3.3356917},{"type":"node","id":2577910542,"lat":51.1681925,"lon":3.3356582},{"type":"node","id":2577910543,"lat":51.1685972,"lon":3.3356499},{"type":"node","id":3645188881,"lat":51.1679055,"lon":3.3337545},{"type":"node","id":6100803124,"lat":51.1673065,"lon":3.3331116},{"type":"node","id":6100803126,"lat":51.1672788,"lon":3.3336938},{"type":"node","id":6100803127,"lat":51.1672758,"lon":3.3333629},{"type":"node","id":6100803129,"lat":51.1675567,"lon":3.3331312},{"type":"node","id":6100803130,"lat":51.1675525,"lon":3.3333096},{"type":"node","id":6100803131,"lat":51.1679171,"lon":3.3333454},{"type":"node","id":2577910520,"lat":51.1681983,"lon":3.3360484},{"type":"node","id":2577910526,"lat":51.1687824,"lon":3.3362878},{"type":"node","id":2577910545,"lat":51.16885,"lon":3.3360431},{"type":"node","id":6145151107,"lat":51.1694008,"lon":3.3386946},{"type":"node","id":6145151108,"lat":51.1688319,"lon":3.3385737},{"type":"node","id":6145151109,"lat":51.1688258,"lon":3.3386467},{"type":"node","id":6145151110,"lat":51.1693942,"lon":3.3387681},{"type":"node","id":6145151114,"lat":51.1691188,"lon":3.3387104},{"type":"node","id":6145151115,"lat":51.1688569,"lon":3.3386537},{"type":"node","id":6418533335,"lat":51.1685948,"lon":3.3361624},{"type":"node","id":3190107371,"lat":51.1741471,"lon":3.3384365},{"type":"node","id":3190107374,"lat":51.1741746,"lon":3.3388275},{"type":"node","id":3190107377,"lat":51.1741968,"lon":3.3384276},{"type":"node","id":3190107393,"lat":51.1743618,"lon":3.3383982},{"type":"node","id":3190107400,"lat":51.1744043,"lon":3.3384036},{"type":"node","id":3190107407,"lat":51.1744809,"lon":3.3386827},{"type":"node","id":3190107408,"lat":51.1744875,"lon":3.3387716},{"type":"node","id":3190107415,"lat":51.1746495,"lon":3.3386505},{"type":"node","id":3190107416,"lat":51.1746548,"lon":3.3387211},{"type":"node","id":3190107420,"lat":51.1747424,"lon":3.3387044},{"type":"node","id":3190107423,"lat":51.1747681,"lon":3.3390477},{"type":"node","id":3190107424,"lat":51.1747706,"lon":3.3384763},{"type":"node","id":3190107428,"lat":51.1747993,"lon":3.3384858},{"type":"node","id":3190107432,"lat":51.1748198,"lon":3.3387594},{"type":"node","id":3190107435,"lat":51.1750772,"lon":3.3389888},{"type":"node","id":3190107439,"lat":51.1752356,"lon":3.3387751},{"type":"node","id":3190107442,"lat":51.1752492,"lon":3.338956},{"type":"node","id":7390697534,"lat":51.1297479,"lon":3.3578326},{"type":"node","id":7390697550,"lat":51.1297706,"lon":3.3588263},{"type":"node","id":7468171405,"lat":51.1279967,"lon":3.3559306},{"type":"node","id":7468171406,"lat":51.1288847,"lon":3.3598888},{"type":"node","id":7468171407,"lat":51.1288383,"lon":3.3606949},{"type":"node","id":7468171408,"lat":51.128796,"lon":3.3619912},{"type":"node","id":7468171422,"lat":51.1285744,"lon":3.3574498},{"type":"node","id":7468171423,"lat":51.1288319,"lon":3.3586353},{"type":"node","id":7468171426,"lat":51.1287158,"lon":3.3579031},{"type":"node","id":7468171428,"lat":51.1288787,"lon":3.3588438},{"type":"node","id":7468171429,"lat":51.1288942,"lon":3.3595607},{"type":"node","id":7468171431,"lat":51.1288954,"lon":3.3592459},{"type":"node","id":7468171433,"lat":51.1287831,"lon":3.3582062},{"type":"node","id":7468171444,"lat":51.1287924,"lon":3.361646},{"type":"node","id":7727393197,"lat":51.1288566,"lon":3.3555067},{"type":"node","id":7727393198,"lat":51.1292196,"lon":3.356551},{"type":"node","id":7727393199,"lat":51.129399,"lon":3.3566378},{"type":"node","id":7190873584,"lat":51.1287267,"lon":3.3682604},{"type":"node","id":7190927785,"lat":51.1287057,"lon":3.3681343},{"type":"node","id":7190927786,"lat":51.1287121,"lon":3.3673045},{"type":"node","id":7190927787,"lat":51.1286419,"lon":3.3641562},{"type":"node","id":7190927788,"lat":51.1286407,"lon":3.3644331},{"type":"node","id":7190927789,"lat":51.128655,"lon":3.3650286},{"type":"node","id":7190927790,"lat":51.128688,"lon":3.3661227},{"type":"node","id":7190927791,"lat":51.1286681,"lon":3.3635132},{"type":"node","id":7190927792,"lat":51.1286863,"lon":3.3631709},{"type":"node","id":7190927793,"lat":51.1286419,"lon":3.3639077},{"type":"node","id":7468171409,"lat":51.1288502,"lon":3.3621534},{"type":"node","id":7468171410,"lat":51.1288454,"lon":3.3623212},{"type":"node","id":7468171411,"lat":51.1287924,"lon":3.36282},{"type":"node","id":7468171412,"lat":51.1287335,"lon":3.363023},{"type":"node","id":7468171413,"lat":51.1286942,"lon":3.363003},{"type":"node","id":7602692242,"lat":51.1287128,"lon":3.3675239},{"type":"node","id":7727393200,"lat":51.129492,"lon":3.3624712},{"type":"node","id":7727393201,"lat":51.1294385,"lon":3.3629631},{"type":"node","id":7727393202,"lat":51.1293629,"lon":3.3638402},{"type":"node","id":7727393203,"lat":51.1293586,"lon":3.3662748},{"type":"node","id":7727393204,"lat":51.1292824,"lon":3.3662748},{"type":"node","id":7727393205,"lat":51.1292881,"lon":3.3667997},{"type":"node","id":7727393206,"lat":51.1293315,"lon":3.3682391},{"type":"node","id":7190873576,"lat":51.1287907,"lon":3.36923},{"type":"node","id":7190873577,"lat":51.1289144,"lon":3.369348},{"type":"node","id":7190873578,"lat":51.1288472,"lon":3.3692931},{"type":"node","id":7190873582,"lat":51.1287752,"lon":3.3691898},{"type":"node","id":7468171450,"lat":51.1287442,"lon":3.3685677},{"type":"node","id":7468171451,"lat":51.1290764,"lon":3.3693851},{"type":"node","id":7468171455,"lat":51.1289835,"lon":3.3693823},{"type":"node","id":7727393207,"lat":51.1293768,"lon":3.368773},{"type":"node","id":7727393208,"lat":51.1293116,"lon":3.3692219},{"type":"node","id":7727393209,"lat":51.1293718,"lon":3.368916},{"type":"node","id":7727393210,"lat":51.1292845,"lon":3.3692843},{"type":"node","id":7727393211,"lat":51.1292517,"lon":3.3693264},{"type":"node","id":7727393212,"lat":51.1291932,"lon":3.3693491},{"type":"node","id":8781449489,"lat":51.1293725,"lon":3.3688971},{"type":"way","id":640979978,"nodes":[6037556099,6037556100,6037556101,6037556102,6037556103,1951759298,6037556128,6037556129,6037556130,6037556131,6037556132,6037556133,6037556134,6037556135,6037556136,6037556137,6037556138,6037556139,6037556140,6037556141,6037556142,6037556143,6037556144,6037556145,6037556148,6037556146,6037556149,6037556150,6037556151,6037556152,6037556153,6037556154,6037556155,6037556156,6037556157,6037556158,6037556159,6037556160,6037556161,6037556162,6037556163,6037556164,6037556165,6037556166,6037556167,6037556168,6037556169,6037556170,6037556171,6037556172,6037556173,6037556174,6037556175,6037556176,6037556177,6037556178,6037556179,6037556180,6037556120,2474247506,1951759314,6037556118]},{"type":"way","id":640979982,"nodes":[6037556099,1494027349,6037556104,1494027348,1494027341,1494027386,1494027376,1494027385,1494027359,1494027374,1494027389,1494027382,1494027372,1494027378,1494027361,1494027342,1494027353,1494027366,1494027373,1494027346,1494027368,6037556098,6037556105,6037556106,6037556107,6037556108,1494027380,1494027365,1494027343,1494027369,6037556109,6037556110,1494027355,1493821404,1801373604,6037556111,6037556112,6037556118]},{"type":"way","id":184402325,"nodes":[1948835900,416917603,416917605,1948835986,416917609,1948836072,1948836068,1948836093,1948836104,1948836209,1948836202,1948836218,1948836237,1948836277,416917612,1948836149,1948836096,1948835950]},{"type":"way","id":184402326,"nodes":[1948835900,1948835842,1948835662]},{"type":"way","id":314899865,"nodes":[2026541851,2026541846,2026541843,2026541841,2026541837,3209560116,3210389695,3209560114,3209560115,3209560117,3209560118,3209560119,3209560121,3209560124,3209560126,3209560127,3209560131,3209560132,3209560134,3209560135,3209560136,3209560138]},{"type":"way","id":315041273,"nodes":[1554514739,1554514640,3211247042,1554514831,1554514750,1554514755,5962496715,1554514658,5962338038,1554514618,5962444841,1554514806,5962340003,8497007481,5962414276,5962415500,5962415499,5962415498,5962415538,5962415543,3211247331,3211247328,3211247291,5962444849,5962444850,5962340009,5962444846,5962340010,3211247059,5962340012,5962340011,3211247266,3211247261,5962496718,3211247058,5962496719,3211247054,3211247265,5962340013,3211247060,5962415564,5962415565]},{"type":"way","id":606165130,"nodes":[5747937641,5747937642,5747937643,5747937644,5747937645,5747937646,5747937647,5747937648,5747937649,5747937650,5747937651,5747937652,5747937653,5747937654,5747937655,5747937656,5747937657,5747937658,5747937659,5747535101,5745963942,5745963943,5745963944,5745963946,3162627509,5745963947,5747937668,5747937669,5747937670,5747937671,5747937672,5747937673,5747937674,5747937675,5747937676,5747937677,5747937678,5747937679,5747937680,5747937681,5747937682,5747937683,5747937684,5747937685,5747937686,5747937687,5747937688,5747937689,5747937690,5747937691,5747937641]},{"type":"way","id":184402332,"nodes":[668981668,416917618,1815081998,668981667,1948835950]},{"type":"way","id":184402334,"nodes":[6206789688,1948835742,668981668]},{"type":"way","id":314956402,"nodes":[2026541851,3209560139,3209560138]},{"type":"way","id":663050030,"nodes":[1948835662,6206789688]},{"type":"way","id":631371232,"nodes":[3211247019,5962496725]},{"type":"way","id":631377344,"nodes":[5962496725,3211247021]},{"type":"way","id":655652321,"nodes":[6142727588,6143074993,6142727589,6142727598,6142727590,6142725081,6142725080,6142725079,6142725078,6142725077,6142725076,6142725074,6142725084,6142727586,6142727585,6142727587,6142727591,6142727592,5745963922,6142727588]},{"type":"way","id":631371231,"nodes":[3211247019,3211247024]},{"type":"way","id":631371234,"nodes":[3211247013,3211247021]},{"type":"way","id":631377343,"nodes":[1554514739,1554514735,1554514744,1554514811,5962339921,1554514655,1554514679,1554514683,1554514765,1554514775,1554514773,1554514747,1554514730,1554514647,1554514789,1554514649,1554514682,1554514787,1554514661,1554514726,1554514693,1554514703,1554514783]},{"type":"way","id":655652319,"nodes":[6142725039,6142725040,8638721239,8638721230,6142725041,6142725042,6142725043,6142725044,6142725045,6142725046,6142725047,6142725048,6142725049,6142725050,6142725051,6142725052,6142725053,6142725054,6142725055,6142725056,6142725057,6142725058,6142725059,6142725060,6142725061,6142725064,6142727594,6142727595,6142727596,6143075018,6142725067,6142725066,6142725065,6143075014,3162627482,6142727597,6142725039]},{"type":"way","id":315041261,"nodes":[3211247039,3211247036]},{"type":"way","id":315041263,"nodes":[3211247032,3211247029,3211247024]},{"type":"way","id":631371223,"nodes":[5962415565,3211247039]},{"type":"way","id":631371228,"nodes":[3211247034,3211247032]},{"type":"way","id":631377341,"nodes":[3211247036,3211247037,3211247034]},{"type":"way","id":631371236,"nodes":[3211247013,3211247010,3211247008,3211247004,3211246997,5962338070,9284562682,5962338069,5962338071,3211246995]},{"type":"way","id":631371237,"nodes":[3211246995,3211246555,3211246542,3203029856,7727406921,1554514783]}]} + "https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%5Btimeout%3A60%5D%5Bbbox%3A51.124212757826875%2C3.251953125%2C51.17934297928927%2C3.33984375%5D%3B" + + query, + { + version: 0.6, + generator: "Overpass API 0.7.57 93a4d346", + osm3s: { + timestamp_osm_base: "2022-02-14T00:02:14Z", + copyright: + "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.", + }, + elements: [ + { + type: "node", + id: 668981602, + lat: 51.1588243, + lon: 3.2558654, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 668981622, + lat: 51.1565636, + lon: 3.2549888, + tags: { + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "no", + }, + }, + { + type: "node", + id: 1580339675, + lat: 51.1395949, + lon: 3.3332507, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 1764571836, + lat: 51.1701118, + lon: 3.3363371, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 2121652779, + lat: 51.1268536, + lon: 3.3239607, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 2386053906, + lat: 51.162161, + lon: 3.263065, + tags: { amenity: "toilets" }, + }, + { + type: "node", + id: 2978180520, + lat: 51.1329149, + lon: 3.3362322, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2978183271, + lat: 51.1324243, + lon: 3.3373735, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2978184471, + lat: 51.1436385, + lon: 3.2916539, + tags: { amenity: "bench", backrest: "yes", check_date: "2021-02-26" }, + }, + { + type: "node", + id: 3925976407, + lat: 51.1787486, + lon: 3.2831866, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 5158056232, + lat: 51.1592067, + lon: 3.2567111, + tags: { + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "no", + }, + }, + { + type: "node", + id: 5718776382, + lat: 51.1609023, + lon: 3.2582509, + tags: { check_date: "2021-02-26", covered: "no", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5718776383, + lat: 51.1609488, + lon: 3.2581877, + tags: { check_date: "2021-02-26", covered: "no", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5745727100, + lat: 51.1594639, + lon: 3.2604304, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745739587, + lat: 51.1580397, + lon: 3.263101, + tags: { check_date: "2021-02-26", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5745739588, + lat: 51.1580631, + lon: 3.2630345, + tags: { check_date: "2021-02-26", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5961596093, + lat: 51.1588103, + lon: 3.2633933, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 5964032193, + lat: 51.1514821, + lon: 3.2723766, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034563379, + lat: 51.1421689, + lon: 3.3022271, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034564191, + lat: 51.1722186, + lon: 3.2823584, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034565298, + lat: 51.1722796, + lon: 3.282329, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6145151111, + lat: 51.1690435, + lon: 3.3388676, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6145151112, + lat: 51.1690023, + lon: 3.3388636, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6216549651, + lat: 51.1292813, + lon: 3.332369, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6216549652, + lat: 51.1292768, + lon: 3.3324259, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7204447030, + lat: 51.1791769, + lon: 3.283116, + tags: { + board_type: "nature", + information: "board", + mapillary: "0BHVgU1XCyTMM9cjvidUqk", + name: "De Assebroekse Meersen", + tourism: "information", + }, + }, + { + type: "node", + id: 7468175778, + lat: 51.1344104, + lon: 3.3348246, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7602473480, + lat: 51.1503874, + lon: 3.2836867, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7602473482, + lat: 51.150244, + lon: 3.2842925, + tags: { + board_type: "wildlife", + information: "board", + name: "Waterbeestjes", + operator: "Natuurpunt Vallei van de Zuidleie", + tourism: "information", + }, + }, + { + type: "node", + id: 7602699080, + lat: 51.1367031, + lon: 3.3320712, + tags: { amenity: "bench", backrest: "yes", material: "metal" }, + }, + { + type: "node", + id: 7680940369, + lat: 51.1380074, + lon: 3.3369928, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7726850522, + lat: 51.1418585, + lon: 3.3064234, + tags: { + "image:0": "https://i.imgur.com/Bh6UjYy.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 7727071212, + lat: 51.1501173, + lon: 3.2845352, + tags: { + board_type: "wildlife", + "image:0": "https://i.imgur.com/mFEQJWd.jpg", + information: "board", + name: "Vleermuizen", + operator: "Natuurpunt Vallei van de Zuidleie", + tourism: "information", + }, + }, + { + type: "node", + id: 9122376662, + lat: 51.1720505, + lon: 3.3308524, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 9425818876, + lat: 51.1325315, + lon: 3.3371616, + tags: { leisure: "picnic_table", mapillary: "101961548889238" }, + }, + { + type: "way", + id: 149408639, + nodes: [1623924235, 1623924236, 1623924238, 1864750831, 1623924241, 1623924235], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 149553206, + nodes: [ + 1625199938, 1625199951, 8377691836, 8378081366, 8378081429, 8378081386, + 1625199950, 6414383775, 1625199827, 1625199938, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 184402308, + nodes: [1948836195, 1948836194, 1948836193, 1948836189, 1948836192, 1948836195], + tags: { + building: "yes", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + wheelchair: "yes", + }, + }, + { + type: "way", + id: 184402309, + nodes: [1948836029, 1948836038, 1948836032, 1948836025, 1948836023, 1948836029], + tags: { + building: "yes", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + "source:geometry:date": "2011-11-07", + "source:geometry:ref": "Gbg/3489204", + }, + }, + { + type: "way", + id: 184402331, + nodes: [1948836104, 1948836072, 1948836068, 1948836093, 1948836104], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 236979353, + nodes: [ + 2449323191, 7962681624, 7962681623, 7962681621, 7962681622, 2449323193, + 7962681620, 7962681619, 8360787098, 4350143592, 6794421028, 6794421027, + 6794421041, 7962681614, 2123461969, 2449323198, 7962681615, 7962681616, + 6794421042, 2449323191, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 251590503, + nodes: [ + 2577910543, 2577910530, 2577910542, 2577910520, 6418533335, 2577910526, + 2577910545, 2577910543, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 313090489, + nodes: [ + 3190107423, 3190107435, 3190107442, 3190107439, 3190107432, 3190107428, + 3190107424, 3190107400, 3190107393, 3190107377, 3190107371, 3190107374, + 3190107408, 3190107407, 3190107415, 3190107416, 3190107420, 3190107423, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 314531418, + nodes: [ + 7468171455, 7468171451, 7727393212, 7727393211, 7727393210, 7727393208, + 7727393209, 8781449489, 7727393207, 7727393206, 7727393205, 7727393204, + 7727393203, 7727393202, 7727393201, 7727393200, 7390697550, 7390697534, + 7727393199, 7727393198, 7727393197, 7390697549, 7727393196, 7727393195, + 7727393194, 7727393193, 7727393192, 7727393191, 7727393190, 7727393189, + 7727393188, 7727393187, 7727393186, 7727393185, 7727339384, 7727339383, + 7727339382, 1553169911, 1553169836, 1493821433, 1493821422, 3185248088, + 7727339364, 7727339365, 7727339366, 7727339367, 7727339368, 7727339369, + 7727339370, 7727339371, 7727339372, 7727339373, 7727339374, 7727339375, + 7727339376, 7727339377, 3185248049, 3185248048, 3185248042, 3185248040, + 7727339378, 7727339379, 7727339380, 7727339381, 7468171438, 7468171442, + 7468171430, 7468171432, 7468171446, 7468171404, 7468171405, 7468171422, + 7468171426, 7468171433, 7468171423, 7468171428, 7468171431, 7468171429, + 7468171406, 7468171407, 7468171444, 7468171408, 7468171409, 7468171410, + 7468171411, 7468171412, 7468171413, 7190927792, 7190927791, 7190927793, + 7190927787, 7190927788, 7190927789, 7190927790, 7190927786, 7602692242, + 7190927785, 7190873584, 7468171450, 7190873582, 7190873576, 7190873578, + 7190873577, 7468171455, + ], + tags: { + access: "yes", + dog: "leashed", + dogs: "leashed", + image: "https://i.imgur.com/cOfwWTj.jpg", + "image:0": "https://i.imgur.com/RliQdyi.jpg", + "image:1": "https://i.imgur.com/IeKHahz.jpg", + "image:2": "https://i.imgur.com/1K0IORH.jpg", + "image:3": "https://i.imgur.com/jojP09s.jpg", + "image:4": "https://i.imgur.com/DK6kT51.jpg", + "image:5": "https://i.imgur.com/RizbGM1.jpg", + "image:6": "https://i.imgur.com/hyoY6Cl.jpg", + "image:7": "https://i.imgur.com/xDd7Wrq.jpg", + leisure: "nature_reserve", + name: "Miseriebocht", + operator: "Natuurpunt Beernem", + website: "https://www.natuurpunt.be/natuurgebied/miseriebocht", + wikidata: "Q97060915", + }, + }, + { + type: "way", + id: 366318480, + nodes: [3702926557, 3702926558, 3702926559, 3702926560, 3702926557], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366318481, + nodes: [ + 3702878648, 3702926561, 3702926562, 3702926563, 3702926564, 3702926565, + 3702926566, 3702926567, 3702878648, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366318482, + nodes: [3702926568, 8292789053, 8292789054, 3702878654, 3702926568], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366320440, + nodes: [3702956173, 3702956174, 3702956175, 3702956176, 3702956173], + tags: { amenity: "parking", name: "Kleine Beer" }, + }, + { + type: "way", + id: 366321706, + nodes: [3702969714, 3702969715, 3702969716, 3702969717, 3702969714], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 480267681, + nodes: [ + 4732689641, 4732689640, 4732689639, 4732689638, 4732689637, 4732689636, + 4732689635, 4732689634, 4732689633, 4732689632, 4732689631, 4732689630, + 4732689629, 4732689628, 4732689627, 4732689626, 8294875888, 4732689641, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 554341620, + nodes: [5349884603, 5349884602, 5349884601, 5349884600, 5349884603], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 554341621, + nodes: [5349884607, 5349884606, 5349884605, 5349884604, 5349884607], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 561902092, + nodes: [5417516023, 5417515520, 5417516021, 5417516022, 5417516023], + tags: { + building: "yes", + image: "https://i.imgur.com/WmViSbL.jpg", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + wheelchair: "no", + }, + }, + { + type: "way", + id: 605915064, + nodes: [5745739924, 5745739925, 5745739926, 5745739927, 5745739924], + tags: { amenity: "parking", surface: "fine2" }, + }, + { + type: "way", + id: 650285088, + nodes: [ + 3645188881, 6100803131, 6100803130, 6100803129, 6100803124, 6100803125, + 6100803128, 6100803127, 6100803126, 3645188881, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 655944574, + nodes: [ + 6145151107, 6145151108, 6145151109, 6145151115, 6145151114, 6145151110, + 6145151107, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 664171069, + nodes: [ + 6216549610, 6216549611, 6216549612, 6216549613, 1413470849, 1413470848, + 6216549605, 6216549604, 6216549610, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 664171076, + nodes: [ + 6216549656, 6216549655, 8307316294, 6216549661, 6216549657, 6216549658, + 6216549659, 6216549660, 6216549656, + ], + tags: { amenity: "parking", capacity: "50" }, + }, + { + type: "way", + id: 665330334, + nodes: [6227395993, 6227395991, 6227395992, 6227395997, 6227395993], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + parking: "surface", + surface: "asphalt", + }, + }, + { + type: "way", + id: 684598363, + nodes: [ + 3227068565, 6416001161, 6414352054, 6414352055, 7274233390, 7274233391, + 7274233397, 7274233395, 7274233396, 6414352053, 3227068568, 3227068565, + ], + tags: { amenity: "parking", fee: "no" }, + }, + { + type: "way", + id: 684599810, + nodes: [ + 1317838331, 8279842668, 1384096112, 1317838328, 6414374315, 3227068446, + 6414374316, 6414374317, 6414374318, 3227068456, 6414374319, 6414374320, + 6414374321, 1317838317, 1317838331, + ], + tags: { access: "no", amenity: "parking", operator: "Politie" }, + }, + { + type: "way", + id: 761474468, + nodes: [ + 7114502201, 7114502203, 7114502200, 7114502202, 3170562439, 3170562437, + 3170562431, 7114502240, 7114502211, 7114502212, 7114502214, 7114502215, + 7114502228, 7114502234, 7114502235, 7114502236, 7114502237, 7114502238, + 7114502239, 7114502233, 7114502232, 7114502231, 7114502229, 7114502230, + 7114502227, 7114502226, 7114502225, 7114502216, 7114502217, 7114502224, + 3170562392, 7114502218, 3170562394, 7114502219, 7114502220, 7114502221, + 7114502222, 7114502223, 3170562395, 3170562396, 3170562397, 3170562402, + 3170562410, 7114502209, 7114502208, 7114502207, 7114502205, 7114502206, + 3170562436, 1475188519, 1475188516, 6627605025, 8294886142, 7114502201, + ], + tags: { + image: "http://valleivandezuidleie.be/wp-content/uploads/2011/12/2011-03-24_G12_088_1_1.JPG", + leisure: "nature_reserve", + name: "Merlebeek-Meerberg", + natural: "wetland", + operator: "Natuurpunt Vallei van de Zuidleie", + start_date: "2011", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/merlebeek/", + wetland: "wet_meadow", + }, + }, + { + type: "way", + id: 813859435, + nodes: [ + 7602479690, 7459257985, 7602479691, 7459154784, 7459154782, 7602479692, + 5482441357, 7602479693, 7602479694, 7602479695, 7602479696, 7602479690, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/nb9nawa.jpg", + landuse: "grass", + leisure: "nature_reserve", + "name:signed": "no", + natural: "grass", + operator: "Natuurpunt Vallei van de Zuidleie", + }, + }, + { + type: "way", + id: 826103452, + nodes: [7713176912, 7713176911, 7713176910, 7713176909, 7713176912], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 893176022, + nodes: [1927235214, 8301349336, 8301349335, 8301349337, 1927235214], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943476, + nodes: [8328251887, 8328251886, 8328251885, 8328251884, 8328251887], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943477, + nodes: [8328251891, 8328251890, 8328251889, 8328251888, 8328251891], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943478, + nodes: [8328251895, 8328251894, 8328251893, 8328251892, 8328251895], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943479, + nodes: [8328251897, 8328251896, 8328251901, 8328251900, 8328251897], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943480, + nodes: [8328251898, 8328251899, 8328251903, 8328251902, 8328251898], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943481, + nodes: [8328251907, 8328251906, 8328251905, 8328251904, 8328251907], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943482, + nodes: [8328251911, 8328251910, 8328251909, 8328251908, 8328251911], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 901952767, + nodes: [8378097127, 8378097126, 8378097125, 8378097124, 8378097127], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "way", + id: 901952768, + nodes: [ + 8378097134, 8378097133, 8378097132, 8378097131, 8378097130, 8378097129, + 8378097128, 8378097134, + ], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "way", + id: 947325182, + nodes: [ + 8497007549, 8768981525, 8768981522, 8768981524, 8768981521, 8768981523, + 8768981520, 8768981519, 6206789709, 8768981533, 8497007549, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 966827815, + nodes: [8945026757, 8945026756, 8945026755, 8945026754, 8945026757], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "relation", + id: 2589413, + members: [ + { type: "way", ref: 663050030, role: "outer" }, + { type: "way", ref: 184402334, role: "outer" }, + { type: "way", ref: 184402332, role: "outer" }, + { type: "way", ref: 184402325, role: "outer" }, + { type: "way", ref: 184402326, role: "outer" }, + { type: "way", ref: 314899865, role: "outer" }, + { type: "way", ref: 314956402, role: "outer" }, + ], + tags: { + access: "yes", + image: "https://i.imgur.com/Yu4qHh5.jpg", + leisure: "nature_reserve", + name: "Warandeputten", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + wikipedia: "nl:Warandeputten", + }, + }, + { + type: "relation", + id: 8782624, + members: [ + { type: "way", ref: 315041273, role: "outer" }, + { type: "way", ref: 631377343, role: "outer" }, + { type: "way", ref: 631371237, role: "outer" }, + { type: "way", ref: 631371236, role: "outer" }, + { type: "way", ref: 631371234, role: "outer" }, + { type: "way", ref: 631377344, role: "outer" }, + { type: "way", ref: 631371232, role: "outer" }, + { type: "way", ref: 631371231, role: "outer" }, + { type: "way", ref: 315041263, role: "outer" }, + { type: "way", ref: 631371228, role: "outer" }, + { type: "way", ref: 631377341, role: "outer" }, + { type: "way", ref: 315041261, role: "outer" }, + { type: "way", ref: 631371223, role: "outer" }, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/VuzX5jW.jpg", + "image:1": "https://i.imgur.com/tPppmJG.jpg", + "image:2": "https://i.imgur.com/ecY3RER.jpg", + "image:3": "https://i.imgur.com/lr4FK6j.jpg", + "image:5": "https://i.imgur.com/uufEeE6.jpg", + leisure: "nature_reserve", + name: "Leiemeersen Noord", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen-noord/", + }, + }, + { + type: "relation", + id: 8890076, + members: [ + { type: "way", ref: 640979982, role: "outer" }, + { type: "way", ref: 640979978, role: "outer" }, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/SAAaKBH.jpg", + "image:1": "https://i.imgur.com/DGK9iBN.jpg", + "image:2": "https://i.imgur.com/bte1KJx.jpg", + "image:3": "https://i.imgur.com/f75Gxnx.jpg", + leisure: "nature_reserve", + name: "Gevaerts Noord", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + }, + }, + { + type: "relation", + id: 9118029, + members: [ + { type: "way", ref: 606165130, role: "outer" }, + { type: "way", ref: 655652319, role: "outer" }, + { type: "way", ref: 655652321, role: "outer" }, + ], + tags: { + access: "no", + "image:0": "https://i.imgur.com/eBufo0v.jpg", + "image:1": "https://i.imgur.com/kBej2Nk.jpg", + "image:2": "https://i.imgur.com/QKoyIRl.jpg", + leisure: "nature_reserve", + name: "De Leiemeersen", + note: "Door de hoge kwetsbaarheid van het gebied zijn De Leiemeersen enkel te bezoeken onder begeleiding van een gids", + "note:mapping": + "NIET VOOR BEGINNENDE MAPPERS! Dit gebied is met relaties als multipolygonen gemapt (zo'n 50 stuks). Als je niet weet hoe dit werkt, vraag hulp.", + note_1: "wetland=marsh is het veenmoerasgebied", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen/", + }, + }, + { + type: "node", + id: 668981602, + lat: 51.1588243, + lon: 3.2558654, + timestamp: "2012-07-06T17:58:39Z", + version: 2, + changeset: 12133044, + user: "martino260", + uid: 655442, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 668981622, + lat: 51.1565636, + lon: 3.2549888, + timestamp: "2020-03-23T23:54:26Z", + version: 4, + changeset: 82544029, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "no", + }, + }, + { + type: "node", + id: 1580339675, + lat: 51.1395949, + lon: 3.3332507, + timestamp: "2012-01-07T00:44:42Z", + version: 1, + changeset: 10317754, + user: "popaultje", + uid: 519184, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 1764571836, + lat: 51.1701118, + lon: 3.3363371, + timestamp: "2012-05-24T21:06:50Z", + version: 1, + changeset: 11693640, + user: "popaultje", + uid: 519184, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 2121652779, + lat: 51.1268536, + lon: 3.3239607, + timestamp: "2013-01-20T20:12:50Z", + version: 1, + changeset: 14725799, + user: "tomvdb", + uid: 437764, + tags: { amenity: "parking" }, + }, + { + type: "node", + id: 2386053906, + lat: 51.162161, + lon: 3.263065, + timestamp: "2018-01-19T21:31:30Z", + version: 2, + changeset: 55589374, + user: "L'imaginaire", + uid: 654234, + tags: { amenity: "toilets" }, + }, + { + type: "node", + id: 2978180520, + lat: 51.1329149, + lon: 3.3362322, + timestamp: "2014-07-24T21:29:38Z", + version: 1, + changeset: 24338416, + user: "pieterjanheyse", + uid: 254767, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2978183271, + lat: 51.1324243, + lon: 3.3373735, + timestamp: "2019-09-14T05:02:25Z", + version: 2, + changeset: 74462201, + user: "JanFi", + uid: 672253, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 2978184471, + lat: 51.1436385, + lon: 3.2916539, + timestamp: "2021-02-26T10:22:54Z", + version: 3, + changeset: 100041319, + user: "s8evq", + uid: 3710738, + tags: { amenity: "bench", backrest: "yes", check_date: "2021-02-26" }, + }, + { + type: "node", + id: 3925976407, + lat: 51.1787486, + lon: 3.2831866, + timestamp: "2019-08-12T19:48:31Z", + version: 2, + changeset: 73281516, + user: "s8evq", + uid: 3710738, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 5158056232, + lat: 51.1592067, + lon: 3.2567111, + timestamp: "2021-04-17T16:21:52Z", + version: 5, + changeset: 103110072, + user: "L'imaginaire", + uid: 654234, + tags: { + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "no", + }, + }, + { + type: "node", + id: 5718776382, + lat: 51.1609023, + lon: 3.2582509, + timestamp: "2021-10-18T10:27:50Z", + version: 3, + changeset: 112646117, + user: "s8evq", + uid: 3710738, + tags: { check_date: "2021-02-26", covered: "no", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5718776383, + lat: 51.1609488, + lon: 3.2581877, + timestamp: "2021-10-18T10:27:50Z", + version: 3, + changeset: 112646117, + user: "s8evq", + uid: 3710738, + tags: { check_date: "2021-02-26", covered: "no", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5745727100, + lat: 51.1594639, + lon: 3.2604304, + timestamp: "2018-07-07T18:00:29Z", + version: 1, + changeset: 60494261, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 5745739587, + lat: 51.1580397, + lon: 3.263101, + timestamp: "2021-02-26T10:07:56Z", + version: 2, + changeset: 100039706, + user: "s8evq", + uid: 3710738, + tags: { check_date: "2021-02-26", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5745739588, + lat: 51.1580631, + lon: 3.2630345, + timestamp: "2021-02-26T10:07:41Z", + version: 2, + changeset: 100039706, + user: "s8evq", + uid: 3710738, + tags: { check_date: "2021-02-26", leisure: "picnic_table" }, + }, + { + type: "node", + id: 5961596093, + lat: 51.1588103, + lon: 3.2633933, + timestamp: "2018-10-06T14:41:12Z", + version: 1, + changeset: 63258667, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 5964032193, + lat: 51.1514821, + lon: 3.2723766, + timestamp: "2018-10-07T12:15:53Z", + version: 1, + changeset: 63277533, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034563379, + lat: 51.1421689, + lon: 3.3022271, + timestamp: "2020-02-11T20:10:26Z", + version: 2, + changeset: 80868434, + user: "s8evq", + uid: 3710738, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034564191, + lat: 51.1722186, + lon: 3.2823584, + timestamp: "2018-11-04T20:27:50Z", + version: 2, + changeset: 64177431, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6034565298, + lat: 51.1722796, + lon: 3.282329, + timestamp: "2018-11-04T20:27:50Z", + version: 2, + changeset: 64177431, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 6145151111, + lat: 51.1690435, + lon: 3.3388676, + timestamp: "2018-12-18T13:13:36Z", + version: 1, + changeset: 65580728, + user: "Siel Nollet", + uid: 3292414, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6145151112, + lat: 51.1690023, + lon: 3.3388636, + timestamp: "2018-12-18T13:13:36Z", + version: 1, + changeset: 65580728, + user: "Siel Nollet", + uid: 3292414, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6216549651, + lat: 51.1292813, + lon: 3.332369, + timestamp: "2019-01-17T16:29:02Z", + version: 1, + changeset: 66400781, + user: "Nilsnn", + uid: 4652000, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 6216549652, + lat: 51.1292768, + lon: 3.3324259, + timestamp: "2019-01-17T16:29:03Z", + version: 1, + changeset: 66400781, + user: "Nilsnn", + uid: 4652000, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7204447030, + lat: 51.1791769, + lon: 3.283116, + timestamp: "2021-01-01T12:18:32Z", + version: 3, + changeset: 96768203, + user: "L'imaginaire", + uid: 654234, + tags: { + board_type: "nature", + information: "board", + mapillary: "0BHVgU1XCyTMM9cjvidUqk", + name: "De Assebroekse Meersen", + tourism: "information", + }, + }, + { + type: "node", + id: 7468175778, + lat: 51.1344104, + lon: 3.3348246, + timestamp: "2020-04-30T19:07:49Z", + version: 1, + changeset: 84433940, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7602473480, + lat: 51.1503874, + lon: 3.2836867, + timestamp: "2020-06-08T10:39:07Z", + version: 1, + changeset: 86349440, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { leisure: "picnic_table" }, + }, + { + type: "node", + id: 7602473482, + lat: 51.150244, + lon: 3.2842925, + timestamp: "2021-02-26T10:18:19Z", + version: 4, + changeset: 100040859, + user: "s8evq", + uid: 3710738, + tags: { + board_type: "wildlife", + information: "board", + name: "Waterbeestjes", + operator: "Natuurpunt Vallei van de Zuidleie", + tourism: "information", + }, + }, + { + type: "node", + id: 7602699080, + lat: 51.1367031, + lon: 3.3320712, + timestamp: "2020-06-08T12:08:11Z", + version: 1, + changeset: 86353903, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { amenity: "bench", backrest: "yes", material: "metal" }, + }, + { + type: "node", + id: 7680940369, + lat: 51.1380074, + lon: 3.3369928, + timestamp: "2020-07-03T17:54:31Z", + version: 1, + changeset: 87513827, + user: "L'imaginaire", + uid: 654234, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 7726850522, + lat: 51.1418585, + lon: 3.3064234, + timestamp: "2020-07-18T15:57:43Z", + version: 1, + changeset: 88179934, + user: "Pieter Vander Vennet", + uid: 3818858, + tags: { + "image:0": "https://i.imgur.com/Bh6UjYy.jpg", + information: "board", + tourism: "information", + }, + }, + { + type: "node", + id: 7727071212, + lat: 51.1501173, + lon: 3.2845352, + timestamp: "2021-02-26T10:17:39Z", + version: 2, + changeset: 100040859, + user: "s8evq", + uid: 3710738, + tags: { + board_type: "wildlife", + "image:0": "https://i.imgur.com/mFEQJWd.jpg", + information: "board", + name: "Vleermuizen", + operator: "Natuurpunt Vallei van de Zuidleie", + tourism: "information", + }, + }, + { + type: "node", + id: 9122376662, + lat: 51.1720505, + lon: 3.3308524, + timestamp: "2021-09-25T13:32:42Z", + version: 1, + changeset: 111688164, + user: "TeamP8", + uid: 718373, + tags: { amenity: "bench" }, + }, + { + type: "node", + id: 9425818876, + lat: 51.1325315, + lon: 3.3371616, + timestamp: "2022-01-28T20:13:29Z", + version: 3, + changeset: 116721474, + user: "L'imaginaire", + uid: 654234, + tags: { leisure: "picnic_table", mapillary: "101961548889238" }, + }, + { + type: "way", + id: 149408639, + timestamp: "2012-08-15T19:09:59Z", + version: 4, + changeset: 12742790, + user: "tomvdb", + uid: 437764, + nodes: [1623924235, 1623924236, 1623924238, 1864750831, 1623924241, 1623924235], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 149553206, + timestamp: "2021-01-30T12:11:53Z", + version: 4, + changeset: 98411765, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 1625199938, 1625199951, 8377691836, 8378081366, 8378081429, 8378081386, + 1625199950, 6414383775, 1625199827, 1625199938, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 184402308, + timestamp: "2020-03-23T23:54:26Z", + version: 6, + changeset: 82544029, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [1948836195, 1948836194, 1948836193, 1948836189, 1948836192, 1948836195], + tags: { + building: "yes", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + wheelchair: "yes", + }, + }, + { + type: "way", + id: 184402309, + timestamp: "2021-12-20T11:33:54Z", + version: 8, + changeset: 115161990, + user: "s8evq", + uid: 3710738, + nodes: [1948836029, 1948836038, 1948836032, 1948836025, 1948836023, 1948836029], + tags: { + building: "yes", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + "source:geometry:date": "2011-11-07", + "source:geometry:ref": "Gbg/3489204", + }, + }, + { + type: "way", + id: 184402331, + timestamp: "2021-02-26T10:03:17Z", + version: 4, + changeset: 100039746, + user: "s8evq", + uid: 3710738, + nodes: [1948836104, 1948836072, 1948836068, 1948836093, 1948836104], + tags: { access: "yes", amenity: "parking", fee: "no", parking: "surface" }, + }, + { + type: "way", + id: 236979353, + timestamp: "2021-01-25T12:38:22Z", + version: 4, + changeset: 98122705, + user: "JosV", + uid: 170722, + nodes: [ + 2449323191, 7962681624, 7962681623, 7962681621, 7962681622, 2449323193, + 7962681620, 7962681619, 8360787098, 4350143592, 6794421028, 6794421027, + 6794421041, 7962681614, 2123461969, 2449323198, 7962681615, 7962681616, + 6794421042, 2449323191, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 251590503, + timestamp: "2019-04-20T22:04:54Z", + version: 2, + changeset: 69412561, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 2577910543, 2577910530, 2577910542, 2577910520, 6418533335, 2577910526, + 2577910545, 2577910543, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 313090489, + timestamp: "2014-11-16T14:59:12Z", + version: 1, + changeset: 26823403, + user: "JanFi", + uid: 672253, + nodes: [ + 3190107423, 3190107435, 3190107442, 3190107439, 3190107432, 3190107428, + 3190107424, 3190107400, 3190107393, 3190107377, 3190107371, 3190107374, + 3190107408, 3190107407, 3190107415, 3190107416, 3190107420, 3190107423, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 314531418, + timestamp: "2021-05-30T17:02:49Z", + version: 21, + changeset: 105576255, + user: "s8evq", + uid: 3710738, + nodes: [ + 7468171455, 7468171451, 7727393212, 7727393211, 7727393210, 7727393208, + 7727393209, 8781449489, 7727393207, 7727393206, 7727393205, 7727393204, + 7727393203, 7727393202, 7727393201, 7727393200, 7390697550, 7390697534, + 7727393199, 7727393198, 7727393197, 7390697549, 7727393196, 7727393195, + 7727393194, 7727393193, 7727393192, 7727393191, 7727393190, 7727393189, + 7727393188, 7727393187, 7727393186, 7727393185, 7727339384, 7727339383, + 7727339382, 1553169911, 1553169836, 1493821433, 1493821422, 3185248088, + 7727339364, 7727339365, 7727339366, 7727339367, 7727339368, 7727339369, + 7727339370, 7727339371, 7727339372, 7727339373, 7727339374, 7727339375, + 7727339376, 7727339377, 3185248049, 3185248048, 3185248042, 3185248040, + 7727339378, 7727339379, 7727339380, 7727339381, 7468171438, 7468171442, + 7468171430, 7468171432, 7468171446, 7468171404, 7468171405, 7468171422, + 7468171426, 7468171433, 7468171423, 7468171428, 7468171431, 7468171429, + 7468171406, 7468171407, 7468171444, 7468171408, 7468171409, 7468171410, + 7468171411, 7468171412, 7468171413, 7190927792, 7190927791, 7190927793, + 7190927787, 7190927788, 7190927789, 7190927790, 7190927786, 7602692242, + 7190927785, 7190873584, 7468171450, 7190873582, 7190873576, 7190873578, + 7190873577, 7468171455, + ], + tags: { + access: "yes", + dog: "leashed", + dogs: "leashed", + image: "https://i.imgur.com/cOfwWTj.jpg", + "image:0": "https://i.imgur.com/RliQdyi.jpg", + "image:1": "https://i.imgur.com/IeKHahz.jpg", + "image:2": "https://i.imgur.com/1K0IORH.jpg", + "image:3": "https://i.imgur.com/jojP09s.jpg", + "image:4": "https://i.imgur.com/DK6kT51.jpg", + "image:5": "https://i.imgur.com/RizbGM1.jpg", + "image:6": "https://i.imgur.com/hyoY6Cl.jpg", + "image:7": "https://i.imgur.com/xDd7Wrq.jpg", + leisure: "nature_reserve", + name: "Miseriebocht", + operator: "Natuurpunt Beernem", + website: "https://www.natuurpunt.be/natuurgebied/miseriebocht", + wikidata: "Q97060915", + }, + }, + { + type: "way", + id: 366318480, + timestamp: "2015-08-18T13:50:59Z", + version: 1, + changeset: 33415758, + user: "xras3r", + uid: 323672, + nodes: [3702926557, 3702926558, 3702926559, 3702926560, 3702926557], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366318481, + timestamp: "2015-08-18T13:50:59Z", + version: 1, + changeset: 33415758, + user: "xras3r", + uid: 323672, + nodes: [ + 3702878648, 3702926561, 3702926562, 3702926563, 3702926564, 3702926565, + 3702926566, 3702926567, 3702878648, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366318482, + timestamp: "2021-01-05T09:04:32Z", + version: 2, + changeset: 96964072, + user: "JosV", + uid: 170722, + nodes: [3702926568, 8292789053, 8292789054, 3702878654, 3702926568], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 366320440, + timestamp: "2015-08-18T14:02:00Z", + version: 1, + changeset: 33415981, + user: "xras3r", + uid: 323672, + nodes: [3702956173, 3702956174, 3702956175, 3702956176, 3702956173], + tags: { amenity: "parking", name: "Kleine Beer" }, + }, + { + type: "way", + id: 366321706, + timestamp: "2015-08-18T14:15:30Z", + version: 1, + changeset: 33416303, + user: "xras3r", + uid: 323672, + nodes: [3702969714, 3702969715, 3702969716, 3702969717, 3702969714], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 480267681, + timestamp: "2021-01-05T21:32:23Z", + version: 2, + changeset: 97006454, + user: "JosV", + uid: 170722, + nodes: [ + 4732689641, 4732689640, 4732689639, 4732689638, 4732689637, 4732689636, + 4732689635, 4732689634, 4732689633, 4732689632, 4732689631, 4732689630, + 4732689629, 4732689628, 4732689627, 4732689626, 8294875888, 4732689641, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 554341620, + timestamp: "2018-01-19T21:31:30Z", + version: 1, + changeset: 55589374, + user: "L'imaginaire", + uid: 654234, + nodes: [5349884603, 5349884602, 5349884601, 5349884600, 5349884603], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 554341621, + timestamp: "2018-01-19T21:31:30Z", + version: 1, + changeset: 55589374, + user: "L'imaginaire", + uid: 654234, + nodes: [5349884607, 5349884606, 5349884605, 5349884604, 5349884607], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 561902092, + timestamp: "2021-01-17T14:52:46Z", + version: 6, + changeset: 97640804, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [5417516023, 5417515520, 5417516021, 5417516022, 5417516023], + tags: { + building: "yes", + image: "https://i.imgur.com/WmViSbL.jpg", + leisure: "bird_hide", + operator: "Natuurpunt Vallei van de Zuidleie", + shelter: "yes", + wheelchair: "no", + }, + }, + { + type: "way", + id: 605915064, + timestamp: "2018-07-07T18:07:31Z", + version: 1, + changeset: 60494380, + user: "Pieter Vander Vennet", + uid: 3818858, + nodes: [5745739924, 5745739925, 5745739926, 5745739927, 5745739924], + tags: { amenity: "parking", surface: "fine2" }, + }, + { + type: "way", + id: 650285088, + timestamp: "2021-01-29T09:37:56Z", + version: 2, + changeset: 98352073, + user: "JosV", + uid: 170722, + nodes: [ + 3645188881, 6100803131, 6100803130, 6100803129, 6100803124, 6100803125, + 6100803128, 6100803127, 6100803126, 3645188881, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 655944574, + timestamp: "2020-02-23T22:05:54Z", + version: 2, + changeset: 81377451, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 6145151107, 6145151108, 6145151109, 6145151115, 6145151114, 6145151110, + 6145151107, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 664171069, + timestamp: "2019-01-17T16:29:08Z", + version: 1, + changeset: 66400781, + user: "Nilsnn", + uid: 4652000, + nodes: [ + 6216549610, 6216549611, 6216549612, 6216549613, 1413470849, 1413470848, + 6216549605, 6216549604, 6216549610, + ], + tags: { amenity: "parking" }, + }, + { + type: "way", + id: 664171076, + timestamp: "2021-01-09T21:56:37Z", + version: 3, + changeset: 97230316, + user: "JosV", + uid: 170722, + nodes: [ + 6216549656, 6216549655, 8307316294, 6216549661, 6216549657, 6216549658, + 6216549659, 6216549660, 6216549656, + ], + tags: { amenity: "parking", capacity: "50" }, + }, + { + type: "way", + id: 665330334, + timestamp: "2019-01-22T14:20:51Z", + version: 1, + changeset: 66539354, + user: "Nilsnn", + uid: 4652000, + nodes: [6227395993, 6227395991, 6227395992, 6227395997, 6227395993], + tags: { + access: "yes", + amenity: "parking", + fee: "no", + parking: "surface", + surface: "asphalt", + }, + }, + { + type: "way", + id: 684598363, + timestamp: "2020-03-07T14:21:22Z", + version: 3, + changeset: 81900556, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 3227068565, 6416001161, 6414352054, 6414352055, 7274233390, 7274233391, + 7274233397, 7274233395, 7274233396, 6414352053, 3227068568, 3227068565, + ], + tags: { amenity: "parking", fee: "no" }, + }, + { + type: "way", + id: 684599810, + timestamp: "2020-12-31T20:58:28Z", + version: 3, + changeset: 96751036, + user: "JosV", + uid: 170722, + nodes: [ + 1317838331, 8279842668, 1384096112, 1317838328, 6414374315, 3227068446, + 6414374316, 6414374317, 6414374318, 3227068456, 6414374319, 6414374320, + 6414374321, 1317838317, 1317838331, + ], + tags: { access: "no", amenity: "parking", operator: "Politie" }, + }, + { + type: "way", + id: 761474468, + timestamp: "2021-01-05T21:32:23Z", + version: 3, + changeset: 97006454, + user: "JosV", + uid: 170722, + nodes: [ + 7114502201, 7114502203, 7114502200, 7114502202, 3170562439, 3170562437, + 3170562431, 7114502240, 7114502211, 7114502212, 7114502214, 7114502215, + 7114502228, 7114502234, 7114502235, 7114502236, 7114502237, 7114502238, + 7114502239, 7114502233, 7114502232, 7114502231, 7114502229, 7114502230, + 7114502227, 7114502226, 7114502225, 7114502216, 7114502217, 7114502224, + 3170562392, 7114502218, 3170562394, 7114502219, 7114502220, 7114502221, + 7114502222, 7114502223, 3170562395, 3170562396, 3170562397, 3170562402, + 3170562410, 7114502209, 7114502208, 7114502207, 7114502205, 7114502206, + 3170562436, 1475188519, 1475188516, 6627605025, 8294886142, 7114502201, + ], + tags: { + image: "http://valleivandezuidleie.be/wp-content/uploads/2011/12/2011-03-24_G12_088_1_1.JPG", + leisure: "nature_reserve", + name: "Merlebeek-Meerberg", + natural: "wetland", + operator: "Natuurpunt Vallei van de Zuidleie", + start_date: "2011", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/merlebeek/", + wetland: "wet_meadow", + }, + }, + { + type: "way", + id: 813859435, + timestamp: "2021-02-26T10:18:26Z", + version: 3, + changeset: 100040879, + user: "s8evq", + uid: 3710738, + nodes: [ + 7602479690, 7459257985, 7602479691, 7459154784, 7459154782, 7602479692, + 5482441357, 7602479693, 7602479694, 7602479695, 7602479696, 7602479690, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/nb9nawa.jpg", + landuse: "grass", + leisure: "nature_reserve", + "name:signed": "no", + natural: "grass", + operator: "Natuurpunt Vallei van de Zuidleie", + }, + }, + { + type: "way", + id: 826103452, + timestamp: "2020-07-14T09:05:14Z", + version: 1, + changeset: 87965884, + user: "L'imaginaire", + uid: 654234, + nodes: [7713176912, 7713176911, 7713176910, 7713176909, 7713176912], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 893176022, + timestamp: "2021-01-07T23:02:07Z", + version: 1, + changeset: 97133621, + user: "JosV", + uid: 170722, + nodes: [1927235214, 8301349336, 8301349335, 8301349337, 1927235214], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943476, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251887, 8328251886, 8328251885, 8328251884, 8328251887], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943477, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251891, 8328251890, 8328251889, 8328251888, 8328251891], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943478, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251895, 8328251894, 8328251893, 8328251892, 8328251895], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943479, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251897, 8328251896, 8328251901, 8328251900, 8328251897], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943480, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251898, 8328251899, 8328251903, 8328251902, 8328251898], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943481, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251907, 8328251906, 8328251905, 8328251904, 8328251907], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 895943482, + timestamp: "2021-01-16T17:56:50Z", + version: 1, + changeset: 97614669, + user: "JosV", + uid: 170722, + nodes: [8328251911, 8328251910, 8328251909, 8328251908, 8328251911], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 901952767, + timestamp: "2021-01-30T12:19:15Z", + version: 1, + changeset: 98412028, + user: "L'imaginaire", + uid: 654234, + nodes: [8378097127, 8378097126, 8378097125, 8378097124, 8378097127], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "way", + id: 901952768, + timestamp: "2021-01-30T12:19:15Z", + version: 1, + changeset: 98412028, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 8378097134, 8378097133, 8378097132, 8378097131, 8378097130, 8378097129, + 8378097128, 8378097134, + ], + tags: { amenity: "parking", parking: "street_side" }, + }, + { + type: "way", + id: 947325182, + timestamp: "2021-05-26T15:16:06Z", + version: 1, + changeset: 105366812, + user: "L'imaginaire", + uid: 654234, + nodes: [ + 8497007549, 8768981525, 8768981522, 8768981524, 8768981521, 8768981523, + 8768981520, 8768981519, 6206789709, 8768981533, 8497007549, + ], + tags: { amenity: "parking", parking: "surface" }, + }, + { + type: "way", + id: 966827815, + timestamp: "2021-07-23T20:01:09Z", + version: 1, + changeset: 108511361, + user: "L'imaginaire", + uid: 654234, + nodes: [8945026757, 8945026756, 8945026755, 8945026754, 8945026757], + tags: { access: "customers", amenity: "parking", parking: "surface" }, + }, + { + type: "relation", + id: 2589413, + timestamp: "2021-03-06T15:13:01Z", + version: 7, + changeset: 100540347, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 663050030, role: "outer" }, + { type: "way", ref: 184402334, role: "outer" }, + { type: "way", ref: 184402332, role: "outer" }, + { type: "way", ref: 184402325, role: "outer" }, + { type: "way", ref: 184402326, role: "outer" }, + { type: "way", ref: 314899865, role: "outer" }, + { type: "way", ref: 314956402, role: "outer" }, + ], + tags: { + access: "yes", + image: "https://i.imgur.com/Yu4qHh5.jpg", + leisure: "nature_reserve", + name: "Warandeputten", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + wikipedia: "nl:Warandeputten", + }, + }, + { + type: "relation", + id: 8782624, + timestamp: "2021-03-08T13:15:15Z", + version: 14, + changeset: 100640534, + user: "M!dgard", + uid: 763799, + members: [ + { type: "way", ref: 315041273, role: "outer" }, + { type: "way", ref: 631377343, role: "outer" }, + { type: "way", ref: 631371237, role: "outer" }, + { type: "way", ref: 631371236, role: "outer" }, + { type: "way", ref: 631371234, role: "outer" }, + { type: "way", ref: 631377344, role: "outer" }, + { type: "way", ref: 631371232, role: "outer" }, + { type: "way", ref: 631371231, role: "outer" }, + { type: "way", ref: 315041263, role: "outer" }, + { type: "way", ref: 631371228, role: "outer" }, + { type: "way", ref: 631377341, role: "outer" }, + { type: "way", ref: 315041261, role: "outer" }, + { type: "way", ref: 631371223, role: "outer" }, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/VuzX5jW.jpg", + "image:1": "https://i.imgur.com/tPppmJG.jpg", + "image:2": "https://i.imgur.com/ecY3RER.jpg", + "image:3": "https://i.imgur.com/lr4FK6j.jpg", + "image:5": "https://i.imgur.com/uufEeE6.jpg", + leisure: "nature_reserve", + name: "Leiemeersen Noord", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen-noord/", + }, + }, + { + type: "relation", + id: 8890076, + timestamp: "2020-07-18T15:56:50Z", + version: 6, + changeset: 88179905, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 640979982, role: "outer" }, + { type: "way", ref: 640979978, role: "outer" }, + ], + tags: { + access: "yes", + "image:0": "https://i.imgur.com/SAAaKBH.jpg", + "image:1": "https://i.imgur.com/DGK9iBN.jpg", + "image:2": "https://i.imgur.com/bte1KJx.jpg", + "image:3": "https://i.imgur.com/f75Gxnx.jpg", + leisure: "nature_reserve", + name: "Gevaerts Noord", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + }, + }, + { + type: "relation", + id: 9118029, + timestamp: "2020-07-11T12:04:16Z", + version: 4, + changeset: 87852045, + user: "Pieter Vander Vennet", + uid: 3818858, + members: [ + { type: "way", ref: 606165130, role: "outer" }, + { type: "way", ref: 655652319, role: "outer" }, + { type: "way", ref: 655652321, role: "outer" }, + ], + tags: { + access: "no", + "image:0": "https://i.imgur.com/eBufo0v.jpg", + "image:1": "https://i.imgur.com/kBej2Nk.jpg", + "image:2": "https://i.imgur.com/QKoyIRl.jpg", + leisure: "nature_reserve", + name: "De Leiemeersen", + note: "Door de hoge kwetsbaarheid van het gebied zijn De Leiemeersen enkel te bezoeken onder begeleiding van een gids", + "note:mapping": + "NIET VOOR BEGINNENDE MAPPERS! Dit gebied is met relaties als multipolygonen gemapt (zo'n 50 stuks). Als je niet weet hoe dit werkt, vraag hulp.", + note_1: "wetland=marsh is het veenmoerasgebied", + operator: "Natuurpunt Vallei van de Zuidleie", + type: "multipolygon", + website: + "http://valleivandezuidleie.be/info-over-de-vallei-van-de-zuidleie/leiemeersen/", + }, + }, + { type: "node", id: 7114502229, lat: 51.1340508, lon: 3.2949303 }, + { type: "node", id: 7114502231, lat: 51.1340428, lon: 3.2959613 }, + { type: "node", id: 7114502232, lat: 51.134096, lon: 3.2971973 }, + { type: "node", id: 1927235214, lat: 51.1267391, lon: 3.3222921 }, + { type: "node", id: 8301349336, lat: 51.1266966, lon: 3.3223356 }, + { type: "node", id: 1413470848, lat: 51.1276558, lon: 3.3278194 }, + { type: "node", id: 1413470849, lat: 51.1276852, lon: 3.3274627 }, + { type: "node", id: 6216549605, lat: 51.1276578, lon: 3.3279376 }, + { type: "node", id: 6216549611, lat: 51.1277354, lon: 3.3273608 }, + { type: "node", id: 6216549612, lat: 51.1277281, lon: 3.3274215 }, + { type: "node", id: 6216549613, lat: 51.1277062, lon: 3.3274249 }, + { type: "node", id: 8301349335, lat: 51.1269096, lon: 3.3228882 }, + { type: "node", id: 8301349337, lat: 51.126955, lon: 3.3228466 }, + { type: "node", id: 8328251884, lat: 51.1278701, lon: 3.3253392 }, + { type: "node", id: 8328251885, lat: 51.127857, lon: 3.3254078 }, + { type: "node", id: 8328251888, lat: 51.1278797, lon: 3.325168 }, + { type: "node", id: 8328251889, lat: 51.1278665, lon: 3.3252369 }, + { type: "node", id: 8328251892, lat: 51.1271569, lon: 3.3248958 }, + { type: "node", id: 8328251893, lat: 51.1272208, lon: 3.3252329 }, + { type: "node", id: 8328251894, lat: 51.1272677, lon: 3.3252111 }, + { type: "node", id: 8328251895, lat: 51.1272033, lon: 3.3248728 }, + { type: "node", id: 8328251896, lat: 51.1271015, lon: 3.3257575 }, + { type: "node", id: 8328251897, lat: 51.1270872, lon: 3.3256877 }, + { type: "node", id: 8328251898, lat: 51.1271271, lon: 3.325744 }, + { type: "node", id: 8328251899, lat: 51.1271128, lon: 3.3256743 }, + { type: "node", id: 8328251900, lat: 51.1270462, lon: 3.3257067 }, + { type: "node", id: 8328251901, lat: 51.1270609, lon: 3.3257756 }, + { type: "node", id: 8328251902, lat: 51.1272095, lon: 3.3257043 }, + { type: "node", id: 8328251903, lat: 51.1271957, lon: 3.3256351 }, + { type: "node", id: 8328251904, lat: 51.1269631, lon: 3.3253915 }, + { type: "node", id: 8328251905, lat: 51.1269756, lon: 3.3254617 }, + { type: "node", id: 8328251906, lat: 51.1271678, lon: 3.3253747 }, + { type: "node", id: 8328251907, lat: 51.1271557, lon: 3.3253045 }, + { type: "node", id: 8328251908, lat: 51.1270115, lon: 3.3255467 }, + { type: "node", id: 8328251909, lat: 51.1270235, lon: 3.3256138 }, + { type: "node", id: 8328251910, lat: 51.1271099, lon: 3.3255749 }, + { type: "node", id: 8328251911, lat: 51.1270975, lon: 3.3255072 }, + { type: "node", id: 2449323191, lat: 51.1340437, lon: 3.3216558 }, + { type: "node", id: 2449323193, lat: 51.134285, lon: 3.3221086 }, + { type: "node", id: 2449323198, lat: 51.1334724, lon: 3.3223472 }, + { type: "node", id: 4350143592, lat: 51.1344661, lon: 3.3226292 }, + { type: "node", id: 4732689626, lat: 51.1301038, lon: 3.3223672 }, + { type: "node", id: 4732689627, lat: 51.1301351, lon: 3.3223533 }, + { type: "node", id: 4732689628, lat: 51.130161, lon: 3.3224805 }, + { type: "node", id: 4732689629, lat: 51.1301379, lon: 3.3224524 }, + { type: "node", id: 4732689630, lat: 51.130151, lon: 3.3225204 }, + { type: "node", id: 4732689631, lat: 51.1301375, lon: 3.3225274 }, + { type: "node", id: 4732689632, lat: 51.1301505, lon: 3.322593 }, + { type: "node", id: 4732689634, lat: 51.1297694, lon: 3.3223954 }, + { type: "node", id: 4732689635, lat: 51.1298034, lon: 3.3223641 }, + { type: "node", id: 4732689636, lat: 51.1297546, lon: 3.3221883 }, + { type: "node", id: 4732689637, lat: 51.1297088, lon: 3.3219708 }, + { type: "node", id: 4732689638, lat: 51.1297683, lon: 3.3219402 }, + { type: "node", id: 4732689639, lat: 51.1297781, lon: 3.3219034 }, + { type: "node", id: 4732689640, lat: 51.1297694, lon: 3.3218478 }, + { type: "node", id: 4732689641, lat: 51.1300088, lon: 3.32173 }, + { type: "node", id: 6794421027, lat: 51.1341852, lon: 3.3223337 }, + { type: "node", id: 6794421042, lat: 51.1336271, lon: 3.3220973 }, + { type: "node", id: 7962681614, lat: 51.1336656, lon: 3.322624 }, + { type: "node", id: 7962681615, lat: 51.1335301, lon: 3.322287 }, + { type: "node", id: 7962681616, lat: 51.1335065, lon: 3.3222317 }, + { type: "node", id: 7962681619, lat: 51.134306, lon: 3.3222136 }, + { type: "node", id: 7962681620, lat: 51.1343186, lon: 3.3221995 }, + { type: "node", id: 7962681621, lat: 51.1341453, lon: 3.321775 }, + { type: "node", id: 7962681622, lat: 51.1341537, lon: 3.321769 }, + { type: "node", id: 7962681623, lat: 51.1341067, lon: 3.3216811 }, + { type: "node", id: 7962681624, lat: 51.1340675, lon: 3.3217193 }, + { type: "node", id: 8294875888, lat: 51.1300197, lon: 3.3219403 }, + { type: "node", id: 8360787098, lat: 51.1344583, lon: 3.3225918 }, + { type: "node", id: 2123461969, lat: 51.1336123, lon: 3.3226786 }, + { type: "node", id: 4732689633, lat: 51.1298776, lon: 3.3227393 }, + { type: "node", id: 6216549604, lat: 51.1280095, lon: 3.328024 }, + { type: "node", id: 6216549610, lat: 51.128134, lon: 3.3274542 }, + { type: "node", id: 6794421028, lat: 51.1343476, lon: 3.3227429 }, + { type: "node", id: 6794421041, lat: 51.1337382, lon: 3.3227794 }, + { type: "node", id: 8328251886, lat: 51.128048, lon: 3.3255015 }, + { type: "node", id: 8328251887, lat: 51.1280616, lon: 3.3254329 }, + { type: "node", id: 8328251890, lat: 51.1280791, lon: 3.3253407 }, + { type: "node", id: 8328251891, lat: 51.1280922, lon: 3.3252721 }, + { type: "node", id: 1623924235, lat: 51.131361, lon: 3.333018 }, + { type: "node", id: 1623924236, lat: 51.1311039, lon: 3.3325764 }, + { type: "node", id: 1623924238, lat: 51.1310489, lon: 3.3326527 }, + { type: "node", id: 1623924241, lat: 51.1314273, lon: 3.3331479 }, + { type: "node", id: 1864750831, lat: 51.1313615, lon: 3.3332222 }, + { type: "node", id: 6216549655, lat: 51.134416, lon: 3.3347284 }, + { type: "node", id: 6216549656, lat: 51.1343187, lon: 3.3349542 }, + { type: "node", id: 6216549657, lat: 51.1334196, lon: 3.335723 }, + { type: "node", id: 6216549658, lat: 51.1334902, lon: 3.3357377 }, + { type: "node", id: 6216549659, lat: 51.133716, lon: 3.335546 }, + { type: "node", id: 6216549660, lat: 51.133883, lon: 3.3353885 }, + { type: "node", id: 6216549661, lat: 51.133662, lon: 3.3355049 }, + { type: "node", id: 8307316294, lat: 51.1338503, lon: 3.3353095 }, + { type: "node", id: 1493821422, lat: 51.1320567, lon: 3.3398517 }, + { type: "node", id: 1493821433, lat: 51.1316132, lon: 3.3408892 }, + { type: "node", id: 1553169836, lat: 51.1311998, lon: 3.3415993 }, + { type: "node", id: 3185248088, lat: 51.1323359, lon: 3.3389757 }, + { type: "node", id: 7727339364, lat: 51.1321819, lon: 3.3388758 }, + { type: "node", id: 7727339365, lat: 51.1319823, lon: 3.339559 }, + { type: "node", id: 7727339366, lat: 51.1316011, lon: 3.3405485 }, + { type: "node", id: 7727339367, lat: 51.1312764, lon: 3.3411848 }, + { type: "node", id: 7727339368, lat: 51.1310889, lon: 3.3414676 }, + { type: "node", id: 7727339369, lat: 51.1304838, lon: 3.3422395 }, + { type: "node", id: 3185248048, lat: 51.1268693, lon: 3.3480331 }, + { type: "node", id: 3185248049, lat: 51.1269831, lon: 3.3475504 }, + { type: "node", id: 7727339376, lat: 51.1274147, lon: 3.3464515 }, + { type: "node", id: 7727339377, lat: 51.1271283, lon: 3.3469817 }, + { type: "node", id: 3185248040, lat: 51.1266778, lon: 3.3491476 }, + { type: "node", id: 3185248042, lat: 51.126712, lon: 3.3489485 }, + { type: "node", id: 7468171404, lat: 51.1277325, lon: 3.3553578 }, + { type: "node", id: 7468171430, lat: 51.1270326, lon: 3.353594 }, + { type: "node", id: 7468171432, lat: 51.1271802, lon: 3.3540454 }, + { type: "node", id: 7468171438, lat: 51.1268064, lon: 3.3526001 }, + { type: "node", id: 7468171442, lat: 51.1268303, lon: 3.3527746 }, + { type: "node", id: 7468171446, lat: 51.1273659, lon: 3.3546333 }, + { type: "node", id: 7727339378, lat: 51.1265656, lon: 3.3505324 }, + { type: "node", id: 7727339379, lat: 51.1266463, lon: 3.350873 }, + { type: "node", id: 7727339380, lat: 51.1266302, lon: 3.3514032 }, + { type: "node", id: 7727339381, lat: 51.1267774, lon: 3.3523929 }, + { type: "node", id: 7727393190, lat: 51.1276264, lon: 3.3489611 }, + { type: "node", id: 7727393191, lat: 51.1274853, lon: 3.3497484 }, + { type: "node", id: 7727393192, lat: 51.127332, lon: 3.350571 }, + { type: "node", id: 7727393193, lat: 51.1273663, lon: 3.3514386 }, + { type: "node", id: 7727393194, lat: 51.1274853, lon: 3.3519206 }, + { type: "node", id: 7727393195, lat: 51.1276889, lon: 3.3531674 }, + { type: "node", id: 1553169911, lat: 51.1313314, lon: 3.3425771 }, + { type: "node", id: 7727339370, lat: 51.1300705, lon: 3.3427304 }, + { type: "node", id: 7727339371, lat: 51.1293083, lon: 3.343643 }, + { type: "node", id: 7727339372, lat: 51.1285642, lon: 3.3445074 }, + { type: "node", id: 7727339373, lat: 51.1285702, lon: 3.3445781 }, + { type: "node", id: 7727339374, lat: 51.1283181, lon: 3.3448801 }, + { type: "node", id: 7727339375, lat: 51.1281023, lon: 3.3451404 }, + { type: "node", id: 7727339382, lat: 51.1311196, lon: 3.3430952 }, + { type: "node", id: 7727339383, lat: 51.1309343, lon: 3.3434996 }, + { type: "node", id: 7727339384, lat: 51.1306234, lon: 3.343745 }, + { type: "node", id: 7727393185, lat: 51.1300873, lon: 3.3443449 }, + { type: "node", id: 7727393186, lat: 51.129401, lon: 3.3453846 }, + { type: "node", id: 7727393187, lat: 51.1290865, lon: 3.345963 }, + { type: "node", id: 7727393188, lat: 51.1285581, lon: 3.3468788 }, + { type: "node", id: 7727393189, lat: 51.1280217, lon: 3.3479457 }, + { type: "node", id: 7390697549, lat: 51.128429, lon: 3.354427 }, + { type: "node", id: 7727393196, lat: 51.1279813, lon: 3.353749 }, + { type: "node", id: 3209560114, lat: 51.1538874, lon: 3.2531858 }, + { type: "node", id: 3209560115, lat: 51.1539649, lon: 3.2531836 }, + { type: "node", id: 3209560116, lat: 51.1541197, lon: 3.2537986 }, + { type: "node", id: 3209560117, lat: 51.1541021, lon: 3.253149 }, + { type: "node", id: 3210389695, lat: 51.1539646, lon: 3.2534427 }, + { type: "node", id: 416917618, lat: 51.1565737, lon: 3.2549365 }, + { type: "node", id: 668981667, lat: 51.1576026, lon: 3.2555383 }, + { type: "node", id: 668981668, lat: 51.1564046, lon: 3.2547045 }, + { type: "node", id: 1815081998, lat: 51.1575578, lon: 3.2555439 }, + { type: "node", id: 1948835662, lat: 51.155957, lon: 3.2562889 }, + { type: "node", id: 1948835742, lat: 51.156182, lon: 3.2540291 }, + { type: "node", id: 1948835950, lat: 51.1591027, lon: 3.2559292 }, + { type: "node", id: 1948836096, lat: 51.1600831, lon: 3.2562989 }, + { type: "node", id: 1948836149, lat: 51.1605378, lon: 3.2568906 }, + { type: "node", id: 2026541837, lat: 51.1543652, lon: 3.2542901 }, + { type: "node", id: 2026541841, lat: 51.1545903, lon: 3.2546809 }, + { type: "node", id: 2026541843, lat: 51.1548384, lon: 3.2550342 }, + { type: "node", id: 2026541846, lat: 51.155086, lon: 3.2553429 }, + { type: "node", id: 2026541851, lat: 51.15565, lon: 3.2542965 }, + { type: "node", id: 3209560118, lat: 51.1542244, lon: 3.2530815 }, + { type: "node", id: 3209560119, lat: 51.1544741, lon: 3.252803 }, + { type: "node", id: 3209560121, lat: 51.1546028, lon: 3.2526667 }, + { type: "node", id: 3209560124, lat: 51.1547447, lon: 3.2525904 }, + { type: "node", id: 3209560126, lat: 51.1550296, lon: 3.2525253 }, + { type: "node", id: 3209560127, lat: 51.1551915, lon: 3.2525297 }, + { type: "node", id: 3209560131, lat: 51.1553436, lon: 3.2525627 }, + { type: "node", id: 3209560132, lat: 51.1554055, lon: 3.2526006 }, + { type: "node", id: 3209560134, lat: 51.155479, lon: 3.2526651 }, + { type: "node", id: 3209560135, lat: 51.155558, lon: 3.2527647 }, + { type: "node", id: 3209560136, lat: 51.1556482, lon: 3.2528805 }, + { type: "node", id: 3209560138, lat: 51.1559652, lon: 3.2534517 }, + { type: "node", id: 3209560139, lat: 51.1560327, lon: 3.2535705 }, + { type: "node", id: 5417515520, lat: 51.1545998, lon: 3.2545767 }, + { type: "node", id: 5417516021, lat: 51.1545476, lon: 3.2544989 }, + { type: "node", id: 5417516022, lat: 51.1545856, lon: 3.2544342 }, + { type: "node", id: 5417516023, lat: 51.1546378, lon: 3.2545119 }, + { type: "node", id: 6206789688, lat: 51.1553578, lon: 3.255668 }, + { type: "node", id: 416917603, lat: 51.1591104, lon: 3.2595563 }, + { type: "node", id: 416917605, lat: 51.1592829, lon: 3.2591123 }, + { type: "node", id: 416917609, lat: 51.1597764, lon: 3.2597849 }, + { type: "node", id: 1554514806, lat: 51.1586314, lon: 3.2636074 }, + { type: "node", id: 1948835842, lat: 51.1570345, lon: 3.2574053 }, + { type: "node", id: 1948835900, lat: 51.1583428, lon: 3.258761 }, + { type: "node", id: 1948835986, lat: 51.1595419, lon: 3.2593891 }, + { type: "node", id: 1948836023, lat: 51.1597284, lon: 3.2585312 }, + { type: "node", id: 1948836025, lat: 51.1597477, lon: 3.2585581 }, + { type: "node", id: 1948836029, lat: 51.1597506, lon: 3.2584887 }, + { type: "node", id: 1948836032, lat: 51.159778, lon: 3.2585905 }, + { type: "node", id: 1948836038, lat: 51.1597992, lon: 3.2585462 }, + { type: "node", id: 1948836068, lat: 51.1598265, lon: 3.2595499 }, + { type: "node", id: 1948836072, lat: 51.1598725, lon: 3.2596329 }, + { type: "node", id: 1948836093, lat: 51.1600505, lon: 3.2592835 }, + { type: "node", id: 1948836104, lat: 51.1601026, lon: 3.2593298 }, + { type: "node", id: 1948836189, lat: 51.1606847, lon: 3.2577063 }, + { type: "node", id: 5745739926, lat: 51.160615, lon: 3.2602336 }, + { type: "node", id: 5747937642, lat: 51.1580603, lon: 3.2636348 }, + { type: "node", id: 5962340003, lat: 51.1587851, lon: 3.2632739 }, + { type: "node", id: 5962414276, lat: 51.158949, lon: 3.2635109 }, + { type: "node", id: 5962415498, lat: 51.1589617, lon: 3.2635523 }, + { type: "node", id: 5962415499, lat: 51.1589623, lon: 3.2635339 }, + { type: "node", id: 5962415500, lat: 51.1589568, lon: 3.2635203 }, + { type: "node", id: 5962415538, lat: 51.1589195, lon: 3.2636407 }, + { type: "node", id: 6206789709, lat: 51.1545829, lon: 3.2585784 }, + { type: "node", id: 8497007481, lat: 51.1588349, lon: 3.2633671 }, + { type: "node", id: 8497007549, lat: 51.1548977, lon: 3.257909 }, + { type: "node", id: 8768981519, lat: 51.1545715, lon: 3.2585859 }, + { type: "node", id: 8768981520, lat: 51.1544691, lon: 3.2584642 }, + { type: "node", id: 8768981521, lat: 51.1543652, lon: 3.2584253 }, + { type: "node", id: 8768981522, lat: 51.1543648, lon: 3.2583401 }, + { type: "node", id: 8768981523, lat: 51.1544439, lon: 3.2585178 }, + { type: "node", id: 8768981524, lat: 51.1543892, lon: 3.258369 }, + { type: "node", id: 8768981525, lat: 51.1547185, lon: 3.2575482 }, + { type: "node", id: 8768981533, lat: 51.1545902, lon: 3.2585725 }, + { type: "node", id: 3162627482, lat: 51.1513402, lon: 3.2701364 }, + { type: "node", id: 3162627509, lat: 51.1517878, lon: 3.2696503 }, + { type: "node", id: 5745963944, lat: 51.1524768, lon: 3.2700312 }, + { type: "node", id: 5745963946, lat: 51.1519527, lon: 3.2698278 }, + { type: "node", id: 5745963947, lat: 51.1513495, lon: 3.2697903 }, + { type: "node", id: 5747937657, lat: 51.1538472, lon: 3.269262 }, + { type: "node", id: 5747937658, lat: 51.1536592, lon: 3.2698471 }, + { type: "node", id: 5747937668, lat: 51.1514068, lon: 3.2699149 }, + { type: "node", id: 5747937669, lat: 51.1514278, lon: 3.26999 }, + { type: "node", id: 5747937670, lat: 51.1514329, lon: 3.2701134 }, + { type: "node", id: 5747937671, lat: 51.1514387, lon: 3.2702207 }, + { type: "node", id: 5747937672, lat: 51.1515532, lon: 3.2702006 }, + { type: "node", id: 5747937673, lat: 51.1515986, lon: 3.2701456 }, + { type: "node", id: 5747937674, lat: 51.151872, lon: 3.2701201 }, + { type: "node", id: 6142725039, lat: 51.1513841, lon: 3.2701799 }, + { type: "node", id: 6142725060, lat: 51.1499168, lon: 3.2700268 }, + { type: "node", id: 6142725061, lat: 51.1497438, lon: 3.2697269 }, + { type: "node", id: 6142725064, lat: 51.1499256, lon: 3.2692437 }, + { type: "node", id: 6142727594, lat: 51.1500369, lon: 3.2694178 }, + { type: "node", id: 6142727597, lat: 51.1513713, lon: 3.2701053 }, + { type: "node", id: 6143075014, lat: 51.1512852, lon: 3.2702041 }, + { type: "node", id: 1554514655, lat: 51.1533288, lon: 3.2766885 }, + { type: "node", id: 1554514811, lat: 51.1537898, lon: 3.2754789 }, + { type: "node", id: 3211247019, lat: 51.1538788, lon: 3.2763248 }, + { type: "node", id: 3211247021, lat: 51.1538471, lon: 3.2764673 }, + { type: "node", id: 5745963922, lat: 51.1499683, lon: 3.2708447 }, + { type: "node", id: 5745963942, lat: 51.1530892, lon: 3.2704581 }, + { type: "node", id: 5745963943, lat: 51.1529125, lon: 3.2703349 }, + { type: "node", id: 5747535101, lat: 51.1533021, lon: 3.2705247 }, + { type: "node", id: 5747937659, lat: 51.1534063, lon: 3.2705828 }, + { type: "node", id: 5747937675, lat: 51.1521092, lon: 3.2702998 }, + { type: "node", id: 5747937676, lat: 51.1524591, lon: 3.2704822 }, + { type: "node", id: 5747937677, lat: 51.1527839, lon: 3.2706727 }, + { type: "node", id: 5747937678, lat: 51.1529841, lon: 3.2707276 }, + { type: "node", id: 5747937679, lat: 51.15326, lon: 3.2708913 }, + { type: "node", id: 5747937680, lat: 51.1535477, lon: 3.2712989 }, + { type: "node", id: 5747937681, lat: 51.1536831, lon: 3.2717402 }, + { type: "node", id: 5747937682, lat: 51.1536839, lon: 3.2721747 }, + { type: "node", id: 5747937683, lat: 51.1536469, lon: 3.2724161 }, + { type: "node", id: 5747937684, lat: 51.1534635, lon: 3.2730652 }, + { type: "node", id: 5747937685, lat: 51.1540625, lon: 3.2735735 }, + { type: "node", id: 5962339921, lat: 51.1535454, lon: 3.2761201 }, + { type: "node", id: 5962496725, lat: 51.1538672, lon: 3.276374 }, + { type: "node", id: 6142725040, lat: 51.1514014, lon: 3.2704388 }, + { type: "node", id: 6142725041, lat: 51.1514255, lon: 3.2722713 }, + { type: "node", id: 6142725042, lat: 51.1514044, lon: 3.2722821 }, + { type: "node", id: 6142725043, lat: 51.1513617, lon: 3.2722639 }, + { type: "node", id: 6142725044, lat: 51.1513141, lon: 3.2722165 }, + { type: "node", id: 6142725045, lat: 51.1512346, lon: 3.2721372 }, + { type: "node", id: 6142725046, lat: 51.1511314, lon: 3.2720304 }, + { type: "node", id: 6142725047, lat: 51.1509926, lon: 3.2718816 }, + { type: "node", id: 6142725048, lat: 51.1508222, lon: 3.2716946 }, + { type: "node", id: 6142725049, lat: 51.1507329, lon: 3.2715909 }, + { type: "node", id: 6142725050, lat: 51.1506561, lon: 3.2714915 }, + { type: "node", id: 6142725051, lat: 51.1506027, lon: 3.2714102 }, + { type: "node", id: 6142725052, lat: 51.1505293, lon: 3.2712712 }, + { type: "node", id: 6142725053, lat: 51.1504912, lon: 3.2711826 }, + { type: "node", id: 6142725054, lat: 51.1504464, lon: 3.2710615 }, + { type: "node", id: 6142725055, lat: 51.1503588, lon: 3.2707712 }, + { type: "node", id: 6142725056, lat: 51.1503432, lon: 3.2707336 }, + { type: "node", id: 6142725057, lat: 51.1503035, lon: 3.2706647 }, + { type: "node", id: 6142725058, lat: 51.1501763, lon: 3.2704663 }, + { type: "node", id: 6142725059, lat: 51.1500465, lon: 3.270256 }, + { type: "node", id: 6142725065, lat: 51.1511856, lon: 3.2703268 }, + { type: "node", id: 6142725066, lat: 51.1510244, lon: 3.2705705 }, + { type: "node", id: 6142725067, lat: 51.1509355, lon: 3.270823 }, + { type: "node", id: 6142725074, lat: 51.149723, lon: 3.271666 }, + { type: "node", id: 6142725076, lat: 51.1498322, lon: 3.2716555 }, + { type: "node", id: 6142725077, lat: 51.1499403, lon: 3.2715209 }, + { type: "node", id: 6142725078, lat: 51.1500262, lon: 3.2714548 }, + { type: "node", id: 6142725079, lat: 51.1501462, lon: 3.2714359 }, + { type: "node", id: 6142725080, lat: 51.1502306, lon: 3.2714572 }, + { type: "node", id: 6142725081, lat: 51.1502676, lon: 3.2714288 }, + { type: "node", id: 6142725084, lat: 51.1496118, lon: 3.2714056 }, + { type: "node", id: 6142727585, lat: 51.1497709, lon: 3.271258 }, + { type: "node", id: 6142727586, lat: 51.1496767, lon: 3.2713056 }, + { type: "node", id: 6142727587, lat: 51.1498468, lon: 3.2711798 }, + { type: "node", id: 6142727588, lat: 51.1500514, lon: 3.2703954 }, + { type: "node", id: 6142727589, lat: 51.1503033, lon: 3.270777 }, + { type: "node", id: 6142727590, lat: 51.1503141, lon: 3.2713705 }, + { type: "node", id: 6142727591, lat: 51.1498992, lon: 3.2710354 }, + { type: "node", id: 6142727592, lat: 51.1499461, lon: 3.270906 }, + { type: "node", id: 6142727595, lat: 51.1505973, lon: 3.2702942 }, + { type: "node", id: 6142727596, lat: 51.1508515, lon: 3.2706917 }, + { type: "node", id: 6142727598, lat: 51.1504337, lon: 3.2712326 }, + { type: "node", id: 6143074993, lat: 51.1501845, lon: 3.2706959 }, + { type: "node", id: 6143075018, lat: 51.1508744, lon: 3.2707275 }, + { type: "node", id: 8638721230, lat: 51.151458, lon: 3.2721756 }, + { type: "node", id: 8638721239, lat: 51.1514715, lon: 3.2719839 }, + { type: "node", id: 1554514618, lat: 51.1581789, lon: 3.2646401 }, + { type: "node", id: 1554514658, lat: 51.1578742, lon: 3.2653982 }, + { type: "node", id: 1554514750, lat: 51.1568056, lon: 3.2677508 }, + { type: "node", id: 1554514755, lat: 51.1573435, lon: 3.2665694 }, + { type: "node", id: 1554514831, lat: 51.1561729, lon: 3.2691965 }, + { type: "node", id: 3211247042, lat: 51.1557516, lon: 3.2701591 }, + { type: "node", id: 3211247054, lat: 51.1570321, lon: 3.2690614 }, + { type: "node", id: 3211247058, lat: 51.1571866, lon: 3.2685962 }, + { type: "node", id: 3211247059, lat: 51.1572111, lon: 3.2673633 }, + { type: "node", id: 3211247060, lat: 51.1572915, lon: 3.2701354 }, + { type: "node", id: 3211247261, lat: 51.157332, lon: 3.2685775 }, + { type: "node", id: 3211247265, lat: 51.1575267, lon: 3.2694937 }, + { type: "node", id: 3211247266, lat: 51.1576636, lon: 3.2677588 }, + { type: "node", id: 3211247291, lat: 51.15873, lon: 3.2640973 }, + { type: "node", id: 3211247328, lat: 51.1594831, lon: 3.2648765 }, + { type: "node", id: 3211247331, lat: 51.1596428, lon: 3.2643669 }, + { type: "node", id: 5747937641, lat: 51.1581621, lon: 3.2637783 }, + { type: "node", id: 5747937643, lat: 51.1576288, lon: 3.2640519 }, + { type: "node", id: 5747937644, lat: 51.1572672, lon: 3.2645347 }, + { type: "node", id: 5747937645, lat: 51.1568533, lon: 3.2650349 }, + { type: "node", id: 5747937646, lat: 51.1564933, lon: 3.2652213 }, + { type: "node", id: 5747937647, lat: 51.1562275, lon: 3.2654708 }, + { type: "node", id: 5747937648, lat: 51.1560046, lon: 3.2656894 }, + { type: "node", id: 5747937649, lat: 51.1556867, lon: 3.2659965 }, + { type: "node", id: 5747937650, lat: 51.1551778, lon: 3.2664082 }, + { type: "node", id: 5747937651, lat: 51.1550608, lon: 3.2664739 }, + { type: "node", id: 5747937652, lat: 51.1547446, lon: 3.2666805 }, + { type: "node", id: 5747937653, lat: 51.1546747, lon: 3.2667583 }, + { type: "node", id: 5747937654, lat: 51.1546066, lon: 3.266895 }, + { type: "node", id: 5747937655, lat: 51.1544363, lon: 3.2672456 }, + { type: "node", id: 5747937656, lat: 51.1543612, lon: 3.2677126 }, + { type: "node", id: 5747937688, lat: 51.1553145, lon: 3.2701637 }, + { type: "node", id: 5747937689, lat: 51.1564724, lon: 3.2675566 }, + { type: "node", id: 5747937690, lat: 51.1580352, lon: 3.2640939 }, + { type: "node", id: 5747937691, lat: 51.1581075, lon: 3.2639222 }, + { type: "node", id: 5962338038, lat: 51.1580663, lon: 3.2649607 }, + { type: "node", id: 5962340009, lat: 51.1585904, lon: 3.2644244 }, + { type: "node", id: 5962340010, lat: 51.1579212, lon: 3.2658517 }, + { type: "node", id: 5962340011, lat: 51.1575197, lon: 3.2676156 }, + { type: "node", id: 5962340012, lat: 51.1573374, lon: 3.2674543 }, + { type: "node", id: 5962340013, lat: 51.1574265, lon: 3.2698457 }, + { type: "node", id: 5962415543, lat: 51.1591427, lon: 3.2638615 }, + { type: "node", id: 5962415564, lat: 51.1563757, lon: 3.269274 }, + { type: "node", id: 5962444841, lat: 51.1584746, lon: 3.2639653 }, + { type: "node", id: 5962444846, lat: 51.1585428, lon: 3.2645259 }, + { type: "node", id: 5962444849, lat: 51.1586783, lon: 3.2642185 }, + { type: "node", id: 5962444850, lat: 51.1586559, lon: 3.2642708 }, + { type: "node", id: 5962496715, lat: 51.1577157, lon: 3.2657479 }, + { type: "node", id: 5962496718, lat: 51.1572172, lon: 3.2685923 }, + { type: "node", id: 5962496719, lat: 51.1571618, lon: 3.2686709 }, + { type: "node", id: 1554514640, lat: 51.1556541, lon: 3.2703864 }, + { type: "node", id: 1554514735, lat: 51.1546949, lon: 3.273022 }, + { type: "node", id: 1554514739, lat: 51.1552715, lon: 3.271391 }, + { type: "node", id: 1554514744, lat: 51.154165, lon: 3.2744194 }, + { type: "node", id: 3211247024, lat: 51.1541505, lon: 3.2756042 }, + { type: "node", id: 3211247029, lat: 51.154764, lon: 3.2739293 }, + { type: "node", id: 3211247032, lat: 51.1550284, lon: 3.2731777 }, + { type: "node", id: 3211247034, lat: 51.1553184, lon: 3.2723493 }, + { type: "node", id: 3211247036, lat: 51.1554324, lon: 3.2715569 }, + { type: "node", id: 3211247037, lat: 51.1555577, lon: 3.2717139 }, + { type: "node", id: 3211247039, lat: 51.1556614, lon: 3.2709968 }, + { type: "node", id: 5747937686, lat: 51.1543123, lon: 3.272868 }, + { type: "node", id: 5747937687, lat: 51.1551484, lon: 3.2706069 }, + { type: "node", id: 5962415565, lat: 51.1558985, lon: 3.2703959 }, + { type: "node", id: 3170562436, lat: 51.138924, lon: 3.2898032 }, + { type: "node", id: 7114502205, lat: 51.1385818, lon: 3.2899019 }, + { type: "node", id: 7114502206, lat: 51.1387244, lon: 3.289738 }, + { type: "node", id: 1475188516, lat: 51.1392568, lon: 3.2899836 }, + { type: "node", id: 1475188519, lat: 51.1391868, lon: 3.2900136 }, + { type: "node", id: 3170562392, lat: 51.1364433, lon: 3.2910119 }, + { type: "node", id: 3170562394, lat: 51.1368907, lon: 3.291708 }, + { type: "node", id: 3170562395, lat: 51.137106, lon: 3.2924864 }, + { type: "node", id: 3170562396, lat: 51.137265, lon: 3.2919875 }, + { type: "node", id: 3170562397, lat: 51.1374825, lon: 3.2913695 }, + { type: "node", id: 3170562402, lat: 51.1378658, lon: 3.2906394 }, + { type: "node", id: 3170562410, lat: 51.1378512, lon: 3.2905949 }, + { type: "node", id: 3170562431, lat: 51.138431, lon: 3.2910415 }, + { type: "node", id: 3170562437, lat: 51.1389596, lon: 3.2905649 }, + { type: "node", id: 3170562439, lat: 51.1391839, lon: 3.2903042 }, + { type: "node", id: 6627605025, lat: 51.1393014, lon: 3.2899729 }, + { type: "node", id: 7114502200, lat: 51.139377, lon: 3.2901873 }, + { type: "node", id: 7114502201, lat: 51.1395801, lon: 3.2901445 }, + { type: "node", id: 7114502202, lat: 51.1393183, lon: 3.290151 }, + { type: "node", id: 7114502203, lat: 51.1395636, lon: 3.2902055 }, + { type: "node", id: 7114502207, lat: 51.13835, lon: 3.2901225 }, + { type: "node", id: 7114502208, lat: 51.138259, lon: 3.2902362 }, + { type: "node", id: 7114502209, lat: 51.1381351, lon: 3.2903372 }, + { type: "node", id: 7114502211, lat: 51.1376987, lon: 3.2921954 }, + { type: "node", id: 7114502212, lat: 51.1373598, lon: 3.2927952 }, + { type: "node", id: 7114502214, lat: 51.1364782, lon: 3.2933157 }, + { type: "node", id: 7114502215, lat: 51.1356558, lon: 3.2938122 }, + { type: "node", id: 7114502216, lat: 51.1364577, lon: 3.2922382 }, + { type: "node", id: 7114502217, lat: 51.1365055, lon: 3.2917901 }, + { type: "node", id: 7114502218, lat: 51.1369977, lon: 3.2911659 }, + { type: "node", id: 7114502219, lat: 51.1367691, lon: 3.2924051 }, + { type: "node", id: 7114502220, lat: 51.1367022, lon: 3.2927526 }, + { type: "node", id: 7114502221, lat: 51.1367354, lon: 3.2929091 }, + { type: "node", id: 7114502222, lat: 51.1367962, lon: 3.2930156 }, + { type: "node", id: 7114502223, lat: 51.1369436, lon: 3.2928838 }, + { type: "node", id: 7114502224, lat: 51.1365124, lon: 3.2914123 }, + { type: "node", id: 7114502225, lat: 51.135804, lon: 3.2926995 }, + { type: "node", id: 7114502226, lat: 51.1354385, lon: 3.2929897 }, + { type: "node", id: 7114502227, lat: 51.135128, lon: 3.2935318 }, + { type: "node", id: 7114502228, lat: 51.1352851, lon: 3.2947114 }, + { type: "node", id: 7114502230, lat: 51.135254, lon: 3.2946241 }, + { type: "node", id: 7114502234, lat: 51.1354035, lon: 3.2959064 }, + { type: "node", id: 7114502235, lat: 51.1362234, lon: 3.2955042 }, + { type: "node", id: 7114502236, lat: 51.1362859, lon: 3.2964578 }, + { type: "node", id: 7114502240, lat: 51.1379417, lon: 3.2917937 }, + { type: "node", id: 8294886142, lat: 51.1394294, lon: 3.290127 }, + { type: "node", id: 7114502233, lat: 51.1354361, lon: 3.2966386 }, + { type: "node", id: 7114502237, lat: 51.1362025, lon: 3.2964851 }, + { type: "node", id: 7114502238, lat: 51.1362303, lon: 3.2969415 }, + { type: "node", id: 7114502239, lat: 51.1355032, lon: 3.297292 }, + { type: "node", id: 1494027348, lat: 51.1419125, lon: 3.3028989 }, + { type: "node", id: 1494027349, lat: 51.1419929, lon: 3.3024731 }, + { type: "node", id: 1951759298, lat: 51.1421456, lon: 3.3024503 }, + { type: "node", id: 6037556099, lat: 51.1420961, lon: 3.3023333 }, + { type: "node", id: 6037556100, lat: 51.142124, lon: 3.302398 }, + { type: "node", id: 6037556101, lat: 51.1421476, lon: 3.3023664 }, + { type: "node", id: 6037556102, lat: 51.1421686, lon: 3.3023966 }, + { type: "node", id: 6037556103, lat: 51.1421392, lon: 3.3024395 }, + { type: "node", id: 6037556104, lat: 51.1419363, lon: 3.3027154 }, + { type: "node", id: 6037556128, lat: 51.1421612, lon: 3.3024776 }, + { type: "node", id: 6037556129, lat: 51.1421598, lon: 3.3025191 }, + { type: "node", id: 1554514647, lat: 51.1510862, lon: 3.2830051 }, + { type: "node", id: 1554514679, lat: 51.1528746, lon: 3.2778124 }, + { type: "node", id: 1554514683, lat: 51.152459, lon: 3.2789175 }, + { type: "node", id: 1554514730, lat: 51.1514294, lon: 3.2817821 }, + { type: "node", id: 1554514747, lat: 51.1515758, lon: 3.2812966 }, + { type: "node", id: 1554514765, lat: 51.1518248, lon: 3.2805268 }, + { type: "node", id: 1554514773, lat: 51.15167, lon: 3.280964 }, + { type: "node", id: 1554514775, lat: 51.1517205, lon: 3.2808218 }, + { type: "node", id: 3211246555, lat: 51.1516922, lon: 3.2825734 }, + { type: "node", id: 3211246995, lat: 51.1531241, lon: 3.2787307 }, + { type: "node", id: 3211246997, lat: 51.1532256, lon: 3.2781298 }, + { type: "node", id: 3211247004, lat: 51.1532551, lon: 3.2780324 }, + { type: "node", id: 3211247008, lat: 51.1532769, lon: 3.2780262 }, + { type: "node", id: 3211247010, lat: 51.1533547, lon: 3.2780182 }, + { type: "node", id: 3211247013, lat: 51.1534072, lon: 3.2778679 }, + { type: "node", id: 5962338069, lat: 51.1531224, lon: 3.2785269 }, + { type: "node", id: 5962338070, lat: 51.1532454, lon: 3.2781435 }, + { type: "node", id: 5962338071, lat: 51.1531839, lon: 3.2785716 }, + { type: "node", id: 9284562682, lat: 51.153154, lon: 3.2784189 }, + { type: "node", id: 1554514649, lat: 51.1502146, lon: 3.285505 }, + { type: "node", id: 1554514661, lat: 51.1499067, lon: 3.2861781 }, + { type: "node", id: 1554514682, lat: 51.150038, lon: 3.2859179 }, + { type: "node", id: 1554514693, lat: 51.1498077, lon: 3.2862946 }, + { type: "node", id: 1554514703, lat: 51.1497368, lon: 3.2863685 }, + { type: "node", id: 1554514726, lat: 51.1498748, lon: 3.2862129 }, + { type: "node", id: 1554514783, lat: 51.1497118, lon: 3.2864368 }, + { type: "node", id: 1554514787, lat: 51.1499524, lon: 3.2860908 }, + { type: "node", id: 1554514789, lat: 51.1506254, lon: 3.2845029 }, + { type: "node", id: 3203029856, lat: 51.1498994, lon: 3.2867119 }, + { type: "node", id: 3211246542, lat: 51.1505988, lon: 3.2851597 }, + { type: "node", id: 5482441357, lat: 51.1504615, lon: 3.2836968 }, + { type: "node", id: 7459154782, lat: 51.1501541, lon: 3.2834044 }, + { type: "node", id: 7459154784, lat: 51.14999, lon: 3.2838049 }, + { type: "node", id: 7459257985, lat: 51.1496077, lon: 3.2846412 }, + { type: "node", id: 7602479690, lat: 51.150018, lon: 3.2848987 }, + { type: "node", id: 7602479691, lat: 51.149806, lon: 3.2842251 }, + { type: "node", id: 7602479692, lat: 51.1504686, lon: 3.2836785 }, + { type: "node", id: 7602479693, lat: 51.1504309, lon: 3.2838118 }, + { type: "node", id: 7602479694, lat: 51.1504255, lon: 3.2838072 }, + { type: "node", id: 7602479695, lat: 51.1503423, lon: 3.2840392 }, + { type: "node", id: 7602479696, lat: 51.1501698, lon: 3.2845196 }, + { type: "node", id: 7727406921, lat: 51.1497539, lon: 3.2864986 }, + { type: "node", id: 8945026754, lat: 51.1518736, lon: 3.2973911 }, + { type: "node", id: 8945026755, lat: 51.1521646, lon: 3.297599 }, + { type: "node", id: 8945026756, lat: 51.1522042, lon: 3.2974333 }, + { type: "node", id: 8945026757, lat: 51.1519287, lon: 3.2972449 }, + { type: "node", id: 416917612, lat: 51.1610842, lon: 3.2579973 }, + { type: "node", id: 1948836192, lat: 51.1607104, lon: 3.2576119 }, + { type: "node", id: 1948836193, lat: 51.1607138, lon: 3.2577265 }, + { type: "node", id: 1948836194, lat: 51.1607261, lon: 3.2576817 }, + { type: "node", id: 1948836195, lat: 51.1607396, lon: 3.2576322 }, + { type: "node", id: 1948836202, lat: 51.1608376, lon: 3.2582648 }, + { type: "node", id: 1948836209, lat: 51.1608764, lon: 3.2583297 }, + { type: "node", id: 1948836218, lat: 51.1609914, lon: 3.2580679 }, + { type: "node", id: 1948836237, lat: 51.1610295, lon: 3.2581432 }, + { type: "node", id: 1948836277, lat: 51.1610699, lon: 3.2580833 }, + { type: "node", id: 5349884600, lat: 51.1623355, lon: 3.2627383 }, + { type: "node", id: 5349884601, lat: 51.1621681, lon: 3.2630327 }, + { type: "node", id: 5349884602, lat: 51.1622043, lon: 3.2630816 }, + { type: "node", id: 5349884603, lat: 51.162375, lon: 3.2627913 }, + { type: "node", id: 5349884604, lat: 51.1622594, lon: 3.2633478 }, + { type: "node", id: 5349884605, lat: 51.1621955, lon: 3.2632528 }, + { type: "node", id: 5349884606, lat: 51.1623824, lon: 3.2629335 }, + { type: "node", id: 5349884607, lat: 51.1624462, lon: 3.2630285 }, + { type: "node", id: 5745739924, lat: 51.1608677, lon: 3.2604028 }, + { type: "node", id: 5745739925, lat: 51.160749, lon: 3.2605301 }, + { type: "node", id: 5745739927, lat: 51.160719, lon: 3.2600865 }, + { type: "node", id: 1494027341, lat: 51.1418334, lon: 3.3035292 }, + { type: "node", id: 1494027359, lat: 51.1413757, lon: 3.3074828 }, + { type: "node", id: 1494027374, lat: 51.14132, lon: 3.3086261 }, + { type: "node", id: 1494027376, lat: 51.1415625, lon: 3.3055864 }, + { type: "node", id: 1494027385, lat: 51.1414514, lon: 3.3065628 }, + { type: "node", id: 1494027386, lat: 51.1416927, lon: 3.3044972 }, + { type: "node", id: 1494027389, lat: 51.1412677, lon: 3.3093509 }, + { type: "node", id: 6037556130, lat: 51.1420599, lon: 3.303249 }, + { type: "node", id: 6037556131, lat: 51.1419859, lon: 3.3038109 }, + { type: "node", id: 6037556132, lat: 51.1418857, lon: 3.3046747 }, + { type: "node", id: 6037556133, lat: 51.1418916, lon: 3.3050562 }, + { type: "node", id: 6037556134, lat: 51.141927, lon: 3.3054665 }, + { type: "node", id: 6037556135, lat: 51.1419362, lon: 3.3056543 }, + { type: "node", id: 6037556136, lat: 51.1419369, lon: 3.3060001 }, + { type: "node", id: 6037556137, lat: 51.1419328, lon: 3.3061768 }, + { type: "node", id: 6037556138, lat: 51.1419139, lon: 3.3062337 }, + { type: "node", id: 6037556139, lat: 51.1419032, lon: 3.3062932 }, + { type: "node", id: 6037556140, lat: 51.1419001, lon: 3.3065944 }, + { type: "node", id: 6037556141, lat: 51.1419262, lon: 3.3066872 }, + { type: "node", id: 6037556142, lat: 51.1419119, lon: 3.3074563 }, + { type: "node", id: 6037556143, lat: 51.1419027, lon: 3.3079359 }, + { type: "node", id: 6037556144, lat: 51.1418924, lon: 3.3090764 }, + { type: "node", id: 6037556145, lat: 51.1418843, lon: 3.3094015 }, + { type: "node", id: 1494027342, lat: 51.1411958, lon: 3.3123073 }, + { type: "node", id: 1494027346, lat: 51.1411509, lon: 3.3144776 }, + { type: "node", id: 1494027353, lat: 51.141193, lon: 3.3127209 }, + { type: "node", id: 1494027361, lat: 51.1411944, lon: 3.3118627 }, + { type: "node", id: 1494027366, lat: 51.1411788, lon: 3.3132011 }, + { type: "node", id: 1494027368, lat: 51.1411457, lon: 3.3148322 }, + { type: "node", id: 1494027372, lat: 51.1412147, lon: 3.3112199 }, + { type: "node", id: 1494027373, lat: 51.1411641, lon: 3.3142758 }, + { type: "node", id: 1494027378, lat: 51.1412256, lon: 3.3114828 }, + { type: "node", id: 1494027382, lat: 51.1412174, lon: 3.3105548 }, + { type: "node", id: 6037556098, lat: 51.1411339, lon: 3.315984 }, + { type: "node", id: 6037556146, lat: 51.1418276, lon: 3.309789 }, + { type: "node", id: 6037556148, lat: 51.1418583, lon: 3.3096011 }, + { type: "node", id: 6037556149, lat: 51.1418039, lon: 3.3098357 }, + { type: "node", id: 6037556150, lat: 51.1417743, lon: 3.3098805 }, + { type: "node", id: 6037556151, lat: 51.1416977, lon: 3.3101207 }, + { type: "node", id: 6037556152, lat: 51.1416834, lon: 3.3102232 }, + { type: "node", id: 6037556153, lat: 51.1416823, lon: 3.3102705 }, + { type: "node", id: 6037556154, lat: 51.1416302, lon: 3.3105557 }, + { type: "node", id: 6037556155, lat: 51.1416042, lon: 3.3107844 }, + { type: "node", id: 6037556156, lat: 51.1415929, lon: 3.3110547 }, + { type: "node", id: 6037556157, lat: 51.1415904, lon: 3.3118741 }, + { type: "node", id: 6037556158, lat: 51.1415817, lon: 3.3145778 }, + { type: "node", id: 6037556159, lat: 51.14157, lon: 3.3146421 }, + { type: "node", id: 6037556160, lat: 51.1415557, lon: 3.3146975 }, + { type: "node", id: 6037556161, lat: 51.1415541, lon: 3.3148416 }, + { type: "node", id: 6037556162, lat: 51.1415551, lon: 3.3149739 }, + { type: "node", id: 6037556163, lat: 51.1415802, lon: 3.3150635 }, + { type: "node", id: 1494027343, lat: 51.1410607, lon: 3.3204128 }, + { type: "node", id: 1494027365, lat: 51.141078, lon: 3.3193848 }, + { type: "node", id: 1494027369, lat: 51.1410588, lon: 3.3212214 }, + { type: "node", id: 1494027380, lat: 51.1410813, lon: 3.317924 }, + { type: "node", id: 6037556105, lat: 51.141131, lon: 3.31628 }, + { type: "node", id: 6037556106, lat: 51.1411581, lon: 3.3165005 }, + { type: "node", id: 6037556107, lat: 51.141174, lon: 3.3167222 }, + { type: "node", id: 6037556108, lat: 51.1411155, lon: 3.3169939 }, + { type: "node", id: 6037556109, lat: 51.1410508, lon: 3.3221866 }, + { type: "node", id: 6037556110, lat: 51.1410676, lon: 3.3223383 }, + { type: "node", id: 6037556164, lat: 51.1415792, lon: 3.3164565 }, + { type: "node", id: 6037556165, lat: 51.141573, lon: 3.3172357 }, + { type: "node", id: 6037556166, lat: 51.1415771, lon: 3.3182023 }, + { type: "node", id: 6037556167, lat: 51.1415495, lon: 3.3182625 }, + { type: "node", id: 6037556168, lat: 51.1415495, lon: 3.3185605 }, + { type: "node", id: 6037556169, lat: 51.1415684, lon: 3.3186069 }, + { type: "node", id: 6037556170, lat: 51.1415633, lon: 3.3207654 }, + { type: "node", id: 6037556171, lat: 51.1415444, lon: 3.3214131 }, + { type: "node", id: 6037556172, lat: 51.1415097, lon: 3.3218544 }, + { type: "node", id: 6037556173, lat: 51.1414642, lon: 3.3225703 }, + { type: "node", id: 1493821404, lat: 51.1410663, lon: 3.3233361 }, + { type: "node", id: 1494027355, lat: 51.1410465, lon: 3.3227899 }, + { type: "node", id: 1801373604, lat: 51.1410713, lon: 3.3237592 }, + { type: "node", id: 1951759314, lat: 51.1411466, lon: 3.3245554 }, + { type: "node", id: 2474247506, lat: 51.1411643, lon: 3.3245589 }, + { type: "node", id: 6037556111, lat: 51.1410551, lon: 3.3241916 }, + { type: "node", id: 6037556112, lat: 51.1410504, lon: 3.3243751 }, + { type: "node", id: 6037556118, lat: 51.1410331, lon: 3.3245183 }, + { type: "node", id: 6037556120, lat: 51.1411823, lon: 3.3245635 }, + { type: "node", id: 6037556174, lat: 51.1414121, lon: 3.3230419 }, + { type: "node", id: 6037556175, lat: 51.1413953, lon: 3.3230745 }, + { type: "node", id: 6037556176, lat: 51.1413682, lon: 3.323164 }, + { type: "node", id: 6037556177, lat: 51.1413426, lon: 3.3233187 }, + { type: "node", id: 6037556178, lat: 51.1413353, lon: 3.3234317 }, + { type: "node", id: 6037556179, lat: 51.1413527, lon: 3.3235128 }, + { type: "node", id: 6037556180, lat: 51.141294, lon: 3.3239374 }, + { type: "node", id: 3702878648, lat: 51.1401344, lon: 3.3337214 }, + { type: "node", id: 3702878654, lat: 51.1395918, lon: 3.3338166 }, + { type: "node", id: 3702926557, lat: 51.1404241, lon: 3.333592 }, + { type: "node", id: 3702926558, lat: 51.1406229, lon: 3.3335608 }, + { type: "node", id: 3702926559, lat: 51.1406259, lon: 3.3336496 }, + { type: "node", id: 3702926560, lat: 51.1404301, lon: 3.333676 }, + { type: "node", id: 3702926561, lat: 51.1403789, lon: 3.3336784 }, + { type: "node", id: 3702926562, lat: 51.1403729, lon: 3.3335752 }, + { type: "node", id: 3702926563, lat: 51.1403021, lon: 3.333592 }, + { type: "node", id: 3702926564, lat: 51.1402991, lon: 3.3335295 }, + { type: "node", id: 3702926565, lat: 51.1402253, lon: 3.3335392 }, + { type: "node", id: 3702926566, lat: 51.1402283, lon: 3.3336112 }, + { type: "node", id: 3702926567, lat: 51.1401304, lon: 3.3336285 }, + { type: "node", id: 3702926568, lat: 51.1400111, lon: 3.3337421 }, + { type: "node", id: 3702956173, lat: 51.1406298, lon: 3.3321023 }, + { type: "node", id: 3702956174, lat: 51.140115, lon: 3.3325162 }, + { type: "node", id: 3702956175, lat: 51.1400702, lon: 3.3323734 }, + { type: "node", id: 3702956176, lat: 51.1405896, lon: 3.3319784 }, + { type: "node", id: 3702969714, lat: 51.1404452, lon: 3.3324432 }, + { type: "node", id: 3702969715, lat: 51.1402175, lon: 3.3326312 }, + { type: "node", id: 3702969716, lat: 51.1401965, lon: 3.3325686 }, + { type: "node", id: 3702969717, lat: 51.1404269, lon: 3.3323835 }, + { type: "node", id: 7713176909, lat: 51.1387744, lon: 3.3335489 }, + { type: "node", id: 7713176910, lat: 51.1389364, lon: 3.3334306 }, + { type: "node", id: 7713176911, lat: 51.1387319, lon: 3.3327211 }, + { type: "node", id: 7713176912, lat: 51.1385501, lon: 3.332864 }, + { type: "node", id: 8292789053, lat: 51.1400165, lon: 3.3338082 }, + { type: "node", id: 8292789054, lat: 51.1396286, lon: 3.333874 }, + { type: "node", id: 8378097124, lat: 51.140324, lon: 3.3355884 }, + { type: "node", id: 8378097125, lat: 51.1403179, lon: 3.3355055 }, + { type: "node", id: 8378097126, lat: 51.1406041, lon: 3.3354515 }, + { type: "node", id: 8378097127, lat: 51.1406102, lon: 3.3355344 }, + { type: "node", id: 8378097131, lat: 51.14074, lon: 3.3354785 }, + { type: "node", id: 8378097132, lat: 51.1407879, lon: 3.3354717 }, + { type: "node", id: 8378097133, lat: 51.1407993, lon: 3.3356206 }, + { type: "node", id: 1317838317, lat: 51.1372361, lon: 3.338639 }, + { type: "node", id: 1317838328, lat: 51.1369619, lon: 3.3386232 }, + { type: "node", id: 1317838331, lat: 51.1371806, lon: 3.3384037 }, + { type: "node", id: 1384096112, lat: 51.1371794, lon: 3.3384971 }, + { type: "node", id: 1625199827, lat: 51.1408446, lon: 3.3375722 }, + { type: "node", id: 1625199950, lat: 51.1407563, lon: 3.3373232 }, + { type: "node", id: 3227068446, lat: 51.1369433, lon: 3.3387663 }, + { type: "node", id: 3227068456, lat: 51.1371252, lon: 3.3388226 }, + { type: "node", id: 3227068565, lat: 51.1371943, lon: 3.3389942 }, + { type: "node", id: 3227068568, lat: 51.1372698, lon: 3.3389467 }, + { type: "node", id: 6227395991, lat: 51.1392741, lon: 3.3367141 }, + { type: "node", id: 6227395992, lat: 51.1392236, lon: 3.3367356 }, + { type: "node", id: 6227395993, lat: 51.139313, lon: 3.3369193 }, + { type: "node", id: 6227395997, lat: 51.1392584, lon: 3.3369405 }, + { type: "node", id: 6414352053, lat: 51.1374056, lon: 3.3395184 }, + { type: "node", id: 6414352054, lat: 51.1371278, lon: 3.3390236 }, + { type: "node", id: 6414352055, lat: 51.1372589, lon: 3.3395769 }, + { type: "node", id: 6414374315, lat: 51.1369166, lon: 3.338648 }, + { type: "node", id: 6414374316, lat: 51.1369683, lon: 3.3388767 }, + { type: "node", id: 6414374317, lat: 51.1370053, lon: 3.3388543 }, + { type: "node", id: 6414374318, lat: 51.1371169, lon: 3.3387872 }, + { type: "node", id: 6414374319, lat: 51.1371271, lon: 3.3388309 }, + { type: "node", id: 6414374320, lat: 51.137238, lon: 3.3387628 }, + { type: "node", id: 6414374321, lat: 51.1372074, lon: 3.338651 }, + { type: "node", id: 6414383775, lat: 51.1407766, lon: 3.3373855 }, + { type: "node", id: 6416001161, lat: 51.1371631, lon: 3.3390024 }, + { type: "node", id: 7274233390, lat: 51.1372936, lon: 3.3395562 }, + { type: "node", id: 7274233391, lat: 51.137309, lon: 3.3396231 }, + { type: "node", id: 7274233395, lat: 51.1374325, lon: 3.3396124 }, + { type: "node", id: 7274233396, lat: 51.137422, lon: 3.3395803 }, + { type: "node", id: 7274233397, lat: 51.1373776, lon: 3.3396587 }, + { type: "node", id: 8279842668, lat: 51.1371603, lon: 3.3384139 }, + { type: "node", id: 8377691836, lat: 51.1409346, lon: 3.337164 }, + { type: "node", id: 8378081366, lat: 51.140887, lon: 3.3372065 }, + { type: "node", id: 8378081386, lat: 51.1407701, lon: 3.3373106 }, + { type: "node", id: 8378081429, lat: 51.1408204, lon: 3.3372657 }, + { type: "node", id: 8378097128, lat: 51.1407837, lon: 3.3359331 }, + { type: "node", id: 8378097129, lat: 51.1407736, lon: 3.3358962 }, + { type: "node", id: 8378097130, lat: 51.1407627, lon: 3.3358224 }, + { type: "node", id: 8378097134, lat: 51.1408355, lon: 3.3359157 }, + { type: "node", id: 1625199938, lat: 51.1411535, lon: 3.3372917 }, + { type: "node", id: 1625199951, lat: 51.1410675, lon: 3.3370397 }, + { type: "node", id: 6100803125, lat: 51.1672329, lon: 3.3331872 }, + { type: "node", id: 6100803128, lat: 51.1672258, lon: 3.3333568 }, + { type: "node", id: 2577910530, lat: 51.1685713, lon: 3.3356917 }, + { type: "node", id: 2577910542, lat: 51.1681925, lon: 3.3356582 }, + { type: "node", id: 2577910543, lat: 51.1685972, lon: 3.3356499 }, + { type: "node", id: 3645188881, lat: 51.1679055, lon: 3.3337545 }, + { type: "node", id: 6100803124, lat: 51.1673065, lon: 3.3331116 }, + { type: "node", id: 6100803126, lat: 51.1672788, lon: 3.3336938 }, + { type: "node", id: 6100803127, lat: 51.1672758, lon: 3.3333629 }, + { type: "node", id: 6100803129, lat: 51.1675567, lon: 3.3331312 }, + { type: "node", id: 6100803130, lat: 51.1675525, lon: 3.3333096 }, + { type: "node", id: 6100803131, lat: 51.1679171, lon: 3.3333454 }, + { type: "node", id: 2577910520, lat: 51.1681983, lon: 3.3360484 }, + { type: "node", id: 2577910526, lat: 51.1687824, lon: 3.3362878 }, + { type: "node", id: 2577910545, lat: 51.16885, lon: 3.3360431 }, + { type: "node", id: 6145151107, lat: 51.1694008, lon: 3.3386946 }, + { type: "node", id: 6145151108, lat: 51.1688319, lon: 3.3385737 }, + { type: "node", id: 6145151109, lat: 51.1688258, lon: 3.3386467 }, + { type: "node", id: 6145151110, lat: 51.1693942, lon: 3.3387681 }, + { type: "node", id: 6145151114, lat: 51.1691188, lon: 3.3387104 }, + { type: "node", id: 6145151115, lat: 51.1688569, lon: 3.3386537 }, + { type: "node", id: 6418533335, lat: 51.1685948, lon: 3.3361624 }, + { type: "node", id: 3190107371, lat: 51.1741471, lon: 3.3384365 }, + { type: "node", id: 3190107374, lat: 51.1741746, lon: 3.3388275 }, + { type: "node", id: 3190107377, lat: 51.1741968, lon: 3.3384276 }, + { type: "node", id: 3190107393, lat: 51.1743618, lon: 3.3383982 }, + { type: "node", id: 3190107400, lat: 51.1744043, lon: 3.3384036 }, + { type: "node", id: 3190107407, lat: 51.1744809, lon: 3.3386827 }, + { type: "node", id: 3190107408, lat: 51.1744875, lon: 3.3387716 }, + { type: "node", id: 3190107415, lat: 51.1746495, lon: 3.3386505 }, + { type: "node", id: 3190107416, lat: 51.1746548, lon: 3.3387211 }, + { type: "node", id: 3190107420, lat: 51.1747424, lon: 3.3387044 }, + { type: "node", id: 3190107423, lat: 51.1747681, lon: 3.3390477 }, + { type: "node", id: 3190107424, lat: 51.1747706, lon: 3.3384763 }, + { type: "node", id: 3190107428, lat: 51.1747993, lon: 3.3384858 }, + { type: "node", id: 3190107432, lat: 51.1748198, lon: 3.3387594 }, + { type: "node", id: 3190107435, lat: 51.1750772, lon: 3.3389888 }, + { type: "node", id: 3190107439, lat: 51.1752356, lon: 3.3387751 }, + { type: "node", id: 3190107442, lat: 51.1752492, lon: 3.338956 }, + { type: "node", id: 7390697534, lat: 51.1297479, lon: 3.3578326 }, + { type: "node", id: 7390697550, lat: 51.1297706, lon: 3.3588263 }, + { type: "node", id: 7468171405, lat: 51.1279967, lon: 3.3559306 }, + { type: "node", id: 7468171406, lat: 51.1288847, lon: 3.3598888 }, + { type: "node", id: 7468171407, lat: 51.1288383, lon: 3.3606949 }, + { type: "node", id: 7468171408, lat: 51.128796, lon: 3.3619912 }, + { type: "node", id: 7468171422, lat: 51.1285744, lon: 3.3574498 }, + { type: "node", id: 7468171423, lat: 51.1288319, lon: 3.3586353 }, + { type: "node", id: 7468171426, lat: 51.1287158, lon: 3.3579031 }, + { type: "node", id: 7468171428, lat: 51.1288787, lon: 3.3588438 }, + { type: "node", id: 7468171429, lat: 51.1288942, lon: 3.3595607 }, + { type: "node", id: 7468171431, lat: 51.1288954, lon: 3.3592459 }, + { type: "node", id: 7468171433, lat: 51.1287831, lon: 3.3582062 }, + { type: "node", id: 7468171444, lat: 51.1287924, lon: 3.361646 }, + { type: "node", id: 7727393197, lat: 51.1288566, lon: 3.3555067 }, + { type: "node", id: 7727393198, lat: 51.1292196, lon: 3.356551 }, + { type: "node", id: 7727393199, lat: 51.129399, lon: 3.3566378 }, + { type: "node", id: 7190873584, lat: 51.1287267, lon: 3.3682604 }, + { type: "node", id: 7190927785, lat: 51.1287057, lon: 3.3681343 }, + { type: "node", id: 7190927786, lat: 51.1287121, lon: 3.3673045 }, + { type: "node", id: 7190927787, lat: 51.1286419, lon: 3.3641562 }, + { type: "node", id: 7190927788, lat: 51.1286407, lon: 3.3644331 }, + { type: "node", id: 7190927789, lat: 51.128655, lon: 3.3650286 }, + { type: "node", id: 7190927790, lat: 51.128688, lon: 3.3661227 }, + { type: "node", id: 7190927791, lat: 51.1286681, lon: 3.3635132 }, + { type: "node", id: 7190927792, lat: 51.1286863, lon: 3.3631709 }, + { type: "node", id: 7190927793, lat: 51.1286419, lon: 3.3639077 }, + { type: "node", id: 7468171409, lat: 51.1288502, lon: 3.3621534 }, + { type: "node", id: 7468171410, lat: 51.1288454, lon: 3.3623212 }, + { type: "node", id: 7468171411, lat: 51.1287924, lon: 3.36282 }, + { type: "node", id: 7468171412, lat: 51.1287335, lon: 3.363023 }, + { type: "node", id: 7468171413, lat: 51.1286942, lon: 3.363003 }, + { type: "node", id: 7602692242, lat: 51.1287128, lon: 3.3675239 }, + { type: "node", id: 7727393200, lat: 51.129492, lon: 3.3624712 }, + { type: "node", id: 7727393201, lat: 51.1294385, lon: 3.3629631 }, + { type: "node", id: 7727393202, lat: 51.1293629, lon: 3.3638402 }, + { type: "node", id: 7727393203, lat: 51.1293586, lon: 3.3662748 }, + { type: "node", id: 7727393204, lat: 51.1292824, lon: 3.3662748 }, + { type: "node", id: 7727393205, lat: 51.1292881, lon: 3.3667997 }, + { type: "node", id: 7727393206, lat: 51.1293315, lon: 3.3682391 }, + { type: "node", id: 7190873576, lat: 51.1287907, lon: 3.36923 }, + { type: "node", id: 7190873577, lat: 51.1289144, lon: 3.369348 }, + { type: "node", id: 7190873578, lat: 51.1288472, lon: 3.3692931 }, + { type: "node", id: 7190873582, lat: 51.1287752, lon: 3.3691898 }, + { type: "node", id: 7468171450, lat: 51.1287442, lon: 3.3685677 }, + { type: "node", id: 7468171451, lat: 51.1290764, lon: 3.3693851 }, + { type: "node", id: 7468171455, lat: 51.1289835, lon: 3.3693823 }, + { type: "node", id: 7727393207, lat: 51.1293768, lon: 3.368773 }, + { type: "node", id: 7727393208, lat: 51.1293116, lon: 3.3692219 }, + { type: "node", id: 7727393209, lat: 51.1293718, lon: 3.368916 }, + { type: "node", id: 7727393210, lat: 51.1292845, lon: 3.3692843 }, + { type: "node", id: 7727393211, lat: 51.1292517, lon: 3.3693264 }, + { type: "node", id: 7727393212, lat: 51.1291932, lon: 3.3693491 }, + { type: "node", id: 8781449489, lat: 51.1293725, lon: 3.3688971 }, + { + type: "way", + id: 640979978, + nodes: [ + 6037556099, 6037556100, 6037556101, 6037556102, 6037556103, 1951759298, + 6037556128, 6037556129, 6037556130, 6037556131, 6037556132, 6037556133, + 6037556134, 6037556135, 6037556136, 6037556137, 6037556138, 6037556139, + 6037556140, 6037556141, 6037556142, 6037556143, 6037556144, 6037556145, + 6037556148, 6037556146, 6037556149, 6037556150, 6037556151, 6037556152, + 6037556153, 6037556154, 6037556155, 6037556156, 6037556157, 6037556158, + 6037556159, 6037556160, 6037556161, 6037556162, 6037556163, 6037556164, + 6037556165, 6037556166, 6037556167, 6037556168, 6037556169, 6037556170, + 6037556171, 6037556172, 6037556173, 6037556174, 6037556175, 6037556176, + 6037556177, 6037556178, 6037556179, 6037556180, 6037556120, 2474247506, + 1951759314, 6037556118, + ], + }, + { + type: "way", + id: 640979982, + nodes: [ + 6037556099, 1494027349, 6037556104, 1494027348, 1494027341, 1494027386, + 1494027376, 1494027385, 1494027359, 1494027374, 1494027389, 1494027382, + 1494027372, 1494027378, 1494027361, 1494027342, 1494027353, 1494027366, + 1494027373, 1494027346, 1494027368, 6037556098, 6037556105, 6037556106, + 6037556107, 6037556108, 1494027380, 1494027365, 1494027343, 1494027369, + 6037556109, 6037556110, 1494027355, 1493821404, 1801373604, 6037556111, + 6037556112, 6037556118, + ], + }, + { + type: "way", + id: 184402325, + nodes: [ + 1948835900, 416917603, 416917605, 1948835986, 416917609, 1948836072, + 1948836068, 1948836093, 1948836104, 1948836209, 1948836202, 1948836218, + 1948836237, 1948836277, 416917612, 1948836149, 1948836096, 1948835950, + ], + }, + { type: "way", id: 184402326, nodes: [1948835900, 1948835842, 1948835662] }, + { + type: "way", + id: 314899865, + nodes: [ + 2026541851, 2026541846, 2026541843, 2026541841, 2026541837, 3209560116, + 3210389695, 3209560114, 3209560115, 3209560117, 3209560118, 3209560119, + 3209560121, 3209560124, 3209560126, 3209560127, 3209560131, 3209560132, + 3209560134, 3209560135, 3209560136, 3209560138, + ], + }, + { + type: "way", + id: 315041273, + nodes: [ + 1554514739, 1554514640, 3211247042, 1554514831, 1554514750, 1554514755, + 5962496715, 1554514658, 5962338038, 1554514618, 5962444841, 1554514806, + 5962340003, 8497007481, 5962414276, 5962415500, 5962415499, 5962415498, + 5962415538, 5962415543, 3211247331, 3211247328, 3211247291, 5962444849, + 5962444850, 5962340009, 5962444846, 5962340010, 3211247059, 5962340012, + 5962340011, 3211247266, 3211247261, 5962496718, 3211247058, 5962496719, + 3211247054, 3211247265, 5962340013, 3211247060, 5962415564, 5962415565, + ], + }, + { + type: "way", + id: 606165130, + nodes: [ + 5747937641, 5747937642, 5747937643, 5747937644, 5747937645, 5747937646, + 5747937647, 5747937648, 5747937649, 5747937650, 5747937651, 5747937652, + 5747937653, 5747937654, 5747937655, 5747937656, 5747937657, 5747937658, + 5747937659, 5747535101, 5745963942, 5745963943, 5745963944, 5745963946, + 3162627509, 5745963947, 5747937668, 5747937669, 5747937670, 5747937671, + 5747937672, 5747937673, 5747937674, 5747937675, 5747937676, 5747937677, + 5747937678, 5747937679, 5747937680, 5747937681, 5747937682, 5747937683, + 5747937684, 5747937685, 5747937686, 5747937687, 5747937688, 5747937689, + 5747937690, 5747937691, 5747937641, + ], + }, + { + type: "way", + id: 184402332, + nodes: [668981668, 416917618, 1815081998, 668981667, 1948835950], + }, + { type: "way", id: 184402334, nodes: [6206789688, 1948835742, 668981668] }, + { type: "way", id: 314956402, nodes: [2026541851, 3209560139, 3209560138] }, + { type: "way", id: 663050030, nodes: [1948835662, 6206789688] }, + { type: "way", id: 631371232, nodes: [3211247019, 5962496725] }, + { type: "way", id: 631377344, nodes: [5962496725, 3211247021] }, + { + type: "way", + id: 655652321, + nodes: [ + 6142727588, 6143074993, 6142727589, 6142727598, 6142727590, 6142725081, + 6142725080, 6142725079, 6142725078, 6142725077, 6142725076, 6142725074, + 6142725084, 6142727586, 6142727585, 6142727587, 6142727591, 6142727592, + 5745963922, 6142727588, + ], + }, + { type: "way", id: 631371231, nodes: [3211247019, 3211247024] }, + { type: "way", id: 631371234, nodes: [3211247013, 3211247021] }, + { + type: "way", + id: 631377343, + nodes: [ + 1554514739, 1554514735, 1554514744, 1554514811, 5962339921, 1554514655, + 1554514679, 1554514683, 1554514765, 1554514775, 1554514773, 1554514747, + 1554514730, 1554514647, 1554514789, 1554514649, 1554514682, 1554514787, + 1554514661, 1554514726, 1554514693, 1554514703, 1554514783, + ], + }, + { + type: "way", + id: 655652319, + nodes: [ + 6142725039, 6142725040, 8638721239, 8638721230, 6142725041, 6142725042, + 6142725043, 6142725044, 6142725045, 6142725046, 6142725047, 6142725048, + 6142725049, 6142725050, 6142725051, 6142725052, 6142725053, 6142725054, + 6142725055, 6142725056, 6142725057, 6142725058, 6142725059, 6142725060, + 6142725061, 6142725064, 6142727594, 6142727595, 6142727596, 6143075018, + 6142725067, 6142725066, 6142725065, 6143075014, 3162627482, 6142727597, + 6142725039, + ], + }, + { type: "way", id: 315041261, nodes: [3211247039, 3211247036] }, + { type: "way", id: 315041263, nodes: [3211247032, 3211247029, 3211247024] }, + { type: "way", id: 631371223, nodes: [5962415565, 3211247039] }, + { type: "way", id: 631371228, nodes: [3211247034, 3211247032] }, + { type: "way", id: 631377341, nodes: [3211247036, 3211247037, 3211247034] }, + { + type: "way", + id: 631371236, + nodes: [ + 3211247013, 3211247010, 3211247008, 3211247004, 3211246997, 5962338070, + 9284562682, 5962338069, 5962338071, 3211246995, + ], + }, + { + type: "way", + id: 631371237, + nodes: [3211246995, 3211246555, 3211246542, 3203029856, 7727406921, 1554514783], + }, + ], + } ) - - - } - describe("GenerateCache", () => { - - it("should generate a cached file for the Natuurpunt-theme", async () => { - // We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this - const dir = "/var/tmp/" - if(!existsSync(dir)){ - console.log("Not executing caching test: no temp directory found") - } - if (existsSync(dir+"/np-cache")) { - ScriptUtils.readDirRecSync(dir+"np-cache").forEach(p => unlinkSync(p)) - rmdirSync(dir+"np-cache") - } - mkdirSync(dir+"np-cache") - initDownloads("(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" - ); - await main([ - "natuurpunt", - "12", - dir+"np-cache", - "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", - "--generate-point-overview", "nature_reserve,visitor_information_centre" - ]) - await ScriptUtils.sleep(250) - const birdhides = JSON.parse(readFileSync(dir+"np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) - expect(birdhides.features.length).deep.equal(5) - expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true - - }) + it("should generate a cached file for the Natuurpunt-theme", async () => { + // We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this + const dir = "/var/tmp/" + if (!existsSync(dir)) { + console.log("Not executing caching test: no temp directory found") + } + if (existsSync(dir + "/np-cache")) { + ScriptUtils.readDirRecSync(dir + "np-cache").forEach((p) => unlinkSync(p)) + rmdirSync(dir + "np-cache") + } + mkdirSync(dir + "np-cache") + initDownloads( + "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" + ) + await main([ + "natuurpunt", + "12", + dir + "np-cache", + "51.15423567022531", + "3.250579833984375", + "51.162821593316934", + "3.262810707092285", + "--generate-point-overview", + "nature_reserve,visitor_information_centre", + ]) + await ScriptUtils.sleep(250) + const birdhides = JSON.parse( + readFileSync(dir + "np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8") + ) + expect(birdhides.features.length).deep.equal(5) + expect( + birdhides.features.some((f) => f.properties.id === "node/5158056232"), + "Didn't find birdhide node/5158056232 " + ).true + }) }) diff --git a/test/testhooks.ts b/test/testhooks.ts index f9ed803ec..178964476 100644 --- a/test/testhooks.ts +++ b/test/testhooks.ts @@ -1,32 +1,41 @@ -import ScriptUtils from "../scripts/ScriptUtils"; -import {Utils} from "../Utils"; +import ScriptUtils from "../scripts/ScriptUtils" +import { Utils } from "../Utils" import * as fakedom from "fake-dom" -import Locale from "../UI/i18n/Locale"; +import Locale from "../UI/i18n/Locale" export const mochaHooks = { - beforeEach(done) { - ScriptUtils.fixUtils(); + ScriptUtils.fixUtils() Locale.language.setData("en") if (fakedom === undefined || window === undefined) { throw "FakeDom not initialized" } - // Block internet access - const realDownloadFunc = Utils.externalDownloadFunction; + const realDownloadFunc = Utils.externalDownloadFunction Utils.externalDownloadFunction = async (url) => { - console.error("Fetching ", url, "blocked in tests, use Utils.injectJsonDownloadForTests") + console.error( + "Fetching ", + url, + "blocked in tests, use Utils.injectJsonDownloadForTests" + ) const data = await realDownloadFunc(url) - console.log("\n\n ----------- \nBLOCKED DATA\n Utils.injectJsonDownloadForTests(\n" + - " ", JSON.stringify(url), ", \n", - " ", JSON.stringify(data), "\n )\n------------------\n\n") - throw new Error("Detected internet access for URL " + url + ", please inject it with Utils.injectJsonDownloadForTests") + console.log( + "\n\n ----------- \nBLOCKED DATA\n Utils.injectJsonDownloadForTests(\n" + " ", + JSON.stringify(url), + ", \n", + " ", + JSON.stringify(data), + "\n )\n------------------\n\n" + ) + throw new Error( + "Detected internet access for URL " + + url + + ", please inject it with Utils.injectJsonDownloadForTests" + ) } - - done(); - } - - -} \ No newline at end of file + + done() + }, +}