reset to previous commit

This commit is contained in:
karelleketers 2021-07-26 15:39:27 +02:00
parent d5b614fc44
commit 196d40084d
90 changed files with 4953 additions and 1922 deletions

View file

@ -3,6 +3,7 @@ import UnitConfigJson from "./UnitConfigJson";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import Combine from "../../UI/Base/Combine"; import Combine from "../../UI/Base/Combine";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
export class Unit { export class Unit {
public readonly appliesToKeys: Set<string>; public readonly appliesToKeys: Set<string>;
@ -81,7 +82,10 @@ export class Unit {
return undefined; return undefined;
} }
const [stripped, denom] = this.findDenomination(value) const [stripped, denom] = this.findDenomination(value)
const human = denom.human const human = denom?.human
if(human === undefined){
return new FixedUiElement(stripped ?? value);
}
const elems = denom.prefix ? [human, stripped] : [stripped, human]; const elems = denom.prefix ? [human, stripped] : [stripped, human];
return new Combine(elems) return new Combine(elems)
@ -152,7 +156,7 @@ export class Denomination {
if (stripped === null) { if (stripped === null) {
return null; return null;
} }
return stripped + " " + this.canonical.trim() return (stripped + " " + this.canonical.trim()).trim();
} }
/** /**

View file

@ -0,0 +1,27 @@
import { TagsFilter } from "../../Logic/Tags/TagsFilter";
import { Translation } from "../../UI/i18n/Translation";
import Translations from "../../UI/i18n/Translations";
import FilterConfigJson from "./FilterConfigJson";
import { FromJSON } from "./FromJSON";
export default class FilterConfig {
readonly options: {
question: Translation;
osmTags: TagsFilter;
}[];
constructor(json: FilterConfigJson, context: string) {
this.options = json.options.map((option, i) => {
const question = Translations.T(
option.question,
context + ".options-[" + i + "].question"
);
const osmTags = FromJSON.Tag(
option.osmTags,
`${context}.options-[${i}].osmTags`
);
return { question: question, osmTags: osmTags };
});
}
}

View file

@ -0,0 +1,11 @@
import { AndOrTagConfigJson } from "./TagConfigJson";
export default interface FilterConfigJson {
/**
* The options for a filter
* If there are multiple options these will be a list of radio buttons
* If there is only one option this will be a checkbox
* Filtering is done based on the given osmTags that are compared to the objects in that layer.
*/
options: { question: string | any; osmTags: AndOrTagConfigJson | string }[];
}

View file

