diff --git a/.gitignore b/.gitignore index f6570ee08f..5d39c4885c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ Docs/Tools/stats.*.json Docs/Tools/stats.csv missing_translations.txt *.swp +.DS_Store +Svg.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..0b81e39b44 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": true, + "singleQuote": false +} diff --git a/Customizations/JSON/FilterConfig.ts b/Customizations/JSON/FilterConfig.ts new file mode 100644 index 0000000000..946aef43c7 --- /dev/null +++ b/Customizations/JSON/FilterConfig.ts @@ -0,0 +1,27 @@ +import { TagsFilter } from "../../Logic/Tags/TagsFilter"; +import { Translation } from "../../UI/i18n/Translation"; +import Translations from "../../UI/i18n/Translations"; +import FilterConfigJson from "./FilterConfigJson"; +import { FromJSON } from "./FromJSON"; + +export default class FilterConfig { + readonly options: { + question: Translation; + osmTags: TagsFilter; + }[]; + + constructor(json: FilterConfigJson, context: string) { + this.options = json.options.map((option, i) => { + const question = Translations.T( + option.question, + context + ".options-[" + i + "].question" + ); + const osmTags = FromJSON.Tag( + option.osmTags ?? {and:[]}, + `${context}.options-[${i}].osmTags` + ); + + return { question: question, osmTags: osmTags }; + }); + } +} diff --git a/Customizations/JSON/FilterConfigJson.ts b/Customizations/JSON/FilterConfigJson.ts new file mode 100644 index 0000000000..f1a8025715 --- /dev/null +++ b/Customizations/JSON/FilterConfigJson.ts @@ -0,0 +1,11 @@ +import { AndOrTagConfigJson } from "./TagConfigJson"; + +export default interface FilterConfigJson { + /** + * The options for a filter + * If there are multiple options these will be a list of radio buttons + * If there is only one option this will be a checkbox + * Filtering is done based on the given osmTags that are compared to the objects in that layer. + */ + options: { question: string | any; osmTags?: AndOrTagConfigJson | string }[]; +} diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index d51c972a43..c20d183ae6 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -6,12 +6,13 @@ import {And} from "../../Logic/Tags/And"; import {Tag} from "../../Logic/Tags/Tag"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import SubstitutingTag from "../../Logic/Tags/SubstitutingTag"; +import ComparingTag from "../../Logic/Tags/ComparingTag"; export class FromJSON { public static SimpleTag(json: string, context?: string): Tag { const tag = Utils.SplitFirst(json, "="); - if(tag.length !== 2){ + if (tag.length !== 2) { throw `Invalid tag: no (or too much) '=' found (in ${context ?? "unkown context"})` } return new Tag(tag[0], tag[1]); @@ -26,6 +27,15 @@ export class FromJSON { } } + private static comparators + : [string, (a: number, b: number) => boolean][] + = [ + ["<=", (a, b) => a <= b], + [">=", (a, b) => a >= b], + ["<", (a, b) => a < b], + [">", (a, b) => a > b], + ] + private static TagUnsafe(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { if (json === undefined) { @@ -33,6 +43,27 @@ export class FromJSON { } if (typeof (json) == "string") { const tag = json as string; + + for (const [operator, comparator] of FromJSON.comparators) { + if (tag.indexOf(operator) >= 0) { + const split = Utils.SplitFirst(tag, operator); + + const val = Number(split[1].trim()) + if (isNaN(val)) { + throw `Error: not a valid value for a comparison: ${split[1]}, make sure it is a number and nothing more (at ${context})` + } + + const f = (value: string | undefined) => { + const b = Number(value?.replace(/[^\d.]/g,'')) + if (isNaN(b)) { + return false; + } + return comparator(b, val) + } + return new ComparingTag(split[0], f, operator + val) + } + } + if (tag.indexOf("!~") >= 0) { const split = Utils.SplitFirst(tag, "!~"); if (split[1] === "*") { @@ -54,11 +85,11 @@ export class FromJSON { new RegExp("^" + split[1] + "$") ); } - if(tag.indexOf(":=") >= 0){ + if (tag.indexOf(":=") >= 0) { const split = Utils.SplitFirst(tag, ":="); return new SubstitutingTag(split[0], split[1]); } - + if (tag.indexOf("!=") >= 0) { const split = Utils.SplitFirst(tag, "!="); if (split[1] === "*") { diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 48e96f4a21..fa91e4d57d 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -18,28 +18,28 @@ import {Tag} from "../../Logic/Tags/Tag"; import BaseUIElement from "../../UI/BaseUIElement"; import {Unit} from "./Denomination"; import DeleteConfig from "./DeleteConfig"; +import FilterConfig from "./FilterConfig"; export default class LayerConfig { - - static WAYHANDLING_DEFAULT = 0; static WAYHANDLING_CENTER_ONLY = 1; static WAYHANDLING_CENTER_AND_WAY = 2; id: string; - name: Translation + name: Translation; description: Translation; source: SourceConfig; - calculatedTags: [string, string][] + calculatedTags: [string, string][]; doNotDownload: boolean; passAllFeatures: boolean; isShown: TagRenderingConfig; minzoom: number; - maxzoom: number; + minzoomVisible: number; + maxzoom:number; title?: TagRenderingConfig; titleIcons: TagRenderingConfig[]; icon: TagRenderingConfig; - iconOverlays: { if: TagsFilter, then: TagRenderingConfig, badge: boolean }[] + iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[]; iconSize: TagRenderingConfig; label: TagRenderingConfig; rotation: TagRenderingConfig; @@ -48,34 +48,42 @@ export default class LayerConfig { dashArray: TagRenderingConfig; wayHandling: number; public readonly units: Unit[]; - public readonly deletion: DeleteConfig | null + public readonly deletion: DeleteConfig | null; + public readonly allowSplit: boolean presets: { title: Translation, tags: Tag[], description?: Translation, - preciseInput?: {preferredBackground?: string} + preciseInput?: { preferredBackground?: string } }[]; - tagRenderings: TagRenderingConfig []; + tagRenderings: TagRenderingConfig[]; + filters: FilterConfig[]; - constructor(json: LayerConfigJson, - units?:Unit[], - context?: string, - official: boolean = true,) { + constructor( + json: LayerConfigJson, + units?: Unit[], + context?: string, + official: boolean = true + ) { this.units = units ?? []; context = context + "." + json.id; const self = this; this.id = json.id; + this.allowSplit = json.allowSplit ?? false; this.name = Translations.T(json.name, context + ".name"); - - if(json.description !== undefined){ - if(Object.keys(json.description).length === 0){ + + if (json.description !== undefined) { + if (Object.keys(json.description).length === 0) { json.description = undefined; } } - - this.description =Translations.T(json.description, context + ".description") ; + + this.description = Translations.T( + json.description, + context + ".description" + ); let legacy = undefined; if (json["overpassTags"] !== undefined) { @@ -84,67 +92,75 @@ export default class LayerConfig { } if (json.source !== undefined) { if (legacy !== undefined) { - throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" + throw ( + context + + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined" + ); } let osmTags: TagsFilter = legacy; if (json.source["osmTags"]) { - osmTags = FromJSON.Tag(json.source["osmTags"], context + "source.osmTags"); + osmTags = FromJSON.Tag( + json.source["osmTags"], + context + "source.osmTags" + ); } - if(json.source["geoJsonSource"] !== undefined){ - throw context + "Use 'geoJson' instead of 'geoJsonSource'" + if (json.source["geoJsonSource"] !== undefined) { + throw context + "Use 'geoJson' instead of 'geoJsonSource'"; } - - this.source = new SourceConfig({ - osmTags: osmTags, - geojsonSource: json.source["geoJson"], - geojsonSourceLevel: json.source["geoJsonZoomLevel"], - overpassScript: json.source["overpassScript"], - isOsmCache: json.source["isOsmCache"] - }, this.id); + + this.source = new SourceConfig( + { + osmTags: osmTags, + geojsonSource: json.source["geoJson"], + geojsonSourceLevel: json.source["geoJsonZoomLevel"], + overpassScript: json.source["overpassScript"], + isOsmCache: json.source["isOsmCache"], + }, + this.id + ); } else { this.source = new SourceConfig({ - osmTags: legacy - }) + osmTags: legacy, + }); } - this.calculatedTags = undefined; if (json.calculatedTags !== undefined) { if (!official) { - console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`) + console.warn( + `Unofficial theme ${this.id} with custom javascript! This is a security risk` + ); } this.calculatedTags = []; for (const kv of json.calculatedTags) { - - const index = kv.indexOf("=") + const index = kv.indexOf("="); const key = kv.substring(0, index); const code = kv.substring(index + 1); - - this.calculatedTags.push([key, code]) + + this.calculatedTags.push([key, code]); } } this.doNotDownload = json.doNotDownload ?? false; this.passAllFeatures = json.passAllFeatures ?? false; this.minzoom = json.minzoom ?? 0; - this.maxzoom = json.maxzoom ?? 1000; + this.minzoomVisible = json.minzoomVisible ?? this.minzoom; this.wayHandling = json.wayHandling ?? 0; this.presets = (json.presets ?? []).map((pr, i) => { - if(pr.preciseInput === true){ + if (pr.preciseInput === true) { pr.preciseInput = { preferredBackground: undefined } } - return ({ + return { title: Translations.T(pr.title, `${context}.presets[${i}].title`), - tags: pr.tags.map(t => FromJSON.SimpleTag(t)), + tags: pr.tags.map((t) => FromJSON.SimpleTag(t)), description: Translations.T(pr.description, `${context}.presets[${i}].description`), preciseInput: pr.preciseInput - }); - }) - + } + }); /** Given a key, gets the corresponding property from the json (or the default if not found * @@ -156,7 +172,11 @@ export default class LayerConfig { if (deflt === undefined) { return undefined; } - return new TagRenderingConfig(deflt, self.source.osmTags, `${context}.${key}.default value`); + return new TagRenderingConfig( + deflt, + self.source.osmTags, + `${context}.${key}.default value` + ); } if (typeof v === "string") { const shared = SharedTagRenderings.SharedTagRendering.get(v); @@ -164,54 +184,80 @@ export default class LayerConfig { return shared; } } - return new TagRenderingConfig(v, self.source.osmTags, `${context}.${key}`); + return new TagRenderingConfig( + v, + self.source.osmTags, + `${context}.${key}` + ); } /** * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig * A string is interpreted as a name to call */ - function trs(tagRenderings?: (string | TagRenderingConfigJson)[], readOnly = false) { + function trs( + tagRenderings?: (string | TagRenderingConfigJson)[], + readOnly = false + ) { if (tagRenderings === undefined) { return []; } - return Utils.NoNull(tagRenderings.map( - (renderingJson, i) => { + return Utils.NoNull( + tagRenderings.map((renderingJson, i) => { if (typeof renderingJson === "string") { - if (renderingJson === "questions") { if (readOnly) { - throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(renderingJson)}` + throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify( + renderingJson + )}`; } - return new TagRenderingConfig("questions", undefined) + return new TagRenderingConfig("questions", undefined); } - - const shared = SharedTagRenderings.SharedTagRendering.get(renderingJson); + const shared = + SharedTagRenderings.SharedTagRendering.get(renderingJson); if (shared !== undefined) { return shared; } - - const keys = Array.from(SharedTagRenderings.SharedTagRendering.keys()) - - if(Utils.runningFromConsole){ + + const keys = Array.from( + SharedTagRenderings.SharedTagRendering.keys() + ); + + if (Utils.runningFromConsole) { return undefined; } - - throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${(keys.join(", "))}\n If you intent to output this text literally, use {\"render\": } instead"}`; + + throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${keys.join( + ", " + )}\n If you intent to output this text literally, use {\"render\": } instead"}`; } - return new TagRenderingConfig(renderingJson, self.source.osmTags, `${context}.tagrendering[${i}]`); - })); + return new TagRenderingConfig( + renderingJson, + self.source.osmTags, + `${context}.tagrendering[${i}]` + ); + }) + ); } this.tagRenderings = trs(json.tagRenderings, false); + this.filters = (json.filter ?? []).map((option, i) => { + return new FilterConfig(option, `${context}.filter-[${i}]`) + }); const titleIcons = []; - const defaultIcons = ["phonelink", "emaillink", "wikipedialink", "osmlink", "sharelink"]; - for (const icon of (json.titleIcons ?? defaultIcons)) { + const defaultIcons = [ + "phonelink", + "emaillink", + "wikipedialink", + "osmlink", + "sharelink", + ]; + for (const icon of json.titleIcons ?? defaultIcons) { if (icon === "defaults") { titleIcons.push(...defaultIcons); } else { @@ -221,74 +267,85 @@ export default class LayerConfig { this.titleIcons = trs(titleIcons, true); - this.title = tr("title", undefined); this.icon = tr("icon", ""); this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { - let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`); - if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) { + let tr = new TagRenderingConfig( + overlay.then, + self.source.osmTags, + `iconoverlays.${i}` + ); + if ( + typeof overlay.then === "string" && + SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined + ) { tr = SharedTagRenderings.SharedIcons.get(overlay.then); } return { if: FromJSON.Tag(overlay.if), then: tr, - badge: overlay.badge ?? false - } + badge: overlay.badge ?? false, + }; }); const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; if (iconPath.startsWith(Utils.assets_path)) { const iconKey = iconPath.substr(Utils.assets_path.length); if (Svg.All[iconKey] === undefined) { - throw "Builtin SVG asset not found: " + iconPath + throw "Builtin SVG asset not found: " + iconPath; } } this.isShown = tr("isShown", "yes"); this.iconSize = tr("iconSize", "40,40,center"); - this.label = tr("label", "") + this.label = tr("label", ""); this.color = tr("color", "#0000ff"); this.width = tr("width", "7"); this.rotation = tr("rotation", "0"); this.dashArray = tr("dashArray", ""); - - this.deletion = null; - if(json.deletion === true){ - json.deletion = { - } - } - if(json.deletion !== undefined && json.deletion !== false){ - this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`) - } + this.deletion = null; + if (json.deletion === true) { + json.deletion = {}; + } + if (json.deletion !== undefined && json.deletion !== false) { + this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`); + } if (json["showIf"] !== undefined) { - throw "Invalid key on layerconfig " + this.id + ": showIf. Did you mean 'isShown' instead?"; + throw ( + "Invalid key on layerconfig " + + this.id + + ": showIf. Did you mean 'isShown' instead?" + ); } } public CustomCodeSnippets(): string[] { if (this.calculatedTags === undefined) { - return [] + return []; } - return this.calculatedTags.map(code => code[1]); + return this.calculatedTags.map((code) => code[1]); } public AddRoamingRenderings(addAll: { - tagRenderings: TagRenderingConfig[], - titleIcons: TagRenderingConfig[], - iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] - + tagRenderings: TagRenderingConfig[]; + titleIcons: TagRenderingConfig[]; + iconOverlays: { + if: TagsFilter; + then: TagRenderingConfig; + badge: boolean; + }[]; }): LayerConfig { - - let insertionPoint = this.tagRenderings.map(tr => tr.IsQuestionBoxElement()).indexOf(true) + let insertionPoint = this.tagRenderings + .map((tr) => tr.IsQuestionBoxElement()) + .indexOf(true); if (insertionPoint < 0) { // No 'questions' defined - we just add them all to the end insertionPoint = this.tagRenderings.length; } this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings); - this.iconOverlays.push(...addAll.iconOverlays); for (const icon of addAll.titleIcons) { this.titleIcons.splice(0, 0, icon); @@ -297,40 +354,41 @@ export default class LayerConfig { } public GetRoamingRenderings(): { - tagRenderings: TagRenderingConfig[], - titleIcons: TagRenderingConfig[], - iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] - + tagRenderings: TagRenderingConfig[]; + titleIcons: TagRenderingConfig[]; + iconOverlays: { + if: TagsFilter; + then: TagRenderingConfig; + badge: boolean; + }[]; } { - - const tagRenderings = this.tagRenderings.filter(tr => tr.roaming); - const titleIcons = this.titleIcons.filter(tr => tr.roaming); - const iconOverlays = this.iconOverlays.filter(io => io.then.roaming) + const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming); + const titleIcons = this.titleIcons.filter((tr) => tr.roaming); + const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming); return { tagRenderings: tagRenderings, titleIcons: titleIcons, - iconOverlays: iconOverlays - } - + iconOverlays: iconOverlays, + }; } - public GenerateLeafletStyle(tags: UIEventSource, clickable: boolean, widthHeight= "100%"): - { - icon: - { - html: BaseUIElement, - iconSize: [number, number], - iconAnchor: [number, number], - popupAnchor: [number, number], - iconUrl: string, - className: string - }, - color: string, - weight: number, - dashArray: number[] - } { - + public GenerateLeafletStyle( + tags: UIEventSource, + clickable: boolean + ): { + icon: { + html: BaseUIElement; + iconSize: [number, number]; + iconAnchor: [number, number]; + popupAnchor: [number, number]; + iconUrl: string; + className: string; + }; + color: string; + weight: number; + dashArray: number[]; + } { function num(str, deflt = 40) { const n = Number(str); if (isNaN(n)) { @@ -349,7 +407,7 @@ export default class LayerConfig { } function render(tr: TagRenderingConfig, deflt?: string) { - const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt); + const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt; return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); } @@ -358,14 +416,16 @@ export default class LayerConfig { 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 weight = rendernum(this.width, 5); const iconW = num(iconSize[0]); let iconH = num(iconSize[1]); - const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" + const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"; let anchorW = iconW / 2; let anchorH = iconH / 2; @@ -385,31 +445,35 @@ export default class LayerConfig { const iconUrlStatic = render(this.icon); const self = this; - const mappedHtml = tags.map(tgs => { + const mappedHtml = tags.map((tgs) => { function genHtmlFromString(sourcePart: string): BaseUIElement { - const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; - let html: BaseUIElement = new FixedUiElement(``); - const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/) + let html: BaseUIElement = new FixedUiElement( + `` + ); + const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/); if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { html = new Combine([ - (Svg.All[match[1] + ".svg"] as string) - .replace(/#000000/g, match[2]) + (Svg.All[match[1] + ".svg"] as string).replace( + /#000000/g, + match[2] + ), ]).SetStyle(style); } return html; } - // What do you mean, 'tgs' is never read? // It is read implicitly in the 'render' method const iconUrl = render(self.icon); const rotation = render(self.rotation, "0deg"); let htmlParts: BaseUIElement[] = []; - let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != "")); + let sourceParts = Utils.NoNull( + iconUrl.split(";").filter((prt) => prt != "") + ); for (const sourcePart of sourceParts) { - htmlParts.push(genHtmlFromString(sourcePart)) + htmlParts.push(genHtmlFromString(sourcePart)); } let badges = []; @@ -419,79 +483,88 @@ export default class LayerConfig { } if (iconOverlay.badge) { const badgeParts: BaseUIElement[] = []; - const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != ""); + const partDefs = iconOverlay.then + .GetRenderValue(tgs) + .txt.split(";") + .filter((prt) => prt != ""); for (const badgePartStr of partDefs) { - badgeParts.push(genHtmlFromString(badgePartStr)) + badgeParts.push(genHtmlFromString(badgePartStr)); } - const badgeCompound = new Combine(badgeParts) - .SetStyle("display:flex;position:relative;width:100%;height:100%;"); - - badges.push(badgeCompound) + const badgeCompound = new Combine(badgeParts).SetStyle( + "display:flex;position:relative;width:100%;height:100%;" + ); + badges.push(badgeCompound); } else { - htmlParts.push(genHtmlFromString( - iconOverlay.then.GetRenderValue(tgs).txt)); + htmlParts.push( + genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt) + ); } } if (badges.length > 0) { - const badgesComponent = new Combine(badges) - .SetStyle("display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"); - htmlParts.push(badgesComponent) + const badgesComponent = new Combine(badges).SetStyle( + "display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;" + ); + htmlParts.push(badgesComponent); } if (sourceParts.length == 0) { - iconH = 0 + iconH = 0; } try { - - const label = self.label?.GetRenderValue(tgs)?.Subs(tgs) + const label = self.label + ?.GetRenderValue(tgs) + ?.Subs(tgs) ?.SetClass("block text-center") - ?.SetStyle("margin-top: " + (iconH + 2) + "px") + ?.SetStyle("margin-top: " + (iconH + 2) + "px"); if (label !== undefined) { - htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center")) + htmlParts.push( + new Combine([label]).SetClass("flex flex-col items-center") + ); } } catch (e) { - console.error(e, tgs) + console.error(e, tgs); } return new Combine(htmlParts); - }) - + }); return { - icon: - { - html: new VariableUiElement(mappedHtml), - iconSize: [iconW, iconH], - iconAnchor: [anchorW, anchorH], - popupAnchor: [0, 3 - anchorH], - iconUrl: iconUrlStatic, - className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable" - }, + icon: { + html: new VariableUiElement(mappedHtml), + iconSize: [iconW, iconH], + iconAnchor: [anchorW, anchorH], + popupAnchor: [0, 3 - anchorH], + iconUrl: iconUrlStatic, + className: clickable + ? "leaflet-div-icon" + : "leaflet-div-icon unclickable", + }, color: color, weight: weight, - dashArray: dashArray + dashArray: dashArray, }; } public ExtractImages(): Set { - const parts: Set[] = [] - parts.push(...this.tagRenderings?.map(tr => tr.ExtractImages(false))) - parts.push(...this.titleIcons?.map(tr => tr.ExtractImages(true))) - parts.push(this.icon?.ExtractImages(true)) - parts.push(...this.iconOverlays?.map(overlay => overlay.then.ExtractImages(true))) + const parts: Set[] = []; + parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false))); + parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true))); + parts.push(this.icon?.ExtractImages(true)); + parts.push( + ...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true)) + ); for (const preset of this.presets) { - parts.push(new Set(preset.description?.ExtractImages(false))) + parts.push(new Set(preset.description?.ExtractImages(false))); } const allIcons = new Set(); for (const part of parts) { - part?.forEach(allIcons.add, allIcons) + part?.forEach(allIcons.add, allIcons); } return allIcons; } - -} \ No newline at end of file +} diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index ca272ecb05..aa4bce1b78 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -1,6 +1,7 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {AndOrTagConfigJson} from "./TagConfigJson"; import {DeleteConfigJson} from "./DeleteConfigJson"; +import FilterConfigJson from "./FilterConfigJson"; /** * Configuration for a single layer @@ -54,7 +55,7 @@ export interface LayerConfigJson { * Note that both geojson-options might set a flag 'isOsmCache' indicating that the data originally comes from OSM too * * - * NOTE: the previous format was 'overpassTags: AndOrTagCOnfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"} + * NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"} * While still supported, this is considered deprecated */ source: { osmTags: AndOrTagConfigJson | string } | @@ -81,7 +82,7 @@ export interface LayerConfigJson { doNotDownload?: boolean; /** - * This tagrendering should either be 'yes' or 'no'. If 'no' is returned, then the feature will be hidden from view. + * This tag rendering should either be 'yes' or 'no'. If 'no' is returned, then the feature will be hidden from view. * This is useful to hide certain features from view. Important: hiding features does not work dynamically, but is only calculated when the data is first renders. * This implies that it is not possible to hide a feature after a tagging change * @@ -91,16 +92,16 @@ export interface LayerConfigJson { /** - * The zoomlevel at which point the data is shown and loaded. + * The minimum needed zoomlevel required before loading of the data start * Default: 0 */ minzoom?: number; /** - * The zoomlevel at which point the data is hidden again + * The zoom level at which point the data is hidden again * Default: 100 (thus: always visible */ - maxzoom?: number; + minzoomVisible?: number; /** * The title shown in a popup for elements of this layer. @@ -120,9 +121,9 @@ export interface LayerConfigJson { * Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets. * * The result of the icon is rendered as follows: - * the resulting string is interpreted as a _list_ of items, seperated by ";". The bottommost layer is the first layer. + * the resulting string is interpreted as a _list_ of items, separated by ";". The bottommost layer is the first layer. * As a result, on could use a generic pin, then overlay it with a specific icon. - * To make things even more practical, one can use all svgs from the folder "assets/svg" and _substitute the color_ in it. + * To make things even more practical, one can use all SVG's from the folder "assets/svg" and _substitute the color_ in it. * E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;` * */ @@ -220,7 +221,7 @@ export interface LayerConfigJson { /** * If set, the user will prompted to confirm the location before actually adding the data. - * THis will be with a 'drag crosshair'-method. + * This will be with a 'drag crosshair'-method. * * If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category. */ @@ -235,7 +236,7 @@ export interface LayerConfigJson { * * Refer to the class `TagRenderingConfigJson` to see the possibilities. * - * Note that we can also use a string here - where the string refers to a tagrenering defined in `assets/questions/questions.json`, + * Note that we can also use a string here - where the string refers to a tag rendering defined in `assets/questions/questions.json`, * where a few very general questions are defined e.g. website, phone number, ... * * A special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox. @@ -243,6 +244,12 @@ export interface LayerConfigJson { */ tagRenderings?: (string | TagRenderingConfigJson) [], + + /** + * All the extra questions for filtering + */ + filter?: (FilterConfigJson) [], + /** * This block defines under what circumstances the delete dialog is shown for objects of this layer. * If set, a dialog is shown to the user to (soft) delete the point. @@ -291,4 +298,9 @@ export interface LayerConfigJson { */ deletion?: boolean | DeleteConfigJson + /** + * IF set, a 'split this road' button is shown + */ + allowSplit?: boolean + } \ No newline at end of file diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index e76c68ac88..bda08db561 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -66,7 +66,7 @@ export default class LayoutConfig { this.language = json.language; } if (this.language.length == 0) { - throw "No languages defined. Define at least one language" + throw `No languages defined. Define at least one language. (${context}.languages)` } if (json.title === undefined) { throw "Title not defined in " + this.id; @@ -95,7 +95,7 @@ export default class LayoutConfig { } ); this.defaultBackgroundId = json.defaultBackgroundId; - this.layers = LayoutConfig.ExtractLayers(json, this.units, official); + this.layers = LayoutConfig.ExtractLayers(json, this.units, official, context); // ALl the layers are constructed, let them share tags in now! const roaming: { r, source: LayerConfig }[] = [] @@ -160,12 +160,12 @@ export default class LayoutConfig { } - private static ExtractLayers(json: LayoutConfigJson, units: Unit[], official: boolean): LayerConfig[] { + private static ExtractLayers(json: LayoutConfigJson, units: Unit[], official: boolean, context: string): LayerConfig[] { const result: LayerConfig[] = [] json.layers.forEach((layer, i) => { if (typeof layer === "string") { - if (AllKnownLayers.sharedLayersJson[layer] !== undefined) { + if (AllKnownLayers.sharedLayersJson.get(layer) !== undefined) { if (json.overrideAll !== undefined) { let lyr = JSON.parse(JSON.stringify(AllKnownLayers.sharedLayersJson[layer])); const newLayer = new LayerConfig(Utils.Merge(json.overrideAll, lyr), units, `${json.id}+overrideAll.layers[${i}]`, official) @@ -176,7 +176,8 @@ export default class LayoutConfig { return } } else { - throw "Unknown fixed layer " + layer; + console.log("Layer ", layer," not kown, try one of", Array.from(AllKnownLayers.sharedLayers.keys()).join(", ")) + throw `Unknown builtin layer ${layer} at ${context}.layers[${i}]`; } } @@ -195,9 +196,9 @@ export default class LayoutConfig { names = [names] } names.forEach(name => { - const shared = AllKnownLayers.sharedLayersJson[name]; + const shared = AllKnownLayers.sharedLayersJson.get(name); if (shared === undefined) { - throw "Unknown fixed layer " + name; + throw `Unknown shared/builtin layer ${name} at ${context}.layers[${i}]. Available layers are ${Array.from(AllKnownLayers.sharedLayersJson.keys()).join(", ")}`; } // @ts-ignore let newLayer: LayerConfigJson = Utils.Merge(layer.override, JSON.parse(JSON.stringify(shared))); // We make a deep copy of the shared layer, in order to protect it from changes diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md index c1df5523b6..2d16a79876 100644 --- a/Docs/CalculatedTags.md +++ b/Docs/CalculatedTags.md @@ -141,6 +141,7 @@ Some advanced functions are available on **feat** as well: - overlapWith - closest - memberships + - score ### distanceTo @@ -168,4 +169,12 @@ Some advanced functions are available on **feat** as well: For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')` + +### score + + Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so for further calculations, use `.map(score => ...)` + +For example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')` + + 0. path Generated from SimpleMetaTagger, ExtraFunction \ No newline at end of file diff --git a/Docs/TagInfo/mapcomplete_artwork.json b/Docs/TagInfo/mapcomplete_artwork.json new file mode 100644 index 0000000000..f968777e1f --- /dev/null +++ b/Docs/TagInfo/mapcomplete_artwork.json @@ -0,0 +1,111 @@ +{ + "data_format": 1, + "project": { + "name": "MapComplete Open Artwork Map", + "description": "Welcome to Open Artwork Map, a map of statues, busts, grafittis and other artwork all over the world", + "project_url": "https://mapcomplete.osm.be/artwork", + "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", + "icon_url": "https://mapcomplete.osm.be/assets/themes/artwork/artwork.svg", + "contact_name": "Pieter Vander Vennet, MapComplete", + "contact_email": "pietervdvn@posteo.net" + }, + "tags": [ + { + "key": "tourism", + "description": "The MapComplete theme Open Artwork Map has a layer Artworks showing features with this tag", + "value": "artwork" + }, + { + "key": "image", + "description": "The layer 'Artworks 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" + }, + { + "key": "mapillary", + "description": "The layer 'Artworks 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" + }, + { + "key": "wikidata", + "description": "The layer 'Artworks 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" + }, + { + "key": "wikipedia", + "description": "The layer 'Artworks 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" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows and asks freeform values for key 'artwork_type' (in the MapComplete.osm.be theme 'Open Artwork Map')" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=architecture with a fixed text, namely 'Architecture' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "architecture" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=mural with a fixed text, namely 'Mural' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "mural" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=painting with a fixed text, namely 'Painting' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "painting" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=sculpture with a fixed text, namely 'Sculpture' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "sculpture" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=statue with a fixed text, namely 'Statue' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "statue" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=bust with a fixed text, namely 'Bust' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "bust" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=stone with a fixed text, namely 'Stone' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "stone" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=installation with a fixed text, namely 'Installation' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "installation" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=graffiti with a fixed text, namely 'Graffiti' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "graffiti" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=relief with a fixed text, namely 'Relief' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "relief" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=azulejo with a fixed text, namely 'Azulejo (Spanish decorative tilework)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "azulejo" + }, + { + "key": "artwork_type", + "description": "Layer 'Artworks' shows artwork_type=tilework with a fixed text, namely 'Tilework' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Artwork Map')", + "value": "tilework" + }, + { + "key": "artist_name", + "description": "Layer 'Artworks' shows and asks freeform values for key 'artist_name' (in the MapComplete.osm.be theme 'Open Artwork Map')" + }, + { + "key": "website", + "description": "Layer 'Artworks' shows and asks freeform values for key 'website' (in the MapComplete.osm.be theme 'Open Artwork Map')" + }, + { + "key": "wikidata", + "description": "Layer 'Artworks' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'Open Artwork Map')" + } + ] +} \ No newline at end of file diff --git a/Docs/TagInfo/mapcomplete_bicyclelib.json b/Docs/TagInfo/mapcomplete_bicyclelib.json index fea4ac4134..21d2340668 100644 --- a/Docs/TagInfo/mapcomplete_bicyclelib.json +++ b/Docs/TagInfo/mapcomplete_bicyclelib.json @@ -5,7 +5,7 @@ "description": "A bicycle library is a place where bicycles can be lent, often for a small yearly fee", "project_url": "https://mapcomplete.osm.be/bicyclelib", "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", - "icon_url": "https://mapcomplete.osm.be/assets/themes/bicycle_library/logo.svg", + "icon_url": "https://mapcomplete.osm.be/assets/themes/bicyclelib/logo.svg", "contact_name": "Pieter Vander Vennet, MapComplete", "contact_email": "pietervdvn@posteo.net" }, diff --git a/Docs/TagInfo/mapcomplete_campersite.json b/Docs/TagInfo/mapcomplete_campersite.json index d10a494747..8fea7d15a6 100644 --- a/Docs/TagInfo/mapcomplete_campersite.json +++ b/Docs/TagInfo/mapcomplete_campersite.json @@ -5,7 +5,7 @@ "description": "Find sites to spend the night with your camper", "project_url": "https://mapcomplete.osm.be/campersite", "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", - "icon_url": "https://mapcomplete.osm.be/assets/themes/campersites/caravan.svg", + "icon_url": "https://mapcomplete.osm.be/assets/themes/campersite/caravan.svg", "contact_name": "Pieter Vander Vennet, joost schouppe", "contact_email": "pietervdvn@posteo.net" }, diff --git a/Docs/TagInfo/mapcomplete_cycle_infra.json b/Docs/TagInfo/mapcomplete_cycle_infra.json new file mode 100644 index 0000000000..94876210aa --- /dev/null +++ b/Docs/TagInfo/mapcomplete_cycle_infra.json @@ -0,0 +1,707 @@ +{ + "data_format": 1, + "project": { + "name": "MapComplete Bicycle infrastructure", + "description": "A map where you can view and edit things related to the bicycle infrastructure.", + "project_url": "https://mapcomplete.osm.be/cycle_infra", + "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", + "icon_url": "https://mapcomplete.osm.be/assets/themes/cycle_infra/cycle-infra.svg", + "contact_name": "Pieter Vander Vennet, ", + "contact_email": "pietervdvn@posteo.net" + }, + "tags": [ + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "cycleway" + }, + { + "key": "cycleway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "lane" + }, + { + "key": "cycleway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "shared_lane" + }, + { + "key": "cycleway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "track" + }, + { + "key": "cyclestreet", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "yes" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "path" + }, + { + "key": "bicycle", + "description": "The MapComplete theme Bicycle infrastructure has a layer Cycleways showing features with this tag", + "value": "designated" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=shared_lane with a fixed text, namely 'There is a shared lane' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "shared_lane" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=lane with a fixed text, namely 'There is a lane next to the road (seperated with paint)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "lane" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=track with a fixed text, namely 'There is a track, but no cycleway drawn seperately from this road on the map.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "track" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=seperate with a fixed text, namely 'There is a seperately drawn cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "seperate" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=no with a fixed text, namely 'There is no cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "cycleway", + "description": "Layer 'Cycleways' shows cycleway=no with a fixed text, namely 'There is no cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "lit", + "description": "Layer 'Cycleways' shows lit=yes with a fixed text, namely 'This street is lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "lit", + "description": "Layer 'Cycleways' shows lit=no with a fixed text, namely 'This road is not lit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "lit", + "description": "Layer 'Cycleways' shows lit=sunset-sunrise with a fixed text, namely 'This road is lit at night' (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "sunset-sunrise" + }, + { + "key": "lit", + "description": "Layer 'Cycleways' shows lit=24/7 with a fixed text, namely 'This road is lit 24/7' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "24/7" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cycleways' shows cyclestreet=yes with a fixed text, namely 'This is a cyclestreet, and a 30km/h zone.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cycleways' shows cyclestreet=yes with a fixed text, namely 'This is a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cycleways' shows cyclestreet= with a fixed text, namely 'This is not a cyclestreet.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows and asks freeform values for key 'maxspeed' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows maxspeed=20 with a fixed text, namely 'The maximum speed is 20 km/h' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "20" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows maxspeed=30 with a fixed text, namely 'The maximum speed is 30 km/h' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "30" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows maxspeed=50 with a fixed text, namely 'The maximum speed is 50 km/h' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "50" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows maxspeed=70 with a fixed text, namely 'The maximum speed is 70 km/h' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "70" + }, + { + "key": "maxspeed", + "description": "Layer 'Cycleways' shows maxspeed=90 with a fixed text, namely 'The maximum speed is 90 km/h' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "90" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows and asks freeform values for key 'cycleway:surface' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows cycleway:surface=wood with a fixed text, namely 'This cycleway is made of wood' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "wood" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows cycleway:surface=concrete with a fixed text, namely 'This cycleway is made of concrete' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "concrete" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows cycleway:surface=cobblestone with a fixed text, namely 'This cycleway is made of cobblestone' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "cobblestone" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows cycleway:surface=asphalt with a fixed text, namely 'This cycleway is made of asphalt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "asphalt" + }, + { + "key": "cycleway:surface", + "description": "Layer 'Cycleways' shows cycleway:surface=paved with a fixed text, namely 'This cycleway is paved' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "paved" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=excellent with a fixed text, namely 'Usable for thin rollers: rollerblade, skateboard' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "excellent" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=good with a fixed text, namely 'Usable for thin wheels: racing bike' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "good" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=intermediate with a fixed text, namely 'Usable for normal wheels: city bike, wheelchair, scooter' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "intermediate" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=bad with a fixed text, namely 'Usable for robust wheels: trekking bike, car, rickshaw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "bad" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=very_bad with a fixed text, namely 'Usable for vehicles with high clearance: light duty off-road vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "very_bad" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=horrible with a fixed text, namely 'Usable for off-road vehicles: heavy duty off-road vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "horrible" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=very_horrible with a fixed text, namely 'Usable for specialized off-road vehicles: tractor, ATV' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "very_horrible" + }, + { + "key": "cycleway:smoothness", + "description": "Layer 'Cycleways' shows cycleway:smoothness=impassable with a fixed text, namely 'Impassable / No wheeled vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "impassable" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows and asks freeform values for key 'surface' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows surface=wood with a fixed text, namely 'This street is made of wood' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "wood" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows surface=concrete with a fixed text, namely 'This street is made of concrete' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "concrete" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows surface=cobblestone with a fixed text, namely 'This street is made of cobblestone' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "cobblestone" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows surface=asphalt with a fixed text, namely 'This street is made of asphalt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "asphalt" + }, + { + "key": "surface", + "description": "Layer 'Cycleways' shows surface=paved with a fixed text, namely 'This street is paved' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "paved" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=excellent with a fixed text, namely 'Usable for thin rollers: rollerblade, skateboard' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "excellent" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=good with a fixed text, namely 'Usable for thin wheels: racing bike' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "good" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=intermediate with a fixed text, namely 'Usable for normal wheels: city bike, wheelchair, scooter' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "intermediate" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=bad with a fixed text, namely 'Usable for robust wheels: trekking bike, car, rickshaw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "bad" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=very_bad with a fixed text, namely 'Usable for vehicles with high clearance: light duty off-road vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "very_bad" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=horrible with a fixed text, namely 'Usable for off-road vehicles: heavy duty off-road vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "horrible" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=very_horrible with a fixed text, namely 'Usable for specialized off-road vehicles: tractor, ATV' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "very_horrible" + }, + { + "key": "smoothness", + "description": "Layer 'Cycleways' shows smoothness=impassable with a fixed text, namely 'Impassable / No wheeled vehicle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "impassable" + }, + { + "key": "width:carriageway", + "description": "Layer 'Cycleways' shows and asks freeform values for key 'width:carriageway' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7 with a fixed text, namely 'Compulsory cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign~^BE:D7;.*$ with a fixed text, namely 'Compulsory cycleway (with supplementary sign) ' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D9 with a fixed text, namely 'Segregated foot/cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D9" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D10 with a fixed text, namely 'Unsegregated foot/cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D10" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=none with a fixed text, namely 'No traffic sign present' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "none" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7 with a fixed text, namely 'Compulsory cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign~^BE:D7;.*$ with a fixed text, namely 'Compulsory cycleway (with supplementary sign) ' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D9 with a fixed text, namely 'Segregated foot/cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D9" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D10 with a fixed text, namely 'Unsegregated foot/cycleway ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D10" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=none with a fixed text, namely 'No traffic sign present' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "none" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M6 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M6" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M13 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M13" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M14 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M14" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M7 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M7" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M15 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M15" + }, + { + "key": "cycleway:traffic_sign", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign=BE:D7;BE:M16 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M16" + }, + { + "key": "cycleway:traffic_sign:supplementary", + "description": "Layer 'Cycleways' shows cycleway:traffic_sign:supplementary=none with a fixed text, namely 'No supplementary traffic sign present' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "none" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7;BE:M6 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M6" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7;BE:M13 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M13" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7;BE:M14 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M14" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7;BE:M7 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M7" + }, + { + "key": ":traffic_sign", + "description": "Layer 'Cycleways' shows :traffic_sign=BE:D7;BE:M15 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M15" + }, + { + "key": "traffic_sign", + "description": "Layer 'Cycleways' shows traffic_sign=BE:D7;BE:M16 with a fixed text, namely '' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "BE:D7;BE:M16" + }, + { + "key": "traffic_sign:supplementary", + "description": "Layer 'Cycleways' shows traffic_sign:supplementary=none with a fixed text, namely 'No supplementary traffic sign present' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "none" + }, + { + "key": "cycleway:buffer", + "description": "Layer 'Cycleways' shows and asks freeform values for key 'cycleway:buffer' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "cycleway:seperation", + "description": "Layer 'Cycleways' shows cycleway:seperation=dashed_line with a fixed text, namely 'This cycleway is seperated by a dashed line' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "dashed_line" + }, + { + "key": "cycleway:seperation", + "description": "Layer 'Cycleways' shows cycleway:seperation=solid_line with a fixed text, namely 'This cycleway is seperated by a solid line' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "solid_line" + }, + { + "key": "cycleway:seperation", + "description": "Layer 'Cycleways' shows cycleway:seperation=parking_lane with a fixed text, namely 'This cycleway is seperated by a parking lane' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "parking_lane" + }, + { + "key": "cycleway:seperation", + "description": "Layer 'Cycleways' shows cycleway:seperation=kerb with a fixed text, namely 'This cycleway is seperated by a kerb' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "kerb" + }, + { + "key": "seperation", + "description": "Layer 'Cycleways' shows seperation=dashed_line with a fixed text, namely 'This cycleway is seperated by a dashed line' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "dashed_line" + }, + { + "key": "seperation", + "description": "Layer 'Cycleways' shows seperation=solid_line with a fixed text, namely 'This cycleway is seperated by a solid line' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "solid_line" + }, + { + "key": "seperation", + "description": "Layer 'Cycleways' shows seperation=parking_lane with a fixed text, namely 'This cycleway is seperated by a parking lane' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "parking_lane" + }, + { + "key": "seperation", + "description": "Layer 'Cycleways' shows seperation=kerb with a fixed text, namely 'This cycleway is seperated by a kerb' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "kerb" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer All streets showing features with this tag", + "value": "residential" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer All streets showing features with this tag", + "value": "tertiary" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer All streets showing features with this tag", + "value": "unclassified" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer All streets showing features with this tag", + "value": "primary" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer All streets showing features with this tag", + "value": "secondary" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows and asks freeform values for key 'cycleway' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=shared_lane with a fixed text, namely 'There is a shared lane' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "shared_lane" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=lane with a fixed text, namely 'There is a lane next to the road (seperated with paint)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "lane" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=track with a fixed text, namely 'There is a track, but no cycleway drawn seperately from this road on the map.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "track" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=seperate with a fixed text, namely 'There is a seperately drawn cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "seperate" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway= with a fixed text, namely 'There is no cycleway known here' (in the MapComplete.osm.be theme 'Bicycle infrastructure') Picking this answer will delete the key cycleway.", + "value": "" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=no with a fixed text, namely 'There is no cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "cycleway", + "description": "Layer 'All streets' shows cycleway=no with a fixed text, namely 'There is no cycleway' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes with a fixed text, namely 'This is a cyclestreet, and a 30km/h zone.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes with a fixed text, namely 'This is a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet= with a fixed text, namely 'This is not a cyclestreet.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "barrier", + "description": "The MapComplete theme Bicycle infrastructure has a layer Barriers showing features with this tag", + "value": "bollard" + }, + { + "key": "barrier", + "description": "The MapComplete theme Bicycle infrastructure has a layer Barriers showing features with this tag", + "value": "cycle_barrier" + }, + { + "key": "bicycle", + "description": "Layer 'Barriers' shows bicycle=yes with a fixed text, namely 'A cyclist can go past this.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "bicycle", + "description": "Layer 'Barriers' shows bicycle=no with a fixed text, namely 'A cyclist can not go past this.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "bollard", + "description": "Layer 'Barriers' shows bollard=removable with a fixed text, namely 'Removable bollard' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "removable" + }, + { + "key": "bollard", + "description": "Layer 'Barriers' shows bollard=fixed with a fixed text, namely 'Fixed bollard' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "fixed" + }, + { + "key": "bollard", + "description": "Layer 'Barriers' shows bollard=foldable with a fixed text, namely 'Bollard that can be folded down' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "foldable" + }, + { + "key": "bollard", + "description": "Layer 'Barriers' shows bollard=flexible with a fixed text, namely 'Flexible bollard, usually plastic' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "flexible" + }, + { + "key": "bollard", + "description": "Layer 'Barriers' shows bollard=rising with a fixed text, namely 'Rising bollard' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "rising" + }, + { + "key": "cycle_barrier:type", + "description": "Layer 'Barriers' shows cycle_barrier:type=single with a fixed text, namely 'Single, just two barriers with a space inbetween ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "single" + }, + { + "key": "cycle_barrier:type", + "description": "Layer 'Barriers' shows cycle_barrier:type=double with a fixed text, namely 'Double, two barriers behind each other ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "double" + }, + { + "key": "cycle_barrier:type", + "description": "Layer 'Barriers' shows cycle_barrier:type=triple with a fixed text, namely 'Triple, three barriers behind each other ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "triple" + }, + { + "key": "cycle_barrier:type", + "description": "Layer 'Barriers' shows cycle_barrier:type=squeeze with a fixed text, namely 'Squeeze gate, gap is smaller at top, than at the bottom ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "squeeze" + }, + { + "key": "maxwidth:physical", + "description": "Layer 'Barriers' shows and asks freeform values for key 'maxwidth:physical' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "width:seperation", + "description": "Layer 'Barriers' shows and asks freeform values for key 'width:seperation' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "width:opening", + "description": "Layer 'Barriers' shows and asks freeform values for key 'width:opening' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "overlap", + "description": "Layer 'Barriers' shows and asks freeform values for key 'overlap' (in the MapComplete.osm.be theme 'Bicycle infrastructure')" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Crossings showing features with this tag", + "value": "traffic_signals" + }, + { + "key": "highway", + "description": "The MapComplete theme Bicycle infrastructure has a layer Crossings showing features with this tag", + "value": "crossing" + }, + { + "key": "crossing", + "description": "Layer 'Crossings' shows crossing=uncontrolled with a fixed text, namely 'Crossing, without traffic lights' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "uncontrolled" + }, + { + "key": "crossing", + "description": "Layer 'Crossings' shows crossing=traffic_signals with a fixed text, namely 'Crossing with traffic signals' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "traffic_signals" + }, + { + "key": "crossing", + "description": "Layer 'Crossings' shows crossing=zebra with a fixed text, namely 'Zebra crossing' (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "zebra" + }, + { + "key": "bicycle", + "description": "Layer 'Crossings' shows bicycle=yes with a fixed text, namely 'A cyclist can use this crossing' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "bicycle", + "description": "Layer 'Crossings' shows bicycle=no with a fixed text, namely 'A cyclist can not use this crossing' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "crossing:island", + "description": "Layer 'Crossings' shows crossing:island=yes with a fixed text, namely 'This crossing has an island in the middle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "crossing:island", + "description": "Layer 'Crossings' shows crossing:island=no with a fixed text, namely 'This crossing does not have an island in the middle' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "tactile_paving", + "description": "Layer 'Crossings' shows tactile_paving=yes with a fixed text, namely 'This crossing has tactile paving' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "tactile_paving", + "description": "Layer 'Crossings' shows tactile_paving=no with a fixed text, namely 'This crossing does not have tactile paving' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "tactile_paving", + "description": "Layer 'Crossings' shows tactile_paving=incorrect with a fixed text, namely 'This crossing has tactile paving, but is not correct' (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "incorrect" + }, + { + "key": "button_operated", + "description": "Layer 'Crossings' shows button_operated=yes with a fixed text, namely 'This traffic light has a button to request green light' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "button_operated", + "description": "Layer 'Crossings' shows button_operated=no with a fixed text, namely 'This traffic light does not have a button to request green light' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "red_turn:right:bicycle", + "description": "Layer 'Crossings' shows red_turn:right:bicycle=yes with a fixed text, namely 'A cyclist can turn right if the light is red ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "red_turn:right:bicycle", + "description": "Layer 'Crossings' shows red_turn:right:bicycle=yes with a fixed text, namely 'A cyclist can turn right if the light is red' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "red_turn:right:bicycle", + "description": "Layer 'Crossings' shows red_turn:right:bicycle=no with a fixed text, namely 'A cyclist can not turn right if the light is red' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + }, + { + "key": "red_turn:straight:bicycle", + "description": "Layer 'Crossings' shows red_turn:straight:bicycle=yes with a fixed text, namely 'A cyclist can go straight on if the light is red ' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "red_turn:straight:bicycle", + "description": "Layer 'Crossings' shows red_turn:straight:bicycle=yes with a fixed text, namely 'A cyclist can go straight on if the light is red' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "yes" + }, + { + "key": "red_turn:straight:bicycle", + "description": "Layer 'Crossings' shows red_turn:straight:bicycle=no with a fixed text, namely 'A cyclist can not go straight on if the light is red' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle infrastructure')", + "value": "no" + } + ] +} \ No newline at end of file diff --git a/Docs/TagInfo/mapcomplete_cyclestreets.json b/Docs/TagInfo/mapcomplete_cyclestreets.json new file mode 100644 index 0000000000..9d21bf3481 --- /dev/null +++ b/Docs/TagInfo/mapcomplete_cyclestreets.json @@ -0,0 +1,264 @@ +{ + "data_format": 1, + "project": { + "name": "MapComplete Cyclestreets", + "description": "A map of cyclestreets", + "project_url": "https://mapcomplete.osm.be/cyclestreets", + "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", + "icon_url": "https://mapcomplete.osm.be/assets/themes/cyclestreets/F111.svg", + "contact_name": "Pieter Vander Vennet, MapComplete", + "contact_email": "pietervdvn@posteo.net" + }, + "tags": [ + { + "key": "cyclestreet", + "description": "The MapComplete theme Cyclestreets has a layer Cyclestreets showing features with this tag", + "value": "yes" + }, + { + "key": "image", + "description": "The layer 'Cyclestreets 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" + }, + { + "key": "mapillary", + "description": "The layer 'Cyclestreets 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" + }, + { + "key": "wikidata", + "description": "The layer 'Cyclestreets 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" + }, + { + "key": "wikipedia", + "description": "The layer 'Cyclestreets 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" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "maxspeed", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "30" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "no" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Cyclestreets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'Cyclestreets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key overtaking:motor_vehicle.", + "value": "" + }, + { + "key": "cyclestreet:start_date", + "description": "Layer 'Cyclestreets' shows and asks freeform values for key 'cyclestreet:start_date' (in the MapComplete.osm.be theme 'Cyclestreets')" + }, + { + "key": "proposed:cyclestreet", + "description": "The MapComplete theme Cyclestreets has a layer Future cyclestreet showing features with this tag", + "value": "yes" + }, + { + "key": "image", + "description": "The layer 'Future cyclestreet 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" + }, + { + "key": "mapillary", + "description": "The layer 'Future cyclestreet 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" + }, + { + "key": "wikidata", + "description": "The layer 'Future cyclestreet 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" + }, + { + "key": "wikipedia", + "description": "The layer 'Future cyclestreet 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" + }, + { + "key": "cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "maxspeed", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "30" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "no" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'Future cyclestreet' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'Future cyclestreet' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key overtaking:motor_vehicle.", + "value": "" + }, + { + "key": "cyclestreet:start_date", + "description": "Layer 'Future cyclestreet' shows and asks freeform values for key 'cyclestreet:start_date' (in the MapComplete.osm.be theme 'Cyclestreets')" + }, + { + "key": "highway", + "description": "The MapComplete theme Cyclestreets has a layer All streets showing features with this tag", + "value": "residential" + }, + { + "key": "highway", + "description": "The MapComplete theme Cyclestreets has a layer All streets showing features with this tag", + "value": "tertiary" + }, + { + "key": "highway", + "description": "The MapComplete theme Cyclestreets has a layer All streets showing features with this tag", + "value": "unclassified" + }, + { + "key": "image", + "description": "The layer 'All streets 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" + }, + { + "key": "mapillary", + "description": "The layer 'All streets 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" + }, + { + "key": "wikidata", + "description": "The layer 'All streets 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" + }, + { + "key": "wikipedia", + "description": "The layer 'All streets 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" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "maxspeed", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "30" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "no" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes&maxspeed=30&overtaking:motor_vehicle=no&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet (and has a speed limit of 30 km/h)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=yes&proposed:cyclestreet= with a fixed text, namely 'This street is a cyclestreet' (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=&proposed:cyclestreet=yes with a fixed text, namely 'This street will become a cyclstreet soon' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets')", + "value": "yes" + }, + { + "key": "cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key cyclestreet.", + "value": "" + }, + { + "key": "proposed:cyclestreet", + "description": "Layer 'All streets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key proposed:cyclestreet.", + "value": "" + }, + { + "key": "overtaking:motor_vehicle", + "description": "Layer 'All streets' shows cyclestreet=&proposed:cyclestreet=&overtaking:motor_vehicle= with a fixed text, namely 'This street is not a cyclestreet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cyclestreets') Picking this answer will delete the key overtaking:motor_vehicle.", + "value": "" + }, + { + "key": "cyclestreet:start_date", + "description": "Layer 'All streets' shows and asks freeform values for key 'cyclestreet:start_date' (in the MapComplete.osm.be theme 'Cyclestreets')" + } + ] +} \ No newline at end of file diff --git a/Docs/TagInfo/mapcomplete_surveillance.json b/Docs/TagInfo/mapcomplete_surveillance.json index 7c0ecfc1b1..0b9b99e17f 100644 --- a/Docs/TagInfo/mapcomplete_surveillance.json +++ b/Docs/TagInfo/mapcomplete_surveillance.json @@ -5,7 +5,7 @@ "description": "Surveillance cameras and other means of surveillance", "project_url": "https://mapcomplete.osm.be/surveillance", "doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/", - "icon_url": "https://mapcomplete.osm.be/assets/themes/surveillance_cameras/logo.svg", + "icon_url": "https://mapcomplete.osm.be/assets/themes/surveillance/logo.svg", "contact_name": "Pieter Vander Vennet, ", "contact_email": "pietervdvn@posteo.net" }, diff --git a/Docs/Tags_format.md b/Docs/Tags_format.md index f6968f615a..7fd6f52d12 100644 --- a/Docs/Tags_format.md +++ b/Docs/Tags_format.md @@ -29,6 +29,16 @@ To check if a key does _not_ equal a certain value, use `key!=value`. This is co This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not empty. +Number comparison +----------------- + +If the value of a tag is a number (e.g. `key=42`), one can use a filter `key<=42`, `key>=35`, `key>40` or `key<50` to match this, e.g. in conditions for renderings. +These tags cannot be used to generate an answer nor can they be used to request data upstream from overpass. + +Note that the value coming from OSM will first be stripped by removing all non-numeric characters. For example, `length=42 meter` will be interpreted as `length=42` and will thus match `length<=42` and `length>=42`. +In special circumstances (e.g. `surface_area=42 m2` or `length=100 feet`), this will result in erronous values (`surface=422` or if a length in meters is compared to). +However, this can be partially alleviated by using 'Units' to rewrite to a default format. + Regex equals ------------ diff --git a/InitUiElements.ts b/InitUiElements.ts index 102c4fd3fe..833c1f4952 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -15,7 +15,7 @@ import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {Utils} from "./Utils"; import Svg from "./Svg"; import Link from "./UI/Base/Link"; -import * as personal from "./assets/themes/personalLayout/personalLayout.json" +import * as personal from "./assets/themes/personal/personal.json" import LayoutConfig from "./Customizations/JSON/LayoutConfig"; import * as L from "leaflet"; import Img from "./UI/Base/Img"; @@ -41,22 +41,34 @@ import AllKnownLayers from "./Customizations/AllKnownLayers"; import LayerConfig from "./Customizations/JSON/LayerConfig"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import ExportPDF from "./Logic/Actors/ExportPDF"; +import {TagsFilter} from "./Logic/Tags/TagsFilter"; +import FilterView from "./UI/BigComponents/FilterView"; export class InitUiElements { - - - static InitAll(layoutToUse: LayoutConfig, layoutFromBase64: string, testing: UIEventSource, layoutName: string, - layoutDefinition: string = "") { - + static InitAll( + layoutToUse: LayoutConfig, + layoutFromBase64: string, + testing: UIEventSource, + layoutName: string, + layoutDefinition: string = "" + ) { if (layoutToUse === undefined) { - console.log("Incorrect layout") - new FixedUiElement(`Error: incorrect layout ${layoutName}
Go back`).AttachTo("centermessage").onClick(() => { - }); - throw "Incorrect layout" + console.log("Incorrect layout"); + new FixedUiElement( + `Error: incorrect layout ${layoutName}
Go back` + ) + .AttachTo("centermessage") + .onClick(() => { + }); + throw "Incorrect layout"; } - console.log("Using layout: ", layoutToUse.id, "LayoutFromBase64 is ", layoutFromBase64); - + console.log( + "Using layout: ", + layoutToUse.id, + "LayoutFromBase64 is ", + layoutFromBase64 + ); State.state = new State(layoutToUse); @@ -65,42 +77,48 @@ export class InitUiElements { window.mapcomplete_state = State.state; if (layoutToUse.hideFromOverview) { - State.state.osmConnection.GetPreference("hidden-theme-" + layoutToUse.id + "-enabled").setData("true"); + State.state.osmConnection + .GetPreference("hidden-theme-" + layoutToUse.id + "-enabled") + .setData("true"); } if (layoutFromBase64 !== "false") { State.state.layoutDefinition = layoutDefinition; - console.log("Layout definition:", Utils.EllipsesAfter(State.state.layoutDefinition, 100)) + console.log( + "Layout definition:", + Utils.EllipsesAfter(State.state.layoutDefinition, 100) + ); if (testing.data !== "true") { State.state.osmConnection.OnLoggedIn(() => { - State.state.osmConnection.GetLongPreference("installed-theme-" + layoutToUse.id).setData(State.state.layoutDefinition); - }) + State.state.osmConnection + .GetLongPreference("installed-theme-" + layoutToUse.id) + .setData(State.state.layoutDefinition); + }); } else { - console.warn("NOT saving custom layout to OSM as we are tesing -> probably in an iFrame") + console.warn( + "NOT saving custom layout to OSM as we are tesing -> probably in an iFrame" + ); } } - function updateFavs() { // This is purely for the personal theme to load the layers there const favs = State.state.favouriteLayers.data ?? []; const neededLayers = new Set(); - console.log("Favourites are: ", favs) + console.log("Favourites are: ", favs); layoutToUse.layers.splice(0, layoutToUse.layers.length); let somethingChanged = false; for (const fav of favs) { - if (AllKnownLayers.sharedLayers.has(fav)) { - const layer = AllKnownLayers.sharedLayers.get(fav) + const layer = AllKnownLayers.sharedLayers.get(fav); if (!neededLayers.has(layer)) { - neededLayers.add(layer) + neededLayers.add(layer); somethingChanged = true; } } - for (const layouts of State.state.installedThemes.data) { for (const layer of layouts.layout.layers) { if (typeof layer === "string") { @@ -108,7 +126,7 @@ export class InitUiElements { } if (layer.id === fav) { if (!neededLayers.has(layer)) { - neededLayers.add(layer) + neededLayers.add(layer); somethingChanged = true; } } @@ -116,15 +134,13 @@ export class InitUiElements { } } if (somethingChanged) { - console.log("layoutToUse.layers:", layoutToUse.layers) + console.log("layoutToUse.layers:", layoutToUse.layers); State.state.layoutToUse.data.layers = Array.from(neededLayers); State.state.layoutToUse.ping(); State.state.layerUpdater?.ForceRefresh(); } - } - if (layoutToUse.customCss !== undefined) { Utils.LoadCustomCss(layoutToUse.customCss); } @@ -132,38 +148,47 @@ export class InitUiElements { InitUiElements.InitBaseMap(); InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { - new UserBadge().AttachTo('userbadge'); + new UserBadge().AttachTo("userbadge"); }); - InitUiElements.OnlyIf((State.state.featureSwitchSearch), () => { + InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => { new SearchAndGo().AttachTo("searchbox"); }); - InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { - InitUiElements.InitWelcomeMessage() + InitUiElements.InitWelcomeMessage(); }); - if ((window != window.top && !State.state.featureSwitchWelcomeMessage.data) || State.state.featureSwitchIframe.data) { + if ( + (window != window.top && !State.state.featureSwitchWelcomeMessage.data) || + State.state.featureSwitchIframe.data + ) { const currentLocation = State.state.locationControl; - const url = `${window.location.origin}${window.location.pathname}?z=${currentLocation.data.zoom ?? 0}&lat=${currentLocation.data.lat ?? 0}&lon=${currentLocation.data.lon ?? 0}`; + const url = `${window.location.origin}${window.location.pathname}?z=${ + currentLocation.data.zoom ?? 0 + }&lat=${currentLocation.data.lat ?? 0}&lon=${ + currentLocation.data.lon ?? 0 + }`; new MapControlButton( - new Link(Svg.pop_out_img, url, true) - .SetClass("block w-full h-full p-1.5") - ) - .AttachTo("messagesbox"); + new Link(Svg.pop_out_img, url, true).SetClass( + "block w-full h-full p-1.5" + ) + ).AttachTo("messagesbox"); } - State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home) - .addCallbackAndRunD(home => { - const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color") + State.state.osmConnection.userDetails + .map((userDetails: UserDetails) => userDetails?.home) + .addCallbackAndRunD((home) => { + const color = getComputedStyle(document.body).getPropertyValue( + "--subtle-detail-color" + ); const icon = L.icon({ iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), iconSize: [30, 30], - iconAnchor: [15, 15] + iconAnchor: [15, 15], }); - const marker = L.marker([home.lat, home.lon], {icon: icon}) - marker.addTo(State.state.leafletMap.data) + const marker = L.marker([home.lat, home.lon], {icon: icon}); + marker.addTo(State.state.leafletMap.data); }); const geolocationButton = new Toggle( @@ -172,30 +197,34 @@ export class InitUiElements { State.state.currentGPSLocation, State.state.leafletMap, State.state.layoutToUse - )), + ), { + dontStyle : true + } + ), undefined, - State.state.featureSwitchGeolocation); + State.state.featureSwitchGeolocation + ); const plus = new MapControlButton( - Svg.plus_ui() + Svg.plus_zoom_svg() ).onClick(() => { State.state.locationControl.data.zoom++; State.state.locationControl.ping(); - }) + }); const min = new MapControlButton( - Svg.min_ui() + Svg.min_zoom_svg() ).onClick(() => { State.state.locationControl.data.zoom--; State.state.locationControl.ping(); - }) + }); const screenshot = new MapControlButton( - new FixedUiElement( - Img.AsImageElement(Svg.bug, "", "width:1.25rem;height:1.25rem") - ) + Svg.bug_svg(), ).onClick(() => { - let createdPDF = new ExportPDF("Screenshot", "natuurpunt"); + // Will already export + new ExportPDF("Screenshot", "natuurpunt"); + }) new Combine([plus, min, geolocationButton, screenshot].map(el => el.SetClass("m-0.5 md:m-1"))) @@ -216,38 +245,45 @@ export class InitUiElements { // Reset the loading message once things are loaded new CenterMessageBox().AttachTo("centermessage"); - document.getElementById("centermessage").classList.add("pointer-events-none") - - + document + .getElementById("centermessage") + .classList.add("pointer-events-none"); } - static LoadLayoutFromHash(userLayoutParam: UIEventSource): [LayoutConfig, string]{ + static LoadLayoutFromHash( + userLayoutParam: UIEventSource + ): [LayoutConfig, string] { try { let hash = location.hash.substr(1); const layoutFromBase64 = userLayoutParam.data; // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter - const dedicatedHashFromLocalStorage = LocalStorageSource.Get("user-layout-" + layoutFromBase64.replace(" ", "_")); + const dedicatedHashFromLocalStorage = LocalStorageSource.Get( + "user-layout-" + layoutFromBase64.replace(" ", "_") + ); if (dedicatedHashFromLocalStorage.data?.length < 10) { 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; } else { - console.log("Saving hash to local storage") + console.log("Saving hash to local storage"); hashFromLocalStorage.setData(hash); dedicatedHashFromLocalStorage.setData(hash); } - let json: {} + let json: {}; try { json = JSON.parse(atob(hash)); } catch (e) { // We try to decode with lz-string - json = JSON.parse(Utils.UnMinify(LZString.decompressFromBase64(hash))) as LayoutConfigJson; - + json = JSON.parse( + Utils.UnMinify(LZString.decompressFromBase64(hash)) + ) as LayoutConfigJson; } // @ts-ignore @@ -255,13 +291,17 @@ export class InitUiElements { userLayoutParam.setData(layoutToUse.id); return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; } catch (e) { - - new FixedUiElement("Error: could not parse the custom layout:
" + e).AttachTo("centermessage"); + new FixedUiElement( + "Error: could not parse the custom layout:
" + e + ).AttachTo("centermessage"); throw e; } } - private static OnlyIf(featureSwitch: UIEventSource, callback: () => void) { + private static OnlyIf( + featureSwitch: UIEventSource, + callback: () => void + ) { featureSwitch.addCallbackAndRun(() => { if (featureSwitch.data) { callback(); @@ -270,19 +310,15 @@ export class InitUiElements { } private static InitWelcomeMessage() { - const isOpened = new UIEventSource(false); const fullOptions = new FullWelcomePaneWithTabs(isOpened); // ?-Button on Desktop, opens panel with close-X. const help = new MapControlButton(Svg.help_svg()); - help.onClick(() => isOpened.setData(true)) - new Toggle( - fullOptions - .SetClass("welcomeMessage"), - help - , isOpened - ).AttachTo("messagesbox"); + help.onClick(() => isOpened.setData(true)); + new Toggle(fullOptions.SetClass("welcomeMessage"), help, isOpened).AttachTo( + "messagesbox" + ); const openedTime = new Date().getTime(); State.state.locationControl.addCallback(() => { if (new Date().getTime() - openedTime < 15 * 1000) { @@ -290,75 +326,107 @@ export class InitUiElements { return; } isOpened.setData(false); - }) + }); - State.state.selectedElement.addCallbackAndRunD(_ => { - isOpened.setData(false); - }) - isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome") + State.state.selectedElement.addCallbackAndRunD((_) => { + isOpened.setData(false); + }); + isOpened.setData( + Hash.hash.data === undefined || + Hash.hash.data === "" || + Hash.hash.data == "welcome" + ); } private static InitLayerSelection(featureSource: FeatureSource) { + const copyrightNotice = new ScrollableFullScreen( + () => Translations.t.general.attribution.attributionTitle.Clone(), + () => + new AttributionPanel( + State.state.layoutToUse, + new ContributorCount(featureSource).Contributors + ), + "copyright" + ); - const copyrightNotice = - new ScrollableFullScreen( - () => Translations.t.general.attribution.attributionTitle.Clone(), - () => new AttributionPanel(State.state.layoutToUse, new ContributorCount(featureSource).Contributors), - "copyright" - ) - - ; const copyrightButton = new Toggle( copyrightNotice, - new MapControlButton(Svg.osm_copyright_svg()), + new MapControlButton(Svg.copyright_svg()), copyrightNotice.isShown - ).ToggleOnClick() - .SetClass("p-0.5") + ) + .ToggleOnClick() + .SetClass("p-0.5"); const layerControlPanel = new LayerControlPanel( - State.state.layerControlIsOpened) - .SetClass("block p-1 rounded-full"); + State.state.layerControlIsOpened + ).SetClass("block p-1 rounded-full"); + const layerControlButton = new Toggle( layerControlPanel, new MapControlButton(Svg.layers_svg()), State.state.layerControlIsOpened - ).ToggleOnClick() + ).ToggleOnClick(); const layerControl = new Toggle( layerControlButton, "", State.state.featureSwitchLayers - ) + ); - new Combine([copyrightButton, layerControl]) + + const filterView = + new ScrollableFullScreen( + () => Translations.t.general.layerSelection.title.Clone(), + () => + new FilterView(State.state.filteredLayers).SetClass( + "block p-1 rounded-full" + ), + "filter", + State.state.filterIsOpened + ); + + + const filterMapControlButton = new MapControlButton( + Svg.filter_svg() + ); + + const filterButton = new Toggle( + filterView, + filterMapControlButton, + State.state.filterIsOpened + ).ToggleOnClick(); + + const filterControl = new Toggle( + filterButton, + undefined, + State.state.featureSwitchFilter + ); + + new Combine([copyrightButton, layerControl, filterControl]) + .SetClass("flex flex-col") .AttachTo("bottom-left"); + State.state.locationControl.addCallback(() => { + // Close the layer selection when the map is moved + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); - State.state.locationControl - .addCallback(() => { - // Close the layer selection when the map is moved - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }); - - State.state.selectedElement.addCallbackAndRunD(_ => { - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }) - + State.state.selectedElement.addCallbackAndRunD((_) => { + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); } private static InitBaseMap() { - - State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); - - State.state.backgroundLayer = State.state.backgroundLayerId - .map((selectedId: string) => { - if(selectedId === undefined){ - return AvailableBaseLayers.osmCarto + State.state.availableBackgroundLayers = + AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); + State.state.backgroundLayer = State.state.backgroundLayerId.map( + (selectedId: string) => { + if (selectedId === undefined) { + return AvailableBaseLayers.osmCarto; } - - + const available = State.state.availableBackgroundLayers.data; for (const layer of available) { if (layer.id === selectedId) { @@ -366,98 +434,126 @@ export class InitUiElements { } } return AvailableBaseLayers.osmCarto; - }, [State.state.availableBackgroundLayers], layer => layer.id); - + }, + [State.state.availableBackgroundLayers], + (layer) => layer.id + ); + new LayerResetter( - State.state.backgroundLayer, State.state.locationControl, - State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId)); + State.state.backgroundLayer, + State.state.locationControl, + State.state.availableBackgroundLayers, + State.state.layoutToUse.map( + (layout: LayoutConfig) => layout.defaultBackgroundId + ) + ); + const attr = new Attribution( + State.state.locationControl, + State.state.osmConnection.userDetails, + State.state.layoutToUse, + State.state.leafletMap + ); - const attr = new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, - State.state.leafletMap); - - const bm = new Basemap("leafletDiv", + const bm = new Basemap( + "leafletDiv", State.state.locationControl, State.state.backgroundLayer, State.state.LastClickLocation, attr ); State.state.leafletMap.setData(bm.map); - const layout = State.state.layoutToUse.data + const layout = State.state.layoutToUse.data; if (layout.lockLocation) { - if (layout.lockLocation === true) { - const tile = Utils.embedded_tile(layout.startLat, layout.startLon, layout.startZoom - 1) - const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y) + const tile = Utils.embedded_tile( + layout.startLat, + layout.startLon, + layout.startZoom - 1 + ); + const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y); // We use the bounds to get a sense of distance for this zoom level - const latDiff = bounds[0][0] - bounds[1][0] - const lonDiff = bounds[0][1] - bounds[1][1] - layout.lockLocation = [[layout.startLat - latDiff, layout.startLon - lonDiff], + const latDiff = bounds[0][0] - bounds[1][0]; + const lonDiff = bounds[0][1] - bounds[1][1]; + layout.lockLocation = [ + [layout.startLat - latDiff, layout.startLon - lonDiff], [layout.startLat + latDiff, layout.startLon + lonDiff], ]; } - console.warn("Locking the bounds to ", layout.lockLocation) + console.warn("Locking the bounds to ", layout.lockLocation); bm.map.setMaxBounds(layout.lockLocation); - bm.map.setMinZoom(layout.startZoom) + bm.map.setMinZoom(layout.startZoom); } - } private static InitLayers(): FeatureSource { - - const state = State.state; - state.filteredLayers = - state.layoutToUse.map(layoutToUse => { - const flayers = []; + state.filteredLayers = state.layoutToUse.map((layoutToUse) => { + const flayers = []; + for (const layer of layoutToUse.layers) { + const isDisplayed = QueryParameters.GetQueryParameter( + "layer-" + layer.id, + "true", + "Wether or not layer " + layer.id + " is shown" + ).map( + (str) => str !== "false", + [], + (b) => b.toString() + ); + const flayer = { + isDisplayed: isDisplayed, + layerDef: layer, + appliedFilters: new UIEventSource(undefined) + }; + flayers.push(flayer); + } + return flayers; + }); - for (const layer of layoutToUse.layers) { - const isDisplayed = QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") - .map((str) => str !== "false", [], (b) => b.toString()); - const flayer = { - isDisplayed: isDisplayed, - layerDef: layer - } - flayers.push(flayer); - } - return flayers; - }); - - const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); + const updater = new LoadFromOverpass( + state.locationControl, + state.layoutToUse, + state.leafletMap + ); State.state.layerUpdater = updater; - - const source = new FeaturePipeline(state.filteredLayers, + const source = new FeaturePipeline( + state.filteredLayers, + State.state.changes, updater, state.osmApiFeatureSource, state.layoutToUse, - state.changes, state.locationControl, - state.selectedElement); + state.selectedElement + ); State.state.featurePipeline = source; + new ShowDataLayer( + source.features, + State.state.leafletMap, + State.state.layoutToUse + ); - new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse); - - const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource); + const selectedFeatureHandler = new SelectedFeatureHandler( + Hash.hash, + State.state.selectedElement, + source, + State.state.osmApiFeatureSource + ); selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); return source; } private static setupAllLayerElements() { - // ------------- Setup the layers ------------------------------- const source = InitUiElements.InitLayers(); InitUiElements.InitLayerSelection(source); - // ------------------ Setup various other UI elements ------------ - InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => { - let presetCount = 0; for (const layer of State.state.filteredLayers.data) { for (const preset of layer.layerDef.presets) { @@ -468,18 +564,18 @@ export class InitUiElements { return; } - const newPointDialogIsShown = new UIEventSource(false); const addNewPoint = new ScrollableFullScreen( () => Translations.t.general.add.title.Clone(), () => new SimpleAddUI(newPointDialogIsShown), "new", - newPointDialogIsShown) - addNewPoint.isShown.addCallback(isShown => { + newPointDialogIsShown + ); + addNewPoint.isShown.addCallback((isShown) => { if (!isShown) { - State.state.LastClickLocation.setData(undefined) + State.state.LastClickLocation.setData(undefined); } - }) + }); new StrayClickHandler( State.state.LastClickLocation, @@ -489,7 +585,5 @@ export class InitUiElements { addNewPoint ); }); - - } -} \ No newline at end of file +} diff --git a/Logic/Actors/ChangeToElementsActor.ts b/Logic/Actors/ChangeToElementsActor.ts new file mode 100644 index 0000000000..9ae5f87ef4 --- /dev/null +++ b/Logic/Actors/ChangeToElementsActor.ts @@ -0,0 +1,36 @@ +import {ElementStorage} from "../ElementStorage"; +import {Changes} from "../Osm/Changes"; + +export default class ChangeToElementsActor { + constructor(changes: Changes, allElements: ElementStorage) { + changes.pendingChanges.addCallbackAndRun(changes => { + for (const change of changes) { + const id = change.type + "/" + change.id; + if (!allElements.has(id)) { + continue; // Ignored as the geometryFixer will introduce this + } + const src = allElements.getEventSourceById(id) + + 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; + } + if (src.data[k] === v) { + continue + } + changed = true; + src.data[k] = v; + } + if (changed) { + src.ping() + } + + + } + }) + } +} \ No newline at end of file diff --git a/Logic/Actors/ExportPDF.ts b/Logic/Actors/ExportPDF.ts index 3eb3fa4d4b..78316be729 100644 --- a/Logic/Actors/ExportPDF.ts +++ b/Logic/Actors/ExportPDF.ts @@ -23,12 +23,16 @@ //minimap op index.html -> hidden daar alles op doen en dan weg //minimap - leaflet map ophalen - boundaries ophalen - State.state.featurePipeline screenshotter.addTo(State.state.leafletMap.data); - let doc = new jsPDF('l'); + let doc = new jsPDF('landscape'); + console.log("Taking screenshot") screenshotter.takeScreen('image').then(image => { + if(!(image instanceof Blob)){ + alert("Exporting failed :(") + return; + } let file = new PDFLayout(); file.AddLayout(layout, doc, image); - console.log("SCREENSHOTTER"); doc.save(name); }) } - } \ No newline at end of file + } diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 7bf736e3fa..169fbbaabf 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -1,11 +1,11 @@ import * as L from "leaflet"; import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; import Svg from "../../Svg"; import Img from "../../UI/Base/Img"; import {LocalStorageSource} from "../Web/LocalStorageSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import {VariableUiElement} from "../../UI/Base/VariableUIElement"; +import BaseUIElement from "../../UI/BaseUIElement"; export default class GeoLocationHandler extends VariableUiElement { /** @@ -44,11 +44,13 @@ export default class GeoLocationHandler extends VariableUiElement { * @private */ 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 */ private _lastUserRequest: Date; + /** * A small flag on localstorage. If the user previously granted the geolocation, it will be set. * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. @@ -77,19 +79,23 @@ export default class GeoLocationHandler extends VariableUiElement { super( hasLocation.map( (hasLocationData) => { + let icon: BaseUIElement; + if (isLocked.data) { - return Svg.crosshair_locked_ui(); + icon = Svg.location_svg(); } else if (hasLocationData) { - return Svg.crosshair_blue_ui(); + icon = Svg.location_empty_svg(); } else if (isActive.data) { - return Svg.crosshair_blue_center_ui(); + icon = Svg.location_empty_svg(); } else { - return Svg.crosshair_ui(); + icon = Svg.location_circle_svg(); } + return icon }, [isActive, isLocked] ) ); + this.SetClass("mapcontrol") this._isActive = isActive; this._isLocked = isLocked; this._permission = new UIEventSource(""); diff --git a/Logic/Actors/PDFLayout.ts b/Logic/Actors/PDFLayout.ts index 2478fb1183..db4ddd66d2 100644 --- a/Logic/Actors/PDFLayout.ts +++ b/Logic/Actors/PDFLayout.ts @@ -23,4 +23,4 @@ doc.addImage(image, 'PNG', 15, 30, 150*screenRatio, 150); return doc; } - } \ No newline at end of file + } diff --git a/Logic/Actors/PendingChangesUploader.ts b/Logic/Actors/PendingChangesUploader.ts index b2a8e100dc..82adde57b3 100644 --- a/Logic/Actors/PendingChangesUploader.ts +++ b/Logic/Actors/PendingChangesUploader.ts @@ -9,7 +9,7 @@ export default class PendingChangesUploader { constructor(changes: Changes, selectedFeature: UIEventSource) { const self = this; this.lastChange = new Date(); - changes.pending.addCallback(() => { + changes.pendingChanges.addCallback(() => { self.lastChange = new Date(); window.setTimeout(() => { @@ -54,7 +54,7 @@ export default class PendingChangesUploader { function onunload(e) { - if (changes.pending.data.length == 0) { + if(changes.pendingChanges.data.length == 0){ return; } changes.flushChanges("onbeforeunload - probably closing or something similar"); diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 3aa5d8e5bc..3cfaa079cf 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -13,7 +13,7 @@ export default class SelectedFeatureHandler { private readonly _hash: UIEventSource; private readonly _selectedFeature: UIEventSource; - private static readonly _no_trigger_on = ["welcome","copyright","layers"] + private static readonly _no_trigger_on = ["welcome","copyright","layers","new"] private readonly _osmApiSource: OsmApiFeatureSource; constructor(hash: UIEventSource, @@ -60,7 +60,9 @@ export default class SelectedFeatureHandler { if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){ return; // No valid feature selected } - // We should have a valid osm-ID and zoom to it + // We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure + try{ + OsmObject.DownloadObject(hash).addCallbackAndRunD(element => { const centerpoint = element.centerpoint(); console.log("Zooming to location for select point: ", centerpoint) @@ -68,6 +70,9 @@ export default class SelectedFeatureHandler { location.data.lon = centerpoint[1] location.ping(); }) + }catch(e){ + console.error("Could not download OSM-object with id", hash, " - probably a weird hash") + } } private downloadFeature(hash: string){ diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index f4512b9b8d..b3b10ac2ab 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -6,6 +6,8 @@ import {Utils} from "../Utils"; import BaseUIElement from "../UI/BaseUIElement"; import List from "../UI/Base/List"; import Title from "../UI/Base/Title"; +import {UIEventSourceTools} from "./UIEventSource"; +import AspectedRouting from "./Osm/aspectedRouting"; export class ExtraFunction { @@ -38,12 +40,14 @@ export class ExtraFunction { ]), "Some advanced functions are available on **feat** as well:" ]).SetClass("flex-col").AsMarkdown(); - - + + private static readonly OverlapFunc = new ExtraFunction( - "overlapWith", - "Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point 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", - ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"], + { + name: "overlapWith", + doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. If the current feature is a point, all features that embed the point 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", + args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] + }, (params, feat) => { return (...layerIds: string[]) => { const result = [] @@ -62,9 +66,11 @@ export class ExtraFunction { } ) private static readonly DistanceToFunc = new ExtraFunction( - "distanceTo", - "Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object", - ["longitude", "latitude"], + { + name: "distanceTo", + doc: "Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object", + args: ["longitude", "latitude"] + }, (featuresPerLayer, feature) => { return (arg0, lat) => { if (typeof arg0 === "number") { @@ -88,9 +94,11 @@ export class ExtraFunction { ) private static readonly ClosestObjectFunc = new ExtraFunction( - "closest", - "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.", - ["list of features"], + { + 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.", + args: ["list of features"] + }, (params, feature) => { return (features) => { if (typeof features === "string") { @@ -139,28 +147,56 @@ export class ExtraFunction { private static readonly Memberships = new ExtraFunction( - "memberships", - "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(';')`", - [], + { + name: "memberships", + 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: [] + }, (params, _) => { return () => params.relations ?? []; } ) - private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, ExtraFunction.Memberships]; + private static readonly AspectedRouting = new ExtraFunction( + { + name: "score", + doc: "Given the path of an aspected routing json file, will calculate the score. This score is wrapped in a UIEventSource, so for further calculations, use `.map(score => ...)`" + + "\n\n" + + "For example: `_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')`", + args: ["path"] + }, + (_, feature) => { + return (path) => { + return UIEventSourceTools.downloadJsonCached(path).map(config => { + if (config === undefined) { + return + } + return new AspectedRouting(config).evaluate(feature.properties) + }) + } + } + ) + + private static readonly allFuncs: ExtraFunction[] = [ + ExtraFunction.DistanceToFunc, + ExtraFunction.OverlapFunc, + ExtraFunction.ClosestObjectFunc, + ExtraFunction.Memberships, + ExtraFunction.AspectedRouting + ]; private readonly _name: string; private readonly _args: string[]; private readonly _doc: string; private readonly _f: (params: { featuresPerLayer: Map, relations: { role: string, relation: Relation }[] }, feat: any) => any; - constructor(name: string, doc: string, args: string[], f: ((params: { featuresPerLayer: Map, relations: { role: string, relation: Relation }[] }, feat: any) => any)) { - this._name = name; - this._doc = doc; - this._args = args; + constructor(options: { name: string, doc: string, args: string[] }, + f: ((params: { featuresPerLayer: Map, relations: { role: string, relation: Relation }[] }, feat: any) => any)) { + this._name = options.name; + this._doc = options.doc; + this._args = options.args; this._f = f; - } public static FullPatchFeature(featuresPerLayer: Map, relations: { role: string, relation: Relation }[], feature) { @@ -186,7 +222,6 @@ export class ExtraFunction { } public PatchFeature(featuresPerLayer: Map, relations: { role: string, relation: Relation }[], feature: any) { - - feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature); + feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature) } } diff --git a/Logic/FeatureSource/ChangeApplicator.ts b/Logic/FeatureSource/ChangeApplicator.ts new file mode 100644 index 0000000000..9b4f2271dc --- /dev/null +++ b/Logic/FeatureSource/ChangeApplicator.ts @@ -0,0 +1,162 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import {Changes} from "../Osm/Changes"; +import {ChangeDescription} from "../Osm/Actions/ChangeDescription"; +import {Utils} from "../../Utils"; +import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; + + +/** + * Applies changes from 'Changes' onto a featureSource + */ +export default class ChangeApplicator implements FeatureSource { + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; + public readonly name: string; + + constructor(source: FeatureSource, changes: Changes, mode?: { + generateNewGeometries: boolean + }) { + + this.name = "ChangesApplied(" + source.name + ")" + this.features = source.features + const seenChanges = new Set(); + const self = this; + let runningUpdate = false; + source.features.addCallbackAndRunD(features => { + if (runningUpdate) { + return; // No need to ping again + } + ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode) + seenChanges.clear() + }) + + changes.pendingChanges.addCallbackAndRunD(changes => { + runningUpdate = true; + changes = changes.filter(ch => !seenChanges.has(ch)) + changes.forEach(c => seenChanges.add(c)) + ChangeApplicator.ApplyChanges(self.features.data, changes, mode) + source.features.ping() + runningUpdate = false; + }) + + + } + + + /** + * Returns true if the geometry is changed and the source should be pinged + */ + private static ApplyChanges(features: { feature: any; freshness: Date }[], cs: ChangeDescription[], mode: { generateNewGeometries: boolean }): boolean { + if (cs.length === 0 || features === undefined) { + return; + } + + console.log("Applying changes ", this.name, cs) + let geometryChanged = false; + const changesPerId: Map = new Map() + for (const c of cs) { + const id = c.type + "/" + c.id + if (!changesPerId.has(id)) { + changesPerId.set(id, []) + } + changesPerId.get(id).push(c) + } + + + const now = new Date() + + function add(feature) { + feature.id = feature.properties.id + features.push({ + feature: feature, + freshness: now + }) + console.log("Added a new feature: ", feature) + geometryChanged = true; + } + + // First, create the new features - they have a negative ID + // We don't set the properties yet though + if (mode?.generateNewGeometries) { + changesPerId.forEach(cs => { + cs + .forEach(change => { + if (change.id >= 0) { + return; // Nothing to do here, already created + } + + if (change.changes === undefined) { + // An update to the object - not the actual created + return; + } + + try { + + switch (change.type) { + case "node": + const n = new OsmNode(change.id) + n.lat = change.changes["lat"] + n.lon = change.changes["lon"] + const geojson = n.asGeoJson() + add(geojson) + break; + case "way": + const w = new OsmWay(change.id) + w.nodes = change.changes["nodes"] + add(w.asGeoJson()) + break; + case "relation": + const r = new OsmRelation(change.id) + r.members = change.changes["members"] + add(r.asGeoJson()) + break; + } + + } catch (e) { + console.error(e) + } + }) + }) + } + + for (const feature of features) { + const f = feature.feature; + const id = f.properties.id; + if (!changesPerId.has(id)) { + continue; + } + + + const changed = {} + // Copy all the properties + Utils.Merge(f, changed) + // play the changes onto the copied object + + for (const change of changesPerId.get(id)) { + for (const kv of change.tags ?? []) { + // Apply tag changes and ping the consumers + f.properties[kv.k] = kv.v; + } + + // Apply other changes to the object + if (change.changes !== undefined) { + geometryChanged = true; + switch (change.type) { + case "node": + // @ts-ignore + const coor: { lat, lon } = change.changes; + f.geometry.coordinates = [coor.lon, coor.lat] + break; + case "way": + f.geometry.coordinates = change.changes["locations"] + break; + case "relation": + console.error("Changes to relations are not yet supported") + break; + } + } + } + } + return geometryChanged + } +} \ No newline at end of file diff --git a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts index 0771314af4..72c370bce1 100644 --- a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts +++ b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts @@ -1,6 +1,6 @@ import FeatureSource from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; -import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import FilteredLayer from "../../Models/FilteredLayer"; /** @@ -13,7 +13,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource { public readonly name; - constructor(layers: UIEventSource<{ layerDef: LayerConfig }[]>, upstream: FeatureSource) { + constructor(layers: UIEventSource, upstream: FeatureSource) { this.name = "FeatureDuplicator of "+upstream.name; this.features = upstream.features.map(features => { const newFeatures: { feature: any, freshness: Date }[] = []; diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 3407105034..be4f2b8c82 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -6,30 +6,32 @@ import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLaye import FeatureSource from "../FeatureSource/FeatureSource"; import {UIEventSource} from "../UIEventSource"; import LocalStorageSaver from "./LocalStorageSaver"; -import LayerConfig from "../../Customizations/JSON/LayerConfig"; import LocalStorageSource from "./LocalStorageSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import Loc from "../../Models/Loc"; import GeoJsonSource from "./GeoJsonSource"; import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; import RegisteringFeatureSource from "./RegisteringFeatureSource"; +import FilteredLayer from "../../Models/FilteredLayer"; +import {Changes} from "../Osm/Changes"; +import ChangeApplicator from "./ChangeApplicator"; export default class FeaturePipeline implements FeatureSource { - public features: UIEventSource<{ feature: any; freshness: Date }[]> ; + public features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name = "FeaturePipeline" - constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource, layerDef: LayerConfig }[]>, + constructor(flayers: UIEventSource, + changes: Changes, updater: FeatureSource, fromOsmApi: FeatureSource, layout: UIEventSource, - newPoints: FeatureSource, locationControl: UIEventSource, selectedElement: UIEventSource) { const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([]) - + // first we metatag, then we save to get the metatags into storage too // Note that we need to register before we do metatagging (as it expects the event sources) @@ -40,39 +42,42 @@ export default class FeaturePipeline implements FeatureSource { new MetaTaggingFeatureSource(allLoadedFeatures, new FeatureDuplicatorPerLayer(flayers, new RegisteringFeatureSource( - updater) + new ChangeApplicator( + updater, changes + )) )), layout)); const geojsonSources: FeatureSource [] = GeoJsonSource .ConstructMultiSource(flayers.data, locationControl) .map(geojsonSource => { - let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); - if(!geojsonSource.isOsmCache){ + let source = new RegisteringFeatureSource( + new FeatureDuplicatorPerLayer(flayers, + new ChangeApplicator(geojsonSource, changes))); + if (!geojsonSource.isOsmCache) { source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); } return source }); const amendedLocalStorageSource = - new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) + new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes)) )); - newPoints = new MetaTaggingFeatureSource(allLoadedFeatures, - new FeatureDuplicatorPerLayer(flayers, - new RegisteringFeatureSource(newPoints))); - const amendedOsmApiSource = new RememberingSource( new MetaTaggingFeatureSource(allLoadedFeatures, new FeatureDuplicatorPerLayer(flayers, - - new RegisteringFeatureSource(fromOsmApi)))); + new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, + { + // We lump in the new points here + generateNewGeometries: true + } + ))))); const merged = new FeatureSourceMerger([ amendedOverpassSource, amendedOsmApiSource, amendedLocalStorageSource, - newPoints, ...geojsonSources ]); diff --git a/Logic/FeatureSource/FeatureSourceMerger.ts b/Logic/FeatureSource/FeatureSourceMerger.ts index e9901d1f5b..26d9ce4986 100644 --- a/Logic/FeatureSource/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/FeatureSourceMerger.ts @@ -1,6 +1,10 @@ import FeatureSource from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; +/** + * Merges features from different featureSources + * Uses the freshest feature available in the case multiple sources offer data with the same identifier + */ export default class FeatureSourceMerger implements FeatureSource { public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 043f2ea3a2..2d06c9dca7 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -3,132 +3,160 @@ import {UIEventSource} from "../UIEventSource"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import Loc from "../../Models/Loc"; import Hash from "../Web/Hash"; +import {TagsFilter} from "../Tags/TagsFilter"; export default class FilteringFeatureSource implements FeatureSource { - public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name = "FilteringFeatureSource" - - constructor(layers: UIEventSource<{ - isDisplayed: UIEventSource, - layerDef: LayerConfig - }[]>, - location: UIEventSource, - selectedElement: UIEventSource, - upstream: FeatureSource) { + public features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource<{ feature: any; freshness: Date }[]>([]); + public readonly name = "FilteringFeatureSource"; + constructor( + layers: UIEventSource<{ + isDisplayed: UIEventSource; + layerDef: LayerConfig; + appliedFilters: UIEventSource; + }[]>, + location: UIEventSource, + selectedElement: UIEventSource, + upstream: FeatureSource + ) { const self = this; function update() { - const layerDict = {}; if (layers.data.length == 0) { - console.warn("No layers defined!") + console.warn("No layers defined!"); return; } for (const layer of layers.data) { + const prev = layerDict[layer.layerDef.id] + if (prev !== undefined) { + // We have seen this layer before! + // We prefer the one which has a name + if (layer.layerDef.name === undefined) { + // This one is hidden, so we skip it + console.log("Ignoring layer selection from ", layer) + continue; + } + } layerDict[layer.layerDef.id] = layer; } - const features: { feature: any, freshness: Date }[] = upstream.features.data; + const features: { feature: any; freshness: Date }[] = + upstream.features.data; const missingLayers = new Set(); - const newFeatures = features.filter(f => { + const newFeatures = features.filter((f) => { const layerId = f.feature._matching_layer_id; - - if(selectedElement.data?.id === f.feature.id || f.feature.id === Hash.hash.data){ - // This is the selected object - it gets a free pass even if zoom is not sufficient + + if ( + selectedElement.data?.id === f.feature.id || + f.feature.id === Hash.hash.data) { + // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away return true; } + + if (layerId === undefined) { + return false; + } + const layer: { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + appliedFilters: UIEventSource; + } = layerDict[layerId]; + if (layer === undefined) { + missingLayers.add(layerId); + return false; + } + + const isShown = layer.layerDef.isShown; + const tags = f.feature.properties; + if (isShown.IsKnown(tags)) { + const result = layer.layerDef.isShown.GetRenderValue( + f.feature.properties + ).txt; + if (result !== "yes") { + return false; + } + } - if (layerId !== undefined) { - const layer: { - isDisplayed: UIEventSource, - layerDef: LayerConfig - } = layerDict[layerId]; - if (layer === undefined) { - missingLayers.add(layerId) - return true; - } - - const isShown = layer.layerDef.isShown - const tags = f.feature.properties; - if (isShown.IsKnown(tags)) { - const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt; - if (result !== "yes") { - return false; - } - } - - if (FilteringFeatureSource.showLayer(layer, location)) { - return true; + const tagsFilter = layer.appliedFilters.data; + if (tagsFilter) { + if (!tagsFilter.matchesProperties(f.feature.properties)) { + // Hidden by the filter on the layer itself - we want to hide it no matter wat + return false; } } - // Does it match any other layer - e.g. because of a switch? - for (const toCheck of layers.data) { - if (!FilteringFeatureSource.showLayer(toCheck, location)) { - continue; - } - if (toCheck.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { - return true; - } + if (!FilteringFeatureSource.showLayer(layer, location)) { + // The layer itself is either disabled or hidden due to zoom constraints + // We should return true, but it might still match some other layer + return false; } - return false; + return true; }); - console.log("Filtering layer source: input: ", upstream.features.data?.length, "output:", newFeatures.length) + self.features.setData(newFeatures); if (missingLayers.size > 0) { - console.error("Some layers were not found: ", Array.from(missingLayers)) + console.error( + "Some layers were not found: ", + Array.from(missingLayers) + ); } } - upstream.features.addCallback(() => { - update() - }); - location.map(l => { - // We want something that is stable for the shown layers - const displayedLayerIndexes = []; - for (let i = 0; i < layers.data.length; i++) { - const layer = layers.data[i]; - if (l.zoom < layer.layerDef.minzoom) { - continue; - } - if (l.zoom > layer.layerDef.maxzoom) { - continue; - } - if (!layer.isDisplayed.data) { - continue; - } - displayedLayerIndexes.push(i); - } - return displayedLayerIndexes.join(",") - }).addCallback(() => { update(); }); + location + .map((l) => { + // We want something that is stable for the shown layers + const displayedLayerIndexes = []; + for (let i = 0; i < layers.data.length; i++) { + const layer = layers.data[i]; + if (l.zoom < layer.layerDef.minzoom) { + continue; + } + + if (!layer.isDisplayed.data) { + continue; + } + displayedLayerIndexes.push(i); + } + return displayedLayerIndexes.join(","); + }) + .addCallback(() => { + update(); + }); layers.addCallback(update); const registered = new Set>(); - layers.addCallbackAndRun(layers => { + layers.addCallbackAndRun((layers) => { for (const layer of layers) { if (registered.has(layer.isDisplayed)) { continue; } registered.add(layer.isDisplayed); layer.isDisplayed.addCallback(() => update()); + layer.appliedFilters.addCallback(() => update()); } - }) + }); update(); - } - private static showLayer(layer: { - isDisplayed: UIEventSource, - layerDef: LayerConfig - }, location: UIEventSource) { - return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom) && (layer.layerDef.maxzoom >= location.data.zoom) + private static showLayer( + layer: { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + }, + location: UIEventSource + ) { + return ( + layer.isDisplayed.data && + layer.layerDef.minzoomVisible <= location.data.zoom + ); } -} \ No newline at end of file +} diff --git a/Logic/FeatureSource/GeoJsonSource.ts b/Logic/FeatureSource/GeoJsonSource.ts index b462c87da0..c2a9db3d69 100644 --- a/Logic/FeatureSource/GeoJsonSource.ts +++ b/Logic/FeatureSource/GeoJsonSource.ts @@ -25,7 +25,7 @@ export default class GeoJsonSource implements FeatureSource { let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); this.name = "GeoJsonSource of " + url; const zoomLevel = flayer.layerDef.source.geojsonZoomLevel; - + this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer; this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) @@ -112,7 +112,17 @@ export default class GeoJsonSource implements FeatureSource { } const neededTiles = locationControl.map( - _ => { + location => { + if (!flayer.isDisplayed.data) { + // No need to download! - the layer is disabled + return undefined; + } + + if (location.zoom < flayer.layerDef.minzoom) { + // No need to download! - the layer is disabled + return undefined; + } + // Yup, this is cheating to just get the bounds here const bounds = State.state.leafletMap.data.getBounds() const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) @@ -126,14 +136,6 @@ export default class GeoJsonSource implements FeatureSource { if (needed === undefined) { return; } - if (!flayer.isDisplayed.data) { - // No need to download! - the layer is disabled - return; - } - - if (locationControl.data.zoom < flayer.layerDef.minzoom) { - return; - } needed.forEach(neededTile => { if (loadedTiles.has(neededTile)) { @@ -153,42 +155,42 @@ export default class GeoJsonSource implements FeatureSource { const self = this; Utils.downloadJson(url) .then(json => { - if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { - self.onFail("Runtime error (timeout)", url) - return; - } - const time = new Date(); - const newFeatures: { feature: any, freshness: Date } [] = [] - let i = 0; - let skipped = 0; - for (const feature of json.features) { - if (feature.properties.id === undefined) { - feature.properties.id = url + "/" + i; - feature.id = url + "/" + i; - i++; + if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { + self.onFail("Runtime error (timeout)", url) + return; } - if (self.seenids.has(feature.properties.id)) { - skipped++; - continue; - } - self.seenids.add(feature.properties.id) + const time = new Date(); + const newFeatures: { feature: any, freshness: Date } [] = [] + let i = 0; + let skipped = 0; + for (const feature of json.features) { + if (feature.properties.id === undefined) { + feature.properties.id = url + "/" + i; + feature.id = url + "/" + i; + i++; + } + if (self.seenids.has(feature.properties.id)) { + skipped++; + continue; + } + self.seenids.add(feature.properties.id) - let freshness: Date = time; - if (feature.properties["_last_edit:timestamp"] !== undefined) { - freshness = new Date(feature["_last_edit:timestamp"]) + let freshness: Date = time; + if (feature.properties["_last_edit:timestamp"] !== undefined) { + freshness = new Date(feature.properties["_last_edit:timestamp"]) + } + + newFeatures.push({feature: feature, freshness: freshness}) + } + console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); + + if (newFeatures.length == 0) { + return; } - newFeatures.push({feature: feature, freshness: freshness}) - } - console.debug("Downloaded " + newFeatures.length + " new features and " + skipped + " already seen features from " + url); + eventSource.setData(eventSource.data.concat(newFeatures)) - if (newFeatures.length == 0) { - return; - } - - eventSource.setData(eventSource.data.concat(newFeatures)) - - }).catch(msg => self.onFail(msg, url)) + }).catch(msg => self.onFail(msg, url)) } } diff --git a/Logic/FeatureSource/OsmApiFeatureSource.ts b/Logic/FeatureSource/OsmApiFeatureSource.ts index ec1c03a743..de90274d2a 100644 --- a/Logic/FeatureSource/OsmApiFeatureSource.ts +++ b/Logic/FeatureSource/OsmApiFeatureSource.ts @@ -15,15 +15,19 @@ export default class OsmApiFeatureSource implements FeatureSource { public load(id: string) { - if(id.indexOf("-") >= 0){ + if (id.indexOf("-") >= 0) { // Newly added point - not yet in OSM return; } console.debug("Downloading", id, "from the OSM-API") OsmObject.DownloadObject(id).addCallbackAndRunD(element => { - const geojson = element.asGeoJson(); - geojson.id = geojson.properties.id; - this.features.setData([{feature: geojson, freshness: element.timestamp}]) + try { + const geojson = element.asGeoJson(); + geojson.id = geojson.properties.id; + this.features.setData([{feature: geojson, freshness: element.timestamp}]) + } catch (e) { + console.error(e) + } }) } @@ -58,7 +62,7 @@ export default class OsmApiFeatureSource implements FeatureSource { const bounds = Utils.tile_bounds(z, x, y); console.log("Loading OSM data tile", z, x, y, " with bounds", bounds) OsmObject.LoadArea(bounds, objects => { - const keptGeoJson: {feature:any, freshness: Date}[] = [] + const keptGeoJson: { feature: any, freshness: Date }[] = [] // Which layer does the object match? for (const object of objects) { @@ -69,7 +73,7 @@ export default class OsmApiFeatureSource implements FeatureSource { if (doesMatch) { const geoJson = object.asGeoJson(); geoJson._matching_layer_id = layer.id - keptGeoJson.push({feature: geoJson, freshness: object.timestamp}) + keptGeoJson.push({feature: geoJson, freshness: object.timestamp}) break; } diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 768a5fe24b..a513bb8e0a 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -6,11 +6,14 @@ export class GeoOperations { return turf.area(feature); } + /** + * Converts a GeoJSon feature to a point feature + * @param feature + */ static centerpoint(feature: any) { const newFeature = turf.center(feature); newFeature.properties = feature.properties; newFeature.id = feature.id; - return newFeature; } @@ -273,14 +276,61 @@ export class GeoOperations { } return undefined; } + /** * Generates the closest point on a way from a given point * @param way The road on which you want to find a point * @param point Point defined as [lon, lat] */ - public static nearestPoint(way, point: [number, number]){ + public static nearestPoint(way, point: [number, number]) { return turf.nearestPointOnLine(way, point, {units: "kilometers"}); } + + public static toCSV(features: any[]): string { + + const headerValuesSeen = new Set(); + const headerValuesOrdered: string[] = [] + + function addH(key) { + if (!headerValuesSeen.has(key)) { + headerValuesSeen.add(key) + headerValuesOrdered.push(key) + } + } + + addH("_lat") + addH("_lon") + + const lines: string[] = [] + + for (const feature of features) { + const properties = feature.properties; + for (const key in properties) { + if (!properties.hasOwnProperty(key)) { + continue; + } + addH(key) + + } + } + headerValuesOrdered.sort() + for (const feature of features) { + const properties = feature.properties; + let line = "" + for (const key of headerValuesOrdered) { + const value = properties[key] + if (value === undefined) { + line += "," + } else { + line += JSON.stringify(value)+"," + } + } + lines.push(line) + } + + return headerValuesOrdered.map(v => JSON.stringify(v)).join(",") + "\n" + lines.join("\n") + } + } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 957ed0e379..48733d9522 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -27,8 +27,8 @@ export default class MetaTagging { relations: Map, layers: LayerConfig[], includeDates = true) { - - if(features === undefined || features.length === 0){ + + if (features === undefined || features.length === 0) { return; } @@ -79,14 +79,10 @@ export default class MetaTagging { } } - - - - - - + + }) - + } @@ -115,6 +111,17 @@ export default class MetaTagging { const f = (featuresPerLayer, feature: any) => { try { let result = func(feature); + if(result instanceof UIEventSource){ + result.addCallbackAndRunD(d => { + if (typeof d !== "string") { + // Make sure it is a string! + d = JSON.stringify(d); + } + feature.properties[key] = d; + }) + result = result.data + } + if (result === undefined || result === "") { return; } @@ -124,11 +131,11 @@ export default class MetaTagging { } feature.properties[key] = result; } catch (e) { - if(MetaTagging. errorPrintCount < MetaTagging.stopErrorOutputAt){ + if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { console.warn("Could not calculate a calculated tag defined by " + code + " due to " + e + ". This is code defined in the theme. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e) - MetaTagging. errorPrintCount ++; - if(MetaTagging. errorPrintCount == MetaTagging.stopErrorOutputAt){ - console.error("Got ",MetaTagging.stopErrorOutputAt," errors calculating this metatagging - stopping output now") + MetaTagging.errorPrintCount++; + if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { + console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") } } } diff --git a/Logic/Osm/Actions/ChangeDescription.ts b/Logic/Osm/Actions/ChangeDescription.ts new file mode 100644 index 0000000000..aefab9c1cb --- /dev/null +++ b/Logic/Osm/Actions/ChangeDescription.ts @@ -0,0 +1,30 @@ +export interface ChangeDescription { + + type: "node" | "way" | "relation", + /** + * Negative for a new objects + */ + id: number, + /* + v = "" or v = undefined to erase this tag + */ + tags?: { k: string, v: string }[], + + changes?: { + lat: number, + lon: number + } | { + // Coordinates are only used for rendering + locations: [number, number][] + nodes: number[], + } | { + members: { type: "node" | "way" | "relation", ref: number, role: string }[] + } + + /* + Set to delete the object + */ + doDelete?: boolean + + +} \ No newline at end of file diff --git a/Logic/Osm/Actions/ChangeTagAction.ts b/Logic/Osm/Actions/ChangeTagAction.ts new file mode 100644 index 0000000000..36bafcbeea --- /dev/null +++ b/Logic/Osm/Actions/ChangeTagAction.ts @@ -0,0 +1,52 @@ +import OsmChangeAction from "./OsmChangeAction"; +import {Changes} from "../Changes"; +import {ChangeDescription} from "./ChangeDescription"; +import {TagsFilter} from "../../Tags/TagsFilter"; + +export default class ChangeTagAction extends OsmChangeAction { + private readonly _elementId: string; + private readonly _tagsFilter: TagsFilter; + private readonly _currentTags: any; + + constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any) { + super(); + this._elementId = elementId; + this._tagsFilter = tagsFilter; + this._currentTags = currentTags; + } + + /** + * 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; + if (key === undefined || key === null) { + console.log("Invalid key"); + return undefined; + } + if (value === undefined || value === null) { + console.log("Invalid value for ", key); + return undefined; + } + + 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()}; + } + + CreateChangeDescriptions(changes: Changes): ChangeDescription [] { + 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 [{ + // @ts-ignore + type: type, + id: id, + tags: changedTags + }] + } +} \ No newline at end of file diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts new file mode 100644 index 0000000000..692271a126 --- /dev/null +++ b/Logic/Osm/Actions/CreateNewNodeAction.ts @@ -0,0 +1,48 @@ +import {Tag} from "../../Tags/Tag"; +import OsmChangeAction from "./OsmChangeAction"; +import {Changes} from "../Changes"; +import {ChangeDescription} from "./ChangeDescription"; +import {And} from "../../Tags/And"; + +export default class CreateNewNodeAction extends OsmChangeAction { + + private readonly _basicTags: Tag[]; + private readonly _lat: number; + private readonly _lon: number; + + public newElementId : string = undefined + + constructor(basicTags: Tag[], lat: number, lon: number) { + super() + this._basicTags = basicTags; + this._lat = lat; + this._lon = lon; + } + + CreateChangeDescriptions(changes: Changes): ChangeDescription[] { + const id = changes.getNewID() + const properties = { + id: "node/" + id + } + this.newElementId = "node/"+id + for (const kv of this._basicTags) { + if (typeof kv.value !== "string") { + throw "Invalid value: don't use a regex in a preset" + } + properties[kv.key] = kv.value; + } + + return [{ + tags: new And(this._basicTags).asChange(properties), + type: "node", + id: id, + changes:{ + lat: this._lat, + lon: this._lon + } + }] + + } + + +} \ No newline at end of file diff --git a/Logic/Osm/DeleteAction.ts b/Logic/Osm/Actions/DeleteAction.ts similarity index 95% rename from Logic/Osm/DeleteAction.ts rename to Logic/Osm/Actions/DeleteAction.ts index 73cb066df6..213bb0e86e 100644 --- a/Logic/Osm/DeleteAction.ts +++ b/Logic/Osm/Actions/DeleteAction.ts @@ -1,9 +1,9 @@ -import {UIEventSource} from "../UIEventSource"; -import {Translation} from "../../UI/i18n/Translation"; -import Translations from "../../UI/i18n/Translations"; -import {OsmObject} from "./OsmObject"; -import State from "../../State"; -import Constants from "../../Models/Constants"; +import {UIEventSource} from "../../UIEventSource"; +import {Translation} from "../../../UI/i18n/Translation"; +import State from "../../../State"; +import {OsmObject} from "../OsmObject"; +import Translations from "../../../UI/i18n/Translations"; +import Constants from "../../../Models/Constants"; export default class DeleteAction { @@ -30,7 +30,7 @@ export default class DeleteAction { * Does actually delete the feature; returns the event source 'this.isDeleted' * If deletion is not allowed, triggers the callback instead */ - public DoDelete(reason: string, onNotAllowed : () => void): UIEventSource { + public DoDelete(reason: string, onNotAllowed : () => void): void { const isDeleted = this.isDeleted const self = this; let deletionStarted = false; @@ -75,8 +75,6 @@ export default class DeleteAction { } ) - - return isDeleted; } /** diff --git a/Logic/Osm/Actions/OsmChangeAction.ts b/Logic/Osm/Actions/OsmChangeAction.ts new file mode 100644 index 0000000000..0308ca8f61 --- /dev/null +++ b/Logic/Osm/Actions/OsmChangeAction.ts @@ -0,0 +1,23 @@ +/** + * 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"; + +export default abstract class OsmChangeAction { + + private isUsed = false + + public Perform(changes: Changes) { + if (this.isUsed) { + throw "This ChangeAction is already used: " + this.constructor.name + } + this.isUsed = true; + return this.CreateChangeDescriptions(changes) + } + + protected abstract CreateChangeDescriptions(changes: Changes): ChangeDescription[] + + +} \ No newline at end of file diff --git a/Logic/Osm/Actions/RelationSplitlHandler.ts b/Logic/Osm/Actions/RelationSplitlHandler.ts new file mode 100644 index 0000000000..215cee8406 --- /dev/null +++ b/Logic/Osm/Actions/RelationSplitlHandler.ts @@ -0,0 +1,20 @@ +/** + * The logic to handle relations after a way within + */ +import OsmChangeAction from "./OsmChangeAction"; +import {Changes} from "../Changes"; +import {ChangeDescription} from "./ChangeDescription"; +import {OsmRelation, OsmWay} from "../OsmObject"; + +export default class RelationSplitlHandler extends OsmChangeAction{ + + constructor(partOf: OsmRelation[], newWayIds: number[], originalNodes: number[]) { + super() + } + + CreateChangeDescriptions(changes: Changes): ChangeDescription[] { + return []; + } + + +} \ No newline at end of file diff --git a/Logic/Osm/Actions/SplitAction.ts b/Logic/Osm/Actions/SplitAction.ts new file mode 100644 index 0000000000..8ee1731e43 --- /dev/null +++ b/Logic/Osm/Actions/SplitAction.ts @@ -0,0 +1,238 @@ +import {OsmRelation, OsmWay} from "../OsmObject"; +import {Changes} from "../Changes"; +import {GeoOperations} from "../../GeoOperations"; +import OsmChangeAction from "./OsmChangeAction"; +import {ChangeDescription} from "./ChangeDescription"; +import RelationSplitlHandler from "./RelationSplitlHandler"; + +interface SplitInfo { + originalIndex?: number, // or negative for new elements + lngLat: [number, number], + doSplit: boolean +} + +export default class SplitAction extends OsmChangeAction { + private readonly roadObject: any; + private readonly osmWay: OsmWay; + private _partOf: OsmRelation[]; + private readonly _splitPoints: any[]; + + constructor(osmWay: OsmWay, wayGeoJson: any, partOf: OsmRelation[], splitPoints: any[]) { + super() + this.osmWay = osmWay; + this.roadObject = wayGeoJson; + this._partOf = partOf; + this._splitPoints = splitPoints; + } + + private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { + const wayParts = [] + let currentPart = [] + for (const splitInfoElement of splitInfo) { + currentPart.push(splitInfoElement) + + if (splitInfoElement.doSplit) { + // We have to do a split! + // We add the current index to the currentParts, flush it and add it again + wayParts.push(currentPart) + currentPart = [splitInfoElement] + } + } + wayParts.push(currentPart) + return wayParts.filter(wp => wp.length > 0) + } + + CreateChangeDescriptions(changes: Changes): ChangeDescription[] { + const splitPoints = this._splitPoints + // We mark the new split points with a new id + console.log(splitPoints) + for (const splitPoint of splitPoints) { + splitPoint.properties["_is_split_point"] = true + } + + + const self = this; + const partOf = this._partOf + const originalElement = this.osmWay + const originalNodes = this.osmWay.nodes; + + // First, calculate splitpoints and remove points close to one another + const splitInfo = self.CalculateSplitCoordinates(splitPoints) + // 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: + for (const element of splitInfo) { + if (element.originalIndex >= 0) { + element.originalIndex = originalElement.nodes[element.originalIndex] + } else { + element.originalIndex = changes.getNewID(); + } + } + + // Next up is creating actual parts from this + 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; + for (const wayPart of wayParts) { + if (longest === undefined) { + longest = wayPart; + continue + } + if (wayPart.length > longest.length) { + longest = wayPart + } + } + + const changeDescription: ChangeDescription[] = [] + // Let's create the new points as needed + for (const element of splitInfo) { + if (element.originalIndex >= 0) { + continue; + } + changeDescription.push({ + type: "node", + id: element.originalIndex, + changes: { + lon: element.lngLat[0], + lat: element.lngLat[1] + } + }) + } + + const newWayIds: number[] = [] + // Lets create OsmWays based on them + for (const wayPart of wayParts) { + + let isOriginal = wayPart === longest + if (isOriginal) { + // We change the actual element! + changeDescription.push({ + type: "way", + id: originalElement.id, + changes: { + locations: wayPart.map(p => p.lngLat), + nodes: wayPart.map(p => p.originalIndex) + } + }) + } else { + let id = changes.getNewID(); + newWayIds.push(id) + + const kv = [] + for (const k in originalElement.tags) { + if (!originalElement.tags.hasOwnProperty(k)) { + continue + } + if (k.startsWith("_") || k === "id") { + continue; + } + kv.push({k: k, v: originalElement.tags[k]}) + } + changeDescription.push({ + type: "way", + id: id, + tags: kv, + changes: { + locations: wayPart.map(p => p.lngLat), + nodes: wayPart.map(p => p.originalIndex) + } + }) + } + + } + + // At last, we still have to check that we aren't part of a relation... + // At least, the order of the ways is identical, so we can keep the same roles + changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).CreateChangeDescriptions(changes)) + + // And we have our objects! + // Time to upload + + return changeDescription + } + + /** + * Calculates the actual points to split + * If another point is closer then ~5m, we reuse that point + */ + private CalculateSplitCoordinates( + splitPoints: any[], + toleranceInM = 5): SplitInfo[] { + + const allPoints = [...splitPoints]; + // We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ... + const originalPoints: [number, number][] = this.roadObject.geometry.coordinates + // We project them onto the line (which should yield pretty much the same point + for (let i = 0; i < originalPoints.length; i++) { + let originalPoint = originalPoints[i]; + let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint) + projected.properties["_is_split_point"] = false + projected.properties["_original_index"] = i + allPoints.push(projected) + } + // 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.properties.location - b.properties.location) + + // When this is done, we check that no now point is too close to an already existing point and no very small segments get created + + /* for (let i = allPoints.length - 1; i > 0; i--) { + + const point = allPoints[i]; + if (point.properties._original_index !== undefined) { + // This point is already in OSM - we have to keep it! + continue; + } + + if (i != allPoints.length - 1) { + const prevPoint = allPoints[i + 1] + const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000 + if (diff <= toleranceInM) { + // To close to the previous point! We delete this point... + allPoints.splice(i, 1) + // ... and mark the previous point as a split point + prevPoint.properties._is_split_point = true + continue; + } + } + + if (i > 0) { + const nextPoint = allPoints[i - 1] + const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000 + if (diff <= toleranceInM) { + // To close to the next point! We delete this point... + allPoints.splice(i, 1) + // ... and mark the next point as a split point + nextPoint.properties._is_split_point = true + // noinspection UnnecessaryContinueJS + continue; + } + } + // We don't have to remove this point... + }*/ + + const splitInfo: SplitInfo[] = [] + let nextId = -1 + + for (const p of allPoints) { + let index = p.properties._original_index + if (index === undefined) { + index = nextId; + nextId--; + } + const splitInfoElement = { + originalIndex: index, + lngLat: p.geometry.coordinates, + doSplit: p.properties._is_split_point + } + splitInfo.push(splitInfoElement) + } + + return splitInfo + } + + +} diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index ff50cc022b..806af3ba74 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -1,81 +1,233 @@ -import {OsmNode, OsmObject} from "./OsmObject"; +import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; import State from "../../State"; -import {Utils} from "../../Utils"; import {UIEventSource} from "../UIEventSource"; import Constants from "../../Models/Constants"; -import FeatureSource from "../FeatureSource/FeatureSource"; -import {TagsFilter} from "../Tags/TagsFilter"; -import {Tag} from "../Tags/Tag"; -import {OsmConnection} from "./OsmConnection"; +import OsmChangeAction from "./Actions/OsmChangeAction"; +import {ChangeDescription} from "./Actions/ChangeDescription"; +import {Utils} from "../../Utils"; import {LocalStorageSource} from "../Web/LocalStorageSource"; /** * Handles all changes made to OSM. * Needs an authenticator via OsmConnection */ -export class Changes implements FeatureSource { +export class Changes { private static _nextId = -1; // Newly assigned ID's are negative public readonly name = "Newly added features" /** - * The newly created points, as a FeatureSource + * All the newly created features as featureSource + all the modified features */ public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); - /** - * All the pending changes - */ - public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) - - /** - * All the pending new objects to upload - */ - private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) + public readonly pendingChanges = LocalStorageSource.GetParsed("pending-changes", []) private readonly isUploading = new UIEventSource(false); + + private readonly previouslyCreated : OsmObject[] = [] - /** - * Adds a change to the pending changes - */ - 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.log("Invalid key"); - return undefined; - } - if (value === undefined || value === null) { - console.log("Invalid value for ", key); - return undefined; - } - - 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()}; + constructor() { + } + private static createChangesetFor(csId: string, + allChanges: { + modifiedObjects: OsmObject[], + newObjects: OsmObject[], + deletedObjects: OsmObject[] + }): string { - addTag(elementId: string, tagsFilter: TagsFilter, - tags?: UIEventSource) { - const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); - const elementTags = eventSource.data; - const changes = tagsFilter.asChange(elementTags).map(Changes.checkChange) - if (changes.length == 0) { - return; + const changedElements = allChanges.modifiedObjects ?? [] + const newElements = allChanges.newObjects ?? [] + const deletedElements = allChanges.deletedObjects ?? [] + + let changes = ``; + if (newElements.length > 0) { + changes += + "\n\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"; } + if (deletedElements.length > 0) { + changes += + "\n\n" + + deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + + "\n" + } + + changes += ""; + return changes; + } + + private static GetNeededIds(changes: ChangeDescription[]) { + return Utils.Dedup(changes.filter(c => c.id >= 0) + .map(c => c.type + "/" + c.id)) + } + + private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { + newObjects: OsmObject[], + modifiedObjects: OsmObject[] + deletedObjects: OsmObject[] + + } { + const objects: Map = new Map() + const states: Map = new Map(); + + for (const o of downloadedOsmObjects) { + objects.set(o.type + "/" + o.id, o) + states.set(o.type + "/" + o.id, "unchanged") + } + + for (const o of this.previouslyCreated) { + objects.set(o.type + "/" + o.id, o) + states.set(o.type + "/" + o.id, "unchanged") + } + + let changed = false; for (const change of changes) { - if (elementTags[change.k] !== change.v) { - elementTags[change.k] = change.v; - console.log("Applied ", change.k, "=", change.v) - // We use 'elementTags.id' here, as we might have retrieved with the id 'node/-1' as new point, but should use the rewritten id - this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v}); + const id = change.type + "/" + change.id + if (!objects.has(id)) { + if(change.id >= 0){ + throw "Did not get an object that should be known: "+id + } + // This is a new object that should be created + states.set(id, "created") + console.log("Creating object for changeDescription", change) + 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; + case "way": + const w = new OsmWay(change.id) + w.nodes = change.changes["nodes"] + osmObj = w + break; + case "relation": + const r = new OsmRelation(change.id) + r.members = change.changes["members"] + osmObj = r + break; + } + if (osmObj === undefined) { + throw "Hmm? This is a bug" + } + objects.set(id, osmObj) + this.previouslyCreated.push(osmObj) + } + + const state = states.get(id) + if (change.doDelete) { + if (state === "created") { + states.set(id, "unchanged") + } else { + states.set(id, "deleted") + } + } + + const obj = objects.get(id) + // Apply tag changes + for (const kv of change.tags ?? []) { + const k = kv.k + let v = kv.v + + if (v === "") { + v = undefined; + } + + const oldV = obj.type[k] + if (oldV === v) { + continue; + } + + obj.tags[k] = v; + changed = true; + + + } + + if (change.changes !== undefined) { + switch (change.type) { + case "node": + // @ts-ignore + const nlat = change.changes.lat; + // @ts-ignore + const nlon = change.changes.lon; + const n = obj + if (n.lat !== nlat || n.lon !== nlon) { + n.lat = nlat; + n.lon = nlon; + changed = true; + } + break; + case "way": + const nnodes = change.changes["nodes"] + const w = obj + if (!Utils.Identical(nnodes, w.nodes)) { + w.nodes = nnodes + changed = true; + } + break; + case "relation": + 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; + } + break; + + } + + } + + if (changed && state === "unchanged") { + states.set(id, "modified") } } - this.pending.ping(); - eventSource.ping(); + + + const result = { + newObjects: [], + modifiedObjects: [], + deletedObjects: [] + } + + objects.forEach((v, id) => { + + const state = states.get(id) + if (state === "created") { + result.newObjects.push(v) + } + if (state === "modified") { + result.modifiedObjects.push(v) + } + if (state === "deleted") { + result.deletedObjects.push(v) + } + + }) + + return result + } + + /** + * Returns a new ID and updates the value for the next ID + */ + public getNewID() { + return Changes._nextId--; } /** @@ -83,194 +235,65 @@ export class Changes implements FeatureSource { * Triggered by the 'PendingChangeUploader'-actor in Actors */ public flushChanges(flushreason: string = undefined) { - if (this.pending.data.length === 0) { + if (this.pendingChanges.data.length === 0) { return; } - if (flushreason !== undefined) { - console.log(flushreason) - } - this.uploadAll(); - } - - /** - * Create a new node element at the given lat/long. - * An internal OsmObject is created to upload later on, a geojson represention is returned. - * Note that the geojson version shares the tags (properties) by pointer, but has _no_ id in properties - */ - public createElement(basicTags: Tag[], lat: number, lon: number) { - console.log("Creating a new element with ", basicTags) - const newId = Changes._nextId; - Changes._nextId--; - - const id = "node/" + newId; - - - const properties = {id: id}; - - const geojson = { - "type": "Feature", - "properties": properties, - "id": id, - "geometry": { - "type": "Point", - "coordinates": [ - lon, - lat - ] - } - } - - // The basictags are COPIED, the id is included in the properties - // The tags are not yet written into the OsmObject, but this is applied onto a - const changes = []; - for (const kv of basicTags) { - if (typeof kv.value !== "string") { - throw "Invalid value: don't use a regex in a preset" - } - properties[kv.key] = kv.value; - changes.push({elementId: id, key: kv.key, value: kv.value}) - } - - console.log("New feature added and pinged") - this.features.data.push({feature: geojson, freshness: new Date()}); - this.features.ping(); - - State.state.allElements.addOrGetElement(geojson).ping(); - - if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) { - properties["_backend"] = State.state.osmConnection.userDetails.data.backend - } - - - this.newObjects.data.push({id: newId, lat: lat, lon: lon}) - this.pending.data.push(...changes) - this.pending.ping(); - this.newObjects.ping(); - return geojson; - } - - private uploadChangesWithLatestVersions( - knownElements: OsmObject[]) { - const knownById = new Map(); - knownElements.forEach(knownElement => { - knownById.set(knownElement.type + "/" + knownElement.id, knownElement) - }) - - const newElements: OsmNode [] = this.newObjects.data.map(spec => { - const newElement = new OsmNode(spec.id); - newElement.lat = spec.lat; - newElement.lon = spec.lon; - return newElement - }) - - - // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements - // We apply the changes on them - for (const change of this.pending.data) { - if (parseInt(change.elementId.split("/")[1]) < 0) { - // This is a new element - we should apply this on one of the new elements - for (const newElement of newElements) { - if (newElement.type + "/" + newElement.id === change.elementId) { - newElement.addTag(change.key, change.value); - } - } - } else { - knownById.get(change.elementId).addTag(change.key, change.value); - } - } - - // Small sanity check for duplicate information - let changedElements = []; - for (const elementId in knownElements) { - const element = knownElements[elementId]; - if (element.changed) { - changedElements.push(element); - } - } - if (changedElements.length == 0 && newElements.length == 0) { - console.log("No changes in any object - clearing"); - this.pending.setData([]) - this.newObjects.setData([]) - return; - } - const self = this; - + if (this.isUploading.data) { + console.log("Is already uploading... Abort") return; } + + this.isUploading.setData(true) - - console.log("Beginning upload..."); + + console.log("Beginning upload... "+flushreason ?? ""); // At last, we build the changeset and upload - State.state.osmConnection.UploadChangeset( - State.state.layoutToUse.data, - State.state.allElements, - function (csId) { - - let modifications = ""; - for (const element of changedElements) { - if (!element.changed) { - continue; - } - modifications += element.ChangesetXML(csId) + "\n"; - } - - - let creations = ""; - for (const newElement of newElements) { - creations += newElement.ChangesetXML(csId); - } - - - let changes = ``; - - if (creations.length > 0) { - changes += - "" + - creations + - ""; - } - - if (modifications.length > 0) { - changes += - "\n" + - modifications + - "\n"; - } - - changes += ""; - - return changes; - }, - () => { - console.log("Upload successfull!") - self.newObjects.setData([]) - self.pending.setData([]); - self.isUploading.setData(false) - }, - () => self.isUploading.setData(false) - ); - }; - - - private uploadAll() { const self = this; + const pending = self.pendingChanges.data; + const neededIds = Changes.GetNeededIds(pending) + console.log("Needed ids", neededIds) + OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => { + console.log("Got the fresh objects!", osmObjects, "pending: ", pending) + const changes: { + newObjects: OsmObject[], + modifiedObjects: OsmObject[] + deletedObjects: OsmObject[] - const pending = this.pending.data; - let neededIds: string[] = []; - for (const change of pending) { - const id = change.elementId; - if (parseFloat(id.split("/")[1]) < 0) { - // New element - we don't have to download this - } else { - neededIds.push(id); + } = self.CreateChangesetObjects(pending, osmObjects) + if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { + console.log("No changes to be made") + self.pendingChanges.setData([]) + self.isUploading.setData(false) + return true; // Unregister the callback } - } - neededIds = Utils.Dedup(neededIds); - OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { - self.uploadChangesWithLatestVersions(knownElements) - }) + + State.state.osmConnection.UploadChangeset( + State.state.layoutToUse.data, + State.state.allElements, + (csId) => Changes.createChangesetFor(csId, changes), + () => { + console.log("Upload successfull!") + self.pendingChanges.setData([]); + self.isUploading.setData(false) + }, + () => { + console.log("Upload failed - trying again later") + return self.isUploading.setData(false); + } // Failed - mark to try again + ) + return true; + + }); + + } + public applyAction(action: OsmChangeAction) { + const changes = action.Perform(this) + console.log("Received changes:", changes) + this.pendingChanges.data.push(...changes); + this.pendingChanges.ping(); + } } \ No newline at end of file diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index 8fba438037..019d0efb29 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -53,6 +53,8 @@ export class ChangesetHandler { element.ping(); } + + } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 92a0823f65..bc81c7b05f 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -249,8 +249,13 @@ export class OsmConnection { }); } + private isChecking = false; private CheckForMessagesContinuously(){ const self =this; + if(this.isChecking){ + return; + } + this.isChecking = true; UIEventSource.Chronic(5 * 60 * 1000).addCallback(_ => { if (self.isLoggedIn .data) { console.log("Checking for messages") diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 09ee7137c7..fcc3da8886 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -23,7 +23,7 @@ export abstract class OsmObject { this.id = id; this.type = type; this.tags = { - id: id + id: `${this.type}/${id}` } } @@ -51,7 +51,10 @@ export abstract class OsmObject { } const splitted = id.split("/"); const type = splitted[0]; - const idN = splitted[1]; + const idN = Number(splitted[1]); + if(idN <0){ + return; + } OsmObject.objectCache.set(id, src); const newContinuation = (element: OsmObject) => { @@ -68,6 +71,8 @@ export abstract class OsmObject { case("relation"): new OsmRelation(idN).Download(newContinuation); break; + default: + throw "Invalid object type:" + type + id; } return src; @@ -103,7 +108,7 @@ export abstract class OsmObject { if (OsmObject.referencingRelationsCache.has(id)) { return OsmObject.referencingRelationsCache.get(id); } - const relsSrc = new UIEventSource([]) + const relsSrc = new UIEventSource(undefined) OsmObject.referencingRelationsCache.set(id, relsSrc); Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`) .then(data => { @@ -123,7 +128,7 @@ export abstract class OsmObject { } const splitted = id.split("/"); const type = splitted[0]; - const idN = splitted[1]; + const idN = Number(splitted[1]); const src = new UIEventSource([]); OsmObject.historyCache.set(id, src); Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { @@ -312,20 +317,6 @@ export abstract class OsmObject { return this; } - public addTag(k: string, v: string): void { - if (k in this.tags) { - const oldV = this.tags[k]; - if (oldV == v) { - return; - } - console.log("Overwriting ", oldV, " with ", v, " for key ", k) - } - this.tags[k] = v; - if (v === undefined || v === "") { - delete this.tags[k]; - } - this.changed = true; - } abstract ChangesetXML(changesetId: string): string; @@ -360,7 +351,7 @@ export class OsmNode extends OsmObject { lat: number; lon: number; - constructor(id) { + constructor(id: number) { super("node", id); } @@ -368,9 +359,9 @@ export class OsmNode extends OsmObject { ChangesetXML(changesetId: string): string { let tags = this.TagsXML(); - return ' \n' + + return ' \n' + tags + - ' \n'; + ' \n'; } SaveExtraData(element) { @@ -413,9 +404,8 @@ export class OsmWay extends OsmObject { lat: number; lon: number; - constructor(id) { + constructor(id: number) { super("way", id); - } centerpoint(): [number, number] { @@ -432,7 +422,7 @@ export class OsmWay extends OsmObject { return ' \n' + nds + tags + - ' \n'; + ' \n'; } SaveExtraData(element, allNodes: OsmNode[]) { @@ -458,7 +448,7 @@ export class OsmWay extends OsmObject { this.nodes = element.nodes; } - asGeoJson() { + public asGeoJson() { return { "type": "Feature", "properties": this.tags, @@ -480,11 +470,14 @@ export class OsmWay extends OsmObject { export class OsmRelation extends OsmObject { - members; + public members: { + type: "node" | "way" | "relation", + ref: number, + role: string + }[]; - constructor(id) { + constructor(id: number) { super("relation", id); - } centerpoint(): [number, number] { diff --git a/Logic/Osm/aspectedRouting.ts b/Logic/Osm/aspectedRouting.ts new file mode 100644 index 0000000000..33a171719e --- /dev/null +++ b/Logic/Osm/aspectedRouting.ts @@ -0,0 +1,194 @@ +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.units = program.unit + this.program = JSON.parse(JSON.stringify(program)) + delete this.program.name + delete this.program.description + delete this.program.unit + } + + public evaluate(properties){ + return AspectedRouting.interpret(this.program, properties) + } + /** + * Interprets the given Aspected-routing program for the given properties + */ + public static interpret(program: any, properties: any) { + if (typeof program !== "object") { + return program; + } + + 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 + } + } + ) + + 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') { + return AspectedRouting.defaultV(functionArguments, otherValues, properties) + } else { + console.error(`Error: Program ${functionName} is not implemented yet. ${JSON.stringify(program)}`); + } + } + + /** + * Given a 'program' without function invocation, interprets it as a dictionary + * + * E.g., given the program + * + * { + * highway: { + * residential: 30, + * living_street: 20 + * }, + * surface: { + * sett : 0.9 + * } + * + * } + * + * in combination with the tags {highway: residential}, + * + * the result should be [30, undefined]; + * + * For the tags {highway: residential, surface: sett} we should get [30, 0.9] + * + * + * @param program + * @param tags + * @return {(undefined|*)[]} + */ + private static interpretAsDictionary(program, tags) { + // @ts-ignore + return Object.entries(tags).map(tag => { + const [key, value] = tag; + const propertyValue = program[key] + if (propertyValue === undefined) { + return undefined + } + if (typeof propertyValue !== "object") { + return propertyValue + } + // @ts-ignore + return propertyValue[value] + }); + } + + private static defaultV(subProgram, otherArgs, tags) { + // @ts-ignore + const normalProgram = Object.entries(otherArgs)[0][1] + const value = AspectedRouting.interpret(normalProgram, tags) + if (value !== undefined) { + return value; + } + return AspectedRouting.interpret(subProgram, tags) + } + + /** + * Multiplies the default score with the proper values + * @param tags {object} the active tags to check against + * @param subprograms which should generate a list of values + * @returns score after multiplication + */ + private static multiplyScore(tags, subprograms) { + let number = 1 + + let subResults: any[] + if (subprograms.length !== undefined) { + 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); + } + + private static getFirstMatchScore(tags, order: any) { + /*Order should be a list of arguments after evaluation*/ + order = AspectedRouting.interpret(order, tags) + for (let key of order) { + // @ts-ignore + for (let entry of Object.entries(JSON.parse(tags))) { + 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; + } + } + } + } + + // Not a single match found... + return undefined + } + + 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); + } + + 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); + } + + private static concatMap(list, f): any[] { + const result = [] + list = list.map(f) + for (const elem of list) { + if (elem.length !== undefined) { + // This is a list + result.push(...elem) + } else { + result.push(elem) + } + } + return result; + } + +} \ No newline at end of file diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index a704e22592..2337c97f3f 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -83,7 +83,7 @@ export default class SimpleMetaTagger { }, (feature => { - const units = State.state.layoutToUse.data.units ?? []; + const units = State.state?.layoutToUse?.data?.units ?? []; let rewritten = false; for (const key in feature.properties) { if (!feature.properties.hasOwnProperty(key)) { @@ -100,10 +100,10 @@ export default class SimpleMetaTagger { break; } console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) - if(canonical === undefined && !unit.eraseInvalid) { + if (canonical === undefined && !unit.eraseInvalid) { break; } - + feature.properties[key] = canonical; rewritten = true; break; diff --git a/Logic/Tags/ComparingTag.ts b/Logic/Tags/ComparingTag.ts new file mode 100644 index 0000000000..7e6951988c --- /dev/null +++ b/Logic/Tags/ComparingTag.ts @@ -0,0 +1,42 @@ +import {TagsFilter} from "./TagsFilter"; + +export default class ComparingTag implements TagsFilter { + 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; + } + + asChange(properties: any): { k: string; v: string }[] { + throw "A comparable tag can not be used to be uploaded to OSM" + } + + asHumanString(linkToWiki: boolean, shorten: boolean, properties: any) { + return this._key+this._representation + } + + asOverpass(): string[] { + throw "A comparable tag can not be used as overpass filter" + } + + isEquivalent(other: TagsFilter): boolean { + return other === this; + } + + isUsableAsAnswer(): boolean { + return false; + } + + matchesProperties(properties: any): boolean { + return this._predicate(properties[this._key]); + } + + usedKeys(): string[] { + return [this._key]; + } + +} \ No newline at end of file diff --git a/Logic/Tags/Or.ts b/Logic/Tags/Or.ts index 12c456aa89..254e193e9b 100644 --- a/Logic/Tags/Or.ts +++ b/Logic/Tags/Or.ts @@ -8,7 +8,7 @@ export class Or extends TagsFilter { super(); this.or = or; } - + matchesProperties(properties: any): boolean { for (const tagsFilter of this.or) { if (tagsFilter.matchesProperties(properties)) { @@ -23,9 +23,7 @@ export class Or extends TagsFilter { const choices = []; for (const tagsFilter of this.or) { const subChoices = tagsFilter.asOverpass(); - for (const subChoice of subChoices) { - choices.push(subChoice) - } + choices.push(...subChoices) } return choices; } diff --git a/Logic/Tags/RegexTag.ts b/Logic/Tags/RegexTag.ts index cf12798f7a..68ca88d089 100644 --- a/Logic/Tags/RegexTag.ts +++ b/Logic/Tags/RegexTag.ts @@ -34,9 +34,9 @@ export class RegexTag extends TagsFilter { asOverpass(): string[] { if (typeof this.key === "string") { - return [`['${this.key}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; + return [`["${this.key}"${this.invert ? "!" : ""}~"${RegexTag.source(this.value)}"]`]; } - return [`[~'${this.key.source}'${this.invert ? "!" : ""}~'${RegexTag.source(this.value)}']`]; + return [`[~"${this.key.source}"${this.invert ? "!" : ""}~"${RegexTag.source(this.value)}"]`]; } isUsableAsAnswer(): boolean { diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index c16833ae58..6246b67902 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -1,7 +1,6 @@ import {Utils} from "../../Utils"; import {RegexTag} from "./RegexTag"; import {TagsFilter} from "./TagsFilter"; -import {TagUtils} from "./TagUtils"; export class Tag extends TagsFilter { public key: string @@ -25,12 +24,19 @@ export class Tag extends TagsFilter { matchesProperties(properties: any): boolean { for (const propertiesKey in properties) { + if(!properties.hasOwnProperty(propertiesKey)){ + continue + } if (this.key === propertiesKey) { const value = properties[propertiesKey]; + if(value === undefined){ + continue + } return value === this.value; } } // The tag was not found + if (this.value === "") { // and it shouldn't be found! return true; @@ -46,11 +52,6 @@ export class Tag extends TagsFilter { } return [`["${this.key}"="${this.value}"]`]; } - - substituteValues(tags: any) { - return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); - } - asHumanString(linkToWiki?: boolean, shorten?: boolean) { let v = this.value; if (shorten) { diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index d18cec86f8..2dfc2d655e 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -2,21 +2,27 @@ import {Utils} from "../Utils"; export class UIEventSource { + private static allSources: UIEventSource[] = UIEventSource.PrepPerf(); public data: T; + public trace: boolean; private readonly tag: string; - private _callbacks = []; - - private static allSources : UIEventSource[] = UIEventSource.PrepPerf(); - - static PrepPerf() : UIEventSource[]{ - if(Utils.runningFromConsole){ + private _callbacks: ((t: T) => (boolean | void | any)) [] = []; + + constructor(data: T, tag: string = "") { + this.tag = tag; + this.data = data; + UIEventSource.allSources.push(this); + } + + static PrepPerf(): UIEventSource[] { + if (Utils.runningFromConsole) { return []; } // @ts-ignore window.mapcomplete_performance = () => { console.log(UIEventSource.allSources.length, "uieventsources created"); const copy = [...UIEventSource.allSources]; - copy.sort((a,b) => b._callbacks.length - a._callbacks.length); + copy.sort((a, b) => b._callbacks.length - a._callbacks.length); console.log("Topten is:") for (let i = 0; i < 10; i++) { console.log(copy[i].tag, copy[i]); @@ -26,12 +32,6 @@ export class UIEventSource { return []; } - constructor(data: T, tag: string = "") { - this.tag = tag; - this.data = data; - UIEventSource.allSources.push(this); - } - public static flatten(source: UIEventSource>, possibleSources: UIEventSource[]): UIEventSource { const sink = new UIEventSource(source.data?.data); @@ -63,11 +63,20 @@ export class UIEventSource { } - public addCallback(callback: ((latestData: T) => void)): UIEventSource { + /** + * Adds a callback + * + * 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))): UIEventSource { 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." } + if (this.trace) { + console.trace("Added a callback") + } this._callbacks.push(callback); return this; } @@ -87,8 +96,21 @@ export class UIEventSource { } public ping(): void { + let toDelete = undefined for (const callback of this._callbacks) { - callback(this.data); + if (callback(this.data) === true) { + // This callback wants to be deleted + if (toDelete === undefined) { + toDelete = [callback] + } else { + toDelete.push(callback) + } + } + } + if (toDelete !== undefined) { + for (const toDeleteElement of toDelete) { + this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1) + } } } @@ -101,12 +123,12 @@ export class UIEventSource { */ public map(f: ((t: T) => J), extraSources: UIEventSource[] = [], - g: ((j:J, t:T) => T) = undefined): UIEventSource { + g: ((j: J, t: T) => T) = undefined): UIEventSource { const self = this; const newSource = new UIEventSource( f(this.data), - "map("+this.tag+")" + "map(" + this.tag + ")" ); const update = function () { @@ -159,11 +181,28 @@ export class UIEventSource { return newSource; } - addCallbackAndRunD(callback: (data :T ) => void) { + addCallbackAndRunD(callback: (data: T) => void) { this.addCallbackAndRun(data => { - if(data !== undefined && data !== null){ - callback(data) + if (data !== undefined && data !== null) { + return callback(data) } }) } +} + +export class UIEventSourceTools { + + private static readonly _download_cache = new Map>() + + public static downloadJsonCached(url: string): UIEventSource{ + const cached = UIEventSourceTools._download_cache.get(url) + if(cached !== undefined){ + return cached; + } + const src = new UIEventSource(undefined) + UIEventSourceTools._download_cache.set(url, src) + Utils.downloadJson(url).then(r => src.setData(r)) + return src; + } + } \ No newline at end of file diff --git a/Models/Constants.ts b/Models/Constants.ts index 33a52c40f6..bc46f859ae 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.4"; + public static vNumber = "0.9.0-rc0"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts new file mode 100644 index 0000000000..9bd72ae8da --- /dev/null +++ b/Models/FilteredLayer.ts @@ -0,0 +1,9 @@ +import {UIEventSource} from "../Logic/UIEventSource"; +import {TagsFilter} from "../Logic/Tags/TagsFilter"; +import LayerConfig from "../Customizations/JSON/LayerConfig"; + +export default interface FilteredLayer { + readonly isDisplayed: UIEventSource; + readonly appliedFilters: UIEventSource; + readonly layerDef: LayerConfig; +} \ No newline at end of file diff --git a/State.ts b/State.ts index 4186572668..fe2e2fdf7a 100644 --- a/State.ts +++ b/State.ts @@ -14,24 +14,23 @@ import Loc from "./Models/Loc"; import Constants from "./Models/Constants"; import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; -import LayerConfig from "./Customizations/JSON/LayerConfig"; import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import {Relation} from "./Logic/Osm/ExtractRelations"; import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; +import FilteredLayer from "./Models/FilteredLayer"; +import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; /** * Contains the global state: a bunch of UI-event sources */ export default class State { - // The singleton of the global state public static state: State; - - public readonly layoutToUse = new UIEventSource(undefined); + public readonly layoutToUse = new UIEventSource(undefined, "layoutToUse"); /** The mapping from id -> UIEventSource @@ -44,7 +43,7 @@ export default class State { /** The leaflet instance of the big basemap */ - public leafletMap = new UIEventSource(undefined); + public leafletMap = new UIEventSource(undefined, "leafletmap"); /** * Background layer id */ @@ -62,26 +61,21 @@ export default class State { public osmApiFeatureSource: OsmApiFeatureSource; - - public filteredLayers: UIEventSource<{ - readonly isDisplayed: UIEventSource, - readonly layerDef: LayerConfig; - }[]> = new UIEventSource<{ - readonly isDisplayed: UIEventSource, - readonly layerDef: LayerConfig; - }[]>([]) - + public filteredLayers: UIEventSource = new UIEventSource([],"filteredLayers"); /** The latest element that was selected */ - public readonly selectedElement = new UIEventSource(undefined, "Selected element") + public readonly selectedElement = new UIEventSource( + undefined, + "Selected element" + ); /** * Keeps track of relations: which way is part of which other way? * Set by the overpass-updater; used in the metatagging */ - public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships") + public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships"); public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; @@ -96,40 +90,71 @@ export default class State { 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 featurePipeline: FeaturePipeline; + public featurePipeline: FeaturePipeline; /** * The map location: currently centered lat, lon and zoom */ - public readonly locationControl = new UIEventSource(undefined); + public readonly locationControl = new UIEventSource(undefined, "locationControl"); public backgroundLayer; public readonly backgroundLayerId: UIEventSource; /* Last location where a click was registered */ - public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined) + public readonly LastClickLocation: UIEventSource<{ + lat: number; + lon: number; + }> = new UIEventSource<{ lat: number; lon: number }>(undefined); /** * The location as delivered by the GPS */ public currentGPSLocation: UIEventSource<{ - latlng: { lat: number, lng: number }, - accuracy: number - }> = new UIEventSource<{ latlng: { lat: number, lng: number }, accuracy: number }>(undefined); + latlng: { lat: number; lng: number }; + accuracy: number; + }> = new UIEventSource<{ + latlng: { lat: number; lng: number }; + accuracy: number; + }>(undefined); public layoutDefinition: string; public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; public layerControlIsOpened: UIEventSource = - QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown") - .map((str) => str !== "false", [], b => "" + b) + QueryParameters.GetQueryParameter( + "layer-control-toggle", + "false", + "Whether or not the layer control is shown" + ).map( + (str) => str !== "false", + [], + (b) => "" + b + ); - public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( - str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n + public filterIsOpened: UIEventSource = + QueryParameters.GetQueryParameter( + "filter-toggle", + "false", + "Whether or not the filter view is shown" + ).map( + (str) => str !== "false", + [], + (b) => "" + b + ); + + public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter( + "tab", + "0", + `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` + ).map( + (str) => (isNaN(Number(str)) ? 0 : Number(str)), + [], + (n) => "" + n ); constructor(layoutToUse: LayoutConfig) { @@ -140,111 +165,193 @@ export default class State { // -- Location control initialization { const zoom = State.asFloat( - QueryParameters.GetQueryParameter("z", "" + (layoutToUse?.startZoom ?? 1), "The initial/current zoom level") - .syncWith(LocalStorageSource.Get("zoom"))); - const lat = State.asFloat(QueryParameters.GetQueryParameter("lat", "" + (layoutToUse?.startLat ?? 0), "The initial/current latitude") - .syncWith(LocalStorageSource.Get("lat"))); - const lon = State.asFloat(QueryParameters.GetQueryParameter("lon", "" + (layoutToUse?.startLon ?? 0), "The initial/current longitude of the app") - .syncWith(LocalStorageSource.Get("lon"))); + QueryParameters.GetQueryParameter( + "z", + "" + (layoutToUse?.startZoom ?? 1), + "The initial/current zoom level" + ).syncWith(LocalStorageSource.Get("zoom")) + ); + const lat = State.asFloat( + QueryParameters.GetQueryParameter( + "lat", + "" + (layoutToUse?.startLat ?? 0), + "The initial/current latitude" + ).syncWith(LocalStorageSource.Get("lat")) + ); + const lon = State.asFloat( + QueryParameters.GetQueryParameter( + "lon", + "" + (layoutToUse?.startLon ?? 0), + "The initial/current longitude of the app" + ).syncWith(LocalStorageSource.Get("lon")) + ); - - this.locationControl = new UIEventSource({ + this.locationControl.setData({ zoom: Utils.asFloat(zoom.data), lat: Utils.asFloat(lat.data), lon: Utils.asFloat(lon.data), - }).addCallback((latlonz) => { + }) + this.locationControl.addCallback((latlonz) => { + // Sync th location controls zoom.setData(latlonz.zoom); lat.setData(latlonz.lat); lon.setData(latlonz.lon); }); - this.layoutToUse.addCallback(layoutToUse => { + this.layoutToUse.addCallback((layoutToUse) => { const lcd = self.locationControl.data; lcd.zoom = lcd.zoom ?? layoutToUse?.startZoom; lcd.lat = lcd.lat ?? layoutToUse?.startLat; lcd.lon = lcd.lon ?? layoutToUse?.startLon; self.locationControl.ping(); }); - } // Helper function to initialize feature switches - function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource { - const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation); + function featSw( + key: string, + deflt: (layout: LayoutConfig) => boolean, + documentation: string + ): UIEventSource { + const queryParameterSource = QueryParameters.GetQueryParameter( + key, + undefined, + documentation + ); // I'm so sorry about someone trying to decipher this // It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened return UIEventSource.flatten( self.layoutToUse.map((layout) => { const defaultValue = deflt(layout); - const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation) - return queryParam.map((str) => str === undefined ? defaultValue : (str !== "false")); - }), [queryParameterSource]); + const queryParam = QueryParameters.GetQueryParameter( + key, + "" + defaultValue, + documentation + ); + return queryParam.map((str) => + str === undefined ? defaultValue : str !== "false" + ); + }), + [queryParameterSource] + ); } // Feature switch initialization - not as a function as the UIEventSources are readonly { + 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.featureSwitchLayers = featSw( + "fs-layers", + (layoutToUse) => layoutToUse?.enableLayers ?? true, + "Disables/Enables the layer control" + ); + this.featureSwitchFilter = featSw( + "fs-filter", + (layoutToUse) => layoutToUse?.enableLayers ?? true, + "Disables/Enables the filter" + ); + 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.featureSwitchIframe = featSw( + "fs-iframe", + () => false, + "Disables/Enables the iframe-popup" + ); + 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.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.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true, - "Disables/Enables the layer control"); - 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.featureSwitchUserbadge.addCallbackAndRun(userbadge => { - if (!userbadge) { - this.featureSwitchAddNew.setData(false) - } - }) + this.featureSwitchIsTesting = QueryParameters.GetQueryParameter( + "test", + "false", + "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org" + ).map( + (str) => str === "true", + [], + (b) => "" + b + ); - this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, - "Disables/enables the help menu or welcome message"); - this.featureSwitchIframe = featSw("fs-iframe", () => false, - "Disables/Enables the iframe-popup"); - 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, - "If set, enables the 'download'-button to download everything as geojson") - - this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false", - "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org") - .map(str => str === "true", [], b => "" + b); + this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter( + "debug", + "false", + "If true, shows some extra debugging help such as all the available tags on every object" + ).map( + (str) => str === "true", + [], + (b) => "" + b + ); this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false", "If true, 'dryrun' mode is activated and a fake user account is loaded") .map(str => str === "true", [], b => "" + b); - this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug", "false", - "If true, shows some extra debugging help such as all the available tags on every object") - .map(str => str === "true", [], b => "" + b) - this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend", "osm", - "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'") + this.featureSwitchApiURL = QueryParameters.GetQueryParameter( + "backend", + "osm", + "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'" + ); + + + this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { + if (!userbadge) { + this.featureSwitchAddNew.setData(false) + } + }) } { // Some other feature switches - const customCssQP = QueryParameters.GetQueryParameter("custom-css", "", "If specified, the custom css from the given link will be loaded additionaly"); + const customCssQP = QueryParameters.GetQueryParameter( + "custom-css", + "", + "If specified, the custom css from the given link will be loaded additionaly" + ); if (customCssQP.data !== undefined && customCssQP.data !== "") { Utils.LoadCustomCss(customCssQP.data); } - - this.backgroundLayerId = QueryParameters.GetQueryParameter("background", + this.backgroundLayerId = QueryParameters.GetQueryParameter( + "background", layoutToUse?.defaultBackgroundId ?? "osm", - "The id of the background layer to start with") - + "The id of the background layer to start with" + ); } - if (Utils.runningFromConsole) { return; } @@ -252,17 +359,22 @@ export default class State { this.osmConnection = new OsmConnection( this.featureSwitchIsTesting.data, this.featureSwitchFakeUser.data, - QueryParameters.GetQueryParameter("oauth_token", undefined, - "Used to complete the login"), + QueryParameters.GetQueryParameter( + "oauth_token", + undefined, + "Used to complete the login" + ), layoutToUse?.id, true, // @ts-ignore this.featureSwitchApiURL.data ); - this.allElements = new ElementStorage(); this.changes = new Changes(); + + new ChangeToElementsActor(this.changes, this.allElements) + this.osmApiFeatureSource = new OsmApiFeatureSource() new PendingChangesUploader(this.changes, this.selectedElement); @@ -271,47 +383,57 @@ export default class State { this.osmConnection.GetLongPreference("identity", "mangrove") ); - - this.installedThemes = new InstalledThemes(this.osmConnection).installedThemes; + this.installedThemes = new InstalledThemes( + this.osmConnection + ).installedThemes; // Important: the favourite layers are initialized _after_ the installed themes, as these might contain an installedTheme this.favouriteLayers = LocalStorageSource.Get("favouriteLayers") .syncWith(this.osmConnection.GetLongPreference("favouriteLayers")) .map( - str => Utils.Dedup(str?.split(";")) ?? [], - [], layers => Utils.Dedup(layers)?.join(";") + (str) => Utils.Dedup(str?.split(";")) ?? [], + [], + (layers) => Utils.Dedup(layers)?.join(";") ); Locale.language.syncWith(this.osmConnection.GetPreference("language")); - - Locale.language.addCallback((currentLanguage) => { - const layoutToUse = self.layoutToUse.data; - if (layoutToUse === undefined) { - return; - } - if (this.layoutToUse.data.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]); - } - }).ping() + Locale.language + .addCallback((currentLanguage) => { + const layoutToUse = self.layoutToUse.data; + if (layoutToUse === undefined) { + return; + } + if (this.layoutToUse.data.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]); + } + }) + .ping(); new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements); - } private static asFloat(source: UIEventSource): UIEventSource { - return source.map(str => { - let parsed = parseFloat(str); - return isNaN(parsed) ? undefined : parsed; - }, [], fl => { - if (fl === undefined || isNaN(fl)) { - return undefined; + return source.map( + (str) => { + let parsed = parseFloat(str); + return isNaN(parsed) ? undefined : parsed; + }, + [], + (fl) => { + if (fl === undefined || isNaN(fl)) { + return undefined; + } + return ("" + fl).substr(0, 8); } - return ("" + fl).substr(0, 8); - }) + ); } - - } diff --git a/Svg.ts b/Svg.ts deleted file mode 100644 index 88e9216263..0000000000 --- a/Svg.ts +++ /dev/null @@ -1,352 +0,0 @@ -import Img from "./UI/Base/Img"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; - -export default class Svg { - - - public static SocialImageForeground = " image/svg+xml 010110010011010110010011  010110010011010110010011  " - public static SocialImageForeground_img = Img.AsImageElement(Svg.SocialImageForeground) - public static SocialImageForeground_svg() { return new Img(Svg.SocialImageForeground, true);} - public static SocialImageForeground_ui() { return new FixedUiElement(Svg.SocialImageForeground_img);} - - public static add = " image/svg+xml " - public static add_img = Img.AsImageElement(Svg.add) - public static add_svg() { return new Img(Svg.add, true);} - public static add_ui() { return new FixedUiElement(Svg.add_img);} - - public static addSmall = " image/svg+xml " - public static addSmall_img = Img.AsImageElement(Svg.addSmall) - public static addSmall_svg() { return new Img(Svg.addSmall, true);} - public static addSmall_ui() { return new FixedUiElement(Svg.addSmall_img);} - - public static ampersand = " image/svg+xml " - public static ampersand_img = Img.AsImageElement(Svg.ampersand) - public static ampersand_svg() { return new Img(Svg.ampersand, true);} - public static ampersand_ui() { return new FixedUiElement(Svg.ampersand_img);} - - public static arrow_left_smooth = " image/svg+xml " - public static arrow_left_smooth_img = Img.AsImageElement(Svg.arrow_left_smooth) - public static arrow_left_smooth_svg() { return new Img(Svg.arrow_left_smooth, true);} - public static arrow_left_smooth_ui() { return new FixedUiElement(Svg.arrow_left_smooth_img);} - - public static arrow_right_smooth = " image/svg+xml " - public static arrow_right_smooth_img = Img.AsImageElement(Svg.arrow_right_smooth) - public static arrow_right_smooth_svg() { return new Img(Svg.arrow_right_smooth, true);} - public static arrow_right_smooth_ui() { return new FixedUiElement(Svg.arrow_right_smooth_img);} - - public static back = " image/svg+xml " - public static back_img = Img.AsImageElement(Svg.back) - public static back_svg() { return new Img(Svg.back, true);} - public static back_ui() { return new FixedUiElement(Svg.back_img);} - - public static bug = " " - public static bug_img = Img.AsImageElement(Svg.bug) - public static bug_svg() { return new Img(Svg.bug, true);} - public static bug_ui() { return new FixedUiElement(Svg.bug_img);} - - public static camera_plus = " image/svg+xml " - public static camera_plus_img = Img.AsImageElement(Svg.camera_plus) - public static camera_plus_svg() { return new Img(Svg.camera_plus, true);} - public static camera_plus_ui() { return new FixedUiElement(Svg.camera_plus_img);} - - public static checkmark = "" - public static checkmark_img = Img.AsImageElement(Svg.checkmark) - public static checkmark_svg() { return new Img(Svg.checkmark, true);} - public static checkmark_ui() { return new FixedUiElement(Svg.checkmark_img);} - - public static circle = " " - public static circle_img = Img.AsImageElement(Svg.circle) - public static circle_svg() { return new Img(Svg.circle, true);} - public static circle_ui() { return new FixedUiElement(Svg.circle_img);} - - public static clock = " image/svg+xml " - public static clock_img = Img.AsImageElement(Svg.clock) - public static clock_svg() { return new Img(Svg.clock, true);} - public static clock_ui() { return new FixedUiElement(Svg.clock_img);} - - public static close = " image/svg+xml " - public static close_img = Img.AsImageElement(Svg.close) - public static close_svg() { return new Img(Svg.close, true);} - public static close_ui() { return new FixedUiElement(Svg.close_img);} - - public static compass = " image/svg+xml N S E W NW SW NE SE " - public static compass_img = Img.AsImageElement(Svg.compass) - public static compass_svg() { return new Img(Svg.compass, true);} - public static compass_ui() { return new FixedUiElement(Svg.compass_img);} - - public static cross_bottom_right = " image/svg+xml " - public static cross_bottom_right_img = Img.AsImageElement(Svg.cross_bottom_right) - public static cross_bottom_right_svg() { return new Img(Svg.cross_bottom_right, true);} - public static cross_bottom_right_ui() { return new FixedUiElement(Svg.cross_bottom_right_img);} - - public static crosshair_blue_center = " image/svg+xml " - public static crosshair_blue_center_img = Img.AsImageElement(Svg.crosshair_blue_center) - public static crosshair_blue_center_svg() { return new Img(Svg.crosshair_blue_center, true);} - public static crosshair_blue_center_ui() { return new FixedUiElement(Svg.crosshair_blue_center_img);} - - public static crosshair_blue = " image/svg+xml " - public static crosshair_blue_img = Img.AsImageElement(Svg.crosshair_blue) - public static crosshair_blue_svg() { return new Img(Svg.crosshair_blue, true);} - public static crosshair_blue_ui() { return new FixedUiElement(Svg.crosshair_blue_img);} - - public static crosshair_empty = " image/svg+xml " - public static crosshair_empty_img = Img.AsImageElement(Svg.crosshair_empty) - public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);} - public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);} - - public static crosshair_locked = " image/svg+xml " - public static crosshair_locked_img = Img.AsImageElement(Svg.crosshair_locked) - public static crosshair_locked_svg() { return new Img(Svg.crosshair_locked, true);} - public static crosshair_locked_ui() { return new FixedUiElement(Svg.crosshair_locked_img);} - - public static crosshair = " image/svg+xml " - public static crosshair_img = Img.AsImageElement(Svg.crosshair) - public static crosshair_svg() { return new Img(Svg.crosshair, true);} - public static crosshair_ui() { return new FixedUiElement(Svg.crosshair_img);} - - public static delete_icon = " image/svg+xml " - public static delete_icon_img = Img.AsImageElement(Svg.delete_icon) - public static delete_icon_svg() { return new Img(Svg.delete_icon, true);} - public static delete_icon_ui() { return new FixedUiElement(Svg.delete_icon_img);} - - public static direction = " image/svg+xml " - public static direction_img = Img.AsImageElement(Svg.direction) - public static direction_svg() { return new Img(Svg.direction, true);} - public static direction_ui() { return new FixedUiElement(Svg.direction_img);} - - public static direction_gradient = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient) - public static direction_gradient_svg() { return new Img(Svg.direction_gradient, true);} - public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);} - - public static direction_masked = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static direction_masked_img = Img.AsImageElement(Svg.direction_masked) - public static direction_masked_svg() { return new Img(Svg.direction_masked, true);} - public static direction_masked_ui() { return new FixedUiElement(Svg.direction_masked_img);} - - public static direction_outline = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static direction_outline_img = Img.AsImageElement(Svg.direction_outline) - public static direction_outline_svg() { return new Img(Svg.direction_outline, true);} - public static direction_outline_ui() { return new FixedUiElement(Svg.direction_outline_img);} - - public static direction_stroke = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static direction_stroke_img = Img.AsImageElement(Svg.direction_stroke) - public static direction_stroke_svg() { return new Img(Svg.direction_stroke, true);} - public static direction_stroke_ui() { return new FixedUiElement(Svg.direction_stroke_img);} - - public static down = " image/svg+xml " - public static down_img = Img.AsImageElement(Svg.down) - public static down_svg() { return new Img(Svg.down, true);} - public static down_ui() { return new FixedUiElement(Svg.down_img);} - - public static envelope = " image/svg+xml " - public static envelope_img = Img.AsImageElement(Svg.envelope) - public static envelope_svg() { return new Img(Svg.envelope, true);} - public static envelope_ui() { return new FixedUiElement(Svg.envelope_img);} - - public static floppy = " " - public static floppy_img = Img.AsImageElement(Svg.floppy) - public static floppy_svg() { return new Img(Svg.floppy, true);} - public static floppy_ui() { return new FixedUiElement(Svg.floppy_img);} - - public static gear = " " - public static gear_img = Img.AsImageElement(Svg.gear) - public static gear_svg() { return new Img(Svg.gear, true);} - public static gear_ui() { return new FixedUiElement(Svg.gear_img);} - - public static help = " " - public static help_img = Img.AsImageElement(Svg.help) - public static help_svg() { return new Img(Svg.help, true);} - public static help_ui() { return new FixedUiElement(Svg.help_img);} - - public static home = " " - public static home_img = Img.AsImageElement(Svg.home) - public static home_svg() { return new Img(Svg.home, true);} - public static home_ui() { return new FixedUiElement(Svg.home_img);} - - public static home_white_bg = " image/svg+xml " - public static home_white_bg_img = Img.AsImageElement(Svg.home_white_bg) - public static home_white_bg_svg() { return new Img(Svg.home_white_bg, true);} - public static home_white_bg_ui() { return new FixedUiElement(Svg.home_white_bg_img);} - - public static josm_logo = " JOSM Logotype 2019 image/svg+xml JOSM Logotype 2019 2019-08-05 Diamond00744 Public Domain " - public static josm_logo_img = Img.AsImageElement(Svg.josm_logo) - public static josm_logo_svg() { return new Img(Svg.josm_logo, true);} - public static josm_logo_ui() { return new FixedUiElement(Svg.josm_logo_img);} - - public static layers = " image/svg+xml " - public static layers_img = Img.AsImageElement(Svg.layers) - public static layers_svg() { return new Img(Svg.layers, true);} - public static layers_ui() { return new FixedUiElement(Svg.layers_img);} - - public static layersAdd = " image/svg+xml " - public static layersAdd_img = Img.AsImageElement(Svg.layersAdd) - public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} - public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - - public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) - public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} - public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} - - public static logo = " image/svg+xml " - public static logo_img = Img.AsImageElement(Svg.logo) - public static logo_svg() { return new Img(Svg.logo, true);} - public static logo_ui() { return new FixedUiElement(Svg.logo_img);} - - public static logout = " image/svg+xml " - public static logout_img = Img.AsImageElement(Svg.logout) - public static logout_svg() { return new Img(Svg.logout, true);} - public static logout_ui() { return new FixedUiElement(Svg.logout_img);} - - public static mapcomplete_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011  010110010011010110010011  " - public static mapcomplete_logo_img = Img.AsImageElement(Svg.mapcomplete_logo) - public static mapcomplete_logo_svg() { return new Img(Svg.mapcomplete_logo, true);} - public static mapcomplete_logo_ui() { return new FixedUiElement(Svg.mapcomplete_logo_img);} - - public static mapillary = "" - public static mapillary_img = Img.AsImageElement(Svg.mapillary) - public static mapillary_svg() { return new Img(Svg.mapillary, true);} - public static mapillary_ui() { return new FixedUiElement(Svg.mapillary_img);} - - public static mapillary_black = " image/svg+xml " - public static mapillary_black_img = Img.AsImageElement(Svg.mapillary_black) - public static mapillary_black_svg() { return new Img(Svg.mapillary_black, true);} - public static mapillary_black_ui() { return new FixedUiElement(Svg.mapillary_black_img);} - - public static min = " image/svg+xml " - public static min_img = Img.AsImageElement(Svg.min) - public static min_svg() { return new Img(Svg.min, true);} - public static min_ui() { return new FixedUiElement(Svg.min_img);} - - public static no_checkmark = " " - public static no_checkmark_img = Img.AsImageElement(Svg.no_checkmark) - public static no_checkmark_svg() { return new Img(Svg.no_checkmark, true);} - public static no_checkmark_ui() { return new FixedUiElement(Svg.no_checkmark_img);} - - public static or = " image/svg+xml " - public static or_img = Img.AsImageElement(Svg.or) - public static or_svg() { return new Img(Svg.or, true);} - public static or_ui() { return new FixedUiElement(Svg.or_img);} - - public static osm_copyright = " image/svg+xml " - public static osm_copyright_img = Img.AsImageElement(Svg.osm_copyright) - public static osm_copyright_svg() { return new Img(Svg.osm_copyright, true);} - public static osm_copyright_ui() { return new FixedUiElement(Svg.osm_copyright_img);} - - public static osm_logo_us = "" - public static osm_logo_us_img = Img.AsImageElement(Svg.osm_logo_us) - public static osm_logo_us_svg() { return new Img(Svg.osm_logo_us, true);} - public static osm_logo_us_ui() { return new FixedUiElement(Svg.osm_logo_us_img);} - - public static osm_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011 010110010011010110010011 " - public static osm_logo_img = Img.AsImageElement(Svg.osm_logo) - public static osm_logo_svg() { return new Img(Svg.osm_logo, true);} - public static osm_logo_ui() { return new FixedUiElement(Svg.osm_logo_img);} - - public static pencil = " " - public static pencil_img = Img.AsImageElement(Svg.pencil) - public static pencil_svg() { return new Img(Svg.pencil, true);} - public static pencil_ui() { return new FixedUiElement(Svg.pencil_img);} - - public static phone = " image/svg+xml " - public static phone_img = Img.AsImageElement(Svg.phone) - public static phone_svg() { return new Img(Svg.phone, true);} - public static phone_ui() { return new FixedUiElement(Svg.phone_img);} - - public static pin = " image/svg+xml " - public static pin_img = Img.AsImageElement(Svg.pin) - public static pin_svg() { return new Img(Svg.pin, true);} - public static pin_ui() { return new FixedUiElement(Svg.pin_img);} - - public static plus = " image/svg+xml " - public static plus_img = Img.AsImageElement(Svg.plus) - public static plus_svg() { return new Img(Svg.plus, true);} - public static plus_ui() { return new FixedUiElement(Svg.plus_img);} - - public static pop_out = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " - public static pop_out_img = Img.AsImageElement(Svg.pop_out) - public static pop_out_svg() { return new Img(Svg.pop_out, true);} - public static pop_out_ui() { return new FixedUiElement(Svg.pop_out_img);} - - public static reload = " " - public static reload_img = Img.AsImageElement(Svg.reload) - public static reload_svg() { return new Img(Svg.reload, true);} - public static reload_ui() { return new FixedUiElement(Svg.reload_img);} - - public static ring = " image/svg+xml " - public static ring_img = Img.AsImageElement(Svg.ring) - public static ring_svg() { return new Img(Svg.ring, true);} - public static ring_ui() { return new FixedUiElement(Svg.ring_img);} - - public static search = " " - public static search_img = Img.AsImageElement(Svg.search) - public static search_svg() { return new Img(Svg.search, true);} - public static search_ui() { return new FixedUiElement(Svg.search_img);} - - public static send_email = " image/svg+xml " - public static send_email_img = Img.AsImageElement(Svg.send_email) - public static send_email_svg() { return new Img(Svg.send_email, true);} - public static send_email_ui() { return new FixedUiElement(Svg.send_email_img);} - - public static share = " image/svg+xml " - public static share_img = Img.AsImageElement(Svg.share) - public static share_svg() { return new Img(Svg.share, true);} - public static share_ui() { return new FixedUiElement(Svg.share_img);} - - public static square = " image/svg+xml " - public static square_img = Img.AsImageElement(Svg.square) - public static square_svg() { return new Img(Svg.square, true);} - public static square_ui() { return new FixedUiElement(Svg.square_img);} - - public static star = " Created by potrace 1.15, written by Peter Selinger 2001-2017 " - public static star_img = Img.AsImageElement(Svg.star) - public static star_svg() { return new Img(Svg.star, true);} - public static star_ui() { return new FixedUiElement(Svg.star_img);} - - public static star_half = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static star_half_img = Img.AsImageElement(Svg.star_half) - public static star_half_svg() { return new Img(Svg.star_half, true);} - public static star_half_ui() { return new FixedUiElement(Svg.star_half_img);} - - public static star_outline = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static star_outline_img = Img.AsImageElement(Svg.star_outline) - public static star_outline_svg() { return new Img(Svg.star_outline, true);} - public static star_outline_ui() { return new FixedUiElement(Svg.star_outline_img);} - - public static star_outline_half = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " - public static star_outline_half_img = Img.AsImageElement(Svg.star_outline_half) - public static star_outline_half_svg() { return new Img(Svg.star_outline_half, true);} - public static star_outline_half_ui() { return new FixedUiElement(Svg.star_outline_half_img);} - - public static statistics = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " - public static statistics_img = Img.AsImageElement(Svg.statistics) - public static statistics_svg() { return new Img(Svg.statistics, true);} - public static statistics_ui() { return new FixedUiElement(Svg.statistics_img);} - - public static translate = " " - public static translate_img = Img.AsImageElement(Svg.translate) - public static translate_svg() { return new Img(Svg.translate, true);} - public static translate_ui() { return new FixedUiElement(Svg.translate_img);} - - public static up = " " - public static up_img = Img.AsImageElement(Svg.up) - public static up_svg() { return new Img(Svg.up, true);} - public static up_ui() { return new FixedUiElement(Svg.up_img);} - - public static wikidata = " " - public static wikidata_img = Img.AsImageElement(Svg.wikidata) - public static wikidata_svg() { return new Img(Svg.wikidata, true);} - public static wikidata_ui() { return new FixedUiElement(Svg.wikidata_img);} - - public static wikimedia_commons_white = " Wikimedia Commons Logo " - public static wikimedia_commons_white_img = Img.AsImageElement(Svg.wikimedia_commons_white) - public static wikimedia_commons_white_svg() { return new Img(Svg.wikimedia_commons_white, true);} - public static wikimedia_commons_white_ui() { return new FixedUiElement(Svg.wikimedia_commons_white_img);} - - public static wikipedia = " Wikipedia logo version 2" - public static wikipedia_img = Img.AsImageElement(Svg.wikipedia) - public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} - public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} - -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"length-crosshair.svg": Svg.length_crosshair,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/CenterFlexedElement.ts b/UI/Base/CenterFlexedElement.ts new file mode 100644 index 0000000000..5052c99e3b --- /dev/null +++ b/UI/Base/CenterFlexedElement.ts @@ -0,0 +1,32 @@ +import BaseUIElement from "../BaseUIElement"; + +export class CenterFlexedElement extends BaseUIElement { + private _html: string; + + constructor(html: string) { + super(); + this._html = html ?? ""; + } + + InnerRender(): string { + 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; + } + + AsMarkdown(): string { + return this._html; + } +} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 6ebf37a756..0c063b672e 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -28,7 +28,7 @@ export default class Minimap extends BaseUIElement { super() options = options ?? {} this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) - this._location = options?.location ?? new UIEventSource(undefined) + this._location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) this._id = "minimap" + Minimap._nextId; this._allowMoving = options.allowMoving ?? true; this._leafletoptions = options.leafletOptions ?? {} @@ -43,6 +43,7 @@ export default class Minimap extends BaseUIElement { div.style.width = "100%" div.style.minWidth = "40px" div.style.minHeight = "40px" + div.style.position = "relative" const wrapper = document.createElement("div") wrapper.appendChild(div) const self = this; diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index e7effe17fd..600f1517e5 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -1,46 +1,43 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; +import { UIEventSource } from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; export class VariableUiElement extends BaseUIElement { + private _element: HTMLElement; - private _element : HTMLElement; - - constructor(contents: UIEventSource) { - super(); - - this._element = document.createElement("span") - const el = this._element - contents.addCallbackAndRun(contents => { - while (el.firstChild) { - el.removeChild( - el.lastChild - ) - } + constructor( + contents: UIEventSource + ) { + super(); - if (contents === undefined) { - return el; - } - if (typeof contents === "string") { - el.innerHTML = contents - } else if (contents instanceof Array) { - for (const content of contents) { - const c = content.ConstructElement(); - if (c !== undefined && c !== null) { - el.appendChild(c) - } + this._element = document.createElement("span"); + const el = this._element; + contents.addCallbackAndRun((contents) => { + while (el.firstChild) { + el.removeChild(el.lastChild); + } - } - } else { - const c = contents.ConstructElement(); - if (c !== undefined && c !== null) { - el.appendChild(c) - } - } - }) - } + if (contents === undefined) { + return el; + } + if (typeof contents === "string") { + el.innerHTML = contents; + } else if (contents instanceof Array) { + for (const content of contents) { + const c = content?.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c); + } + } + } else { + const c = contents.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c); + } + } + }); + } - protected InnerConstructElement(): HTMLElement { - return this._element; - } - -} \ No newline at end of file + protected InnerConstructElement(): HTMLElement { + return this._element; + } +} diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts new file mode 100644 index 0000000000..0451d28fc4 --- /dev/null +++ b/UI/BigComponents/DownloadPanel.ts @@ -0,0 +1,55 @@ +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import Translations from "../i18n/Translations"; +import State from "../../State"; +import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource"; +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"; + +export class DownloadPanel extends Toggle { + constructor() { + const t = Translations.t.general.download + const somethingLoaded = State.state.featurePipeline.features.map(features => features.length > 0); + const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()]) + const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) + const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), + new Combine([t.downloadGeojson.Clone().SetClass("font-bold"), + t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col")) + .onClick(() => { + const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data}) + const name = State.state.layoutToUse.data.id; + Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), + `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.Clone().SetClass("font-bold"), + t.downloadCSVHelper.Clone()]).SetClass("flex flex-col")) + .onClick(() => { + const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: 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 downloadButtons = new Combine( + [new Title(t.title), buttonGeoJson, buttonCSV, includeMetaToggle, t.licenseInfo.Clone().SetClass("link-underline")]) + .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4") + + super( + downloadButtons, + t.noDataLoaded.Clone(), + somethingLoaded) + } +} \ No newline at end of file diff --git a/UI/BigComponents/ExportDataButton.ts b/UI/BigComponents/ExportDataButton.ts deleted file mode 100644 index 9a161de9fd..0000000000 --- a/UI/BigComponents/ExportDataButton.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import Translations from "../i18n/Translations"; -import State from "../../State"; -import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource"; -import {Utils} from "../../Utils"; -import Combine from "../Base/Combine"; - -export class ExportDataButton extends Combine { - constructor() { - const t = Translations.t.general.download - const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold")) - .onClick(() => { - const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline) - const name = State.state.layoutToUse.data.id; - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`); - }) - - super([button, t.licenseInfo.Clone().SetClass("link-underline")]) - } -} \ No newline at end of file diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts new file mode 100644 index 0000000000..17318a6100 --- /dev/null +++ b/UI/BigComponents/FilterView.ts @@ -0,0 +1,153 @@ +import {Utils} from "../../Utils"; +import {FixedInputElement} from "../Input/FixedInputElement"; +import {RadioButton} from "../Input/RadioButton"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Toggle from "../Input/Toggle"; +import Combine from "../Base/Combine"; +import Translations from "../i18n/Translations"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import {Translation} from "../i18n/Translation"; +import Svg from "../../Svg"; +import FilterConfig from "../../Customizations/JSON/FilterConfig"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import {And} from "../../Logic/Tags/And"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; +import State from "../../State"; +import FilteredLayer from "../../Models/FilteredLayer"; + + +/** + * Shows the filter + */ + +export default class FilterView extends VariableUiElement { + constructor(filteredLayer: UIEventSource) { + super( + filteredLayer.map((filteredLayers) => + filteredLayers?.map(l => FilterView.createOneFilteredLayerElement(l)) + ) + ); + } + + private static createOneFilteredLayerElement(filteredLayer) { + if(filteredLayer.layerDef.name === undefined){ + // Name is not defined: we hide this one + return undefined; + } + const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem"; + + const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); + const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( + iconStyle + ); + + if (filteredLayer.layerDef.name === undefined) { + return; + } + + + const name: Translation = Translations.WT( + filteredLayer.layerDef.name + )?.Clone(); + + const styledNameChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const styledNameUnChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const zoomStatus = + new Toggle( + undefined, + Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone() + .SetClass("alert") + .SetStyle("display: block ruby;width:min-content;"), + State.state.locationControl.map(location =>location.zoom >= filteredLayer.layerDef.minzoom ) + ) + + + const style = + "display:flex;align-items:center;padding:0.5rem 0;"; + const layerChecked = new Combine([icon, styledNameChecked, zoomStatus]) + .SetStyle(style) + .onClick(() => filteredLayer.isDisplayed.setData(false)); + + const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked]) + .SetStyle(style) + .onClick(() => filteredLayer.isDisplayed.setData(true)); + + + const filterPanel: BaseUIElement = FilterView.createFilterPanel(filteredLayer) + + + + return new Toggle( + new Combine([layerChecked, filterPanel]), + layerNotChecked, + filteredLayer.isDisplayed + ); + } + + static createFilterPanel(flayer: { + layerDef: LayerConfig, + appliedFilters: UIEventSource + }): BaseUIElement { + const layer = flayer.layerDef + if (layer.filters.length === 0) { + return undefined; + } + + let listFilterElements: [BaseUIElement, UIEventSource][] = layer.filters.map( + FilterView.createFilter + ); + + const update = () => { + let listTagsFilters = Utils.NoNull( + listFilterElements.map((input) => input[1].data) + ); + flayer.appliedFilters.setData(new And(listTagsFilters)); + }; + + listFilterElements.forEach((inputElement) => + inputElement[1].addCallback((_) => update()) + ); + + return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3"))) + .SetClass("flex flex-col ml-8 bg-gray-300 rounded-xl p-2") + + } + + static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource] { + if (filterConfig.options.length === 1) { + let option = filterConfig.options[0]; + + const icon = Svg.checkbox_filled_svg().SetClass("block mr-2"); + const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2"); + + const toggle = new Toggle( + new Combine([icon, option.question.Clone()]).SetClass("flex"), + new Combine([iconUnselected, option.question.Clone()]).SetClass("flex") + ) + .ToggleOnClick() + .SetClass("block m-1") + + return [toggle, toggle.isEnabled.map(enabled => enabled ? option.osmTags : undefined)] + } + + let options = filterConfig.options; + + const radio = new RadioButton( + options.map( + (option) => + new FixedInputElement(option.question.Clone(), option.osmTags) + ), + { + dontStyle: true + } + ); + return [radio, radio.GetValue()] + } +} diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index b05c26d0d2..cd16929ce1 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -1,6 +1,6 @@ import State from "../../State"; import ThemeIntroductionPanel from "./ThemeIntroductionPanel"; -import * as personal from "../../assets/themes/personalLayout/personalLayout.json"; +import * as personal from "../../assets/themes/personal/personal.json"; import PersonalLayersPanel from "./PersonalLayersPanel"; import Svg from "../../Svg"; import Translations from "../i18n/Translations"; diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index c8837fbccc..656c7084f2 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -1,13 +1,12 @@ import State from "../../State"; import BackgroundSelector from "./BackgroundSelector"; -import LayerSelection from "./LayerSelection"; 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 {ExportDataButton} from "./ExportDataButton"; +import {DownloadPanel} from "./DownloadPanel"; export default class LayerControlPanel extends ScrollableFullScreen { @@ -16,11 +15,13 @@ export default class LayerControlPanel extends ScrollableFullScreen { } private static GenTitle(): BaseUIElement { - return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2") + return Translations.t.general.layerSelection.title + .Clone() + .SetClass("text-2xl break-words font-bold p-2"); } private static GeneratePanel(): BaseUIElement { - const elements: BaseUIElement[] = [] + const elements: BaseUIElement[] = []; if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { const backgroundSelector = new BackgroundSelector(); @@ -31,18 +32,21 @@ export default class LayerControlPanel extends ScrollableFullScreen { } elements.push(new Toggle( - new LayerSelection(State.state.filteredLayers), - undefined, - State.state.filteredLayers.map(layers => layers.length > 1) - )) - - elements.push(new Toggle( - new ExportDataButton(), + new DownloadPanel(), undefined, State.state.featureSwitchEnableExport )) + - return new Combine(elements).SetClass("flex flex-col") - } -} \ No newline at end of file + elements.push( + new Toggle( + new DownloadPanel(), + undefined, + State.state.featureSwitchEnableExport + ) + ); + + return new Combine(elements).SetClass("flex flex-col"); + } +} diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts deleted file mode 100644 index 3c7f108e8c..0000000000 --- a/UI/BigComponents/LayerSelection.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import State from "../../State"; -import Toggle from "../Input/Toggle"; -import Combine from "../Base/Combine"; -import Translations from "../i18n/Translations"; -import LayerConfig from "../../Customizations/JSON/LayerConfig"; -import BaseUIElement from "../BaseUIElement"; -import {Translation} from "../i18n/Translation"; - -/** - * Shows the panel with all layers and a toggle for each of them - */ -export default class LayerSelection extends Combine { - - - constructor(activeLayers: UIEventSource<{ - readonly isDisplayed: UIEventSource, - readonly layerDef: LayerConfig; - }[]>) { - - if (activeLayers === undefined) { - throw "ActiveLayers should be defined..." - } - - - const checkboxes: BaseUIElement[] = []; - - for (const layer of activeLayers.data) { - const leafletStyle = layer.layerDef.GenerateLeafletStyle( - new UIEventSource({id: "node/-1"}), - false) - const leafletStyleNa = layer.layerDef.GenerateLeafletStyle( - new UIEventSource({id: "node/-1"}), - false) - const icon = new Combine([leafletStyle.icon.html]).SetClass("single-layer-selection-toggle") - let iconUnselected: BaseUIElement = new Combine([leafletStyleNa.icon.html]) - .SetClass("single-layer-selection-toggle") - .SetStyle("opacity:0.2;"); - - if (layer.layerDef.name === undefined) { - continue; - } - - const name: Translation = Translations.WT(layer.layerDef.name)?.Clone() - name.SetStyle("font-size:large;margin-left: 0.5em;"); - - const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => { - if (location.zoom < layer.layerDef.minzoom) { - return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone() - .SetClass("alert") - .SetStyle("display: block ruby;width:min-content;") - } - return "" - })) - const zoomStatusNonActive = new VariableUiElement(State.state.locationControl.map(location => { - if (location.zoom < layer.layerDef.minzoom) { - return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone() - .SetClass("alert") - .SetStyle("display: block ruby;width:min-content;") - } - return "" - })) - - const style = "display:flex;align-items:center;" - const styleWhole = "display:flex; flex-wrap: wrap" - checkboxes.push(new Toggle( - new Combine([new Combine([icon, name.Clone()]).SetStyle(style), zoomStatus]) - .SetStyle(styleWhole), - new Combine([new Combine([iconUnselected, "", name.Clone(), ""]).SetStyle(style), zoomStatusNonActive]) - .SetStyle(styleWhole), - layer.isDisplayed).ToggleOnClick() - .SetStyle("margin:0.3em;") - ); - } - - super(checkboxes) - this.SetStyle("display:flex;flex-direction:column;") - - } -} \ No newline at end of file diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 91ab436b28..97d9dbb96d 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -6,7 +6,7 @@ import State from "../../State"; import Combine from "../Base/Combine"; import {SubtleButton} from "../Base/SubtleButton"; import Translations from "../i18n/Translations"; -import * as personal from "../../assets/themes/personalLayout/personalLayout.json" +import * as personal from "../../assets/themes/personal/personal.json" import Constants from "../../Models/Constants"; import LanguagePicker from "../LanguagePicker"; import IndexText from "./IndexText"; diff --git a/UI/BigComponents/PersonalLayersPanel.ts b/UI/BigComponents/PersonalLayersPanel.ts index 64032f27ad..a2f9e6d2ec 100644 --- a/UI/BigComponents/PersonalLayersPanel.ts +++ b/UI/BigComponents/PersonalLayersPanel.ts @@ -64,13 +64,11 @@ export default class PersonalLayersPanel extends VariableUiElement { private static CreateLayerToggle(layer: LayerConfig): Toggle { let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle( new UIEventSource({id: "node/-1"}), - false, - "2em" + false ).icon.html]).SetClass("relative") let iconUnset =new Combine([ layer.GenerateLeafletStyle( new UIEventSource({id: "node/-1"}), - false, - "2em" + false ).icon.html]).SetClass("relative") iconUnset.SetStyle("opacity:0.1") diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 825ba2c96e..a5eb5c81d9 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -1,84 +1,83 @@ import Locale from "../i18n/Locale"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import {Translation} from "../i18n/Translation"; -import {VariableUiElement} from "../Base/VariableUIElement"; +import { UIEventSource } from "../../Logic/UIEventSource"; +import { Translation } from "../i18n/Translation"; +import { VariableUiElement } from "../Base/VariableUIElement"; import Svg from "../../Svg"; import State from "../../State"; -import {TextField} from "../Input/TextField"; -import {Geocoding} from "../../Logic/Osm/Geocoding"; +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"; export default class SearchAndGo extends Combine { + constructor() { + const goButton = Svg.search_ui().SetClass( + "w-8 h-8 full-rounded border-black float-right" + ); - constructor() { - const goButton = Svg.search_ui().SetClass('w-8 h-8 full-rounded border-black float-right'); + const placeholder = new UIEventSource( + Translations.t.general.search.search + ); + const searchField = new TextField({ + placeholder: new VariableUiElement(placeholder), + value: new UIEventSource(""), + inputStyle: + " background: transparent;\n" + + " border: none;\n" + + " font-size: large;\n" + + " width: 100%;\n" + + " height: 100%;\n" + + " box-sizing: border-box;\n" + + " color: var(--foreground-color);", + }); - const placeholder = new UIEventSource(Translations.t.general.search.search) - const searchField = new TextField({ - placeholder: new VariableUiElement( - placeholder.map(uiElement => uiElement, [Locale.language]) - ), - value: new UIEventSource(""), - - inputStyle: " background: transparent;\n" + - " border: none;\n" + - " font-size: large;\n" + - " width: 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)") + 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.SetStyle("background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;") + this.SetClass("block h-8"); + this.SetStyle( + "background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;" + ); + // Triggered by 'enter' or onclick + function runSearch() { + const searchString = searchField.GetValue().data; + if (searchString === undefined || searchString === "") { + return; + } + searchField.GetValue().setData(""); + placeholder.setData(Translations.t.general.search.searching); + Geocoding.Search( + searchString, + (result) => { + console.log("Search result", result); + if (result.length == 0) { + placeholder.setData(Translations.t.general.search.nothing); + return; + } - // Triggered by 'enter' or onclick - function runSearch() { - const searchString = searchField.GetValue().data; - if (searchString === undefined || searchString === "") { - return; - } - searchField.GetValue().setData(""); - placeholder.setData(Translations.t.general.search.searching); - Geocoding.Search(searchString, (result) => { - - console.log("Search result", result) - if (result.length == 0) { - placeholder.setData(Translations.t.general.search.nothing); - return; - } - - const poi = result[0]; - const bb = poi.boundingbox; - const bounds: [[number, number], [number, number]] = [ - [bb[0], bb[2]], - [bb[1], bb[3]] - ] - State.state.selectedElement.setData(undefined); - Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); - State.state.leafletMap.data.fitBounds(bounds); - placeholder.setData(Translations.t.general.search.search); - }, - () => { - searchField.GetValue().setData(""); - placeholder.setData(Translations.t.general.search.error); - }); - + const poi = result[0]; + const bb = poi.boundingbox; + const bounds: [[number, number], [number, number]] = [ + [bb[0], bb[2]], + [bb[1], bb[3]], + ]; + State.state.selectedElement.setData(undefined); + Hash.hash.setData(poi.osm_type + "/" + poi.osm_id); + State.state.leafletMap.data.fitBounds(bounds); + placeholder.setData(Translations.t.general.search.search); + }, + () => { + searchField.GetValue().setData(""); + placeholder.setData(Translations.t.general.search.error); } - - - searchField.enterPressed.addCallback(runSearch); - goButton.onClick(runSearch); + ); } - -} \ No newline at end of file + searchField.enterPressed.addCallback(runSearch); + goButton.onClick(runSearch); + } +} diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 9d1fd1475e..62ea506bbd 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -20,6 +20,8 @@ import LocationInput from "../Input/LocationInput"; import {InputElement} from "../Input/InputElement"; import Loc from "../../Models/Loc"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; +import Hash from "../../Logic/Web/Hash"; /* * The SimpleAddUI is a single panel, which can have multiple states: @@ -61,11 +63,6 @@ export default class SimpleAddUI extends Toggle { const selectedPreset = new UIEventSource(undefined); isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened - function createNewPoint(tags: any[], location: { lat: number, lon: number }) { - let feature = State.state.changes.createElement(tags, location.lat, location.lon); - State.state.selectedElement.setData(feature); - } - const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) const addUi = new VariableUiElement( @@ -75,8 +72,16 @@ export default class SimpleAddUI extends Toggle { } return SimpleAddUI.CreateConfirmButton(preset, (tags, location) => { - createNewPoint(tags, location) + const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon) + State.state.changes.applyAction(newElementAction) selectedPreset.setData(undefined) + isShown.setData(false) + State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( + newElementAction.newElementId + )) + console.log("Did set selected element to",State.state.allElements.ContainingFeatures.get( + newElementAction.newElementId + )) }, () => { selectedPreset.setData(undefined) }) @@ -121,16 +126,16 @@ export default class SimpleAddUI extends Toggle { lon: location.data.lon, zoom: 19 }); - + let backgroundLayer = undefined; - if(preset.preciseInput.preferredBackground){ - backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource(preset.preciseInput.preferredBackground)) + if (preset.preciseInput.preferredBackground) { + backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource(preset.preciseInput.preferredBackground)) } - + preciseInput = new LocationInput({ mapBackground: backgroundLayer, - centerLocation:locationSrc - + centerLocation: locationSrc + }) preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") } @@ -145,7 +150,7 @@ export default class SimpleAddUI extends Toggle { .onClick(() => { confirm(preset.tags, (preciseInput?.GetValue() ?? location).data); }); - + if (preciseInput !== undefined) { confirmButton = new Combine([preciseInput, confirmButton]) } @@ -241,7 +246,7 @@ export default class SimpleAddUI extends Toggle { for (const preset of presets) { const tags = TagUtils.KVtoProperties(preset.tags ?? []); - let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html + let icon: () => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html .SetClass("w-12 h-12 block relative"); const presetInfo: PresetInfo = { tags: preset.tags, diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index 6f8fbb8565..2824c21848 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -5,6 +5,7 @@ import Combine from "../Base/Combine"; import State from "../../State"; import Svg from "../../Svg"; import {Tag} from "../../Logic/Tags/Tag"; +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; export default class DeleteImage extends Toggle { @@ -15,14 +16,17 @@ export default class DeleteImage extends Toggle { .SetClass("rounded-full p-1") .SetStyle("color:white;background:#ff8c8c") .onClick(() => { - State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags); + State.state?.changes?. + applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data)) }); 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;") .onClick(() => { - State.state?.changes?.addTag(tags.data.id, new Tag(key, ""), tags); + State.state?.changes?.applyAction( + new ChangeTagAction( tags.data.id, new Tag(key, ""), tags.data) + ) }); 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;"); diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 58d9a37602..d978296095 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -11,6 +11,7 @@ import FileSelectorButton from "../Input/FileSelectorButton"; import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; export class ImageUploadFlow extends Toggle { @@ -28,7 +29,10 @@ export class ImageUploadFlow extends Toggle { key = imagePrefix + ":" + freeIndex; } console.log("Adding image:" + key, url); - State.state.changes.addTag(tags.id, new Tag(key, url), tagsSource); + State.state.changes + .applyAction(new ChangeTagAction( + tags.id, new Tag(key, url), tagsSource.data + )) }) diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index e88d676335..f1fc6da947 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -1,6 +1,6 @@ -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"; import BaseUIElement from "../BaseUIElement"; /** @@ -9,20 +9,21 @@ import BaseUIElement from "../BaseUIElement"; export default class CheckBoxes extends InputElement { private static _nextId = 0; IsSelected: UIEventSource = new UIEventSource(false); - private readonly value: UIEventSource + private readonly value: UIEventSource; private readonly _elements: BaseUIElement[]; - constructor(elements: BaseUIElement[], value = new UIEventSource([])) { + constructor( + elements: BaseUIElement[], + value = new UIEventSource([]) + ) { super(); this.value = value; this._elements = Utils.NoNull(elements); - this.SetClass("flex flex-col") - + this.SetClass("flex flex-col"); } IsValid(ts: number[]): boolean { return ts !== undefined; - } GetValue(): UIEventSource { @@ -30,48 +31,58 @@ export default class CheckBoxes extends InputElement { } protected InnerConstructElement(): HTMLElement { - const el = document.createElement("form") + const el = document.createElement("form"); 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 + const input = document.createElement("input"); + const id = CheckBoxes._nextId; CheckBoxes._nextId++; - input.id = "checkbox" + id + 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("span") - wrapper.classList.add("flex","w-full","border", "border-gray-400","m-1") - wrapper.appendChild(input) - wrapper.appendChild(label) - el.appendChild(wrapper) - - value.addCallbackAndRunD(selectedValues => { + const wrapper = document.createElement("span"); + wrapper.classList.add( + "wrapper", + "flex", + "w-full", + "border", + "border-gray-400", + "m-1" + ); + wrapper.appendChild(input); + wrapper.appendChild(label); + el.appendChild(wrapper); + + value.addCallbackAndRunD((selectedValues) => { if (selectedValues.indexOf(i) >= 0) { input.checked = true; } - - if(input.checked){ - wrapper.classList.remove("border-gray-400") - wrapper.classList.add("border-black") - }else{ - wrapper.classList.add("border-gray-400") - wrapper.classList.remove("border-black") + if (input.checked) { + wrapper.classList.remove("border-gray-400"); + wrapper.classList.add("border-black"); + } else { + wrapper.classList.add("border-gray-400"); + wrapper.classList.remove("border-black"); } - - }) + }); input.onchange = () => { // Index = index in the list of already checked items @@ -83,14 +94,9 @@ export default class CheckBoxes extends InputElement { value.data.splice(index, 1); value.ping(); } - } - - + }; } - return el; } - - -} \ No newline at end of file +} diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index d568e4443f..d54af67919 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -42,7 +42,6 @@ export default class LocationInput extends InputElement { } ) map.leafletMap.addCallbackAndRunD(leaflet => { - console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15)) leaflet.setMaxBounds( leaflet.getBounds().pad(0.15) ) diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 2822b2166f..fa951c5ca1 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -8,45 +8,52 @@ export class RadioButton extends InputElement { private readonly value: UIEventSource; private _elements: InputElement[]; private _selectFirstAsDefault: boolean; + private _dontStyle: boolean - constructor(elements: InputElement[], - selectFirstAsDefault = true) { - super() - this._selectFirstAsDefault = selectFirstAsDefault; + constructor( + elements: InputElement[], + options?: { + selectFirstAsDefault?: boolean, + dontStyle?: boolean + } + ) { + super(); + options = options ?? {} + this._selectFirstAsDefault = options.selectFirstAsDefault ?? true; this._elements = Utils.NoNull(elements); - this.value = new UIEventSource(undefined) + this.value = new UIEventSource(undefined); + this._dontStyle = options.dontStyle ?? false } + protected InnerConstructElement(): HTMLElement { const elements = this._elements; const selectFirstAsDefault = this._selectFirstAsDefault; - - const selectedElementIndex: UIEventSource = new UIEventSource(null); - const value = - UIEventSource.flatten(selectedElementIndex.map( - (selectedIndex) => { - if (selectedIndex !== undefined && selectedIndex !== null) { - return elements[selectedIndex].GetValue() - } - } - ), elements.map(e => e?.GetValue())); - value.syncWith(this.value) - - if(selectFirstAsDefault){ - - value.addCallbackAndRun(selected =>{ - if(selected === undefined){ - for (const element of elements) { - const v = element.GetValue().data; - if(v !== undefined){ - value.setData(v) - break; - } - } - - - } - }) + const selectedElementIndex: UIEventSource = + new UIEventSource(null); + + const value = UIEventSource.flatten( + selectedElementIndex.map((selectedIndex) => { + if (selectedIndex !== undefined && selectedIndex !== null) { + return elements[selectedIndex].GetValue(); + } + }), + elements.map((e) => e?.GetValue()) + ); + value.syncWith(this.value); + + if (selectFirstAsDefault) { + value.addCallbackAndRun((selected) => { + if (selected === undefined) { + for (const element of elements) { + const v = element.GetValue().data; + if (v !== undefined) { + value.setData(v); + break; + } + } + } + }); } for (let i = 0; i < elements.length; i++) { @@ -54,85 +61,108 @@ export class RadioButton extends InputElement { elements[i]?.onClick(() => { selectedElementIndex.setData(i); }); - elements[i].IsSelected.addCallback(isSelected => { + elements[i].IsSelected.addCallback((isSelected) => { if (isSelected) { selectedElementIndex.setData(i); } - }) + }); elements[i].GetValue().addCallback(() => { selectedElementIndex.setData(i); - }) + }); } + const groupId = "radiogroup" + RadioButton._nextId; + RadioButton._nextId++; - const groupId = "radiogroup" + RadioButton._nextId - RadioButton._nextId++ + const form = document.createElement("form"); + + const inputs = []; + const wrappers: HTMLElement[] = []; - const form = document.createElement("form") - const inputs = [] - const wrappers: HTMLElement[] = [] - for (let i1 = 0; i1 < elements.length; i1++) { let element = elements[i1]; const labelHtml = element.ConstructElement(); if (labelHtml === undefined) { continue; } - - const input = document.createElement("input") + + const input = document.createElement("input"); input.id = "radio" + groupId + "-" + i1; input.name = groupId; - input.type = "radio" - input.classList.add("p-1","cursor-pointer","ml-2","pl-2","pr-0","m-3","mr-0") + input.type = "radio"; + input.classList.add( + "cursor-pointer", + "p-1", + "mr-2" + ); - input.onchange = () => { - if(input.checked){ - selectedElementIndex.setData(i1) - } + + if (!this._dontStyle) { + input.classList.add( + "p-1", + "ml-2", + "pl-2", + "pr-0", + "m-3", + "mr-0" + ); } - - - inputs.push(input) + input.onchange = () => { + if (input.checked) { + selectedElementIndex.setData(i1); + } + }; - const label = document.createElement("label") - label.appendChild(labelHtml) + inputs.push(input); + + const label = document.createElement("label"); + label.appendChild(labelHtml); label.htmlFor = input.id; - label.classList.add("block","w-full","p-2","cursor-pointer","bg-red") + 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","border", "rounded-3xl", "border-gray-400","m-1") - wrappers.push(block) + 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", + "rounded-3xl", + "border-gray-400", + ) + } + wrappers.push(block); - form.appendChild(block) + form.appendChild(block); } + value.addCallbackAndRun((selected) => { + 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; - value.addCallbackAndRun( - selected => { - - 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 - - if(input.checked){ - wrappers[i].classList.remove("border-gray-400") - wrappers[i].classList.add("border-black") - }else{ - wrappers[i].classList.add("border-gray-400") - wrappers[i].classList.remove("border-black") - } - + if (input.checked) { + wrappers[i].classList.remove("border-gray-400"); + wrappers[i].classList.add("border-black"); + } else { + wrappers[i].classList.add("border-gray-400"); + wrappers[i].classList.remove("border-black"); } } - ) + }); + this.SetClass("flex flex-col"); - this.SetClass("flex flex-col") return form; } @@ -149,30 +179,26 @@ export class RadioButton extends InputElement { return this.value; } - - /* - public ShowValue(t: T): boolean { - if (t === undefined) { - return false; - } - if (!this.IsValid(t)) { - return false; - } - // We check that what is selected matches the previous rendering - for (let i = 0; i < this._elements.length; i++) { - const e = this._elements[i]; - if (e.IsValid(t)) { - this._selectedElementIndex.setData(i); - e.GetValue().setData(t); - const radio = document.getElementById(this.IdFor(i)); - // @ts-ignore - radio?.checked = true; - return; - } - - } - }*/ - - -} \ No newline at end of file + public ShowValue(t: T): boolean { + if (t === undefined) { + return false; + } + if (!this.IsValid(t)) { + return false; + } + // We check that what is selected matches the previous rendering + for (let i = 0; i < this._elements.length; i++) { + const e = this._elements[i]; + if (e.IsValid(t)) { + this._selectedElementIndex.setData(i); + e.GetValue().setData(t); + const radio = document.getElementById(this.IdFor(i)); + // @ts-ignore + radio?.checked = true; + return; + } + + } + }*/ +} diff --git a/UI/MapControlButton.ts b/UI/MapControlButton.ts index af1cef9a8b..b1a595034b 100644 --- a/UI/MapControlButton.ts +++ b/UI/MapControlButton.ts @@ -5,11 +5,16 @@ import Combine from "./Base/Combine"; * A button floating above the map, in a uniform style */ export default class MapControlButton extends Combine { - - constructor(contents: BaseUIElement) { - super([contents]); - this.SetClass("relative block rounded-full w-10 h-10 p-1 pointer-events-auto z-above-map subtle-background") - this.SetStyle("box-shadow: 0 0 10px var(--shadow-color);"); + constructor(contents: BaseUIElement, options?:{ + dontStyle?: boolean + }) { + super([contents]); + if(!options?.dontStyle){ + contents.SetClass("mapcontrol p-1") } - -} \ No newline at end of file + 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);"); + } +} diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index 1b2ab4b76e..146c404d49 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -3,7 +3,7 @@ import State from "../../State"; import Toggle from "../Input/Toggle"; import Translations from "../i18n/Translations"; import Svg from "../../Svg"; -import DeleteAction from "../../Logic/Osm/DeleteAction"; +import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; import {Tag} from "../../Logic/Tags/Tag"; import {UIEventSource} from "../../Logic/UIEventSource"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; @@ -19,6 +19,7 @@ import {Changes} from "../../Logic/Osm/Changes"; import {And} from "../../Logic/Tags/And"; import Constants from "../../Models/Constants"; import DeleteConfig from "../../Customizations/JSON/DeleteConfig"; +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; export default class DeleteWizard extends Toggle { /** @@ -58,7 +59,9 @@ export default class DeleteWizard extends Toggle { }) } (State.state?.changes ?? new Changes()) - .addTag(id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource); + .applyAction(new ChangeTagAction( + id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data + )) } function doDelete(selected: TagsFilter) { diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index b456c0ab96..1bd09cb143 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -13,6 +13,7 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; import BaseUIElement from "../BaseUIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import DeleteWizard from "./DeleteWizard"; +import SplitRoadWizard from "./SplitRoadWizard"; export default class FeatureInfoBox extends ScrollableFullScreen { @@ -66,10 +67,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { renderings.push(questionBox); } - const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) - if (!hasMinimap) { - renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) - } if (layerConfig.deletion) { renderings.push( @@ -81,6 +78,19 @@ export default class FeatureInfoBox extends ScrollableFullScreen { )) } + if (layerConfig.allowSplit) { + renderings.push( + new VariableUiElement(tags.map(tags => tags.id).map(id => + new SplitRoadWizard(id)) + )) + } + + + const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) + if (!hasMinimap) { + renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) + } + renderings.push( new VariableUiElement( State.state.osmConnection.userDetails diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts new file mode 100644 index 0000000000..f445263e38 --- /dev/null +++ b/UI/Popup/SplitRoadWizard.ts @@ -0,0 +1,155 @@ +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 State from "../../State"; +import ShowDataLayer from "../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 LayoutConfig from "../../Customizations/JSON/LayoutConfig"; +import SplitAction from "../../Logic/Osm/Actions/SplitAction"; +import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; +import Title from "../Base/Title"; + +export default class SplitRoadWizard extends Toggle { + private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout()) + + /** + * A UI Element used for splitting roads + * + * @param id: The id of the road to remove + */ + constructor(id: string) { + + 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 hasBeenSplit = new UIEventSource(false) + + // Toggle variable between show split button and map + const splitClicked = new UIEventSource(false); + + // Minimap on which you can select the points to be splitted + const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false}); + miniMap.SetStyle("width: 100%; height: 24rem;"); + + // Define how a cut is displayed on the map + + // Load the road with given id on the minimap + const roadElement = State.state.allElements.ContainingFeatures.get(id) + const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]); + // Datalayer displaying the road and the cut points (if any) + new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true, "splitRoadWay"); + new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false, "splitRoad: splitpoints") + + /** + * Handles a click on the overleaf map. + * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. + * @param coordinates Clicked location, [lon, lat] + */ + function onMapClick(coordinates) { + // Get nearest point on the road + const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates); // pointOnRoad is a geojson + + // Update point properties to let it match the layer + pointOnRoad.properties._cutposition = "yes"; + pointOnRoad["_matching_layer_id"] = "splitpositions"; + + // let the state remember the point, to be able to retrieve it later by id + State.state.allElements.addOrGetElement(pointOnRoad); + + // 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 + } + + // When clicked, pass clicked location coordinates to onMapClick function + 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(), t.inviteToSplit.Clone()); + splitButton.onClick( + () => { + splitClicked.setData(true) + } + ) + + // Only show the splitButton if logged in, else show login prompt + const loginBtn = t.loginToSplit.Clone() + .onClick(() => State.state.osmConnection.AttemptLogin()) + .SetClass("login-button-friendly"); + const splitToggle = new Toggle(splitButton, loginBtn, State.state.osmConnection.isLoggedIn) + + // Save button + const saveButton = new Button(t.split.Clone(), () => { + hasBeenSplit.setData(true) + const way = OsmObject.DownloadObject(id) + const partOfSrc = OsmObject.DownloadReferencingRelations(id); + let hasRun = false + way.map(way => { + const partOf = partOfSrc.data + if(way === undefined || partOf === undefined){ + return; + } + if(hasRun){ + return + } + hasRun = true + const splitAction = new SplitAction( + way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature) + ) + State.state.changes.applyAction(splitAction) + + }, [partOfSrc]) + + + }); + 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 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); + }); + + cancelButton.SetClass("btn btn-secondary block"); + + const splitTitle = new Title(t.splitTitle); + + const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle]).SetClass("flex flex-row")]); + mapView.SetClass("question") + const confirm = new Toggle(mapView, splitToggle, splitClicked); + super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) + } + + private static GetSplitLayout(): LayoutConfig { + return new LayoutConfig({ + maintainer: "mapcomplete", + language: ["en"], + startLon: 0, + startLat: 0, + description: "Split points visualisations - built in at SplitRoadWizard.ts", + icon: "", startZoom: 0, + title: "Split locations", + version: "", + + id: "splitpositions", + layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}] + }, true, "(BUILTIN) SplitRoadWizard.ts") + + } +} \ No newline at end of file diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index c723759590..5eba4693aa 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -25,6 +25,7 @@ import BaseUIElement from "../BaseUIElement"; import {DropDown} from "../Input/DropDown"; import {Unit} from "../../Customizations/JSON/Denomination"; import InputElementWrapper from "../Input/InputElementWrapper"; +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; /** * Shows the question element. @@ -56,7 +57,9 @@ export default class TagRenderingQuestion extends Combine { const selection = inputElement.GetValue().data; if (selection) { (State.state?.changes ?? new Changes()) - .addTag(tags.data.id, selection, tags); + .applyAction(new ChangeTagAction( + tags.data.id, selection, tags.data + )) } if (options.afterSave) { @@ -164,7 +167,7 @@ export default class TagRenderingQuestion extends Combine { if (configuration.multiAnswer) { return TagRenderingQuestion.GenerateMultiAnswer(configuration, inputEls, ff, configuration.mappings.map(mp => mp.ifnot)) } else { - return new RadioButton(inputEls, false) + return new RadioButton(inputEls, {selectFirstAsDefault: false}) } } @@ -195,9 +198,7 @@ export default class TagRenderingQuestion extends Combine { oppositeTags.push(notSelected); } tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); - const actualTags = TagUtils.FlattenMultiAnswer(tags); - console.log("Converted ", indices.join(","), "into", actualTags.asHumanString(false, false, {}), "with elems", elements) - return actualTags; + return TagUtils.FlattenMultiAnswer(tags); }, (tags: TagsFilter) => { // {key --> values[]} diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 59225640f2..3358266284 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -22,7 +22,8 @@ export default class ShowDataLayer { leafletMap: UIEventSource, layoutToUse: UIEventSource, enablePopups = true, - zoomToFeatures = false) { + zoomToFeatures = false, + name?: string) { this._leafletMap = leafletMap; this._enablePopups = enablePopups; this._features = features; @@ -85,9 +86,7 @@ export default class ShowDataLayer { console.error(e) } } - - - State.state.selectedElement.ping(); + State.state.selectedElement.ping() } features.addCallback(() => update()); @@ -131,6 +130,7 @@ export default class ShowDataLayer { }) }); } + private postProcessFeature(feature, leafletLayer: L.Layer) { const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; if (layer === undefined) { @@ -160,7 +160,7 @@ export default class ShowDataLayer { leafletLayer.on("popupopen", () => { State.state.selectedElement.setData(feature) - + if (infobox === undefined) { const tags = State.state.allElements.getEventSourceById(feature.properties.id); infobox = new FeatureInfoBox(tags, layer); @@ -175,7 +175,7 @@ export default class ShowDataLayer { infobox.AttachTo(id) - infobox.Activate(); + infobox.Activate(); }); const self = this; State.state.selectedElement.addCallbackAndRunD(selected => { @@ -188,11 +188,13 @@ export default class ShowDataLayer { if (selected.properties.id === feature.properties.id) { // A small sanity check to prevent infinite loops: 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 - - && feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too - ) { + && feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too + ) { leafletLayer.openPopup() } + if(feature.id !== feature.properties.id){ + console.trace("Not opening the popup for", feature) + } } }) diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 5a38e8184a..ec6c34b5cf 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -56,9 +56,12 @@ export default class SpecialVisualizations { if (!tags.hasOwnProperty(key)) { continue; } - parts.push(key + "=" + tags[key]); + parts.push([key , tags[key] ?? "undefined" ]); } - return parts.join("
") + return new Table( + ["key","value"], + parts + ) })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") }) }, @@ -127,6 +130,7 @@ export default class SpecialVisualizations { // This is a list of values idList = JSON.parse(value) } + for (const id of idList) { features.push({ freshness: new Date(), diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 43352aa5b4..361540e65f 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -19,7 +19,6 @@ export class SubstitutedTranslation extends VariableUiElement { const extraMappings: SpecialVisualization[] = []; mapping?.forEach((value, key) => { - console.log("KV:", key, value) extraMappings.push( { funcName: key, @@ -73,11 +72,6 @@ export class SubstitutedTranslation extends VariableUiElement { } }[] { - if (extraMappings.length > 0) { - - console.log("Extra mappings are", extraMappings) - } - for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index e6e2dfba2b..f153b1e653 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -109,9 +109,9 @@ export class Translation extends BaseUIElement { // @ts-ignore const date: Date = el; rtext = date.toLocaleString(); - } else if (el.ConstructElement() === undefined) { - console.error("InnerREnder is not defined", el); - throw "Hmmm, el.InnerRender is not defined?" + } else if (el.ConstructElement === undefined) { + console.error("ConstructElement is not defined", el); + throw "ConstructElement is not defined, you are working with a "+(typeof el)+":"+(el.constructor.name) } else { Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day rtext = el.ConstructElement().innerHTML; diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index eb91b6cd86..d544149883 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -19,7 +19,7 @@ export default class Translations { static T(t: string | any, context = undefined): Translation { - if(t === undefined){ + if(t === undefined || t === null){ return undefined; } if(typeof t === "string"){ @@ -38,7 +38,7 @@ export default class Translations { private static wtcache = {} public static WT(s: string | Translation): Translation { - if(s === undefined){ + if(s === undefined || s === null){ return undefined; } if (typeof (s) === "string") { diff --git a/Utils.ts b/Utils.ts index f16f987af2..8414f8882d 100644 --- a/Utils.ts +++ b/Utils.ts @@ -136,6 +136,19 @@ export class Utils { return newArr; } + public static Identical(t1: T[], t2: T[], eq?: (t: T, t0: T) => boolean): boolean{ + if(t1.length !== t2.length){ + return false + } + eq = (a, b) => a === b + for (let i = 0; i < t1.length ; i++) { + if(!eq(t1[i] ,t2[i])){ + return false + } + } + return true; + } + public static MergeTags(a: any, b: any) { const t = {}; for (const k in a) { @@ -210,7 +223,9 @@ export class Utils { if (sourceV?.length !== undefined && targetV?.length !== undefined && key.startsWith("+")) { target[key] = targetV.concat(sourceV) } else if (typeof sourceV === "object") { - if (targetV === undefined) { + if (sourceV === null) { + target[key] = null + } else if (targetV === undefined) { target[key] = sourceV; } else { Utils.Merge(sourceV, targetV); @@ -356,12 +371,16 @@ export class Utils { /** * Triggers a 'download file' popup which will download the contents - * @param contents - * @param fileName */ - public static offerContentsAsDownloadableFile(contents: string, fileName: string = "download.txt") { + public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt", + options?: { mimetype: string }) { const element = document.createElement("a"); - const file = new Blob([contents], {type: 'text/plain'}); + 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 @@ -449,8 +468,8 @@ export class Utils { } } - public static setDefaults(options, defaults){ - for (let key in defaults){ + public static setDefaults(options, defaults) { + for (let key in defaults) { if (!(key in options)) options[key] = defaults[key]; } return options; diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000000..046c7db674 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/layers/.DS_Store b/assets/layers/.DS_Store new file mode 100644 index 0000000000..4db61cc285 Binary files /dev/null and b/assets/layers/.DS_Store differ diff --git a/assets/layers/bench/bench.json b/assets/layers/bench/bench.json index 312b21cfcc..1162dd8638 100644 --- a/assets/layers/bench/bench.json +++ b/assets/layers/bench/bench.json @@ -152,10 +152,11 @@ "hu": "Anyag: {material}", "it": "Materiale: {material}", "ru": "Материал: {material}", - "zh_Hans": "材质: {material}", + "zh_Hanå¨s": "材质: {material}", "zh_Hant": "材質:{material}", "nb_NO": "Materiale: {material}", - "fi": "Materiaali: {material}" + "fi": "Materiaali: {material}", + "zh_Hans": "材质: {material}" }, "freeform": { "key": "material", @@ -517,13 +518,8 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { - "render": "./assets/themes/benches/bench_poi.svg", - "mappings": [] - }, - "width": { - "render": "8" + "render": "circle:#FE6F32;./assets/layers/bench/bench.svg" }, "iconSize": { "render": "35,35,center" diff --git a/assets/layers/bench/bench.svg b/assets/layers/bench/bench.svg new file mode 100644 index 0000000000..dc4fc07882 --- /dev/null +++ b/assets/layers/bench/bench.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/layers/bench/license_info.json b/assets/layers/bench/license_info.json new file mode 100644 index 0000000000..2e404ab20d --- /dev/null +++ b/assets/layers/bench/license_info.json @@ -0,0 +1,8 @@ +[ + { + "authors": [], + "path": "bench.svg", + "license": "CC0", + "sources": [] + } +] \ No newline at end of file diff --git a/assets/layers/bench_at_pt/bench_at_pt.json b/assets/layers/bench_at_pt/bench_at_pt.json index bb2661e25f..fc80219b38 100644 --- a/assets/layers/bench_at_pt/bench_at_pt.json +++ b/assets/layers/bench_at_pt/bench_at_pt.json @@ -126,7 +126,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/benches/bench_public_transport.svg" }, diff --git a/assets/layers/bicycle_library/bicycle_library.json b/assets/layers/bicycle_library/bicycle_library.json index d56f3bc428..dd9ec5741c 100644 --- a/assets/layers/bicycle_library/bicycle_library.json +++ b/assets/layers/bicycle_library/bicycle_library.json @@ -210,7 +210,6 @@ }, "description" ], - "hideUnderlayingFeaturesMinPercentage": 1, "presets": [ { "title": { diff --git a/assets/layers/bike_cafe/bike_cafe.json b/assets/layers/bike_cafe/bike_cafe.json index 30dcb1760a..9bf3d61a82 100644 --- a/assets/layers/bike_cafe/bike_cafe.json +++ b/assets/layers/bike_cafe/bike_cafe.json @@ -293,7 +293,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/layers/bike_cafe/bike_cafe.svg" }, diff --git a/assets/layers/bike_monitoring_station/bike_monitoring_station.json b/assets/layers/bike_monitoring_station/bike_monitoring_station.json index 34b4d1b630..86209610e3 100644 --- a/assets/layers/bike_monitoring_station/bike_monitoring_station.json +++ b/assets/layers/bike_monitoring_station/bike_monitoring_station.json @@ -63,7 +63,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/layers/bike_monitoring_station/monitoring_station.svg" }, diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json index 7f60ced897..47c5d0d57c 100644 --- a/assets/layers/bike_shop/bike_shop.json +++ b/assets/layers/bike_shop/bike_shop.json @@ -601,7 +601,6 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 1, "presets": [ { "title": { @@ -610,7 +609,8 @@ "fr": "Magasin et réparateur de vélo", "gl": "Tenda/arranxo de bicicletas", "de": "Fahrradwerkstatt/geschäft", - "it": "Negozio/riparatore di bici" + "it": "Negozio/riparatore di bici", + "ru": "Обслуживание велосипедов/магазин" }, "tags": [ "shop=bicycle" diff --git a/assets/layers/bike_themed_object/bike_themed_object.json b/assets/layers/bike_themed_object/bike_themed_object.json index e3960b9d69..bae9e790ec 100644 --- a/assets/layers/bike_themed_object/bike_themed_object.json +++ b/assets/layers/bike_themed_object/bike_themed_object.json @@ -56,7 +56,6 @@ "phone", "opening_hours" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/layers/bike_themed_object/other_services.svg" }, diff --git a/assets/layers/birdhide/birdhide.json b/assets/layers/birdhide/birdhide.json index 71a80d88d1..50fce43cb5 100644 --- a/assets/layers/birdhide/birdhide.json +++ b/assets/layers/birdhide/birdhide.json @@ -88,6 +88,17 @@ "nl": "Vogelkijkhut" } }, + { + "if": { + "and": [ + "building=tower", + "bird_hide=tower" + ] + }, + "then": { + "nl": "Vogelkijktoren" + } + }, { "if": { "or": [ @@ -241,5 +252,44 @@ } } ], - "wayHandling": 2 + "wayHandling": 1, + "filter": [ + { + "options": [ + { + "question": { + "nl": "Rolstoeltoegankelijk", + "en": "Wheelchair accessible" + }, + "osmTags": { + "or": [ + "wheelchair=yes", + "wheelchair=designated", + "wheelchair=permissive" + ] + } + } + ] + }, + { + "options": [ + { + "question": { + "nl": "Enkel overdekte kijkhutten" + }, + "osmTags": { + "and": [ + { + "or": [ + "shelter=yes", + "building~*" + ] + }, + "covered!=no" + ] + } + } + ] + } + ] } \ No newline at end of file diff --git a/assets/layers/crossings/Belgian_road_sign_B22.svg b/assets/layers/crossings/Belgian_road_sign_B22.svg new file mode 100644 index 0000000000..7d195a6262 --- /dev/null +++ b/assets/layers/crossings/Belgian_road_sign_B22.svg @@ -0,0 +1,123 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/layers/crossings/Belgian_road_sign_B23.svg b/assets/layers/crossings/Belgian_road_sign_B23.svg new file mode 100644 index 0000000000..c6b2de88ab --- /dev/null +++ b/assets/layers/crossings/Belgian_road_sign_B23.svg @@ -0,0 +1,124 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/layers/crossings/crossings.json b/assets/layers/crossings/crossings.json new file mode 100644 index 0000000000..0dcfc9aa19 --- /dev/null +++ b/assets/layers/crossings/crossings.json @@ -0,0 +1,310 @@ +{ + "id": "crossings", + "name": { + "en": "Crossings", + "nl": "Oversteekplaatsen" + }, + "description": { + "en": "Crossings for pedestrians and cyclists", + "nl": "Oversteekplaatsen voor voetgangers en fietsers" + }, + "source": { + "osmTags": { + "or": [ + "highway=traffic_signals", + "highway=crossing" + ] + } + }, + "minzoom": 17, + "title": { + "render": { + "en": "Crossing", + "nl": "Oversteekplaats" + }, + "mappings": [ + { + "if": "highway=traffic_signals", + "then": { + "en": "Traffic signal", + "nl": "Verkeerslicht" + } + }, + { + "if": "crossing=traffic_signals", + "then": { + "en": "Crossing with traffic signals", + "nl": "Oversteektplaats met verkeerslichten" + } + } + ] + }, + "icon": { + "render": "./assets/layers/crossings/pedestrian_crossing.svg", + "mappings": [ + { + "if": { + "or": [ + "highway=traffic_signals", + "crossing=traffic_signals" + ] + }, + "then": "./assets/layers/crossings/traffic_lights.svg" + } + ] + }, + "width": "5", + "presets": [ + { + "title": { + "en": "Crossing", + "nl": "Oversteekplaats" + }, + "tags": [ + "highway=crossing" + ], + "description": { + "en": "Crossing for pedestrians and/or cyclists", + "nl": "Oversteekplaats voor voetgangers en/of fietsers" + } + }, + { + "title": { + "en": "Traffic signal", + "nl": "Verkeerslicht" + }, + "tags": [ + "highway=traffic_signals" + ], + "description": { + "en": "Traffic signal on a road", + "nl": "Verkeerslicht op een weg" + } + } + ], + "tagRenderings": [ + { + "question": { + "en": "What kind of crossing is this?", + "nl": "Wat voor oversteekplaats is dit?" + }, + "condition": "highway=crossing", + "mappings": [ + { + "if": "crossing=uncontrolled", + "then": { + "en": "Crossing, without traffic lights", + "nl": "Oversteekplaats, zonder verkeerslichten" + } + }, + { + "if": "crossing=traffic_signals", + "then": { + "en": "Crossing with traffic signals", + "nl": "Oversteekplaats met verkeerslichten" + } + }, + { + "if": "crossing=zebra", + "then": { + "en": "Zebra crossing", + "nl": "Zebrapad" + }, + "hideInAnswer": true + } + ] + }, + { + "question": { + "en": "Is this is a zebra crossing?", + "nl": "Is dit een zebrapad?" + }, + "condition": "crossing=uncontrolled", + "mappings": [ + { + "if": "crossing_ref=zebra", + "then": { + "en": "This is a zebra crossing", + "nl": "Dit is een zebrapad" + } + }, + { + "if": "crossing_ref=", + "then": { + "en": "This is not a zebra crossing", + "nl": "Dit is niet een zebrapad" + } + } + ] + }, + { + "question": { + "en": "Is this crossing also for bicycles?", + "nl": "Is deze oversteekplaats ook voor fietsers" + }, + "condition": "highway=crossing", + "mappings": [ + { + "if": "bicycle=yes", + "then": { + "en": "A cyclist can use this crossing", + "nl": "Een fietser kan deze oversteekplaats gebruiken" + } + }, + { + "if": "bicycle=no", + "then": { + "en": "A cyclist can not use this crossing", + "nl": "Een fietser kan niet deze oversteekplaats gebruiken" + } + } + ] + }, + { + "question": { + "en": "Does this crossing have an island in the middle?", + "nl": "Heeft deze oversteekplaats een verkeerseiland in het midden?" + }, + "condition": "highway=crossing", + "mappings": [ + { + "if": "crossing:island=yes", + "then": { + "en": "This crossing has an island in the middle", + "nl": "Deze oversteekplaats heeft een verkeerseiland in het midden" + } + }, + { + "if": "crossing:island=no", + "then": { + "en": "This crossing does not have an island in the middle", + "nl": "Deze oversteekplaats heeft niet een verkeerseiland in het midden" + } + } + ] + }, + { + "question": { + "en": "Does this crossing have tactile paving?", + "nl": "Heeft deze oversteekplaats een geleidelijn?" + }, + "condition": "highway=crossing", + "mappings": [ + { + "if": "tactile_paving=yes", + "then": { + "en": "This crossing has tactile paving", + "nl": "Deze oversteekplaats heeft een geleidelijn" + } + }, + { + "if": "tactile_paving=no", + "then": { + "en": "This crossing does not have tactile paving", + "nl": "Deze oversteekplaats heeft niet een geleidelijn" + } + }, + { + "if": "tactile_paving=incorrect", + "then": { + "en": "This crossing has tactile paving, but is not correct", + "nl": "Deze oversteekplaats heeft een geleidelijn, die incorrect is." + }, + "hideInAnswer": true + } + ] + }, + { + "question": { + "en": "Does this traffic light have a button to request green light?", + "nl": "Heeft dit verkeerslicht een knop voor groen licht?" + }, + "condition": { + "or": [ + "highway=traffic_signals", + "crossing=traffic_signals" + ] + }, + "mappings": [ + { + "if": "button_operated=yes", + "then": { + "en": "This traffic light has a button to request green light", + "nl": "Dit verkeerslicht heeft een knop voor groen licht" + } + }, + { + "if": "button_operated=no", + "then": { + "en": "This traffic light does not have a button to request green light", + "nl": "Dit verkeerlicht heeft niet een knop voor groen licht" + } + } + ] + }, + { + "question": { + "en": "Can a cyclist turn right when the light is red?", + "nl": "Mag een fietser rechtsaf slaan als het licht rood is?" + }, + "condition": "highway=traffic_signals", + "mappings": [ + { + "if": "red_turn:right:bicycle=yes", + "then": { + "en": "A cyclist can turn right if the light is red ", + "nl": "Een fietser mag wel rechtsaf slaan als het licht rood is " + }, + "hideInAnswer": "_country!=be" + }, + { + "if": "red_turn:right:bicycle=yes", + "then": { + "en": "A cyclist can turn right if the light is red", + "nl": "Een fietser mag wel rechtsaf slaan als het licht rood is" + }, + "hideInAnswer": "_country=be" + }, + { + "if": "red_turn:right:bicycle=no", + "then": { + "en": "A cyclist can not turn right if the light is red", + "nl": "Een fietser mag niet rechtsaf slaan als het licht rood is" + } + } + ] + }, + { + "question": { + "en": "Can a cyclist go straight on when the light is red?", + "nl": "Mag een fietser rechtdoor gaan als het licht rood is?" + }, + "condition": "highway=traffic_signals", + "mappings": [ + { + "if": "red_turn:straight:bicycle=yes", + "then": { + "en": "A cyclist can go straight on if the light is red ", + "nl": "Een fietser mag wel rechtdoor gaan als het licht rood is " + }, + "hideInAnswer": "_country!=be" + }, + { + "if": "red_turn:straight:bicycle=yes", + "then": { + "en": "A cyclist can go straight on if the light is red", + "nl": "Een fietser mag wel rechtdoor gaan als het licht rood is" + }, + "hideInAnswer": "_country=be" + }, + { + "if": "red_turn:straight:bicycle=no", + "then": { + "en": "A cyclist can not go straight on if the light is red", + "nl": "Een fietser mag niet rechtdoor gaan als het licht rood is" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/assets/layers/crossings/license_info.json b/assets/layers/crossings/license_info.json new file mode 100644 index 0000000000..2d1d83740f --- /dev/null +++ b/assets/layers/crossings/license_info.json @@ -0,0 +1,42 @@ +[ + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_road_sign_B22.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_road_sign_B22.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_road_sign_B23.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_road_sign_B23.svg" + ] + }, + { + "authors": [ + "Tobias Zwick" + ], + "path": "pedestrian_crossing.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/quest%20icons/pedestrian_crossing.svg" + ] + }, + { + "authors": [ + "Tobias Zwick" + ], + "path": "traffic_lights.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/quest%20icons/traffic_lights.svg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/crossings/pedestrian_crossing.svg b/assets/layers/crossings/pedestrian_crossing.svg new file mode 100644 index 0000000000..843eefbbc0 --- /dev/null +++ b/assets/layers/crossings/pedestrian_crossing.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/layers/crossings/traffic_lights.svg b/assets/layers/crossings/traffic_lights.svg new file mode 100644 index 0000000000..dd1d856370 --- /dev/null +++ b/assets/layers/crossings/traffic_lights.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/information_board/information_board.json b/assets/layers/information_board/information_board.json index 017f3aeb63..4b0c47f4ff 100644 --- a/assets/layers/information_board/information_board.json +++ b/assets/layers/information_board/information_board.json @@ -29,13 +29,9 @@ "tagRenderings": [ "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/layers/information_board/board.svg" }, - "width": { - "render": "8" - }, "iconSize": { "render": "40,40,center" }, diff --git a/assets/layers/map/map.json b/assets/layers/map/map.json index 2c3cf3ea74..453a7f0b7f 100644 --- a/assets/layers/map/map.json +++ b/assets/layers/map/map.json @@ -159,7 +159,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/layers/map/map.svg", "mappings": [ diff --git a/assets/layers/nature_reserve/nature_reserve.json b/assets/layers/nature_reserve/nature_reserve.json index a0fd58df80..a19ada050c 100644 --- a/assets/layers/nature_reserve/nature_reserve.json +++ b/assets/layers/nature_reserve/nature_reserve.json @@ -6,9 +6,18 @@ "minzoom": 12, "source": { "osmTags": { - "or": [ - "leisure=nature_reserve", - "boundary=protected_area" + "and": [ + { + "or": [ + "leisure=nature_reserve", + { + "and": [ + "protect_class!=98", + "boundary=protected_area" + ] + } + ] + } ] } }, @@ -383,13 +392,12 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 10, "wayHandling": 2, "icon": { "render": "./assets/themes/buurtnatuur/nature_reserve.svg" }, "width": { - "render": "5" + "render": "1" }, "iconSize": { "render": "50,50,center" @@ -410,5 +418,43 @@ "nl": "Voeg een ontbrekend, erkend natuurreservaat toe, bv. een gebied dat beheerd wordt door het ANB of natuurpunt" } } + ], + "filter": [ + { + "options": [ + { + "question": { + "nl": "Vrij te bezoeken" + }, + "osmTags": "access=yes" + } + ] + }, + { + "options": [ + { + "question": { + "nl": "Alle natuurgebieden" + } + }, + { + "question": { + "nl": "Honden mogen vrij rondlopen" + }, + "osmTags": "dog=yes" + }, + { + "question": { + "nl": "Honden welkom aan de leiband" + }, + "osmTags": { + "or": [ + "dog=yes", + "dog=leashed" + ] + } + } + ] + } ] } \ No newline at end of file diff --git a/assets/layers/parking/license_info.json b/assets/layers/parking/license_info.json new file mode 100644 index 0000000000..ddeaebaf9c --- /dev/null +++ b/assets/layers/parking/license_info.json @@ -0,0 +1,12 @@ +[ + { + "authors": [ + "Freepik" + ], + "path": "parking.svg", + "license": "CC-BY", + "sources": [ + "https://www.flaticon.com/free-icon/placeholder_608690?term=parking&page=1&position=16&page=1&position=16&related_id=608690&origin=search" + ] + } +] \ No newline at end of file diff --git a/assets/layers/parking/parking.json b/assets/layers/parking/parking.json new file mode 100644 index 0000000000..d4d1cab73e --- /dev/null +++ b/assets/layers/parking/parking.json @@ -0,0 +1,192 @@ +{ + "id": "parking", + "name": { + "nl": "parking" + }, + "minzoom": 12, + "source": { + "osmTags": { + "and": [ + { + "or": [ + "amenity=parking", + "amenity=motorcycle_parking", + "amenity=bicycle_parking" + ] + } + ] + } + }, + "title": { + "render": { + "nl": "Parking" + }, + "mappings": [ + { + "if": "amenity=parking", + "then": { + "nl": "{name:nl}" + } + }, + { + "if": "amenity=motorcycle_parking", + "then": { + "nl": "{name}" + } + }, + { + "if": "amenity=bicycle_parking", + "then": { + "nl": "Fietsenstalling" + } + } + ] + }, + "icon": { + "render": "./assets/layers/parking/parking.svg" + }, + "description": { + "nl": "Parking" + }, + "tagRenderings": [ + "images", + { + "#": "Access tag", + "render": { + "nl": "De toegankelijkheid van dit gebied is: {access:description}" + }, + "question": { + "nl": "Is dit gebied toegankelijk?" + }, + "freeform": { + "key": "access:description" + }, + "mappings": [ + { + "if": { + "and": [ + "access=yes", + "fee=" + ] + }, + "then": { + "nl": "Vrij toegankelijk" + } + }, + { + "if": { + "and": [ + "access=no", + "fee=" + ] + }, + "then": { + "nl": "Niet toegankelijk" + } + }, + { + "if": { + "and": [ + "access=private", + "fee=" + ] + }, + "then": { + "nl": "Niet toegankelijk, want privégebied" + } + }, + { + "if": { + "and": [ + "access=permissive", + "fee=" + ] + }, + "then": { + "nl": "Toegankelijk, ondanks dat het privegebied is" + } + }, + { + "if": { + "and": [ + "access=guided", + "fee=" + ] + }, + "then": { + "nl": "Enkel toegankelijk met een gids of tijdens een activiteit" + } + }, + { + "if": { + "and": [ + "access=yes", + "fee=yes" + ] + }, + "then": { + "nl": "Toegankelijk mits betaling" + } + } + ] + }, + { + "#": "Operator tag", + "render": { + "nl": "Beheer door {operator}" + }, + "question": { + "nl": "Wie beheert dit pad?" + }, + "freeform": { + "key": "operator" + }, + "mappings": [ + { + "if": { + "and": [ + "operator=Natuurpunt" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door Natuurpunt" + } + }, + { + "if": { + "and": [ + "operator~(n|N)atuurpunt.*" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door {operator}" + }, + "hideInAnswer": true + } + ] + } + ], + "wayHandling": 1, + "iconSize": { + "render": "36,36,center" + }, + "color": { + "render": "#E1AD01" + }, + "presets": [ + { + "tags": [ + "amenity=parking", + "amenity=motorcycle_parking", + "amenity=bicycle_parking", + "fixme=Toegevoegd met MapComplete, geometry nog uit te tekenen" + ], + "title": { + "nl": "Paden" + }, + "description": { + "nl": "Voeg een ontbrekend, erkend pad toe." + } + } + ] +} \ No newline at end of file diff --git a/assets/layers/parking/parking.svg b/assets/layers/parking/parking.svg new file mode 100644 index 0000000000..5f4a83b081 --- /dev/null +++ b/assets/layers/parking/parking.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/layers/picnic_table/picnic_table.json b/assets/layers/picnic_table/picnic_table.json index 672e41f07f..ac46da5363 100644 --- a/assets/layers/picnic_table/picnic_table.json +++ b/assets/layers/picnic_table/picnic_table.json @@ -72,13 +72,9 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "circle:#e6cf39;./assets/layers/picnic_table/picnic_table.svg" }, - "width": { - "render": "8" - }, "iconSize": { "render": "35,35,center" }, diff --git a/assets/layers/play_forest/play_forest.json b/assets/layers/play_forest/play_forest.json index eed73151f8..5bbc4b5125 100644 --- a/assets/layers/play_forest/play_forest.json +++ b/assets/layers/play_forest/play_forest.json @@ -87,7 +87,6 @@ "render": "{reviews(name, play_forest)}" } ], - "hideUnderlayingFeaturesMinPercentage": 0, "hideFromOverview": false, "icon": { "render": "./assets/layers/play_forest/icon.svg" diff --git a/assets/layers/playground/playground.json b/assets/layers/playground/playground.json index ec97eca2bf..f8f4c0027a 100644 --- a/assets/layers/playground/playground.json +++ b/assets/layers/playground/playground.json @@ -437,7 +437,6 @@ "render": "{reviews(name, playground)}" } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/playgrounds/playground.svg" }, diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 1efe04b3a9..234e9d379f 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -430,5 +430,46 @@ ] }, "neededChangesets": 5 - } + }, + "filter": [ + { + "options": [ + { + "question": "Kinderboeken aanwezig?", + "osmTags": "books~.*children.*" + } + ] + }, + { + "options": [ + { + "question": "Boeken voor volwassenen aanwezig?", + "osmTags": "books~.*adults.*" + } + ] + }, + { + "options": [ + { + "question": "Binnen of buiten", + "osmTags": { + "and": [] + } + }, + { + "question": "Binnen?", + "osmTags": "indoor=yes" + }, + { + "question": "Buiten?", + "osmTags": { + "or": [ + "indoor=no", + "indoor=" + ] + } + } + ] + } + ] } \ No newline at end of file diff --git a/assets/layers/slow_roads/slow_roads.json b/assets/layers/slow_roads/slow_roads.json index 7ee9e2ba9c..9c7d6e237d 100644 --- a/assets/layers/slow_roads/slow_roads.json +++ b/assets/layers/slow_roads/slow_roads.json @@ -215,7 +215,6 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 0, "width": { "render": "7" }, diff --git a/assets/layers/sport_pitch/sport_pitch.json b/assets/layers/sport_pitch/sport_pitch.json index 140e222b2a..b871523f70 100644 --- a/assets/layers/sport_pitch/sport_pitch.json +++ b/assets/layers/sport_pitch/sport_pitch.json @@ -383,7 +383,6 @@ "render": "{reviews(name, sportpitch)}" } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "circle:white;./assets/layers/sport_pitch/sport_pitch.svg", "mappings": [ diff --git a/assets/layers/surveillance_camera/surveillance_camera.json b/assets/layers/surveillance_camera/surveillance_camera.json index 20f1e77bf0..7a6a4248c8 100644 --- a/assets/layers/surveillance_camera/surveillance_camera.json +++ b/assets/layers/surveillance_camera/surveillance_camera.json @@ -421,21 +421,20 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { - "render": "./assets/themes/surveillance_cameras/logo.svg", + "render": "./assets/themes/surveillance/logo.svg", "mappings": [ { "if": "camera:type=dome", - "then": "./assets/themes/surveillance_cameras/dome.svg" + "then": "./assets/themes/surveillance/dome.svg" }, { "if": "_direction:leftright=right", - "then": "./assets/themes/surveillance_cameras/cam_right.svg" + "then": "./assets/themes/surveillance/cam_right.svg" }, { "if": "_direction:leftright=left", - "then": "./assets/themes/surveillance_cameras/cam_left.svg" + "then": "./assets/themes/surveillance/cam_left.svg" } ] }, diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index a33ddf7423..cbdd19d286 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -8,6 +8,7 @@ "ru": "Туалеты", "it": "Servizi igienici" }, + "minzoom": 12, "source": { "osmTags": "amenity=toilets" }, @@ -37,8 +38,7 @@ "color": { "render": "#0000ff" }, - "minzoom": 12, - "wayHandling": 2, + "wayHandling": 1, "presets": [ { "title": { diff --git a/assets/layers/trail/license_info.json b/assets/layers/trail/license_info.json new file mode 100644 index 0000000000..54c6454691 --- /dev/null +++ b/assets/layers/trail/license_info.json @@ -0,0 +1,28 @@ +[ + { + "authors": [ + "Freepik" + ], + "path": "trail.svg", + "license": "CC-BY", + "sources": [ + "https://www.flaticon.com/free-icon/trail_3273172?term=trail&page=1&position=11&page=1&position=11&related_id=3273172&origin=search" + ] + }, + { + "authors": [ + "Freepik" + ], + "path": "pushchair.svg", + "license": "CC-BY", + "sources": [ + "https://www.flaticon.com/free-icon/stroller_1032915?term=pushchair&page=1&position=76&page=1&position=76&related_id=1032915&origin=search" + ] + }, + { + "authors": [], + "path": "wheelchair.svg", + "license": "CC0", + "sources": [] + } +] \ No newline at end of file diff --git a/assets/layers/trail/pushchair.svg b/assets/layers/trail/pushchair.svg new file mode 100644 index 0000000000..7684a8193a --- /dev/null +++ b/assets/layers/trail/pushchair.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/trail/trail.json b/assets/layers/trail/trail.json new file mode 100644 index 0000000000..4638de35ee --- /dev/null +++ b/assets/layers/trail/trail.json @@ -0,0 +1,207 @@ +{ + "id": "trail", + "name": { + "en": "Trails", + "nl": "Wandeltochten" + }, + "minzoom": 12, + "source": { + "osmTags": { + "and": [ + { + "or": [ + "route=hiking", + "route=bycicle", + "route=horse" + ] + } + ] + } + }, + "title": { + "render": { + "en": "Trail", + "nl": "Wandeltocht" + }, + "mappings": [ + { + "if": "name~*", + "then": "{name}" + } + ] + }, + "tagRenderings": [ + "images", + { + "render": { + "en": "The trail is {_length:km} kilometers long", + "nl": "Deze wandeling is {_length:km} kilometer lang" + } + }, + { + "#": "Name", + "question": { + "nl": "Wat is de naam van deze wandeling?" + }, + "render": { + "nl": "Deze wandeling heet {name}" + }, + "freeform": { + "key": "name" + } + }, + { + "#": "Operator tag", + "render": { + "nl": "Beheer door {operator}" + }, + "question": { + "nl": "Wie beheert deze wandeltocht?" + }, + "freeform": { + "key": "operator" + }, + "mappings": [ + { + "if": { + "and": [ + "operator=Natuurpunt" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door Natuurpunt" + } + }, + { + "if": { + "and": [ + "operator~(n|N)atuurpunt.*" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door {operator}" + }, + "hideInAnswer": true + } + ] + }, + { + "#": "Color", + "question": { + "nl": "Welke kleur heeft deze wandeling?" + }, + "render": { + "nl": "Deze wandeling heeft kleur {colour}" + }, + "freeform": { + "key": "colour", + "type": "color" + }, + "mappings": [ + { + "if": "colour=blue", + "then": { + "nl": "Blauwe wandeling", + "en": "Blue trail" + } + }, + { + "if": "colour=red", + "then": { + "nl": "Rode wandeling", + "en": "Red trail" + } + }, + { + "if": "colour=green", + "then": { + "nl": "Groene wandeling", + "en": "Green trail" + } + }, + { + "if": "colour=yellow", + "then": { + "nl": "Gele wandeling", + "en": "Yellow trail" + } + } + ] + }, + { + "#": "Wheelchair access", + "question": { + "nl": "Is deze wandeling toegankelijk met de rolstoel?" + }, + "mappings": [ + { + "then": { + "nl": "deze wandeltocht is toegankelijk met de rolstoel" + }, + "if": "wheelchair=yes" + }, + { + "then": { + "nl": "deze wandeltocht is niet toegankelijk met de rolstoel" + }, + "if": "wheelchair=no" + } + ] + }, + { + "#": "pushchair access", + "question": { + "nl": "Is deze wandeltocht toegankelijk met de buggy?" + }, + "mappings": [ + { + "then": { + "nl": "deze wandeltocht is toegankelijk met de buggy" + }, + "if": "pushchair=yes" + }, + { + "then": { + "nl": "deze wandeltocht is niet toegankelijk met de buggy" + }, + "if": "pushchair=no" + } + ] + } + ], + "icon": { + "render": "./assets/layers/trail/trail.svg", + "mappings": [ + { + "if": "wheelchair=yes", + "then": "./assets/layers/trail/wheelchair.svg" + }, + { + "if": "pushchair=yes", + "then": "./assets/layers/trail/pushchair.svg" + } + ] + }, + "description": { + "nl": "Aangeduide wandeltochten" + }, + "wayHandling": 0, + "width": { + "render": "3" + }, + "iconSize": { + "render": "35,35,center" + }, + "color": { + "render": "#335D9F", + "mappings": [ + { + "if": "colour~*", + "then": "{colour}" + } + ] + }, + "dashArray": { + "render": "5 5" + } +} \ No newline at end of file diff --git a/assets/layers/trail/trail.svg b/assets/layers/trail/trail.svg new file mode 100644 index 0000000000..62ec11778e --- /dev/null +++ b/assets/layers/trail/trail.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/layers/trail/wheelchair.svg b/assets/layers/trail/wheelchair.svg new file mode 100644 index 0000000000..2c3a2ecd7e --- /dev/null +++ b/assets/layers/trail/wheelchair.svg @@ -0,0 +1,73 @@ + + + +image/svg+xml + + + \ No newline at end of file diff --git a/assets/layers/tree_node/tree_node.json b/assets/layers/tree_node/tree_node.json index 1f3cc7fdeb..432e3c634c 100644 --- a/assets/layers/tree_node/tree_node.json +++ b/assets/layers/tree_node/tree_node.json @@ -449,7 +449,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "circle:#ffffff;./assets/themes/trees/unknown.svg", "mappings": [ diff --git a/assets/layers/viewpoint/viewpoint.json b/assets/layers/viewpoint/viewpoint.json index c47aa23468..5c77ff8fd4 100644 --- a/assets/layers/viewpoint/viewpoint.json +++ b/assets/layers/viewpoint/viewpoint.json @@ -5,7 +5,8 @@ "nl": "Uitzicht", "de": "Aussichtspunkt", "fr": "Point de vue", - "it": "Punto panoramico" + "it": "Punto panoramico", + "ru": "Смотровая площадка" }, "description": { "en": "A nice viewpoint or nice view. Ideal to add an image if no other category fits", diff --git a/assets/layers/visitor_information_centre/information.svg b/assets/layers/visitor_information_centre/information.svg new file mode 100644 index 0000000000..3a43f1128d --- /dev/null +++ b/assets/layers/visitor_information_centre/information.svg @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/visitor_information_centre/license_info.json b/assets/layers/visitor_information_centre/license_info.json new file mode 100644 index 0000000000..8e1ac5bb9c --- /dev/null +++ b/assets/layers/visitor_information_centre/license_info.json @@ -0,0 +1,12 @@ +[ + { + "authors": [ + "Bobarino" + ], + "path": "information.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Information.svg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/visitor_information_centre/visitor_information_centre.json b/assets/layers/visitor_information_centre/visitor_information_centre.json new file mode 100644 index 0000000000..10c3cd0de1 --- /dev/null +++ b/assets/layers/visitor_information_centre/visitor_information_centre.json @@ -0,0 +1,65 @@ +{ + "id": "visitor_information_centre", + "name": { + "en": "Visitor Information Centre", + "nl": "Bezoekerscentrum" + }, + "minzoom": 12, + "source": { + "osmTags": { + "and": [ + { + "or": [ + "information=visitor_centre", + "information=office" + ] + } + ] + } + }, + "title": { + "render": { + "nl": "{name}", + "en": "{name}" + }, + "mappings": [ + { + "if": { + "and": [ + "name:nl~*" + ] + }, + "then": { + "nl": "{name:nl}" + } + }, + { + "if": { + "and": [ + "name~*" + ] + }, + "then": { + "nl": "{name}", + "en": "{name}" + } + } + ] + }, + "description": { + "en": "A visitor center offers information about a specific attraction or place of interest where it is located.", + "nl": "Een bezoekerscentrum biedt informatie over een specifieke attractie of bezienswaardigheid waar het is gevestigd." + }, + "tagRenderings": [], + "icon": { + "render": "./assets/layers/visitor_information_centre/information.svg" + }, + "iconSize": { + "render": "40,40,center" + }, + "color": { + "render": "#E64C00" + }, + "presets": [], + "wayHandling": 1 +} \ No newline at end of file diff --git a/assets/layers/watermill/license_info.json b/assets/layers/watermill/license_info.json new file mode 100644 index 0000000000..44bc785297 --- /dev/null +++ b/assets/layers/watermill/license_info.json @@ -0,0 +1,8 @@ +[ + { + "authors": [], + "path": "watermill.svg", + "license": "CC0", + "sources": [] + } +] \ No newline at end of file diff --git a/assets/layers/watermill/watermill.json b/assets/layers/watermill/watermill.json new file mode 100644 index 0000000000..029c14b759 --- /dev/null +++ b/assets/layers/watermill/watermill.json @@ -0,0 +1,186 @@ +{ + "id": "watermill", + "name": { + "nl": "watermolens" + }, + "minzoom": 12, + "source": { + "osmTags": { + "and": [ + "man_made=watermill" + ] + } + }, + "title": { + "render": { + "nl": "Watermolens" + }, + "mappings": [ + { + "if": { + "and": [ + "name:nl~*" + ] + }, + "then": { + "nl": "{name:nl}" + } + }, + { + "if": { + "and": [ + "name~*" + ] + }, + "then": { + "nl": "{name}" + } + } + ] + }, + "description": { + "nl": "Watermolens" + }, + "tagRenderings": [ + "images", + { + "#": "Access tag", + "render": { + "nl": "De toegankelijkheid van dit gebied is: {access:description}" + }, + "question": { + "nl": "Is dit gebied toegankelijk?" + }, + "freeform": { + "key": "access:description" + }, + "mappings": [ + { + "if": { + "and": [ + "access=yes", + "fee=" + ] + }, + "then": { + "nl": "Vrij toegankelijk" + } + }, + { + "if": { + "and": [ + "access=no", + "fee=" + ] + }, + "then": { + "nl": "Niet toegankelijk" + } + }, + { + "if": { + "and": [ + "access=private", + "fee=" + ] + }, + "then": { + "nl": "Niet toegankelijk, want privégebied" + } + }, + { + "if": { + "and": [ + "access=permissive", + "fee=" + ] + }, + "then": { + "nl": "Toegankelijk, ondanks dat het privegebied is" + } + }, + { + "if": { + "and": [ + "access=guided", + "fee=" + ] + }, + "then": { + "nl": "Enkel toegankelijk met een gids of tijdens een activiteit" + } + }, + { + "if": { + "and": [ + "access=yes", + "fee=yes" + ] + }, + "then": { + "nl": "Toegankelijk mits betaling" + } + } + ] + }, + { + "#": "Operator tag", + "render": { + "nl": "Beheer door {operator}" + }, + "question": { + "nl": "Wie beheert dit pad?" + }, + "freeform": { + "key": "operator" + }, + "mappings": [ + { + "if": { + "and": [ + "operator=Natuurpunt" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door Natuurpunt" + } + }, + { + "if": { + "and": [ + "operator~(n|N)atuurpunt.*" + ] + }, + "then": { + "nl": "Dit gebied wordt beheerd door {operator}" + }, + "hideInAnswer": true + } + ] + } + ], + "wayHandling": 1, + "icon": { + "render": "./assets/layers/watermill/watermill.svg" + }, + "iconSize": { + "render": "50,50,center" + }, + "color": { + "render": "#FFC0CB" + }, + "presets": [ + { + "tags": [ + "man_made=watermill", + "fixme=Toegevoegd met MapComplete, geometry nog uit te tekenen" + ], + "title": { + "nl": "Paden" + }, + "description": { + "nl": "Voeg een ontbrekend, erkend pad toe." + } + } + ] +} \ No newline at end of file diff --git a/assets/layers/watermill/watermill.svg b/assets/layers/watermill/watermill.svg new file mode 100644 index 0000000000..05528d3c4d --- /dev/null +++ b/assets/layers/watermill/watermill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/arrow-download.svg b/assets/svg/arrow-download.svg new file mode 100644 index 0000000000..52d29b4338 --- /dev/null +++ b/assets/svg/arrow-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/arrow-left-thin.svg b/assets/svg/arrow-left-thin.svg new file mode 100644 index 0000000000..e4329b6151 --- /dev/null +++ b/assets/svg/arrow-left-thin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/barrier.svg b/assets/svg/barrier.svg new file mode 100644 index 0000000000..08bb800f50 --- /dev/null +++ b/assets/svg/barrier.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/checkbox-empty.svg b/assets/svg/checkbox-empty.svg new file mode 100644 index 0000000000..e4a9dc8663 --- /dev/null +++ b/assets/svg/checkbox-empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/checkbox-filled.svg b/assets/svg/checkbox-filled.svg new file mode 100644 index 0000000000..166f917855 --- /dev/null +++ b/assets/svg/checkbox-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/copyright.svg b/assets/svg/copyright.svg new file mode 100644 index 0000000000..bfde059802 --- /dev/null +++ b/assets/svg/copyright.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/svg/crosshair-blue-center.svg b/assets/svg/crosshair-blue-center.svg index 55996b6306..2e187d424e 100644 --- a/assets/svg/crosshair-blue-center.svg +++ b/assets/svg/crosshair-blue-center.svg @@ -1,6 +1,3 @@ - - - + + \ No newline at end of file diff --git a/assets/svg/filter.svg b/assets/svg/filter.svg new file mode 100644 index 0000000000..a6783b2484 --- /dev/null +++ b/assets/svg/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index 4e1b331445..0ea2d3a2dc 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -57,6 +57,686 @@ "license": "CC0", "sources": [] }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "download.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "copyright.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "arrow-download.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "ampersand.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "arrow-left-smooth.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "arrow-right-smooth.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "back.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Github" + ], + "path": "bug.svg", + "license": "MIT", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Octicons-bug.svg", + " https://github.com/primer/octicons" + ] + }, + { + "path": "camera-plus.svg", + "license": "CC-BY-SA 3.0", + "authors": [ + "Dave Gandy", + "Pieter Vander Vennet" + ], + "sources": [ + "https://fontawesome.com/", + "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" + ] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "checkmark.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "circle.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "clock.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "close.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "compass.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "cross_bottom_right.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "crosshair-blue-center.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "crosshair-blue.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "crosshair.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "Dave Gandy" + ], + "path": "delete_icon.svg", + "license": "CC-BY-SA", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Trash_font_awesome.svg\rT" + ] + }, + { + "authors": [], + "path": "direction.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "direction_gradient.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "down.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "envelope.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "The Tango Desktop Project" + ], + "path": "floppy.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Media-floppy.svg", + " http://tango.freedesktop.org/Tango_Desktop_Project" + ] + }, + { + "authors": [], + "path": "gear.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "help.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "Timothy Miller" + ], + "path": "home.svg", + "license": "CC-BY-SA 3.0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Home-icon.svg" + ] + }, + { + "authors": [ + "Timothy Miller" + ], + "path": "home_white_bg.svg", + "license": "CC-BY-SA 3.0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Home-icon.svg" + ] + }, + { + "authors": [ + "JOSM Team" + ], + "path": "josm_logo.svg", + "license": "CC0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:JOSM_Logotype_2019.svg", + " https://josm.openstreetmap.de/" + ] + }, + { + "authors": [], + "path": "layers.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "layersAdd.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "path": "Ornament-Horiz-0.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-1.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-2.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-3.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-4.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-5.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "path": "Ornament-Horiz-6.svg", + "license": "CC-BY", + "authors": [ + "Nightwolfdezines" + ], + "sources": [ + "https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes" + ] + }, + { + "authors": [], + "path": "star.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "star_outline.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "star_half.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "star_outline_half.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "logo.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "logout.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet", + " OSM" + ], + "path": "mapcomplete_logo.svg", + "license": "Logo; CC-BY-SA", + "sources": [ + "https://mapcomplete.osm.be" + ] + }, + { + "authors": [ + "Mapillary" + ], + "path": "mapillary.svg", + "license": "Logo; All rights reserved", + "sources": [ + "https://mapillary.com/" + ] + }, + { + "authors": [], + "path": "min.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "no_checkmark.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "or.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "osm-copyright.svg", + "license": "logo; all rights reserved", + "sources": [ + "https://www.OpenStreetMap.org" + ] + }, + { + "authors": [ + "OpenStreetMap U.S. Chapter" + ], + "path": "osm-logo-us.svg", + "license": "Logo", + "sources": [ + "https://www.openstreetmap.us/" + ] + }, + { + "authors": [], + "path": "osm-logo.svg", + "license": "logo; all rights reserved", + "sources": [ + "https://www.OpenStreetMap.org" + ] + }, + { + "authors": [ + "GitHub Octicons" + ], + "path": "pencil.svg", + "license": "MIT", + "sources": [ + "https://github.com/primer/octicons", + " https://commons.wikimedia.org/wiki/File:Octicons-pencil.svg" + ] + }, + { + "authors": [ + "@ tyskrat" + ], + "path": "phone.svg", + "license": "CC-BY 3.0", + "sources": [ + "https://www.onlinewebfonts.com/icon/1059" + ] + }, + { + "authors": [], + "path": "pin.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "plus.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "@fatih" + ], + "path": "pop-out.svg", + "license": "CC-BY 3.0", + "sources": [ + "https://www.onlinewebfonts.com/icon/2151" + ] + }, + { + "authors": [], + "path": "reload.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "ring.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "OOjs UI Team and other contributors" + ], + "path": "search.svg", + "license": "MIT", + "sources": [ + "https://commons.wikimedia.org/wiki/File:OOjs_UI_indicator_search-rtl.svg", + "https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/AUTHORS.txt" + ] + }, + { + "authors": [], + "path": "send_email.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "share.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [], + "path": "square.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "@felpgrc" + ], + "path": "statistics.svg", + "license": "CC-BY 3.0", + "sources": [ + "https://www.onlinewebfonts.com/icon/197818" + ] + }, + { + "authors": [ + "MGalloway (WMF)" + ], + "path": "translate.svg", + "license": "CC-BY-SA 3.0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg" + ] + }, + { + "authors": [], + "path": "up.svg", + "license": "CC0; trivial", + "sources": [] + }, + { + "authors": [ + "Wikidata" + ], + "path": "wikidata.svg", + "license": "Logo; All rights reserved", + "sources": [ + "https://www.wikidata.org" + ] + }, + { + "authors": [ + "Wikimedia" + ], + "path": "wikimedia-commons-white.svg", + "license": "Logo; All rights reserved", + "sources": [ + "https://commons.wikimedia.org" + ] + }, + { + "authors": [ + "Wikipedia" + ], + "path": "wikipedia.svg", + "license": "Logo; All rights reserved", + "sources": [ + "https://www.wikipedia.org/" + ] + }, + { + "authors": [ + "Mapillary" + ], + "path": "mapillary_black.svg", + "license": "Logo; All rights reserved", + "sources": [ + "https://www.mapillary.com/" + ] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "location.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pol Labaut" + ], + "path": "location-circle.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pol Labaut" + ], + "path": "location-empty.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "min-zoom.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "plus-zoom.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "filter.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "checkbox-empty.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "checkbox-filled.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Hannah Declerck" + ], + "path": "arrow-left-thin.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "direction_masked.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "direction_outline.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "direction_stroke.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "SocialImageForeground.svg", + "license": "CC-BY-SA", + "sources": [ + "https://mapcomplete.osm.be" + ] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "add.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "addSmall.svg", + "license": "CC0", + "sources": [] + }, { "authors": [], "path": "ampersand.svg", @@ -87,6 +767,16 @@ "license": "CC0", "sources": [] }, + { + "authors": [ + "Tobias Zwick" + ], + "path": "barrier.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/quest%20icons/barrier.svg" + ] + }, { "authors": [ "Github" @@ -612,5 +1302,15 @@ "sources": [ "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" ] + }, + { + "authors": [ + "The noun project - Basith Ibrahi" + ], + "path": "scissors.svg", + "license": "CC-BY 3.0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" + ] } ] \ No newline at end of file diff --git a/assets/svg/location-circle.svg b/assets/svg/location-circle.svg new file mode 100644 index 0000000000..6689a4bdfa --- /dev/null +++ b/assets/svg/location-circle.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/assets/svg/location-empty.svg b/assets/svg/location-empty.svg new file mode 100644 index 0000000000..d95789ccca --- /dev/null +++ b/assets/svg/location-empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/location.svg b/assets/svg/location.svg new file mode 100644 index 0000000000..3c4d168d6f --- /dev/null +++ b/assets/svg/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/min-zoom.svg b/assets/svg/min-zoom.svg new file mode 100644 index 0000000000..f617af1970 --- /dev/null +++ b/assets/svg/min-zoom.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/plus-zoom.svg b/assets/svg/plus-zoom.svg new file mode 100644 index 0000000000..fc9ff3c806 --- /dev/null +++ b/assets/svg/plus-zoom.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/svg/scissors.svg b/assets/svg/scissors.svg new file mode 100644 index 0000000000..6868fe515f --- /dev/null +++ b/assets/svg/scissors.svg @@ -0,0 +1,62 @@ + +image/svg+xml \ No newline at end of file diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 66d034cdc3..85cb3ebb28 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -18,7 +18,8 @@ "ru": "Какой номер телефона у {name}?", "sv": "Vad är telefonnumret till {name}?", "zh_Hant": "{name} 的電話號碼是什麼?", - "it": "Qual è il numero di telefono di {name}?" + "it": "Qual è il numero di telefono di {name}?", + "pt_BR": "Qual o número de telefone de {name}?" }, "render": "{phone}", "freeform": { @@ -49,7 +50,9 @@ "ru": "Какой адрес электронной почты у {name}?", "id": "Apa alamat surel dari {name}?", "zh_Hant": "{name} 的電子郵件地址是什麼?", - "it": "Qual è l'indirizzo email di {name}?" + "it": "Qual è l'indirizzo email di {name}?", + "de": "Was ist die Mail-Adresse von {name}?", + "pt_BR": "Qual o endereço de e-mail de {name}?" }, "freeform": { "key": "email", @@ -66,7 +69,9 @@ "ru": "Какой сайт у {name}?", "id": "Apa situs web dari {name}?", "zh_Hant": "{name} 網址是什麼?", - "it": "Qual è il sito web di {name}?" + "it": "Qual è il sito web di {name}?", + "de": "Was ist die Website von {name}?", + "pt_BR": "Qual o site de {name}?" }, "render": "{website}", "freeform": { @@ -82,7 +87,8 @@ "nb_NO": "Er det noe mer som er relevant du ikke kunne opplyse om i tidligere svar? Legg det til her.
Ikke gjenta fakta som allerede er nevnt", "ru": "Есть ли еще что-то важное, о чем вы не смогли рассказать в предыдущих вопросах? Добавьте это здесь.
Не повторяйте уже изложенные факты", "zh_Hant": "有什麼相關的資訊你無法在先前的問題回應的嗎?請加在這邊吧。
不要重覆答覆已經知道的事情", - "it": "C'è ancora qualche informazione importante che non è stato possibile fornire nelle domande precedenti? Aggiungila qui.
Non ripetere informazioni già fornite" + "it": "C'è ancora qualche informazione importante che non è stato possibile fornire nelle domande precedenti? Aggiungila qui.
Non ripetere informazioni già fornite", + "de": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.
Bitte keine bereits erhobenen Informationen." }, "render": "{description}", "freeform": { @@ -98,7 +104,8 @@ "nb_NO": "Hva er åpningstidene for {name})", "ru": "Какое время работы у {name}?", "zh_Hant": "{name} 的開放時間是什麼?", - "it": "Quali sono gli orari di apertura di {name}?" + "it": "Quali sono gli orari di apertura di {name}?", + "pt_BR": "Qual o horário de funcionamento de {name}?" }, "render": { "de": "

Öffnungszeiten

{opening_hours_table(opening_hours)}", @@ -127,11 +134,15 @@ "level": { "question": { "nl": "Op welke verdieping bevindt dit punt zich?", - "en": "On what level is this feature located?" + "en": "On what level is this feature located?", + "de": "In welchem Stockwerk befindet sich dieses Objekt?" }, "render": { "en": "Located on the {level}th floor", - "nl": "Bevindt zich op de {level}de verdieping" + "nl": "Bevindt zich op de {level}de verdieping", + "de": "Befindet sich im {level}ten Stock", + "pt_BR": "Localizado no {level}o andar", + "ru": "Расположено на {level}ом этаже" }, "freeform": { "key": "level", @@ -142,7 +153,9 @@ "if": "location=underground", "then": { "en": "Located underground", - "nl": "Bevindt zich ondergronds" + "nl": "Bevindt zich ondergronds", + "pt_BR": "Localizado no subsolo", + "ru": "Расположено под землей" }, "hideInAnswer": true }, @@ -150,14 +163,20 @@ "if": "level=0", "then": { "en": "Located on the ground floor", - "nl": "Bevindt zich gelijkvloers" + "nl": "Bevindt zich gelijkvloers", + "de": "Ist im Erdgeschoss", + "pt_BR": "Localizado no térreo", + "ru": "Расположено на первом этаже" } }, { "if": "level=1", "then": { "en": "Located on the first floor", - "nl": "Bevindt zich op de eerste verdieping" + "nl": "Bevindt zich op de eerste verdieping", + "de": "Ist im ersten Stock", + "pt_BR": "Localizado no primeiro andar", + "ru": "Расположено на первом этаже" } } ] diff --git a/assets/themes/.DS_Store b/assets/themes/.DS_Store new file mode 100644 index 0000000000..7d02d7d4e6 Binary files /dev/null and b/assets/themes/.DS_Store differ diff --git a/assets/themes/artwork/artwork.json b/assets/themes/artwork/artwork.json index c3b7b7c552..feb957abc9 100644 --- a/assets/themes/artwork/artwork.json +++ b/assets/themes/artwork/artwork.json @@ -1,5 +1,5 @@ { - "id": "artworks", + "id": "artwork", "version": "2020-08-30", "title": { "en": "Open Artwork Map", diff --git a/assets/themes/bicycle_library/bicycle_library.json b/assets/themes/bicyclelib/bicyclelib.json similarity index 98% rename from assets/themes/bicycle_library/bicycle_library.json rename to assets/themes/bicyclelib/bicyclelib.json index 6d0cf61e2b..1871153ea2 100644 --- a/assets/themes/bicycle_library/bicycle_library.json +++ b/assets/themes/bicyclelib/bicyclelib.json @@ -33,7 +33,7 @@ "fr": "Une vélothèque est un endroit où on peut emprunter des vélos, souvent moyennant une petite somme annuelle. Un cas d'utilisation notable est celui des vélothèques pour les enfants, qui leur permettent de passer à un vélo plus grand quand ils sont trop grands pour leur vélo actuel", "zh_Hant": "單車圖書館是指每年支付小額費用,然後可以租用單車的地方。最有名的單車圖書館案例是給小孩的,能夠讓長大的小孩用目前的單車換成比較大的單車" }, - "icon": "./assets/themes/bicycle_library/logo.svg", + "icon": "./assets/themes/bicyclelib/logo.svg", "socialImage": null, "startLat": 0, "startLon": 0, diff --git a/assets/themes/bicycle_library/license_info.json b/assets/themes/bicyclelib/license_info.json similarity index 100% rename from assets/themes/bicycle_library/license_info.json rename to assets/themes/bicyclelib/license_info.json diff --git a/assets/themes/bicycle_library/logo.svg b/assets/themes/bicyclelib/logo.svg similarity index 100% rename from assets/themes/bicycle_library/logo.svg rename to assets/themes/bicyclelib/logo.svg diff --git a/assets/themes/bookcases/Bookcases.json b/assets/themes/bookcases/bookcases.json similarity index 100% rename from assets/themes/bookcases/Bookcases.json rename to assets/themes/bookcases/bookcases.json diff --git a/assets/themes/buurtnatuur/buurtnatuur.json b/assets/themes/buurtnatuur/buurtnatuur.json index a6e6cc9e4d..9c82e4d443 100644 --- a/assets/themes/buurtnatuur/buurtnatuur.json +++ b/assets/themes/buurtnatuur/buurtnatuur.json @@ -29,7 +29,7 @@ "socialImage": "./assets/themes/buurtnatuur/social_image.jpg", "layers": [ { - "id": "nature_reserve", + "id": "nature_reserve_buurtnatuur", "name": { "nl": "Natuurgebied" }, @@ -141,7 +141,7 @@ } }, "calculatedTags": [ - "_overlapWithUpperLayers=Math.max(...feat.overlapWith('nature_reserve').map(o => o.overlap))/feat.area", + "_overlapWithUpperLayers=Math.max(...feat.overlapWith('nature_reserve_buurtnatuur').map(o => o.overlap))/feat.area", "_tooMuchOverlap=Number(feat.properties._overlapWithUpperLayers) > 0.1 ? 'yes' :'no'" ], "isShown": { @@ -240,7 +240,7 @@ } }, "calculatedTags": [ - "_overlapWithUpperLayers=Math.max(...feat.overlapWith('parks','nature_reserve').map(o => o.overlap))/feat.area", + "_overlapWithUpperLayers=Math.max(...feat.overlapWith('parks','nature_reserve_buurtnatuur').map(o => o.overlap))/feat.area", "_tooMuchOverlap=Number(feat.properties._overlapWithUpperLayers) > 0.1 ? 'yes' : 'no'" ], "isShown": { diff --git a/assets/themes/campersites/Barßel_Wohnmobilstellplatz.jpg b/assets/themes/campersite/Barßel_Wohnmobilstellplatz.jpg similarity index 100% rename from assets/themes/campersites/Barßel_Wohnmobilstellplatz.jpg rename to assets/themes/campersite/Barßel_Wohnmobilstellplatz.jpg diff --git a/assets/themes/campersites/campersites.json b/assets/themes/campersite/campersite.json similarity index 98% rename from assets/themes/campersites/campersites.json rename to assets/themes/campersite/campersite.json index cc248f2780..d458d80859 100644 --- a/assets/themes/campersites/campersites.json +++ b/assets/themes/campersite/campersite.json @@ -38,13 +38,13 @@ "nb_NO" ], "maintainer": "joost schouppe", - "icon": "./assets/themes/campersites/caravan.svg", + "icon": "./assets/themes/campersite/caravan.svg", "version": "0", "startLat": 43.14, "startLon": 3.14, "startZoom": 14, "widenFactor": 0.05, - "socialImage": "./assets/themes/campersites/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", + "socialImage": "./assets/themes/campersite/Bar%C3%9Fel_Wohnmobilstellplatz.jpg", "layers": [ { "id": "caravansites", @@ -521,9 +521,8 @@ "questions", "reviews" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { - "render": "circle:white;./assets/themes/campersites/caravan.svg", + "render": "circle:white;./assets/themes/campersite/caravan.svg", "mappings": [ { "if": { @@ -531,7 +530,7 @@ "fee=no" ] }, - "then": "circle:white;./assets/themes/campersites/caravan_green.svg" + "then": "circle:white;./assets/themes/campersite/caravan_green.svg" } ] }, @@ -862,9 +861,8 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { - "render": "circle:white;./assets/themes/campersites/sanitary_dump_station.svg" + "render": "circle:white;./assets/themes/campersite/sanitary_dump_station.svg" }, "width": { "render": "8" diff --git a/assets/themes/campersites/caravan.svg b/assets/themes/campersite/caravan.svg similarity index 100% rename from assets/themes/campersites/caravan.svg rename to assets/themes/campersite/caravan.svg diff --git a/assets/themes/campersites/caravan_green.svg b/assets/themes/campersite/caravan_green.svg similarity index 100% rename from assets/themes/campersites/caravan_green.svg rename to assets/themes/campersite/caravan_green.svg diff --git a/assets/themes/campersites/license_info.json b/assets/themes/campersite/license_info.json similarity index 100% rename from assets/themes/campersites/license_info.json rename to assets/themes/campersite/license_info.json diff --git a/assets/themes/campersites/sanitary_dump_station.svg b/assets/themes/campersite/sanitary_dump_station.svg similarity index 100% rename from assets/themes/campersites/sanitary_dump_station.svg rename to assets/themes/campersite/sanitary_dump_station.svg diff --git a/assets/themes/charging_stations/charging_stations.json b/assets/themes/charging_stations/charging_stations.json index 16bc127864..3fe101f089 100644 --- a/assets/themes/charging_stations/charging_stations.json +++ b/assets/themes/charging_stations/charging_stations.json @@ -307,7 +307,6 @@ ] } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "pin:#fff;./assets/themes/charging_stations/plug.svg", "mappings": [ diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index fbf13310c3..7b4cc30bc9 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -140,7 +140,6 @@ "phone", "opening_hours" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/climbing/club.svg" }, @@ -280,7 +279,6 @@ "opening_hours", "reviews" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/climbing/climbing_gym.svg" }, @@ -471,7 +469,6 @@ }, "reviews" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "circle:white;./assets/themes/climbing/climbing_route.svg" }, @@ -695,7 +692,6 @@ }, "reviews" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/climbing/climbing_no_rope.svg" }, @@ -850,7 +846,6 @@ } ], "icon": "./assets/themes/climbing/climbing_unknown.svg", - "hideUnderlayingFeaturesMinPercentage": 0, "width": { "render": "2" }, diff --git a/assets/themes/cycle_infra/Belgian_road_sign_D07.svg b/assets/themes/cycle_infra/Belgian_road_sign_D07.svg new file mode 100644 index 0000000000..5a831ed93f --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_road_sign_D07.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/cycle_infra/Belgian_road_sign_D09.svg b/assets/themes/cycle_infra/Belgian_road_sign_D09.svg new file mode 100644 index 0000000000..154a21600e --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_road_sign_D09.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/cycle_infra/Belgian_road_sign_D10.svg b/assets/themes/cycle_infra/Belgian_road_sign_D10.svg new file mode 100644 index 0000000000..03549e2866 --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_road_sign_D10.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M13.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M13.svg new file mode 100644 index 0000000000..abdbfe8b2a --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M13.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M14.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M14.svg new file mode 100644 index 0000000000..5f0c40bda6 --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M14.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M15.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M15.svg new file mode 100644 index 0000000000..470c6aadd3 --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M15.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M16.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M16.svg new file mode 100644 index 0000000000..5933626229 --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M16.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M6.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M6.svg new file mode 100644 index 0000000000..3d4124992a --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M6.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Belgian_traffic_sign_M7.svg b/assets/themes/cycle_infra/Belgian_traffic_sign_M7.svg new file mode 100644 index 0000000000..f72de9e965 --- /dev/null +++ b/assets/themes/cycle_infra/Belgian_traffic_sign_M7.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/cycle_infra/Cycle_barrier_double.png b/assets/themes/cycle_infra/Cycle_barrier_double.png new file mode 100644 index 0000000000..76b51a2e65 Binary files /dev/null and b/assets/themes/cycle_infra/Cycle_barrier_double.png differ diff --git a/assets/themes/cycle_infra/Cycle_barrier_single.png b/assets/themes/cycle_infra/Cycle_barrier_single.png new file mode 100644 index 0000000000..ec957da7c5 Binary files /dev/null and b/assets/themes/cycle_infra/Cycle_barrier_single.png differ diff --git a/assets/themes/cycle_infra/Cycle_barrier_squeeze.png b/assets/themes/cycle_infra/Cycle_barrier_squeeze.png new file mode 100644 index 0000000000..a5ba6c7ed2 Binary files /dev/null and b/assets/themes/cycle_infra/Cycle_barrier_squeeze.png differ diff --git a/assets/themes/cycle_infra/Cycle_barrier_triple.png b/assets/themes/cycle_infra/Cycle_barrier_triple.png new file mode 100644 index 0000000000..b64f30c4fe Binary files /dev/null and b/assets/themes/cycle_infra/Cycle_barrier_triple.png differ diff --git a/assets/themes/cycle_infra/bicycleway.svg b/assets/themes/cycle_infra/bicycleway.svg new file mode 100644 index 0000000000..dbbc55590a --- /dev/null +++ b/assets/themes/cycle_infra/bicycleway.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/themes/cycle_infra/cycle-infra.svg b/assets/themes/cycle_infra/cycle-infra.svg new file mode 100644 index 0000000000..8d82097bee --- /dev/null +++ b/assets/themes/cycle_infra/cycle-infra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/cycle_infra/cycle_infra.json b/assets/themes/cycle_infra/cycle_infra.json new file mode 100644 index 0000000000..8d4183a7c7 --- /dev/null +++ b/assets/themes/cycle_infra/cycle_infra.json @@ -0,0 +1,1551 @@ +{ + "id": "cycle_infra", + "title": { + "en": "Bicycle infrastructure", + "nl": "Fietsinfrastructuur" + }, + "shortDescription": { + "en": "A map where you can view and edit things related to the bicycle infrastructure.", + "nl": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken." + }, + "description": { + "en": "A map where you can view and edit things related to the bicycle infrastructure. Made during #osoc21.", + "nl": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken. Gemaakt tijdens #osoc21." + }, + "language": [ + "en", + "nl" + ], + "maintainer": "", + "defaultBackgroundId": "CartoDB.Voyager", + "icon": "./assets/themes/cycle_infra/cycle-infra.svg", + "version": "0", + "startLat": 51, + "startLon": 3.75, + "startZoom": 11, + "widenFactor": 0.05, + "socialImage": "./assets/themes/cycle_infra/cycle-infra.svg", + "enableExportButton": true, + "layers": [ + { + "id": "cycleways", + "name": { + "en": "Cycleways", + "nl": "Fietspaden" + }, + "minzoom": 14, + "source": { + "osmTags": { + "or": [ + "highway=cycleway", + "cycleway=lane", + "cycleway=shared_lane", + "cycleway=track", + "cyclestreet=yes", + { + "and": [ + "highway=path", + "bicycle=designated" + ] + } + ] + } + }, + "calculatedTags": [ + "_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')" + ], + "title": { + "render": { + "en": "Cycleways", + "nl": "Fietspaden" + }, + "mappings": [ + { + "if": { + "or": [ + "highway=cycleway", + "highway=path" + ] + }, + "then": { + "nl": "Fietsweg", + "en": "Cycleway" + } + }, + { + "if": "cycleway=shared_lane", + "then": { + "nl": "Fietssuggestiestrook", + "en": "Shared lane" + } + }, + { + "if": "cycleway=lane", + "then": { + "nl": "Fietsstrook", + "en": "Bike lane" + } + }, + { + "if": "cycleway=track", + "then": { + "en": "Cycleway next to the road", + "nl": "Fietsweg naast de weg" + } + }, + { + "if": "cyclestreet=yes", + "then": { + "nl": "Fietsstraat", + "en": "Cyclestreet" + } + } + ] + }, + "description": {}, + "tagRenderings": [ + { + "question": { + "en": "What kind of cycleway is there?", + "nl": "Wat voor fietspad is hier?" + }, + "condition": { + "and": [ + "highway!=cycleway", + "highway!=path" + ] + }, + "mappings": [ + { + "if": "cycleway=shared_lane", + "then": { + "en": "There is a shared lane", + "nl": "Er is een fietssuggestiestrook" + } + }, + { + "if": "cycleway=lane", + "then": { + "en": "There is a lane next to the road (seperated with paint)", + "nl": "Er is een fietspad aangrenzend aan de weg (gescheiden met verf)" + } + }, + { + "if": "cycleway=track", + "then": { + "en": "There is a track, but no cycleway drawn seperately from this road on the map.", + "nl": "Er is een fietspad (los van de weg), maar geen fietspad afzonderlijk getekend naast deze weg." + } + }, + { + "if": "cycleway=seperate", + "then": { + "en": "There is a seperately drawn cycleway", + "nl": "Er is een apart getekend fietspad." + } + }, + { + "if": "cycleway=no", + "then": { + "en": "There is no cycleway", + "nl": "Er is geen fietspad aanwezig" + }, + "hideInAnswer": "cycleway=opposite" + }, + { + "if": "cycleway=no", + "then": { + "en": "There is no cycleway", + "nl": "Er is geen fietspad aanwezig" + }, + "hideInAnswer": "cycleway!=opposite", + "addExtraTags": [ + "oneway:bicycle=no", + "fixme=Changed from cycleway=opposite" + ] + } + ] + }, + { + "question": { + "en": "Is this street lit?", + "nl": "Is deze weg verlicht?" + }, + "mappings": [ + { + "if": "lit=yes", + "then": { + "en": "This street is lit", + "nl": "Deze weg is verlicht" + } + }, + { + "if": "lit=no", + "then": { + "en": "This road is not lit", + "nl": "Deze weg is niet verlicht" + } + }, + { + "if": "lit=sunset-sunrise", + "then": { + "en": "This road is lit at night", + "nl": "Deze weg is 's nachts verlicht" + }, + "hideInAnswer": true + }, + { + "if": "lit=24/7", + "then": { + "en": "This road is lit 24/7", + "nl": "Deze weg is 24/7 verlicht" + } + } + ] + }, + { + "question": { + "en": "Is this a cyclestreet?", + "nl": "Is dit een fietsstraat?" + }, + "condition": { + "and": [ + "highway!=cycleway", + "highway!=path" + ] + }, + "mappings": [ + { + "if": "cyclestreet=yes", + "then": { + "en": "This is a cyclestreet, and a 30km/h zone.", + "nl": "Dit is een fietsstraat, en dus een 30km/h zone" + }, + "addExtraTags": [ + "overtaking:motor_vehicle=no", + "maxspeed=30" + ], + "hideInAnswer": "_country!=be" + }, + { + "if": "cyclestreet=yes", + "then": { + "en": "This is a cyclestreet", + "nl": "Dit is een fietsstraat" + }, + "hideInAnswer": "_country=be" + }, + { + "if": "cyclestreet=", + "then": { + "en": "This is not a cyclestreet.", + "nl": "Dit is niet een fietsstraat" + }, + "addExtraTags": [ + "overtaking:motor_vehicle=" + ] + } + ] + }, + { + "render": { + "en": "The maximum speed on this road is {maxspeed} km/h", + "nl": "De maximumsnelheid op deze weg is {maxspeed} km/u" + }, + "freeform": { + "key": "maxspeed", + "type": "nat" + }, + "condition": { + "or": [ + "cycleway=shared_lane", + "cycleway=lane" + ] + }, + "mappings": [ + { + "if": "maxspeed=20", + "then": { + "en": "The maximum speed is 20 km/h", + "nl": "De maximumsnelheid is 20 km/u" + } + }, + { + "if": "maxspeed=30", + "then": { + "en": "The maximum speed is 30 km/h", + "nl": "De maximumsnelheid is 30 km/u" + } + }, + { + "if": "maxspeed=50", + "then": { + "en": "The maximum speed is 50 km/h", + "nl": "De maximumsnelheid is 50 km/u" + } + }, + { + "if": "maxspeed=70", + "then": { + "en": "The maximum speed is 70 km/h", + "nl": "De maximumsnelheid is 70 km/u" + } + }, + { + "if": "maxspeed=90", + "then": { + "en": "The maximum speed is 90 km/h", + "nl": "De maximumsnelheid is 90 km/u" + } + } + ], + "question": { + "en": "What is the maximum speed in this street?", + "nl": "Wat is de maximumsnelheid in deze straat?" + } + }, + { + "render": { + "en": "This cyleway is made of {cycleway:surface}", + "nl": "Dit fietspad is gemaakt van {cycleway:surface}" + }, + "freeform": { + "key": "cycleway:surface", + "addExtraTags": [] + }, + "condition": { + "or": [ + "cycleway=shared_lane", + "cycleway=lane", + "cycleway=track" + ] + }, + "mappings": [ + { + "if": "cycleway:surface=wood", + "then": { + "en": "This cycleway is made of wood", + "nl": "Dit fietspad is gemaakt van hout" + } + }, + { + "if": "cycleway:surface=concrete", + "then": { + "en": "This cycleway is made of concrete", + "nl": "Dit fietspad is gemaakt van beton" + } + }, + { + "if": "cycleway:surface=cobblestone", + "then": { + "en": "This cycleway is made of cobblestone", + "nl": "Dit fietspad is gemaakt van kasseien" + } + }, + { + "if": "cycleway:surface=unhewn_cobblestone", + "then": { + "en": "This cycleway is made of asphalt", + "nl": "Dit fietspad is gemaakt van asfalt" + } + }, + { + "if": "cycleway:surface=sett", + "then": { + "en": "This cycleway is paved", + "nl": "Dit fietspad is geplaveid" + } + }, + { + "if": "cycleway:surface=asphalt", + "then": { + "en": "This cycleway is paved with stones with a flat top", + "nl": "Dit fietspad is geplaveid met stenen" + } + }, + { + "if": "cycleway:surface=paved", + "then": { + "en": "This cycleway is made of gravel", + "nl": "Dit fietspad is gemaakt van grind" + } + }, + { + "if": "cycleway:surface=paving_stones", + "then": { + "en": "This cycleway is unhardened", + "nl": "Dit fietspad is onverhard" + } + }, + { + "if": "cycleway:surface=gravel", + "then": { + "en": "This cycleway is made of gravel", + "nl": "Dit fietspad is gemaakt van grind" + } + }, + { + "if": "cycleway:surface=fine_gravel", + "then": { + "en": "This cycleway is made of fine gravel", + "nl": "Dit fietspad is gemaakt van fijn grind" + } + }, + { + "if": "cycleway:surface=pebblestone", + "then": { + "en": "This cycleway is made of pebblestone", + "nl": "Dit fietspad is gemaakt van kiezelsteentjes" + } + }, + { + "if": "cycleway:surface=ground", + "then": { + "en": "This cycleway is made from raw ground", + "nl": "Dit fietspad is gemaakt van aarde" + } + } + ], + "question": { + "en": "What is the surface of the cycleway made from?", + "nl": "Waaruit is het oppervlak van het fietspad van gemaakt?" + } + }, + { + "question": { + "en": "What is the smoothness of this cycleway?", + "nl": "Wat is de kwaliteit van dit fietspad?" + }, + "condition": { + "or": [ + "cycleway=shared_lane", + "cycleway=lane", + "cycleway=track" + ] + }, + "mappings": [ + { + "if": "cycleway:smoothness=excellent", + "then": { + "en": "Usable for thin rollers: rollerblade, skateboard" + } + }, + { + "if": "cycleway:smoothness=good", + "then": { + "en": "Usable for thin wheels: racing bike" + } + }, + { + "if": "cycleway:smoothness=intermediate", + "then": { + "en": "Usable for normal wheels: city bike, wheelchair, scooter" + } + }, + { + "if": "cycleway:smoothness=bad", + "then": { + "en": "Usable for robust wheels: trekking bike, car, rickshaw" + } + }, + { + "if": "cycleway:smoothness=very_bad", + "then": { + "en": "Usable for vehicles with high clearance: light duty off-road vehicle" + } + }, + { + "if": "cycleway:smoothness=horrible", + "then": { + "en": "Usable for off-road vehicles: heavy duty off-road vehicle" + } + }, + { + "if": "cycleway:smoothness=very_horrible", + "then": { + "en": "Usable for specialized off-road vehicles: tractor, ATV" + } + }, + { + "if": "cycleway:smoothness=impassable", + "then": { + "en": "Impassable / No wheeled vehicle" + } + } + ] + }, + { + "render": { + "en": "This road is made of {surface}", + "nl": "Deze weg is gemaakt van {surface}" + }, + "freeform": { + "key": "surface", + "addExtraTags": [] + }, + "mappings": [ + { + "if": "surface=wood", + "then": { + "en": "This street is made of wood", + "nl": "Deze straat is gemaakt van hout" + } + }, + { + "if": "surface=concrete", + "then": { + "en": "This street is made of concrete", + "nl": "Deze straat is gemaakt van beton" + } + }, + { + "if": "surface=sett", + "then": { + "en": "This street is made of cobblestone", + "nl": "Deze straat is gemaakt van kasseien" + } + }, + { + "if": "surface=asphalt", + "then": { + "en": "This street is made of asphalt", + "nl": "Deze straat is gemaakt van asfalt" + } + }, + { + "if": "surface=paved", + "then": { + "en": "This street is paved", + "nl": "Deze straat is geplaveid" + } + }, + { + "if": "surface=fine_gravel", + "then": { + "en": "This street is made from fine gravel", + "nl": "Deze straat is gemaakt van fijn grind" + }, + "hideInAnswer": true + }, + { + "if": "surface=cobblestone", + "then": { + "en": "This street is made from cobblestone", + "nl": "Deze straat is gemaakt van kasseien" + }, + "hideInAnswer": true + }, + { + "if": "surface=paving_stones", + "then": { + "en": "This street is made from paving stones", + "nl": "Deze straat is gemaakt van geplaveide stenen" + }, + "hideInAnswer": true + } + ], + "question": { + "en": "What is the surface of the street made from?", + "nl": "Waaruit is het oppervlak van de straat gemaakt?" + } + }, + { + "question": { + "en": "What is the smoothness of this street?", + "nl": "Wat is de kwaliteit van deze straat?" + }, + "condition": { + "or": [ + "cycleway=no", + "highway=cycleway" + ] + }, + "mappings": [ + { + "if": "smoothness=excellent", + "then": { + "en": "Usable for thin rollers: rollerblade, skateboard" + } + }, + { + "if": "smoothness=good", + "then": { + "en": "Usable for thin wheels: racing bike" + } + }, + { + "if": "smoothness=intermediate", + "then": { + "en": "Usable for normal wheels: city bike, wheelchair, scooter" + } + }, + { + "if": "smoothness=bad", + "then": { + "en": "Usable for robust wheels: trekking bike, car, rickshaw" + } + }, + { + "if": "smoothness=very_bad", + "then": { + "en": "Usable for vehicles with high clearance: light duty off-road vehicle" + } + }, + { + "if": "smoothness=horrible", + "then": { + "en": "Usable for off-road vehicles: heavy duty off-road vehicle" + } + }, + { + "if": "smoothness=very_horrible", + "then": { + "en": "Usable for specialized off-road vehicles: tractor, ATV" + } + }, + { + "if": "smoothness=impassable", + "then": { + "en": "Impassable / No wheeled vehicle" + } + } + ] + }, + { + "condition": { + "or": [ + "cycleway=shared_lane", + "cycleway=no", + "cycleway=" + ] + }, + "render": { + "en": "The carriage width of this road is {width:carriageway}m", + "nl": "De breedte van deze rijbaan in deze straat is {width:carriageway}m" + }, + "freeform": { + "key": "width:carriageway", + "addExtraTags": [], + "type": "pfloat" + }, + "question": { + "en": "What is the carriage width of this road (in meters)?", + "nl": "Hoe breed is de rijbaan in deze straat (in meters)?" + } + }, + { + "question": { + "en": "What traffic sign does this cycleway have?", + "nl": "Welk verkeersbord heeft dit fietspad?" + }, + "condition": { + "or": [ + "cycleway=lane", + "cycleway=track" + ] + }, + "mappings": [ + { + "if": "cycleway:traffic_sign=BE:D7", + "then": { + "en": "Compulsory cycleway ", + "nl": "Verplicht fietspad " + }, + "hideInAnswer": "_country!=be" + }, + { + "if": "cycleway:traffic_sign~BE:D7;.*", + "then": { + "en": "Compulsory cycleway (with supplementary sign) ", + "nl": "Verplicht fietspad (met onderbord)" + }, + "hideInAnswer": true + }, + { + "if": "cycleway:traffic_sign=BE:D9", + "then": { + "en": "Segregated foot/cycleway ", + "nl": "Afgescheiden voet-/fietspad " + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:foot=designated", + "cycleway:segregated=yes" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D10", + "then": { + "en": "Unsegregated foot/cycleway ", + "nl": "Gedeeld voet-/fietspad " + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:foot=designated", + "cycleway:segregated=no" + ] + }, + { + "if": "cycleway:traffic_sign=none", + "then": { + "en": "No traffic sign present", + "nl": "Geen verkeersbord aanwezig" + } + } + ] + }, + { + "question": { + "en": "What traffic sign does this cycleway have?", + "nl": "Welk verkeersbord heeft dit fietspad?" + }, + "condition": { + "or": [ + "highway=cycleway", + "highway=path" + ] + }, + "mappings": [ + { + "if": "traffic_sign=BE:D7", + "then": { + "en": "Compulsory cycleway ", + "nl": "Verplicht fietspad " + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "bicycle=designated", + "mofa=designated", + "moped=yes", + "speed_pedelec=yes" + ] + }, + { + "if": "traffic_sign~BE:D7;.*", + "then": { + "en": "Compulsory cycleway (with supplementary sign) ", + "nl": "Verplicht fietspad (met onderbord)" + }, + "hideInAnswer": true + }, + { + "if": "traffic_sign=BE:D9", + "then": { + "en": "Segregated foot/cycleway ", + "nl": "Afgescheiden voet-/fietspad " + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "foot=designated", + "bicycle=designated", + "mofa=designated", + "moped=no", + "speed_pedelec=no", + "segregated=yes" + ] + }, + { + "if": "traffic_sign=BE:D10", + "then": { + "en": "Unsegregated foot/cycleway ", + "nl": "Gedeeld voet-/fietspad " + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "foot=designated", + "bicycle=designated", + "mofa=designated", + "moped=no", + "speed_pedelec=no", + "segregated=no" + ] + }, + { + "if": "traffic_sign=none", + "then": { + "en": "No traffic sign present", + "nl": "Geen verkeersbord aanwezig" + } + } + ] + }, + { + "question": { + "en": "Does the traffic sign D7 () have a supplementary sign?", + "nl": "Heeft het verkeersbord D7 () een onderbord?" + }, + "condition": { + "or": [ + "cycleway:traffic_sign=BE:D7", + "cycleway:traffic_sign~BE:D7;.*" + ] + }, + "mappings": [ + { + "if": "cycleway:traffic_sign=BE:D7;BE:M6", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:moped=designated" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D7;BE:M13", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:speed_pedelec=designated" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D7;BE:M14", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:moped=designated", + "cycleway:speed_pedelec=designated" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D7;BE:M7", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:moped=no" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D7;BE:M15", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:speed_pedelec=no" + ] + }, + { + "if": "cycleway:traffic_sign=BE:D7;BE:M16", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "cycleway:moped=designated", + "cycleway:speed_pedelec=no" + ] + }, + { + "if": "cycleway:traffic_sign:supplementary=none", + "then": { + "en": "No supplementary traffic sign present", + "nl": "Geen onderbord aanwezig" + } + } + ] + }, + { + "question": { + "en": "Does the traffic sign D7 () have a supplementary sign?", + "nl": "Heeft het verkeersbord D7 () een onderbord?" + }, + "condition": { + "or": [ + "traffic_sign=BE:D7", + "traffic_sign~BE:D7;.*" + ] + }, + "mappings": [ + { + "if": "traffic_sign=BE:D7;BE:M6", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "moped=designated" + ] + }, + { + "if": "traffic_sign=BE:D7;BE:M13", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "speed_pedelec=designated" + ] + }, + { + "if": "traffic_sign=BE:D7;BE:M14", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "moped=designated", + "speed_pedelec=designated" + ] + }, + { + "if": "traffic_sign=BE:D7;BE:M7", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "moped=no" + ] + }, + { + "if": ":traffic_sign=BE:D7;BE:M15", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "speed_pedelec=no" + ] + }, + { + "if": "traffic_sign=BE:D7;BE:M16", + "then": { + "en": "", + "nl": "" + }, + "hideInAnswer": "_country!=be", + "addExtraTags": [ + "moped=designated", + "speed_pedelec=no" + ] + }, + { + "if": "traffic_sign:supplementary=none", + "then": { + "en": "No supplementary traffic sign present", + "nl": "Geen onderbord aanwezig" + } + } + ] + }, + { + "render": { + "en": "The buffer besides this cycleway is {cycleway:buffer} m", + "nl": "De schrikafstand van dit fietspad is {cycleway:buffer} m" + }, + "question": { + "en": "How wide is the gap between the cycleway and the road?", + "nl": "Hoe breed is de ruimte tussen het fietspad en de weg?" + }, + "condition": { + "or": [ + "cycleway=track", + "cycleway=lane" + ] + }, + "freeform": { + "key": "cycleway:buffer", + "type": "pfloat" + } + }, + { + "question": { + "en": "How is this cycleway seperated from the road?", + "nl": "Hoe is dit fietspad gescheiden van de weg?" + }, + "condition": { + "or": [ + "cycleway=track", + "cycleway=lane" + ] + }, + "mappings": [ + { + "if": "cycleway:seperation=dashed_line", + "then": { + "en": "This cycleway is seperated by a dashed line", + "nl": "Dit fietspad is gescheiden van de weg met een onderbroken streep" + } + }, + { + "if": "cycleway:seperation=solid_line", + "then": { + "en": "This cycleway is seperated by a solid line", + "nl": "Dit fietspad is gescheiden van de weg met een doorgetrokken streep" + } + }, + { + "if": "cycleway:seperation=parking_lane", + "then": { + "en": "This cycleway is seperated by a parking lane", + "nl": "Dit fietspad is gescheiden van de weg met parkeervakken" + } + }, + { + "if": "cycleway:seperation=kerb", + "then": { + "en": "This cycleway is seperated by a kerb", + "nl": "Dit fietspad is gescheiden van de weg met een stoeprand" + } + } + ] + }, + { + "question": { + "en": "How is this cycleway seperated from the road?", + "nl": "Hoe is dit fietspad gescheiden van de weg?" + }, + "condition": { + "or": [ + "highway=cycleway", + "highway=path" + ] + }, + "mappings": [ + { + "if": "seperation=dashed_line", + "then": { + "en": "This cycleway is seperated by a dashed line", + "nl": "Dit fietspad is gescheiden van de weg met een onderbroken streep" + } + }, + { + "if": "seperation=solid_line", + "then": { + "en": "This cycleway is seperated by a solid line", + "nl": "Dit fietspad is gescheiden van de weg met een doorgetrokken streep" + } + }, + { + "if": "seperation=parking_lane", + "then": { + "en": "This cycleway is seperated by a parking lane", + "nl": "Dit fietspad is gescheiden van de weg met parkeervakken" + } + }, + { + "if": "seperation=kerb", + "then": { + "en": "This cycleway is seperated by a kerb", + "nl": "Dit fietspad is gescheiden van de weg met een stoeprand" + } + } + ] + } + ], + "icon": { + "render": "./assets/themes/cycle_infra/bicycleway.svg" + }, + "width": { + "render": "8" + }, + "iconSize": { + "render": "40,40,center" + }, + "color": { + "render": "rgba(170, 170, 170, 0.7)", + "mappings": [ + { + "if": "highway=cycleway", + "then": "rgba(0, 189, 141, 0.7)" + }, + { + "if": "highway=path", + "then": "rgba(204, 74, 207, 0.7)" + }, + { + "if": "cycleway=track", + "then": "rgba(113, 3, 200, 0.7)" + }, + { + "if": "cycleway=shared_lane", + "then": "rgba(74, 59, 247, 0.7)" + }, + { + "if": "cycleway=lane", + "then": "rgba(254, 155, 6, 0.9)" + }, + { + "if": "cyclestreet=yes", + "then": "rgba(57, 159, 191, 0.7)" + } + ] + }, + "dashArray": { + "render": "", + "mappings": [ + { + "if": { + "or": [ + "oneway=yes", + { + "or": [ + "highway=cycleway", + "highway=path" + ] + } + ] + }, + "then": "" + }, + { + "if": "cycleway=track", + "then": "" + }, + { + "if": "cycleway=shared_lane", + "then": "15 30" + }, + { + "if": "cycleway=lane", + "then": "25 15 15 15 25" + }, + { + "if": "cyclestreet=yes", + "then": "" + } + ] + }, + "presets": [] + }, + { + "id": "all_streets", + "name": { + "nl": "Alle straten", + "en": "All streets" + }, + "description": { + "nl": "Laag waar je nieuwe fietspaden kan aanduiden", + "en": "Layer to mark new cycleways" + }, + "source": { + "osmTags": { + "or": [ + "highway=residential", + "highway=tertiary", + "highway=unclassified", + "highway=primary", + "highway=secondary" + ] + } + }, + "calculatedTags": [ + "_comfort_score=feat.score('https://raw.githubusercontent.com/pietervdvn/AspectedRouting/master/Examples/bicycle/aspects/bicycle.comfort.json')" + ], + "minzoom": 14, + "wayHandling": 0, + "title": { + "render": { + "nl": "Straat", + "en": "Street" + }, + "mappings": [ + { + "if": "name~*", + "then": "{name}" + } + ] + }, + "icon": "./assets/themes/cycle_infra/street.svg", + "width": "5", + "color": { + "render": "rgba(170, 170, 170, 0.7)", + "mappings": [] + }, + "tagRenderings": [ + { + "freeform": { + "key": "cycleway", + "addExtraTags": [] + }, + "question": { + "en": "Is there a cycleway?", + "nl": "Is er een fietspad?" + }, + "condition": { + "and": [ + "highway!=cycleway", + "highway!=path" + ] + }, + "mappings": [ + { + "if": "cycleway=shared_lane", + "then": { + "en": "There is a shared lane", + "nl": "Er is een fietssugestiestrook" + } + }, + { + "if": "cycleway=lane", + "then": { + "en": "There is a lane next to the road (seperated with paint)", + "nl": "Er is een fietspad aangrenzend aan de weg (gescheiden met verf)" + } + }, + { + "if": "cycleway=track", + "then": { + "en": "There is a track, but no cycleway drawn seperately from this road on the map.", + "nl": "Er is een fietspad (los van de weg), maar geen fietspad afzonderlijk getekend naast deze weg." + } + }, + { + "if": "cycleway=seperate", + "then": { + "en": "There is a seperately drawn cycleway", + "nl": "Er is een apart getekend fietspad." + } + }, + { + "if": "cycleway=", + "then": { + "en": "There is no cycleway known here", + "nl": "Er is geen fietspad bekend hier" + }, + "hideInAnswer": true + }, + { + "if": "cycleway=no", + "then": { + "en": "There is no cycleway", + "nl": "Er is geen fietspad aanwezig" + }, + "hideInAnswer": "cycleway=opposite" + }, + { + "if": "cycleway=no", + "then": { + "en": "There is no cycleway", + "nl": "Er is geen fietspad aanwezig" + }, + "hideInAnswer": "cycleway!=opposite", + "addExtraTags": [ + "oneway:bicycle=no", + "fixme=Changed from cycleway=opposite" + ] + } + ], + "render": { + "en": "Unknown cycleway situation", + "nl": "Onbekende fietspad situatie" + } + }, + { + "question": { + "en": "Is this a cyclestreet?", + "nl": "Is dit een fietsstraat?" + }, + "condition": { + "and": [ + "highway!=cycleway", + "highway!=path" + ] + }, + "mappings": [ + { + "if": "cyclestreet=yes", + "then": { + "en": "This is a cyclestreet, and a 30km/h zone.", + "nl": "Dit is een fietsstraat, en dus een 30km/h zone" + }, + "addExtraTags": [ + "overtaking:motor_vehicle=no", + "maxspeed=30" + ], + "hideInAnswer": "_country!=be" + }, + { + "if": "cyclestreet=yes", + "then": { + "en": "This is a cyclestreet", + "nl": "Dit is een fietsstraat" + }, + "hideInAnswer": "_country=be" + }, + { + "if": "cyclestreet=", + "then": { + "en": "This is not a cyclestreet.", + "nl": "Dit is niet een fietsstraat" + }, + "addExtraTags": [ + "overtaking:motor_vehicle=" + ] + } + ] + } + ] + }, + { + "id": "barriers", + "name": { + "en": "Barriers", + "nl": "Barrières" + }, + "description": { + "en": "Obstacles while cycling, such as bollards and cycle barriers", + "nl": "Hindernissen tijdens het fietsen, zoals paaltjes en fietshekjes" + }, + "source": { + "osmTags": { + "or": [ + "barrier=bollard", + "barrier=cycle_barrier" + ] + } + }, + "minzoom": 17, + "title": { + "render": { + "en": "Barrier", + "nl": "Barrière" + }, + "mappings": [ + { + "if": "barrier=bollard", + "then": { + "en": "Bollard", + "nl": "Paaltje" + } + }, + { + "if": "barrier=cycle_barrier", + "then": { + "en": "Cycling Barrier", + "nl": "Fietshekjes" + } + } + ] + }, + "icon": "./assets/svg/barrier.svg", + "width": "5", + "presets": [ + { + "title": { + "en": "Bollard", + "nl": "Paaltje" + }, + "tags": [ + "barrier=bollard" + ], + "description": { + "en": "A bollard in the road", + "nl": "Een paaltje in de weg" + } + }, + { + "title": { + "en": "Cycle barrier", + "nl": "Fietshekjes" + }, + "tags": [ + "barrier=bollard" + ], + "description": { + "en": "Cycle barrier, slowing down cyclists", + "nl": "Fietshekjes, voor het afremmen van fietsers" + } + } + ], + "tagRenderings": [ + { + "question": { + "en": "Can a bicycle go past this barrier?", + "nl": "Kan een fietser langs deze barrière?" + }, + "mappings": [ + { + "if": "bicycle=yes", + "then": { + "en": "A cyclist can go past this.", + "nl": "Een fietser kan hier langs." + } + }, + { + "if": "bicycle=no", + "then": { + "en": "A cyclist can not go past this.", + "nl": "Een fietser kan hier niet langs." + } + } + ] + }, + { + "question": { + "en": "What kind of bollard is this?", + "nl": "Wat voor soort paal is dit?" + }, + "condition": "barrier=bollard", + "mappings": [ + { + "if": "bollard=removable", + "then": { + "en": "Removable bollard", + "nl": "Verwijderbare paal" + } + }, + { + "if": "bollard=fixed", + "then": { + "en": "Fixed bollard", + "nl": "Vaste paal" + } + }, + { + "if": "bollard=foldable", + "then": { + "en": "Bollard that can be folded down", + "nl": "Paal die platgevouwen kan worden" + } + }, + { + "if": "bollard=flexible", + "then": { + "en": "Flexible bollard, usually plastic", + "nl": "Flexibele paal, meestal plastic" + } + }, + { + "if": "bollard=rising", + "then": { + "en": "Rising bollard", + "nl": "Verzonken poller" + } + } + ] + }, + { + "question": { + "en": "What kind of cycling barrier is this?", + "nl": "Wat voor fietshekjes zijn dit?" + }, + "condition": "barrier=cycle_barrier", + "mappings": [ + { + "if": "cycle_barrier:type=single", + "then": { + "en": "Single, just two barriers with a space inbetween ", + "nl": "Enkelvoudig, slechts twee hekjes met ruimte ertussen " + } + }, + { + "if": "cycle_barrier:type=double", + "then": { + "en": "Double, two barriers behind each other ", + "nl": "Dubbel, twee hekjes achter elkaar " + } + }, + { + "if": "cycle_barrier:type=triple", + "then": { + "en": "Triple, three barriers behind each other ", + "nl": "Drievoudig, drie hekjes achter elkaar " + } + }, + { + "if": "cycle_barrier:type=squeeze", + "then": { + "en": "Squeeze gate, gap is smaller at top, than at the bottom " + } + } + ] + }, + { + "render": { + "en": "Maximum width: {maxwidth:physical} m", + "nl": "Maximumbreedte: {maxwidth:physical} m" + }, + "question": { + "en": "How wide is the gap left over besides the barrier?", + "nl": "Hoe breed is de ruimte naast de barrière?" + }, + "condition": { + "and": [ + "cycle_barrier:type!=double", + "cycle_barrier:type!=triple" + ] + }, + "freeform": { + "key": "maxwidth:physical", + "type": "pfloat" + } + }, + { + "render": { + "en": "Space between barriers (along the length of the road): {width:seperation} m" + }, + "question": { + "en": "How much space is there between the barriers (along the length of the road)?" + }, + "condition": { + "or": [ + "cycle_barrier:type=double", + "cycle_barrier:type=triple" + ] + }, + "freeform": { + "key": "width:seperation", + "type": "pfloat" + } + }, + { + "render": { + "en": "Width of opening: {width:opening} m" + }, + "question": { + "en": "How wide is the smallest opening next to the barriers?" + }, + "condition": { + "or": [ + "cycle_barrier:type=double", + "cycle_barrier:type=triple" + ] + }, + "freeform": { + "key": "width:opening", + "type": "pfloat" + } + }, + { + "render": { + "en": "Overlap: {overlap} m" + }, + "question": { + "en": "How much overlap do the barriers have?" + }, + "condition": { + "or": [ + "cycle_barrier:type=double", + "cycle_barrier:type=triple" + ] + }, + "freeform": { + "key": "overlap", + "type": "pfloat" + } + } + ] + }, + "crossings" + ] +} \ No newline at end of file diff --git a/assets/themes/cycle_infra/license_info.json b/assets/themes/cycle_infra/license_info.json new file mode 100644 index 0000000000..bd6a9f2bb2 --- /dev/null +++ b/assets/themes/cycle_infra/license_info.json @@ -0,0 +1,173 @@ +[ + { + "authors": [ + "Dávid Gladiš", + "Hannah Declerck" + ], + "path": "cycle-infra.svg", + "license": "CC-BY", + "sources": [ + "https://thenounproject.com/davidgladis/collection/bicycles/?i=808040" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_road_sign_D07.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_road_sign_D07.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_road_sign_D09.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_road_sign_D09.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_road_sign_D10.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_road_sign_D10.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M6.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M6.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M7.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M7.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M13.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M13.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M14.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M14.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M15.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M15.svg" + ] + }, + { + "authors": [ + "Belgische Wetgever" + ], + "path": "Belgian_traffic_sign_M16.svg", + "license": "CC0", + "sources": [ + "https://commons.wikimedia.org/wiki/File:Belgian_traffic_sign_M16.svg" + ] + }, + { + "authors": [ + "Supaplex030" + ], + "path": "Cycle_barrier_single.png", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Cycle_barrier_single.png" + ] + }, + { + "authors": [ + "Supaplex030" + ], + "path": "Cycle_barrier_double.png", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Cycle_barrier_double.png" + ] + }, + { + "authors": [ + "Supaplex030" + ], + "path": "Cycle_barrier_triple.png", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Cycle_barrier_triple.png" + ] + }, + { + "authors": [ + "Supaplex030" + ], + "path": "Cycle_barrier_angular.png", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Cycle_barrier_angular.png" + ] + }, + { + "authors": [ + "Supaplex030" + ], + "path": "Cycle_barrier_squeeze.png", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Cycle_barrier_squeeze.png" + ] + }, + { + "authors": [ + "Tobias Zwick" + ], + "path": "street.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/quest%20icons/street.svg" + ] + }, + { + "authors": [ + "Tobias Zwick" + ], + "path": "bicycleway.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/quest%20icons/bicycleway.svg" + ] + } +] \ No newline at end of file diff --git a/assets/themes/cycle_infra/street.svg b/assets/themes/cycle_infra/street.svg new file mode 100644 index 0000000000..796df93b2a --- /dev/null +++ b/assets/themes/cycle_infra/street.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index b3b3abbf4c..04398254e1 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -1,5 +1,5 @@ { - "id": "fietsstraten", + "id": "cyclestreets", "version": "2020-08-30", "title": { "nl": "Fietsstraten", @@ -34,7 +34,7 @@ "startZoom": 14, "startLon": 3.2228, "maintainer": "MapComplete", - "widenfactor": 0.05, + "widenfactor": 0.01, "roamingRenderings": [ { "question": { @@ -68,7 +68,7 @@ ] }, "then": { - "nl": "Deze straat is een fietsstraat", + "nl": "Deze straat i een fietsstraat", "en": "This street is a cyclestreet", "ja": "この通りはcyclestreetだ", "nb_NO": "Denne gaten er en sykkelvei" @@ -276,7 +276,8 @@ }, "tagRenderings": [ "images" - ] + ], + "allowSplit": true } ] } \ No newline at end of file diff --git a/assets/themes/facadegardens/facadegardens.json b/assets/themes/facadegardens/facadegardens.json index 8cc9369815..c69fb08e29 100644 --- a/assets/themes/facadegardens/facadegardens.json +++ b/assets/themes/facadegardens/facadegardens.json @@ -330,7 +330,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "circle:white;./assets/themes/facadegardens/geveltuin.svg", "mappings": [ diff --git a/assets/themes/fruit_trees/fruit_trees.json b/assets/themes/fruit_trees/fruit_trees.json index dc59132c91..60d6e8a234 100644 --- a/assets/themes/fruit_trees/fruit_trees.json +++ b/assets/themes/fruit_trees/fruit_trees.json @@ -1,5 +1,5 @@ { - "id": "boomgaarden", + "id": "fruit_trees", "title": { "nl": "Open Boomgaardenkaart" }, @@ -43,7 +43,6 @@ "tagRenderings": [ "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/buurtnatuur/forest.svg" }, @@ -143,7 +142,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/fruit_trees/fruit_tree.svg" }, diff --git a/assets/themes/grb.json b/assets/themes/grb.json index 7f38732af7..536b694e75 100644 --- a/assets/themes/grb.json +++ b/assets/themes/grb.json @@ -173,7 +173,6 @@ } } ], - "hideUnderlayingFeaturesMinPercentage": 0, "label": { "mappings": [ { diff --git a/assets/themes/hailhydrant/hailhydrant.json b/assets/themes/hailhydrant/hailhydrant.json index d465b72eb2..fc74cd62c9 100644 --- a/assets/themes/hailhydrant/hailhydrant.json +++ b/assets/themes/hailhydrant/hailhydrant.json @@ -258,7 +258,6 @@ }, "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/hailhydrant/hydrant.svg" }, @@ -365,7 +364,6 @@ }, "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/hailhydrant/Twemoji12_1f9ef.svg" }, @@ -568,7 +566,6 @@ }, "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/hailhydrant/Twemoji12_1f692.svg" }, @@ -744,7 +741,6 @@ }, "images" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/hailhydrant/Twemoji_1f691.svg" }, diff --git a/assets/themes/natuurpunt/bench.svg b/assets/themes/natuurpunt/bench.svg new file mode 100644 index 0000000000..ddab90ca99 --- /dev/null +++ b/assets/themes/natuurpunt/bench.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/birdhide.svg b/assets/themes/natuurpunt/birdhide.svg new file mode 100644 index 0000000000..730e3f712f --- /dev/null +++ b/assets/themes/natuurpunt/birdhide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/drips.svg b/assets/themes/natuurpunt/drips.svg new file mode 100644 index 0000000000..94517d71eb --- /dev/null +++ b/assets/themes/natuurpunt/drips.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/information.svg b/assets/themes/natuurpunt/information.svg new file mode 100644 index 0000000000..248cbccb52 --- /dev/null +++ b/assets/themes/natuurpunt/information.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/information_board.svg b/assets/themes/natuurpunt/information_board.svg new file mode 100644 index 0000000000..42d2b07459 --- /dev/null +++ b/assets/themes/natuurpunt/information_board.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/license_info.json b/assets/themes/natuurpunt/license_info.json new file mode 100644 index 0000000000..c192defd61 --- /dev/null +++ b/assets/themes/natuurpunt/license_info.json @@ -0,0 +1,120 @@ +[ + { + "authors": [ + "Natuurpunt" + ], + "path": "natuurpunt.png", + "license": "Logo; All rights reserved", + "sources": [ + "https://www.natuurpunt.be/" + ] + }, + { + "authors": [], + "path": "bench.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "watermill.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "information.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "trail.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "toilets.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "urinal.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "wheelchair.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "walk_wheelchair.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "information_board.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "pushchair.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "picnic_table.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "parking.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "parkingbike.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "parkingmotor.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "parkingwheels.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "nature_reserve.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "drips.svg", + "license": "CC0", + "sources": [] + }, + { + "authors": [], + "path": "birdhide.svg", + "license": "CC0", + "sources": [] + } +] \ No newline at end of file diff --git a/assets/themes/natuurpunt/nature_reserve.svg b/assets/themes/natuurpunt/nature_reserve.svg new file mode 100644 index 0000000000..e1085da76c --- /dev/null +++ b/assets/themes/natuurpunt/nature_reserve.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/natuurpunt.css b/assets/themes/natuurpunt/natuurpunt.css new file mode 100644 index 0000000000..e64cd75be7 --- /dev/null +++ b/assets/themes/natuurpunt/natuurpunt.css @@ -0,0 +1,17 @@ +:root { + --subtle-detail-color: #007759; + --subtle-detail-color-contrast: #ffffff; + --subtle-detail-color-light-contrast: lightgrey; + + --catch-detail-color: #0fff00; + --catch-detail-color-contrast: #ffffff; + --alert-color: #fee4d1; + --background-color: white; + --foreground-color: #007759; + --popup-border: white; + --shadow-color: #00000066; + --variable-title-height: 0px; /* Set by javascript */ + --return-to-the-map-height: 2em; + + --image-carousel-height: 350px; +} \ No newline at end of file diff --git a/assets/themes/natuurpunt/natuurpunt.json b/assets/themes/natuurpunt/natuurpunt.json new file mode 100644 index 0000000000..4d66fada57 --- /dev/null +++ b/assets/themes/natuurpunt/natuurpunt.json @@ -0,0 +1,257 @@ +{ + "id": "natuurpunt", + "customCss": "./assets/themes/natuurpunt/natuurpunt.css", + "title": { + "nl": "Natuurgebieden", + "en": "Nature Reserves" + }, + "shortDescription": { + "nl": "Deze kaart toont de natuurgebieden van Natuurpunt", + "en": "This map shows the nature reserves of Natuurpunt" + }, + "description": { + "nl": "Op deze kaart vind je alle natuurgebieden die Natuurpunt ter beschikking stelt", + "en": "On this map you can find all the nature reserves that Natuurpunt offers " + }, + "language": [ + "nl", + "en" + ], + "maintainer": "", + "icon": "./assets/themes/natuurpunt/natuurpunt.png", + "version": "0", + "startLat": 51.20875, + "startLon": 3.22435, + "startZoom": 15, + "widenFactor": 0.05, + "socialImage": "", + "defaultBackgroundId": "CartoDB.Positron", + "layers": [ + { + "#": "Nature reserve with geometry, z>=13", + "builtin": "nature_reserve", + "override": { + "name": null, + "source": { + "osmTags": { + "+and": [ + "operator~.*[nN]atuurpunt.*" + ] + }, + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "minzoom": 13, + "minzoomVisible": 0, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" + } + } + }, + { + "#": "Nature reserve overview from cache, points only, z < 13", + "builtin": "nature_reserve", + "override": { + "source": { + "osmTags": { + "+and": [ + "operator~.*[nN]atuurpunt.*" + ] + }, + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson", + "isOsmCache": "duplicate" + }, + "minzoom": 1, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" + } + } + }, + { + "builtin": "visitor_information_centre", + "override": { + "source": { + "osmTags": { + "+and": [ + "operator~.*[nN]atuurpunt.*" + ] + }, + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 1, + "isOsmCache": true + }, + "minzoom": "10", + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/information.svg" + } + } + }, + { + "builtin": "trail", + "override": { + "source": { + "osmTags": { + "+and": [ + "operator~.*[nN]atuurpunt.*" + ] + }, + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "minzoom": "13", + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/trail.svg", + "mappings": [ + { + "if": "wheelchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/walk_wheelchair.svg" + }, + { + "if": "pushchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/pushchair.svg" + } + ] + } + } + }, + { + "builtin": "toilet", + "override": { + "minzoom": "15", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", + "mappings": [ + { + "if": "wheelchair=yes", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/wheelchair.svg" + }, + { + "if": "toilets:position=urinals", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/urinal.svg" + } + ] + } + } + }, + { + "builtin": "birdhide", + "override": { + "minzoom": 12, + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg", + "mappings": null + } + } + }, + { + "builtin": "picnic_table", + "override": { + "minzoom": "16", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" + } + } + }, + { + "builtin": "drinking_water", + "override": { + "minzoom": "16", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" + } + } + }, + { + "builtin": "parking", + "override": { + "minzoom": "16", + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/parking.svg", + "mappings": [ + { + "if": "amenity=bicycle_parking", + "then": "circle:#FE6F32;./assets/themes/natuurpunt/parkingbike.svg" + } + ] + }, + "iconOverlays": [ + { + "if": "amenity=motorcycle_parking", + "then": "circle:#335D9F;./assets/themes/natuurpunt/parkingmotor.svg", + "badge": true + }, + { + "if": "capacity:disabled=yes", + "then": "circle:#335D9F;./assets/themes/natuurpunt/parkingwheels.svg", + "badge": true + } + ] + } + }, + { + "builtin": "information_board", + "override": { + "minzoom": "16", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" + } + } + }, + { + "builtin": "bench", + "override": { + "minzoom": "18", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" + } + } + }, + { + "builtin": "watermill", + "override": { + "minzoom": "18", + "source": { + "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson", + "geoJsonZoomLevel": 12, + "isOsmCache": true + }, + "icon": { + "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" + } + } + } + ], + "roamingRenderings": [] +} \ No newline at end of file diff --git a/assets/themes/natuurpunt/natuurpunt.png b/assets/themes/natuurpunt/natuurpunt.png new file mode 100644 index 0000000000..1fd1486a52 Binary files /dev/null and b/assets/themes/natuurpunt/natuurpunt.png differ diff --git a/assets/themes/natuurpunt/parking.svg b/assets/themes/natuurpunt/parking.svg new file mode 100644 index 0000000000..fceb3f23f7 --- /dev/null +++ b/assets/themes/natuurpunt/parking.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/parkingbike.svg b/assets/themes/natuurpunt/parkingbike.svg new file mode 100644 index 0000000000..e22f683e53 --- /dev/null +++ b/assets/themes/natuurpunt/parkingbike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/parkingmotor.svg b/assets/themes/natuurpunt/parkingmotor.svg new file mode 100644 index 0000000000..0fc18d9a68 --- /dev/null +++ b/assets/themes/natuurpunt/parkingmotor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/parkingwheels.svg b/assets/themes/natuurpunt/parkingwheels.svg new file mode 100644 index 0000000000..3e08f733bb --- /dev/null +++ b/assets/themes/natuurpunt/parkingwheels.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/picnic_table.svg b/assets/themes/natuurpunt/picnic_table.svg new file mode 100644 index 0000000000..6629c237ef --- /dev/null +++ b/assets/themes/natuurpunt/picnic_table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/pushchair.svg b/assets/themes/natuurpunt/pushchair.svg new file mode 100644 index 0000000000..725e1dee32 --- /dev/null +++ b/assets/themes/natuurpunt/pushchair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/toilets.svg b/assets/themes/natuurpunt/toilets.svg new file mode 100644 index 0000000000..006dcceca5 --- /dev/null +++ b/assets/themes/natuurpunt/toilets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/trail.svg b/assets/themes/natuurpunt/trail.svg new file mode 100644 index 0000000000..3839ed493b --- /dev/null +++ b/assets/themes/natuurpunt/trail.svg @@ -0,0 +1 @@ + diff --git a/assets/themes/natuurpunt/urinal.svg b/assets/themes/natuurpunt/urinal.svg new file mode 100644 index 0000000000..90038efb6a --- /dev/null +++ b/assets/themes/natuurpunt/urinal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/walk_wheelchair.svg b/assets/themes/natuurpunt/walk_wheelchair.svg new file mode 100644 index 0000000000..6f2e46fe9c --- /dev/null +++ b/assets/themes/natuurpunt/walk_wheelchair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/watermill.svg b/assets/themes/natuurpunt/watermill.svg new file mode 100644 index 0000000000..1c62cd1473 --- /dev/null +++ b/assets/themes/natuurpunt/watermill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/natuurpunt/wheelchair.svg b/assets/themes/natuurpunt/wheelchair.svg new file mode 100644 index 0000000000..9a7124a876 --- /dev/null +++ b/assets/themes/natuurpunt/wheelchair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/parkings/license_info.json b/assets/themes/parkings/license_info.json new file mode 100644 index 0000000000..a8eeb9f1cb --- /dev/null +++ b/assets/themes/parkings/license_info.json @@ -0,0 +1,12 @@ +[ + { + "authors": [ + "Freepik" + ], + "path": "parkings.svg", + "license": "CC-BY", + "sources": [ + "https://www.flaticon.com/free-icon/placeholder_608690?term=parking&page=1&position=16&page=1&position=16&related_id=608690&origin=search" + ] + } +] \ No newline at end of file diff --git a/assets/themes/parkings/parkings.json b/assets/themes/parkings/parkings.json new file mode 100644 index 0000000000..bb25e8bfd2 --- /dev/null +++ b/assets/themes/parkings/parkings.json @@ -0,0 +1,31 @@ +{ + "id": "parkings", + "title": { + "nl": "Parking", + "en": "Parking" + }, + "shortDescription": { + "nl": "Deze kaart toont verschillende parkeerplekken", + "en": "This map shows different parking spots" + }, + "description": { + "nl": "Deze kaart toont verschillende parkeerplekken", + "en": "This map shows different parking spots" + }, + "language": [ + "nl", + "en" + ], + "maintainer": "", + "icon": "./assets/themes/parkings/parkings.svg", + "version": "0", + "startLat": 51.20875, + "startLon": 3.22435, + "startZoom": 12, + "widenFactor": 0.05, + "socialImage": "", + "layers": [ + "parking" + ], + "roamingRenderings": [] +} \ No newline at end of file diff --git a/assets/themes/parkings/parkings.svg b/assets/themes/parkings/parkings.svg new file mode 100644 index 0000000000..5f4a83b081 --- /dev/null +++ b/assets/themes/parkings/parkings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/themes/personalLayout/personalLayout.json b/assets/themes/personal/personal.json similarity index 100% rename from assets/themes/personalLayout/personalLayout.json rename to assets/themes/personal/personal.json diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json index 571d0e1b6d..5acddf6b79 100644 --- a/assets/themes/shops/shops.json +++ b/assets/themes/shops/shops.json @@ -305,7 +305,6 @@ "questions", "reviews" ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/shops/shop.svg" }, diff --git a/assets/themes/speelplekken/speelplekken_temp.json b/assets/themes/speelplekken/speelplekken_temp.json index bf3975dc36..8672982153 100644 --- a/assets/themes/speelplekken/speelplekken_temp.json +++ b/assets/themes/speelplekken/speelplekken_temp.json @@ -28,7 +28,7 @@ "builtin": "play_forest", "override": { "source": { - "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", + "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", "geoJsonZoomLevel": 14, "isOsmCache": true }, @@ -255,21 +255,21 @@ "maxZoom": 16, "minNeededElements": 100 }, - "overrideAll": { - "+tagRenderings": [ - { - "render": "Maakt deel uit van {_part_of_walking_routes}", - "condition": "_part_of_walking_routes~*" + "roamingRenderings": [ + { + "render": "Maakt deel uit van {_part_of_walking_routes}", + "condition": "_part_of_walking_routes~*" + }, + { + "render": "Een kinder-reportage vinden jullie hier", + "freeform": { + "key": "video", + "type": "url" }, - { - "render": "Een kinder-reportage vinden jullie hier", - "freeform": { - "key": "video", - "type": "url" - }, - "question": "Wat is de link naar de video-reportage?" - } - ], + "question": "Wat is de link naar de video-reportage?" + } + ], + "overrideAll": { "isShown": { "render": "yes", "mappings": [ diff --git a/assets/themes/speelplekken/walking_route.svg b/assets/themes/speelplekken/walking_route.svg deleted file mode 100644 index 5e4fc3449f..0000000000 --- a/assets/themes/speelplekken/walking_route.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/assets/themes/surveillance_cameras/cam_left.svg b/assets/themes/surveillance/cam_left.svg similarity index 100% rename from assets/themes/surveillance_cameras/cam_left.svg rename to assets/themes/surveillance/cam_left.svg diff --git a/assets/themes/surveillance_cameras/cam_right.svg b/assets/themes/surveillance/cam_right.svg similarity index 100% rename from assets/themes/surveillance_cameras/cam_right.svg rename to assets/themes/surveillance/cam_right.svg diff --git a/assets/themes/surveillance_cameras/custom_theme.css b/assets/themes/surveillance/custom_theme.css similarity index 100% rename from assets/themes/surveillance_cameras/custom_theme.css rename to assets/themes/surveillance/custom_theme.css diff --git a/assets/themes/surveillance_cameras/dome.svg b/assets/themes/surveillance/dome.svg similarity index 100% rename from assets/themes/surveillance_cameras/dome.svg rename to assets/themes/surveillance/dome.svg diff --git a/assets/themes/surveillance_cameras/license_info.json b/assets/themes/surveillance/license_info.json similarity index 100% rename from assets/themes/surveillance_cameras/license_info.json rename to assets/themes/surveillance/license_info.json diff --git a/assets/themes/surveillance_cameras/logo.svg b/assets/themes/surveillance/logo.svg similarity index 100% rename from assets/themes/surveillance_cameras/logo.svg rename to assets/themes/surveillance/logo.svg diff --git a/assets/themes/surveillance_cameras/surveillance_cameras.json b/assets/themes/surveillance/surveillance.json similarity index 94% rename from assets/themes/surveillance_cameras/surveillance_cameras.json rename to assets/themes/surveillance/surveillance.json index 62c17e76a6..6090c3eee6 100644 --- a/assets/themes/surveillance_cameras/surveillance_cameras.json +++ b/assets/themes/surveillance/surveillance.json @@ -25,7 +25,7 @@ "zh_Hant" ], "maintainer": "", - "icon": "./assets/themes/surveillance_cameras/logo.svg", + "icon": "./assets/themes/surveillance/logo.svg", "version": "0", "startLat": 0, "startLon": 0, diff --git a/assets/themes/trails/license_info.json b/assets/themes/trails/license_info.json new file mode 100644 index 0000000000..90a51b9edf --- /dev/null +++ b/assets/themes/trails/license_info.json @@ -0,0 +1,12 @@ +[ + { + "authors": [ + "Freepik" + ], + "path": "trails.svg", + "license": "CC-BY", + "sources": [ + "https://www.flaticon.com/free-icon/trail_3273172?term=trail&page=1&position=11&page=1&position=11&related_id=3273172&origin=search" + ] + } +] \ No newline at end of file diff --git a/assets/themes/trails/trails.json b/assets/themes/trails/trails.json new file mode 100644 index 0000000000..e52b6f22fb --- /dev/null +++ b/assets/themes/trails/trails.json @@ -0,0 +1,31 @@ +{ + "id": "trails", + "title": { + "nl": "Wandeltochten", + "en": "Trails" + }, + "shortDescription": { + "nl": "Deze kaart toont verschillende wandeltochten", + "en": "This map shows trails" + }, + "description": { + "nl": "Deze kaart toont alle verschillende wandeltochten", + "en": "This map shows trails" + }, + "language": [ + "nl", + "en" + ], + "maintainer": "", + "icon": "./assets/themes/trails/trails.svg", + "version": "0", + "startLat": 51.20875, + "startLon": 3.22435, + "startZoom": 8, + "widenFactor": 0.05, + "socialImage": "", + "layers": [ + "trail" + ], + "roamingRenderings": [] +} \ No newline at end of file diff --git a/assets/themes/trails/trails.svg b/assets/themes/trails/trails.svg new file mode 100644 index 0000000000..62ec11778e --- /dev/null +++ b/assets/themes/trails/trails.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/themes/waste_basket/license_info.json b/assets/themes/waste_basket/license_info.json new file mode 100644 index 0000000000..502b9b8a24 --- /dev/null +++ b/assets/themes/waste_basket/license_info.json @@ -0,0 +1,10 @@ +[ + { + "authors": [], + "path": "waste_basket.svg", + "license": "CC0", + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Waste-basket-12.svg" + ] + } +] \ No newline at end of file diff --git a/assets/themes/waste_basket/waste_basket.json b/assets/themes/waste_basket/waste_basket.json new file mode 100644 index 0000000000..1ddfa7cccc --- /dev/null +++ b/assets/themes/waste_basket/waste_basket.json @@ -0,0 +1,96 @@ +{ + "id": "waste_basket", + "title": { + "en": "Waste Basket", + "nl": "Vuilnisbak" + }, + "shortDescription": { + "en": "Throw away waste", + "nl": "Afval weggooien" + }, + "description": { + "en": "This is a public waste basket, thrash can, where you can throw away your thrash.", + "nl": "Dit is een publieke vuilnisbak waar je je afval kan weggooien." + }, + "language": [ + "en", + "nl" + ], + "maintainer": "", + "icon": "./assets/themes/waste_basket/waste_basket.svg", + "version": "7/7/2021", + "startLat": 0, + "startLon": 0, + "startZoom": 1, + "widenFactor": 0.05, + "socialImage": "", + "layers": [ + { + "id": "waste_basket", + "name": { + "en": "Waste Basket", + "nl": "Vuilnisbak" + }, + "minzoom": 12, + "source": { + "osmTags": { + "and": [ + "amenity=waste_basket" + ] + } + }, + "title": { + "render": { + "en": "Waste Basket", + "nl": "Vuilnisbak" + } + }, + "description": { + "en": "This is a public waste basket, thrash can, where you can throw away your thrash.", + "nl": "Dit is een publieke vuilnisbak waar je je afval kan weggooien." + }, + "tagRenderings": [], + "icon": { + "render": "./assets/themes/waste_basket/waste_basket.svg" + }, + "width": { + "render": "8" + }, + "iconSize": { + "render": "40,40,center", + "mappings": [ + { + "if": { + "and": [ + "amenity=waste_basket" + ] + }, + "then": { + "en": "Waste Basket", + "nl": "Vuilnisbak" + } + } + ] + }, + "color": { + "render": "#00f" + }, + "presets": [ + { + "tags": [ + "amenity=waste_basket" + ], + "title": { + "en": "Waste Basket", + "nl": "Vuilnisbak" + }, + "description": { + "en": "Throw away waste", + "nl": "Afval weggooien" + } + } + ] + } + ], + "roamingRenderings": [] +} \ No newline at end of file diff --git a/assets/themes/waste_basket/waste_basket.svg b/assets/themes/waste_basket/waste_basket.svg new file mode 100644 index 0000000000..f9fb4b29c4 --- /dev/null +++ b/assets/themes/waste_basket/waste_basket.svg @@ -0,0 +1,26 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/themes/widths/width.json b/assets/themes/widths/width.json index 298b9a1281..5ee75eb7ef 100644 --- a/assets/themes/widths/width.json +++ b/assets/themes/widths/width.json @@ -69,7 +69,10 @@ "freeform": { "key": "width:carriageway", "type": "length", - "helperArgs": [21, "map"] + "helperArgs": [ + 21, + "map" + ] } }, { @@ -144,7 +147,6 @@ "render": "{_width:needed}m nodig in het totaal" } ], - "hideUnderlayingFeaturesMinPercentage": 0, "icon": { "render": "./assets/themes/widths/icon.svg" }, diff --git a/index.css b/index.css index 0bd790f55c..804f0774fb 100644 --- a/index.css +++ b/index.css @@ -93,10 +93,26 @@ svg, img { display: unset; } +.mapcontrol svg path{ + fill: var(--subtle-detail-color-contrast) !important; +} + a { color: var(--foreground-color); } +btn { + margin-top: 0.25rem; + margin-right: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + --tw-text-opacity: 1; + color: var(--catch-detail-color-contrast); + --tw-bg-opacity: 1; + background-color: var(--catch-detail-color); +} + .h-min { height: min-content; } diff --git a/langs/en.json b/langs/en.json index aac35335c1..94ec9719c5 100644 --- a/langs/en.json +++ b/langs/en.json @@ -27,6 +27,14 @@ "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", "pickTheme": "Pick a theme below to get started." }, + "split": { + "split": "Split", + "cancel": "Cancel", + "inviteToSplit": "Split this road", + "loginToSplit": "You must be logged in to split a road", + "splitTitle": "Choose on the map where to split this road", + "hasBeenSplit": "This way has been split" + }, "delete": { "delete": "Delete", "cancel": "Cancel", @@ -56,7 +64,7 @@ "loginWithOpenStreetMap": "Login with OpenStreetMap", "welcomeBack": "You are logged in, welcome back!", "loginToStart": "Login to answer this question", - "openStreetMapIntro": "

An Open Map

Wouldn't it be cool if there was a single map, which everyone could freely use and edit? A single place to store all geo-information? Then, all those websites with different, small and incompatible maps (which are always outdated) wouldn't be needed anymore.

OpenStreetMap is this map. The map data can be used for free (with attribution and publication of changes to that data). On top of that, everyone can freely add new data and fix errors. This website uses OpenStreetMap as well. All the data is from there, and your answers and corrections are added there as well.

A ton of people and application already use OpenStreetMap: Maps.me, OsmAnd, but also the maps at Facebook, Instagram, Apple-maps and Bing-maps are (partly) powered by OpenStreetMap. If you change something here, it'll be reflected in those applications too - after their next update!

", + "openStreetMapIntro": "

An Open Map

Wouldn't it be cool if there was a single map, which everyone could freely use and edit? A single place to store all geo-information? Then, all those websites with different, small and incompatible maps (which are always outdated) wouldn't be needed anymore.

OpenStreetMap is this map. The map data can be used for free (with attribution and publication of changes to that data). On top of that, everyone can freely add new data and fix errors. This website uses OpenStreetMap as well. All the data is from there, and your answers and corrections are added there as well.

A ton of people and application already use OpenStreetMap: Organic Maps, OsmAnd, but also the maps at Facebook, Instagram, Apple-maps and Bing-maps are (partly) powered by OpenStreetMap. If you change something here, it'll be reflected in those applications too - after their next update!

", "search": { "search": "Search a location", "searching": "Searching…", @@ -150,8 +158,14 @@ "title": "Select layers" }, "download": { + "title": "Download visible data", "downloadGeojson": "Download visible data as geojson", - "licenseInfo": "

Copyright notice

The provided is available under ODbL. Reusing this data is free for any purpose, but
  • the attribution © OpenStreetMap contributors
  • Any change to this data must be republished under the same license
. Please see the full copyright notice for details" + "downloadGeoJsonHelper": "Compatible with QGIS, OsmAnd, ArcGIS, ESRI, ...", + "downloadCSV": "Download as CSV", + "downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, ...", + "includeMetaData": "Include metadata (last editor, calculated values, ...)", + "licenseInfo": "

Copyright notice

The provided is available under ODbL. Reusing this data is free for any purpose, but
  • the attribution © OpenStreetMap contributors is required
  • Any change to this data must be republished under the same license
Please read the full copyright notice for details", + "noDataLoaded": "No data is loaded yet. Download will be available soon" }, "weekdays": { "abbreviations": { diff --git a/langs/ja.json b/langs/ja.json index a2ecbc58c2..908681485d 100644 --- a/langs/ja.json +++ b/langs/ja.json @@ -126,7 +126,7 @@ "goToInbox": "受信トレイを開く", "fewChangesBefore": "新しいポイントを追加する前に、既存のポイントに関するいくつかの質問に答えてください。", "readYourMessages": "新しいポイントを追加する前に、OpenStreetMapのメッセージをすべて読んでください。", - "openStreetMapIntro": "

オープン地図

誰もが自由に使用して編集できる1つのマップがあればクールではないでしょうか?すべての地理情報を格納するための単一の場所?そうすれば、異なる、小さくて互換性のない地図(常に時代遅れのもの)を持つウェブサイトは、必要なくなるでしょう。

OpenStreetMapこそが地図です。地図データは、(データ変更の公開と帰属表示をすれば)無料で利用できます。さらに、誰でも自由に新しいデータを追加したり、間違いを修正したりすることができます。このサイトでもOpenStreetMapを使っています。すべてのデータは、OSMからのものであり、あなたの答えと訂正もOSMに追加されます。

すでに多くの人やアプリケーションがOpenStreetMapを使っています:Maps.meや、OsmAndなど。Fさらに、acebook、Instagram、Apple-maps、Bing-mapsも(部分的に)OpenStreetMapを利用しています。ここで何かを変更すると、次の更新後にアプリケーションにも反映されます。

", + "openStreetMapIntro": "

オープン地図

誰もが自由に使用して編集できる1つのマップがあればクールではないでしょうか?すべての地理情報を格納するための単一の場所?そうすれば、異なる、小さくて互換性のない地図(常に時代遅れのもの)を持つウェブサイトは、必要なくなるでしょう。

OpenStreetMapこそが地図です。地図データは、(データ変更の公開と帰属表示をすれば)無料で利用できます。さらに、誰でも自由に新しいデータを追加したり、間違いを修正したりすることができます。このサイトでもOpenStreetMapを使っています。すべてのデータは、OSMからのものであり、あなたの答えと訂正もOSMに追加されます。

すでに多くの人やアプリケーションがOpenStreetMapを使っています:Maps.meや、OsmAndなど。Fさらに、acebook、Instagram、Apple-maps、Bing-mapsも(部分的に)OpenStreetMapを利用しています。ここで何かを変更すると、次の更新後にアプリケーションにも反映されます。

", "noNameCategory": "名前のない {category}", "pickLanguage": "言語を選択します: ", "number": "number", diff --git a/langs/layers/en.json b/langs/layers/en.json index 24a6bfac60..44c0b1495e 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -690,6 +690,144 @@ } } }, + "birdhide": { + "filter": { + "0": { + "options": { + "0": { + "question": "Wheelchair accessible" + } + } + } + } + }, + "crossings": { + "name": "Crossings", + "description": "Crossings for pedestrians and cyclists", + "title": { + "render": "Crossing", + "mappings": { + "0": { + "then": "Traffic signal" + }, + "1": { + "then": "Crossing with traffic signals" + } + } + }, + "presets": { + "0": { + "title": "Crossing", + "description": "Crossing for pedestrians and/or cyclists" + }, + "1": { + "title": "Traffic signal", + "description": "Traffic signal on a road" + } + }, + "tagRenderings": { + "0": { + "question": "What kind of crossing is this?", + "mappings": { + "0": { + "then": "Crossing, without traffic lights" + }, + "1": { + "then": "Crossing with traffic signals" + }, + "2": { + "then": "Zebra crossing" + } + } + }, + "1": { + "question": "Is this is a zebra crossing?", + "mappings": { + "0": { + "then": "This is a zebra crossing" + }, + "1": { + "then": "This is not a zebra crossing" + } + } + }, + "2": { + "question": "Is this crossing also for bicycles?", + "mappings": { + "0": { + "then": "A cyclist can use this crossing" + }, + "1": { + "then": "A cyclist can not use this crossing" + } + } + }, + "3": { + "question": "Does this crossing have an island in the middle?", + "mappings": { + "0": { + "then": "This crossing has an island in the middle" + }, + "1": { + "then": "This crossing does not have an island in the middle" + } + } + }, + "4": { + "question": "Does this crossing have tactile paving?", + "mappings": { + "0": { + "then": "This crossing has tactile paving" + }, + "1": { + "then": "This crossing does not have tactile paving" + }, + "2": { + "then": "This crossing has tactile paving, but is not correct" + } + } + }, + "5": { + "question": "Does this traffic light have a button to request green light?", + "mappings": { + "0": { + "then": "This traffic light has a button to request green light" + }, + "1": { + "then": "This traffic light does not have a button to request green light" + } + } + }, + "6": { + "question": "Can a cyclist turn right when the light is red?", + "mappings": { + "0": { + "then": "A cyclist can turn right if the light is red " + }, + "1": { + "then": "A cyclist can turn right if the light is red" + }, + "2": { + "then": "A cyclist can not turn right if the light is red" + } + } + }, + "7": { + "question": "Can a cyclist go straight on when the light is red?", + "mappings": { + "0": { + "then": "A cyclist can go straight on if the light is red " + }, + "1": { + "then": "A cyclist can go straight on if the light is red" + }, + "2": { + "then": "A cyclist can not go straight on if the light is red" + } + } + } + } + }, "defibrillator": { "name": "Defibrillators", "title": { @@ -1627,6 +1765,33 @@ } } }, + "trail": { + "name": "Trails", + "title": { + "render": "Trail" + }, + "tagRenderings": { + "1": { + "render": "The trail is {_length:km} kilometers long" + }, + "4": { + "mappings": { + "0": { + "then": "Blue trail" + }, + "1": { + "then": "Red trail" + }, + "2": { + "then": "Green trail" + }, + "3": { + "then": "Yellow trail" + } + } + } + } + }, "tree_node": { "name": "Tree", "title": { @@ -1769,5 +1934,17 @@ "question": "Do you want to add a description?" } } + }, + "visitor_information_centre": { + "name": "Visitor Information Centre", + "title": { + "render": "{name}", + "mappings": { + "1": { + "then": "{name}" + } + } + }, + "description": "A visitor center offers information about a specific attraction or place of interest where it is located." } } \ No newline at end of file diff --git a/langs/layers/nl.json b/langs/layers/nl.json index ca4156d911..c736486bbd 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -715,6 +715,9 @@ "then": "Vogelkijkhut" }, "2": { + "then": "Vogelkijktoren" + }, + "3": { "then": "Vogelkijkhut" } } @@ -770,6 +773,149 @@ "title": "Vogelkijkwand", "description": "Een vogelkijkwand waarachter men kan staan om vogels te kijken" } + }, + "filter": { + "0": { + "options": { + "0": { + "question": "Rolstoeltoegankelijk" + } + } + }, + "1": { + "options": { + "0": { + "question": "Enkel overdekte kijkhutten" + } + } + } + } + }, + "crossings": { + "name": "Oversteekplaatsen", + "description": "Oversteekplaatsen voor voetgangers en fietsers", + "title": { + "render": "Oversteekplaats", + "mappings": { + "0": { + "then": "Verkeerslicht" + }, + "1": { + "then": "Oversteektplaats met verkeerslichten" + } + } + }, + "presets": { + "0": { + "title": "Oversteekplaats", + "description": "Oversteekplaats voor voetgangers en/of fietsers" + }, + "1": { + "title": "Verkeerslicht", + "description": "Verkeerslicht op een weg" + } + }, + "tagRenderings": { + "0": { + "question": "Wat voor oversteekplaats is dit?", + "mappings": { + "0": { + "then": "Oversteekplaats, zonder verkeerslichten" + }, + "1": { + "then": "Oversteekplaats met verkeerslichten" + }, + "2": { + "then": "Zebrapad" + } + } + }, + "1": { + "question": "Is dit een zebrapad?", + "mappings": { + "0": { + "then": "Dit is een zebrapad" + }, + "1": { + "then": "Dit is niet een zebrapad" + } + } + }, + "2": { + "question": "Is deze oversteekplaats ook voor fietsers", + "mappings": { + "0": { + "then": "Een fietser kan deze oversteekplaats gebruiken" + }, + "1": { + "then": "Een fietser kan niet deze oversteekplaats gebruiken" + } + } + }, + "3": { + "question": "Heeft deze oversteekplaats een verkeerseiland in het midden?", + "mappings": { + "0": { + "then": "Deze oversteekplaats heeft een verkeerseiland in het midden" + }, + "1": { + "then": "Deze oversteekplaats heeft niet een verkeerseiland in het midden" + } + } + }, + "4": { + "question": "Heeft deze oversteekplaats een geleidelijn?", + "mappings": { + "0": { + "then": "Deze oversteekplaats heeft een geleidelijn" + }, + "1": { + "then": "Deze oversteekplaats heeft niet een geleidelijn" + }, + "2": { + "then": "Deze oversteekplaats heeft een geleidelijn, die incorrect is." + } + } + }, + "5": { + "question": "Heeft dit verkeerslicht een knop voor groen licht?", + "mappings": { + "0": { + "then": "Dit verkeerslicht heeft een knop voor groen licht" + }, + "1": { + "then": "Dit verkeerlicht heeft niet een knop voor groen licht" + } + } + }, + "6": { + "question": "Mag een fietser rechtsaf slaan als het licht rood is?", + "mappings": { + "0": { + "then": "Een fietser mag wel rechtsaf slaan als het licht rood is " + }, + "1": { + "then": "Een fietser mag wel rechtsaf slaan als het licht rood is" + }, + "2": { + "then": "Een fietser mag niet rechtsaf slaan als het licht rood is" + } + } + }, + "7": { + "question": "Mag een fietser rechtdoor gaan als het licht rood is?", + "mappings": { + "0": { + "then": "Een fietser mag wel rechtdoor gaan als het licht rood is " + }, + "1": { + "then": "Een fietser mag wel rechtdoor gaan als het licht rood is" + }, + "2": { + "then": "Een fietser mag niet rechtdoor gaan als het licht rood is" + } + } + } } }, "defibrillator": { @@ -1173,6 +1319,90 @@ "title": "Natuurreservaat", "description": "Voeg een ontbrekend, erkend natuurreservaat toe, bv. een gebied dat beheerd wordt door het ANB of natuurpunt" } + }, + "filter": { + "0": { + "options": { + "0": { + "question": "Vrij te bezoeken" + } + } + }, + "1": { + "options": { + "0": { + "question": "Alle natuurgebieden" + }, + "1": { + "question": "Honden mogen vrij rondlopen" + }, + "2": { + "question": "Honden welkom aan de leiband" + } + } + } + } + }, + "parking": { + "name": "parking", + "title": { + "render": "Parking", + "mappings": { + "0": { + "then": "{name:nl}" + }, + "1": { + "then": "{name}" + }, + "2": { + "then": "Fietsenstalling" + } + } + }, + "description": "Parking", + "tagRenderings": { + "1": { + "render": "De toegankelijkheid van dit gebied is: {access:description}", + "question": "Is dit gebied toegankelijk?", + "mappings": { + "0": { + "then": "Vrij toegankelijk" + }, + "1": { + "then": "Niet toegankelijk" + }, + "2": { + "then": "Niet toegankelijk, want privégebied" + }, + "3": { + "then": "Toegankelijk, ondanks dat het privegebied is" + }, + "4": { + "then": "Enkel toegankelijk met een gids of tijdens een activiteit" + }, + "5": { + "then": "Toegankelijk mits betaling" + } + } + }, + "2": { + "render": "Beheer door {operator}", + "question": "Wie beheert dit pad?", + "mappings": { + "0": { + "then": "Dit gebied wordt beheerd door Natuurpunt" + }, + "1": { + "then": "Dit gebied wordt beheerd door {operator}" + } + } + } + }, + "presets": { + "0": { + "title": "Paden", + "description": "Voeg een ontbrekend, erkend pad toe." + } } }, "picnic_table": { @@ -1846,6 +2076,74 @@ } } }, + "trail": { + "name": "Wandeltochten", + "title": { + "render": "Wandeltocht" + }, + "tagRenderings": { + "1": { + "render": "Deze wandeling is {_length:km} kilometer lang" + }, + "2": { + "question": "Wat is de naam van deze wandeling?", + "render": "Deze wandeling heet {name}" + }, + "3": { + "render": "Beheer door {operator}", + "question": "Wie beheert deze wandeltocht?", + "mappings": { + "0": { + "then": "Dit gebied wordt beheerd door Natuurpunt" + }, + "1": { + "then": "Dit gebied wordt beheerd door {operator}" + } + } + }, + "4": { + "question": "Welke kleur heeft deze wandeling?", + "render": "Deze wandeling heeft kleur {colour}", + "mappings": { + "0": { + "then": "Blauwe wandeling" + }, + "1": { + "then": "Rode wandeling" + }, + "2": { + "then": "Groene wandeling" + }, + "3": { + "then": "Gele wandeling" + } + } + }, + "5": { + "question": "Is deze wandeling toegankelijk met de rolstoel?", + "mappings": { + "0": { + "then": "deze wandeltocht is toegankelijk met de rolstoel" + }, + "1": { + "then": "deze wandeltocht is niet toegankelijk met de rolstoel" + } + } + }, + "6": { + "question": "Is deze wandeltocht toegankelijk met de buggy?", + "mappings": { + "0": { + "then": "deze wandeltocht is toegankelijk met de buggy" + }, + "1": { + "then": "deze wandeltocht is niet toegankelijk met de buggy" + } + } + } + }, + "description": "Aangeduide wandeltochten" + }, "tree_node": { "name": "Boom", "title": { @@ -1999,5 +2297,79 @@ } } } + }, + "visitor_information_centre": { + "name": "Bezoekerscentrum", + "title": { + "render": "{name}", + "mappings": { + "0": { + "then": "{name:nl}" + }, + "1": { + "then": "{name}" + } + } + }, + "description": "Een bezoekerscentrum biedt informatie over een specifieke attractie of bezienswaardigheid waar het is gevestigd." + }, + "watermill": { + "name": "watermolens", + "title": { + "render": "Watermolens", + "mappings": { + "0": { + "then": "{name:nl}" + }, + "1": { + "then": "{name}" + } + } + }, + "description": "Watermolens", + "tagRenderings": { + "1": { + "render": "De toegankelijkheid van dit gebied is: {access:description}", + "question": "Is dit gebied toegankelijk?", + "mappings": { + "0": { + "then": "Vrij toegankelijk" + }, + "1": { + "then": "Niet toegankelijk" + }, + "2": { + "then": "Niet toegankelijk, want privégebied" + }, + "3": { + "then": "Toegankelijk, ondanks dat het privegebied is" + }, + "4": { + "then": "Enkel toegankelijk met een gids of tijdens een activiteit" + }, + "5": { + "then": "Toegankelijk mits betaling" + } + } + }, + "2": { + "render": "Beheer door {operator}", + "question": "Wie beheert dit pad?", + "mappings": { + "0": { + "then": "Dit gebied wordt beheerd door Natuurpunt" + }, + "1": { + "then": "Dit gebied wordt beheerd door {operator}" + } + } + } + }, + "presets": { + "0": { + "title": "Paden", + "description": "Voeg een ontbrekend, erkend pad toe." + } + } } } \ No newline at end of file diff --git a/langs/layers/ru.json b/langs/layers/ru.json index c0f5353343..74a19a9ccb 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -1057,6 +1057,7 @@ } }, "viewpoint": { + "name": "Смотровая площадка", "presets": { "0": { "title": "Смотровая площадка" @@ -1069,7 +1070,6 @@ "1": { "question": "Вы хотите добавить описание?" } - }, - "name": "Смотровая площадка" + } } -} +} \ No newline at end of file diff --git a/langs/layers/zh_Hanå¨s.json b/langs/layers/zh_Hanå¨s.json new file mode 100644 index 0000000000..8a25d1e17e --- /dev/null +++ b/langs/layers/zh_Hanå¨s.json @@ -0,0 +1,9 @@ +{ + "bench": { + "tagRenderings": { + "3": { + "render": "材质: {material}" + } + } + } +} \ No newline at end of file diff --git a/langs/nl.json b/langs/nl.json index 31b6109ff4..9b54b354d0 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -69,7 +69,7 @@ "emailOf": "Wat is het email-adres van {category}?", "emailIs": "Het email-adres van {category} is {email}" }, - "openStreetMapIntro": "

Een open kaart

Zou het niet fantastisch zijn als er een open kaart zou zijn die door iedereen aangepast én gebruikt kan worden? Een kaart waar iedereen zijn interesses aan zou kunnen toevoegen? Dan zouden er geen duizend-en-één verschillende kleine kaartjes, websites, ... meer nodig zijn

OpenStreetMap is deze open kaart. Je mag de kaartdata gratis gebruiken (mits bronvermelding en herpublicatie van aanpassingen). Daarenboven mag je de kaart ook gratis aanpassen als je een account maakt. Ook deze website is gebaseerd op OpenStreetMap. Als je hier een vraag beantwoord, gaat het antwoord daar ook naartoe

Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar Maps.me, OsmAnd, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...
Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!

Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.

", + "openStreetMapIntro": "

Een open kaart

Zou het niet fantastisch zijn als er een open kaart zou zijn die door iedereen aangepast én gebruikt kan worden? Een kaart waar iedereen zijn interesses aan zou kunnen toevoegen? Dan zouden er geen duizend-en-één verschillende kleine kaartjes, websites, ... meer nodig zijn

OpenStreetMap is deze open kaart. Je mag de kaartdata gratis gebruiken (mits bronvermelding en herpublicatie van aanpassingen). Daarenboven mag je de kaart ook gratis aanpassen als je een account maakt. Ook deze website is gebaseerd op OpenStreetMap. Als je hier een vraag beantwoord, gaat het antwoord daar ook naartoe

Tenslotte zijn er reeds vele gebruikers van OpenStreetMap. Denk maar Organic Maps, OsmAnd, verschillende gespecialiseerde routeplanners, de achtergrondkaarten op Facebook, Instagram,...
Zelfs Apple Maps en Bing-Maps gebruiken OpenStreetMap in hun kaarten!

Kortom, als je hier een punt toevoegd of een vraag beantwoord, zal dat na een tijdje ook in al dié applicaties te zien zijn.

", "attribution": { "attributionTitle": "Met dank aan", "attributionContent": "

Alle data is voorzien door OpenStreetMap, gratis en vrij te hergebruiken onder de Open DataBase Licentie.

", diff --git a/langs/ru.json b/langs/ru.json index 19feb61d8f..af43acac2e 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -121,7 +121,7 @@ "attributionContent": "

Все данные предоставлены OpenStreetMap, свободное повторное использование согласно Open DataBase License.

", "attributionTitle": "Уведомление об авторстве" }, - "openStreetMapIntro": "

Свободная карта

Было бы здорово если бы была одна карта, которую каждый может свободно использовать и редактировать? Использовать как общее хранилище для всей гео-информации? Тогда, все сайты с разной, неполной и несовместимой информацией (которая обычно устарела) будут больше не нужны.

OpenStreetMap такая карта. Данные карты могу быть свободно использованы (с аннотацией и публикацией изменений к данным). Более того, каждый может свободно добавлять информацию и исправлять ошибки. Этот сайт также использует OpenStreetMap. Все данные берутся оттуда, а ваши ответы и исправления отправляются обратно туда.

Огромное количество людей уже использует OpenStreetMap: Maps.me, OsmAnd, а также карты в Facebook, Instagram, Apple-карты и Bing-карты (частично) используют OpenStreetMap. Если вы что-то измените здесь, это также будет отражено в этих приложениях - после их следующего обновления!

" + "openStreetMapIntro": "

Свободная карта

Было бы здорово если бы была одна карта, которую каждый может свободно использовать и редактировать? Использовать как общее хранилище для всей гео-информации? Тогда, все сайты с разной, неполной и несовместимой информацией (которая обычно устарела) будут больше не нужны.

OpenStreetMap такая карта. Данные карты могу быть свободно использованы (с аннотацией и публикацией изменений к данным). Более того, каждый может свободно добавлять информацию и исправлять ошибки. Этот сайт также использует OpenStreetMap. Все данные берутся оттуда, а ваши ответы и исправления отправляются обратно туда.

Огромное количество людей уже использует OpenStreetMap: Organic Maps, OsmAnd, а также карты в Facebook, Instagram, Apple-карты и Bing-карты (частично) используют OpenStreetMap. Если вы что-то измените здесь, это также будет отражено в этих приложениях - после их следующего обновления!

" }, "index": { "pickTheme": "Выберите тему ниже, чтобы начать.", diff --git a/langs/shared-questions/de.json b/langs/shared-questions/de.json index 6faff774e2..5d859a6940 100644 --- a/langs/shared-questions/de.json +++ b/langs/shared-questions/de.json @@ -3,30 +3,30 @@ "phone": { "question": "Was ist die Telefonnummer von {name}?" }, + "email": { + "question": "Was ist die Mail-Adresse von {name}?" + }, + "website": { + "question": "Was ist die Website von {name}?" + }, + "description": { + "question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.
Bitte keine bereits erhobenen Informationen." + }, "opening_hours": { "question": "Was sind die Öffnungszeiten von {name}?", "render": "

Öffnungszeiten

{opening_hours_table(opening_hours)}" }, "level": { + "question": "In welchem Stockwerk befindet sich dieses Objekt?", + "render": "Befindet sich im {level}ten Stock", "mappings": { - "2": { - "then": "Ist im ersten Stock" - }, "1": { "then": "Ist im Erdgeschoss" + }, + "2": { + "then": "Ist im ersten Stock" } - }, - "render": "Befindet sich im {level}ten Stock", - "question": "In welchem Stockwerk befindet sich dieses Objekt?" - }, - "description": { - "question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.
Bitte keine bereits erhobenen Informationen." - }, - "website": { - "question": "Was ist die Website von {name}?" - }, - "email": { - "question": "Was ist die Mail-Adresse von {name}?" + } } } -} +} \ No newline at end of file diff --git a/langs/shared-questions/pt_BR.json b/langs/shared-questions/pt_BR.json index 9c577c3966..7016de7f0e 100644 --- a/langs/shared-questions/pt_BR.json +++ b/langs/shared-questions/pt_BR.json @@ -1,30 +1,30 @@ { "undefined": { - "level": { - "render": "Localizado no {level}o andar", - "mappings": { - "2": { - "then": "Localizado no primeiro andar" - }, - "1": { - "then": "Localizado no térreo" - }, - "0": { - "then": "Localizado no subsolo" - } - } - }, - "opening_hours": { - "question": "Qual o horário de funcionamento de {name}?" - }, - "website": { - "question": "Qual o site de {name}?" + "phone": { + "question": "Qual o número de telefone de {name}?" }, "email": { "question": "Qual o endereço de e-mail de {name}?" }, - "phone": { - "question": "Qual o número de telefone de {name}?" + "website": { + "question": "Qual o site de {name}?" + }, + "opening_hours": { + "question": "Qual o horário de funcionamento de {name}?" + }, + "level": { + "render": "Localizado no {level}o andar", + "mappings": { + "0": { + "then": "Localizado no subsolo" + }, + "1": { + "then": "Localizado no térreo" + }, + "2": { + "then": "Localizado no primeiro andar" + } + } } } -} +} \ No newline at end of file diff --git a/langs/shared-questions/ru.json b/langs/shared-questions/ru.json index 93c56dc441..d1df15a6fc 100644 --- a/langs/shared-questions/ru.json +++ b/langs/shared-questions/ru.json @@ -17,18 +17,18 @@ "render": "

Часы работы

{opening_hours_table(opening_hours)}" }, "level": { + "render": "Расположено на {level}ом этаже", "mappings": { - "2": { - "then": "Расположено на первом этаже" + "0": { + "then": "Расположено под землей" }, "1": { "then": "Расположено на первом этаже" }, - "0": { - "then": "Расположено под землей" + "2": { + "then": "Расположено на первом этаже" } - }, - "render": "Расположено на {level}ом этаже" + } } } -} +} \ No newline at end of file diff --git a/langs/themes/de.json b/langs/themes/de.json index 6a0b29fad1..48972b7683 100644 --- a/langs/themes/de.json +++ b/langs/themes/de.json @@ -3,7 +3,7 @@ "title": "AED-Karte öffnen", "description": "Auf dieser Karte kann man nahe gelegene Defibrillatoren finden und markieren" }, - "artworks": { + "artwork": { "title": "Freie Kunstwerk-Karte", "description": "Willkommen bei der Freien Kunstwerk-Karte, einer Karte von Statuen, Büsten, Grafitti, ... auf der ganzen Welt", "layers": { diff --git a/langs/themes/en.json b/langs/themes/en.json index f5db488b97..94744363f7 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -3,7 +3,7 @@ "title": "Open AED Map", "description": "On this map, one can find and mark nearby defibrillators" }, - "artworks": { + "artwork": { "title": "Open Artwork Map", "description": "Welcome to Open Artwork Map, a map of statues, busts, grafittis and other artwork all over the world", "layers": { @@ -692,7 +692,519 @@ } } }, - "fietsstraten": { + "cycle_infra": { + "title": "Bicycle infrastructure", + "shortDescription": "A map where you can view and edit things related to the bicycle infrastructure.", + "description": "A map where you can view and edit things related to the bicycle infrastructure. Made during #osoc21.", + "layers": { + "0": { + "name": "Cycleways", + "title": { + "render": "Cycleways", + "mappings": { + "0": { + "then": "Bike road" + }, + "1": { + "then": "Shared lane" + }, + "2": { + "then": "Bike lane" + }, + "3": { + "then": "Bike road next to the road" + }, + "4": { + "then": "Cyclestreet" + } + } + }, + "tagRenderings": { + "0": { + "question": "What kind of cycleway is there?", + "mappings": { + "0": { + "then": "There is a shared lane" + }, + "1": { + "then": "There is a lane next to the road (seperated with paint)" + }, + "2": { + "then": "There is a track, but no cycleway drawn seperately from this road on the map." + }, + "3": { + "then": "There is a seperately drawn cycleway" + }, + "4": { + "then": "There is no cycleway" + }, + "5": { + "then": "There is no cycleway" + } + } + }, + "1": { + "question": "Is this street lit?", + "mappings": { + "0": { + "then": "This street is lit" + }, + "1": { + "then": "This road is not lit" + }, + "2": { + "then": "This road is lit at night" + }, + "3": { + "then": "This road is lit 24/7" + } + } + }, + "2": { + "question": "Is this a cyclestreet?", + "mappings": { + "0": { + "then": "This is a cyclestreet, and a 30km/h zone." + }, + "1": { + "then": "This is a cyclestreet" + }, + "2": { + "then": "This is not a cyclestreet." + } + } + }, + "3": { + "render": "The maximum speed on this road is {maxspeed} km/h", + "mappings": { + "0": { + "then": "The maximum speed is 20 km/h" + }, + "1": { + "then": "The maximum speed is 30 km/h" + }, + "2": { + "then": "The maximum speed is 50 km/h" + }, + "3": { + "then": "The maximum speed is 70 km/h" + }, + "4": { + "then": "The maximum speed is 90 km/h" + } + }, + "question": "What is the maximum speed in this street?" + }, + "4": { + "render": "This cyleway is made of {cycleway:surface}", + "mappings": { + "0": { + "then": "This cycleway is made of wood" + }, + "1": { + "then": "This cycleway is made of concrete" + }, + "2": { + "then": "This cycleway is made of cobblestone" + }, + "3": { + "then": "This cycleway is made of asphalt" + }, + "4": { + "then": "This cycleway is paved" + }, + "5": { + "then": "This cycleway is paved with stones with a flat top" + }, + "6": { + "then": "This cycleway is made of gravel" + }, + "7": { + "then": "This cycleway is unhardened" + }, + "8": { + "then": "This cycleway is made of gravel" + }, + "9": { + "then": "This cycleway is made of fine gravel" + }, + "10": { + "then": "This cycleway is made of pebblestone" + }, + "11": { + "then": "This cycleway is made from raw ground" + } + }, + "question": "What is the surface of the cycleway made from?" + }, + "5": { + "question": "What is the smoothness of this cycleway?", + "mappings": { + "0": { + "then": "Usable for thin rollers: rollerblade, skateboard" + }, + "1": { + "then": "Usable for thin wheels: racing bike" + }, + "2": { + "then": "Usable for normal wheels: city bike, wheelchair, scooter" + }, + "3": { + "then": "Usable for robust wheels: trekking bike, car, rickshaw" + }, + "4": { + "then": "Usable for vehicles with high clearance: light duty off-road vehicle" + }, + "5": { + "then": "Usable for off-road vehicles: heavy duty off-road vehicle" + }, + "6": { + "then": "Usable for specialized off-road vehicles: tractor, ATV" + }, + "7": { + "then": "Impassable / No wheeled vehicle" + } + } + }, + "6": { + "render": "This road is made of {surface}", + "mappings": { + "0": { + "then": "This street is made of wood" + }, + "1": { + "then": "This street is made of concrete" + }, + "2": { + "then": "This street is made of cobblestone" + }, + "3": { + "then": "This street is made of asphalt" + }, + "4": { + "then": "This street is paved" + }, + "5": { + "then": "This street is made from fine gravel" + }, + "6": { + "then": "This street is made from cobblestone" + }, + "7": { + "then": "This street is made from paving stones" + } + }, + "question": "What is the surface of the street made from?" + }, + "7": { + "question": "What is the smoothness of this street?", + "mappings": { + "0": { + "then": "Usable for thin rollers: rollerblade, skateboard" + }, + "1": { + "then": "Usable for thin wheels: racing bike" + }, + "2": { + "then": "Usable for normal wheels: city bike, wheelchair, scooter" + }, + "3": { + "then": "Usable for robust wheels: trekking bike, car, rickshaw" + }, + "4": { + "then": "Usable for vehicles with high clearance: light duty off-road vehicle" + }, + "5": { + "then": "Usable for off-road vehicles: heavy duty off-road vehicle" + }, + "6": { + "then": "Usable for specialized off-road vehicles: tractor, ATV" + }, + "7": { + "then": "Impassable / No wheeled vehicle" + } + } + }, + "8": { + "render": "The carriage width of this road is {width:carriageway}m", + "question": "What is the carriage width of this road (in meters)?" + }, + "9": { + "question": "What traffic sign does this cycleway have?", + "mappings": { + "0": { + "then": "Compulsory cycleway " + }, + "1": { + "then": "Compulsory cycleway (with supplementary sign) " + }, + "2": { + "then": "Segregated foot/cycleway " + }, + "3": { + "then": "Unsegregated foot/cycleway " + }, + "4": { + "then": "No traffic sign present" + } + } + }, + "10": { + "question": "What traffic sign does this cycleway have?", + "mappings": { + "0": { + "then": "Compulsory cycleway " + }, + "1": { + "then": "Compulsory cycleway (with supplementary sign) " + }, + "2": { + "then": "Segregated foot/cycleway " + }, + "3": { + "then": "Unsegregated foot/cycleway " + }, + "4": { + "then": "No traffic sign present" + } + } + }, + "11": { + "question": "Does the traffic sign D7 () have a supplementary sign?", + "mappings": { + "0": { + "then": "" + }, + "1": { + "then": "" + }, + "2": { + "then": "" + }, + "3": { + "then": "" + }, + "4": { + "then": "" + }, + "5": { + "then": "" + }, + "6": { + "then": "No supplementary traffic sign present" + } + } + }, + "12": { + "question": "Does the traffic sign D7 () have a supplementary sign?", + "mappings": { + "0": { + "then": "" + }, + "1": { + "then": "" + }, + "2": { + "then": "" + }, + "3": { + "then": "" + }, + "4": { + "then": "" + }, + "5": { + "then": "" + }, + "6": { + "then": "No supplementary traffic sign present" + } + } + }, + "13": { + "render": "The buffer besides this cycleway is {cycleway:buffer} m", + "question": "How wide is the gap between the cycleway and the road?" + }, + "14": { + "question": "How is this cycleway seperated from the road?", + "mappings": { + "0": { + "then": "This cycleway is seperated by a dashed line" + }, + "1": { + "then": "This cycleway is seperated by a solid line" + }, + "2": { + "then": "This cycleway is seperated by a parking lane" + }, + "3": { + "then": "This cycleway is seperated by a kerb" + } + } + }, + "15": { + "question": "How is this cycleway seperated from the road?", + "mappings": { + "0": { + "then": "This cycleway is seperated by a dashed line" + }, + "1": { + "then": "This cycleway is seperated by a solid line" + }, + "2": { + "then": "This cycleway is seperated by a parking lane" + }, + "3": { + "then": "This cycleway is seperated by a kerb" + } + } + } + } + }, + "1": { + "name": "All streets", + "description": "Layer to mark new cycleways", + "title": { + "render": "Street" + }, + "tagRenderings": { + "0": { + "question": "Is there a cycleway?", + "mappings": { + "0": { + "then": "There is a shared lane" + }, + "1": { + "then": "There is a lane next to the road (seperated with paint)" + }, + "2": { + "then": "There is a track, but no cycleway drawn seperately from this road on the map." + }, + "3": { + "then": "There is a seperately drawn cycleway" + }, + "4": { + "then": "There is no cycleway known here" + }, + "5": { + "then": "There is no cycleway" + }, + "6": { + "then": "There is no cycleway" + } + }, + "render": "Unknown cycleway situation" + }, + "1": { + "question": "Is this a cyclestreet?", + "mappings": { + "0": { + "then": "This is a cyclestreet, and a 30km/h zone." + }, + "1": { + "then": "This is a cyclestreet" + }, + "2": { + "then": "This is not a cyclestreet." + } + } + } + } + }, + "2": { + "name": "Barriers", + "description": "Obstacles while cycling, such as bollards and cycle barriers", + "title": { + "render": "Barrier", + "mappings": { + "0": { + "then": "Bollard" + }, + "1": { + "then": "Cycling Barrier" + } + } + }, + "presets": { + "0": { + "title": "Bollard", + "description": "A bollard in the road" + }, + "1": { + "title": "Cycle barrier", + "description": "Cycle barrier, slowing down cyclists" + } + }, + "tagRenderings": { + "0": { + "question": "Can a bicycle go past this barrier?", + "mappings": { + "0": { + "then": "A cyclist can go past this." + }, + "1": { + "then": "A cyclist can not go past this." + } + } + }, + "1": { + "question": "What kind of bollard is this?", + "mappings": { + "0": { + "then": "Removable bollard" + }, + "1": { + "then": "Fixed bollard" + }, + "2": { + "then": "Bollard that can be folded down" + }, + "3": { + "then": "Flexible bollard, usually plastic" + }, + "4": { + "then": "Rising bollard" + } + } + }, + "2": { + "question": "What kind of cycling barrier is this?", + "mappings": { + "0": { + "then": "Single, just two barriers with a space inbetween " + }, + "1": { + "then": "Double, two barriers behind each other " + }, + "2": { + "then": "Triple, three barriers behind each other " + }, + "3": { + "then": "Squeeze gate, gap is smaller at top, than at the bottom " + } + } + }, + "3": { + "render": "Maximum width: {maxwidth:physical} m", + "question": "How wide is the gap left over besides the barrier?" + }, + "4": { + "render": "Space between barriers (along the length of the road): {width:seperation} m", + "question": "How much space is there between the barriers (along the length of the road)?" + }, + "5": { + "render": "Width of opening: {width:opening} m", + "question": "How wide is the smallest opening next to the barriers?" + }, + "6": { + "render": "Overlap: {overlap} m", + "question": "How much overlap do the barriers have?" + } + } + } + } + }, + "cyclestreets": { "title": "Cyclestreets", "shortDescription": "A map of cyclestreets", "description": "A cyclestreet is is a street where motorized traffic is not allowed to overtake cyclists. They are signposted by a special traffic sign. Cyclestreets can be found in the Netherlands and Belgium, but also in Germany and France. ", @@ -1084,6 +1596,11 @@ "shortDescription": "This theme shows all (touristic) maps that OpenStreetMap knows of", "description": "On this map you can find all maps OpenStreetMap knows - typically a big map on an information board showing the area, city or region, e.g. a tourist map on the back of a billboard, a map of a nature reserve, a map of cycling networks in the region, ...)

If a map is missing, you can easily map this map on OpenStreetMap." }, + "natuurpunt": { + "title": "Nature Reserves", + "shortDescription": "This map shows the nature reserves of Natuurpunt", + "description": "On this map you can find all the nature reserves that Natuurpunt offers " + }, "openwindpowermap": { "title": "OpenWindPowerMap", "description": "A map for showing and editing wind turbines.", @@ -1153,6 +1670,11 @@ } } }, + "parkings": { + "title": "Parking", + "shortDescription": "This map shows different parking spots", + "description": "This map shows different parking spots" + }, "personal": { "title": "Personal theme", "description": "Create a personal theme based on all the available layers of all themes" @@ -1252,9 +1774,41 @@ "title": "Open Toilet Map", "description": "A map of public toilets" }, + "trails": { + "title": "Trails", + "shortDescription": "This map shows trails", + "description": "This map shows trails" + }, "trees": { "title": "Trees", "shortDescription": "Map all the trees", "description": "Map all the trees!" + }, + "waste_basket": { + "title": "Waste Basket", + "shortDescription": "Throw away waste", + "description": "This is a public waste basket, thrash can, where you can throw away your thrash.", + "layers": { + "0": { + "name": "Waste Basket", + "title": { + "render": "Waste Basket" + }, + "description": "This is a public waste basket, thrash can, where you can throw away your thrash.", + "iconSize": { + "mappings": { + "0": { + "then": "Waste Basket" + } + } + }, + "presets": { + "0": { + "title": "Waste Basket", + "description": "Throw away waste" + } + } + } + } } } \ No newline at end of file diff --git a/langs/themes/es.json b/langs/themes/es.json index 03668a5d5c..91a123d7f0 100644 --- a/langs/themes/es.json +++ b/langs/themes/es.json @@ -3,7 +3,7 @@ "title": "Mapa abierto de desfibriladores (DEA)", "description": "En este mapa , cualquiera puede encontrar y marcar los desfibriladores externos automáticos más cercanos" }, - "artworks": { + "artwork": { "description": "Bienvenido a Open Artwork Map, un mapa de estatuas, bustos, grafitis y otras obras de arte de todo el mundo", "layers": { "0": { diff --git a/langs/themes/fr.json b/langs/themes/fr.json index 25bff80566..9d927b6183 100644 --- a/langs/themes/fr.json +++ b/langs/themes/fr.json @@ -3,7 +3,7 @@ "title": "Carte des défibrillateurs (DAE)", "description": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs" }, - "artworks": { + "artwork": { "title": "Carte ouverte des œuvres d'art", "description": "Bienvenue sur la carte ouverte des œuvres d'art, une carte des statues, fresques, ... du monde entier", "layers": { diff --git a/langs/themes/hu.json b/langs/themes/hu.json index 10a97477f6..cb8f0fd7ff 100644 --- a/langs/themes/hu.json +++ b/langs/themes/hu.json @@ -2,7 +2,7 @@ "aed": { "title": "Nyílt AED Térkép" }, - "artworks": { + "artwork": { "title": "Nyít Műalkotás Térkép" } } \ No newline at end of file diff --git a/langs/themes/id.json b/langs/themes/id.json index 31649d682e..c436e745eb 100644 --- a/langs/themes/id.json +++ b/langs/themes/id.json @@ -3,7 +3,7 @@ "title": "Buka Peta AED", "description": "Di peta ini, seseorang dapat menemukan dan menandai defibrillator terdekat" }, - "artworks": { + "artwork": { "title": "Buka Peta Karya Seni", "description": "Selamat datang di Open Artwork Map, peta untuk patung, grafiti, dan karya seni lain di seluruh dunia", "layers": { diff --git a/langs/themes/it.json b/langs/themes/it.json index 89189d3aec..313e2715f9 100644 --- a/langs/themes/it.json +++ b/langs/themes/it.json @@ -3,7 +3,7 @@ "title": "Mappa dei defibrillatori (DAE)", "description": "Su questa mappa puoi trovare e segnalare i defibrillatori nelle vicinanze" }, - "artworks": { + "artwork": { "title": "Mappa libera delle opere d'arte", "description": "Benvenuto/a sulla mappa libera dell’arte, una mappa delle statue, i busti, i graffiti e le altre realizzazioni artistiche di tutto il mondo", "layers": { @@ -396,7 +396,7 @@ } } }, - "fietsstraten": { + "cyclestreets": { "roamingRenderings": { "0": { "mappings": { diff --git a/langs/themes/ja.json b/langs/themes/ja.json index 2754b88120..3dfd0a2e4a 100644 --- a/langs/themes/ja.json +++ b/langs/themes/ja.json @@ -3,7 +3,7 @@ "title": "オープンAEDマップ", "description": "この地図では近くにある除細動器(AED)を見つけてマークします" }, - "artworks": { + "artwork": { "title": "オープン アートワーク マップ", "description": "オープン アートワーク マップへようこそ。世界中の銅像や胸像、壁の落書きなどのアートワークの地図です", "layers": { @@ -583,7 +583,7 @@ } } }, - "fietsstraten": { + "cyclestreets": { "title": "Cyclestreets", "shortDescription": "cyclestreetsの地図", "description": "cyclestreetとは、自動車がサイクリストを追い越すことができない道です。専用の道路標識で表示されます。Cyclestreetsはオランダやベルギーにもありますが、ドイツやフランスにもあります。 ", diff --git a/langs/themes/nb_NO.json b/langs/themes/nb_NO.json index 9792f7c736..29f14be05b 100644 --- a/langs/themes/nb_NO.json +++ b/langs/themes/nb_NO.json @@ -2,7 +2,7 @@ "aed": { "title": "Åpne AED-kart" }, - "artworks": { + "artwork": { "layers": { "0": { "name": "Kunstverk", @@ -213,7 +213,7 @@ } } }, - "fietsstraten": { + "cyclestreets": { "shortDescription": "Et kart over sykkelveier", "roamingRenderings": { "0": { diff --git a/langs/themes/nl.json b/langs/themes/nl.json index e1ab2203fd..5f39530463 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -7,7 +7,7 @@ "title": "Open AED-kaart - Brugge edition", "description": "Op deze kaart kan je informatie over AEDs vinden en verbeteren + een export van de brugse defibrillatoren" }, - "artworks": { + "artwork": { "title": "Kunstwerkenkaart", "description": "Welkom op de Open Kunstwerken Kaart", "layers": { @@ -580,7 +580,452 @@ } } }, - "fietsstraten": { + "cycle_infra": { + "title": "Fietsinfrastructuur", + "shortDescription": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken.", + "description": "Een kaart waar je info over de fietsinfrastructuur kan bekijken en bewerken. Gemaakt tijdens #osoc21.", + "layers": { + "0": { + "name": "Fietspaden", + "title": { + "render": "Fietspaden", + "mappings": { + "0": { + "then": "Fietsweg" + }, + "1": { + "then": "Fietssuggestiestrook" + }, + "2": { + "then": "Fietsstrook" + }, + "3": { + "then": "Fietsweg naast de weg" + }, + "4": { + "then": "Fietsstraat" + } + } + }, + "tagRenderings": { + "0": { + "question": "Wat voor fietspad is hier?", + "mappings": { + "0": { + "then": "Er is een fietssuggestiestrook" + }, + "1": { + "then": "Er is een fietspad aangrenzend aan de weg (gescheiden met verf)" + }, + "2": { + "then": "Er is een fietspad (los van de weg), maar geen fietspad afzonderlijk getekend naast deze weg." + }, + "3": { + "then": "Er is een apart getekend fietspad." + }, + "4": { + "then": "Er is geen fietspad aanwezig" + }, + "5": { + "then": "Er is geen fietspad aanwezig" + } + } + }, + "1": { + "question": "Is deze weg verlicht?", + "mappings": { + "0": { + "then": "Deze weg is verlicht" + }, + "1": { + "then": "Deze weg is niet verlicht" + }, + "2": { + "then": "Deze weg is 's nachts verlicht" + }, + "3": { + "then": "Deze weg is 24/7 verlicht" + } + } + }, + "2": { + "question": "Is dit een fietsstraat?", + "mappings": { + "0": { + "then": "Dit is een fietstraat, en dus een 30km/h zone" + }, + "1": { + "then": "Dit is een fietstraat" + }, + "2": { + "then": "Dit is niet een fietstraat" + } + } + }, + "3": { + "render": "De maximumsnelheid op deze weg is {maxspeed} km/u", + "mappings": { + "0": { + "then": "De maximumsnelheid is 20 km/u" + }, + "1": { + "then": "De maximumsnelheid is 30 km/u" + }, + "2": { + "then": "De maximumsnelheid is 50 km/u" + }, + "3": { + "then": "De maximumsnelheid is 70 km/u" + }, + "4": { + "then": "De maximumsnelheid is 90 km/u" + } + }, + "question": "Wat is de maximumsnelheid in deze straat?" + }, + "4": { + "render": "Dit fietspad is gemaakt van {cycleway:surface}", + "mappings": { + "0": { + "then": "Dit fietspad is gemaakt van hout" + }, + "1": { + "then": "Dit fietspad is gemaakt van beton" + }, + "2": { + "then": "Dit fietspad is gemaakt van kasseien" + }, + "3": { + "then": "Dit fietspad is gemaakt van asfalt" + }, + "4": { + "then": "Dit fietspad is geplaveid" + }, + "5": { + "then": "Dit fietspad is geplaveid met stenen" + }, + "6": { + "then": "Dit fietspad is gemaakt van grind" + }, + "7": { + "then": "Dit fietspad is onverhard" + }, + "8": { + "then": "Dit fietspad is gemaakt van grind" + }, + "9": { + "then": "Dit fietspad is gemaakt van fijn grind" + }, + "10": { + "then": "Dit fietspad is gemaakt van kiezelsteentjes" + }, + "11": { + "then": "Dit fietspad is gemaakt van aarde" + } + }, + "question": "Waaruit is het oppervlak van het fietspad van gemaakt?" + }, + "5": { + "question": "Wat is de kwaliteit van dit fietspad?" + }, + "6": { + "render": "Deze weg is gemaakt van {surface}", + "mappings": { + "0": { + "then": "Deze straat is gemaakt van hout" + }, + "1": { + "then": "Deze straat is gemaakt van beton" + }, + "2": { + "then": "Deze straat is gemaakt van kasseien" + }, + "3": { + "then": "Deze straat is gemaakt van asfalt" + }, + "4": { + "then": "Deze straat is geplaveid" + }, + "5": { + "then": "Deze straat is gemaakt van fijn grind" + }, + "6": { + "then": "Deze straat is gemaakt van kasseien" + }, + "7": { + "then": "Deze straat is gemaakt van geplaveide stenen" + } + }, + "question": "Waaruit is het oppervlak van de straat gemaakt?" + }, + "7": { + "question": "Wat is de kwaliteit van deze straat?" + }, + "8": { + "render": "De breedte van deze rijbaan in deze straat is {width:carriageway}m", + "question": "Hoe breed is de rijbaan in deze straat (in meters)?" + }, + "9": { + "question": "Welk verkeersbord heeft dit fietspad?", + "mappings": { + "0": { + "then": "Verplicht fietspad " + }, + "1": { + "then": "Verplicht fietspad (met onderbord)" + }, + "2": { + "then": "Afgescheiden voet-/fietspad " + }, + "3": { + "then": "Gedeeld voet-/fietspad " + }, + "4": { + "then": "Geen verkeersbord aanwezig" + } + } + }, + "10": { + "question": "Welk verkeersbord heeft dit fietspad?", + "mappings": { + "0": { + "then": "Verplicht fietspad " + }, + "1": { + "then": "Verplicht fietspad (met onderbord)" + }, + "2": { + "then": "Afgescheiden voet-/fietspad " + }, + "3": { + "then": "Gedeeld voet-/fietspad " + }, + "4": { + "then": "Geen verkeersbord aanwezig" + } + } + }, + "11": { + "question": "Heeft het verkeersbord D7 () een onderbord?", + "mappings": { + "0": { + "then": "" + }, + "1": { + "then": "" + }, + "2": { + "then": "" + }, + "3": { + "then": "" + }, + "4": { + "then": "" + }, + "5": { + "then": "" + }, + "6": { + "then": "Geen onderbord aanwezig" + } + } + }, + "12": { + "question": "Heeft het verkeersbord D7 () een onderbord?", + "mappings": { + "0": { + "then": "" + }, + "1": { + "then": "" + }, + "2": { + "then": "" + }, + "3": { + "then": "" + }, + "4": { + "then": "" + }, + "5": { + "then": "" + }, + "6": { + "then": "Geen onderbord aanwezig" + } + } + }, + "13": { + "render": "De schrikafstand van dit fietspad is {cycleway:buffer} m", + "question": "Hoe breed is de ruimte tussen het fietspad en de weg?" + }, + "14": { + "question": "Hoe is dit fietspad gescheiden van de weg?", + "mappings": { + "0": { + "then": "Dit fietspad is gescheiden van de weg met een onderbroken streep" + }, + "1": { + "then": "Dit fietspad is gescheiden van de weg met een doorgetrokken streep" + }, + "2": { + "then": "Dit fietspad is gescheiden van de weg met parkeervakken" + }, + "3": { + "then": "Dit fietspad is gescheiden van de weg met een stoeprand" + } + } + }, + "15": { + "question": "Hoe is dit fietspad gescheiden van de weg?", + "mappings": { + "0": { + "then": "Dit fietspad is gescheiden van de weg met een onderbroken streep" + }, + "1": { + "then": "Dit fietspad is gescheiden van de weg met een doorgetrokken streep" + }, + "2": { + "then": "Dit fietspad is gescheiden van de weg met parkeervakken" + }, + "3": { + "then": "Dit fietspad is gescheiden van de weg met een stoeprand" + } + } + } + } + }, + "1": { + "name": "Alle straten", + "description": "Laag waar je nieuwe fietspaden kan aanduiden", + "title": { + "render": "Straat" + }, + "tagRenderings": { + "0": { + "question": "Is er een fietspad?", + "mappings": { + "0": { + "then": "Er is een fietssugestiestrook" + }, + "1": { + "then": "Er is een fietspad aangrenzend aan de weg (gescheiden met verf)" + }, + "2": { + "then": "Er is een fietspad (los van de weg), maar geen fietspad afzonderlijk getekend naast deze weg." + }, + "3": { + "then": "Er is een apart getekend fietspad." + }, + "4": { + "then": "Er is geen fietspad bekend hier" + }, + "5": { + "then": "Er is geen fietspad aanwezig" + }, + "6": { + "then": "Er is geen fietspad aanwezig" + } + }, + "render": "Onbekende fietspad situatie" + }, + "1": { + "question": "Is dit een fietsstraat?", + "mappings": { + "0": { + "then": "Dit is een fietstraat, en dus een 30km/h zone" + }, + "1": { + "then": "Dit is een fietstraat" + }, + "2": { + "then": "Dit is niet een fietstraat" + } + } + } + } + }, + "2": { + "name": "Barrières", + "description": "Hindernissen tijdens het fietsen, zoals paaltjes en fietshekjes", + "title": { + "render": "Barrière", + "mappings": { + "0": { + "then": "Paaltje" + }, + "1": { + "then": "Fietshekjes" + } + } + }, + "presets": { + "0": { + "title": "Paaltje", + "description": "Een paaltje in de weg" + }, + "1": { + "title": "Fietshekjes", + "description": "Fietshekjes, voor het afremmen van fietsers" + } + }, + "tagRenderings": { + "0": { + "question": "Kan een fietser langs deze barrière?", + "mappings": { + "0": { + "then": "Een fietser kan hier langs." + }, + "1": { + "then": "Een fietser kan hier niet langs." + } + } + }, + "1": { + "question": "Wat voor soort paal is dit?", + "mappings": { + "0": { + "then": "Verwijderbare paal" + }, + "1": { + "then": "Vaste paal" + }, + "2": { + "then": "Paal die platgevouwen kan worden" + }, + "3": { + "then": "Flexibele paal, meestal plastic" + }, + "4": { + "then": "Verzonken poller" + } + } + }, + "2": { + "question": "Wat voor fietshekjes zijn dit?", + "mappings": { + "0": { + "then": "Enkelvoudig, slechts twee hekjes met ruimte ertussen " + }, + "1": { + "then": "Dubbel, twee hekjes achter elkaar " + }, + "2": { + "then": "Drievoudig, drie hekjes achter elkaar " + } + } + }, + "3": { + "render": "Maximumbreedte: {maxwidth:physical} m", + "question": "Hoe breed is de ruimte naast de barrière?" + } + } + } + } + }, + "cyclestreets": { "title": "Fietsstraten", "shortDescription": "Een kaart met alle gekende fietsstraten", "description": "Een fietsstraat is een straat waar
  • automobilisten geen fietsers mogen inhalen
  • Er een maximumsnelheid van 30km/u geldt
  • Fietsers gemotoriseerde voortuigen links mogen inhalen
  • Fietsers nog steeds voorrang aan rechts moeten verlenen - ook aan auto's en voetgangers op het zebrapad


Op deze open kaart kan je alle gekende fietsstraten zien en kan je ontbrekende fietsstraten aanduiden. Om de kaart aan te passen, moet je je aanmelden met OpenStreetMap en helemaal inzoomen tot straatniveau.", @@ -592,7 +1037,7 @@ "then": "Deze straat is een fietsstraat (en dus zone 30)" }, "1": { - "then": "Deze straat is een fietsstraat" + "then": "Deze straat i een fietsstraat" }, "2": { "then": "Deze straat wordt binnenkort een fietsstraat" @@ -820,7 +1265,7 @@ } } }, - "boomgaarden": { + "fruit_trees": { "title": "Open Boomgaardenkaart", "shortDescription": "Boomgaarden en fruitbomen", "description": "Op deze kaart vindt je boomgaarden en fruitbomen", @@ -934,6 +1379,11 @@ "shortDescription": "Deze kaart bevat informatie voor natuurliefhebbers", "description": "Op deze kaart vind je informatie voor natuurliefhebbers, zoals info over het natuurgebied waar je inzit, vogelkijkhutten, informatieborden, ..." }, + "natuurpunt": { + "title": "Natuurgebieden", + "shortDescription": "Deze kaart toont de natuurgebieden van Natuurpunt", + "description": "Op deze kaart vind je alle natuurgebieden die Natuurpunt ter beschikking stelt" + }, "openwindpowermap": { "units": { "0": { @@ -961,6 +1411,11 @@ } } }, + "parkings": { + "title": "Parking", + "shortDescription": "Deze kaart toont verschillende parkeerplekken", + "description": "Deze kaart toont verschillende parkeerplekken" + }, "personal": { "title": "Persoonlijk thema", "description": "Stel je eigen thema samen door lagen te combineren van alle andere themas" @@ -1115,11 +1570,43 @@ "title": "Open Toilettenkaart", "description": "Een kaart met openbare toiletten" }, + "trails": { + "title": "Wandeltochten", + "shortDescription": "Deze kaart toont verschillende wandeltochten", + "description": "Deze kaart toont alle verschillende wandeltochten" + }, "trees": { "title": "Bomen", "shortDescription": "Breng bomen in kaart", "description": "Breng bomen in kaart!" }, + "waste_basket": { + "title": "Vuilnisbak", + "shortDescription": "Afval weggooien", + "description": "Dit is een publieke vuilnisbak waar je je afval kan weggooien.", + "layers": { + "0": { + "name": "Vuilnisbak", + "title": { + "render": "Vuilnisbak" + }, + "description": "Dit is een publieke vuilnisbak waar je je afval kan weggooien.", + "iconSize": { + "mappings": { + "0": { + "then": "Vuilnisbak" + } + } + }, + "presets": { + "0": { + "title": "Vuilnisbak", + "description": "Afval weggooien" + } + } + } + } + }, "width": { "title": "Straatbreedtes", "shortDescription": "Is de straat breed genoeg?", diff --git a/langs/themes/ru.json b/langs/themes/ru.json index 74f15b8753..ccc9571e3f 100644 --- a/langs/themes/ru.json +++ b/langs/themes/ru.json @@ -3,7 +3,7 @@ "title": "Открытая карта АВД (Автоматизированных внешних дефибрилляторов)", "description": "На этой карте вы можете найти и отметить ближайшие дефибрилляторы" }, - "artworks": { + "artwork": { "title": "Открытая карта произведений искусства", "description": "Добро пожаловать на Open Artwork Map, карту статуй, бюстов, граффити и других произведений искусства по всему миру", "layers": { @@ -407,7 +407,7 @@ } } }, - "fietsstraten": { + "cyclestreets": { "layers": { "2": { "name": "Все улицы", diff --git a/langs/themes/sv.json b/langs/themes/sv.json index 2a881c73c7..59d44792db 100644 --- a/langs/themes/sv.json +++ b/langs/themes/sv.json @@ -3,7 +3,7 @@ "title": "Öppna AED-karta", "description": "På denna karta kan man hitta och markera närliggande defibrillatorer" }, - "artworks": { + "artwork": { "title": "Öppen konstverkskarta" } } \ No newline at end of file diff --git a/langs/themes/zh_Hant.json b/langs/themes/zh_Hant.json index 73063a9d78..cd855a4a4f 100644 --- a/langs/themes/zh_Hant.json +++ b/langs/themes/zh_Hant.json @@ -3,7 +3,7 @@ "title": "開放AED地圖", "description": "在這份地圖上,你可以找到與標記附近的除顫器" }, - "artworks": { + "artwork": { "title": "開放藝術品地圖", "description": "歡迎來到開放藝術品地圖,這份地圖會顯示全世界的雕像、半身像、塗鴉以及其他類型的藝術品", "layers": { @@ -294,7 +294,7 @@ } } }, - "fietsstraten": { + "cyclestreets": { "title": "單車街道", "shortDescription": "單車街道的地圖", "description": "單車街道是機動車輛受限制,只允許單車通行的道路。通常會有路標顯示特別的交通指標。單車街道通常在荷蘭、比利時看到,但德國與法國也有。 ", diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index 4aae9b875c..e98a31535f 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -101,7 +101,7 @@ "attributionContent": "

所有資料由開放街圖提供,在開放資料庫授權條款之下自由再利用。

", "attributionTitle": "署名通知" }, - "openStreetMapIntro": "

開放的地圖

如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。

開放街圖就是這樣的地圖,人人都能免費這些圖資 (只要署名與公開變動這資料)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。

許多人與應用程式已經採用開放街圖了:Maps.meOsmAnd,還有 Facebook、Instagram,蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!

", + "openStreetMapIntro": "

開放的地圖

如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。

開放街圖就是這樣的地圖,人人都能免費這些圖資 (只要署名與公開變動這資料)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。

許多人與應用程式已經採用開放街圖了:Organic MapsOsmAnd,還有 Facebook、Instagram,蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!

", "questions": { "emailIs": "{category} 的電子郵件地址是{email}", "emailOf": "{category} 的電子郵件地址是?", diff --git a/package-lock.json b/package-lock.json index cb3d63083b..03a7bb70b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -934,6 +934,16 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/runtime-corejs3": { + "version": "7.14.8", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.8.tgz", + "integrity": "sha512-4dMD5QRBkumn45oweR0SxoNtt15oz3BUBAQ8cIx7HJqZTtE8zjpM0My8aHJHVnyf4XfRg6DNzaE1080WLBiC1w==", + "optional": true, + "requires": { + "core-js-pure": "^3.15.0", + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", @@ -1028,25 +1038,25 @@ } }, "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "requires": { - "@nodelib/fs.stat": "2.0.4", + "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "requires": { - "@nodelib/fs.scandir": "2.1.4", + "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, @@ -2714,6 +2724,12 @@ "integrity": "sha512-29GS75BE8asnTno3yB6ubOJOO0FboExEqNJy4bpz0GSmW/8wPTNL4h9h63c6s1uTrOopCmJYe/4yJLh5r92ZUA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, "@types/prompt-sync": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.1.0.tgz", @@ -2724,6 +2740,12 @@ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" }, + "@types/raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", + "optional": true + }, "@types/sizzle": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", @@ -2865,12 +2887,14 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2921,11 +2945,6 @@ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, - "array-flatten": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", - "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==" - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -3188,6 +3207,12 @@ } } }, + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", + "optional": true + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3223,6 +3248,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3233,6 +3259,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -3242,6 +3269,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3408,6 +3436,11 @@ "node-releases": "^1.1.71" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -3541,6 +3574,20 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001223.tgz", "integrity": "sha512-k/RYs6zc/fjbxTjaWZemeSmOjO0JJV+KguOBA3NwPup8uzxM1cMhR2BD9XmO86GuqaqTCO8CgkgH9Rz//vdDiA==" }, + "canvg": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz", + "integrity": "sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==", + "optional": true, + "requires": { + "@babel/runtime-corejs3": "^7.9.6", + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^5.0.5" + } + }, "cardinal": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-0.4.4.tgz", @@ -3583,7 +3630,8 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "cipher-base": { "version": "1.0.4", @@ -3651,7 +3699,8 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true }, "collection-visit": { "version": "1.0.0", @@ -3783,7 +3832,8 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true }, "constants-browserify": { "version": "1.0.0", @@ -3841,6 +3891,12 @@ } } }, + "core-js-pure": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.2.tgz", + "integrity": "sha512-D42L7RYh1J2grW8ttxoY1+17Y4wXZeKe7uyplAI3FkNQyI5OgBIAjUfFiTPfL1rs0qLpxaabITNbjKl1Sp82tA==", + "optional": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3956,6 +4012,15 @@ "timsort": "^0.3.0" } }, + "css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "optional": true, + "requires": { + "base64-arraybuffer": "^0.2.0" + } + }, "css-modules-loader-core": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", @@ -4298,6 +4363,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, "requires": { "mimic-response": "^2.0.0" } @@ -4318,7 +4384,8 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true }, "deep-is": { "version": "0.1.3", @@ -4398,7 +4465,8 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true }, "density-clustering": { "version": "1.3.0", @@ -4427,7 +4495,8 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true }, "detective": { "version": "5.2.0", @@ -4485,6 +4554,11 @@ } } }, + "dom-to-image-more": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-2.8.0.tgz", + "integrity": "sha512-YqlHI1i+TMuaKwkFRO5oDPjC3eWf+6Hln9rHZcnFYvmoXwCrGZmZ7BYXBJOjw5utYg2Lp+QF9YO96F7CsDC4eQ==" + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -4511,6 +4585,12 @@ "domelementtype": "1" } }, + "dompurify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.0.tgz", + "integrity": "sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw==", + "optional": true + }, "domutils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.3.0.tgz", @@ -4614,6 +4694,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, "requires": { "once": "^1.4.0" } @@ -4811,7 +4892,8 @@ "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true }, "extend": { "version": "3.0.2", @@ -4953,13 +5035,23 @@ "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" }, "fastq": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", - "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", + "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==", "requires": { "reusify": "^1.0.4" } }, + "fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -5058,7 +5150,8 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true }, "fs-extra": { "version": "9.1.0", @@ -5091,6 +5184,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -5105,12 +5199,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5210,7 +5306,8 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "dev": true }, "glob": { "version": "7.1.7", @@ -5349,7 +5446,8 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true }, "has-value": { "version": "1.0.0", @@ -5473,6 +5571,15 @@ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" }, + "html2canvas": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.1.4.tgz", + "integrity": "sha512-uHgQDwrXsRmFdnlOVFvHin9R7mdjjZvoBoXxicPR+NnucngkaLa5zIDW9fzMkiip0jSffyTyWedE8iVogYOeWg==", + "optional": true, + "requires": { + "css-line-break": "1.1.1" + } + }, "htmlnano": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-0.2.9.tgz", @@ -5728,6 +5835,15 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "requires": { + "import-from": "^3.0.0" + } + }, "import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -5737,6 +5853,23 @@ "resolve-from": "^3.0.0" } }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "incremental-convex-hull": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz", @@ -5777,7 +5910,8 @@ "ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true }, "is-absolute-url": { "version": "2.1.0", @@ -5937,6 +6071,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6232,6 +6367,12 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6293,6 +6434,28 @@ } } }, + "jspdf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.3.1.tgz", + "integrity": "sha512-1vp0USP1mQi1h7NKpwxjFgQkJ5ncZvtH858aLpycUc/M+r/RpWJT8PixAU7Cw/3fPd4fpC8eB/Bj42LnsR21YQ==", + "requires": { + "atob": "^2.1.2", + "btoa": "^1.2.1", + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "fflate": "^0.4.8", + "html2canvas": "^1.0.0-rc.5" + }, + "dependencies": { + "core-js": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz", + "integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==", + "optional": true + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7877,6 +8040,15 @@ "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.12.0.tgz", "integrity": "sha512-pU/mR4B+NbayBGCg5/88dmRq7t1EGiNPhsVGV3yqHuDn594vIwus4CiPVW0RtiKJNKg8Vf1pILAbFl0i+yk+lQ==" }, + "leaflet-simple-map-screenshoter": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/leaflet-simple-map-screenshoter/-/leaflet-simple-map-screenshoter-0.4.4.tgz", + "integrity": "sha512-n5r04/PxXvqPQUJH+kP+vYj1Sg231YITPwoPMmdHwe+nSB+NJtQS0emEh9BaXXIbkZxubxeWQ1mKXpJYOxCAmw==", + "requires": { + "dom-to-image-more": "^2.8.0", + "file-saver": "^2.0.2" + } + }, "leaflet.markercluster": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz", @@ -7905,11 +8077,23 @@ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.17.tgz", "integrity": "sha512-ElJki901OynMg1l+evooPH1VyHrECuLqpgc12z2BkK25dFU5lUKTuMHEYV2jXxvtns/PIuJax56cBeoSK7ANow==" }, + "lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true + }, "lineclip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/lineclip/-/lineclip-1.1.5.tgz", "integrity": "sha1-K/JgZ9lDVP6r+R5CdoI221YW/RM=" }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -8175,7 +8359,8 @@ "mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true }, "minimalistic-assert": { "version": "1.0.1", @@ -8237,7 +8422,8 @@ "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true }, "mocha": { "version": "2.5.3", @@ -8338,11 +8524,6 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "optional": true }, - "nanoid": { - "version": "3.1.22", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", - "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" - }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -8364,7 +8545,8 @@ "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true }, "nice-try": { "version": "1.0.5", @@ -8372,9 +8554,10 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-abi": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.26.0.tgz", - "integrity": "sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", + "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", + "dev": true, "requires": { "semver": "^5.4.1" }, @@ -8382,7 +8565,8 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -8470,11 +8654,6 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==" }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -8514,6 +8693,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -8537,7 +8717,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "nwsapi": { "version": "2.2.0", @@ -9010,6 +9191,23 @@ } } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, "parse-asn1": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", @@ -9149,9 +9347,9 @@ "integrity": "sha1-GN4vl+S/epVRrXURlCtUlverpmA=" }, "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, "pify": { "version": "2.3.0", @@ -9333,6 +9531,17 @@ "postcss": "^7.0.18" } }, + "postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "requires": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, "postcss-merge-longhand": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", @@ -9863,9 +10072,10 @@ "integrity": "sha512-eSToKjNLu0FiF76SSGMHjOFXYzAc/CJqi677Sq6hYvcvFCBtD6de/W5l+0IYPf7ypscqAfjCttxvTdMJt5Gj8Q==" }, "prebuild-install": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.2.tgz", - "integrity": "sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz", + "integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==", + "dev": true, "requires": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -9874,25 +10084,12 @@ "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^2.21.0", - "noop-logger": "^0.1.1", "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^3.0.3", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "simple-get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", - "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - } } }, "prelude-ls": { @@ -9965,6 +10162,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -9994,6 +10192,13 @@ "colorette": "^1.2.2", "nanoid": "^3.1.22", "source-map": "^0.6.1" + }, + "dependencies": { + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + } } }, "source-map": { @@ -10048,6 +10253,15 @@ "through2": "^2.0.0" } }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "requires": { + "performance-now": "^2.1.0" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10082,6 +10296,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -10386,6 +10601,12 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", + "optional": true + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -10543,7 +10764,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "set-value": { "version": "2.0.1", @@ -10608,18 +10830,17 @@ } }, "sharp": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.27.2.tgz", - "integrity": "sha512-w3FVoONPG/x5MXCc3wsjOS+b9h3CI60qkus6EPQU4dkT0BDm0PyGhDCK6KhtfT3/vbeOMOXAKFNSw+I3QGWkMA==", + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.28.3.tgz", + "integrity": "sha512-21GEP45Rmr7q2qcmdnjDkNP04Ooh5v0laGS5FDpojOO84D1DJwUijLiSq8XNNM6e8aGXYtoYRh3sVNdm8NodMA==", + "dev": true, "requires": { - "array-flatten": "^3.0.0", "color": "^3.1.3", "detect-libc": "^1.0.3", - "node-addon-api": "^3.1.0", - "npmlog": "^4.1.2", - "prebuild-install": "^6.0.1", - "semver": "^7.3.4", - "simple-get": "^4.0.0", + "node-addon-api": "^3.2.0", + "prebuild-install": "^6.1.2", + "semver": "^7.3.5", + "simple-get": "^3.1.0", "tar-fs": "^2.1.1", "tunnel-agent": "^0.6.0" }, @@ -10628,19 +10849,22 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } }, "node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -10673,31 +10897,18 @@ "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true }, "simple-get": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", - "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dev": true, "requires": { - "decompress-response": "^6.0.0", + "decompress-response": "^4.2.0", "once": "^1.3.1", "simple-concat": "^1.0.0" - }, - "dependencies": { - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } } }, "simple-swizzle": { @@ -10842,6 +11053,12 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -10954,6 +11171,12 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" }, + "stackblur-canvas": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz", + "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", + "optional": true + }, "static-eval": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz", @@ -11080,6 +11303,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11089,12 +11313,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11163,7 +11389,8 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true }, "stylehacks": { "version": "4.0.3", @@ -11200,6 +11427,12 @@ "has-flag": "^3.0.0" } }, + "svg-pathdata": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz", + "integrity": "sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==", + "optional": true + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -11226,62 +11459,131 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "tailwindcss": { - "version": "npm:@tailwindcss/postcss7-compat@2.1.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.1.2.tgz", - "integrity": "sha512-bH2kw6uyqLnDMP8wzDUsis5ovrsRzfHEyiL1McADvqlW54g6y0KVHX1xzO7PH8Fl5s0Sq8vDOAp4+3V8MEcZ9g==", + "version": "npm:@tailwindcss/postcss7-compat@2.2.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.7.tgz", + "integrity": "sha512-1QkWUEeLV1AoNipMCE6IlL7XYScGb+DAzaXy35ooMDvl0G8kCMHBNqGxyVAnTcK8gyJNUzkKXExkUnbjAndd/g==", + "dev": true, "requires": { - "@fullhuman/postcss-purgecss": "^3.1.3", + "arg": "^5.0.0", "autoprefixer": "^9", "bytes": "^3.0.0", - "chalk": "^4.1.0", - "chokidar": "^3.5.1", - "color": "^3.1.3", + "chalk": "^4.1.1", + "chokidar": "^3.5.2", + "color": "^3.2.0", + "cosmiconfig": "^7.0.0", "detective": "^5.2.0", - "didyoumean": "^1.2.1", + "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.5", - "fs-extra": "^9.1.0", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.0", "html-tags": "^3.1.0", + "is-glob": "^4.0.1", "lodash": "^4.17.21", "lodash.topath": "^4.5.2", - "modern-normalize": "^1.0.0", + "modern-normalize": "^1.1.0", "node-emoji": "^1.8.1", "normalize-path": "^3.0.0", - "object-hash": "^2.1.1", - "parse-glob": "^3.0.4", + "object-hash": "^2.2.0", "postcss": "^7", "postcss-functions": "^3", "postcss-js": "^2", + "postcss-load-config": "^3.1.0", "postcss-nested": "^4", - "postcss-selector-parser": "^6.0.4", + "postcss-selector-parser": "^6.0.6", "postcss-value-parser": "^4.1.0", "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", "quick-lru": "^5.1.1", "reduce-css-calc": "^2.1.8", - "resolve": "^1.20.0" + "resolve": "^1.20.0", + "tmp": "^0.2.1" }, "dependencies": { "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } }, + "arg": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", + "integrity": "sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ==", + "dev": true + }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -11289,17 +11591,183 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "color-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "glob-parent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.1.tgz", + "integrity": "sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "purgecss": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", + "dev": true, + "requires": { + "commander": "^6.0.0", + "glob": "^7.0.0", + "postcss": "^8.2.1", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", + "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + } + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -11310,6 +11778,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -11321,6 +11790,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -11333,6 +11803,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11409,6 +11880,26 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -12512,6 +13003,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -12572,7 +13064,14 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index 6f26034508..b978834dc7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "main": "index.js", "scripts": { "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", - "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", + "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", "test": "ts-node test/TestAll.ts", "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", @@ -20,7 +20,8 @@ "generate:layouts": "ts-node scripts/generateLayouts.ts", "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", - "generate:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", + "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre", + "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", "generate:contributor-list": "git log --pretty='%aN' | sort | uniq -c | sort -hr | sed 's/ *\\([0-9]*\\) \\(.*\\)$/{\"contributor\":\"\\2\", \"commits\":\\1}/' | tr '\\n' ',' | sed 's/^/{\"contributors\":[/' | sed 's/,$/]}/' > assets/contributors.json", @@ -28,7 +29,7 @@ "validate:licenses": "ts-node scripts/generateLicenseInfo.ts --report", "optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", "reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json", - "generate": "mkdir -p ./assets/generated && npm run reset:layeroverview && npm run generate:images && npm run generate:translations && npm run generate:licenses && npm run generate:licenses && npm run generate:layeroverview", + "generate": "mkdir -p ./assets/generated && npm run reset:layeroverview && npm run generate:images && npm run generate:translations && npm run generate:layeroverview", "build": "rm -rf dist/ && npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", "prepare-deploy": "npm run generate && npm run test && npm run generate:editor-layer-index && npm run generate:layeroverview && npm run generate:layouts && npm run build && rm -rf .cache && npm run generate:docs", "deploy:staging": "npm run prepare-deploy && rm -rf ~/git/pietervdvn.github.io/Staging/* && cp -r dist/* ~/git/pietervdvn.github.io/Staging/ && cd ~/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean", @@ -64,9 +65,11 @@ "escape-html": "^1.0.3", "i18next-client": "^1.11.4", "jquery": "^3.6.0", + "jspdf": "^2.3.1", "latlon2country": "^1.1.3", "leaflet": "^1.7.1", "leaflet-providers": "^1.10.2", + "leaflet-simple-map-screenshoter": "^0.4.4", "leaflet.markercluster": "^1.4.1", "libphonenumber": "0.0.10", "libphonenumber-js": "^1.7.55", @@ -79,8 +82,6 @@ "parcel": "^1.2.4", "postcss": "^7.0.36", "prompt-sync": "^4.2.0", - "sharp": "^0.27.0", - "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", "tslint": "^6.1.3" }, "devDependencies": { @@ -90,6 +91,8 @@ "fs": "0.0.1-security", "marked": "^2.0.0", "read-file": "^0.2.0", + "sharp": "^0.28.3", + "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.7", "ts-node": "^9.0.0", "ts-node-dev": "^1.0.0-pre.63", "tslint-no-circular-imports": "^0.7.0", diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index 174b1d3df0..3427071dd5 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -1,6 +1,5 @@ import {lstatSync, readdirSync, readFileSync} from "fs"; import {Utils} from "../Utils"; - Utils.runningFromConsole = true import * as https from "https"; import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; @@ -86,6 +85,10 @@ export default class ScriptUtils { }) } + + public static erasableLog(...text) { + process.stdout.write("\r "+text.join(" ")+" \r") + } public static sleep(ms) { if (ms <= 0) { @@ -104,7 +107,12 @@ export default class ScriptUtils { .filter(path => path.indexOf("license_info.json") < 0) .map(path => { try { - const parsed = JSON.parse(readFileSync(path, "UTF8")); + 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 parse file ", "./assets/layers/" + path, "due to ", e) @@ -118,7 +126,11 @@ export default class ScriptUtils { .filter(path => path.indexOf("license_info.json") < 0) .map(path => { try { - const parsed = JSON.parse(readFileSync(path, "UTF8")); + 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) diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index 733e798fc1..d76347d252 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -17,6 +17,7 @@ import MetaTagging from "../Logic/MetaTagging"; import LayerConfig from "../Customizations/JSON/LayerConfig"; import {GeoOperations} from "../Logic/GeoOperations"; import {UIEventSource} from "../Logic/UIEventSource"; +import * as fs from "fs"; import {TileRange} from "../Models/TileRange"; @@ -33,7 +34,7 @@ function createOverpassObject(theme: LayoutConfig) { if (layer.source.geojsonSource !== undefined) { // This layer defines a geoJson-source // SHould it be cached? - if (!layer.source.isOsmCacheLayer) { + if (layer.source.isOsmCacheLayer !== true) { continue; } } @@ -89,13 +90,24 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ await ScriptUtils.DownloadJSON(url) .then(json => { + if (json.elements.length === 0) { + console.log("Got an empty response!") + if ((json.remark ?? "").startsWith("runtime error")) { + console.error("Got a runtime error: ", json.remark) + failed++; + return + } + + } + + console.log("Got the response - writing 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+")") + console.log("Could not download - probably hit the rate limit; waiting a bit. (" + err + ")") failed++; return ScriptUtils.sleep(60000).then(() => console.log("Waiting is done")) }) @@ -119,7 +131,7 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { if (source === undefined) { continue; } - if (layer.source.isOsmCacheLayer) { + if (layer.source.isOsmCacheLayer !== undefined) { // Cached layers are not considered here continue; } @@ -129,14 +141,14 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { return allFeatures; } -async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { +function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { let processed = 0; const layerIndex = theme.LayerIndex(); for (let x = r.xstart; x <= r.xend; x++) { for (let y = r.ystart; y <= r.yend; y++) { processed++; const filename = rawJsonName(targetdir, x, y, r.zoomlevel) - console.log(" Post processing", processed, "/", r.total, filename) + ScriptUtils.erasableLog(" Post processing", processed, "/", r.total, filename) if (!existsSync(filename)) { console.error("Not found - and not downloaded. Run this script again!: " + filename) continue; @@ -144,7 +156,7 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, // 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 osmTime = new Date(rawOsm.osm3s.timestamp_osm_base); @@ -169,7 +181,7 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, // Extract the relationship information const relations = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(rawOsm)) - MetaTagging.addMetatags(featuresFreshness, new UIEventSource<{feature: any; freshness: Date}[]>(featuresFreshness) , relations, theme.layers, false); + MetaTagging.addMetatags(featuresFreshness, new UIEventSource<{ feature: any; freshness: Date }[]>(featuresFreshness), relations, theme.layers, false); for (const feature of geojson.features) { @@ -193,7 +205,7 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, delete feature["bbox"] } - const targetPath = geoJsonName(targetdir+".unfiltered", x, y, r.zoomlevel) + const targetPath = geoJsonName(targetdir + ".unfiltered", x, y, r.zoomlevel) // This is the geojson file containing all features writeFileSync(targetPath, JSON.stringify(geojson, null, " ")) @@ -201,11 +213,12 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, } } -async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { +function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { const z = r.zoomlevel; + const generated = {} // layer --> x --> y[] for (let x = r.xstart; x <= r.xend; x++) { for (let y = r.ystart; y <= r.yend; y++) { - const file = readFileSync(geoJsonName(targetdir+".unfiltered", x, y, z), "UTF8") + const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") for (const layer of theme.layers) { if (!layer.source.isOsmCacheLayer) { @@ -217,30 +230,86 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi .filter(f => f._matching_layer_id === layer.id) .filter(f => { const isShown = layer.isShown.GetRenderValue(f.properties).txt - if (isShown === "no") { - return false; - } - return true; + return isShown !== "no"; + }) const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); - console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length,")" ) + ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") if (geojson.features.length == 0) { - console.log("Not writing geojson file as it is empty", new_path) continue; } writeFileSync(new_path, JSON.stringify(geojson, null, " ")) + + if (generated[layer.id] === undefined) { + generated[layer.id] = {} + } + if (generated[layer.id][x] === undefined) { + generated[layer.id][x] = [] + } + generated[layer.id][x].push(y) + } - - } } + + for (const layer of theme.layers) { + const id = layer.id + const loaded = generated[id] + if(loaded === undefined){ + console.log("No features loaded for layer ",id) + continue; + } + writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) + } + } +async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) { + const allFeatures = [] + for (let x = r.xstart; x <= r.xend; x++) { + for (let y = r.ystart; y <= r.yend; y++) { + const read_path = geoJsonName(targetdir + "_" + layername, x, y, z); + if (!fs.existsSync(read_path)) { + continue; + } + const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features + const pointsOnly = features.map(f => { + + f.properties["_last_edit:timestamp"] = "1970-01-01" + + if (f.geometry.type === "Point") { + return f + } else { + return GeoOperations.centerpoint(f) + } + + }) + allFeatures.push(...pointsOnly) + } + } + + const featuresDedup = [] + const seen = new Set() + for (const feature of allFeatures) { + const id = feature.properties.id + if(seen.has(id)){ + continue + } + seen.add(id) + featuresDedup.push(feature) + } + + const geojson = { + "type": "FeatureCollection", + "features": featuresDedup + } + writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " ")) +} async function main(args: string[]) { if (args.length == 0) { - console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1") + console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]") return; } const themeName = args[0] @@ -275,8 +344,18 @@ async function main(args: string[]) { } while (failed > 0) const extraFeatures = await downloadExtraData(theme); - await postProcess(targetdir, tileRange, theme, extraFeatures) - await splitPerLayer(targetdir, tileRange, theme) + postProcess(targetdir, tileRange, theme, extraFeatures) + splitPerLayer(targetdir, tileRange, theme) + + if (args[7] === "--generate-point-overview") { + const targetLayers = args[8].split(",") + for (const targetLayer of targetLayers) { + if (!theme.layers.some(l => l.id === targetLayer)) { + throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",") + } + createOverview(targetdir, tileRange, zoomlevel, targetLayer) + } + } } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index d903867c84..44d4ac9ade 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -56,6 +56,10 @@ class LayerOverviewUtils { if (path != undefined && path.indexOf(expected) < 0) { errorCount.push("Layer is in an incorrect place. The path is " + path + ", but expected " + expected) } + if(layerJson["hideUnderlayingFeaturesMinPercentage"] !== undefined){ + errorCount.push("Layer "+layer.id+" contains an old 'hideUnderlayingFeaturesMinPercentage'") + } + for (const image of images) { if (image.indexOf("{") >= 0) { @@ -78,9 +82,9 @@ class LayerOverviewUtils { main(args: string[]) { - const lt = this.loadThemesAndLayers(); - const layerFiles = lt.layers; - const themeFiles = lt.themes; + const layerFiles = ScriptUtils.getLayerFiles(); + const themeFiles = ScriptUtils.getThemeFiles(); + console.log(" ---------- VALIDATING ---------") const licensePaths = [] @@ -93,12 +97,17 @@ class LayerOverviewUtils { const knownLayerIds = new Map(); for (const layerFile of layerFiles) { + if (knownLayerIds.has(layerFile.parsed.id)) { + throw "Duplicate identifier: " + layerFile.parsed.id + " in file " + layerFile.path + } layerErrorCount.push(...this.validateLayer(layerFile.parsed, layerFile.path, knownPaths)) knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed, AllKnownLayers.sharedUnits)) } let themeErrorCount = [] - for (const themeFile of themeFiles) { + for (const themeInfo of themeFiles) { + const themeFile = themeInfo.parsed + const themePath = themeInfo.path if (typeof themeFile.language === "string") { themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") } @@ -107,21 +116,28 @@ class LayerOverviewUtils { if (!knownLayerIds.has(layer)) { themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) } - } else { - if (layer.builtin !== undefined) { - if (!knownLayerIds.has(layer.builtin)) { - themeErrorCount.push("Unknown layer id: " + layer.builtin + "(which uses inheritance)") + } else if (layer["builtin"] !== undefined) { + let names = layer["builtin"]; + if (typeof names === "string") { + names = [names] + } + names.forEach(name => { + if (!knownLayerIds.has(name)) { + themeErrorCount.push("Unknown layer id: " + name + "(which uses inheritance)") } - } else { - // layer.builtin contains layer overrides - we can skip those - layerErrorCount.push(...this.validateLayer(layer, undefined, knownPaths, themeFile.id)) + return + }) + } else { + layerErrorCount.push(...this.validateLayer(layer, undefined, knownPaths, themeFile.id)) + if (knownLayerIds.has(layer["id"])) { + throw `The theme ${themeFile.id} defines a layer with id ${layer["id"]}, which is the same as an already existing layer` } } } themeFile.layers = themeFile.layers .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason - .filter(l => l.builtin === undefined) + .filter(l => l["builtin"] === undefined) try { @@ -129,6 +145,12 @@ class LayerOverviewUtils { if (theme.id !== theme.id.toLowerCase()) { themeErrorCount.push("Theme ids should be in lowercase, but it is " + theme.id) } + let filename = themePath.substring(themePath.lastIndexOf("/") + 1, themePath.length - 5) + if(theme.id !== filename){ + themeErrorCount.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename "+filename+" ("+themePath+")") + } + + } catch (e) { themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e) } diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 747de7ead3..281acbf37b 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -84,7 +84,14 @@ async function createManifest(layout: LayoutConfig) { sizes: "513x513", type: "image/svg" }) - } else { + } else if (icon.endsWith(".png")){ + icons.push({ + src: icon, + sizes: "513x513", + type: "image/png" + }) + } + else { console.log(icon) throw "Icon is not an svg for " + layout.id } @@ -199,7 +206,7 @@ for (const i in all) { createLandingPage(layout, manifObj).then(landing => { writeFile(enc(layout.id) + ".html", landing, err) }); - }) + }).catch(e => console.log("Could not generate the manifest: ", e)) } diff --git a/test.html b/test.html index 8b6c44878d..e93cbd6f2c 100644 --- a/test.html +++ b/test.html @@ -4,10 +4,16 @@ Small tests - - - - + + + + + + + + + + diff --git a/test.ts b/test.ts index 21ca94b74b..e69de29bb2 100644 --- a/test.ts +++ b/test.ts @@ -1,169 +0,0 @@ -import {OsmObject} from "./Logic/Osm/OsmObject"; -import DeleteButton from "./UI/Popup/DeleteWizard"; -import Combine from "./UI/Base/Combine"; -import State from "./State"; -import DeleteWizard from "./UI/Popup/DeleteWizard"; -import {UIEventSource} from "./Logic/UIEventSource"; -import {Tag} from "./Logic/Tags/Tag"; -import {QueryParameters} from "./Logic/Web/QueryParameters"; -import {Translation} from "./UI/i18n/Translation"; -import LocationInput from "./UI/Input/LocationInput"; -import Loc from "./Models/Loc"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import LengthInput from "./UI/Input/LengthInput"; -import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -/*import ValidatedTextField from "./UI/Input/ValidatedTextField"; -import Combine from "./UI/Base/Combine"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import {UIEventSource} from "./Logic/UIEventSource"; -import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; -import State from "./State"; -import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; -import {SlideShow} from "./UI/Image/SlideShow"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import Img from "./UI/Base/Img"; -import {AttributedImage} from "./UI/Image/AttributedImage"; -import {Imgur} from "./Logic/ImageProviders/Imgur"; -import Minimap from "./UI/Base/Minimap"; -import Loc from "./Models/Loc"; -import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -import ShowDataLayer from "./UI/ShowDataLayer"; -import LayoutConfig from "./Customizations/JSON/LayoutConfig"; -import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; - - -function TestSlideshow() { - const elems = new UIEventSource([ - new FixedUiElement("A"), - new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), - new Img("https://i.imgur.com/8lIQ5Hv.jpg"), - new AttributedImage("https://i.imgur.com/y5XudzW.jpg", Imgur.singleton), - new Img("https://www.grunge.com/img/gallery/the-real-reason-your-cat-sleeps-so-much/intro-1601496900.webp") - ]) - new SlideShow(elems).AttachTo("maindiv") -} - -function TestTagRendering() { - State.state = new State(undefined) - const tagsSource = new UIEventSource({ - id: "node/1" - }) - new TagRenderingQuestion( - tagsSource, - new TagRenderingConfig({ - multiAnswer: false, - freeform: { - key: "valve" - }, - question: "What valves are supported?", - render: "This pump supports {valve}", - mappings: [ - { - if: "valve=dunlop", - then: "This pump supports dunlop" - }, - { - if: "valve=shrader", - then: "shrader is supported", - } - ], - - }, undefined, "test"), - [] - ).AttachTo("maindiv") - new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") -} - -function TestAllInputMethods() { - - new Combine(ValidatedTextField.tpList.map(tp => { - const tf = ValidatedTextField.InputForType(tp.name); - - return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); - })).AttachTo("maindiv") -} - -function TestMiniMap() { - - const location = new UIEventSource({ - lon: 4.84771728515625, - lat: 51.17920846421931, - zoom: 14 - }) - const map0 = new Minimap({ - location: location, - allowMoving: true, - background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) - }) - map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") - .AttachTo("maindiv") - - const layout = AllKnownLayouts.layoutsList[1] - State.state = new State(layout) - console.log("LAYOUT is", layout.id) - - const feature = { - "type": "Feature", - _matching_layer_id: "bike_repair_station", - "properties": { - id: "node/-1", - "amenity": "bicycle_repair_station" - }, - "geometry": { - "type": "Point", - "coordinates": [ - 4.84771728515625, - 51.17920846421931 - ] - } - } - - ; - - State.state.allElements.addOrGetElement(feature) - - const featureSource = new UIEventSource([{ - freshness: new Date(), - feature: feature - }]) - - new ShowDataLayer( - featureSource, - map0.leafletMap, - new UIEventSource(layout) - ) - - const map1 = new Minimap({ - location: location, - allowMoving: true, - background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) - }, - ) - - map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") - .AttachTo("extradiv") - - - new ShowDataLayer( - featureSource, - map1.leafletMap, - new UIEventSource(layout) - ) - - featureSource.ping() -} -//*/ - -const loc = new UIEventSource({ - zoom: 24, - lat: 51.21043, - lon: 3.21389 -}) -const li = new LengthInput( - AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource("map","photo")), - loc -) - li.SetStyle("height: 30rem; background: aliceblue;") - .AttachTo("maindiv") - -new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv") \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 4c32dc2e72..5373b46307 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -4,20 +4,17 @@ import T from "./TestHelper"; import {FromJSON} from "../Customizations/JSON/FromJSON"; import Locale from "../UI/i18n/Locale"; import Translations from "../UI/i18n/Translations"; -import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; -import EditableTagRendering from "../UI/Popup/EditableTagRendering"; import {Translation} from "../UI/i18n/Translation"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; -import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; -import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; import {Tag} from "../Logic/Tags/Tag"; import {And} from "../Logic/Tags/And"; + Utils.runningFromConsole = true; -export default class TagSpec extends T{ - +export default class TagSpec extends T { + constructor() { super("Tags", [ ["Tag replacement works in translation", () => { @@ -88,12 +85,35 @@ export default class TagSpec extends T{ equal(assign.matchesProperties({"survey:date": "2021-03-29"}), false); equal(assign.matchesProperties({"_date:now": "2021-03-29"}), false); equal(assign.matchesProperties({"some_key": "2021-03-29"}), false); - - const notEmptyList = FromJSON.Tag("xyz!~\\[\\]") - equal(notEmptyList.matchesProperties({"xyz":undefined}), true); - equal(notEmptyList.matchesProperties({"xyz":"[]"}), false); - equal(notEmptyList.matchesProperties({"xyz":"[\"abc\"]"}), true); + const notEmptyList = FromJSON.Tag("xyz!~\\[\\]") + equal(notEmptyList.matchesProperties({"xyz": undefined}), true); + equal(notEmptyList.matchesProperties({"xyz": "[]"}), false); + equal(notEmptyList.matchesProperties({"xyz": "[\"abc\"]"}), true); + + let compare = FromJSON.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); + + + compare = FromJSON.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); + + compare = FromJSON.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); + compare = FromJSON.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); })], ["Is equivalent test", (() => { @@ -162,41 +182,35 @@ export default class TagSpec extends T{ } ], [ - "Tagrendering test", + "Test not with overpass", () => { - - const def = { - "render": { - "nl": "De toegankelijkheid van dit gebied is: {access:description}" - }, - "question": { - "nl": "Is dit gebied toegankelijk?" - }, - "freeform": { - "key": "access:description" - }, - "mappings": [ - { - "if": { - "and": [ - "access:description=", - "access=", - "leisure=park" - ] - }, - "then": { - "nl": "Dit gebied is vrij toegankelijk" - }, - "hideInAnswer": true - }, - { - "if": "access=no", - "then": "Niet toegankelijk" - } + const t = { + and: [ + "boundary=protected_area", + "protect_class!=98" ] - }; + } + const filter = FromJSON.Tag(t) + const overpass = filter.asOverpass(); + console.log(overpass) + equal(overpass[0], "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]") + const or = { + or: [ + "leisure=nature_reserve", + t + ] + } + const overpassOr = FromJSON.Tag(or).asOverpass() + equal(2, overpassOr.length) + equal(overpassOr[1], "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]") + const orInOr = {or:[ + "amenity=drinking_water", + or + ]} + const overpassOrInor = FromJSON.Tag(orInOr).asOverpass() + equal(3, overpassOrInor.length) } ], [ "Merge touching opening hours", @@ -358,27 +372,27 @@ export default class TagSpec extends T{ ]); equal(rules, "Tu 10:00-12:00; Su 13:00-17:00"); }], - ["JOIN OH with end hours", () =>{ + ["JOIN OH with end hours", () => { const rules = OH.ToString( OH.MergeTimes([ - { - weekday: 1, - endHour: 23, - endMinutes: 30, - startHour: 23, - startMinutes: 0 - }, { - weekday: 1, - endHour: 24, - endMinutes: 0, - startHour: 23, - startMinutes: 30 - }, + { + weekday: 1, + endHour: 23, + endMinutes: 30, + startHour: 23, + startMinutes: 0 + }, { + weekday: 1, + endHour: 24, + endMinutes: 0, + startHour: 23, + startMinutes: 30 + }, - ])); + ])); equal(rules, "Tu 23:00-00:00"); - }], ["JOIN OH with overflowed hours", () =>{ + }], ["JOIN OH with overflowed hours", () => { const rules = OH.ToString( OH.MergeTimes([ @@ -464,10 +478,10 @@ export default class TagSpec extends T{ ] }; - const tagRendering = new TagRenderingConfig(config, null, "test"); + const tagRendering = new TagRenderingConfig(config, null, "test"); equal(true, tagRendering.IsKnown({bottle: "yes"})) equal(false, tagRendering.IsKnown({})) }]]); } - + } diff --git a/test/Utils.spec.ts b/test/Utils.spec.ts index c94494eba6..4984d082f4 100644 --- a/test/Utils.spec.ts +++ b/test/Utils.spec.ts @@ -2,6 +2,8 @@ import T from "./TestHelper"; import {Utils} from "../Utils"; import {equal} from "assert"; import LZString from "lz-string"; +import * as Assert from "assert"; +import * as assert from "assert"; export default class UtilsSpec extends T { private static readonly example = { @@ -104,6 +106,16 @@ export default class UtilsSpec extends T { equal(result.list1[1], "appended") equal(result.list2.length, 1) equal(result.list2[0], "should-be-untouched") + }], + ["Test merge with null", () => { + const obj = { + someValue: 42 + } + const override = { + someValue: null + } + Utils.Merge(override, obj) + equal(obj.someValue, null) }] ]); }