Small fixes

This commit is contained in:
Pieter Vander Vennet 2021-07-27 22:38:30 +02:00
commit 55539b7c3a
263 changed files with 13321 additions and 2357 deletions

2
.gitignore vendored
View file

@ -14,3 +14,5 @@ Docs/Tools/stats.*.json
Docs/Tools/stats.csv
missing_translations.txt
*.swp
.DS_Store
Svg.ts

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false
}

View file

@ -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 };
});
}
}

View file

@ -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 }[];
}

View file

@ -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] === "*") {

View file

@ -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\": <your text>} 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\": <your text>} 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<any>, 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<any>,
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(`<img src="${sourcePart}" style="${style}" />`);
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
let html: BaseUIElement = new FixedUiElement(
`<img src="${sourcePart}" style="${style}" />`
);
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<string> {
const parts: Set<string>[] = []
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<string>[] = [];
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<string>(preset.description?.ExtractImages(false)))
parts.push(new Set<string>(preset.description?.ExtractImages(false)));
}
const allIcons = new Set<string>();
for (const part of parts) {
part?.forEach(allIcons.add, allIcons)
part?.forEach(allIcons.add, allIcons);
}
return allIcons;
}
}
}

View file

@ -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;<path to my icon.svg>`
*
*/
@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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')"
}
]
}

View file

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

View file

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

View file

@ -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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D07.svg' style='height: 3em'>' 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)<img src='./assets/themes/cycle_infra/Belgian_road_sign_D07.svg' style='height: 3em'> ' (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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D09.svg' style='height: 3em'>' 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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D10.svg' style='height: 3em'>' 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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D07.svg' style='height: 3em'>' 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)<img src='./assets/themes/cycle_infra/Belgian_road_sign_D07.svg' style='height: 3em'> ' (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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D09.svg' style='height: 3em'>' 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 <img src='./assets/themes/cycle_infra/Belgian_road_sign_D10.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M6.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M13.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M14.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M7.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M15.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M16.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M6.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M13.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M14.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M7.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M15.svg' style='height: 3em'>' 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 '<img src='./assets/themes/cycle_infra/Belgian_traffic_sign_M16.svg' style='height: 3em'>' 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 <img src='./assets/themes/cycle_infra/Cycle_barrier_single.png' style='width:8em'>' 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 <img src='./assets/themes/cycle_infra/Cycle_barrier_double.png' style='width:8em'>' 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 <img src='./assets/themes/cycle_infra/Cycle_barrier_triple.png' style='width:8em'>' 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 <img src='./assets/themes/cycle_infra/Cycle_barrier_squeeze.png' style='width:8em'>' 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 <img src='./assets/layers/crossings/Belgian_road_sign_B22.svg' style='height: 3em'>' 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 <img src='./assets/layers/crossings/Belgian_road_sign_B23.svg' style='height: 3em'>' 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"
}
]
}

View file

@ -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')"
}
]
}

View file

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

View file

@ -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
------------

View file

@ -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<string>, layoutName: string,
layoutDefinition: string = "") {
static InitAll(
layoutToUse: LayoutConfig,
layoutFromBase64: string,
testing: UIEventSource<string>,
layoutName: string,
layoutDefinition: string = ""
) {
if (layoutToUse === undefined) {
console.log("Incorrect layout")
new FixedUiElement(`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`).AttachTo("centermessage").onClick(() => {
});
throw "Incorrect layout"
console.log("Incorrect layout");
new FixedUiElement(
`Error: incorrect layout <i>${layoutName}</i><br/><a href='https://${window.location.host}/'>Go back</a>`
)
.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<LayerConfig>();
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<string>): [LayoutConfig, string]{
static LoadLayoutFromHash(
userLayoutParam: UIEventSource<string>
): [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:<br/> " + e).AttachTo("centermessage");
new FixedUiElement(
"Error: could not parse the custom layout:<br/> " + e
).AttachTo("centermessage");
throw e;
}
}
private static OnlyIf(featureSwitch: UIEventSource<boolean>, callback: () => void) {
private static OnlyIf(
featureSwitch: UIEventSource<boolean>,
callback: () => void
) {
featureSwitch.addCallbackAndRun(() => {
if (featureSwitch.data) {
callback();
@ -270,19 +310,15 @@ export class InitUiElements {
}
private static InitWelcomeMessage() {
const isOpened = new UIEventSource<boolean>(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<boolean>(
(str) => str !== "false",
[],
(b) => b.toString()
);
const flayer = {
isDisplayed: isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<TagsFilter>(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<boolean>((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<boolean>(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
);
});
}
}
}

View file

@ -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()
}
}
})
}
}

View file

@ -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);
})
}
}
}

View file

@ -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<L.Map>;
/**
* 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<string>("");

View file

@ -23,4 +23,4 @@
doc.addImage(image, 'PNG', 15, 30, 150*screenRatio, 150);
return doc;
}
}
}

View file

@ -9,7 +9,7 @@ export default class PendingChangesUploader {
constructor(changes: Changes, selectedFeature: UIEventSource<any>) {
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");

View file

@ -13,7 +13,7 @@ export default class SelectedFeatureHandler {
private readonly _hash: UIEventSource<string>;
private readonly _selectedFeature: UIEventSource<any>;
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<string>,
@ -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){

View file

@ -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<string, any[]>, relations: { role: string, relation: Relation }[] }, feat: any) => any;
constructor(name: string, doc: string, args: string[], f: ((params: { featuresPerLayer: Map<string, any[]>, 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<string, any[]>, 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<string, any[]>, relations: { role: string, relation: Relation }[], feature) {
@ -186,7 +222,6 @@ export class ExtraFunction {
}
public PatchFeature(featuresPerLayer: Map<string, any[]>, 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)
}
}

View file

@ -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<ChangeDescription>();
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<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
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
}
}

View file

@ -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<FilteredLayer[]>, upstream: FeatureSource) {
this.name = "FeatureDuplicator of "+upstream.name;
this.features = upstream.features.map(features => {
const newFeatures: { feature: any, freshness: Date }[] = [];

View file

@ -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<boolean>, layerDef: LayerConfig }[]>,
constructor(flayers: UIEventSource<FilteredLayer[]>,
changes: Changes,
updater: FeatureSource,
fromOsmApi: FeatureSource,
layout: UIEventSource<LayoutConfig>,
newPoints: FeatureSource,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>) {
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
]);

View file

@ -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 }[]>([]);

View file

@ -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<boolean>,
layerDef: LayerConfig
}[]>,
location: UIEventSource<Loc>,
selectedElement: UIEventSource<any>,
upstream: FeatureSource) {
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name = "FilteringFeatureSource";
constructor(
layers: UIEventSource<{
isDisplayed: UIEventSource<boolean>;
layerDef: LayerConfig;
appliedFilters: UIEventSource<TagsFilter>;
}[]>,
location: UIEventSource<Loc>,
selectedElement: UIEventSource<any>,
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<string>();
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<boolean>;
layerDef: LayerConfig;
appliedFilters: UIEventSource<TagsFilter>;
} = 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<boolean>,
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<UIEventSource<boolean>>();
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<boolean>,
layerDef: LayerConfig
}, location: UIEventSource<Loc>) {
return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom) && (layer.layerDef.maxzoom >= location.data.zoom)
private static showLayer(
layer: {
isDisplayed: UIEventSource<boolean>;
layerDef: LayerConfig;
},
location: UIEventSource<Loc>
) {
return (
layer.isDisplayed.data &&
layer.layerDef.minzoomVisible <= location.data.zoom
);
}
}
}

View file

@ -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))
}
}

View file

@ -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;
}

View file

@ -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<string>();
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")
}
}

View file

@ -27,8 +27,8 @@ export default class MetaTagging {
relations: Map<string, { role: string, relation: Relation }[]>,
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")
}
}
}

View file

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

View file

@ -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
}]
}
}

View file

@ -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
}
}]
}
}

View file

@ -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<boolean> {
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;
}
/**

View file

@ -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[]
}

View file

@ -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 [];
}
}

View file

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

View file

@ -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<ChangeDescription[]>("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<any>) {
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 = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
if (newElements.length > 0) {
changes +=
"\n<create>\n" +
newElements.map(e => e.ChangesetXML(csId)).join("\n") +
"</create>";
}
if (changedElements.length > 0) {
changes +=
"\n<modify>\n" +
changedElements.map(e => e.ChangesetXML(csId)).join("\n") +
"\n</modify>";
}
if (deletedElements.length > 0) {
changes +=
"\n<deleted>\n" +
deletedElements.map(e => e.ChangesetXML(csId)).join("\n") +
"\n</deleted>"
}
changes += "</osmChange>";
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<string, OsmObject> = new Map<string, OsmObject>()
const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = 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 = <OsmNode>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 = <OsmWay>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 = <OsmRelation>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<string, OsmObject>();
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 = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
if (creations.length > 0) {
changes +=
"<create>" +
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"<modify>\n" +
modifications +
"\n</modify>";
}
changes += "</osmChange>";
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();
}
}

View file

@ -53,6 +53,8 @@ export class ChangesetHandler {
element.ping();
}
}
}

View file

@ -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")

View file

@ -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<OsmRelation[]>([])
const relsSrc = new UIEventSource<OsmRelation[]>(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[]>([]);
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 ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
return ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
tags +
' </node>\n';
' </node>\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 ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
nds +
tags +
' </way>\n';
' </way>\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] {

View file

@ -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 = <string[]>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;
}
}

View file

@ -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;

View file

@ -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];
}
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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) {

View file

@ -2,21 +2,27 @@ import {Utils} from "../Utils";
export class UIEventSource<T> {
private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf();
public data: T;
public trace: boolean;
private readonly tag: string;
private _callbacks = [];
private static allSources : UIEventSource<any>[] = UIEventSource.PrepPerf();
static PrepPerf() : UIEventSource<any>[]{
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<any>[] {
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<T> {
return [];
}
constructor(data: T, tag: string = "") {
this.tag = tag;
this.data = data;
UIEventSource.allSources.push(this);
}
public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data);
@ -63,11 +63,20 @@ export class UIEventSource<T> {
}
public addCallback(callback: ((latestData: T) => void)): UIEventSource<T> {
/**
* 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<T> {
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<T> {
}
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<T> {
*/
public map<J>(f: ((t: T) => J),
extraSources: UIEventSource<any>[] = [],
g: ((j:J, t:T) => T) = undefined): UIEventSource<J> {
g: ((j: J, t: T) => T) = undefined): UIEventSource<J> {
const self = this;
const newSource = new UIEventSource<J>(
f(this.data),
"map("+this.tag+")"
"map(" + this.tag + ")"
);
const update = function () {
@ -159,11 +181,28 @@ export class UIEventSource<T> {
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<string, UIEventSource<any>>()
public static downloadJsonCached(url: string): UIEventSource<any>{
const cached = UIEventSourceTools._download_cache.get(url)
if(cached !== undefined){
return cached;
}
const src = new UIEventSource<any>(undefined)
UIEventSourceTools._download_cache.set(url, src)
Utils.downloadJson(url).then(r => src.setData(r))
return src;
}
}

View file

@ -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 = {

9
Models/FilteredLayer.ts Normal file
View file

@ -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<boolean>;
readonly appliedFilters: UIEventSource<TagsFilter>;
readonly layerDef: LayerConfig;
}

350
State.ts
View file

@ -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<LayoutConfig>(undefined);
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined, "layoutToUse");
/**
The mapping from id -> UIEventSource<properties>
@ -44,7 +43,7 @@ export default class State {
/**
The leaflet instance of the big basemap
*/
public leafletMap = new UIEventSource<L.Map>(undefined);
public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap");
/**
* Background layer id
*/
@ -62,26 +61,21 @@ export default class State {
public osmApiFeatureSource: OsmApiFeatureSource;
public filteredLayers: UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]> = new UIEventSource<{
readonly isDisplayed: UIEventSource<boolean>,
readonly layerDef: LayerConfig;
}[]>([])
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([],"filteredLayers");
/**
The latest element that was selected
*/
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
public readonly selectedElement = new UIEventSource<any>(
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<Map<string, { role: string, relation: Relation }[]>>(undefined, "Relation memberships")
public readonly knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships");
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>;
@ -96,40 +90,71 @@ export default class State {
public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
public readonly featureSwitchApiURL: UIEventSource<string>;
public readonly featureSwitchFilter: UIEventSource<boolean>;
public readonly featureSwitchEnableExport: UIEventSource<boolean>;
public readonly featureSwitchFakeUser: UIEventSource<boolean>;
public readonly featurePipeline: FeaturePipeline;
public featurePipeline: FeaturePipeline;
/**
* The map location: currently centered lat, lon and zoom
*/
public readonly locationControl = new UIEventSource<Loc>(undefined);
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
public backgroundLayer;
public readonly backgroundLayerId: UIEventSource<string>;
/* 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<boolean> =
QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown")
.map<boolean>((str) => str !== "false", [], b => "" + b)
QueryParameters.GetQueryParameter(
"layer-control-toggle",
"false",
"Whether or not the layer control is shown"
).map<boolean>(
(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<number>(
str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n
public filterIsOpened: UIEventSource<boolean> =
QueryParameters.GetQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter view is shown"
).map<boolean>(
(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<number>(
(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<Loc>({
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<boolean> {
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
function featSw(
key: string,
deflt: (layout: LayoutConfig) => boolean,
documentation: string
): UIEventSource<boolean> {
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<string>): UIEventSource<number> {
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);
})
);
}
}

352
Svg.ts

File diff suppressed because one or more lines are too long

View file

@ -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;
}
}

View file

@ -28,7 +28,7 @@ export default class Minimap extends BaseUIElement {
super()
options = options ?? {}
this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this._location = options?.location ?? new UIEventSource<Loc>(undefined)
this._location = options?.location ?? new UIEventSource<Loc>({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;

View file

@ -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<string | BaseUIElement | BaseUIElement[]>) {
super();
this._element = document.createElement("span")
const el = this._element
contents.addCallbackAndRun(contents => {
while (el.firstChild) {
el.removeChild(
el.lastChild
)
}
constructor(
contents: UIEventSource<string | BaseUIElement | BaseUIElement[]>
) {
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;
}
}
protected InnerConstructElement(): HTMLElement {
return this._element;
}
}

View file

@ -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)
}
}

View file

@ -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")])
}
}

View file

@ -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<FilteredLayer[]>) {
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<TagsFilter>
}): BaseUIElement {
const layer = flayer.layerDef
if (layer.filters.length === 0) {
return undefined;
}
let listFilterElements: [BaseUIElement, UIEventSource<TagsFilter>][] = 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<TagsFilter>] {
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()]
}
}

View file

@ -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";

View file

@ -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")
}
}
elements.push(
new Toggle(
new DownloadPanel(),
undefined,
State.state.featureSwitchEnableExport
)
);
return new Combine(elements).SetClass("flex flex-col");
}
}

View file

@ -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<boolean>,
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<any>({id: "node/-1"}),
false)
const leafletStyleNa = layer.layerDef.GenerateLeafletStyle(
new UIEventSource<any>({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, "<del>", name.Clone(), "</del>"]).SetStyle(style), zoomStatusNonActive])
.SetStyle(styleWhole),
layer.isDisplayed).ToggleOnClick()
.SetStyle("margin:0.3em;")
);
}
super(checkboxes)
this.SetStyle("display:flex;flex-direction:column;")
}
}

View file

@ -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";

View file

@ -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<any>({id: "node/-1"}),
false,
"2em"
false
).icon.html]).SetClass("relative")
let iconUnset =new Combine([ layer.GenerateLeafletStyle(
new UIEventSource<any>({id: "node/-1"}),
false,
"2em"
false
).icon.html]).SetClass("relative")
iconUnset.SetStyle("opacity:0.1")

View file

@ -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<Translation>(
Translations.t.general.search.search
);
const searchField = new TextField({
placeholder: new VariableUiElement(placeholder),
value: new UIEventSource<string>(""),
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<Translation>(Translations.t.general.search.search)
const searchField = new TextField({
placeholder: new VariableUiElement(
placeholder.map(uiElement => uiElement, [Locale.language])
),
value: new UIEventSource<string>(""),
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);
);
}
}
searchField.enterPressed.addCallback(runSearch);
goButton.onClick(runSearch);
}
}

View file

@ -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<PresetInfo>(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<string | string[]>(preset.preciseInput.preferredBackground))
if (preset.preciseInput.preferredBackground) {
backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(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<any>(tags), false).icon.html
let icon: () => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
.SetClass("w-12 h-12 block relative");
const presetInfo: PresetInfo = {
tags: preset.tags,

View file

@ -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;");

View file

@ -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
))
})

View file

@ -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<number[]> {
private static _nextId = 0;
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly value: UIEventSource<number[]>
private readonly value: UIEventSource<number[]>;
private readonly _elements: BaseUIElement[];
constructor(elements: BaseUIElement[], value = new UIEventSource<number[]>([])) {
constructor(
elements: BaseUIElement[],
value = new UIEventSource<number[]>([])
) {
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<number[]> {
@ -30,48 +31,58 @@ export default class CheckBoxes extends InputElement<number[]> {
}
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<number[]> {
value.data.splice(index, 1);
value.ping();
}
}
};
}
return el;
}
}
}

View file

@ -42,7 +42,6 @@ export default class LocationInput extends InputElement<Loc> {
}
)
map.leafletMap.addCallbackAndRunD(leaflet => {
console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15))
leaflet.setMaxBounds(
leaflet.getBounds().pad(0.15)
)

View file

@ -8,45 +8,52 @@ export class RadioButton<T> extends InputElement<T> {
private readonly value: UIEventSource<T>;
private _elements: InputElement<T>[];
private _selectFirstAsDefault: boolean;
private _dontStyle: boolean
constructor(elements: InputElement<T>[],
selectFirstAsDefault = true) {
super()
this._selectFirstAsDefault = selectFirstAsDefault;
constructor(
elements: InputElement<T>[],
options?: {
selectFirstAsDefault?: boolean,
dontStyle?: boolean
}
) {
super();
options = options ?? {}
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true;
this._elements = Utils.NoNull(elements);
this.value = new UIEventSource<T>(undefined)
this.value = new UIEventSource<T>(undefined);
this._dontStyle = options.dontStyle ?? false
}
protected InnerConstructElement(): HTMLElement {
const elements = this._elements;
const selectFirstAsDefault = this._selectFirstAsDefault;
const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(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<number> =
new UIEventSource<number>(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<T> extends InputElement<T> {
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<T> extends InputElement<T> {
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;
}
}
}*/
}
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;
}
}
}*/
}

View file

@ -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")
}
}
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);");
}
}

View file

@ -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) {

View file

@ -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

155
UI/Popup/SplitRoadWizard.ts Normal file
View file

@ -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<boolean>(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(
<OsmWay>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")
}
}

View file

@ -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[]}

View file

@ -22,7 +22,8 @@ export default class ShowDataLayer {
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>,
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)
}
}
})

View file

@ -56,9 +56,12 @@ export default class SpecialVisualizations {
if (!tags.hasOwnProperty(key)) {
continue;
}
parts.push(key + "=" + tags[key]);
parts.push([key , tags[key] ?? "<b>undefined</b>" ]);
}
return parts.join("<br/>")
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(),

View file

@ -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'

View file

@ -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;

View file

@ -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") {

View file

@ -136,6 +136,19 @@ export class Utils {
return newArr;
}
public static Identical<T>(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;

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/layers/.DS_Store vendored Normal file

Binary file not shown.

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.18 10.85"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M20.29,10.85h1.28V6.77h1.61V5.65H21.23v-1h.93V3.1h-.93V1.85h.93V0H1V1.85H2V3.1H1V4.63H2v1H0V6.77H1.61v4.08H2.89V6.77h17.4Zm-17.63-9H20.52V3.1H2.66Zm0,3.8v-1H20.52v1Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 391 B

View file

@ -0,0 +1,8 @@
[
{
"authors": [],
"path": "bench.svg",
"license": "CC0",
"sources": []
}
]

View file

@ -126,7 +126,6 @@
}
}
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
"render": "./assets/themes/benches/bench_public_transport.svg"
},

View file

@ -210,7 +210,6 @@
},
"description"
],
"hideUnderlayingFeaturesMinPercentage": 1,
"presets": [
{
"title": {

View file

@ -293,7 +293,6 @@
}
}
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
"render": "./assets/layers/bike_cafe/bike_cafe.svg"
},

View file

@ -63,7 +63,6 @@
}
}
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
"render": "./assets/layers/bike_monitoring_station/monitoring_station.svg"
},

View file

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

View file

@ -56,7 +56,6 @@
"phone",
"opening_hours"
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
"render": "./assets/layers/bike_themed_object/other_services.svg"
},

View file

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

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg2816"
version="1.1"
inkscape:version="0.47 r22583"
width="95.81649"
height="83.729599"
xml:space="preserve"
sodipodi:docname="Belgian road sign B22.svg"><metadata
id="metadata2822"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs2820"><inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective2824" /><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2844"><path
d="m 2562,672.668 3,0 0,-108 -3,0 0,108 z"
id="path2846" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2854"><path
d="m 731,6133.67 780,0 0,-679 -780,0 0,679 z"
id="path2856" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2892"><path
d="m 731,4841.67 780,0 0,-680 -780,0 0,680 z"
id="path2894" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3090"><path
d="m 731,2387.67 779,0 0,-684 -779,0"
id="path3092" /></clipPath><inkscape:perspective
id="perspective3129"
inkscape:persp3d-origin="47.908001 : 27.909665 : 1"
inkscape:vp_z="95.816002 : 41.864498 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 41.864498 : 1"
sodipodi:type="inkscape:persp3d" /><inkscape:perspective
id="perspective4211"
inkscape:persp3d-origin="39.790001 : 26.495 : 1"
inkscape:vp_z="79.580002 : 39.7425 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 39.7425 : 1"
sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="838"
id="namedview2818"
showgrid="false"
inkscape:zoom="2.140677"
inkscape:cx="47.742493"
inkscape:cy="42.339324"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g2826" /><g
id="g2826"
inkscape:groupmode="layer"
inkscape:label="Belgio"
transform="matrix(1.25,0,0,-1.25,346.87909,480.97823)"><g
id="g2840"
transform="matrix(0.09762902,0,0,0.09762902,-202.75728,115.54083)"><g
clip-path="url(#clipPath2844)"
id="g2842"><g
transform="scale(8.33333,8.33333)"
id="g2848" /></g></g>
<path
id="path3115"
d="m -242.61678,319.6165 c 0.704,-1.1168 1.9512,-1.8176 3.3904,-1.8176 1.3752,0 1.984,0.6704 2.7192,1.6896 l 34.8976,58.5576 c 1.216,1.5944 0.9264,4.1144 -0.544,5.5816 -0.832,0.8288 -1.792,1.212 -2.8792,1.148 l -69.1224,-0.032 c -0.7992,-0.1272 -1.5672,-0.5104 -2.1752,-1.116 -1.1832,-1.212 -1.4712,-2.9656 -0.864,-4.4656 l 34.5776,-59.5456 z"
style="fill:#ed1c24" /><path
id="path3117"
d="m -242.61678,319.6165 c 0.704,-1.1168 1.9512,-1.8176 3.3904,-1.8176 1.3752,0 1.984,0.6704 2.7192,1.6896 l 34.8976,58.5576 c 1.216,1.5944 0.9264,4.1144 -0.544,5.5816 -0.832,0.8296 -1.792,1.212 -2.8792,1.148 l -69.1224,-0.032 c -0.7992,-0.1272 -1.5672,-0.5104 -2.1752,-1.116 -1.1832,-1.212 -1.4712,-2.9656 -0.864,-4.4656 l 34.5776,-59.5456 z"
stroke-miterlimit="3.863"
style="fill:none;stroke:#ed1c24;stroke-width:0;stroke-miterlimit:3.86299992" /><polygon
id="polygon3119"
points="47.926,68.261 81.192,10.892 14.66,10.892 "
clip-rule="evenodd"
transform="matrix(0.8,0,0,-0.8,-277.50318,384.7821)"
style="fill:#ffffff;fill-rule:evenodd" /><polygon
id="polygon3121"
points="47.926,68.261 81.192,10.892 14.66,10.892 "
stroke-miterlimit="3.863"
transform="matrix(0.8,0,0,-0.8,-277.50318,384.7821)"
style="fill:none;stroke:#ed1c24;stroke-width:0;stroke-miterlimit:3.86299992" /><path
style="fill:#ffed45;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.31999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="m -231.26354,357.69838 c -1.34367,0.12281 -2.52121,0.69462 -3.46633,1.68322 -0.998,1.04394 -1.53671,2.27606 -1.60888,3.67984 -0.0339,0.65964 0.11226,1.24682 0.51395,2.0645 0.57894,1.1785 1.43752,2.19077 2.29437,2.70506 0.10668,0.0641 0.31479,0.15627 0.46244,0.20495 0.52155,0.17194 0.62599,0.26164 0.62055,0.53293 -0.006,0.26596 -0.33704,0.82354 -0.7403,1.2444 -0.12913,0.13476 -0.16161,0.15184 -0.28886,0.15184 -0.16967,0 -0.31579,-0.0771 -0.958,-0.5056 -2.04063,-1.36152 -6.41288,-4.73175 -6.60552,-5.09169 -0.0559,-0.10455 -0.0285,-0.42985 0.0492,-0.58259 0.0315,-0.062 0.12797,-0.20932 0.21429,-0.32741 0.31765,-0.43453 0.41348,-0.73823 0.38564,-1.22219 -0.032,-0.55638 -0.20725,-0.9062 -0.6338,-1.26523 -0.51867,-0.43658 -0.64949,-0.45651 -1.37516,-0.20944 -0.27308,0.093 -0.50906,0.1534 -0.59909,0.1534 -0.24478,0 -0.41506,-0.14213 -0.83113,-0.69367 -0.82327,-1.09133 -1.74662,-1.78358 -2.92939,-2.19619 -0.60899,-0.21244 -0.95078,-0.26471 -1.73015,-0.26455 -0.73272,1.3e-4 -0.93055,0.0262 -1.49924,0.19736 -1.92475,0.57934 -3.4522,2.40105 -3.72021,4.43692 -0.30411,2.3101 1.01272,4.62854 3.15722,5.55867 0.64918,0.28156 1.19823,0.39229 2.06223,0.41592 0.76732,0.021 1.32941,-0.0495 1.88997,-0.23708 0.70208,-0.2349 1.19254,-0.54828 1.99128,-1.27237 0.52363,-0.4747 0.92806,-0.76798 1.03743,-0.75235 0.0574,0.008 0.0679,0.0305 0.0622,0.13141 -0.0225,0.38996 -0.5451,1.79029 -1.27378,3.41288 -0.39655,0.88303 -0.45502,0.99838 -0.67399,1.32971 -0.24305,0.36776 -0.39642,0.48005 -0.778,0.56964 -0.35042,0.0823 -0.51951,0.16055 -0.61842,0.28628 -0.16426,0.20883 -0.006,0.37121 0.43469,0.4444 0.39259,0.0653 3.63288,0.0655 3.99004,3.1e-4 0.30374,-0.0554 0.42221,-0.11317 0.46536,-0.22667 0.0531,-0.1398 -0.0206,-0.27007 -0.19734,-0.34823 -0.12228,-0.0541 -0.28608,-0.0742 -0.89427,-0.10965 -0.40952,-0.0239 -0.80194,-0.0602 -0.87206,-0.0805 -0.11725,-0.0341 -0.12596,-0.0447 -0.10851,-0.13201 0.10648,-0.53243 2.27576,-5.44935 2.40417,-5.44935 0.15339,0 0.83299,0.39345 1.84192,1.06636 0.91635,0.61116 1.72519,1.19134 4.07094,2.92004 l 1.83714,1.35389 1.84918,0.0223 c 2.38508,0.0288 2.55796,0.0498 2.81579,0.34273 0.15082,0.17134 0.32526,0.57007 0.37407,0.855 0.0282,0.16441 0.0242,0.24368 -0.0211,0.42763 -0.073,0.29561 -0.28752,0.73526 -0.42498,0.87079 -0.25047,0.24694 -0.44151,0.28458 -1.85986,0.36645 -1.21249,0.07 -1.53162,0.10049 -1.92144,0.18373 -0.23089,0.0493 -0.33407,0.1108 -0.29417,0.17536 0.0366,0.0592 0.46162,0.1317 1.0045,0.17138 0.69842,0.051 2.40993,0.0222 2.70529,-0.0456 0.34217,-0.0786 0.55789,-0.20879 0.78273,-0.47256 0.50948,-0.59769 0.72258,-1.2471 0.60028,-1.8293 -0.0842,-0.40068 -0.25149,-0.70122 -0.56153,-1.00855 -0.30557,-0.3029 -0.57152,-0.46469 -0.99823,-0.60726 -0.3324,-0.11107 -0.62458,-0.15262 -1.38578,-0.19709 -0.72069,-0.0421 -0.90676,-0.12032 -0.90536,-0.38058 8e-4,-0.18061 0.0813,-0.34664 0.32122,-0.66377 0.52666,-0.69617 0.97508,-0.97276 1.87468,-1.15631 0.72541,-0.14801 1.1711,-0.32548 1.78453,-0.71059 1.7548,-1.10164 2.62477,-2.72806 2.55011,-4.76748 -0.03,-0.81857 -0.19121,-1.47945 -0.53315,-2.18515 -0.29617,-0.61127 -0.67699,-1.08214 -1.25031,-1.54601 -1.20112,-0.97181 -2.72396,-1.52872 -3.88898,-1.42224 z"
id="path4186" /><path
style="fill:#ffed45;fill-opacity:1;stroke:#000000;stroke-width:0.31999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m -236.21364,351.37729 0.0849,2.17946 2.34342,-4.05028 -2.48076,-4.20684 0.0189,2.18003 -8.2238,0.17008 0.0334,3.86459 8.22401,-0.13708 0,-1e-5 0,5e-5 z"
id="path4222"
sodipodi:nodetypes="cccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg2816"
version="1.1"
inkscape:version="0.47 r22583"
width="95.81649"
height="83.729599"
xml:space="preserve"
sodipodi:docname="Belgio.pdf"><metadata
id="metadata2822"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs2820"><inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
id="perspective2824" /><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2844"><path
d="m 2562,672.668 3,0 0,-108 -3,0 0,108 z"
id="path2846" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2854"><path
d="m 731,6133.67 780,0 0,-679 -780,0 0,679 z"
id="path2856" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2892"><path
d="m 731,4841.67 780,0 0,-680 -780,0 0,680 z"
id="path2894" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3090"><path
d="m 731,2387.67 779,0 0,-684 -779,0"
id="path3092" /></clipPath><inkscape:perspective
id="perspective3129"
inkscape:persp3d-origin="47.908001 : 27.909665 : 1"
inkscape:vp_z="95.816002 : 41.864498 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 41.864498 : 1"
sodipodi:type="inkscape:persp3d" /><inkscape:perspective
id="perspective4211"
inkscape:persp3d-origin="39.790001 : 26.495 : 1"
inkscape:vp_z="79.580002 : 39.7425 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 39.7425 : 1"
sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="838"
id="namedview2818"
showgrid="false"
inkscape:zoom="2.140677"
inkscape:cx="47.742493"
inkscape:cy="42.339324"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g2826" /><g
id="g2826"
inkscape:groupmode="layer"
inkscape:label="Belgio"
transform="matrix(1.25,0,0,-1.25,346.87909,480.97823)"><g
id="g2840"
transform="matrix(0.09762902,0,0,0.09762902,-202.75728,115.54083)"><g
clip-path="url(#clipPath2844)"
id="g2842"><g
transform="scale(8.33333,8.33333)"
id="g2848" /></g></g>
<g
id="g4226"><path
style="fill:#ed1c24"
d="m -242.61678,319.6165 c 0.704,-1.1168 1.9512,-1.8176 3.3904,-1.8176 1.3752,0 1.984,0.6704 2.7192,1.6896 l 34.8976,58.5576 c 1.216,1.5944 0.9264,4.1144 -0.544,5.5816 -0.832,0.8288 -1.792,1.212 -2.8792,1.148 l -69.1224,-0.032 c -0.7992,-0.1272 -1.5672,-0.5104 -2.1752,-1.116 -1.1832,-1.212 -1.4712,-2.9656 -0.864,-4.4656 l 34.5776,-59.5456 z"
id="path3115" /><path
style="fill:none;stroke:#ed1c24;stroke-width:0;stroke-miterlimit:3.86299992"
stroke-miterlimit="3.863"
d="m -242.61678,319.6165 c 0.704,-1.1168 1.9512,-1.8176 3.3904,-1.8176 1.3752,0 1.984,0.6704 2.7192,1.6896 l 34.8976,58.5576 c 1.216,1.5944 0.9264,4.1144 -0.544,5.5816 -0.832,0.8296 -1.792,1.212 -2.8792,1.148 l -69.1224,-0.032 c -0.7992,-0.1272 -1.5672,-0.5104 -2.1752,-1.116 -1.1832,-1.212 -1.4712,-2.9656 -0.864,-4.4656 l 34.5776,-59.5456 z"
id="path3117" /><polygon
style="fill:#ffffff;fill-rule:evenodd"
transform="matrix(0.8,0,0,-0.8,-277.50318,384.7821)"
clip-rule="evenodd"
points="81.192,10.892 14.66,10.892 47.926,68.261 "
id="polygon3119" /><polygon
style="fill:none;stroke:#ed1c24;stroke-width:0;stroke-miterlimit:3.86299992"
transform="matrix(0.8,0,0,-0.8,-277.50318,384.7821)"
stroke-miterlimit="3.863"
points="81.192,10.892 14.66,10.892 47.926,68.261 "
id="polygon3121" /><path
id="path4186"
d="m -231.26354,357.69838 c -1.34367,0.12281 -2.52121,0.69462 -3.46633,1.68322 -0.998,1.04394 -1.53671,2.27606 -1.60888,3.67984 -0.0339,0.65964 0.11226,1.24682 0.51395,2.0645 0.57894,1.1785 1.43752,2.19077 2.29437,2.70506 0.10668,0.0641 0.31479,0.15627 0.46244,0.20495 0.52155,0.17194 0.62599,0.26164 0.62055,0.53293 -0.006,0.26596 -0.33704,0.82354 -0.7403,1.2444 -0.12913,0.13476 -0.16161,0.15184 -0.28886,0.15184 -0.16967,0 -0.31579,-0.0771 -0.958,-0.5056 -2.04063,-1.36152 -6.41288,-4.73175 -6.60552,-5.09169 -0.0559,-0.10455 -0.0285,-0.42985 0.0492,-0.58259 0.0315,-0.062 0.12797,-0.20932 0.21429,-0.32741 0.31765,-0.43453 0.41348,-0.73823 0.38564,-1.22219 -0.032,-0.55638 -0.20725,-0.9062 -0.6338,-1.26523 -0.51867,-0.43658 -0.64949,-0.45651 -1.37516,-0.20944 -0.27308,0.093 -0.50906,0.1534 -0.59909,0.1534 -0.24478,0 -0.41506,-0.14213 -0.83113,-0.69367 -0.82327,-1.09133 -1.74662,-1.78358 -2.92939,-2.19619 -0.60899,-0.21244 -0.95078,-0.26471 -1.73015,-0.26455 -0.73272,1.3e-4 -0.93055,0.0262 -1.49924,0.19736 -1.92475,0.57934 -3.4522,2.40105 -3.72021,4.43692 -0.30411,2.3101 1.01272,4.62854 3.15722,5.55867 0.64918,0.28156 1.19823,0.39229 2.06223,0.41592 0.76732,0.021 1.32941,-0.0495 1.88997,-0.23708 0.70208,-0.2349 1.19254,-0.54828 1.99128,-1.27237 0.52363,-0.4747 0.92806,-0.76798 1.03743,-0.75235 0.0574,0.008 0.0679,0.0305 0.0622,0.13141 -0.0225,0.38996 -0.5451,1.79029 -1.27378,3.41288 -0.39655,0.88303 -0.45502,0.99838 -0.67399,1.32971 -0.24305,0.36776 -0.39642,0.48005 -0.778,0.56964 -0.35042,0.0823 -0.51951,0.16055 -0.61842,0.28628 -0.16426,0.20883 -0.006,0.37121 0.43469,0.4444 0.39259,0.0653 3.63288,0.0655 3.99004,3.1e-4 0.30374,-0.0554 0.42221,-0.11317 0.46536,-0.22667 0.0531,-0.1398 -0.0206,-0.27007 -0.19734,-0.34823 -0.12228,-0.0541 -0.28608,-0.0742 -0.89427,-0.10965 -0.40952,-0.0239 -0.80194,-0.0602 -0.87206,-0.0805 -0.11725,-0.0341 -0.12596,-0.0447 -0.10851,-0.13201 0.10648,-0.53243 2.27576,-5.44935 2.40417,-5.44935 0.15339,0 0.83299,0.39345 1.84192,1.06636 0.91635,0.61116 1.72519,1.19134 4.07094,2.92004 l 1.83714,1.35389 1.84918,0.0223 c 2.38508,0.0288 2.55796,0.0498 2.81579,0.34273 0.15082,0.17134 0.32526,0.57007 0.37407,0.855 0.0282,0.16441 0.0242,0.24368 -0.0211,0.42763 -0.073,0.29561 -0.28752,0.73526 -0.42498,0.87079 -0.25047,0.24694 -0.44151,0.28458 -1.85986,0.36645 -1.21249,0.07 -1.53162,0.10049 -1.92144,0.18373 -0.23089,0.0493 -0.33407,0.1108 -0.29417,0.17536 0.0366,0.0592 0.46162,0.1317 1.0045,0.17138 0.69842,0.051 2.40993,0.0222 2.70529,-0.0456 0.34217,-0.0786 0.55789,-0.20879 0.78273,-0.47256 0.50948,-0.59769 0.72258,-1.2471 0.60028,-1.8293 -0.0842,-0.40068 -0.25149,-0.70122 -0.56153,-1.00855 -0.30557,-0.3029 -0.57152,-0.46469 -0.99823,-0.60726 -0.3324,-0.11107 -0.62458,-0.15262 -1.38578,-0.19709 -0.72069,-0.0421 -0.90676,-0.12032 -0.90536,-0.38058 8e-4,-0.18061 0.0813,-0.34664 0.32122,-0.66377 0.52666,-0.69617 0.97508,-0.97276 1.87468,-1.15631 0.72541,-0.14801 1.1711,-0.32548 1.78453,-0.71059 1.7548,-1.10164 2.62477,-2.72806 2.55011,-4.76748 -0.03,-0.81857 -0.19121,-1.47945 -0.53315,-2.18515 -0.29617,-0.61127 -0.67699,-1.08214 -1.25031,-1.54601 -1.20112,-0.97181 -2.72396,-1.52872 -3.88898,-1.42224 z"
style="fill:#ffed45;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.31999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /><path
sodipodi:nodetypes="cccccccccc"
id="path4222"
d="m -241.06949,349.14367 -2.18011,0.0661 4.0299,2.37829 4.22809,-2.44436 -2.18011,4e-5 -0.0991,-8.22496 -3.86473,10e-6 0.0661,8.22488 1e-5,0 -5e-5,0 z"
style="fill:#ffed45;fill-opacity:1;stroke:#000000;stroke-width:0.31999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></g></svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -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 <img src='./assets/layers/crossings/Belgian_road_sign_B22.svg' style='height: 3em'>",
"nl": "Een fietser mag wel rechtsaf slaan als het licht rood is <img src='./assets/layers/crossings/Belgian_road_sign_B22.svg' style='height: 3em'>"
},
"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 <img src='./assets/layers/crossings/Belgian_road_sign_B23.svg' style='height: 3em'>",
"nl": "Een fietser mag wel rechtdoor gaan als het licht rood is <img src='./assets/layers/crossings/Belgian_road_sign_B23.svg' style='height: 3em'>"
},
"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"
}
}
]
}
]
}

View file

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

Some files were not shown because too many files have changed in this diff Show more