@ -18,19 +18,18 @@ import {Tag} from "../../Logic/Tags/Tag";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import {Unit} from "./Denomination"; import {Unit} from "./Denomination";
import DeleteConfig from "./DeleteConfig"; import DeleteConfig from "./DeleteConfig";
import FilterConfig from "./FilterConfig";
export default class LayerConfig { export default class LayerConfig {
static WAYHANDLING_DEFAULT = 0; static WAYHANDLING_DEFAULT = 0;
static WAYHANDLING_CENTER_ONLY = 1; static WAYHANDLING_CENTER_ONLY = 1;
static WAYHANDLING_CENTER_AND_WAY = 2; static WAYHANDLING_CENTER_AND_WAY = 2;
id: string; id: string;
name: Translation name: Translation;
description: Translation; description: Translation;
source: SourceConfig; source: SourceConfig;
calculatedTags: [string, string][] calculatedTags: [string, string][];
doNotDownload: boolean; doNotDownload: boolean;
passAllFeatures: boolean; passAllFeatures: boolean;
isShown: TagRenderingConfig; isShown: TagRenderingConfig;
@ -39,7 +38,7 @@ export default class LayerConfig {
title?: TagRenderingConfig; title?: TagRenderingConfig;
titleIcons: TagRenderingConfig[]; titleIcons: TagRenderingConfig[];
icon: TagRenderingConfig; icon: TagRenderingConfig;
iconOverlays: { if: TagsFilter, then: TagRenderingConfig, badge: boolean }[] iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[];
iconSize: TagRenderingConfig; iconSize: TagRenderingConfig;
label: TagRenderingConfig; label: TagRenderingConfig;
rotation: TagRenderingConfig; rotation: TagRenderingConfig;
@ -48,33 +47,40 @@ export default class LayerConfig {
dashArray: TagRenderingConfig; dashArray: TagRenderingConfig;
wayHandling: number; wayHandling: number;
public readonly units: Unit[]; public readonly units: Unit[];
public readonly deletion: DeleteConfig | null public readonly deletion: DeleteConfig | null;
presets: { presets: {
title: Translation, title: Translation,
tags: Tag[], tags: Tag[],
description?: Translation, description?: Translation,
preciseInput?: { preferredBackground?: string }
}[]; }[];
tagRenderings: TagRenderingConfig []; tagRenderings: TagRenderingConfig[];
filters: FilterConfig[];
constructor(json: LayerConfigJson, constructor(
units?:Unit[], json: LayerConfigJson,
context?: string, units?: Unit[],
official: boolean = true,) { context?: string,
official: boolean = true
) {
this.units = units ?? []; this.units = units ?? [];
context = context + "." + json.id; context = context + "." + json.id;
const self = this; const self = this;
this.id = json.id; this.id = json.id;
this.name = Translations.T(json.name, context + ".name"); this.name = Translations.T(json.name, context + ".name");
if(json.description !== undefined){ if (json.description !== undefined) {
if(Object.keys(json.description).length === 0){ if (Object.keys(json.description).length === 0) {
json.description = undefined; json.description = undefined;
} }
} }
this.description =Translations.T(json.description, context + ".description") ; this.description = Translations.T(
json.description,
context + ".description"
);
let legacy = undefined; let legacy = undefined;
if (json["overpassTags"] !== undefined) { if (json["overpassTags"] !== undefined) {
@ -83,45 +89,54 @@ export default class LayerConfig {
} }
if (json.source !== undefined) { if (json.source !== undefined) {
if (legacy !== 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; let osmTags: TagsFilter = legacy;
if (json.source["osmTags"]) { 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){ if (json.source["geoJsonSource"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geoJsonSource'" throw context + "Use 'geoJson' instead of 'geoJsonSource'";
} }
this.source = new SourceConfig({ this.source = new SourceConfig(
osmTags: osmTags, {
geojsonSource: json.source["geoJson"], osmTags: osmTags,
geojsonSourceLevel: json.source["geoJsonZoomLevel"], geojsonSource: json.source["geoJson"],
overpassScript: json.source["overpassScript"], geojsonSourceLevel: json.source["geoJsonZoomLevel"],
isOsmCache: json.source["isOsmCache"] overpassScript: json.source["overpassScript"],
}, this.id); isOsmCache: json.source["isOsmCache"],
},
this.id
);
} else { } else {
this.source = new SourceConfig({ this.source = new SourceConfig({
osmTags: legacy osmTags: legacy,
}) });
} }
this.calculatedTags = undefined; this.calculatedTags = undefined;
if (json.calculatedTags !== undefined) { if (json.calculatedTags !== undefined) {
if (!official) { 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 = []; this.calculatedTags = [];
for (const kv of json.calculatedTags) { for (const kv of json.calculatedTags) {
const index = kv.indexOf("=");
const index = kv.indexOf("=")
const key = kv.substring(0, index); const key = kv.substring(0, index);
const code = kv.substring(index + 1); const code = kv.substring(index + 1);
this.calculatedTags.push([key, code]) this.calculatedTags.push([key, code]);
} }
} }
@ -130,13 +145,19 @@ export default class LayerConfig {
this.minzoom = json.minzoom ?? 0; this.minzoom = json.minzoom ?? 0;
this.maxzoom = json.maxzoom ?? 1000; this.maxzoom = json.maxzoom ?? 1000;
this.wayHandling = json.wayHandling ?? 0; this.wayHandling = json.wayHandling ?? 0;
this.presets = (json.presets ?? []).map((pr, i) => this.presets = (json.presets ?? []).map((pr, i) => {
({ if (pr.preciseInput === true) {
pr.preciseInput = {
preferredBackground: undefined
}
}
return {
title: Translations.T(pr.title, `${context}.presets[${i}].title`), 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`) 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 /** Given a key, gets the corresponding property from the json (or the default if not found
* *
@ -148,7 +169,11 @@ export default class LayerConfig {
if (deflt === undefined) { if (deflt === undefined) {
return 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") { if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering.get(v); const shared = SharedTagRenderings.SharedTagRendering.get(v);
@ -156,54 +181,80 @@ export default class LayerConfig {
return shared; 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 * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
* A string is interpreted as a name to call * 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) { if (tagRenderings === undefined) {
return []; return [];
} }
return Utils.NoNull(tagRenderings.map( return Utils.NoNull(
(renderingJson, i) => { tagRenderings.map((renderingJson, i) => {
if (typeof renderingJson === "string") { if (typeof renderingJson === "string") {
if (renderingJson === "questions") { if (renderingJson === "questions") {
if (readOnly) { 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 =
const shared = SharedTagRenderings.SharedTagRendering.get(renderingJson); SharedTagRenderings.SharedTagRendering.get(renderingJson);
if (shared !== undefined) { if (shared !== undefined) {
return shared; return shared;
} }
const keys = Array.from(SharedTagRenderings.SharedTagRendering.keys()) const keys = Array.from(
SharedTagRenderings.SharedTagRendering.keys()
if(Utils.runningFromConsole){ );
if (Utils.runningFromConsole) {
return undefined; 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.tagRenderings = trs(json.tagRenderings, false);
this.filters = (json.filter ?? []).map((option, i) => {
return new FilterConfig(option, `${context}.filter-[${i}]`)
});
const titleIcons = []; const titleIcons = [];
const defaultIcons = ["phonelink", "emaillink", "wikipedialink", "osmlink", "sharelink"]; const defaultIcons = [
for (const icon of (json.titleIcons ?? defaultIcons)) { "phonelink",
"emaillink",
"wikipedialink",
"osmlink",
"sharelink",
];
for (const icon of json.titleIcons ?? defaultIcons) {
if (icon === "defaults") { if (icon === "defaults") {
titleIcons.push(...defaultIcons); titleIcons.push(...defaultIcons);
} else { } else {
@ -213,74 +264,85 @@ export default class LayerConfig {
this.titleIcons = trs(titleIcons, true); this.titleIcons = trs(titleIcons, true);
this.title = tr("title", undefined); this.title = tr("title", undefined);
this.icon = tr("icon", ""); this.icon = tr("icon", "");
this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => { this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => {
let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`); let tr = new TagRenderingConfig(
if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) { overlay.then,
self.source.osmTags,
`iconoverlays.${i}`
);
if (
typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then); tr = SharedTagRenderings.SharedIcons.get(overlay.then);
} }
return { return {
if: FromJSON.Tag(overlay.if), if: FromJSON.Tag(overlay.if),
then: tr, then: tr,
badge: overlay.badge ?? false badge: overlay.badge ?? false,
} };
}); });
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
if (iconPath.startsWith(Utils.assets_path)) { if (iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length); const iconKey = iconPath.substr(Utils.assets_path.length);
if (Svg.All[iconKey] === undefined) { 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.isShown = tr("isShown", "yes");
this.iconSize = tr("iconSize", "40,40,center"); this.iconSize = tr("iconSize", "40,40,center");
this.label = tr("label", "") this.label = tr("label", "");
this.color = tr("color", "#0000ff"); this.color = tr("color", "#0000ff");
this.width = tr("width", "7"); this.width = tr("width", "7");
this.rotation = tr("rotation", "0"); this.rotation = tr("rotation", "0");
this.dashArray = tr("dashArray", ""); 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) { 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[] { public CustomCodeSnippets(): string[] {
if (this.calculatedTags === undefined) { if (this.calculatedTags === undefined) {
return [] return [];
} }
return this.calculatedTags.map(code => code[1]); return this.calculatedTags.map((code) => code[1]);
} }
public AddRoamingRenderings(addAll: { public AddRoamingRenderings(addAll: {
tagRenderings: TagRenderingConfig[], tagRenderings: TagRenderingConfig[];
titleIcons: TagRenderingConfig[], titleIcons: TagRenderingConfig[];
iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] iconOverlays: {
if: TagsFilter;
then: TagRenderingConfig;
badge: boolean;
}[];
}): LayerConfig { }): LayerConfig {
let insertionPoint = this.tagRenderings
let insertionPoint = this.tagRenderings.map(tr => tr.IsQuestionBoxElement()).indexOf(true) .map((tr) => tr.IsQuestionBoxElement())
.indexOf(true);
if (insertionPoint < 0) { if (insertionPoint < 0) {
// No 'questions' defined - we just add them all to the end // No 'questions' defined - we just add them all to the end
insertionPoint = this.tagRenderings.length; insertionPoint = this.tagRenderings.length;
} }
this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings); this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings);
this.iconOverlays.push(...addAll.iconOverlays); this.iconOverlays.push(...addAll.iconOverlays);
for (const icon of addAll.titleIcons) { for (const icon of addAll.titleIcons) {
this.titleIcons.splice(0, 0, icon); this.titleIcons.splice(0, 0, icon);
@ -289,40 +351,42 @@ export default class LayerConfig {
} }
public GetRoamingRenderings(): { public GetRoamingRenderings(): {
tagRenderings: TagRenderingConfig[], tagRenderings: TagRenderingConfig[];
titleIcons: TagRenderingConfig[], titleIcons: TagRenderingConfig[];
iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[] iconOverlays: {
if: TagsFilter;
then: TagRenderingConfig;
badge: boolean;
}[];
} { } {
const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming);
const tagRenderings = this.tagRenderings.filter(tr => tr.roaming); const titleIcons = this.titleIcons.filter((tr) => tr.roaming);
const titleIcons = this.titleIcons.filter(tr => tr.roaming); const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming);
const iconOverlays = this.iconOverlays.filter(io => io.then.roaming)
return { return {
tagRenderings: tagRenderings, tagRenderings: tagRenderings,
titleIcons: titleIcons, titleIcons: titleIcons,
iconOverlays: iconOverlays iconOverlays: iconOverlays,
} };
} }
public GenerateLeafletStyle(tags: UIEventSource<any>, clickable: boolean, widthHeight= "100%"): public GenerateLeafletStyle(
{ tags: UIEventSource<any>,
icon: clickable: boolean,
{ widthHeight = "100%"
html: BaseUIElement, ): {
iconSize: [number, number], icon: {
iconAnchor: [number, number], html: BaseUIElement;
popupAnchor: [number, number], iconSize: [number, number];
iconUrl: string, iconAnchor: [number, number];
className: string popupAnchor: [number, number];
}, iconUrl: string;
color: string, className: string;
weight: number, };
dashArray: number[] color: string;
} { weight: number;
dashArray: number[];
} {
function num(str, deflt = 40) { function num(str, deflt = 40) {
const n = Number(str); const n = Number(str);
if (isNaN(n)) { if (isNaN(n)) {
@ -341,7 +405,7 @@ export default class LayerConfig {
} }
function render(tr: TagRenderingConfig, deflt?: string) { 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, ""); return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
} }
@ -350,14 +414,16 @@ export default class LayerConfig {
let color = render(this.color, "#00f"); let color = render(this.color, "#00f");
if (color.startsWith("--")) { 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 weight = rendernum(this.width, 5);
const iconW = num(iconSize[0]); const iconW = num(iconSize[0]);
let iconH = num(iconSize[1]); let iconH = num(iconSize[1]);
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
let anchorW = iconW / 2; let anchorW = iconW / 2;
let anchorH = iconH / 2; let anchorH = iconH / 2;
@ -377,31 +443,35 @@ export default class LayerConfig {
const iconUrlStatic = render(this.icon); const iconUrlStatic = render(this.icon);
const self = this; const self = this;
const mappedHtml = tags.map(tgs => { const mappedHtml = tags.map((tgs) => {
function genHtmlFromString(sourcePart: string): BaseUIElement { function genHtmlFromString(sourcePart: string): BaseUIElement {
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
let html: BaseUIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`); let html: BaseUIElement = new FixedUiElement(
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/) `<img src="${sourcePart}" style="${style}" />`
);
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) { if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
html = new Combine([ html = new Combine([
(Svg.All[match[1] + ".svg"] as string) (Svg.All[match[1] + ".svg"] as string).replace(
.replace(/#000000/g, match[2]) /#000000/g,
match[2]
),
]).SetStyle(style); ]).SetStyle(style);
} }
return html; return html;
} }
// What do you mean, 'tgs' is never read? // What do you mean, 'tgs' is never read?
// It is read implicitly in the 'render' method // It is read implicitly in the 'render' method
const iconUrl = render(self.icon); const iconUrl = render(self.icon);
const rotation = render(self.rotation, "0deg"); const rotation = render(self.rotation, "0deg");
let htmlParts: BaseUIElement[] = []; 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) { for (const sourcePart of sourceParts) {
htmlParts.push(genHtmlFromString(sourcePart)) htmlParts.push(genHtmlFromString(sourcePart));
} }
let badges = []; let badges = [];
@ -411,79 +481,88 @@ export default class LayerConfig {
} }
if (iconOverlay.badge) { if (iconOverlay.badge) {
const badgeParts: BaseUIElement[] = []; 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) { for (const badgePartStr of partDefs) {
badgeParts.push(genHtmlFromString(badgePartStr)) badgeParts.push(genHtmlFromString(badgePartStr));
} }
const badgeCompound = new Combine(badgeParts) const badgeCompound = new Combine(badgeParts).SetStyle(
.SetStyle("display:flex;position:relative;width:100%;height:100%;"); "display:flex;position:relative;width:100%;height:100%;"
);
badges.push(badgeCompound)
badges.push(badgeCompound);
} else { } else {
htmlParts.push(genHtmlFromString( htmlParts.push(
iconOverlay.then.GetRenderValue(tgs).txt)); genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt)
);
} }
} }
if (badges.length > 0) { if (badges.length > 0) {
const badgesComponent = new Combine(badges) const badgesComponent = new Combine(badges).SetStyle(
.SetStyle("display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"); "display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"
htmlParts.push(badgesComponent) );
htmlParts.push(badgesComponent);
} }
if (sourceParts.length == 0) { if (sourceParts.length == 0) {
iconH = 0 iconH = 0;
} }
try { try {
const label = self.label
const label = self.label?.GetRenderValue(tgs)?.Subs(tgs) ?.GetRenderValue(tgs)
?.Subs(tgs)
?.SetClass("block text-center") ?.SetClass("block text-center")
?.SetStyle("margin-top: " + (iconH + 2) + "px") ?.SetStyle("margin-top: " + (iconH + 2) + "px");
if (label !== undefined) { 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) { } catch (e) {
console.error(e, tgs) console.error(e, tgs);
} }
return new Combine(htmlParts); return new Combine(htmlParts);
}) });
return { return {
icon: icon: {
{ html: new VariableUiElement(mappedHtml),
html: new VariableUiElement(mappedHtml), iconSize: [iconW, iconH],
iconSize: [iconW, iconH], iconAnchor: [anchorW, anchorH],
iconAnchor: [anchorW, anchorH], popupAnchor: [0, 3 - anchorH],
popupAnchor: [0, 3 - anchorH], iconUrl: iconUrlStatic,
iconUrl: iconUrlStatic, className: clickable
className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable" ? "leaflet-div-icon"
}, : "leaflet-div-icon unclickable",
},
color: color, color: color,
weight: weight, weight: weight,
dashArray: dashArray dashArray: dashArray,
}; };
} }
public ExtractImages(): Set<string> { public ExtractImages(): Set<string> {
const parts: Set<string>[] = [] const parts: Set<string>[] = [];
parts.push(...this.tagRenderings?.map(tr => tr.ExtractImages(false))) parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false)));
parts.push(...this.titleIcons?.map(tr => tr.ExtractImages(true))) parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true)));
parts.push(this.icon?.ExtractImages(true)) parts.push(this.icon?.ExtractImages(true));
parts.push(...this.iconOverlays?.map(overlay => overlay.then.ExtractImages(true))) parts.push(
...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true))
);
for (const preset of this.presets) { 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>(); const allIcons = new Set<string>();
for (const part of parts) { for (const part of parts) {
part?.forEach(allIcons.add, allIcons) part?.forEach(allIcons.add, allIcons);
} }
return allIcons; return allIcons;
} }
}
}

View file

@ -1,6 +1,7 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson"; import {AndOrTagConfigJson} from "./TagConfigJson";
import {DeleteConfigJson} from "./DeleteConfigJson"; import {DeleteConfigJson} from "./DeleteConfigJson";
import FilterConfigJson from "./FilterConfigJson";
/** /**
* Configuration for a single layer * Configuration for a single layer
@ -217,6 +218,16 @@ export interface LayerConfigJson {
* (The first sentence is until the first '.'-character in the description) * (The first sentence is until the first '.'-character in the description)
*/ */
description?: string | any, description?: string | any,
/**
* If set, the user will prompted to confirm the location before actually adding the data.
* THis will be with a 'drag crosshair'-method.
*
* If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category.
*/
preciseInput?: true | {
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string
}
}[], }[],
/** /**
@ -233,6 +244,12 @@ export interface LayerConfigJson {
*/ */
tagRenderings?: (string | TagRenderingConfigJson) [], 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. * 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. * If set, a dialog is shown to the user to (soft) delete the point.

View file

@ -42,6 +42,7 @@ export default class LayoutConfig {
public readonly enableGeolocation: boolean; public readonly enableGeolocation: boolean;
public readonly enableBackgroundLayerSelection: boolean; public readonly enableBackgroundLayerSelection: boolean;
public readonly enableShowAllQuestions: boolean; public readonly enableShowAllQuestions: boolean;
public readonly enableExportButton: boolean;
public readonly customCss?: string; public readonly customCss?: string;
/* /*
How long is the cache valid, in seconds? How long is the cache valid, in seconds?
@ -152,6 +153,7 @@ export default class LayoutConfig {
this.enableAddNewPoints = json.enableAddNewPoints ?? true; this.enableAddNewPoints = json.enableAddNewPoints ?? true;
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true; this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
this.enableExportButton = json.enableExportButton ?? false;
this.customCss = json.customCss; this.customCss = json.customCss;
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)

View file

@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson";
* General remark: a type (string | any) indicates either a fixed or a translatable string. * General remark: a type (string | any) indicates either a fixed or a translatable string.
*/ */
export interface LayoutConfigJson { export interface LayoutConfigJson {
/** /**
* The id of this layout. * The id of this layout.
* *
@ -225,6 +226,10 @@ export interface LayoutConfigJson {
* *
* Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user.
* This is handled by defining units. * This is handled by defining units.
*
* # Rendering
*
* To render a value with long (human) denomination, use {canonical(key)}
* *
* # Usage * # Usage
* *
@ -331,4 +336,5 @@ export interface LayoutConfigJson {
enableGeolocation?: boolean; enableGeolocation?: boolean;
enableBackgroundLayerSelection?: boolean; enableBackgroundLayerSelection?: boolean;
enableShowAllQuestions?: boolean; enableShowAllQuestions?: boolean;
enableExportButton?: boolean;
} }

View file

@ -26,6 +26,9 @@ export default class TagRenderingConfig {
readonly key: string, readonly key: string,
readonly type: string, readonly type: string,
readonly addExtraTags: TagsFilter[]; readonly addExtraTags: TagsFilter[];
readonly inline: boolean,
readonly default?: string,
readonly helperArgs?: (string | number | boolean)[]
}; };
readonly multiAnswer: boolean; readonly multiAnswer: boolean;
@ -73,7 +76,9 @@ export default class TagRenderingConfig {
type: json.freeform.type ?? "string", type: json.freeform.type ?? "string",
addExtraTags: json.freeform.addExtraTags?.map((tg, i) => addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
helperArgs: json.freeform.helperArgs
} }
if (json.freeform["extraTags"] !== undefined) { if (json.freeform["extraTags"] !== undefined) {
@ -332,20 +337,20 @@ export default class TagRenderingConfig {
* Note: this might be hidden by conditions * Note: this might be hidden by conditions
*/ */
public hasMinimap(): boolean { public hasMinimap(): boolean {
const translations : Translation[]= Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); const translations: Translation[] = Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]);
for (const translation of translations) { for (const translation of translations) {
for (const key in translation.translations) { for (const key in translation.translations) {
if(!translation.translations.hasOwnProperty(key)){ if (!translation.translations.hasOwnProperty(key)) {
continue continue
} }
const template = translation.translations[key] const template = translation.translations[key]
const parts = SubstitutedTranslation.ExtractSpecialComponents(template) const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap") const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
if(hasMiniMap){ if (hasMiniMap) {
return true; return true;
} }
} }
} }
return false; return false;
} }
} }

View file

@ -30,6 +30,7 @@ export interface TagRenderingConfigJson {
* Allow freeform text input from the user * Allow freeform text input from the user
*/ */
freeform?: { freeform?: {
/** /**
* If this key is present, then 'render' is used to display the value. * If this key is present, then 'render' is used to display the value.
* If this is undefined, the rendering is _always_ shown * If this is undefined, the rendering is _always_ shown
@ -40,13 +41,30 @@ export interface TagRenderingConfigJson {
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
*/ */
type?: string, type?: string,
/**
* Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md'
*/
helperArgs?: (string | number | boolean)[];
/** /**
* If a value is added with the textfield, these extra tag is addded. * If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked' * Useful to add a 'fixme=freeform textfield used - to be checked'
**/ **/
addExtraTags?: string[]; addExtraTags?: string[];
/**
* When set, influences the way a question is asked.
* Instead of showing a full-widht text field, the text field will be shown within the rendering of the question.
*
* This combines badly with special input elements, as it'll distort the layout.
*/
inline?: boolean
/**
* default value to enter if no previous tagging is present.
* Normally undefined (aka do not enter anything)
*/
default?: string
}, },
/** /**

View file

@ -18,9 +18,9 @@
Development Development
----------- -----------
**Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio, open everything in a 'new WSL Window'. **Windows users**: All scripts are made for linux devices. Use the Ubuntu terminal for Windows (or even better - make the switch ;) ). If you are using Visual Studio Code you can use a [WSL Remote](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) window, or use the Devcontainer (see more details later).
To develop and build MapComplete, yo To develop and build MapComplete, you
0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15 0. Make sure you have a recent version of nodejs - at least 12.0, preferably 15
0. Make a fork and clone the repository. 0. Make a fork and clone the repository.
@ -29,6 +29,30 @@
4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html 4. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version. 5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
Development using Windows
------------------------
For Windows you can use the devcontainer, or the WSL subsystem.
To use the devcontainer in Visual Studio Code:
0. Make sure you have installed the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension and it's dependencies.
1. Make a fork and clone the repository.
2. After cloning, Visual Studio Code will ask you if you want to use the devcontainer.
3. Then you can either clone it again in a volume (for better performance), or open the current folder in a container.
4. By now, you should be able to run `npm run start` to host a local testversion at http://localhost:1234/index.html
5. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
To use the WSL in Visual Studio Code:
0. Make sure you have installed the [Remote - WSL]() extension and it's dependencies.
1. Open a remote WSL window using the button in the bottom left.
2. Make a fork and clone the repository.
3. Install `npm` using `sudo apt install npm`.
4. Run `npm run init` and generate some additional dependencies and generated files. Note that it'll install the dependencies too
5. Run `npm run start` to host a local testversion at http://localhost:1234/index.html
6. By default, a landing page with available themes is served. In order to load a single theme, use `layout=themename` or `userlayout=true#<layout configuration>` as [Query parameter](URL_Parameters.md). Note that the shorter URLs (e.g. `bookcases.html`, `aed.html`, ...) _don't_ exist on the development version.
Automatic deployment Automatic deployment
-------------------- --------------------

View file

@ -20,126 +20,158 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are
Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
layer-control-toggle backend
----------------------
Whether or not the layer control is shown The default value is _false_
tab
-----
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 >50 changesets) The default value is _0_
z
---
The initial/current zoom level The default value is _0_
lat
-----
The initial/current latitude The default value is _0_
lon
-----
The initial/current longitude of the app The default value is _0_
fs-userbadge
--------------
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. The default value is _true_
fs-search
-----------
Disables/Enables the search bar The default value is _true_
fs-layers
-----------
Disables/Enables the layer control The default value is _true_
fs-add-new
------------
Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_
fs-welcome-message
--------------------
Disables/enables the help menu or welcome message The default value is _true_
fs-iframe
-----------
Disables/Enables the iframe-popup The default value is _false_
fs-more-quests
----------------
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
fs-share-screen
-----------------
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
fs-geolocation
----------------
Disables/Enables the geolocation button The default value is _true_
fs-all-questions
------------------
Always show all questions The default value is _false_
test
------
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 The default value is _false_
debug
-------
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
backend
--------- ---------
The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_
custom-css test
------
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 The default value is _false_
layout
--------
The layout to load into MapComplete The default value is __
userlayout
------------ ------------
If specified, the custom css from the given link will be loaded additionaly The default value is __ If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways:
- The hash of the URL contains a base64-encoded .json-file containing the theme definition
- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator
- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme The default value is _false_
background layer-control-toggle
----------------------
Whether or not the layer control is shown The default value is _false_
tab
-----
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 >50 changesets) The default value is _0_
z
---
The initial/current zoom level The default value is _14_
lat
-----
The initial/current latitude The default value is _51.2095_
lon
-----
The initial/current longitude of the app The default value is _3.2228_
fs-userbadge
--------------
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. The default value is _true_
fs-search
-----------
Disables/Enables the search bar The default value is _true_
fs-layers
-----------
Disables/Enables the layer control The default value is _true_
fs-add-new
------------ ------------
The id of the background layer to start with The default value is _osm_ Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_
fs-welcome-message
--------------------
Disables/enables the help menu or welcome message The default value is _true_
fs-iframe
-----------
Disables/Enables the iframe-popup The default value is _false_
fs-more-quests
----------------
Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_
fs-share-screen
-----------------
Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_
fs-geolocation
----------------
Disables/Enables the geolocation button The default value is _true_
fs-all-questions
------------------
Always show all questions The default value is _false_
fs-export
-----------
If set, enables the 'download'-button to download everything as geojson The default value is _false_
fake-user
-----------
If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_
debug
-------
If true, shows some extra debugging help such as all the available tags on every object The default value is _false_
custom-css
------------
If specified, the custom css from the given link will be loaded additionaly The default value is __
background
------------
The id of the background layer to start with The default value is _osm_
oauth_token
-------------
Used to complete the login No default value set
layer-<layer-id> layer-<layer-id>
------------------ ------------------

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,12 @@
import * as editorlayerindex from "../../assets/editor-layer-index.json" import * as editorlayerindex from "../../assets/editor-layer-index.json"
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import * as L from "leaflet"; import * as L from "leaflet";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers"; import * as X from "leaflet-providers";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations"; import {GeoOperations} from "../GeoOperations";
import {TileLayer} from "leaflet";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
/** /**
* Calculates which layers are available at the current location * Calculates which layers are available at the current location
@ -24,45 +25,87 @@ export default class AvailableBaseLayers {
false, false), false, false),
feature: null, feature: null,
max_zoom: 19, max_zoom: 19,
min_zoom: 0 min_zoom: 0,
isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
category: "osmbasedmap"
} }
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
public availableEditorLayers: UIEventSource<BaseLayer[]>;
constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) { public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const self = this; const source = location.map(
this.availableEditorLayers = (currentLocation) => {
location.map(
(currentLocation) => {
if (currentLocation === undefined) { if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview; return AvailableBaseLayers.layerOverview;
} }
const currentLayers = self.availableEditorLayers?.data; const currentLayers = source?.data; // A bit unorthodox - I know
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
if (currentLayers === undefined) { if (currentLayers === undefined) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers; return newLayers;
} }
if (newLayers.length !== currentLayers.length) { }
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
}
return currentLayers;
});
return currentLayers;
});
return source;
} }
private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.AvailableLayersAt(location).map(available => {
// First float all 'best layers' to the top
available.sort((a, b) => {
if (a.isBest && b.isBest) {
return 0;
}
if (!a.isBest) {
return 1
}
return -1;
}
)
if (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
}
prefered.reverse();
for (const category of prefered) {
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
available.sort((a, b) => {
if (a.category === category && b.category === category) {
return 0;
}
if (a.category !== category) {
return 1
}
return -1;
}
)
}
return available[0]
})
}
private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [AvailableBaseLayers.osmCarto] const availableLayers = [AvailableBaseLayers.osmCarto]
const globalLayers = []; const globalLayers = [];
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
@ -140,7 +183,9 @@ export default class AvailableBaseLayers {
min_zoom: props.min_zoom ?? 1, min_zoom: props.min_zoom ?? 1,
name: props.name, name: props.name,
layer: leafletLayer, layer: leafletLayer,
feature: layer feature: layer,
isBest: props.best ?? false,
category: props.category
}); });
} }
return layers; return layers;
@ -152,15 +197,16 @@ export default class AvailableBaseLayers {
function l(id: string, name: string): BaseLayer { function l(id: string, name: string): BaseLayer {
try { try {
const layer: any = () => L.tileLayer.provider(id, undefined); const layer: any = () => L.tileLayer.provider(id, undefined);
const baseLayer: BaseLayer = { return {
feature: null, feature: null,
id: id, id: id,
name: name, name: name,
layer: layer, layer: layer,
min_zoom: layer.minzoom, min_zoom: layer.minzoom,
max_zoom: layer.maxzoom max_zoom: layer.maxzoom,
category: "osmbasedmap",
isBest: false
} }
return baseLayer
} catch (e) { } catch (e) {
console.error("Could not find provided layer", name, e); console.error("Could not find provided layer", name, e);
return null; return null;

View file

@ -1,265 +1,271 @@
import * as L from "leaflet"; import * as L from "leaflet";
import { UIEventSource } from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import { Utils } from "../../Utils";
import Svg from "../../Svg"; import Svg from "../../Svg";
import Img from "../../UI/Base/Img"; import Img from "../../UI/Base/Img";
import { LocalStorageSource } from "../Web/LocalStorageSource"; import {LocalStorageSource} from "../Web/LocalStorageSource";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import { VariableUiElement } from "../../UI/Base/VariableUIElement"; import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement"; import {CenterFlexedElement} from "../../UI/Base/CenterFlexedElement";
export default class GeoLocationHandler extends VariableUiElement { export default class GeoLocationHandler extends VariableUiElement {
/** /**
* Wether or not the geolocation is active, aka the user requested the current location * Wether or not the geolocation is active, aka the user requested the current location
* @private * @private
*/ */
private readonly _isActive: UIEventSource<boolean>; private readonly _isActive: UIEventSource<boolean>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{
latlng: any;
accuracy: number;
}>;
/**
* Kept in order to update the marker
* @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.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
constructor( /**
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
leafletMap: UIEventSource<L.Map>, * @private
layoutToUse: UIEventSource<LayoutConfig> */
) { private readonly _isLocked: UIEventSource<boolean>;
const hasLocation = currentGPSLocation.map(
(location) => location !== undefined
);
const previousLocationGrant = LocalStorageSource.Get(
"geolocation-permissions"
);
const isActive = new UIEventSource<boolean>(false);
super( /**
hasLocation.map( * The callback over the permission API
(hasLocation) => { * @private
if (hasLocation) { */
return new CenterFlexedElement( private readonly _permission: UIEventSource<string>;
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
); // crosshair_blue_ui()
}
if (isActive.data) {
return new CenterFlexedElement(
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
); // crosshair_blue_center_ui
}
return new CenterFlexedElement(
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
); //crosshair_ui
},
[isActive]
)
);
this._isActive = isActive;
this._permission = new UIEventSource<string>("");
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
const currentPointer = this._isActive.map( /***
(isActive) => { * The marker on the map, in order to update it
if (isActive && !self._hasLocation.data) { * @private
return "cursor-wait"; */
} private _marker: L.Marker;
return "cursor-pointer"; /**
}, * Literally: _currentGPSLocation.data != undefined
[this._hasLocation] * @private
); */
currentPointer.addCallbackAndRun((pointerClass) => { private readonly _hasLocation: UIEventSource<boolean>;
self.SetClass(pointerClass); private readonly _currentGPSLocation: UIEventSource<{
}); latlng: any;
accuracy: number;
}>;
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
this.onClick(() => self.init(true));
this.init(false);
}
private init(askPermission: boolean) { /**
const self = this; * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
const map = this._leafletMap.data; * @private
*/
private _lastUserRequest: Date;
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest = /**
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; * A small flag on localstorage. If the user previously granted the geolocation, it will be set.
if (timeSinceRequest < 30) { * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
self.MoveToCurrentLoction(16); *
} * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
let color = "#1111cc"; constructor(
try { currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
color = getComputedStyle(document.body).getPropertyValue( leafletMap: UIEventSource<L.Map>,
"--catch-detail-color" layoutToUse: UIEventSource<LayoutConfig>
);
} catch (e) {
console.error(e);
}
const icon = L.icon({
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
});
const newMarker = L.marker(location.latlng, { icon: icon });
newMarker.addTo(map);
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
try {
navigator?.permissions
?.query({ name: "geolocation" })
?.then(function (status) {
console.log("Geolocation is already", status);
if (status.state === "granted") {
self.StartGeolocating(false);
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
};
});
} catch (e) {
console.error(e);
}
if (askPermission) {
self.StartGeolocating(true);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
self.StartGeolocating(false);
}
}
private locate() {
const self = this;
const map: any = this._leafletMap.data;
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function (position) {
self._currentGPSLocation.setData({
latlng: [position.coords.latitude, position.coords.longitude],
accuracy: position.coords.accuracy,
});
},
function () {
console.warn("Could not get location with navigator.geolocation");
}
);
return;
} else {
map.findAccuratePosition({
maxWait: 10000, // defaults to 10000
desiredAccuracy: 50, // defaults to 20
});
}
}
private MoveToCurrentLoction(targetZoom = 16) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
if (
this._currentGPSLocation.data.latlng[0] === 0 &&
this._currentGPSLocation.data.latlng[1] === 0
) { ) {
console.debug("Not moving to GPS-location: it is null island"); const hasLocation = currentGPSLocation.map(
return; (location) => location !== undefined
);
const previousLocationGrant = LocalStorageSource.Get(
"geolocation-permissions"
);
const isActive = new UIEventSource<boolean>(false);
const isLocked = new UIEventSource<boolean>(false);
super(
hasLocation.map(
(hasLocationData) => {
let icon: string;
if (isLocked.data) {
icon = Svg.crosshair_locked;
} else if (hasLocationData) {
icon = Svg.crosshair_blue;
} else if (isActive.data) {
icon = Svg.crosshair_blue_center;
} else {
icon = Svg.crosshair;
}
return new CenterFlexedElement(
Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem")
);
},
[isActive, isLocked]
)
);
this._isActive = isActive;
this._isLocked = isLocked;
this._permission = new UIEventSource<string>("");
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
const currentPointer = this._isActive.map(
(isActive) => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait";
}
return "cursor-pointer";
},
[this._hasLocation]
);
currentPointer.addCallbackAndRun((pointerClass) => {
self.SetClass(pointerClass);
});
this.onClick(() => {
if (self._hasLocation.data) {
self._isLocked.setData(!self._isLocked.data);
}
self.init(true);
});
this.init(false);
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest =
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
self.MoveToCurrentLoction(16);
} else if (self._isLocked.data) {
self.MoveToCurrentLoction();
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
} catch (e) {
console.error(e);
}
const icon = L.icon({
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
});
const map = self._leafletMap.data;
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
} }
// We check that the GPS location is not out of bounds private init(askPermission: boolean) {
const b = this._layoutToUse.data.lockLocation; const self = this;
let inRange = true; if (self._isActive.data) {
if (b) { self.MoveToCurrentLoction(16);
if (b !== true) { return;
// B is an array with our locklocation }
inRange = try {
b[0][0] <= location.latlng[0] && navigator?.permissions
location.latlng[0] <= b[1][0] && ?.query({name: "geolocation"})
b[0][1] <= location.latlng[1] && ?.then(function (status) {
location.latlng[1] <= b[1][1]; console.log("Geolocation is already", status);
} if (status.state === "granted") {
} self.StartGeolocating(false);
if (!inRange) { }
console.log( self._permission.setData(status.state);
"Not zooming to GPS location: out of bounds", status.onchange = function () {
b, self._permission.setData(status.state);
location.latlng };
); });
} else { } catch (e) {
this._leafletMap.data.setView(location.latlng, targetZoom); console.error(e);
} }
} if (askPermission) {
self.StartGeolocating(true);
private StartGeolocating(zoomToGPS = true) { } else if (this._previousLocationGrant.data === "granted") {
const self = this; this._previousLocationGrant.setData("");
console.log("Starting geolocation"); self.StartGeolocating(false);
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16);
}
console.log("Searching location using GPS");
this.locate();
if (!self._isActive.data) {
self._isActive.setData(true);
Utils.DoEvery(60000, () => {
if (document.visibilityState !== "visible") {
console.log("Not starting gps: document not visible");
return;
} }
this.locate();
});
} }
}
private MoveToCurrentLoction(targetZoom = 16) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
if (
this._currentGPSLocation.data.latlng[0] === 0 &&
this._currentGPSLocation.data.latlng[1] === 0
) {
console.debug("Not moving to GPS-location: it is null island");
return;
}
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.data.lockLocation;
let inRange = true;
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange =
b[0][0] <= location.latlng[0] &&
location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] &&
location.latlng[1] <= b[1][1];
}
}
if (!inRange) {
console.log(
"Not zooming to GPS location: out of bounds",
b,
location.latlng
);
} else {
this._leafletMap.data.setView(location.latlng, targetZoom);
}
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
console.log("Starting geolocation");
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16);
}
console.log("Searching location using GPS");
if (self._isActive.data) {
return;
}
self._isActive.setData(true);
navigator.geolocation.watchPosition(
function (position) {
self._currentGPSLocation.setData({
latlng: [position.coords.latitude, position.coords.longitude],
accuracy: position.coords.accuracy,
});
},
function () {
console.warn("Could not get location with navigator.geolocation");
}
);
}
} }

View file

@ -47,7 +47,12 @@ export default class StrayClickHandler {
popupAnchor: [0, -45] popupAnchor: [0, -45]
}) })
}); });
const popup = L.popup().setContent("<div id='strayclick'></div>"); const popup = L.popup({
autoPan: true,
autoPanPaddingTopLeft: [15,15],
closeOnEscapeKey: true,
autoClose: true
}).setContent("<div id='strayclick' style='height: 65vh'></div>");
self._lastMarker.addTo(leafletMap.data); self._lastMarker.addTo(leafletMap.data);
self._lastMarker.bindPopup(popup); self._lastMarker.bindPopup(popup);

View file

@ -16,7 +16,7 @@ import RegisteringFeatureSource from "./RegisteringFeatureSource";
export default class FeaturePipeline implements FeatureSource { export default class FeaturePipeline implements FeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]> ; public features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name = "FeaturePipeline" public readonly name = "FeaturePipeline"
@ -29,7 +29,7 @@ export default class FeaturePipeline implements FeatureSource {
selectedElement: UIEventSource<any>) { selectedElement: UIEventSource<any>) {
const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([]) const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([])
// first we metatag, then we save to get the metatags into storage too // 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) // Note that we need to register before we do metatagging (as it expects the event sources)
@ -46,8 +46,11 @@ export default class FeaturePipeline implements FeatureSource {
const geojsonSources: FeatureSource [] = GeoJsonSource const geojsonSources: FeatureSource [] = GeoJsonSource
.ConstructMultiSource(flayers.data, locationControl) .ConstructMultiSource(flayers.data, locationControl)
.map(geojsonSource => { .map(geojsonSource => {
let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); let source = new RegisteringFeatureSource(
if(!geojsonSource.isOsmCache){ new FeatureDuplicatorPerLayer(flayers,
geojsonSource
));
if (!geojsonSource.isOsmCache) {
source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features);
} }
return source return source

View file

@ -1,9 +1,45 @@
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
export default interface FeatureSource { export default interface FeatureSource {
features: UIEventSource<{feature: any, freshness: Date}[]>; features: UIEventSource<{ feature: any, freshness: Date }[]>;
/** /**
* Mainly used for debuging * Mainly used for debuging
*/ */
name: string; name: string;
}
export class FeatureSourceUtils {
/**
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
* @param featurePipeline The FeaturePipeline you want to export
* @param options The options object
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
*/
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
let defaults = {
metadata: false,
}
options = Utils.setDefaults(options, defaults);
// Select all features, ignore the freshness and other data
let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature);
if (!options.metadata) {
for (let i = 0; i < featureList.length; i++) {
let feature = featureList[i];
for (let property in feature.properties) {
if (property[0] == "_") {
delete featureList[i]["properties"][property];
}
}
}
}
return {type: "FeatureCollection", features: featureList}
}
} }

View file

@ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource {
let freshness: Date = time; let freshness: Date = time;
if (feature.properties["_last_edit:timestamp"] !== undefined) { if (feature.properties["_last_edit:timestamp"] !== undefined) {
freshness = new Date(feature["_last_edit:timestamp"]) freshness = new Date(feature.properties["_last_edit:timestamp"])
} }
newFeatures.push({feature: feature, freshness: freshness}) newFeatures.push({feature: feature, freshness: freshness})

View file

@ -6,11 +6,14 @@ export class GeoOperations {
return turf.area(feature); return turf.area(feature);
} }
/**
* Converts a GeoJSon feature to a point feature
* @param feature
*/
static centerpoint(feature: any) { static centerpoint(feature: any) {
const newFeature = turf.center(feature); const newFeature = turf.center(feature);
newFeature.properties = feature.properties; newFeature.properties = feature.properties;
newFeature.id = feature.id; newFeature.id = feature.id;
return newFeature; return newFeature;
} }
@ -273,6 +276,14 @@ export class GeoOperations {
} }
return undefined; 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]){
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
}
} }

View file

@ -6,31 +6,38 @@ import Constants from "../../Models/Constants";
import FeatureSource from "../FeatureSource/FeatureSource"; import FeatureSource from "../FeatureSource/FeatureSource";
import {TagsFilter} from "../Tags/TagsFilter"; import {TagsFilter} from "../Tags/TagsFilter";
import {Tag} from "../Tags/Tag"; import {Tag} from "../Tags/Tag";
import {OsmConnection} from "./OsmConnection";
import {LocalStorageSource} from "../Web/LocalStorageSource";
/** /**
* Handles all changes made to OSM. * Handles all changes made to OSM.
* Needs an authenticator via OsmConnection * Needs an authenticator via OsmConnection
*/ */
export class Changes implements FeatureSource{ export class Changes implements FeatureSource {
private static _nextId = -1; // Newly assigned ID's are negative
public readonly name = "Newly added features" public readonly name = "Newly added features"
/** /**
* The newly created points, as a FeatureSource * The newly created points, as a FeatureSource
*/ */
public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
private static _nextId = -1; // Newly assigned ID's are negative
/** /**
* All the pending changes * All the pending changes
*/ */
public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> = public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", [])
new UIEventSource<{elementId: string; key: string; value: string}[]>([]);
/**
* All the pending new objects to upload
*/
private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", [])
private readonly isUploading = new UIEventSource(false);
/** /**
* Adds a change to the pending changes * Adds a change to the pending changes
*/ */
private static checkChange(kv: {k: string, v: string}): { k: string, v: string } { private static checkChange(kv: { k: string, v: string }): { k: string, v: string } {
const key = kv.k; const key = kv.k;
const value = kv.v; const value = kv.v;
if (key === undefined || key === null) { if (key === undefined || key === null) {
@ -49,8 +56,7 @@ export class Changes implements FeatureSource{
return {k: key.trim(), v: value.trim()}; return {k: key.trim(), v: value.trim()};
} }
addTag(elementId: string, tagsFilter: TagsFilter, addTag(elementId: string, tagsFilter: TagsFilter,
tags?: UIEventSource<any>) { tags?: UIEventSource<any>) {
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
@ -59,7 +65,7 @@ export class Changes implements FeatureSource{
if (changes.length == 0) { if (changes.length == 0) {
return; return;
} }
for (const change of changes) { for (const change of changes) {
if (elementTags[change.k] !== change.v) { if (elementTags[change.k] !== change.v) {
elementTags[change.k] = change.v; elementTags[change.k] = change.v;
@ -76,16 +82,16 @@ export class Changes implements FeatureSource{
* Uploads all the pending changes in one go. * Uploads all the pending changes in one go.
* Triggered by the 'PendingChangeUploader'-actor in Actors * Triggered by the 'PendingChangeUploader'-actor in Actors
*/ */
public flushChanges(flushreason: string = undefined){ public flushChanges(flushreason: string = undefined) {
if(this.pending.data.length === 0){ if (this.pending.data.length === 0) {
return; return;
} }
if(flushreason !== undefined){ if (flushreason !== undefined) {
console.log(flushreason) console.log(flushreason)
} }
this.uploadAll([], this.pending.data); this.uploadAll();
this.pending.setData([]);
} }
/** /**
* Create a new node element at the given lat/long. * Create a new node element at the given lat/long.
* An internal OsmObject is created to upload later on, a geojson represention is returned. * An internal OsmObject is created to upload later on, a geojson represention is returned.
@ -93,12 +99,12 @@ export class Changes implements FeatureSource{
*/ */
public createElement(basicTags: Tag[], lat: number, lon: number) { public createElement(basicTags: Tag[], lat: number, lon: number) {
console.log("Creating a new element with ", basicTags) console.log("Creating a new element with ", basicTags)
const osmNode = new OsmNode(Changes._nextId); const newId = Changes._nextId;
Changes._nextId--; Changes._nextId--;
const id = "node/" + osmNode.id; const id = "node/" + newId;
osmNode.lat = lat;
osmNode.lon = lon;
const properties = {id: id}; const properties = {id: id};
const geojson = { const geojson = {
@ -118,35 +124,49 @@ export class Changes implements FeatureSource{
// The tags are not yet written into the OsmObject, but this is applied onto a // The tags are not yet written into the OsmObject, but this is applied onto a
const changes = []; const changes = [];
for (const kv of basicTags) { for (const kv of basicTags) {
properties[kv.key] = kv.value;
if (typeof kv.value !== "string") { if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset" 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}) changes.push({elementId: id, key: kv.key, value: kv.value})
} }
console.log("New feature added and pinged") console.log("New feature added and pinged")
this.features.data.push({feature:geojson, freshness: new Date()}); this.features.data.push({feature: geojson, freshness: new Date()});
this.features.ping(); this.features.ping();
State.state.allElements.addOrGetElement(geojson).ping(); State.state.allElements.addOrGetElement(geojson).ping();
this.uploadAll([osmNode], changes); 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; return geojson;
} }
private uploadChangesWithLatestVersions( private uploadChangesWithLatestVersions(
knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { knownElements: OsmObject[]) {
const knownById = new Map<string, OsmObject>(); const knownById = new Map<string, OsmObject>();
knownElements.forEach(knownElement => { knownElements.forEach(knownElement => {
knownById.set(knownElement.type + "/" + knownElement.id, 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 // 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 // We apply the changes on them
for (const change of pending) { for (const change of this.pending.data) {
if (parseInt(change.elementId.split("/")[1]) < 0) { if (parseInt(change.elementId.split("/")[1]) < 0) {
// This is a new element - we should apply this on one of the new elements // This is a new element - we should apply this on one of the new elements
for (const newElement of newElements) { for (const newElement of newElements) {
@ -168,9 +188,17 @@ export class Changes implements FeatureSource{
} }
} }
if (changedElements.length == 0 && newElements.length == 0) { if (changedElements.length == 0 && newElements.length == 0) {
console.log("No changes in any object"); console.log("No changes in any object - clearing");
this.pending.setData([])
this.newObjects.setData([])
return; return;
} }
const self = this;
if (this.isUploading.data) {
return;
}
this.isUploading.setData(true)
console.log("Beginning upload..."); console.log("Beginning upload...");
// At last, we build the changeset and upload // At last, we build the changeset and upload
@ -213,17 +241,22 @@ export class Changes implements FeatureSource{
changes += "</osmChange>"; changes += "</osmChange>";
return changes; return changes;
}); },
() => {
console.log("Upload successfull!")
self.newObjects.setData([])
self.pending.setData([]);
self.isUploading.setData(false)
},
() => self.isUploading.setData(false)
);
}; };
private uploadAll( private uploadAll() {
newElements: OsmObject[],
pending: { elementId: string; key: string; value: string }[]
) {
const self = this; const self = this;
const pending = this.pending.data;
let neededIds: string[] = []; let neededIds: string[] = [];
for (const change of pending) { for (const change of pending) {
const id = change.elementId; const id = change.elementId;
@ -236,8 +269,7 @@ export class Changes implements FeatureSource{
neededIds = Utils.Dedup(neededIds); neededIds = Utils.Dedup(neededIds);
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
console.log("KnownElements:", knownElements) self.uploadChangesWithLatestVersions(knownElements)
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
}) })
} }

View file

@ -27,7 +27,7 @@ export class ChangesetHandler {
} }
} }
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void {
const nodes = response.getElementsByTagName("node"); const nodes = response.getElementsByTagName("node");
// @ts-ignore // @ts-ignore
for (const node of nodes) { for (const node of nodes) {
@ -69,7 +69,9 @@ export class ChangesetHandler {
public UploadChangeset( public UploadChangeset(
layout: LayoutConfig, layout: LayoutConfig,
allElements: ElementStorage, allElements: ElementStorage,
generateChangeXML: (csid: string) => string) { generateChangeXML: (csid: string) => string,
whenDone: (csId: string) => void,
onFail: () => void) {
if (this.userDetails.data.csCount == 0) { if (this.userDetails.data.csCount == 0) {
// The user became a contributor! // The user became a contributor!
@ -80,6 +82,7 @@ export class ChangesetHandler {
if (this._dryRun) { if (this._dryRun) {
const changesetXML = generateChangeXML("123456"); const changesetXML = generateChangeXML("123456");
console.log(changesetXML); console.log(changesetXML);
whenDone("123456")
return; return;
} }
@ -93,12 +96,14 @@ export class ChangesetHandler {
console.log(changeset); console.log(changeset);
self.AddChange(csId, changeset, self.AddChange(csId, changeset,
allElements, allElements,
() => { whenDone,
},
(e) => { (e) => {
console.error("UPLOADING FAILED!", e) console.error("UPLOADING FAILED!", e)
onFail()
} }
) )
}, {
onFail: onFail
}) })
} else { } else {
// There still exists an open changeset (or at least we hope so) // There still exists an open changeset (or at least we hope so)
@ -107,15 +112,13 @@ export class ChangesetHandler {
csId, csId,
generateChangeXML(csId), generateChangeXML(csId),
allElements, allElements,
() => { whenDone,
},
(e) => { (e) => {
console.warn("Could not upload, changeset is probably closed: ", e); console.warn("Could not upload, changeset is probably closed: ", e);
// Mark the CS as closed... // Mark the CS as closed...
this.currentChangeset.setData(""); this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist // ... and try again. As the cs is closed, no recursive loop can exist
self.UploadChangeset(layout, allElements, generateChangeXML); self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
} }
) )
@ -161,18 +164,22 @@ export class ChangesetHandler {
const self = this; const self = this;
this.OpenChangeset(layout, (csId: string) => { this.OpenChangeset(layout, (csId: string) => {
// The cs is open - let us actually upload! // The cs is open - let us actually upload!
const changes = generateChangeXML(csId) const changes = generateChangeXML(csId)
self.AddChange(csId, changes, allElements, (csId) => { self.AddChange(csId, changes, allElements, (csId) => {
console.log("Successfully deleted ", object.id) console.log("Successfully deleted ", object.id)
self.CloseChangeset(csId, continuation) self.CloseChangeset(csId, continuation)
}, (csId) => { }, (csId) => {
alert("Deletion failed... Should not happend") alert("Deletion failed... Should not happend")
// FAILED // FAILED
self.CloseChangeset(csId, continuation) self.CloseChangeset(csId, continuation)
}) })
}, true, reason) }, {
isDeletionCS: true,
deletionReason: reason
}
)
} }
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
@ -204,15 +211,20 @@ export class ChangesetHandler {
private OpenChangeset( private OpenChangeset(
layout: LayoutConfig, layout: LayoutConfig,
continuation: (changesetId: string) => void, continuation: (changesetId: string) => void,
isDeletionCS: boolean = false, options?: {
deletionReason: string = undefined) { isDeletionCS?: boolean,
deletionReason?: string,
onFail?: () => void
}
) {
options = options ?? {}
options.isDeletionCS = options.isDeletionCS ?? false
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
if (isDeletionCS) { if (options.isDeletionCS) {
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
if (deletionReason) { if (options.deletionReason) {
comment += ": " + deletionReason; comment += ": " + options.deletionReason;
} }
} }
@ -221,7 +233,7 @@ export class ChangesetHandler {
const metadata = [ const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`], ["created_by", `MapComplete ${Constants.vNumber}`],
["comment", comment], ["comment", comment],
["deletion", isDeletionCS ? "yes" : undefined], ["deletion", options.isDeletionCS ? "yes" : undefined],
["theme", layout.id], ["theme", layout.id],
["language", Locale.language.data], ["language", Locale.language.data],
["host", window.location.host], ["host", window.location.host],
@ -244,7 +256,9 @@ export class ChangesetHandler {
}, function (err, response) { }, function (err, response) {
if (response === undefined) { if (response === undefined) {
console.log("err", err); console.log("err", err);
alert("Could not upload change (opening failed). Please file a bug report") if(options.onFail){
options.onFail()
}
return; return;
} else { } else {
continuation(response); continuation(response);
@ -265,7 +279,7 @@ export class ChangesetHandler {
private AddChange(changesetId: string, private AddChange(changesetId: string,
changesetXML: string, changesetXML: string,
allElements: ElementStorage, allElements: ElementStorage,
continuation: ((changesetId: string, idMapping: any) => void), continuation: ((changesetId: string) => void),
onFail: ((changesetId: string, reason: string) => void) = undefined) { onFail: ((changesetId: string, reason: string) => void) = undefined) {
this.auth.xhr({ this.auth.xhr({
method: 'POST', method: 'POST',
@ -280,9 +294,9 @@ export class ChangesetHandler {
} }
return; return;
} }
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements); ChangesetHandler.parseUploadChangesetResponse(response, allElements);
console.log("Uploaded changeset ", changesetId); console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping); continuation(changesetId);
}); });
} }

View file

@ -30,7 +30,7 @@ export default class UserDetails {
export class OsmConnection { export class OsmConnection {
public static readonly _oauth_configs = { public static readonly oauth_configs = {
"osm": { "osm": {
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
@ -47,6 +47,7 @@ export class OsmConnection {
public auth; public auth;
public userDetails: UIEventSource<UserDetails>; public userDetails: UIEventSource<UserDetails>;
public isLoggedIn: UIEventSource<boolean> public isLoggedIn: UIEventSource<boolean>
private fakeUser: boolean;
_dryRun: boolean; _dryRun: boolean;
public preferencesHandler: OsmPreferences; public preferencesHandler: OsmPreferences;
public changesetHandler: ChangesetHandler; public changesetHandler: ChangesetHandler;
@ -59,20 +60,31 @@ export class OsmConnection {
url: string url: string
}; };
constructor(dryRun: boolean, oauth_token: UIEventSource<string>, constructor(dryRun: boolean,
fakeUser: boolean,
oauth_token: UIEventSource<string>,
// Used to keep multiple changesets open and to write to the correct changeset // Used to keep multiple changesets open and to write to the correct changeset
layoutName: string, layoutName: string,
singlePage: boolean = true, singlePage: boolean = true,
osmConfiguration: "osm" | "osm-test" = 'osm' osmConfiguration: "osm" | "osm-test" = 'osm'
) { ) {
this.fakeUser = fakeUser;
this._singlePage = singlePage; this._singlePage = singlePage;
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm;
console.debug("Using backend", this._oauth_config.url) console.debug("Using backend", this._oauth_config.url)
OsmObject.SetBackendUrl(this._oauth_config.url + "/") OsmObject.SetBackendUrl(this._oauth_config.url + "/")
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
this.userDetails.data.dryRun = dryRun; this.userDetails.data.dryRun = dryRun || fakeUser;
if(fakeUser){
const ud = this.userDetails.data;
ud.csCount = 5678
ud.loggedIn= true;
ud.unreadMessages = 0
ud.name = "Fake user"
ud.totalMessages = 42;
}
const self =this; const self =this;
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
@ -110,8 +122,10 @@ export class OsmConnection {
public UploadChangeset( public UploadChangeset(
layout: LayoutConfig, layout: LayoutConfig,
allElements: ElementStorage, allElements: ElementStorage,
generateChangeXML: (csid: string) => string) { generateChangeXML: (csid: string) => string,
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); whenDone: (csId: string) => void,
onFail: () => {}) {
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
} }
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
@ -136,6 +150,10 @@ export class OsmConnection {
} }
public AttemptLogin() { public AttemptLogin() {
if(this.fakeUser){
console.log("AttemptLogin called, but ignored as fakeUser is set")
return;
}
const self = this; const self = this;
console.log("Trying to log in..."); console.log("Trying to log in...");
this.updateAuthObject(); this.updateAuthObject();

View file

@ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource";
export abstract class OsmObject { export abstract class OsmObject {
protected static backendURL = "https://www.openstreetmap.org/" private static defaultBackend = "https://www.openstreetmap.org/"
protected static backendURL = OsmObject.defaultBackend;
private static polygonFeatures = OsmObject.constructPolygonFeatures() private static polygonFeatures = OsmObject.constructPolygonFeatures()
private static objectCache = new Map<string, UIEventSource<OsmObject>>(); private static objectCache = new Map<string, UIEventSource<OsmObject>>();
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
@ -37,15 +38,15 @@ export abstract class OsmObject {
} }
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> { static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
let src : UIEventSource<OsmObject>; let src: UIEventSource<OsmObject>;
if (OsmObject.objectCache.has(id)) { if (OsmObject.objectCache.has(id)) {
src = OsmObject.objectCache.get(id) src = OsmObject.objectCache.get(id)
if(forceRefresh){ if (forceRefresh) {
src.setData(undefined) src.setData(undefined)
}else{ } else {
return src; return src;
} }
}else{ } else {
src = new UIEventSource<OsmObject>(undefined) src = new UIEventSource<OsmObject>(undefined)
} }
const splitted = id.split("/"); const splitted = id.split("/");
@ -157,7 +158,7 @@ export abstract class OsmObject {
const minlat = bounds[1][0] const minlat = bounds[1][0]
const maxlat = bounds[0][0]; const maxlat = bounds[0][0];
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
Utils.downloadJson(url).then( data => { Utils.downloadJson(url).then(data => {
const elements: any[] = data.elements; const elements: any[] = data.elements;
const objects = OsmObject.ParseObjects(elements) const objects = OsmObject.ParseObjects(elements)
callback(objects); callback(objects);
@ -291,6 +292,7 @@ export abstract class OsmObject {
self.LoadData(element) self.LoadData(element)
self.SaveExtraData(element, nodes); self.SaveExtraData(element, nodes);
const meta = { const meta = {
"_last_edit:contributor": element.user, "_last_edit:contributor": element.user,
"_last_edit:contributor:uid": element.uid, "_last_edit:contributor:uid": element.uid,
@ -299,6 +301,11 @@ export abstract class OsmObject {
"_version_number": element.version "_version_number": element.version
} }
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
self.tags["_backend"] = OsmObject.backendURL
meta["_backend"] = OsmObject.backendURL;
}
continuation(self, meta); continuation(self, meta);
} }
); );

View file

@ -83,7 +83,8 @@ export default class SimpleMetaTagger {
}, },
(feature => { (feature => {
const units = State.state.layoutToUse.data.units ?? []; const units = State.state?.layoutToUse?.data?.units ?? [];
let rewritten = false;
for (const key in feature.properties) { for (const key in feature.properties) {
if (!feature.properties.hasOwnProperty(key)) { if (!feature.properties.hasOwnProperty(key)) {
continue; continue;
@ -95,16 +96,23 @@ export default class SimpleMetaTagger {
const value = feature.properties[key] const value = feature.properties[key]
const [, denomination] = unit.findDenomination(value) const [, denomination] = unit.findDenomination(value)
let canonical = denomination?.canonicalValue(value) ?? undefined; let canonical = denomination?.canonicalValue(value) ?? undefined;
console.log("Rewritten ", key, " from", value, "into", canonical) if(canonical === value){
if(canonical === undefined && !unit.eraseInvalid) {
break; break;
} }
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
if (canonical === undefined && !unit.eraseInvalid) {
break;
}
feature.properties[key] = canonical; feature.properties[key] = canonical;
rewritten = true;
break; break;
} }
} }
if(rewritten){
State.state.allElements.getEventSourceById(feature.id).ping();
}
}) })
) )

View file

@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource";
* UIEventsource-wrapper around localStorage * UIEventsource-wrapper around localStorage
*/ */
export class LocalStorageSource { export class LocalStorageSource {
static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{
return LocalStorageSource.Get(key).map(
str => {
if(str === undefined){
return defaultValue
}
try{
return JSON.parse(str)
}catch{
return defaultValue
}
}, [],
value => JSON.stringify(value)
)
}
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
try { try {

View file

@ -7,4 +7,6 @@ export default interface BaseLayer {
max_zoom: number, max_zoom: number,
min_zoom: number; min_zoom: number;
feature: any, feature: any,
isBest?: boolean,
category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string
} }

View file

@ -2,7 +2,7 @@ import { Utils } from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.8.3f"; public static vNumber = "0.8.4-rc3";
// The user journey states thresholds when a new feature gets unlocked // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {

8
Models/TileRange.ts Normal file
View file

@ -0,0 +1,8 @@
export interface TileRange {
xstart: number,
ystart: number,
xend: number,
yend: number,
total: number,
zoomlevel: number
}

773
State.ts
View file

@ -1,13 +1,13 @@
import { Utils } from "./Utils"; import {Utils} from "./Utils";
import { ElementStorage } from "./Logic/ElementStorage"; import {ElementStorage} from "./Logic/ElementStorage";
import { Changes } from "./Logic/Osm/Changes"; import {Changes} from "./Logic/Osm/Changes";
import { OsmConnection } from "./Logic/Osm/OsmConnection"; import {OsmConnection} from "./Logic/Osm/OsmConnection";
import Locale from "./UI/i18n/Locale"; import Locale from "./UI/i18n/Locale";
import { UIEventSource } from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import { QueryParameters } from "./Logic/Web/QueryParameters"; import {QueryParameters} from "./Logic/Web/QueryParameters";
import LayoutConfig from "./Customizations/JSON/LayoutConfig"; import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import { MangroveIdentity } from "./Logic/Web/MangroveReviews"; import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
import InstalledThemes from "./Logic/Actors/InstalledThemes"; import InstalledThemes from "./Logic/Actors/InstalledThemes";
import BaseLayer from "./Models/BaseLayer"; import BaseLayer from "./Models/BaseLayer";
import Loc from "./Models/Loc"; import Loc from "./Models/Loc";
@ -17,410 +17,423 @@ import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
import LayerConfig from "./Customizations/JSON/LayerConfig"; import LayerConfig from "./Customizations/JSON/LayerConfig";
import TitleHandler from "./Logic/Actors/TitleHandler"; import TitleHandler from "./Logic/Actors/TitleHandler";
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
import { Relation } from "./Logic/Osm/ExtractRelations"; import {Relation} from "./Logic/Osm/ExtractRelations";
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
/** /**
* Contains the global state: a bunch of UI-event sources * Contains the global state: a bunch of UI-event sources
*/ */
export default class State { export default class State {
// The singleton of the global state // The singleton of the global state
public static state: State; public static state: State;
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined); public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined);
/** /**
The mapping from id -> UIEventSource<properties> The mapping from id -> UIEventSource<properties>
*/ */
public allElements: ElementStorage; public allElements: ElementStorage;
/** /**
THe change handler THe change handler
*/ */
public changes: Changes; public changes: Changes;
/** /**
The leaflet instance of the big basemap The leaflet instance of the big basemap
*/ */
public leafletMap = new UIEventSource<L.Map>(undefined); public leafletMap = new UIEventSource<L.Map>(undefined);
/** /**
* Background layer id * Background layer id
*/ */
public availableBackgroundLayers: UIEventSource<BaseLayer[]>; public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
/** /**
The user credentials The user credentials
*/ */
public osmConnection: OsmConnection; public osmConnection: OsmConnection;
public mangroveIdentity: MangroveIdentity; public mangroveIdentity: MangroveIdentity;
public favouriteLayers: UIEventSource<string[]>; public favouriteLayers: UIEventSource<string[]>;
public layerUpdater: OverpassFeatureSource; public layerUpdater: OverpassFeatureSource;
public osmApiFeatureSource: OsmApiFeatureSource; public osmApiFeatureSource: OsmApiFeatureSource;
public filteredLayers: UIEventSource< public filteredLayers: UIEventSource<{
{ readonly isDisplayed: UIEventSource<boolean>;
readonly isDisplayed: UIEventSource<boolean>; readonly layerDef: LayerConfig;
readonly layerDef: LayerConfig; }[]> = new UIEventSource<{
}[] readonly isDisplayed: UIEventSource<boolean>;
> = new UIEventSource< readonly layerDef: LayerConfig;
{ }[]>([]);
readonly isDisplayed: UIEventSource<boolean>;
readonly layerDef: LayerConfig;
}[]
>([]);
/** /**
The latest element that was selected The latest element that was selected
*/ */
public readonly selectedElement = new UIEventSource<any>( 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 featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>;
public readonly featureSwitchLayers: UIEventSource<boolean>;
public readonly featureSwitchAddNew: UIEventSource<boolean>;
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>;
public readonly featureSwitchIframe: UIEventSource<boolean>;
public readonly featureSwitchMoreQuests: UIEventSource<boolean>;
public readonly featureSwitchShareScreen: UIEventSource<boolean>;
public readonly featureSwitchGeolocation: UIEventSource<boolean>;
public readonly featureSwitchIsTesting: UIEventSource<boolean>;
public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
public readonly featureSwitchApiURL: UIEventSource<string>;
public readonly featureSwitchFilter: UIEventSource<boolean>;
/**
* The map location: currently centered lat, lon and zoom
*/
public readonly locationControl = new UIEventSource<Loc>(undefined);
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);
/**
* 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);
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
);
public FilterIsOpened: UIEventSource<boolean> =
QueryParameters.GetQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter 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) {
const self = this;
this.layoutToUse.setData(layoutToUse);
// -- 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"))
);
this.locationControl = new UIEventSource<Loc>({
zoom: Utils.asFloat(zoom.data),
lat: Utils.asFloat(lat.data),
lon: Utils.asFloat(lon.data),
}).addCallback((latlonz) => {
zoom.setData(latlonz.zoom);
lat.setData(latlonz.lat);
lon.setData(latlonz.lon);
});
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, undefined,
documentation "Selected element"
);
// 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]
);
}
// 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.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.featureSwitchApiURL = QueryParameters.GetQueryParameter(
"backend",
"osm",
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
);
}
{
// Some other feature switches
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",
layoutToUse?.defaultBackgroundId ?? "osm",
"The id of the background layer to start with"
);
}
if (Utils.runningFromConsole) {
return;
}
this.osmConnection = new OsmConnection(
this.featureSwitchIsTesting.data,
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(); * Keeps track of relations: which way is part of which other way?
this.osmApiFeatureSource = new OsmApiFeatureSource(); * Set by the overpass-updater; used in the metatagging
*/
public readonly knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships");
new PendingChangesUploader(this.changes, this.selectedElement); public readonly featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>;
public readonly featureSwitchLayers: UIEventSource<boolean>;
public readonly featureSwitchAddNew: UIEventSource<boolean>;
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>;
public readonly featureSwitchIframe: UIEventSource<boolean>;
public readonly featureSwitchMoreQuests: UIEventSource<boolean>;
public readonly featureSwitchShareScreen: UIEventSource<boolean>;
public readonly featureSwitchGeolocation: UIEventSource<boolean>;
public readonly featureSwitchIsTesting: UIEventSource<boolean>;
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>;
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
);
this.installedThemes = new InstalledThemes( public featurePipeline: FeaturePipeline;
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")) * The map location: currently centered lat, lon and zoom
.map( */
(str) => Utils.Dedup(str?.split(";")) ?? [], public readonly locationControl = new UIEventSource<Loc>(undefined);
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);
/**
* 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);
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
);
public FilterIsOpened: UIEventSource<boolean> =
QueryParameters.GetQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter 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)),
[], [],
(layers) => Utils.Dedup(layers)?.join(";") (n) => "" + n
);
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();
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 ("" + fl).substr(0, 8);
}
); );
}
constructor(layoutToUse: LayoutConfig) {
const self = this;
this.layoutToUse.setData(layoutToUse);
// -- 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"))
);
this.locationControl = new UIEventSource<Loc>({
zoom: Utils.asFloat(zoom.data),
lat: Utils.asFloat(lat.data),
lon: Utils.asFloat(lon.data),
}).addCallback((latlonz) => {
zoom.setData(latlonz.zoom);
lat.setData(latlonz.lat);
lon.setData(latlonz.lon);
});
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
);
// 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]
);
}
// 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.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.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"
);
if (customCssQP.data !== undefined && customCssQP.data !== "") {
Utils.LoadCustomCss(customCssQP.data);
}
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId ?? "osm",
"The id of the background layer to start with"
);
}
if (Utils.runningFromConsole) {
return;
}
this.osmConnection = new OsmConnection(
this.featureSwitchIsTesting.data,
this.featureSwitchFakeUser.data,
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();
this.osmApiFeatureSource = new OsmApiFeatureSource();
new PendingChangesUploader(this.changes, this.selectedElement);
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
);
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(";")
);
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();
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 ("" + fl).substr(0, 8);
}
);
}
} }

32
Svg.ts

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {Map} from "leaflet"; import {Map} from "leaflet";
import {Utils} from "../../Utils";
export default class Minimap extends BaseUIElement { export default class Minimap extends BaseUIElement {
@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement {
private readonly _location: UIEventSource<Loc>; private readonly _location: UIEventSource<Loc>;
private _isInited = false; private _isInited = false;
private _allowMoving: boolean; private _allowMoving: boolean;
private readonly _leafletoptions: any;
constructor(options?: { constructor(options?: {
background?: UIEventSource<BaseLayer>, background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>, location?: UIEventSource<Loc>,
allowMoving?: boolean allowMoving?: boolean,
leafletOptions?: any
} }
) { ) {
super() super()
@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement {
this._location = options?.location ?? new UIEventSource<Loc>(undefined) this._location = options?.location ?? new UIEventSource<Loc>(undefined)
this._id = "minimap" + Minimap._nextId; this._id = "minimap" + Minimap._nextId;
this._allowMoving = options.allowMoving ?? true; this._allowMoving = options.allowMoving ?? true;
this._leafletoptions = options.leafletOptions ?? {}
Minimap._nextId++ Minimap._nextId++
} }
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div") const div = document.createElement("div")
div.id = this._id; div.id = this._id;
@ -44,7 +48,6 @@ export default class Minimap extends BaseUIElement {
const self = this; const self = this;
// @ts-ignore // @ts-ignore
const resizeObserver = new ResizeObserver(_ => { const resizeObserver = new ResizeObserver(_ => {
console.log("Change in size detected!")
self.InitMap(); self.InitMap();
self.leafletMap?.data?.invalidateSize() self.leafletMap?.data?.invalidateSize()
}); });
@ -72,8 +75,8 @@ export default class Minimap extends BaseUIElement {
const location = this._location; const location = this._location;
let currentLayer = this._background.data.layer() let currentLayer = this._background.data.layer()
const map = L.map(this._id, { const options = {
center: [location.data?.lat ?? 0, location.data?.lon ?? 0], center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0],
zoom: location.data?.zoom ?? 2, zoom: location.data?.zoom ?? 2,
layers: [currentLayer], layers: [currentLayer],
zoomControl: false, zoomControl: false,
@ -82,8 +85,14 @@ export default class Minimap extends BaseUIElement {
scrollWheelZoom: this._allowMoving, scrollWheelZoom: this._allowMoving,
doubleClickZoom: this._allowMoving, doubleClickZoom: this._allowMoving,
keyboard: this._allowMoving, keyboard: this._allowMoving,
touchZoom: this._allowMoving touchZoom: this._allowMoving,
}); // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving,
fadeAnimation: this._allowMoving
}
Utils.Merge(this._leafletoptions, options)
const map = L.map(this._id, options);
map.setMaxBounds( map.setMaxBounds(
[[-100, -200], [100, 200]] [[-100, -200], [100, 200]]

View file

@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
export class Basemap { export class Basemap {
@ -35,9 +36,8 @@ export class Basemap {
); );
this.map.attributionControl.setPrefix( this.map.attributionControl.setPrefix(
"<span id='leaflet-attribution'></span> | <a href='https://osm.org'>OpenStreetMap</a>"); "<span id='leaflet-attribution'>A</span>");
extraAttribution.AttachTo('leaflet-attribution')
const self = this; const self = this;
currentLayer.addCallbackAndRun(layer => { currentLayer.addCallbackAndRun(layer => {
@ -77,6 +77,7 @@ export class Basemap {
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
}); });
extraAttribution.AttachTo('leaflet-attribution')
} }

View file

@ -0,0 +1,21 @@
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import State from "../../State";
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
import Combine from "../Base/Combine";
export class ExportDataButton extends Combine {
constructor() {
const t = Translations.t.general.download
const button = new SubtleButton(Svg.floppy_ui(), t.downloadGeojson.Clone().SetClass("font-bold"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline)
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), `MapComplete_${name}_export_${new Date().toISOString().substr(0,19)}.geojson`);
})
super([button, t.licenseInfo.Clone().SetClass("link-underline")])
}
}

View file

@ -10,6 +10,7 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import { Translation } from "../i18n/Translation"; import { Translation } from "../i18n/Translation";
import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Svg from "../../Svg";
/** /**
* Shows the filter * Shows the filter
@ -26,14 +27,63 @@ export default class FilterView extends ScrollableFullScreen {
} }
private static Generatecontent(): BaseUIElement { private static Generatecontent(): BaseUIElement {
let filterPanel: BaseUIElement = new FixedUiElement("more stuff"); let filterPanel: BaseUIElement = new FixedUiElement("");
if (State.state.filteredLayers.data.length > 1) { if (State.state.filteredLayers.data.length > 1) {
let layers = State.state.filteredLayers; let activeLayers = State.state.filteredLayers;
console.log(layers);
filterPanel = new Combine(["layerssss", "<br/>", filterPanel]);
}
return filterPanel; if (activeLayers === undefined) {
throw "ActiveLayers should be defined...";
}
const checkboxes: BaseUIElement[] = [];
for (const layer of activeLayers.data) {
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 (layer.layerDef.name === undefined) {
continue;
}
const style = "display:flex;align-items:center;color:#007759";
const name: Translation = Translations.WT(layer.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 layerChecked = new Combine([icon, styledNameChecked]).SetStyle(
style
);
const layerNotChecked = new Combine([
iconUnselected,
styledNameUnChecked,
]).SetStyle(style);
checkboxes.push(
new Toggle(layerChecked, layerNotChecked, layer.isDisplayed)
.ToggleOnClick()
.SetStyle("margin:0.3em;")
);
}
let combinedCheckboxes = new Combine(checkboxes);
combinedCheckboxes.SetStyle("display:flex;flex-direction:column;");
filterPanel = new Combine([combinedCheckboxes]);
return filterPanel;
}
} }
} }

View file

@ -2,11 +2,12 @@ import State from "../../State";
import BackgroundSelector from "./BackgroundSelector"; import BackgroundSelector from "./BackgroundSelector";
import LayerSelection from "./LayerSelection"; import LayerSelection from "./LayerSelection";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import {ExportDataButton} from "./ExportDataButton";
export default class LayerControlPanel extends ScrollableFullScreen { export default class LayerControlPanel extends ScrollableFullScreen {
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown); super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
} }
private static GenTitle():BaseUIElement { 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 { private static GeneratePanel(): BaseUIElement {
let layerControlPanel: BaseUIElement = new FixedUiElement(""); const elements: BaseUIElement[] = []
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
layerControlPanel = new BackgroundSelector(); const backgroundSelector = new BackgroundSelector();
layerControlPanel.SetStyle("margin:1em"); backgroundSelector.SetStyle("margin:1em");
layerControlPanel.onClick(() => { backgroundSelector.onClick(() => {
}); });
elements.push(backgroundSelector)
} }
if (State.state.filteredLayers.data.length > 1) { elements.push(new Toggle(
const layerSelection = new LayerSelection(State.state.filteredLayers); new LayerSelection(State.state.filteredLayers),
layerSelection.onClick(() => { undefined,
}); State.state.filteredLayers.map(layers => layers.length > 1)
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]); ))
}
return layerControlPanel; elements.push(new Toggle(
new ExportDataButton(),
undefined,
State.state.featureSwitchEnableExport
))
return new Combine(elements).SetClass("flex flex-col")
} }
} }

View file

@ -74,7 +74,6 @@ export default class LayerSelection extends Combine {
); );
} }
super(checkboxes) super(checkboxes)
this.SetStyle("display:flex;flex-direction:column;") this.SetStyle("display:flex;flex-direction:column;")

View file

@ -62,6 +62,10 @@ export default class MoreScreen extends Combine {
let officialThemes = AllKnownLayouts.layoutsList let officialThemes = AllKnownLayouts.layoutsList
let buttons = officialThemes.map((layout) => { let buttons = officialThemes.map((layout) => {
if(layout === undefined){
console.trace("Layout is undefined")
return undefined
}
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
if(layout.id === personal.id){ if(layout.id === personal.id){
return new VariableUiElement( return new VariableUiElement(

View file

@ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import UserDetails from "../../Logic/Osm/OsmConnection"; import UserDetails from "../../Logic/Osm/OsmConnection";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import LocationInput from "../Input/LocationInput";
import {InputElement} from "../Input/InputElement";
import Loc from "../../Models/Loc";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
/* /*
* The SimpleAddUI is a single panel, which can have multiple states: * The SimpleAddUI is a single panel, which can have multiple states:
@ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation";
* - A 'read your unread messages before adding a point' * - A 'read your unread messages before adding a point'
*/ */
/*private*/
interface PresetInfo { interface PresetInfo {
description: string | Translation, description: string | Translation,
name: string | BaseUIElement, name: string | BaseUIElement,
icon: BaseUIElement, icon: () => BaseUIElement,
tags: Tag[], tags: Tag[],
layerToAddTo: { layerToAddTo: {
layerDef: LayerConfig, layerDef: LayerConfig,
isDisplayed: UIEventSource<boolean> isDisplayed: UIEventSource<boolean>
},
preciseInput?: {
preferredBackground?: string
} }
} }
@ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle {
new SubtleButton(Svg.envelope_ui(), new SubtleButton(Svg.envelope_ui(),
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}) Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
]); ]);
const selectedPreset = new UIEventSource<PresetInfo>(undefined); const selectedPreset = new UIEventSource<PresetInfo>(undefined);
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
function createNewPoint(tags: any[]){ function createNewPoint(tags: any[], location: { lat: number, lon: number }) {
const loc = State.state.LastClickLocation.data; let feature = State.state.changes.createElement(tags, location.lat, location.lon);
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
State.state.selectedElement.setData(feature); State.state.selectedElement.setData(feature);
} }
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
const addUi = new VariableUiElement( const addUi = new VariableUiElement(
@ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle {
return presetsOverview return presetsOverview
} }
return SimpleAddUI.CreateConfirmButton(preset, return SimpleAddUI.CreateConfirmButton(preset,
tags => { (tags, location) => {
createNewPoint(tags) createNewPoint(tags, location)
selectedPreset.setData(undefined) selectedPreset.setData(undefined)
}, () => { }, () => {
selectedPreset.setData(undefined) selectedPreset.setData(undefined)
@ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle {
addUi, addUi,
State.state.layerUpdater.runningQuery State.state.layerUpdater.runningQuery
), ),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert") , Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
), ),
readYourMessages, readYourMessages,
@ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle {
} }
private static CreateConfirmButton(preset: PresetInfo, private static CreateConfirmButton(preset: PresetInfo,
confirm: (tags: any[]) => void, confirm: (tags: any[], location: { lat: number, lon: number }) => void,
cancel: () => void): BaseUIElement { cancel: () => void): BaseUIElement {
let location = State.state.LastClickLocation;
let preciseInput: InputElement<Loc> = undefined
if (preset.preciseInput !== undefined) {
const locationSrc = new UIEventSource({
lat: location.data.lat,
lon: location.data.lon,
zoom: 19
});
let backgroundLayer = undefined;
if(preset.preciseInput.preferredBackground){
backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation:locationSrc
})
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
}
const confirmButton = new SubtleButton(preset.icon,
let confirmButton: BaseUIElement = new SubtleButton(preset.icon(),
new Combine([ new Combine([
Translations.t.general.add.addNew.Subs({category: preset.name}), Translations.t.general.add.addNew.Subs({category: preset.name}),
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert") Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
).SetClass("font-bold break-words") ).SetClass("font-bold break-words")
.onClick(() => confirm(preset.tags)); .onClick(() => {
confirm(preset.tags, (preciseInput?.GetValue() ?? location).data);
});
if (preciseInput !== undefined) {
confirmButton = new Combine([preciseInput, confirmButton])
}
const openLayerControl =
const openLayerControl =
new SubtleButton( new SubtleButton(
Svg.layers_ui(), Svg.layers_ui(),
new Combine([ new Combine([
@ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle {
Translations.t.general.add.openLayerControl Translations.t.general.add.openLayerControl
]) ])
) )
.onClick(() => State.state.layerControlIsOpened.setData(true)) .onClick(() => State.state.layerControlIsOpened.setData(true))
const openLayerOrConfirm = new Toggle( const openLayerOrConfirm = new Toggle(
confirmButton, confirmButton,
openLayerControl, openLayerControl,
@ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle {
const cancelButton = new SubtleButton(Svg.close_ui(), const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel Translations.t.general.cancel
).onClick(cancel ) ).onClick(cancel)
return new Combine([ return new Combine([
Translations.t.general.add.confirmIntro.Subs({title: preset.name}), Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
State.state.osmConnection.userDetails.data.dryRun ? State.state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined , Translations.t.general.testing.Clone().SetClass("alert") : undefined,
openLayerOrConfirm, openLayerOrConfirm,
cancelButton, cancelButton,
preset.description, preset.description,
@ -180,11 +212,11 @@ export default class SimpleAddUI extends Toggle {
} }
private static CreatePresetSelectButton(preset: PresetInfo){ private static CreatePresetSelectButton(preset: PresetInfo) {
const tagInfo =SimpleAddUI.CreateTagInfoFor(preset, false); const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false);
return new SubtleButton( return new SubtleButton(
preset.icon, preset.icon(),
new Combine([ new Combine([
Translations.t.general.add.addNew.Subs({ Translations.t.general.add.addNew.Subs({
category: preset.name category: preset.name
@ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle {
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
) )
} }
/* /*
* Generates the list with all the buttons.*/ * Generates the list with all the buttons.*/
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement { private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
const allButtons = []; const allButtons = [];
for (const layer of State.state.filteredLayers.data) { for (const layer of State.state.filteredLayers.data) {
if(layer.isDisplayed.data === false && State.state.featureSwitchLayers){ if (layer.isDisplayed.data === false && State.state.featureSwitchLayers) {
continue; continue;
} }
const presets = layer.layerDef.presets; const presets = layer.layerDef.presets;
for (const preset of presets) { for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? []); 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"); .SetClass("w-12 h-12 block relative");
const presetInfo: PresetInfo = { const presetInfo: PresetInfo = {
tags: preset.tags, tags: preset.tags,
layerToAddTo: layer, layerToAddTo: layer,
name: preset.title, name: preset.title,
description: preset.description, description: preset.description,
icon: icon icon: icon,
preciseInput: preset.preciseInput
} }
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);

View file

@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> {
}) })
this.RegisterTriggers(element) this.RegisterTriggers(element)
element.style.overflow = "hidden"
return element; return element;
} }

View file

@ -0,0 +1,35 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import {Translation} from "../i18n/Translation";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
export default class InputElementWrapper<T> extends InputElement<T> {
public readonly IsSelected: UIEventSource<boolean>;
private readonly _inputElement: InputElement<T>;
private readonly _renderElement: BaseUIElement
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>) {
super()
this._inputElement = inputElement;
this.IsSelected = inputElement.IsSelected
const mapping = new Map<string, BaseUIElement>()
mapping.set(key, inputElement)
this._renderElement = new SubstitutedTranslation(translation, tags, mapping)
}
GetValue(): UIEventSource<T> {
return this._inputElement.GetValue();
}
IsValid(t: T): boolean {
return this._inputElement.IsValid(t);
}
protected InnerConstructElement(): HTMLElement {
return this._renderElement.ConstructElement();
}
}

185
UI/Input/LengthInput.ts Normal file
View file

@ -0,0 +1,185 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../../Logic/GeoOperations";
import DirectionInput from "./DirectionInput";
import {RadioButton} from "./RadioButton";
import {FixedInputElement} from "./FixedInputElement";
/**
* Selects a length after clicking on the minimap, in meters
*/
export default class LengthInput extends InputElement<string> {
private readonly _location: UIEventSource<Loc>;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly value: UIEventSource<string>;
private background;
constructor(mapBackground: UIEventSource<any>,
location: UIEventSource<Loc>,
value?: UIEventSource<string>) {
super();
this._location = location;
this.value = value ?? new UIEventSource<string>(undefined);
this.background = mapBackground;
this.SetClass("block")
}
GetValue(): UIEventSource<string> {
return this.value;
}
IsValid(str: string): boolean {
const t = Number(str)
return !isNaN(t) && t >= 0 && t <= 360;
}
protected InnerConstructElement(): HTMLElement {
const modeElement = new RadioButton([
new FixedInputElement("Measure", "measure"),
new FixedInputElement("Move", "move")
])
// @ts-ignore
let map = undefined
if (!Utils.runningFromConsole) {
map = DirectionInput.constructMinimap({
background: this.background,
allowMoving: false,
location: this._location,
leafletOptions: {
tap: true
}
})
}
const element = new Combine([
new Combine([Svg.length_crosshair_svg().SetStyle(
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`)
])
.SetClass("block length-crosshair-svg relative")
.SetStyle("z-index: 1000; visibility: hidden"),
map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"),
])
.SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden")
.ConstructElement()
this.RegisterTriggers(element, map?.leafletMap)
element.style.overflow = "hidden"
element.style.display = "block"
return element
}
private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource<L.Map>) {
let firstClickXY: [number, number] = undefined
let lastClickXY: [number, number] = undefined
const self = this;
function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) {
if (x === undefined || y === undefined) {
// Touch end
firstClickXY = undefined;
lastClickXY = undefined;
return;
}
const rect = htmlElement.getBoundingClientRect();
// From the central part of location
const dx = x - rect.left;
const dy = y - rect.top;
if (isDown) {
if (lastClickXY === undefined && firstClickXY === undefined) {
firstClickXY = [dx, dy];
} else if (firstClickXY !== undefined && lastClickXY === undefined) {
lastClickXY = [dx, dy]
} else if (firstClickXY !== undefined && lastClickXY !== undefined) {
// we measure again
firstClickXY = [dx, dy]
lastClickXY = undefined;
}
}
if (isUp) {
const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0]))
if (distance > 15) {
lastClickXY = [dx, dy]
}
} else if (lastClickXY !== undefined) {
return;
}
const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement
const measurementCrosshairInner: HTMLElement = <HTMLElement>measurementCrosshair.firstChild
if (firstClickXY === undefined) {
measurementCrosshair.style.visibility = "hidden"
} else {
measurementCrosshair.style.visibility = "unset"
measurementCrosshair.style.left = firstClickXY[0] + "px";
measurementCrosshair.style.top = firstClickXY[1] + "px"
const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI;
const angleGeo = (angle + 270) % 360
measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`;
const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0]))
measurementCrosshairInner.style.width = (distance * 2) + "px"
measurementCrosshairInner.style.marginLeft = -distance + "px"
measurementCrosshairInner.style.marginTop = -distance + "px"
const leaflet = leafletMap?.data
if (leaflet) {
const first = leaflet.layerPointToLatLng(firstClickXY)
const last = leaflet.layerPointToLatLng([dx, dy])
const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100
self.value.setData("" + geoDist)
}
}
}
htmlElement.ontouchstart = (ev: TouchEvent) => {
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true);
ev.preventDefault();
}
htmlElement.ontouchmove = (ev: TouchEvent) => {
onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false);
ev.preventDefault();
}
htmlElement.ontouchend = (ev: TouchEvent) => {
onPosChange(undefined, undefined, false, true);
ev.preventDefault();
}
htmlElement.onmousedown = (ev: MouseEvent) => {
onPosChange(ev.clientX, ev.clientY, true);
ev.preventDefault();
}
htmlElement.onmouseup = (ev) => {
onPosChange(ev.clientX, ev.clientY, false, true);
ev.preventDefault();
}
htmlElement.onmousemove = (ev: MouseEvent) => {
onPosChange(ev.clientX, ev.clientY, false);
ev.preventDefault();
}
}
}

76
UI/Input/LocationInput.ts Normal file
View file

@ -0,0 +1,76 @@
import {InputElement} from "./InputElement";
import Loc from "../../Models/Loc";
import {UIEventSource} from "../../Logic/UIEventSource";
import Minimap from "../Base/Minimap";
import BaseLayer from "../../Models/BaseLayer";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import State from "../../State";
export default class LocationInput extends InputElement<Loc> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _centerLocation: UIEventSource<Loc>;
private readonly mapBackground : UIEventSource<BaseLayer>;
constructor(options?: {
mapBackground?: UIEventSource<BaseLayer>,
centerLocation?: UIEventSource<Loc>,
}) {
super();
options = options ?? {}
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
this._centerLocation = options.centerLocation;
this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
this.SetClass("block h-full")
}
GetValue(): UIEventSource<Loc> {
return this._centerLocation;
}
IsValid(t: Loc): boolean {
return t !== undefined;
}
protected InnerConstructElement(): HTMLElement {
const map = new Minimap(
{
location: this._centerLocation,
background: this.mapBackground
}
)
map.leafletMap.addCallbackAndRunD(leaflet => {
console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15))
leaflet.setMaxBounds(
leaflet.getBounds().pad(0.15)
)
})
this.mapBackground.map(layer => {
const leaflet = map.leafletMap.data
if (leaflet === undefined || layer === undefined) {
return;
}
leaflet.setMaxZoom(layer.max_zoom)
leaflet.setMinZoom(layer.max_zoom - 3)
leaflet.setZoom(layer.max_zoom - 1)
}, [map.leafletMap])
return new Combine([
new Combine([
Svg.crosshair_empty_ui()
.SetClass("block relative")
.SetStyle("left: -1.25rem; top: -1.25rem; width: 2.5rem; height: 2.5rem")
]).SetClass("block w-0 h-0 z-10 relative")
.SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"),
map
.SetClass("z-0 relative block w-full h-full bg-gray-100")
]).ConstructElement();
}
}

View file

@ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> {
const block = document.createElement("div") const block = document.createElement("div")
block.appendChild(input) block.appendChild(input)
block.appendChild(label) block.appendChild(label)
block.classList.add("flex","w-full","border", "rounded-full", "border-gray-400","m-1") block.classList.add("flex","w-full","border", "rounded-3xl", "border-gray-400","m-1")
wrappers.push(block) wrappers.push(block)
form.appendChild(block) form.appendChild(block)

View file

@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
this.SetClass("form-text-field") this.SetClass("form-text-field")
let inputEl: HTMLElement let inputEl: HTMLElement
if (options.htmlType === "area") { if (options.htmlType === "area") {
this.SetClass("w-full box-border max-w-full")
const el = document.createElement("textarea") const el = document.createElement("textarea")
el.placeholder = placeholder el.placeholder = placeholder
el.rows = options.textAreaRows el.rows = options.textAreaRows
el.cols = 50 el.cols = 50
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
inputEl = el; inputEl = el;
} else { } else {
const el = document.createElement("input") const el = document.createElement("input")

View file

@ -13,6 +13,8 @@ import {Utils} from "../../Utils";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {Unit} from "../../Customizations/JSON/Denomination"; import {Unit} from "../../Customizations/JSON/Denomination";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import LengthInput from "./LengthInput";
import {GeoOperations} from "../../Logic/GeoOperations";
interface TextFieldDef { interface TextFieldDef {
name: string, name: string,
@ -21,14 +23,16 @@ interface TextFieldDef {
reformat?: ((s: string, country?: () => string) => string), reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?: { inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number], location: [number, number],
mapBackgroundLayer?: UIEventSource<any> mapBackgroundLayer?: UIEventSource<any>,
args: (string | number | boolean)[]
feature?: any
}) => InputElement<string>, }) => InputElement<string>,
inputmode?: string inputmode?: string
} }
export default class ValidatedTextField { export default class ValidatedTextField {
public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any
public static tpList: TextFieldDef[] = [ public static tpList: TextFieldDef[] = [
ValidatedTextField.tp( ValidatedTextField.tp(
@ -63,6 +67,83 @@ export default class ValidatedTextField {
return [year, month, day].join('-'); return [year, month, day].join('-');
}, },
(value) => new SimpleDatePicker(value)), (value) => new SimpleDatePicker(value)),
ValidatedTextField.tp(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
}, str => str,
(value, options) => {
const args = options.args ?? []
let zoom = 19
if (args[0]) {
zoom = Number(args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
}
}
const location = new UIEventSource<Loc>({
lat: options.location[0],
lon: options.location[1],
zoom: zoom
})
if (args[1]) {
// We have a prefered map!
options.mapBackgroundLayer = ValidatedTextField.bestLayerAt(
location, new UIEventSource<string[]>(args[1].split(","))
)
}
const di = new DirectionInput(options.mapBackgroundLayer, location, value)
di.SetStyle("height: 20rem;");
return di;
},
"numeric"
),
ValidatedTextField.tp(
"length",
"A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma seperated) ], e.g. `[\"21\", \"map,photo\"]",
(str) => {
const t = Number(str)
return !isNaN(t)
},
str => str,
(value, options) => {
const args = options.args ?? []
let zoom = 19
if (args[0]) {
zoom = Number(args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
}
}
// Bit of a hack: we project the centerpoint to the closes point on the road - if available
if(options.feature){
const lonlat: [number, number] = [...options.location]
lonlat.reverse()
options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates
options.location.reverse()
}
options.feature
const location = new UIEventSource<Loc>({
lat: options.location[0],
lon: options.location[1],
zoom: zoom
})
if (args[1]) {
// We have a prefered map!
options.mapBackgroundLayer = ValidatedTextField.bestLayerAt(
location, new UIEventSource<string[]>(args[1].split(","))
)
}
const li = new LengthInput(options.mapBackgroundLayer, location, value)
li.SetStyle("height: 20rem;")
return li;
}
),
ValidatedTextField.tp( ValidatedTextField.tp(
"wikidata", "wikidata",
"A wikidata identifier, e.g. Q42", "A wikidata identifier, e.g. Q42",
@ -113,22 +194,6 @@ export default class ValidatedTextField {
undefined, undefined,
undefined, undefined,
"numeric"), "numeric"),
ValidatedTextField.tp(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)",
(str) => {
str = "" + str;
return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360
}, str => str,
(value, options) => {
return new DirectionInput(options.mapBackgroundLayer , new UIEventSource<Loc>({
lat: options.location[0],
lon: options.location[1],
zoom: 19
}),value);
},
"numeric"
),
ValidatedTextField.tp( ValidatedTextField.tp(
"float", "float",
"A decimal", "A decimal",
@ -222,6 +287,7 @@ export default class ValidatedTextField {
* {string (typename) --> TextFieldDef} * {string (typename) --> TextFieldDef}
*/ */
public static AllTypes = ValidatedTextField.allTypesDict(); public static AllTypes = ValidatedTextField.allTypesDict();
public static InputForType(type: string, options?: { public static InputForType(type: string, options?: {
placeholder?: string | BaseUIElement, placeholder?: string | BaseUIElement,
value?: UIEventSource<string>, value?: UIEventSource<string>,
@ -233,7 +299,9 @@ export default class ValidatedTextField {
country?: () => string, country?: () => string,
location?: [number /*lat*/, number /*lon*/], location?: [number /*lat*/, number /*lon*/],
mapBackgroundLayer?: UIEventSource<any>, mapBackgroundLayer?: UIEventSource<any>,
unit?: Unit unit?: Unit,
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
feature?: any
}): InputElement<string> { }): InputElement<string> {
options = options ?? {}; options = options ?? {};
options.placeholder = options.placeholder ?? type; options.placeholder = options.placeholder ?? type;
@ -247,7 +315,7 @@ export default class ValidatedTextField {
if (str === undefined) { if (str === undefined) {
return false; return false;
} }
if(options.unit) { if (options.unit) {
str = options.unit.stripUnitParts(str) str = options.unit.stripUnitParts(str)
} }
return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country);
@ -268,7 +336,7 @@ export default class ValidatedTextField {
}) })
} }
if(options.unit) { if (options.unit) {
// We need to apply a unit. // We need to apply a unit.
// This implies: // This implies:
// We have to create a dropdown with applicable denominations, and fuse those values // We have to create a dropdown with applicable denominations, and fuse those values
@ -282,23 +350,22 @@ export default class ValidatedTextField {
}) })
) )
unitDropDown.GetValue().setData(unit.defaultDenom) unitDropDown.GetValue().setData(unit.defaultDenom)
unitDropDown.SetStyle("width: min-content") unitDropDown.SetClass("w-min")
input = new CombinedInputElement( input = new CombinedInputElement(
input, input,
unitDropDown, unitDropDown,
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM // combine the value from the textfield and the dropdown into the resulting value that should go into OSM
(text, denom) => denom?.canonicalValue(text, true) ?? undefined, (text, denom) => denom?.canonicalValue(text, true) ?? undefined,
(valueWithDenom: string) => { (valueWithDenom: string) => {
// Take the value from OSM and feed it into the textfield and the dropdown // Take the value from OSM and feed it into the textfield and the dropdown
const withDenom = unit.findDenomination(valueWithDenom); const withDenom = unit.findDenomination(valueWithDenom);
if(withDenom === undefined) if (withDenom === undefined) {
{
// Not a valid value at all - we give it undefined and leave the details up to the other elements // Not a valid value at all - we give it undefined and leave the details up to the other elements
return [undefined, undefined] return [undefined, undefined]
} }
const [strippedText, denom] = withDenom const [strippedText, denom] = withDenom
if(strippedText === undefined){ if (strippedText === undefined) {
return [undefined, undefined] return [undefined, undefined]
} }
return [strippedText, denom] return [strippedText, denom]
@ -306,18 +373,20 @@ export default class ValidatedTextField {
).SetClass("flex") ).SetClass("flex")
} }
if (tp.inputHelper) { if (tp.inputHelper) {
const helper = tp.inputHelper(input.GetValue(), { const helper = tp.inputHelper(input.GetValue(), {
location: options.location, location: options.location,
mapBackgroundLayer: options.mapBackgroundLayer mapBackgroundLayer: options.mapBackgroundLayer,
args: options.args,
feature: options.feature
}) })
input = new CombinedInputElement(input, helper, input = new CombinedInputElement(input, helper,
(a, _) => a, // We can ignore b, as they are linked earlier (a, _) => a, // We can ignore b, as they are linked earlier
a => [a, a] a => [a, a]
); );
} }
return input; return input;
} }
public static HelpText(): string { public static HelpText(): string {
const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n")
return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations
@ -329,7 +398,9 @@ export default class ValidatedTextField {
reformat?: ((s: string, country?: () => string) => string), reformat?: ((s: string, country?: () => string) => string),
inputHelper?: (value: UIEventSource<string>, options?: { inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number], location: [number, number],
mapBackgroundLayer: UIEventSource<any> mapBackgroundLayer: UIEventSource<any>,
args: string[],
feature: any
}) => InputElement<string>, }) => InputElement<string>,
inputmode?: string): TextFieldDef { inputmode?: string): TextFieldDef {

View file

@ -36,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
const titleIcons = new Combine( const titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
"block w-8 h-8 align-baseline box-content sm:p-0.5") "block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem;")
)) ))
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")

View file

@ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement {
throw "Trying to generate a tagRenderingAnswer without configuration..." throw "Trying to generate a tagRenderingAnswer without configuration..."
} }
super(tagsSource.map(tags => { super(tagsSource.map(tags => {
if(tags === undefined){ if (tags === undefined) {
return undefined; return undefined;
} }
if(configuration.condition){ if (configuration.condition) {
if(!configuration.condition.matchesProperties(tags)){ if (!configuration.condition.matchesProperties(tags)) {
return undefined; return undefined;
} }
} }
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
if(trs.length === 0){
return undefined;
}
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
if(valuesToRender.length === 1){
return valuesToRender[0];
}else if(valuesToRender.length > 1){
return new List(valuesToRender)
}
return undefined;
}).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") const trs = Utils.NoNull(configuration.GetRenderValues(tags));
if (trs.length === 0) {
return undefined;
}
const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource))
if (valuesToRender.length === 1) {
return valuesToRender[0];
} else if (valuesToRender.length > 1) {
return new List(valuesToRender)
}
return undefined;
}).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
this.SetClass("flex items-center flex-row text-lg link-underline")
this.SetStyle("word-wrap: anywhere;"); this.SetStyle("word-wrap: anywhere;");
} }

View file

@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {DropDown} from "../Input/DropDown"; import {DropDown} from "../Input/DropDown";
import {Unit} from "../../Customizations/JSON/Denomination"; import {Unit} from "../../Customizations/JSON/Denomination";
import InputElementWrapper from "../Input/InputElementWrapper";
/** /**
* Shows the question element. * Shows the question element.
@ -128,7 +129,7 @@ export default class TagRenderingQuestion extends Combine {
} }
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot)) return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
} }
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data); const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource);
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
if (mappings.length < 8 || configuration.multiAnswer || hasImages) { if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
(t0, t1) => t1.isEquivalent(t0)); (t0, t1) => t1.isEquivalent(t0));
} }
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> { private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> {
const freeform = configuration.freeform; const freeform = configuration.freeform;
if (freeform === undefined) { if (freeform === undefined) {
return undefined; return undefined;
@ -328,20 +329,34 @@ export default class TagRenderingQuestion extends Combine {
return undefined; return undefined;
} }
let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, { const tagsData = tags.data;
const feature = State.state.allElements.ContainingFeatures.get(tagsData.id)
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
isValid: (str) => (str.length <= 255), isValid: (str) => (str.length <= 255),
country: () => tagsData._country, country: () => tagsData._country,
location: [tagsData._lat, tagsData._lon], location: [tagsData._lat, tagsData._lon],
mapBackgroundLayer: State.state.backgroundLayer, mapBackgroundLayer: State.state.backgroundLayer,
unit: applicableUnit unit: applicableUnit,
args: configuration.freeform.helperArgs,
feature: feature
}); });
input.GetValue().setData(tagsData[configuration.freeform.key]); input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
return new InputElementMap( let inputTagsFilter : InputElement<TagsFilter> = new InputElementMap(
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
pickString, toString pickString, toString
); );
if(freeform.inline){
inputTagsFilter.SetClass("w-16-imp")
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
inputTagsFilter.SetClass("block")
}
return inputTagsFilter;
} }

View file

@ -80,9 +80,7 @@ export default class ShowDataLayer {
if (zoomToFeatures) { if (zoomToFeatures) {
try { try {
mp.fitBounds(geoLayer.getBounds(), {animate: false})
mp.fitBounds(geoLayer.getBounds())
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -148,7 +146,9 @@ export default class ShowDataLayer {
const popup = L.popup({ const popup = L.popup({
autoPan: true, autoPan: true,
closeOnEscapeKey: true, closeOnEscapeKey: true,
closeButton: false closeButton: false,
autoPanPaddingTopLeft: [15,15],
}, leafletLayer); }, leafletLayer);
leafletLayer.bindPopup(popup); leafletLayer.bindPopup(popup);

View file

@ -39,7 +39,8 @@ export default class SpecialVisualizations {
static constructMiniMap: (options?: { static constructMiniMap: (options?: {
background?: UIEventSource<BaseLayer>, background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>, location?: UIEventSource<Loc>,
allowMoving?: boolean allowMoving?: boolean,
leafletOptions?: any
}) => BaseUIElement; }) => BaseUIElement;
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any; static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
public static specialVisualizations: SpecialVisualization[] = public static specialVisualizations: SpecialVisualization[] =
@ -369,7 +370,6 @@ export default class SpecialVisualizations {
if (unit === undefined) { if (unit === undefined) {
return value; return value;
} }
return unit.asHumanLongValue(value); return unit.asHumanLongValue(value);
}, },
@ -379,6 +379,7 @@ export default class SpecialVisualizations {
} }
] ]
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
private static GenHelpMessage() { private static GenHelpMessage() {

View file

@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {VariableUiElement} from "./Base/VariableUIElement"; import {VariableUiElement} from "./Base/VariableUIElement";
import Combine from "./Base/Combine"; import Combine from "./Base/Combine";
import BaseUIElement from "./BaseUIElement";
export class SubstitutedTranslation extends VariableUiElement { export class SubstitutedTranslation extends VariableUiElement {
public constructor( public constructor(
translation: Translation, translation: Translation,
tagsSource: UIEventSource<any>) { tagsSource: UIEventSource<any>,
mapping: Map<string, BaseUIElement> = undefined) {
const extraMappings: SpecialVisualization[] = [];
mapping?.forEach((value, key) => {
console.log("KV:", key, value)
extraMappings.push(
{
funcName: key,
constr: (() => {
return value
}),
docs: "Dynamically injected input element",
args: [],
example: ""
}
)
})
super( super(
Locale.language.map(language => { Locale.language.map(language => {
const txt = translation.textFor(language) let txt = translation.textFor(language);
if (txt === undefined) { if (txt === undefined) {
return undefined return undefined
} }
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map( mapping?.forEach((_, key) => {
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
})
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map(
proto => { proto => {
if (proto.fixed !== undefined) { if (proto.fixed !== undefined) {
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
}) })
) )
this.SetClass("w-full") this.SetClass("w-full")
} }
public static ExtractSpecialComponents(template: string): { public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
fixed?: string, special?: { fixed?: string,
special?: {
func: SpecialVisualization, func: SpecialVisualization,
args: string[], args: string[],
style: string style: string
} }
}[] { }[] {
for (const knownSpecial of SpecialVisualizations.specialVisualizations) { 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' // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
if (matched != null) { if (matched != null) {
// We found a special component that should be brought to live // We found a special component that should be brought to live
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]); const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings);
const argument = matched[2].trim(); const argument = matched[2].trim();
const style = matched[3]?.substring(1) ?? "" const style = matched[3]?.substring(1) ?? ""
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]); const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
if (argument.length > 0) { if (argument.length > 0) {
const realArgs = argument.split(",").map(str => str.trim()); const realArgs = argument.split(",").map(str => str.trim());
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
} }
let element; let element;
element = {special:{ element = {
args: args, special: {
style: style, args: args,
func: knownSpecial style: style,
}} func: knownSpecial
}
}
return [...partBefore, element, ...partAfter] return [...partBefore, element, ...partAfter]
} }
} }

View file

@ -1,4 +1,5 @@
import * as colors from "./assets/colors.json" import * as colors from "./assets/colors.json"
import {TileRange} from "./Models/TileRange";
export class Utils { export class Utils {
@ -134,7 +135,7 @@ export class Utils {
} }
return newArr; return newArr;
} }
public static MergeTags(a: any, b: any) { public static MergeTags(a: any, b: any) {
const t = {}; const t = {};
for (const k in a) { for (const k in a) {
@ -358,9 +359,12 @@ export class Utils {
* @param contents * @param contents
* @param fileName * @param fileName
*/ */
public static offerContentsAsDownloadableFile(contents: string, fileName: string = "download.txt") { public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt") {
const element = document.createElement("a"); const element = document.createElement("a");
const file = new Blob([contents], {type: 'text/plain'}); let file;
if(typeof(contents) === "string"){
file = new Blob([contents], {type: 'text/plain'});
}else {file = contents;}
element.href = URL.createObjectURL(file); element.href = URL.createObjectURL(file);
element.download = fileName; element.download = fileName;
document.body.appendChild(element); // Required for this to work in FireFox document.body.appendChild(element); // Required for this to work in FireFox
@ -447,14 +451,12 @@ export class Utils {
b: parseInt(hex.substr(5, 2), 16), b: parseInt(hex.substr(5, 2), 16),
} }
} }
public static setDefaults(options, defaults){
for (let key in defaults){
if (!(key in options)) options[key] = defaults[key];
}
return options;
}
} }
export interface TileRange {
xstart: number,
ystart: number,
xend: number,
yend: number,
total: number,
zoomlevel: number
}

View file

@ -1,7 +1,7 @@
{ {
"id": "parking", "id": "parking",
"name": { "name": {
"nl": "parking" "nl": "Parking"
}, },
"minzoom": 12, "minzoom": 12,
"source": { "source": {
@ -25,13 +25,13 @@
{ {
"if": "amenity=parking", "if": "amenity=parking",
"then": { "then": {
"nl": "{name:nl}" "nl": "Auto Parking"
} }
}, },
{ {
"if": "amenity=motorcycle_parking", "if": "amenity=motorcycle_parking",
"then": { "then": {
"nl": "{name}" "nl": "Motorfiets Parking"
} }
}, },
{ {

View file

@ -73,7 +73,10 @@
}, },
"tags": [ "tags": [
"amenity=public_bookcase" "amenity=public_bookcase"
] ],
"preciseInput": {
"preferredBackground": "photo"
}
} }
], ],
"tagRenderings": [ "tagRenderings": [
@ -139,7 +142,8 @@
}, },
"freeform": { "freeform": {
"key": "capacity", "key": "capacity",
"type": "nat" "type": "nat",
"inline": true
} }
}, },
{ {

View file

@ -1,7 +1,7 @@
{ {
"id": "watermill", "id": "watermill",
"name": { "name": {
"nl": "watermolens" "nl": "Watermolens"
}, },
"minzoom": 12, "minzoom": 12,
"source": { "source": {

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="16" height="16" rx="3" stroke="#007759" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 187 B

View file

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="16" height="16" rx="3" fill="#007759" stroke="#007759" stroke-width="2"/>
<path d="M3.5 8L8 13L14 5" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1,83 @@
<?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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="crosshair-empty.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568542"
inkscape:cx="22.669779"
inkscape:cy="52.573519"
inkscape:document-units="px"
inkscape:current-layer="g848"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="999"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<g
id="g848">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#5555ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 13.162109,273.57617 c -5.6145729,0 -10.1933596,4.58074 -10.193359,10.19531 -6e-7,5.61458 4.5787861,10.19336 10.193359,10.19336 5.614574,0 10.195313,-4.57878 10.195313,-10.19336 0,-5.61457 -4.580739,-10.19531 -10.195313,-10.19531 z m 0,2.64649 c 4.184659,0 7.548829,3.36417 7.548829,7.54882 0,4.18466 -3.36417,7.54883 -7.548829,7.54883 -4.1846584,0 -7.546875,-3.36417 -7.5468746,-7.54883 -4e-7,-4.18465 3.3622162,-7.54882 7.5468746,-7.54882 z"
id="path815"
inkscape:connector-curvature="0" />
<path
id="path839"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0055ec;fill-opacity:0.98823529;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.26458333;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 13.212891,286.88672 a 1.0487243,1.0487243 0 0 0 -1.033203,1.06445 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.06445 z m 0,-16.36914 a 1.0487243,1.0487243 0 0 0 -1.033203,1.0625 v 7.94922 a 1.048828,1.048828 0 1 0 2.097656,0 v -7.94922 a 1.0487243,1.0487243 0 0 0 -1.064453,-1.0625 z m 4.246093,12.20508 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.949219 a 1.048825,1.048825 0 1 0 0,-2.09765 z m -16.4179684,0 a 1.048825,1.048825 0 1 0 0,2.09765 h 7.9492188 a 1.048825,1.048825 0 1 0 0,-2.09765 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,106 @@
<?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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="crosshair-locked.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568542"
inkscape:cx="27.044982"
inkscape:cy="77.667126"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="999"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:snap-global="false">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<g
id="g827">
<circle
r="8.8715391"
cy="283.77081"
cx="13.16302"
id="path815"
style="fill:none;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817"
d="M 3.2841366,283.77082 H 1.0418969"
style="fill:none;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3"
d="M 25.405696,283.77082 H 23.286471"
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3-6"
d="m 13.229167,295.9489 v -2.11763"
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3-6-7"
d="m 13.229167,275.05759 v -3.44507"
style="fill:none;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
</g>
<path
style="fill:#5555ec;fill-opacity:0.98823529;stroke-width:0.6151033"
inkscape:connector-curvature="0"
d="m 16.850267,281.91543 h -0.65616 v -1.85094 c 0,0 0,-3.08489 -3.066169,-3.08489 -3.066169,0 -3.066169,3.08489 -3.066169,3.08489 v 1.85094 H 9.4056091 a 1.1835412,1.1907685 0 0 0 -1.1835412,1.19077 v 5.02838 a 1.1835412,1.1907685 0 0 0 1.1835412,1.1846 h 7.4446579 a 1.1835412,1.1907685 0 0 0 1.183541,-1.19078 v -5.0222 a 1.1835412,1.1907685 0 0 0 -1.183541,-1.19077 z m -3.722329,4.93583 a 1.2264675,1.233957 0 1 1 1.226468,-1.23395 1.2264675,1.233957 0 0 1 -1.226468,1.23395 z m 1.839702,-4.93583 h -3.679403 v -1.54245 c 0,-0.92546 0,-2.15942 1.839701,-2.15942 1.839702,0 1.839702,1.23396 1.839702,2.15942 z"
id="path822" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

3
assets/svg/download.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.08 8.86C8.13 8.53 8.24 8.24 8.38 7.99C8.52 7.74 8.72 7.53 8.97 7.37C9.21 7.22 9.51 7.15 9.88 7.14C10.11 7.15 10.32 7.19 10.51 7.27C10.71 7.36 10.89 7.48 11.03 7.63C11.17 7.78 11.28 7.96 11.37 8.16C11.46 8.36 11.5 8.58 11.51 8.8H13.3C13.28 8.33 13.19 7.9 13.02 7.51C12.85 7.12 12.62 6.78 12.32 6.5C12.02 6.22 11.66 6 11.24 5.84C10.82 5.68 10.36 5.61 9.85 5.61C9.2 5.61 8.63 5.72 8.15 5.95C7.67 6.18 7.27 6.48 6.95 6.87C6.63 7.26 6.39 7.71 6.24 8.23C6.09 8.75 6 9.29 6 9.87V10.14C6 10.72 6.08 11.26 6.23 11.78C6.38 12.3 6.62 12.75 6.94 13.13C7.26 13.51 7.66 13.82 8.14 14.04C8.62 14.26 9.19 14.38 9.84 14.38C10.31 14.38 10.75 14.3 11.16 14.15C11.57 14 11.93 13.79 12.24 13.52C12.55 13.25 12.8 12.94 12.98 12.58C13.16 12.22 13.27 11.84 13.28 11.43H11.49C11.48 11.64 11.43 11.83 11.34 12.01C11.25 12.19 11.13 12.34 10.98 12.47C10.83 12.6 10.66 12.7 10.46 12.77C10.27 12.84 10.07 12.86 9.86 12.87C9.5 12.86 9.2 12.79 8.97 12.64C8.72 12.48 8.52 12.27 8.38 12.02C8.24 11.77 8.13 11.47 8.08 11.14C8.03 10.81 8 10.47 8 10.14V9.87C8 9.52 8.03 9.19 8.08 8.86V8.86ZM10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
version="1.0"
width="859.53607pt"
height="858.4754pt"
viewBox="0 0 859.53607 858.4754"
preserveAspectRatio="xMidYMid meet"
id="svg14"
sodipodi:docname="length-crosshair.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<defs
id="defs18" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="999"
id="namedview16"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.5"
inkscape:cx="307.56567"
inkscape:cy="-35.669379"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg14"
inkscape:snap-smooth-nodes="true" />
<metadata
id="metadata2">
Created by potrace 1.15, written by Peter Selinger 2001-2017
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:71.99999853,71.99999853;stroke-dashoffset:0;stroke-opacity:1"
id="path816"
transform="rotate(-89.47199)"
sodipodi:type="arc"
sodipodi:cx="-425.24921"
sodipodi:cy="433.71375"
sodipodi:rx="428.34982"
sodipodi:ry="427.81949"
sodipodi:start="0"
sodipodi:end="4.7117019"
sodipodi:open="true"
d="M 3.1006165,433.71375 A 428.34982,427.81949 0 0 1 -425.1511,861.53322 428.34982,427.81949 0 0 1 -853.59898,433.90971 428.34982,427.81949 0 0 1 -425.54352,5.8943576" />
<path
style="fill:none;stroke:#000000;stroke-width:4.49999991;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 429.76804,430.08754 0,-429.19968"
id="path820"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0"
d="m 857.58749,429.23771 -855.6389371,0 v 0"
id="path822"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path814"
d="M 429.76804,857.30628 V 428.78674"
style="fill:none;stroke:#000000;stroke-width:1.49999997;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:35.99999926,35.99999926;stroke-dashoffset:0" />
<path
inkscape:connector-curvature="0"
id="path826"
d="M 857.32232,1.0332137 H 1.6833879 v 0"
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.99999963, 17.99999963;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path828"
d="M 857.58749,858.2377 H 1.9485529 v 0"
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.99999982, 8.99999982;stroke-dashoffset:0;stroke-opacity:1" />
<path
cx="-429.2377"
cy="429.76804"
rx="428.34982"
ry="427.81949"
transform="rotate(-90)"
id="path825"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:11.99999975;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
d="M -5.3639221,-424.71887 A 428.34982,427.81949 0 0 1 -429.83855,3.0831087 428.34982,427.81949 0 0 1 -861.99345,-416.97839"
sodipodi:open="true"
sodipodi:end="3.1234988"
sodipodi:start="0"
sodipodi:ry="427.81949"
sodipodi:rx="428.34982"
sodipodi:cy="-424.71887"
sodipodi:cx="-433.71375"
sodipodi:type="arc"
transform="rotate(-179.47199)"
id="path827"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -49,6 +49,14 @@
"license": "CC0", "license": "CC0",
"sources": [] "sources": []
}, },
{
"authors": [
"Hannah"
],
"path": "download.svg",
"license": "CC0",
"sources": []
},
{ {
"authors": [], "authors": [],
"path": "ampersand.svg", "path": "ampersand.svg",
@ -614,5 +622,635 @@
"path": "filter.svg", "path": "filter.svg",
"license": "CC0", "license": "CC0",
"sources": [] "sources": []
},
{
"authors": [
"Hannah Declerck"
],
"path": "checkbox-empty.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Hannah Declerck"
],
"path": "checkbox-filled.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Hannah Declerck"
],
"path": "arrow-left-thin.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "direction_masked.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "direction_outline.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "direction_stroke.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "SocialImageForeground.svg",
"license": "CC-BY-SA",
"sources": [
"https://mapcomplete.osm.be"
]
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "add.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "addSmall.svg",
"license": "CC0",
"sources": []
},
{
"authors": [],
"path": "ampersand.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "arrow-left-smooth.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "arrow-right-smooth.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "back.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Github"
],
"path": "bug.svg",
"license": "MIT",
"sources": [
"https://commons.wikimedia.org/wiki/File:Octicons-bug.svg",
" https://github.com/primer/octicons"
]
},
{
"path": "camera-plus.svg",
"license": "CC-BY-SA 3.0",
"authors": [
"Dave Gandy",
"Pieter Vander Vennet"
],
"sources": [
"https://fontawesome.com/",
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
]
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "checkmark.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "circle.svg",
"license": "CC0",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet"
],
"path": "clock.svg",
"license": "CC0",
"sources": []
},
{
"authors": [],
"path": "close.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "compass.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "cross_bottom_right.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair-blue-center.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair-blue.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair-empty.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair-locked.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"Dave Gandy"
],
"path": "delete_icon.svg",
"license": "CC-BY-SA",
"sources": [
"https://commons.wikimedia.org/wiki/File:Trash_font_awesome.svg\rT"
]
},
{
"authors": [],
"path": "direction.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "direction_gradient.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "down.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "envelope.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"The Tango Desktop Project"
],
"path": "floppy.svg",
"license": "CC0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg",
"http://tango.freedesktop.org/Tango_Desktop_Project"
]
},
{
"authors": [],
"path": "gear.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "help.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"Timothy Miller"
],
"path": "home.svg",
"license": "CC-BY-SA 3.0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
]
},
{
"authors": [
"Timothy Miller"
],
"path": "home_white_bg.svg",
"license": "CC-BY-SA 3.0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Home-icon.svg"
]
},
{
"authors": [
"JOSM Team"
],
"path": "josm_logo.svg",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:JOSM_Logotype_2019.svg",
" https://josm.openstreetmap.de/"
]
},
{
"authors": [],
"path": "layers.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "layersAdd.svg",
"license": "CC0; trivial",
"sources": []
},
{
"path": "Ornament-Horiz-0.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-1.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-2.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-3.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-4.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-5.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"path": "Ornament-Horiz-6.svg",
"license": "CC-BY",
"authors": [
"Nightwolfdezines"
],
"sources": [
"https://www.vecteezy.com/vector-art/226361-ornaments-and-flourishes"
]
},
{
"authors": [],
"path": "star.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "star_outline.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "star_half.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "star_outline_half.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "logo.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "logout.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"Pieter Vander Vennet",
" OSM"
],
"path": "mapcomplete_logo.svg",
"license": "Logo; CC-BY-SA",
"sources": [
"https://mapcomplete.osm.be"
]
},
{
"authors": [
"Mapillary"
],
"path": "mapillary.svg",
"license": "Logo; All rights reserved",
"sources": [
"https://mapillary.com/"
]
},
{
"authors": [],
"path": "min.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "no_checkmark.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "or.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "osm-copyright.svg",
"license": "logo; all rights reserved",
"sources": [
"https://www.OpenStreetMap.org"
]
},
{
"authors": [
"OpenStreetMap U.S. Chapter"
],
"path": "osm-logo-us.svg",
"license": "Logo",
"sources": [
"https://www.openstreetmap.us/"
]
},
{
"authors": [],
"path": "osm-logo.svg",
"license": "logo; all rights reserved",
"sources": [
"https://www.OpenStreetMap.org"
]
},
{
"authors": [
"GitHub Octicons"
],
"path": "pencil.svg",
"license": "MIT",
"sources": [
"https://github.com/primer/octicons",
" https://commons.wikimedia.org/wiki/File:Octicons-pencil.svg"
]
},
{
"authors": [
"@ tyskrat"
],
"path": "phone.svg",
"license": "CC-BY 3.0",
"sources": [
"https://www.onlinewebfonts.com/icon/1059"
]
},
{
"authors": [],
"path": "pin.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "plus.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"@fatih"
],
"path": "pop-out.svg",
"license": "CC-BY 3.0",
"sources": [
"https://www.onlinewebfonts.com/icon/2151"
]
},
{
"authors": [],
"path": "reload.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "ring.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"OOjs UI Team and other contributors"
],
"path": "search.svg",
"license": "MIT",
"sources": [
"https://commons.wikimedia.org/wiki/File:OOjs_UI_indicator_search-rtl.svg",
"https://phabricator.wikimedia.org/diffusion/GOJU/browse/master/AUTHORS.txt"
]
},
{
"authors": [],
"path": "send_email.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "share.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "square.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"@felpgrc"
],
"path": "statistics.svg",
"license": "CC-BY 3.0",
"sources": [
"https://www.onlinewebfonts.com/icon/197818"
]
},
{
"authors": [
"MGalloway (WMF)"
],
"path": "translate.svg",
"license": "CC-BY-SA 3.0",
"sources": [
"https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg"
]
},
{
"authors": [],
"path": "up.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [
"Wikidata"
],
"path": "wikidata.svg",
"license": "Logo; All rights reserved",
"sources": [
"https://www.wikidata.org"
]
},
{
"authors": [
"Wikimedia"
],
"path": "wikimedia-commons-white.svg",
"license": "Logo; All rights reserved",
"sources": [
"https://commons.wikimedia.org"
]
},
{
"authors": [
"Wikipedia"
],
"path": "wikipedia.svg",
"license": "Logo; All rights reserved",
"sources": [
"https://www.wikipedia.org/"
]
},
{
"authors": [
"Mapillary"
],
"path": "mapillary_black.svg",
"license": "Logo; All rights reserved",
"sources": [
"https://www.mapillary.com/"
]
},
{
"authors": [
"The Tango! Desktop Project"
],
"path": "floppy.svg",
"license": "CC0",
"sources": [
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg"
]
} }
] ]

