forked from MapComplete/MapComplete
Small fixes
This commit is contained in:
commit
55539b7c3a
263 changed files with 13321 additions and 2357 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,3 +14,5 @@ Docs/Tools/stats.*.json
|
|||
Docs/Tools/stats.csv
|
||||
missing_translations.txt
|
||||
*.swp
|
||||
.DS_Store
|
||||
Svg.ts
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
27
Customizations/JSON/FilterConfig.ts
Normal file
27
Customizations/JSON/FilterConfig.ts
Normal 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 };
|
||||
});
|
||||
}
|
||||
}
|
11
Customizations/JSON/FilterConfigJson.ts
Normal file
11
Customizations/JSON/FilterConfigJson.ts
Normal 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 }[];
|
||||
}
|
|
@ -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] === "*") {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
111
Docs/TagInfo/mapcomplete_artwork.json
Normal file
111
Docs/TagInfo/mapcomplete_artwork.json
Normal 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')"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
707
Docs/TagInfo/mapcomplete_cycle_infra.json
Normal file
707
Docs/TagInfo/mapcomplete_cycle_infra.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
264
Docs/TagInfo/mapcomplete_cyclestreets.json
Normal file
264
Docs/TagInfo/mapcomplete_cyclestreets.json
Normal 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')"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
------------
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
36
Logic/Actors/ChangeToElementsActor.ts
Normal file
36
Logic/Actors/ChangeToElementsActor.ts
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>("");
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
doc.addImage(image, 'PNG', 15, 30, 150*screenRatio, 150);
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
162
Logic/FeatureSource/ChangeApplicator.ts
Normal file
162
Logic/FeatureSource/ChangeApplicator.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 }[] = [];
|
||||
|
|
|
@ -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
|
||||
]);
|
||||
|
||||
|
|
|
@ -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 }[]>([]);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
Logic/Osm/Actions/ChangeDescription.ts
Normal file
30
Logic/Osm/Actions/ChangeDescription.ts
Normal 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
|
||||
|
||||
|
||||
}
|
52
Logic/Osm/Actions/ChangeTagAction.ts
Normal file
52
Logic/Osm/Actions/ChangeTagAction.ts
Normal 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
|
||||
}]
|
||||
}
|
||||
}
|
48
Logic/Osm/Actions/CreateNewNodeAction.ts
Normal file
48
Logic/Osm/Actions/CreateNewNodeAction.ts
Normal 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
|
||||
}
|
||||
}]
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
23
Logic/Osm/Actions/OsmChangeAction.ts
Normal file
23
Logic/Osm/Actions/OsmChangeAction.ts
Normal 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[]
|
||||
|
||||
|
||||
}
|
20
Logic/Osm/Actions/RelationSplitlHandler.ts
Normal file
20
Logic/Osm/Actions/RelationSplitlHandler.ts
Normal 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 [];
|
||||
}
|
||||
|
||||
|
||||
}
|
238
Logic/Osm/Actions/SplitAction.ts
Normal file
238
Logic/Osm/Actions/SplitAction.ts
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -53,6 +53,8 @@ export class ChangesetHandler {
|
|||
element.ping();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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] {
|
||||
|
|
194
Logic/Osm/aspectedRouting.ts
Normal file
194
Logic/Osm/aspectedRouting.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
42
Logic/Tags/ComparingTag.ts
Normal file
42
Logic/Tags/ComparingTag.ts
Normal 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];
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
9
Models/FilteredLayer.ts
Normal 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
350
State.ts
|
@ -14,24 +14,23 @@ import Loc from "./Models/Loc";
|
|||
import Constants from "./Models/Constants";
|
||||
|
||||
import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
|
||||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
import FilteredLayer from "./Models/FilteredLayer";
|
||||
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
*/
|
||||
|
||||
export default class State {
|
||||
|
||||
// The singleton of the global state
|
||||
public static state: State;
|
||||
|
||||
|
||||
public readonly layoutToUse = new UIEventSource<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
352
Svg.ts
File diff suppressed because one or more lines are too long
32
UI/Base/CenterFlexedElement.ts
Normal file
32
UI/Base/CenterFlexedElement.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
55
UI/BigComponents/DownloadPanel.ts
Normal file
55
UI/BigComponents/DownloadPanel.ts
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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")])
|
||||
}
|
||||
}
|
153
UI/BigComponents/FilterView.ts
Normal file
153
UI/BigComponents/FilterView.ts
Normal 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()]
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;")
|
||||
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;");
|
||||
|
|
|
@ -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
|
||||
))
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -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);");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
155
UI/Popup/SplitRoadWizard.ts
Normal 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")
|
||||
|
||||
}
|
||||
}
|
|
@ -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[]}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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") {
|
||||
|
|
33
Utils.ts
33
Utils.ts
|
@ -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
BIN
assets/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
assets/layers/.DS_Store
vendored
Normal file
BIN
assets/layers/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -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"
|
||||
|
|
1
assets/layers/bench/bench.svg
Normal file
1
assets/layers/bench/bench.svg
Normal 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 |
8
assets/layers/bench/license_info.json
Normal file
8
assets/layers/bench/license_info.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"authors": [],
|
||||
"path": "bench.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
}
|
||||
]
|
|
@ -126,7 +126,6 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/themes/benches/bench_public_transport.svg"
|
||||
},
|
||||
|
|
|
@ -210,7 +210,6 @@
|
|||
},
|
||||
"description"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 1,
|
||||
"presets": [
|
||||
{
|
||||
"title": {
|
||||
|
|
|
@ -293,7 +293,6 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/layers/bike_cafe/bike_cafe.svg"
|
||||
},
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/layers/bike_monitoring_station/monitoring_station.svg"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -56,7 +56,6 @@
|
|||
"phone",
|
||||
"opening_hours"
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/layers/bike_themed_object/other_services.svg"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
123
assets/layers/crossings/Belgian_road_sign_B22.svg
Normal file
123
assets/layers/crossings/Belgian_road_sign_B22.svg
Normal 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 |
124
assets/layers/crossings/Belgian_road_sign_B23.svg
Normal file
124
assets/layers/crossings/Belgian_road_sign_B23.svg
Normal 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 |
310
assets/layers/crossings/crossings.json
Normal file
310
assets/layers/crossings/crossings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
42
assets/layers/crossings/license_info.json
Normal file
42
assets/layers/crossings/license_info.json
Normal 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
Loading…
Add table
Add a link
Reference in a new issue