View file

@ -1,15 +1,15 @@
{ {
"wikipedialink": { "wikipedialink": {
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>", "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' alt='WP'/></a>",
"condition": "wikipedia~*", "condition": {
"or": [
"wikipedia~*",
"wikidata~*"
]
},
"mappings": [ "mappings": [
{ {
"if": { "if": "wikipedia=",
"and": [
"wikipedia=",
"wikidata~*"
]
},
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>" "then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
} }
] ]
@ -59,8 +59,12 @@
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>", "render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>",
"mappings": [ "mappings": [
{ {
"if": "id~=-", "if": "id~.*/-.*",
"then": "<span class='alert'>Uploading...</alert>" "then": ""
},
{
"if": "_backend~*",
"then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>"
} }
], ],
"condition": "id~(node|way|relation)/[0-9]*" "condition": "id~(node|way|relation)/[0-9]*"

Binary file not shown.

View file

@ -736,7 +736,7 @@
"_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)", "_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)",
"_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])", "_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])",
"_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])", "_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])",
"_contained_climbing_routes_count=JSON.parse(_contained_climbing_routes).length" "_contained_climbing_routes_count=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').length"
] ]
}, },
{ {
@ -1412,8 +1412,8 @@
"_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})", "_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})",
"_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]", "_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]",
"_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'", "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'",
"_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock", "_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock",
"_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id", "_embedding_features_with_rock:id=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.id",
"_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access", "_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access",
"_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']", "_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id" "_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 20.28"><defs><style>.cls-1,.cls-3{fill:#fff;}.cls-2{fill:none;stroke-width:1.2px;}.cls-2,.cls-3{stroke:#fff;stroke-miterlimit:10;}.cls-3{stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M18.77,10.71h0l-.1-.15-2.54-3.8h0a2.1,2.1,0,0,0-4,.89,2.22,2.22,0,0,0,0,.45h0v.26l0-.37-.48.3V8.7H11V8.29L10.49,8l0,.37V8.1h0a2.22,2.22,0,0,0,0-.45,2.09,2.09,0,0,0-4-.89h0L4,10.56a1.3,1.3,0,0,0-.1.15h0a3.56,3.56,0,0,0-.56,1.91,3.6,3.6,0,0,0,7.18.29v-1.6l.59-.53,0-.72h.41l0,.72.6.53v1.13c0,.06,0,.12,0,.18s0,.06,0,.09v.22l0,0a3.59,3.59,0,1,0,6.62-2.2Zm-11.85,4a2.14,2.14,0,1,1,2.14-2.14A2.14,2.14,0,0,1,6.92,14.76Zm8.81,0a2.14,2.14,0,1,1,2.14-2.14A2.14,2.14,0,0,1,15.73,14.76Z"/><path class="cls-2" d="M21.61,17.74V6.56a.35.35,0,0,0-.19-.3L11.58.67a.53.53,0,0,0-.52,0L1.23,6.26a.35.35,0,0,0-.19.3V17.74"/><path class="cls-3" d="M22.35,20H.65a.4.4,0,0,1,0-.8h21.7a.4.4,0,0,1,0,.8Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1,021 B

View file

@ -116,11 +116,5 @@
"path": "birdhide.svg", "path": "birdhide.svg",
"license": "CC0", "license": "CC0",
"sources": [] "sources": []
},
{
"authors": [],
"path": "birdshelter.svg",
"license": "CC0",
"sources": []
} }
] ]

View file

@ -24,36 +24,58 @@
"startZoom": 15, "startZoom": 15,
"widenFactor": 0.05, "widenFactor": 0.05,
"socialImage": "", "socialImage": "",
"defaultBackgroundId": "CartoDB.Positron",
"layers": [ "layers": [
{ {
"builtin": [ "#": "Nature reserve with geometry, z>=13",
"nature_reserve" "builtin": "nature_reserve",
],
"override": { "override": {
"source": { "source": {
"osmTags": { "osmTags": {
"+and": [ "+and": [
"operator~.*[nN]atuurpunt.*" "operator~.*[nN]atuurpunt.*"
] ]
} },
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
}, },
"minzoom": "10", "minzoom": "13",
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg"
} }
} }
}, },
{ {
"builtin": [ "#": "Nature reserve overview from cache, points only, z < 13",
"visitor_information_centre" "builtin": "nature_reserve",
],
"override": { "override": {
"source": { "source": {
"osmTags": { "osmTags": {
"+and": [ "+and": [
"operator~.*[nN]atuurpunt.*" "operator~.*[nN]atuurpunt.*"
] ]
} },
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson"
},
"minzoom": "0",
"icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg"
}
}
},
{
"builtin": "visitor_information_centre",
"override": {
"source": {
"osmTags": {
"+and": [
"operator~.*[nN]atuurpunt.*"
]
},
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
}, },
"minzoom": "10", "minzoom": "10",
"icon": { "icon": {
@ -62,16 +84,17 @@
} }
}, },
{ {
"builtin": [ "builtin": "trail",
"trail"
],
"override": { "override": {
"source": { "source": {
"osmTags": { "osmTags": {
"+and": [ "+and": [
"operator~.*[nN]atuurpunt.*" "operator~.*[nN]atuurpunt.*"
] ]
} },
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
}, },
"minzoom": "13", "minzoom": "13",
"icon": { "icon": {
@ -90,11 +113,14 @@
} }
}, },
{ {
"builtin": [ "builtin": "toilet",
"toilet"
],
"override": { "override": {
"minzoom": "15", "minzoom": "15",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg", "render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg",
"mappings": [ "mappings": [
@ -111,42 +137,49 @@
} }
}, },
{ {
"builtin": [ "builtin": "birdhide",
"birdhide"
],
"override": { "override": {
"minzoom": "15", "minzoom": "15",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg"
} }
} }
}, },
{ {
"builtin": [ "builtin": "picnic_table",
"picnic_table"
],
"override": { "override": {
"minzoom": "16", "minzoom": "16",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg"
} }
} }
}, },
{ {
"builtin": [ "builtin": "drinking_water",
"drinking_water"
],
"override": { "override": {
"minzoom": "16", "minzoom": "16",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg"
} }
} }
}, },
{ {
"builtin": [ "builtin": "parking",
"parking"
],
"override": { "override": {
"minzoom": "16", "minzoom": "16",
"icon": { "icon": {
@ -173,33 +206,42 @@
} }
}, },
{ {
"builtin": [ "builtin": "information_board",
"information_board"
],
"override": { "override": {
"minzoom": "16", "minzoom": "16",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg"
} }
} }
}, },
{ {
"builtin": [ "builtin": "bench",
"bench"
],
"override": { "override": {
"minzoom": "18", "minzoom": "18",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg"
} }
} }
}, },
{ {
"builtin": [ "builtin": "watermill",
"watermill"
],
"override": { "override": {
"minzoom": "18", "minzoom": "18",
"source": {
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"icon": { "icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg" "render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg"
} }

View file

@ -62,7 +62,8 @@
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
}, },
"freeform": { "freeform": {
"key": "generator:output:electricity" "key": "generator:output:electricity",
"type": "pfloat"
} }
}, },
{ {
@ -85,7 +86,7 @@
}, },
"freeform": { "freeform": {
"key": "height", "key": "height",
"type": "float" "type": "pfloat"
} }
}, },
{ {
@ -179,6 +180,24 @@
} }
], ],
"eraseInvalidValues": true "eraseInvalidValues": true
},
{
"appliesToKey": [
"height",
"rotor:diameter"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": " meter",
"nl": " meter"
}
}
]
} }
], ],
"defaultBackgroundId": "CartoDB.Voyager" "defaultBackgroundId": "CartoDB.Voyager"

View file

@ -105,11 +105,31 @@
{ {
"builtin": "slow_roads", "builtin": "slow_roads",
"override": { "override": {
"+tagRenderings": [
{
"question": "Is dit een publiek toegankelijk pad?",
"mappings": [
{
"if": "access=private",
"then": "Dit is een privaat pad"
},
{
"if": "access=no",
"then": "Dit is een privaat pad",
"hideInAnswer": true
},
{
"if": "access=permissive",
"then": "Dit pad is duidelijk in private eigendom, maar er hangen geen verbodsborden dus mag men erover"
}
]
}
],
"calculatedTags": [ "calculatedTags": [
"_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')", "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\"))).join(', ')",
"_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''"
], ],
"minzoom": 9, "minzoom": 18,
"source": { "source": {
"geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson", "geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson",
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",

View file

@ -64,7 +64,13 @@
}, },
"tagRenderings": [ "tagRenderings": [
{ {
"render": "Deze straat is <b>{width:carriageway}m</b> breed" "render": "Deze straat is <b>{width:carriageway}m</b> breed",
"question": "Hoe breed is deze straat?",
"freeform": {
"key": "width:carriageway",
"type": "length",
"helperArgs": [21, "map"]
}
}, },
{ {
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:", "render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",

View file

@ -82,6 +82,10 @@ html, body {
box-sizing: initial !important; box-sizing: initial !important;
} }
.leaflet-control-attribution {
display: block ruby;
}
svg, img { svg, img {
box-sizing: content-box; box-sizing: content-box;
width: 100%; width: 100%;
@ -101,6 +105,10 @@ a {
width: min-content; width: min-content;
} }
.w-16-imp {
width: 4rem !important;
}
.space-between{ .space-between{
justify-content: space-between; justify-content: space-between;
} }

View file

@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput";
import SpecialVisualizations from "./UI/SpecialVisualizations"; import SpecialVisualizations from "./UI/SpecialVisualizations";
import ShowDataLayer from "./UI/ShowDataLayer"; import ShowDataLayer from "./UI/ShowDataLayer";
import * as L from "leaflet"; import * as L from "leaflet";
import ValidatedTextField from "./UI/Input/ValidatedTextField";
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
DirectionInput.constructMinimap = options => new Minimap(options) DirectionInput.constructMinimap = options => new Minimap(options)
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
SpecialVisualizations.constructMiniMap = options => new Minimap(options) SpecialVisualizations.constructMiniMap = options => new Minimap(options)
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
leafletMap: UIEventSource<L.Map>, leafletMap: UIEventSource<L.Map>,

View file

@ -149,6 +149,10 @@
"zoomInToSeeThisLayer": "Zoom in to see this layer", "zoomInToSeeThisLayer": "Zoom in to see this layer",
"title": "Select layers" "title": "Select layers"
}, },
"download": {
"downloadGeojson": "Download visible data as geojson",
"licenseInfo": "<h3>Copyright notice</h3>The provided is available under ODbL. Reusing this data is free for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b></li><li>Any change to this data must be republished under the same license</li></ul>. Please see the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details"
},
"weekdays": { "weekdays": {
"abbreviations": { "abbreviations": {
"monday": "Mon", "monday": "Mon",

View file

@ -1185,9 +1185,6 @@
}, },
"1": { "1": {
"then": "{name}" "then": "{name}"
},
"2": {
"then": "Fietsenstalling"
} }
} }
}, },

View file

@ -487,6 +487,11 @@
} }
} }
} }
},
"presets": {
"0": {
"title": "Обслуживание велосипедов/магазин"
}
} }
}, },
"defibrillator": { "defibrillator": {
@ -1064,6 +1069,7 @@
"1": { "1": {
"question": "Вы хотите добавить описание?" "question": "Вы хотите добавить описание?"
} }
} },
"name": "Смотровая площадка"
} }
} }

View file

@ -122,8 +122,10 @@
"thanksForSharing": "Obrigado por compartilhar!", "thanksForSharing": "Obrigado por compartilhar!",
"copiedToClipboard": "Link copiado para a área de transferência", "copiedToClipboard": "Link copiado para a área de transferência",
"addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.", "addToHomeScreen": "<h3>Adicionar à sua tela inicial</h3>Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.",
"intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:" "intro": "<h3>Compartilhe este mapa</h3> Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:",
} "embedIntro": "<h3>Incorpore em seu site</h3>Por favor, incorpore este mapa em seu site.<br>Nós o encorajamos a fazer isso - você nem precisa pedir permissão.<br>É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará."
},
"aboutMapcomplete": "<h3>Sobre o MapComplete</h3><p>Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre um<b>único tema.</b>Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! O<b>mantenedor do tema</b>define elementos, questões e linguagens para o tema.</p><h3>Saiba mais</h3><p>MapComplete sempre<b>oferece a próxima etapa</b>para saber mais sobre o OpenStreetMap.</p><ul><li>Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira</li><li>A versão em tela inteira oferece informações sobre o OpenStreetMap</li><li>A visualização funciona sem login, mas a edição requer um login do OSM.</li><li>Se você não estiver conectado, será solicitado que você faça o login</li><li>Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa </li><li> Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki </li></ul><p></p><br><p>Você percebeu<b>um problema</b>? Você tem uma<b>solicitação de recurso </b>? Quer<b>ajudar a traduzir</b>? Acesse <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">o código-fonte</a>ou <a href=\"https: //github.com/pietervdvn/MapComplete / issues \" target=\" _ blank \">rastreador de problemas.</a></p><p>Quer ver<b>seu progresso</b>? Siga a contagem de edição em<a href=\"https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D\" target=\"_blank\">OsmCha</a>.</p>"
}, },
"index": { "index": {
"pickTheme": "Escolha um tema abaixo para começar.", "pickTheme": "Escolha um tema abaixo para começar.",
@ -142,10 +144,13 @@
"no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!",
"name_required": "É necessário um nome para exibir e criar comentários", "name_required": "É necessário um nome para exibir e criar comentários",
"title_singular": "Um comentário", "title_singular": "Um comentário",
"title": "{count} comentários" "title": "{count} comentários",
"tos": "Se você criar um comentário, você concorda com <a href=\"https://mangrove.reviews/terms\" target=\"_blank\"> o TOS e a política de privacidade de Mangrove.reviews </a>",
"affiliated_reviewer_warning": "(Revisão de afiliados)"
}, },
"favourite": { "favourite": {
"reload": "Recarregar dados", "reload": "Recarregar dados",
"panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais" "panelIntro": "<h3>Seu tema pessoal</h3>Ative suas camadas favoritas de todos os temas oficiais",
"loginNeeded": "<h3>Entrar</h3> Um layout pessoal está disponível apenas para usuários do OpenStreetMap"
} }
} }

View file

@ -6,6 +6,27 @@
"opening_hours": { "opening_hours": {
"question": "Was sind die Öffnungszeiten von {name}?", "question": "Was sind die Öffnungszeiten von {name}?",
"render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Öffnungszeiten</h3>{opening_hours_table(opening_hours)}"
},
"level": {
"mappings": {
"2": {
"then": "Ist im ersten Stock"
},
"1": {
"then": "Ist im Erdgeschoss"
}
},
"render": "Befindet sich im {level}ten Stock",
"question": "In welchem Stockwerk befindet sich dieses Objekt?"
},
"description": {
"question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.<br/><span style='font-size: small'>Bitte keine bereits erhobenen Informationen.</span>"
},
"website": {
"question": "Was ist die Website von {name}?"
},
"email": {
"question": "Was ist die Mail-Adresse von {name}?"
} }
} }
} }

View file

@ -1 +1,30 @@
{} {
"undefined": {
"level": {
"render": "Localizado no {level}o andar",
"mappings": {
"2": {
"then": "Localizado no primeiro andar"
},
"1": {
"then": "Localizado no térreo"
},
"0": {
"then": "Localizado no subsolo"
}
}
},
"opening_hours": {
"question": "Qual o horário de funcionamento de {name}?"
},
"website": {
"question": "Qual o site de {name}?"
},
"email": {
"question": "Qual o endereço de e-mail de {name}?"
},
"phone": {
"question": "Qual o número de telefone de {name}?"
}
}
}

View file

@ -15,6 +15,20 @@
"opening_hours": { "opening_hours": {
"question": "Какое время работы у {name}?", "question": "Какое время работы у {name}?",
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}" "render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
},
"level": {
"mappings": {
"2": {
"then": "Расположено на первом этаже"
},
"1": {
"then": "Расположено на первом этаже"
},
"0": {
"then": "Расположено под землей"
}
},
"render": "Расположено на {level}ом этаже"
} }
} }
} }

View file

@ -1148,6 +1148,13 @@
"human": " gigawatts" "human": " gigawatts"
} }
} }
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
} }
} }
}, },

View file

@ -956,6 +956,13 @@
"human": " gigawatt" "human": " gigawatt"
} }
} }
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
} }
} }
}, },

920
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
"start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*",
"test": "ts-node test/TestAll.ts", "test": "ts-node test/TestAll.ts",
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean",
"add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers",
@ -20,7 +20,7 @@
"generate:layouts": "ts-node scripts/generateLayouts.ts", "generate:layouts": "ts-node scripts/generateLayouts.ts",
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
"generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56",
"generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4", "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail",
"generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",
@ -65,9 +65,11 @@
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"i18next-client": "^1.11.4", "i18next-client": "^1.11.4",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"jspdf": "^2.3.1",
"latlon2country": "^1.1.3", "latlon2country": "^1.1.3",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-providers": "^1.10.2", "leaflet-providers": "^1.10.2",
"leaflet-simple-map-screenshoter": "^0.4.4",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"libphonenumber": "0.0.10", "libphonenumber": "0.0.10",
"libphonenumber-js": "^1.7.55", "libphonenumber-js": "^1.7.55",
@ -81,7 +83,6 @@
"postcss": "^7.0.36", "postcss": "^7.0.36",
"prompt-sync": "^4.2.0", "prompt-sync": "^4.2.0",
"sharp": "^0.27.0", "sharp": "^0.27.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
"tslint": "^6.1.3" "tslint": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
@ -91,6 +92,7 @@
"fs": "0.0.1-security", "fs": "0.0.1-security",
"marked": "^2.0.0", "marked": "^2.0.0",
"read-file": "^0.2.0", "read-file": "^0.2.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"ts-node-dev": "^1.0.0-pre.63", "ts-node-dev": "^1.0.0-pre.63",
"tslint-no-circular-imports": "^0.7.0", "tslint-no-circular-imports": "^0.7.0",

View file

@ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement";
import Table from "./UI/Base/Table"; import Table from "./UI/Base/Table";
const connection = new OsmConnection(false, new UIEventSource<string>(undefined), ""); const connection = new OsmConnection(false, false, new UIEventSource<string>(undefined), "");
let rendered = false; let rendered = false;

View file

@ -1,7 +1,7 @@
/** /**
* Generates a collection of geojson files based on an overpass query for a given theme * Generates a collection of geojson files based on an overpass query for a given theme
*/ */
import {TileRange, Utils} from "../Utils"; import {Utils} from "../Utils";
Utils.runningFromConsole = true Utils.runningFromConsole = true
import {Overpass} from "../Logic/Osm/Overpass"; import {Overpass} from "../Logic/Osm/Overpass";
@ -17,6 +17,8 @@ import MetaTagging from "../Logic/MetaTagging";
import LayerConfig from "../Customizations/JSON/LayerConfig"; import LayerConfig from "../Customizations/JSON/LayerConfig";
import {GeoOperations} from "../Logic/GeoOperations"; import {GeoOperations} from "../Logic/GeoOperations";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import * as fs from "fs";
import {TileRange} from "../Models/TileRange";
function createOverpassObject(theme: LayoutConfig) { function createOverpassObject(theme: LayoutConfig) {
@ -139,7 +141,7 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ {
return allFeatures; return allFeatures;
} }
async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) {
let processed = 0; let processed = 0;
const layerIndex = theme.LayerIndex(); const layerIndex = theme.LayerIndex();
for (let x = r.xstart; x <= r.xend; x++) { for (let x = r.xstart; x <= r.xend; x++) {
@ -211,8 +213,9 @@ async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig,
} }
} }
async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) {
const z = r.zoomlevel; const z = r.zoomlevel;
const generated = {} // layer --> x --> y[]
for (let x = r.xstart; x <= r.xend; x++) { for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) { for (let y = r.ystart; y <= r.yend; y++) {
const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8")
@ -227,10 +230,8 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi
.filter(f => f._matching_layer_id === layer.id) .filter(f => f._matching_layer_id === layer.id)
.filter(f => { .filter(f => {
const isShown = layer.isShown.GetRenderValue(f.properties).txt const isShown = layer.isShown.GetRenderValue(f.properties).txt
if (isShown === "no") { return isShown !== "no";
return false;
}
return true;
}) })
const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z);
console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")")
@ -239,18 +240,66 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi
continue; continue;
} }
writeFileSync(new_path, JSON.stringify(geojson, null, " ")) writeFileSync(new_path, JSON.stringify(geojson, null, " "))
if (generated[layer.id] === undefined) {
generated[layer.id] = {}
}
if (generated[layer.id][x] === undefined) {
generated[layer.id][x] = []
}
generated[layer.id][x].push(y)
} }
} }
} }
for (const layer of theme.layers) {
const id = layer.id
const loaded = generated[id]
if(loaded === undefined){
console.log("No features loaded for layer ",id)
continue;
}
writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded))
}
} }
async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) {
const allFeatures = []
for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) {
const read_path = geoJsonName(targetdir + "_" + layername, x, y, z);
if (!fs.existsSync(read_path)) {
continue;
}
const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features
const pointsOnly = features.map(f => {
f.properties["_last_edit:timestamp"] = "1970-01-01"
if (f.geometry.type === "Point") {
return f
} else {
return GeoOperations.centerpoint(f)
}
})
allFeatures.push(...pointsOnly)
}
}
const geojson = {
"type": "FeatureCollection",
"features": allFeatures
}
writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " "))
}
async function main(args: string[]) { async function main(args: string[]) {
if (args.length == 0) { if (args.length == 0) {
console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1") console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]")
return; return;
} }
const themeName = args[0] const themeName = args[0]
@ -285,8 +334,18 @@ async function main(args: string[]) {
} while (failed > 0) } while (failed > 0)
const extraFeatures = await downloadExtraData(theme); const extraFeatures = await downloadExtraData(theme);
await postProcess(targetdir, tileRange, theme, extraFeatures) postProcess(targetdir, tileRange, theme, extraFeatures)
await splitPerLayer(targetdir, tileRange, theme) splitPerLayer(targetdir, tileRange, theme)
if (args[7] === "--generate-point-overview") {
const targetLayers = args[8].split(",")
for (const targetLayer of targetLayers) {
if (!theme.layers.some(l => l.id === targetLayer)) {
throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",")
}
createOverview(targetdir, tileRange, zoomlevel, targetLayer)
}
}
} }

View file

@ -4,7 +4,6 @@ import LayerConfig from "../Customizations/JSON/LayerConfig";
import * as licenses from "../assets/generated/license_info.json" import * as licenses from "../assets/generated/license_info.json"
import LayoutConfig from "../Customizations/JSON/LayoutConfig"; import LayoutConfig from "../Customizations/JSON/LayoutConfig";
import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson";
import {Translation} from "../UI/i18n/Translation";
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
import AllKnownLayers from "../Customizations/AllKnownLayers"; import AllKnownLayers from "../Customizations/AllKnownLayers";
@ -77,63 +76,6 @@ class LayerOverviewUtils {
return errorCount return errorCount
} }
validateTranslationCompletenessOfObject(object: any, expectedLanguages: string[], context: string) {
const missingTranlations = []
const translations: { tr: Translation, context: string }[] = [];
const queue: { object: any, context: string }[] = [{object: object, context: context}]
while (queue.length > 0) {
const item = queue.pop();
const o = item.object
for (const key in o) {
const v = o[key];
if (v === undefined) {
continue;
}
if (v instanceof Translation || v?.translations !== undefined) {
translations.push({tr: v, context: item.context});
} else if (
["string", "function", "boolean", "number"].indexOf(typeof (v)) < 0) {
queue.push({object: v, context: item.context + "." + key})
}
}
}
const missing = {}
const present = {}
for (const ln of expectedLanguages) {
missing[ln] = 0;
present[ln] = 0;
for (const translation of translations) {
if (translation.tr.translations["*"] !== undefined) {
continue;
}
const txt = translation.tr.translations[ln];
const isMissing = txt === undefined || txt === "" || txt.toLowerCase().indexOf("todo") >= 0;
if (isMissing) {
missingTranlations.push(`${translation.context},${ln},${translation.tr.txt}`)
missing[ln]++
} else {
present[ln]++;
}
}
}
let message = `Translation completeness for ${context}`
let isComplete = true;
for (const ln of expectedLanguages) {
const amiss = missing[ln];
const ok = present[ln];
const total = amiss + ok;
message += ` ${ln}: ${ok}/${total}`
if (ok !== total) {
isComplete = false;
}
}
return missingTranlations
}
main(args: string[]) { main(args: string[]) {
const lt = this.loadThemesAndLayers(); const lt = this.loadThemesAndLayers();
@ -160,7 +102,6 @@ class LayerOverviewUtils {
} }
let themeErrorCount = [] let themeErrorCount = []
let missingTranslations = []
for (const themeFile of themeFiles) { for (const themeFile of themeFiles) {
if (typeof themeFile.language === "string") { if (typeof themeFile.language === "string") {
themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings") themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings")
@ -169,10 +110,6 @@ class LayerOverviewUtils {
if (typeof layer === "string") { if (typeof layer === "string") {
if (!knownLayerIds.has(layer)) { if (!knownLayerIds.has(layer)) {
themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`) themeErrorCount.push(`Unknown layer id: ${layer} in theme ${themeFile.id}`)
} else {
const layerConfig = knownLayerIds.get(layer);
missingTranslations.push(...this.validateTranslationCompletenessOfObject(layerConfig, themeFile.language, "Layer " + layer))
} }
} else if (layer.builtin !== undefined) { } else if (layer.builtin !== undefined) {
let names = layer.builtin; let names = layer.builtin;
@ -197,7 +134,6 @@ class LayerOverviewUtils {
.filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason .filter(l => typeof l != "string") // We remove all the builtin layer references as they don't work with ts-node for some weird reason
.filter(l => l.builtin === undefined) .filter(l => l.builtin === undefined)
missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id))
try { try {
const theme = new LayoutConfig(themeFile, true, "test") const theme = new LayoutConfig(themeFile, true, "test")
@ -209,11 +145,6 @@ class LayerOverviewUtils {
} }
} }
if (missingTranslations.length > 0) {
console.log(missingTranslations.length, "missing translations")
writeFileSync("missing_translations.txt", missingTranslations.join("\n"))
}
if (layerErrorCount.length + themeErrorCount.length == 0) { if (layerErrorCount.length + themeErrorCount.length == 0) {
console.log("All good!") console.log("All good!")

35
test.ts
View file

@ -7,6 +7,11 @@ import {UIEventSource} from "./Logic/UIEventSource";
import {Tag} from "./Logic/Tags/Tag"; import {Tag} from "./Logic/Tags/Tag";
import {QueryParameters} from "./Logic/Web/QueryParameters"; import {QueryParameters} from "./Logic/Web/QueryParameters";
import {Translation} from "./UI/i18n/Translation"; import {Translation} from "./UI/i18n/Translation";
import LocationInput from "./UI/Input/LocationInput";
import Loc from "./Models/Loc";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import LengthInput from "./UI/Input/LengthInput";
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
/*import ValidatedTextField from "./UI/Input/ValidatedTextField"; /*import ValidatedTextField from "./UI/Input/ValidatedTextField";
import Combine from "./UI/Base/Combine"; import Combine from "./UI/Base/Combine";
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
@ -148,19 +153,17 @@ function TestMiniMap() {
featureSource.ping() featureSource.ping()
} }
//*/ //*/
QueryParameters.GetQueryParameter("test", "true").setData("true")
State.state= new State(undefined) const loc = new UIEventSource<Loc>({
const id = "node/5414688303" zoom: 24,
State.state.allElements.addElementById(id, new UIEventSource<any>({id: id})) lat: 51.21043,
new Combine([ lon: 3.21389
new DeleteWizard(id, { })
noDeleteOptions: [ const li = new LengthInput(
{ AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")),
if:[ new Tag("access","private")], loc
then: new Translation({ )
en: "Very private! Delete now or me send lawfull lawyer" li.SetStyle("height: 30rem; background: aliceblue;")
}) .AttachTo("maindiv")
}
] new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv")
}),
]).AttachTo("maindiv")

View file

@ -15,7 +15,7 @@ export default class OsmConnectionSpec extends T {
super("OsmConnectionSpec-test", [ super("OsmConnectionSpec-test", [
["login on dev", ["login on dev",
() => { () => {
const osmConn = new OsmConnection(false, const osmConn = new OsmConnection(false,false,
new UIEventSource<string>(undefined), new UIEventSource<string>(undefined),
"Unit test", "Unit test",
true, true,

View file

@ -1,10 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-no-circular-imports"
],
"jsRules": {},
"rules": {},
"rulesDirectory": []
}