reset to previous commit
|
@ -3,6 +3,7 @@ import UnitConfigJson from "./UnitConfigJson";
|
|||
import Translations from "../../UI/i18n/Translations";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
|
||||
export class Unit {
|
||||
public readonly appliesToKeys: Set<string>;
|
||||
|
@ -81,7 +82,10 @@ export class Unit {
|
|||
return undefined;
|
||||
}
|
||||
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];
|
||||
return new Combine(elems)
|
||||
|
@ -152,7 +156,7 @@ export class Denomination {
|
|||
if (stripped === null) {
|
||||
return null;
|
||||
}
|
||||
return stripped + " " + this.canonical.trim()
|
||||
return (stripped + " " + this.canonical.trim()).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
27
Customizations/JSON/FilterConfig.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { TagsFilter } from "../../Logic/Tags/TagsFilter";
|
||||
import { Translation } from "../../UI/i18n/Translation";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import FilterConfigJson from "./FilterConfigJson";
|
||||
import { FromJSON } from "./FromJSON";
|
||||
|
||||
export default class FilterConfig {
|
||||
readonly options: {
|
||||
question: Translation;
|
||||
osmTags: TagsFilter;
|
||||
}[];
|
||||
|
||||
constructor(json: FilterConfigJson, context: string) {
|
||||
this.options = json.options.map((option, i) => {
|
||||
const question = Translations.T(
|
||||
option.question,
|
||||
context + ".options-[" + i + "].question"
|
||||
);
|
||||
const osmTags = FromJSON.Tag(
|
||||
option.osmTags,
|
||||
`${context}.options-[${i}].osmTags`
|
||||
);
|
||||
|
||||
return { question: question, osmTags: osmTags };
|
||||
});
|
||||
}
|
||||
}
|
11
Customizations/JSON/FilterConfigJson.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { AndOrTagConfigJson } from "./TagConfigJson";
|
||||
|
||||
export default interface FilterConfigJson {
|
||||
/**
|
||||
* The options for a filter
|
||||
* If there are multiple options these will be a list of radio buttons
|
||||
* If there is only one option this will be a checkbox
|
||||
* Filtering is done based on the given osmTags that are compared to the objects in that layer.
|
||||
*/
|
||||
options: { question: string | any; osmTags: AndOrTagConfigJson | string }[];
|
||||
}
|
|
@ -18,19 +18,18 @@ import {Tag} from "../../Logic/Tags/Tag";
|
|||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import {Unit} from "./Denomination";
|
||||
import DeleteConfig from "./DeleteConfig";
|
||||
import FilterConfig from "./FilterConfig";
|
||||
|
||||
export default class LayerConfig {
|
||||
|
||||
|
||||
static WAYHANDLING_DEFAULT = 0;
|
||||
static WAYHANDLING_CENTER_ONLY = 1;
|
||||
static WAYHANDLING_CENTER_AND_WAY = 2;
|
||||
|
||||
id: string;
|
||||
name: Translation
|
||||
name: Translation;
|
||||
description: Translation;
|
||||
source: SourceConfig;
|
||||
calculatedTags: [string, string][]
|
||||
calculatedTags: [string, string][];
|
||||
doNotDownload: boolean;
|
||||
passAllFeatures: boolean;
|
||||
isShown: TagRenderingConfig;
|
||||
|
@ -39,7 +38,7 @@ export default class LayerConfig {
|
|||
title?: TagRenderingConfig;
|
||||
titleIcons: TagRenderingConfig[];
|
||||
icon: TagRenderingConfig;
|
||||
iconOverlays: { if: TagsFilter, then: TagRenderingConfig, badge: boolean }[]
|
||||
iconOverlays: { if: TagsFilter; then: TagRenderingConfig; badge: boolean }[];
|
||||
iconSize: TagRenderingConfig;
|
||||
label: TagRenderingConfig;
|
||||
rotation: TagRenderingConfig;
|
||||
|
@ -48,33 +47,40 @@ export default class LayerConfig {
|
|||
dashArray: TagRenderingConfig;
|
||||
wayHandling: number;
|
||||
public readonly units: Unit[];
|
||||
public readonly deletion: DeleteConfig | null
|
||||
public readonly deletion: DeleteConfig | null;
|
||||
|
||||
presets: {
|
||||
title: Translation,
|
||||
tags: Tag[],
|
||||
description?: Translation,
|
||||
preciseInput?: { preferredBackground?: string }
|
||||
}[];
|
||||
|
||||
tagRenderings: TagRenderingConfig [];
|
||||
tagRenderings: TagRenderingConfig[];
|
||||
filters: FilterConfig[];
|
||||
|
||||
constructor(json: LayerConfigJson,
|
||||
units?:Unit[],
|
||||
context?: string,
|
||||
official: boolean = true,) {
|
||||
constructor(
|
||||
json: LayerConfigJson,
|
||||
units?: Unit[],
|
||||
context?: string,
|
||||
official: boolean = true
|
||||
) {
|
||||
this.units = units ?? [];
|
||||
context = context + "." + json.id;
|
||||
const self = this;
|
||||
this.id = json.id;
|
||||
this.name = Translations.T(json.name, context + ".name");
|
||||
|
||||
if(json.description !== undefined){
|
||||
if(Object.keys(json.description).length === 0){
|
||||
|
||||
if (json.description !== undefined) {
|
||||
if (Object.keys(json.description).length === 0) {
|
||||
json.description = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.description =Translations.T(json.description, context + ".description") ;
|
||||
|
||||
this.description = Translations.T(
|
||||
json.description,
|
||||
context + ".description"
|
||||
);
|
||||
|
||||
let legacy = undefined;
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
|
@ -83,45 +89,54 @@ export default class LayerConfig {
|
|||
}
|
||||
if (json.source !== undefined) {
|
||||
if (legacy !== undefined) {
|
||||
throw context + "Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
|
||||
throw (
|
||||
context +
|
||||
"Both the legacy 'layer.overpasstags' and the new 'layer.source'-field are defined"
|
||||
);
|
||||
}
|
||||
|
||||
let osmTags: TagsFilter = legacy;
|
||||
if (json.source["osmTags"]) {
|
||||
osmTags = FromJSON.Tag(json.source["osmTags"], context + "source.osmTags");
|
||||
osmTags = FromJSON.Tag(
|
||||
json.source["osmTags"],
|
||||
context + "source.osmTags"
|
||||
);
|
||||
}
|
||||
|
||||
if(json.source["geoJsonSource"] !== undefined){
|
||||
throw context + "Use 'geoJson' instead of 'geoJsonSource'"
|
||||
if (json.source["geoJsonSource"] !== undefined) {
|
||||
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
|
||||
}
|
||||
|
||||
this.source = new SourceConfig({
|
||||
osmTags: osmTags,
|
||||
geojsonSource: json.source["geoJson"],
|
||||
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
|
||||
overpassScript: json.source["overpassScript"],
|
||||
isOsmCache: json.source["isOsmCache"]
|
||||
}, this.id);
|
||||
|
||||
this.source = new SourceConfig(
|
||||
{
|
||||
osmTags: osmTags,
|
||||
geojsonSource: json.source["geoJson"],
|
||||
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
|
||||
overpassScript: json.source["overpassScript"],
|
||||
isOsmCache: json.source["isOsmCache"],
|
||||
},
|
||||
this.id
|
||||
);
|
||||
} else {
|
||||
this.source = new SourceConfig({
|
||||
osmTags: legacy
|
||||
})
|
||||
osmTags: legacy,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.calculatedTags = undefined;
|
||||
if (json.calculatedTags !== undefined) {
|
||||
if (!official) {
|
||||
console.warn(`Unofficial theme ${this.id} with custom javascript! This is a security risk`)
|
||||
console.warn(
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
|
||||
);
|
||||
}
|
||||
this.calculatedTags = [];
|
||||
for (const kv of json.calculatedTags) {
|
||||
|
||||
const index = kv.indexOf("=")
|
||||
const index = kv.indexOf("=");
|
||||
const key = kv.substring(0, index);
|
||||
const code = kv.substring(index + 1);
|
||||
|
||||
this.calculatedTags.push([key, code])
|
||||
|
||||
this.calculatedTags.push([key, code]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,13 +145,19 @@ export default class LayerConfig {
|
|||
this.minzoom = json.minzoom ?? 0;
|
||||
this.maxzoom = json.maxzoom ?? 1000;
|
||||
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`),
|
||||
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
|
||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`)
|
||||
}))
|
||||
|
||||
tags: pr.tags.map((t) => FromJSON.SimpleTag(t)),
|
||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`),
|
||||
preciseInput: pr.preciseInput
|
||||
}
|
||||
});
|
||||
|
||||
/** Given a key, gets the corresponding property from the json (or the default if not found
|
||||
*
|
||||
|
@ -148,7 +169,11 @@ export default class LayerConfig {
|
|||
if (deflt === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new TagRenderingConfig(deflt, self.source.osmTags, `${context}.${key}.default value`);
|
||||
return new TagRenderingConfig(
|
||||
deflt,
|
||||
self.source.osmTags,
|
||||
`${context}.${key}.default value`
|
||||
);
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
const shared = SharedTagRenderings.SharedTagRendering.get(v);
|
||||
|
@ -156,54 +181,80 @@ export default class LayerConfig {
|
|||
return shared;
|
||||
}
|
||||
}
|
||||
return new TagRenderingConfig(v, self.source.osmTags, `${context}.${key}`);
|
||||
return new TagRenderingConfig(
|
||||
v,
|
||||
self.source.osmTags,
|
||||
`${context}.${key}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig
|
||||
* A string is interpreted as a name to call
|
||||
*/
|
||||
function trs(tagRenderings?: (string | TagRenderingConfigJson)[], readOnly = false) {
|
||||
function trs(
|
||||
tagRenderings?: (string | TagRenderingConfigJson)[],
|
||||
readOnly = false
|
||||
) {
|
||||
if (tagRenderings === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Utils.NoNull(tagRenderings.map(
|
||||
(renderingJson, i) => {
|
||||
return Utils.NoNull(
|
||||
tagRenderings.map((renderingJson, i) => {
|
||||
if (typeof renderingJson === "string") {
|
||||
|
||||
if (renderingJson === "questions") {
|
||||
if (readOnly) {
|
||||
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(renderingJson)}`
|
||||
throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(
|
||||
renderingJson
|
||||
)}`;
|
||||
}
|
||||
|
||||
return new TagRenderingConfig("questions", undefined)
|
||||
return new TagRenderingConfig("questions", undefined);
|
||||
}
|
||||
|
||||
|
||||
const shared = SharedTagRenderings.SharedTagRendering.get(renderingJson);
|
||||
const shared =
|
||||
SharedTagRenderings.SharedTagRendering.get(renderingJson);
|
||||
if (shared !== undefined) {
|
||||
return shared;
|
||||
}
|
||||
|
||||
const keys = Array.from(SharedTagRenderings.SharedTagRendering.keys())
|
||||
|
||||
if(Utils.runningFromConsole){
|
||||
|
||||
const keys = Array.from(
|
||||
SharedTagRenderings.SharedTagRendering.keys()
|
||||
);
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${(keys.join(", "))}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
|
||||
|
||||
throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${keys.join(
|
||||
", "
|
||||
)}\n If you intent to output this text literally, use {\"render\": <your text>} instead"}`;
|
||||
}
|
||||
return new TagRenderingConfig(renderingJson, self.source.osmTags, `${context}.tagrendering[${i}]`);
|
||||
}));
|
||||
return new TagRenderingConfig(
|
||||
renderingJson,
|
||||
self.source.osmTags,
|
||||
`${context}.tagrendering[${i}]`
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.tagRenderings = trs(json.tagRenderings, false);
|
||||
|
||||
this.filters = (json.filter ?? []).map((option, i) => {
|
||||
return new FilterConfig(option, `${context}.filter-[${i}]`)
|
||||
});
|
||||
|
||||
const titleIcons = [];
|
||||
const defaultIcons = ["phonelink", "emaillink", "wikipedialink", "osmlink", "sharelink"];
|
||||
for (const icon of (json.titleIcons ?? defaultIcons)) {
|
||||
const defaultIcons = [
|
||||
"phonelink",
|
||||
"emaillink",
|
||||
"wikipedialink",
|
||||
"osmlink",
|
||||
"sharelink",
|
||||
];
|
||||
for (const icon of json.titleIcons ?? defaultIcons) {
|
||||
if (icon === "defaults") {
|
||||
titleIcons.push(...defaultIcons);
|
||||
} else {
|
||||
|
@ -213,74 +264,85 @@ export default class LayerConfig {
|
|||
|
||||
this.titleIcons = trs(titleIcons, true);
|
||||
|
||||
|
||||
this.title = tr("title", undefined);
|
||||
this.icon = tr("icon", "");
|
||||
this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => {
|
||||
let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`);
|
||||
if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) {
|
||||
let tr = new TagRenderingConfig(
|
||||
overlay.then,
|
||||
self.source.osmTags,
|
||||
`iconoverlays.${i}`
|
||||
);
|
||||
if (
|
||||
typeof overlay.then === "string" &&
|
||||
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
|
||||
) {
|
||||
tr = SharedTagRenderings.SharedIcons.get(overlay.then);
|
||||
}
|
||||
return {
|
||||
if: FromJSON.Tag(overlay.if),
|
||||
then: tr,
|
||||
badge: overlay.badge ?? false
|
||||
}
|
||||
badge: overlay.badge ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
|
||||
if (iconPath.startsWith(Utils.assets_path)) {
|
||||
const iconKey = iconPath.substr(Utils.assets_path.length);
|
||||
if (Svg.All[iconKey] === undefined) {
|
||||
throw "Builtin SVG asset not found: " + iconPath
|
||||
throw "Builtin SVG asset not found: " + iconPath;
|
||||
}
|
||||
}
|
||||
this.isShown = tr("isShown", "yes");
|
||||
this.iconSize = tr("iconSize", "40,40,center");
|
||||
this.label = tr("label", "")
|
||||
this.label = tr("label", "");
|
||||
this.color = tr("color", "#0000ff");
|
||||
this.width = tr("width", "7");
|
||||
this.rotation = tr("rotation", "0");
|
||||
this.dashArray = tr("dashArray", "");
|
||||
|
||||
this.deletion = null;
|
||||
if(json.deletion === true){
|
||||
json.deletion = {
|
||||
}
|
||||
}
|
||||
if(json.deletion !== undefined && json.deletion !== false){
|
||||
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`)
|
||||
}
|
||||
|
||||
this.deletion = null;
|
||||
if (json.deletion === true) {
|
||||
json.deletion = {};
|
||||
}
|
||||
if (json.deletion !== undefined && json.deletion !== false) {
|
||||
this.deletion = new DeleteConfig(json.deletion, `${context}.deletion`);
|
||||
}
|
||||
|
||||
if (json["showIf"] !== undefined) {
|
||||
throw "Invalid key on layerconfig " + this.id + ": showIf. Did you mean 'isShown' instead?";
|
||||
throw (
|
||||
"Invalid key on layerconfig " +
|
||||
this.id +
|
||||
": showIf. Did you mean 'isShown' instead?"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public CustomCodeSnippets(): string[] {
|
||||
if (this.calculatedTags === undefined) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.calculatedTags.map(code => code[1]);
|
||||
return this.calculatedTags.map((code) => code[1]);
|
||||
}
|
||||
|
||||
public AddRoamingRenderings(addAll: {
|
||||
tagRenderings: TagRenderingConfig[],
|
||||
titleIcons: TagRenderingConfig[],
|
||||
iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[]
|
||||
|
||||
tagRenderings: TagRenderingConfig[];
|
||||
titleIcons: TagRenderingConfig[];
|
||||
iconOverlays: {
|
||||
if: TagsFilter;
|
||||
then: TagRenderingConfig;
|
||||
badge: boolean;
|
||||
}[];
|
||||
}): LayerConfig {
|
||||
|
||||
let insertionPoint = this.tagRenderings.map(tr => tr.IsQuestionBoxElement()).indexOf(true)
|
||||
let insertionPoint = this.tagRenderings
|
||||
.map((tr) => tr.IsQuestionBoxElement())
|
||||
.indexOf(true);
|
||||
if (insertionPoint < 0) {
|
||||
// No 'questions' defined - we just add them all to the end
|
||||
insertionPoint = this.tagRenderings.length;
|
||||
}
|
||||
this.tagRenderings.splice(insertionPoint, 0, ...addAll.tagRenderings);
|
||||
|
||||
|
||||
this.iconOverlays.push(...addAll.iconOverlays);
|
||||
for (const icon of addAll.titleIcons) {
|
||||
this.titleIcons.splice(0, 0, icon);
|
||||
|
@ -289,40 +351,42 @@ export default class LayerConfig {
|
|||
}
|
||||
|
||||
public GetRoamingRenderings(): {
|
||||
tagRenderings: TagRenderingConfig[],
|
||||
titleIcons: TagRenderingConfig[],
|
||||
iconOverlays: { "if": TagsFilter, then: TagRenderingConfig, badge: boolean }[]
|
||||
|
||||
tagRenderings: TagRenderingConfig[];
|
||||
titleIcons: TagRenderingConfig[];
|
||||
iconOverlays: {
|
||||
if: TagsFilter;
|
||||
then: TagRenderingConfig;
|
||||
badge: boolean;
|
||||
}[];
|
||||
} {
|
||||
|
||||
const tagRenderings = this.tagRenderings.filter(tr => tr.roaming);
|
||||
const titleIcons = this.titleIcons.filter(tr => tr.roaming);
|
||||
const iconOverlays = this.iconOverlays.filter(io => io.then.roaming)
|
||||
const tagRenderings = this.tagRenderings.filter((tr) => tr.roaming);
|
||||
const titleIcons = this.titleIcons.filter((tr) => tr.roaming);
|
||||
const iconOverlays = this.iconOverlays.filter((io) => io.then.roaming);
|
||||
|
||||
return {
|
||||
tagRenderings: tagRenderings,
|
||||
titleIcons: titleIcons,
|
||||
iconOverlays: iconOverlays
|
||||
}
|
||||
|
||||
iconOverlays: iconOverlays,
|
||||
};
|
||||
}
|
||||
|
||||
public GenerateLeafletStyle(tags: UIEventSource<any>, clickable: boolean, widthHeight= "100%"):
|
||||
{
|
||||
icon:
|
||||
{
|
||||
html: BaseUIElement,
|
||||
iconSize: [number, number],
|
||||
iconAnchor: [number, number],
|
||||
popupAnchor: [number, number],
|
||||
iconUrl: string,
|
||||
className: string
|
||||
},
|
||||
color: string,
|
||||
weight: number,
|
||||
dashArray: number[]
|
||||
} {
|
||||
|
||||
public GenerateLeafletStyle(
|
||||
tags: UIEventSource<any>,
|
||||
clickable: boolean,
|
||||
widthHeight = "100%"
|
||||
): {
|
||||
icon: {
|
||||
html: BaseUIElement;
|
||||
iconSize: [number, number];
|
||||
iconAnchor: [number, number];
|
||||
popupAnchor: [number, number];
|
||||
iconUrl: string;
|
||||
className: string;
|
||||
};
|
||||
color: string;
|
||||
weight: number;
|
||||
dashArray: number[];
|
||||
} {
|
||||
function num(str, deflt = 40) {
|
||||
const n = Number(str);
|
||||
if (isNaN(n)) {
|
||||
|
@ -341,7 +405,7 @@ export default class LayerConfig {
|
|||
}
|
||||
|
||||
function render(tr: TagRenderingConfig, deflt?: string) {
|
||||
const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt);
|
||||
const str = tr?.GetRenderValue(tags.data)?.txt ?? deflt;
|
||||
return Utils.SubstituteKeys(str, tags.data).replace(/{.*}/g, "");
|
||||
}
|
||||
|
||||
|
@ -350,14 +414,16 @@ export default class LayerConfig {
|
|||
let color = render(this.color, "#00f");
|
||||
|
||||
if (color.startsWith("--")) {
|
||||
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
|
||||
color = getComputedStyle(document.body).getPropertyValue(
|
||||
"--catch-detail-color"
|
||||
);
|
||||
}
|
||||
|
||||
const weight = rendernum(this.width, 5);
|
||||
|
||||
const iconW = num(iconSize[0]);
|
||||
let iconH = num(iconSize[1]);
|
||||
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center"
|
||||
const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center";
|
||||
|
||||
let anchorW = iconW / 2;
|
||||
let anchorH = iconH / 2;
|
||||
|
@ -377,31 +443,35 @@ export default class LayerConfig {
|
|||
|
||||
const iconUrlStatic = render(this.icon);
|
||||
const self = this;
|
||||
const mappedHtml = tags.map(tgs => {
|
||||
const mappedHtml = tags.map((tgs) => {
|
||||
function genHtmlFromString(sourcePart: string): BaseUIElement {
|
||||
|
||||
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`;
|
||||
let html: BaseUIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||
let html: BaseUIElement = new FixedUiElement(
|
||||
`<img src="${sourcePart}" style="${style}" />`
|
||||
);
|
||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/);
|
||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||
html = new Combine([
|
||||
(Svg.All[match[1] + ".svg"] as string)
|
||||
.replace(/#000000/g, match[2])
|
||||
(Svg.All[match[1] + ".svg"] as string).replace(
|
||||
/#000000/g,
|
||||
match[2]
|
||||
),
|
||||
]).SetStyle(style);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
// What do you mean, 'tgs' is never read?
|
||||
// It is read implicitly in the 'render' method
|
||||
const iconUrl = render(self.icon);
|
||||
const rotation = render(self.rotation, "0deg");
|
||||
|
||||
let htmlParts: BaseUIElement[] = [];
|
||||
let sourceParts = Utils.NoNull(iconUrl.split(";").filter(prt => prt != ""));
|
||||
let sourceParts = Utils.NoNull(
|
||||
iconUrl.split(";").filter((prt) => prt != "")
|
||||
);
|
||||
for (const sourcePart of sourceParts) {
|
||||
htmlParts.push(genHtmlFromString(sourcePart))
|
||||
htmlParts.push(genHtmlFromString(sourcePart));
|
||||
}
|
||||
|
||||
let badges = [];
|
||||
|
@ -411,79 +481,88 @@ export default class LayerConfig {
|
|||
}
|
||||
if (iconOverlay.badge) {
|
||||
const badgeParts: BaseUIElement[] = [];
|
||||
const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";").filter(prt => prt != "");
|
||||
const partDefs = iconOverlay.then
|
||||
.GetRenderValue(tgs)
|
||||
.txt.split(";")
|
||||
.filter((prt) => prt != "");
|
||||
|
||||
for (const badgePartStr of partDefs) {
|
||||
badgeParts.push(genHtmlFromString(badgePartStr))
|
||||
badgeParts.push(genHtmlFromString(badgePartStr));
|
||||
}
|
||||
|
||||
const badgeCompound = new Combine(badgeParts)
|
||||
.SetStyle("display:flex;position:relative;width:100%;height:100%;");
|
||||
|
||||
badges.push(badgeCompound)
|
||||
const badgeCompound = new Combine(badgeParts).SetStyle(
|
||||
"display:flex;position:relative;width:100%;height:100%;"
|
||||
);
|
||||
|
||||
badges.push(badgeCompound);
|
||||
} else {
|
||||
htmlParts.push(genHtmlFromString(
|
||||
iconOverlay.then.GetRenderValue(tgs).txt));
|
||||
htmlParts.push(
|
||||
genHtmlFromString(iconOverlay.then.GetRenderValue(tgs).txt)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (badges.length > 0) {
|
||||
const badgesComponent = new Combine(badges)
|
||||
.SetStyle("display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;");
|
||||
htmlParts.push(badgesComponent)
|
||||
const badgesComponent = new Combine(badges).SetStyle(
|
||||
"display:flex;height:50%;width:100%;position:absolute;top:50%;left:50%;"
|
||||
);
|
||||
htmlParts.push(badgesComponent);
|
||||
}
|
||||
|
||||
if (sourceParts.length == 0) {
|
||||
iconH = 0
|
||||
iconH = 0;
|
||||
}
|
||||
try {
|
||||
|
||||
const label = self.label?.GetRenderValue(tgs)?.Subs(tgs)
|
||||
const label = self.label
|
||||
?.GetRenderValue(tgs)
|
||||
?.Subs(tgs)
|
||||
?.SetClass("block text-center")
|
||||
?.SetStyle("margin-top: " + (iconH + 2) + "px")
|
||||
?.SetStyle("margin-top: " + (iconH + 2) + "px");
|
||||
if (label !== undefined) {
|
||||
htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center"))
|
||||
htmlParts.push(
|
||||
new Combine([label]).SetClass("flex flex-col items-center")
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, tgs)
|
||||
console.error(e, tgs);
|
||||
}
|
||||
return new Combine(htmlParts);
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
icon:
|
||||
{
|
||||
html: new VariableUiElement(mappedHtml),
|
||||
iconSize: [iconW, iconH],
|
||||
iconAnchor: [anchorW, anchorH],
|
||||
popupAnchor: [0, 3 - anchorH],
|
||||
iconUrl: iconUrlStatic,
|
||||
className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable"
|
||||
},
|
||||
icon: {
|
||||
html: new VariableUiElement(mappedHtml),
|
||||
iconSize: [iconW, iconH],
|
||||
iconAnchor: [anchorW, anchorH],
|
||||
popupAnchor: [0, 3 - anchorH],
|
||||
iconUrl: iconUrlStatic,
|
||||
className: clickable
|
||||
? "leaflet-div-icon"
|
||||
: "leaflet-div-icon unclickable",
|
||||
},
|
||||
color: color,
|
||||
weight: weight,
|
||||
dashArray: dashArray
|
||||
dashArray: dashArray,
|
||||
};
|
||||
}
|
||||
|
||||
public ExtractImages(): Set<string> {
|
||||
const parts: Set<string>[] = []
|
||||
parts.push(...this.tagRenderings?.map(tr => tr.ExtractImages(false)))
|
||||
parts.push(...this.titleIcons?.map(tr => tr.ExtractImages(true)))
|
||||
parts.push(this.icon?.ExtractImages(true))
|
||||
parts.push(...this.iconOverlays?.map(overlay => overlay.then.ExtractImages(true)))
|
||||
const parts: Set<string>[] = [];
|
||||
parts.push(...this.tagRenderings?.map((tr) => tr.ExtractImages(false)));
|
||||
parts.push(...this.titleIcons?.map((tr) => tr.ExtractImages(true)));
|
||||
parts.push(this.icon?.ExtractImages(true));
|
||||
parts.push(
|
||||
...this.iconOverlays?.map((overlay) => overlay.then.ExtractImages(true))
|
||||
);
|
||||
for (const preset of this.presets) {
|
||||
parts.push(new Set<string>(preset.description?.ExtractImages(false)))
|
||||
parts.push(new Set<string>(preset.description?.ExtractImages(false)));
|
||||
}
|
||||
|
||||
const allIcons = new Set<string>();
|
||||
for (const part of parts) {
|
||||
part?.forEach(allIcons.add, allIcons)
|
||||
part?.forEach(allIcons.add, allIcons);
|
||||
}
|
||||
|
||||
return allIcons;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import {AndOrTagConfigJson} from "./TagConfigJson";
|
||||
import {DeleteConfigJson} from "./DeleteConfigJson";
|
||||
import FilterConfigJson from "./FilterConfigJson";
|
||||
|
||||
/**
|
||||
* Configuration for a single layer
|
||||
|
@ -217,6 +218,16 @@ export interface LayerConfigJson {
|
|||
* (The first sentence is until the first '.'-character in the description)
|
||||
*/
|
||||
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) [],
|
||||
|
||||
|
||||
/**
|
||||
* All the extra questions for filtering
|
||||
*/
|
||||
filter?: (FilterConfigJson) [],
|
||||
|
||||
/**
|
||||
* This block defines under what circumstances the delete dialog is shown for objects of this layer.
|
||||
* If set, a dialog is shown to the user to (soft) delete the point.
|
||||
|
|
|
@ -42,6 +42,7 @@ export default class LayoutConfig {
|
|||
public readonly enableGeolocation: boolean;
|
||||
public readonly enableBackgroundLayerSelection: boolean;
|
||||
public readonly enableShowAllQuestions: boolean;
|
||||
public readonly enableExportButton: boolean;
|
||||
public readonly customCss?: string;
|
||||
/*
|
||||
How long is the cache valid, in seconds?
|
||||
|
@ -152,6 +153,7 @@ export default class LayoutConfig {
|
|||
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
|
||||
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
|
||||
this.enableShowAllQuestions = json.enableShowAllQuestions ?? false;
|
||||
this.enableExportButton = json.enableExportButton ?? false;
|
||||
this.customCss = json.customCss;
|
||||
this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import UnitConfigJson from "./UnitConfigJson";
|
|||
* General remark: a type (string | any) indicates either a fixed or a translatable string.
|
||||
*/
|
||||
export interface LayoutConfigJson {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This is handled by defining units.
|
||||
*
|
||||
* # Rendering
|
||||
*
|
||||
* To render a value with long (human) denomination, use {canonical(key)}
|
||||
*
|
||||
* # Usage
|
||||
*
|
||||
|
@ -331,4 +336,5 @@ export interface LayoutConfigJson {
|
|||
enableGeolocation?: boolean;
|
||||
enableBackgroundLayerSelection?: boolean;
|
||||
enableShowAllQuestions?: boolean;
|
||||
enableExportButton?: boolean;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ export default class TagRenderingConfig {
|
|||
readonly key: string,
|
||||
readonly type: string,
|
||||
readonly addExtraTags: TagsFilter[];
|
||||
readonly inline: boolean,
|
||||
readonly default?: string,
|
||||
readonly helperArgs?: (string | number | boolean)[]
|
||||
};
|
||||
|
||||
readonly multiAnswer: boolean;
|
||||
|
@ -73,7 +76,9 @@ export default class TagRenderingConfig {
|
|||
type: json.freeform.type ?? "string",
|
||||
addExtraTags: json.freeform.addExtraTags?.map((tg, 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) {
|
||||
|
@ -332,20 +337,20 @@ export default class TagRenderingConfig {
|
|||
* Note: this might be hidden by conditions
|
||||
*/
|
||||
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 key in translation.translations) {
|
||||
if(!translation.translations.hasOwnProperty(key)){
|
||||
if (!translation.translations.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
const template = translation.translations[key]
|
||||
const parts = SubstitutedTranslation.ExtractSpecialComponents(template)
|
||||
const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap")
|
||||
if(hasMiniMap){
|
||||
const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap")
|
||||
if (hasMiniMap) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ export interface TagRenderingConfigJson {
|
|||
* Allow freeform text input from the user
|
||||
*/
|
||||
freeform?: {
|
||||
|
||||
/**
|
||||
* If this key is present, then 'render' is used to display the value.
|
||||
* If this is undefined, the rendering is _always_ shown
|
||||
|
@ -40,13 +41,30 @@ export interface TagRenderingConfigJson {
|
|||
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
|
||||
*/
|
||||
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.
|
||||
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
||||
**/
|
||||
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
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
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 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
|
||||
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
|
||||
--------------------
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
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 _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
|
||||
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>
|
||||
------------------
|
||||
|
||||
|
|
1045
InitUiElements.ts
|
@ -1,11 +1,12 @@
|
|||
import * as editorlayerindex from "../../assets/editor-layer-index.json"
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import * as L from "leaflet";
|
||||
import {TileLayer} from "leaflet";
|
||||
import * as X from "leaflet-providers";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import {TileLayer} from "leaflet";
|
||||
import {Utils} from "../../Utils";
|
||||
import Loc from "../../Models/Loc";
|
||||
|
||||
/**
|
||||
* Calculates which layers are available at the current location
|
||||
|
@ -24,45 +25,87 @@ export default class AvailableBaseLayers {
|
|||
false, false),
|
||||
feature: null,
|
||||
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 availableEditorLayers: UIEventSource<BaseLayer[]>;
|
||||
|
||||
constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) {
|
||||
const self = this;
|
||||
this.availableEditorLayers =
|
||||
location.map(
|
||||
(currentLocation) => {
|
||||
public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
|
||||
const source = location.map(
|
||||
(currentLocation) => {
|
||||
|
||||
if (currentLocation === undefined) {
|
||||
return AvailableBaseLayers.layerOverview;
|
||||
}
|
||||
if (currentLocation === undefined) {
|
||||
return AvailableBaseLayers.layerOverview;
|
||||
}
|
||||
|
||||
const currentLayers = self.availableEditorLayers?.data;
|
||||
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
|
||||
const currentLayers = source?.data; // A bit unorthodox - I know
|
||||
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;
|
||||
}
|
||||
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 globalLayers = [];
|
||||
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
|
||||
|
@ -140,7 +183,9 @@ export default class AvailableBaseLayers {
|
|||
min_zoom: props.min_zoom ?? 1,
|
||||
name: props.name,
|
||||
layer: leafletLayer,
|
||||
feature: layer
|
||||
feature: layer,
|
||||
isBest: props.best ?? false,
|
||||
category: props.category
|
||||
});
|
||||
}
|
||||
return layers;
|
||||
|
@ -152,15 +197,16 @@ export default class AvailableBaseLayers {
|
|||
function l(id: string, name: string): BaseLayer {
|
||||
try {
|
||||
const layer: any = () => L.tileLayer.provider(id, undefined);
|
||||
const baseLayer: BaseLayer = {
|
||||
return {
|
||||
feature: null,
|
||||
id: id,
|
||||
name: name,
|
||||
layer: layer,
|
||||
min_zoom: layer.minzoom,
|
||||
max_zoom: layer.maxzoom
|
||||
max_zoom: layer.maxzoom,
|
||||
category: "osmbasedmap",
|
||||
isBest: false
|
||||
}
|
||||
return baseLayer
|
||||
} catch (e) {
|
||||
console.error("Could not find provided layer", name, e);
|
||||
return null;
|
||||
|
|
|
@ -1,265 +1,271 @@
|
|||
import * as L from "leaflet";
|
||||
import { UIEventSource } from "../UIEventSource";
|
||||
import { Utils } from "../../Utils";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import Svg from "../../Svg";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import { VariableUiElement } from "../../UI/Base/VariableUIElement";
|
||||
import { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
import {CenterFlexedElement} from "../../UI/Base/CenterFlexedElement";
|
||||
|
||||
export default class GeoLocationHandler extends VariableUiElement {
|
||||
/**
|
||||
* Wether or not the geolocation is active, aka the user requested the current location
|
||||
* @private
|
||||
*/
|
||||
private readonly _isActive: UIEventSource<boolean>;
|
||||
/**
|
||||
* Wether or not the geolocation is active, aka the user requested the current location
|
||||
* @private
|
||||
*/
|
||||
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 }>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>
|
||||
) {
|
||||
const hasLocation = currentGPSLocation.map(
|
||||
(location) => location !== undefined
|
||||
);
|
||||
const previousLocationGrant = LocalStorageSource.Get(
|
||||
"geolocation-permissions"
|
||||
);
|
||||
const isActive = new UIEventSource<boolean>(false);
|
||||
/**
|
||||
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
|
||||
* @private
|
||||
*/
|
||||
private readonly _isLocked: UIEventSource<boolean>;
|
||||
|
||||
super(
|
||||
hasLocation.map(
|
||||
(hasLocation) => {
|
||||
if (hasLocation) {
|
||||
return new CenterFlexedElement(
|
||||
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;
|
||||
/**
|
||||
* The callback over the permission API
|
||||
* @private
|
||||
*/
|
||||
private readonly _permission: UIEventSource<string>;
|
||||
|
||||
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);
|
||||
});
|
||||
/***
|
||||
* 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>;
|
||||
|
||||
this.onClick(() => self.init(true));
|
||||
this.init(false);
|
||||
}
|
||||
|
||||
private init(askPermission: boolean) {
|
||||
const self = this;
|
||||
const map = this._leafletMap.data;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted");
|
||||
|
||||
const timeSinceRequest =
|
||||
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||
if (timeSinceRequest < 30) {
|
||||
self.MoveToCurrentLoction(16);
|
||||
}
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
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 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
|
||||
constructor(
|
||||
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>
|
||||
) {
|
||||
console.debug("Not moving to GPS-location: it is null island");
|
||||
return;
|
||||
const hasLocation = currentGPSLocation.map(
|
||||
(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
|
||||
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");
|
||||
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;
|
||||
private init(askPermission: boolean) {
|
||||
const self = this;
|
||||
if (self._isActive.data) {
|
||||
self.MoveToCurrentLoction(16);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,12 @@ export default class StrayClickHandler {
|
|||
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.bindPopup(popup);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import RegisteringFeatureSource from "./RegisteringFeatureSource";
|
|||
|
||||
export default class FeaturePipeline implements FeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> ;
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
public readonly name = "FeaturePipeline"
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
selectedElement: UIEventSource<any>) {
|
||||
|
||||
const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
|
||||
|
||||
// first we metatag, then we save to get the metatags into storage too
|
||||
// Note that we need to register before we do metatagging (as it expects the event sources)
|
||||
|
||||
|
@ -46,8 +46,11 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
const geojsonSources: FeatureSource [] = GeoJsonSource
|
||||
.ConstructMultiSource(flayers.data, locationControl)
|
||||
.map(geojsonSource => {
|
||||
let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource));
|
||||
if(!geojsonSource.isOsmCache){
|
||||
let source = new RegisteringFeatureSource(
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
geojsonSource
|
||||
));
|
||||
if (!geojsonSource.isOsmCache) {
|
||||
source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features);
|
||||
}
|
||||
return source
|
||||
|
|
|
@ -1,9 +1,45 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default interface FeatureSource {
|
||||
features: UIEventSource<{feature: any, freshness: Date}[]>;
|
||||
features: UIEventSource<{ feature: any, freshness: Date }[]>;
|
||||
/**
|
||||
* Mainly used for debuging
|
||||
*/
|
||||
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}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
|
||||
let freshness: Date = time;
|
||||
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})
|
||||
|
|
|
@ -6,11 +6,14 @@ export class GeoOperations {
|
|||
return turf.area(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GeoJSon feature to a point feature
|
||||
* @param feature
|
||||
*/
|
||||
static centerpoint(feature: any) {
|
||||
const newFeature = turf.center(feature);
|
||||
newFeature.properties = feature.properties;
|
||||
newFeature.id = feature.id;
|
||||
|
||||
return newFeature;
|
||||
}
|
||||
|
||||
|
@ -273,6 +276,14 @@ export class GeoOperations {
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way, point: [number, number]){
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,31 +6,38 @@ import Constants from "../../Models/Constants";
|
|||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {TagsFilter} from "../Tags/TagsFilter";
|
||||
import {Tag} from "../Tags/Tag";
|
||||
import {OsmConnection} from "./OsmConnection";
|
||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||
|
||||
/**
|
||||
* Handles all changes made to OSM.
|
||||
* 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"
|
||||
/**
|
||||
* The newly created points, as a FeatureSource
|
||||
*/
|
||||
public features = new UIEventSource<{feature: any, freshness: Date}[]>([]);
|
||||
|
||||
private static _nextId = -1; // Newly assigned ID's are negative
|
||||
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||
/**
|
||||
* All the pending changes
|
||||
*/
|
||||
public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =
|
||||
new UIEventSource<{elementId: string; key: string; value: string}[]>([]);
|
||||
public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", [])
|
||||
|
||||
/**
|
||||
* All the pending new objects to upload
|
||||
*/
|
||||
private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", [])
|
||||
|
||||
private readonly isUploading = new UIEventSource(false);
|
||||
|
||||
/**
|
||||
* 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 value = kv.v;
|
||||
if (key === undefined || key === null) {
|
||||
|
@ -49,8 +56,7 @@ export class Changes implements FeatureSource{
|
|||
return {k: key.trim(), v: value.trim()};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
addTag(elementId: string, tagsFilter: TagsFilter,
|
||||
tags?: UIEventSource<any>) {
|
||||
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
|
||||
|
@ -59,7 +65,7 @@ export class Changes implements FeatureSource{
|
|||
if (changes.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const change of changes) {
|
||||
if (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.
|
||||
* Triggered by the 'PendingChangeUploader'-actor in Actors
|
||||
*/
|
||||
public flushChanges(flushreason: string = undefined){
|
||||
if(this.pending.data.length === 0){
|
||||
public flushChanges(flushreason: string = undefined) {
|
||||
if (this.pending.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
if(flushreason !== undefined){
|
||||
if (flushreason !== undefined) {
|
||||
console.log(flushreason)
|
||||
}
|
||||
this.uploadAll([], this.pending.data);
|
||||
this.pending.setData([]);
|
||||
this.uploadAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new node element at the given lat/long.
|
||||
* An internal OsmObject is created to upload later on, a geojson represention is returned.
|
||||
|
@ -93,12 +99,12 @@ export class Changes implements FeatureSource{
|
|||
*/
|
||||
public createElement(basicTags: Tag[], lat: number, lon: number) {
|
||||
console.log("Creating a new element with ", basicTags)
|
||||
const osmNode = new OsmNode(Changes._nextId);
|
||||
const newId = Changes._nextId;
|
||||
Changes._nextId--;
|
||||
|
||||
const id = "node/" + osmNode.id;
|
||||
osmNode.lat = lat;
|
||||
osmNode.lon = lon;
|
||||
const id = "node/" + newId;
|
||||
|
||||
|
||||
const properties = {id: id};
|
||||
|
||||
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
|
||||
const changes = [];
|
||||
for (const kv of basicTags) {
|
||||
properties[kv.key] = kv.value;
|
||||
if (typeof kv.value !== "string") {
|
||||
throw "Invalid value: don't use a regex in a preset"
|
||||
}
|
||||
properties[kv.key] = kv.value;
|
||||
changes.push({elementId: id, key: kv.key, value: kv.value})
|
||||
}
|
||||
|
||||
|
||||
console.log("New feature added and pinged")
|
||||
this.features.data.push({feature:geojson, freshness: new Date()});
|
||||
this.features.data.push({feature: geojson, freshness: new Date()});
|
||||
this.features.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;
|
||||
}
|
||||
|
||||
private uploadChangesWithLatestVersions(
|
||||
knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
|
||||
knownElements: OsmObject[]) {
|
||||
const knownById = new Map<string, OsmObject>();
|
||||
|
||||
knownElements.forEach(knownElement => {
|
||||
knownById.set(knownElement.type + "/" + knownElement.id, knownElement)
|
||||
})
|
||||
|
||||
|
||||
|
||||
const newElements: OsmNode [] = this.newObjects.data.map(spec => {
|
||||
const newElement = new OsmNode(spec.id);
|
||||
newElement.lat = spec.lat;
|
||||
newElement.lon = spec.lon;
|
||||
return newElement
|
||||
})
|
||||
|
||||
|
||||
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
|
||||
// We apply the changes on them
|
||||
for (const change of pending) {
|
||||
for (const change of this.pending.data) {
|
||||
if (parseInt(change.elementId.split("/")[1]) < 0) {
|
||||
// This is a new element - we should apply this on one of the new elements
|
||||
for (const newElement of newElements) {
|
||||
|
@ -168,9 +188,17 @@ export class Changes implements FeatureSource{
|
|||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
const self = this;
|
||||
|
||||
if (this.isUploading.data) {
|
||||
return;
|
||||
}
|
||||
this.isUploading.setData(true)
|
||||
|
||||
console.log("Beginning upload...");
|
||||
// At last, we build the changeset and upload
|
||||
|
@ -213,17 +241,22 @@ export class Changes implements FeatureSource{
|
|||
changes += "</osmChange>";
|
||||
|
||||
return changes;
|
||||
});
|
||||
},
|
||||
() => {
|
||||
console.log("Upload successfull!")
|
||||
self.newObjects.setData([])
|
||||
self.pending.setData([]);
|
||||
self.isUploading.setData(false)
|
||||
},
|
||||
() => self.isUploading.setData(false)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
private uploadAll(
|
||||
newElements: OsmObject[],
|
||||
pending: { elementId: string; key: string; value: string }[]
|
||||
) {
|
||||
private uploadAll() {
|
||||
const self = this;
|
||||
|
||||
|
||||
const pending = this.pending.data;
|
||||
let neededIds: string[] = [];
|
||||
for (const change of pending) {
|
||||
const id = change.elementId;
|
||||
|
@ -236,8 +269,7 @@ export class Changes implements FeatureSource{
|
|||
|
||||
neededIds = Utils.Dedup(neededIds);
|
||||
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
|
||||
console.log("KnownElements:", knownElements)
|
||||
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
|
||||
self.uploadChangesWithLatestVersions(knownElements)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
// @ts-ignore
|
||||
for (const node of nodes) {
|
||||
|
@ -69,7 +69,9 @@ export class ChangesetHandler {
|
|||
public UploadChangeset(
|
||||
layout: LayoutConfig,
|
||||
allElements: ElementStorage,
|
||||
generateChangeXML: (csid: string) => string) {
|
||||
generateChangeXML: (csid: string) => string,
|
||||
whenDone: (csId: string) => void,
|
||||
onFail: () => void) {
|
||||
|
||||
if (this.userDetails.data.csCount == 0) {
|
||||
// The user became a contributor!
|
||||
|
@ -80,6 +82,7 @@ export class ChangesetHandler {
|
|||
if (this._dryRun) {
|
||||
const changesetXML = generateChangeXML("123456");
|
||||
console.log(changesetXML);
|
||||
whenDone("123456")
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -93,12 +96,14 @@ export class ChangesetHandler {
|
|||
console.log(changeset);
|
||||
self.AddChange(csId, changeset,
|
||||
allElements,
|
||||
() => {
|
||||
},
|
||||
whenDone,
|
||||
(e) => {
|
||||
console.error("UPLOADING FAILED!", e)
|
||||
onFail()
|
||||
}
|
||||
)
|
||||
}, {
|
||||
onFail: onFail
|
||||
})
|
||||
} else {
|
||||
// There still exists an open changeset (or at least we hope so)
|
||||
|
@ -107,15 +112,13 @@ export class ChangesetHandler {
|
|||
csId,
|
||||
generateChangeXML(csId),
|
||||
allElements,
|
||||
() => {
|
||||
},
|
||||
whenDone,
|
||||
(e) => {
|
||||
console.warn("Could not upload, changeset is probably closed: ", e);
|
||||
// Mark the CS as closed...
|
||||
this.currentChangeset.setData("");
|
||||
// ... 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;
|
||||
this.OpenChangeset(layout, (csId: string) => {
|
||||
|
||||
// The cs is open - let us actually upload!
|
||||
const changes = generateChangeXML(csId)
|
||||
// The cs is open - let us actually upload!
|
||||
const changes = generateChangeXML(csId)
|
||||
|
||||
self.AddChange(csId, changes, allElements, (csId) => {
|
||||
console.log("Successfully deleted ", object.id)
|
||||
self.CloseChangeset(csId, continuation)
|
||||
}, (csId) => {
|
||||
alert("Deletion failed... Should not happend")
|
||||
// FAILED
|
||||
self.CloseChangeset(csId, continuation)
|
||||
})
|
||||
}, true, reason)
|
||||
self.AddChange(csId, changes, allElements, (csId) => {
|
||||
console.log("Successfully deleted ", object.id)
|
||||
self.CloseChangeset(csId, continuation)
|
||||
}, (csId) => {
|
||||
alert("Deletion failed... Should not happend")
|
||||
// FAILED
|
||||
self.CloseChangeset(csId, continuation)
|
||||
})
|
||||
}, {
|
||||
isDeletionCS: true,
|
||||
deletionReason: reason
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
|
||||
|
@ -204,15 +211,20 @@ export class ChangesetHandler {
|
|||
private OpenChangeset(
|
||||
layout: LayoutConfig,
|
||||
continuation: (changesetId: string) => void,
|
||||
isDeletionCS: boolean = false,
|
||||
deletionReason: string = undefined) {
|
||||
|
||||
options?: {
|
||||
isDeletionCS?: boolean,
|
||||
deletionReason?: string,
|
||||
onFail?: () => void
|
||||
}
|
||||
) {
|
||||
options = options ?? {}
|
||||
options.isDeletionCS = options.isDeletionCS ?? false
|
||||
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
|
||||
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}`
|
||||
if (deletionReason) {
|
||||
comment += ": " + deletionReason;
|
||||
if (options.deletionReason) {
|
||||
comment += ": " + options.deletionReason;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,7 +233,7 @@ export class ChangesetHandler {
|
|||
const metadata = [
|
||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||
["comment", comment],
|
||||
["deletion", isDeletionCS ? "yes" : undefined],
|
||||
["deletion", options.isDeletionCS ? "yes" : undefined],
|
||||
["theme", layout.id],
|
||||
["language", Locale.language.data],
|
||||
["host", window.location.host],
|
||||
|
@ -244,7 +256,9 @@ export class ChangesetHandler {
|
|||
}, function (err, response) {
|
||||
if (response === undefined) {
|
||||
console.log("err", err);
|
||||
alert("Could not upload change (opening failed). Please file a bug report")
|
||||
if(options.onFail){
|
||||
options.onFail()
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
continuation(response);
|
||||
|
@ -265,7 +279,7 @@ export class ChangesetHandler {
|
|||
private AddChange(changesetId: string,
|
||||
changesetXML: string,
|
||||
allElements: ElementStorage,
|
||||
continuation: ((changesetId: string, idMapping: any) => void),
|
||||
continuation: ((changesetId: string) => void),
|
||||
onFail: ((changesetId: string, reason: string) => void) = undefined) {
|
||||
this.auth.xhr({
|
||||
method: 'POST',
|
||||
|
@ -280,9 +294,9 @@ export class ChangesetHandler {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements);
|
||||
ChangesetHandler.parseUploadChangesetResponse(response, allElements);
|
||||
console.log("Uploaded changeset ", changesetId);
|
||||
continuation(changesetId, mapping);
|
||||
continuation(changesetId);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class UserDetails {
|
|||
|
||||
export class OsmConnection {
|
||||
|
||||
public static readonly _oauth_configs = {
|
||||
public static readonly oauth_configs = {
|
||||
"osm": {
|
||||
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
|
||||
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
|
||||
|
@ -47,6 +47,7 @@ export class OsmConnection {
|
|||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public isLoggedIn: UIEventSource<boolean>
|
||||
private fakeUser: boolean;
|
||||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
|
@ -59,20 +60,31 @@ export class OsmConnection {
|
|||
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
|
||||
layoutName: string,
|
||||
singlePage: boolean = true,
|
||||
osmConfiguration: "osm" | "osm-test" = 'osm'
|
||||
) {
|
||||
this.fakeUser = fakeUser;
|
||||
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)
|
||||
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
|
||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
|
||||
|
||||
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;
|
||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
|
||||
|
@ -110,8 +122,10 @@ export class OsmConnection {
|
|||
public UploadChangeset(
|
||||
layout: LayoutConfig,
|
||||
allElements: ElementStorage,
|
||||
generateChangeXML: (csid: string) => string) {
|
||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
|
||||
generateChangeXML: (csid: string) => string,
|
||||
whenDone: (csId: string) => void,
|
||||
onFail: () => {}) {
|
||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
|
||||
}
|
||||
|
||||
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
|
@ -136,6 +150,10 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public AttemptLogin() {
|
||||
if(this.fakeUser){
|
||||
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
console.log("Trying to log in...");
|
||||
this.updateAuthObject();
|
||||
|
|
|
@ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource";
|
|||
|
||||
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 objectCache = new Map<string, UIEventSource<OsmObject>>();
|
||||
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> {
|
||||
let src : UIEventSource<OsmObject>;
|
||||
let src: UIEventSource<OsmObject>;
|
||||
if (OsmObject.objectCache.has(id)) {
|
||||
src = OsmObject.objectCache.get(id)
|
||||
if(forceRefresh){
|
||||
if (forceRefresh) {
|
||||
src.setData(undefined)
|
||||
}else{
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
src = new UIEventSource<OsmObject>(undefined)
|
||||
}
|
||||
const splitted = id.split("/");
|
||||
|
@ -157,7 +158,7 @@ export abstract class OsmObject {
|
|||
const minlat = bounds[1][0]
|
||||
const maxlat = bounds[0][0];
|
||||
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 objects = OsmObject.ParseObjects(elements)
|
||||
callback(objects);
|
||||
|
@ -291,6 +292,7 @@ export abstract class OsmObject {
|
|||
|
||||
self.LoadData(element)
|
||||
self.SaveExtraData(element, nodes);
|
||||
|
||||
const meta = {
|
||||
"_last_edit:contributor": element.user,
|
||||
"_last_edit:contributor:uid": element.uid,
|
||||
|
@ -299,6 +301,11 @@ export abstract class OsmObject {
|
|||
"_version_number": element.version
|
||||
}
|
||||
|
||||
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
self.tags["_backend"] = OsmObject.backendURL
|
||||
meta["_backend"] = OsmObject.backendURL;
|
||||
}
|
||||
|
||||
continuation(self, meta);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -83,7 +83,8 @@ export default class SimpleMetaTagger {
|
|||
|
||||
},
|
||||
(feature => {
|
||||
const units = State.state.layoutToUse.data.units ?? [];
|
||||
const units = State.state?.layoutToUse?.data?.units ?? [];
|
||||
let rewritten = false;
|
||||
for (const key in feature.properties) {
|
||||
if (!feature.properties.hasOwnProperty(key)) {
|
||||
continue;
|
||||
|
@ -95,16 +96,23 @@ export default class SimpleMetaTagger {
|
|||
const value = feature.properties[key]
|
||||
const [, denomination] = unit.findDenomination(value)
|
||||
let canonical = denomination?.canonicalValue(value) ?? undefined;
|
||||
console.log("Rewritten ", key, " from", value, "into", canonical)
|
||||
if(canonical === undefined && !unit.eraseInvalid) {
|
||||
if(canonical === value){
|
||||
break;
|
||||
}
|
||||
|
||||
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
|
||||
if (canonical === undefined && !unit.eraseInvalid) {
|
||||
break;
|
||||
}
|
||||
|
||||
feature.properties[key] = canonical;
|
||||
rewritten = true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(rewritten){
|
||||
State.state.allElements.getEventSourceById(feature.id).ping();
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource";
|
|||
* UIEventsource-wrapper around localStorage
|
||||
*/
|
||||
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> {
|
||||
try {
|
||||
|
|
|
@ -7,4 +7,6 @@ export default interface BaseLayer {
|
|||
max_zoom: number,
|
||||
min_zoom: number;
|
||||
feature: any,
|
||||
isBest?: boolean,
|
||||
category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
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
|
||||
public static userJourney = {
|
||||
|
|
8
Models/TileRange.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface TileRange {
|
||||
xstart: number,
|
||||
ystart: number,
|
||||
xend: number,
|
||||
yend: number,
|
||||
total: number,
|
||||
zoomlevel: number
|
||||
}
|
773
State.ts
|
@ -1,13 +1,13 @@
|
|||
import { Utils } from "./Utils";
|
||||
import { ElementStorage } from "./Logic/ElementStorage";
|
||||
import { Changes } from "./Logic/Osm/Changes";
|
||||
import { OsmConnection } from "./Logic/Osm/OsmConnection";
|
||||
import {Utils} from "./Utils";
|
||||
import {ElementStorage} from "./Logic/ElementStorage";
|
||||
import {Changes} from "./Logic/Osm/Changes";
|
||||
import {OsmConnection} from "./Logic/Osm/OsmConnection";
|
||||
import Locale from "./UI/i18n/Locale";
|
||||
import { UIEventSource } from "./Logic/UIEventSource";
|
||||
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource";
|
||||
import { QueryParameters } from "./Logic/Web/QueryParameters";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
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 BaseLayer from "./Models/BaseLayer";
|
||||
import Loc from "./Models/Loc";
|
||||
|
@ -17,410 +17,423 @@ import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
|
|||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import { Relation } from "./Logic/Osm/ExtractRelations";
|
||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
*/
|
||||
|
||||
export default class State {
|
||||
// The singleton of the global state
|
||||
public static state: State;
|
||||
// The singleton of the global state
|
||||
public static state: State;
|
||||
|
||||
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined);
|
||||
public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined);
|
||||
|
||||
/**
|
||||
/**
|
||||
The mapping from id -> UIEventSource<properties>
|
||||
*/
|
||||
public allElements: ElementStorage;
|
||||
/**
|
||||
public allElements: ElementStorage;
|
||||
/**
|
||||
THe change handler
|
||||
*/
|
||||
public changes: Changes;
|
||||
/**
|
||||
public changes: Changes;
|
||||
/**
|
||||
The leaflet instance of the big basemap
|
||||
*/
|
||||
public leafletMap = new UIEventSource<L.Map>(undefined);
|
||||
/**
|
||||
* Background layer id
|
||||
*/
|
||||
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
|
||||
/**
|
||||
public leafletMap = new UIEventSource<L.Map>(undefined);
|
||||
/**
|
||||
* Background layer id
|
||||
*/
|
||||
public availableBackgroundLayers: UIEventSource<BaseLayer[]>;
|
||||
/**
|
||||
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<
|
||||
{
|
||||
readonly isDisplayed: UIEventSource<boolean>;
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]
|
||||
> = new UIEventSource<
|
||||
{
|
||||
readonly isDisplayed: UIEventSource<boolean>;
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]
|
||||
>([]);
|
||||
public filteredLayers: UIEventSource<{
|
||||
readonly isDisplayed: UIEventSource<boolean>;
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]> = new UIEventSource<{
|
||||
readonly isDisplayed: UIEventSource<boolean>;
|
||||
readonly layerDef: LayerConfig;
|
||||
}[]>([]);
|
||||
|
||||
/**
|
||||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
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,
|
||||
public readonly selectedElement = new UIEventSource<any>(
|
||||
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.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
|
||||
"Selected element"
|
||||
);
|
||||
|
||||
this.allElements = new ElementStorage();
|
||||
this.changes = new Changes();
|
||||
this.osmApiFeatureSource = new OsmApiFeatureSource();
|
||||
/**
|
||||
* 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");
|
||||
|
||||
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(
|
||||
this.osmConnection
|
||||
).installedThemes;
|
||||
public featurePipeline: FeaturePipeline;
|
||||
|
||||
// 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(";")) ?? [],
|
||||
|
||||
/**
|
||||
* 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)),
|
||||
[],
|
||||
(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);
|
||||
}
|
||||
(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,
|
||||
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
|
@ -5,6 +5,7 @@ import Loc from "../../Models/Loc";
|
|||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {Map} from "leaflet";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class Minimap extends BaseUIElement {
|
||||
|
||||
|
@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement {
|
|||
private readonly _location: UIEventSource<Loc>;
|
||||
private _isInited = false;
|
||||
private _allowMoving: boolean;
|
||||
private readonly _leafletoptions: any;
|
||||
|
||||
constructor(options?: {
|
||||
background?: UIEventSource<BaseLayer>,
|
||||
location?: UIEventSource<Loc>,
|
||||
allowMoving?: boolean
|
||||
allowMoving?: boolean,
|
||||
leafletOptions?: any
|
||||
}
|
||||
) {
|
||||
super()
|
||||
|
@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement {
|
|||
this._location = options?.location ?? new UIEventSource<Loc>(undefined)
|
||||
this._id = "minimap" + Minimap._nextId;
|
||||
this._allowMoving = options.allowMoving ?? true;
|
||||
this._leafletoptions = options.leafletOptions ?? {}
|
||||
Minimap._nextId++
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const div = document.createElement("div")
|
||||
div.id = this._id;
|
||||
|
@ -44,7 +48,6 @@ export default class Minimap extends BaseUIElement {
|
|||
const self = this;
|
||||
// @ts-ignore
|
||||
const resizeObserver = new ResizeObserver(_ => {
|
||||
console.log("Change in size detected!")
|
||||
self.InitMap();
|
||||
self.leafletMap?.data?.invalidateSize()
|
||||
});
|
||||
|
@ -72,8 +75,8 @@ export default class Minimap extends BaseUIElement {
|
|||
const location = this._location;
|
||||
|
||||
let currentLayer = this._background.data.layer()
|
||||
const map = L.map(this._id, {
|
||||
center: [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
||||
const options = {
|
||||
center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0],
|
||||
zoom: location.data?.zoom ?? 2,
|
||||
layers: [currentLayer],
|
||||
zoomControl: false,
|
||||
|
@ -82,8 +85,14 @@ export default class Minimap extends BaseUIElement {
|
|||
scrollWheelZoom: this._allowMoving,
|
||||
doubleClickZoom: 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(
|
||||
[[-100, -200], [100, 200]]
|
||||
|
|
|
@ -3,6 +3,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import Loc from "../../Models/Loc";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class Basemap {
|
||||
|
||||
|
@ -35,9 +36,8 @@ export class Basemap {
|
|||
);
|
||||
|
||||
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;
|
||||
|
||||
currentLayer.addCallbackAndRun(layer => {
|
||||
|
@ -77,6 +77,7 @@ export class Basemap {
|
|||
lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
});
|
||||
|
||||
extraAttribution.AttachTo('leaflet-attribution')
|
||||
|
||||
}
|
||||
|
||||
|
|
21
UI/BigComponents/ExportDataButton.ts
Normal 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")])
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
/**
|
||||
* Shows the filter
|
||||
|
@ -26,14 +27,63 @@ export default class FilterView extends ScrollableFullScreen {
|
|||
}
|
||||
|
||||
private static Generatecontent(): BaseUIElement {
|
||||
let filterPanel: BaseUIElement = new FixedUiElement("more stuff");
|
||||
let filterPanel: BaseUIElement = new FixedUiElement("");
|
||||
|
||||
if (State.state.filteredLayers.data.length > 1) {
|
||||
let layers = State.state.filteredLayers;
|
||||
console.log(layers);
|
||||
filterPanel = new Combine(["layerssss", "<br/>", filterPanel]);
|
||||
}
|
||||
let activeLayers = State.state.filteredLayers;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ import State from "../../State";
|
|||
import BackgroundSelector from "./BackgroundSelector";
|
||||
import LayerSelection from "./LayerSelection";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import {ExportDataButton} from "./ExportDataButton";
|
||||
|
||||
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||
|
||||
|
@ -14,27 +15,34 @@ export default class LayerControlPanel extends ScrollableFullScreen {
|
|||
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")
|
||||
}
|
||||
|
||||
private static GeneratePanel() : BaseUIElement {
|
||||
let layerControlPanel: BaseUIElement = new FixedUiElement("");
|
||||
private static GeneratePanel(): BaseUIElement {
|
||||
const elements: BaseUIElement[] = []
|
||||
|
||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||
layerControlPanel = new BackgroundSelector();
|
||||
layerControlPanel.SetStyle("margin:1em");
|
||||
layerControlPanel.onClick(() => {
|
||||
const backgroundSelector = new BackgroundSelector();
|
||||
backgroundSelector.SetStyle("margin:1em");
|
||||
backgroundSelector.onClick(() => {
|
||||
});
|
||||
elements.push(backgroundSelector)
|
||||
}
|
||||
|
||||
if (State.state.filteredLayers.data.length > 1) {
|
||||
const layerSelection = new LayerSelection(State.state.filteredLayers);
|
||||
layerSelection.onClick(() => {
|
||||
});
|
||||
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
||||
}
|
||||
elements.push(new Toggle(
|
||||
new LayerSelection(State.state.filteredLayers),
|
||||
undefined,
|
||||
State.state.filteredLayers.map(layers => layers.length > 1)
|
||||
))
|
||||
|
||||
return layerControlPanel;
|
||||
elements.push(new Toggle(
|
||||
new ExportDataButton(),
|
||||
undefined,
|
||||
State.state.featureSwitchEnableExport
|
||||
))
|
||||
|
||||
return new Combine(elements).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
}
|
|
@ -74,7 +74,6 @@ export default class LayerSelection extends Combine {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
super(checkboxes)
|
||||
this.SetStyle("display:flex;flex-direction:column;")
|
||||
|
||||
|
|
|
@ -62,6 +62,10 @@ export default class MoreScreen extends Combine {
|
|||
let officialThemes = AllKnownLayouts.layoutsList
|
||||
|
||||
let buttons = officialThemes.map((layout) => {
|
||||
if(layout === undefined){
|
||||
console.trace("Layout is undefined")
|
||||
return undefined
|
||||
}
|
||||
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
|
||||
if(layout.id === personal.id){
|
||||
return new VariableUiElement(
|
||||
|
|
|
@ -16,6 +16,10 @@ import {VariableUiElement} from "../Base/VariableUIElement";
|
|||
import Toggle from "../Input/Toggle";
|
||||
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||
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:
|
||||
|
@ -25,14 +29,18 @@ import {Translation} from "../i18n/Translation";
|
|||
* - A 'read your unread messages before adding a point'
|
||||
*/
|
||||
|
||||
/*private*/
|
||||
interface PresetInfo {
|
||||
description: string | Translation,
|
||||
name: string | BaseUIElement,
|
||||
icon: BaseUIElement,
|
||||
icon: () => BaseUIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: {
|
||||
layerDef: LayerConfig,
|
||||
isDisplayed: UIEventSource<boolean>
|
||||
},
|
||||
preciseInput?: {
|
||||
preferredBackground?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,18 +56,16 @@ export default class SimpleAddUI extends Toggle {
|
|||
new SubtleButton(Svg.envelope_ui(),
|
||||
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
|
||||
]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
|
||||
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
|
||||
|
||||
function createNewPoint(tags: any[]){
|
||||
const loc = State.state.LastClickLocation.data;
|
||||
let feature = State.state.changes.createElement(tags, loc.lat, loc.lon);
|
||||
|
||||
function createNewPoint(tags: any[], location: { lat: number, lon: number }) {
|
||||
let feature = State.state.changes.createElement(tags, location.lat, location.lon);
|
||||
State.state.selectedElement.setData(feature);
|
||||
}
|
||||
|
||||
|
||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
|
||||
|
||||
const addUi = new VariableUiElement(
|
||||
|
@ -68,8 +74,8 @@ export default class SimpleAddUI extends Toggle {
|
|||
return presetsOverview
|
||||
}
|
||||
return SimpleAddUI.CreateConfirmButton(preset,
|
||||
tags => {
|
||||
createNewPoint(tags)
|
||||
(tags, location) => {
|
||||
createNewPoint(tags, location)
|
||||
selectedPreset.setData(undefined)
|
||||
}, () => {
|
||||
selectedPreset.setData(undefined)
|
||||
|
@ -86,7 +92,7 @@ export default class SimpleAddUI extends Toggle {
|
|||
addUi,
|
||||
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)
|
||||
),
|
||||
readYourMessages,
|
||||
|
@ -103,22 +109,48 @@ export default class SimpleAddUI extends Toggle {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private static CreateConfirmButton(preset: PresetInfo,
|
||||
confirm: (tags: any[]) => void,
|
||||
confirm: (tags: any[], location: { lat: number, lon: number }) => void,
|
||||
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([
|
||||
Translations.t.general.add.addNew.Subs({category: preset.name}),
|
||||
Translations.t.general.add.warnVisibleForEveryone.Clone().SetClass("alert")
|
||||
]).SetClass("flex flex-col")
|
||||
).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(
|
||||
Svg.layers_ui(),
|
||||
new Combine([
|
||||
|
@ -128,9 +160,9 @@ export default class SimpleAddUI extends Toggle {
|
|||
Translations.t.general.add.openLayerControl
|
||||
])
|
||||
)
|
||||
|
||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||
|
||||
|
||||
.onClick(() => State.state.layerControlIsOpened.setData(true))
|
||||
|
||||
const openLayerOrConfirm = new Toggle(
|
||||
confirmButton,
|
||||
openLayerControl,
|
||||
|
@ -140,12 +172,12 @@ export default class SimpleAddUI extends Toggle {
|
|||
|
||||
const cancelButton = new SubtleButton(Svg.close_ui(),
|
||||
Translations.t.general.cancel
|
||||
).onClick(cancel )
|
||||
).onClick(cancel)
|
||||
|
||||
return new Combine([
|
||||
Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined ,
|
||||
State.state.osmConnection.userDetails.data.dryRun ?
|
||||
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
|
||||
openLayerOrConfirm,
|
||||
cancelButton,
|
||||
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(
|
||||
preset.icon,
|
||||
preset.icon(),
|
||||
new Combine([
|
||||
Translations.t.general.add.addNew.Subs({
|
||||
category: preset.name
|
||||
|
@ -194,29 +226,30 @@ export default class SimpleAddUI extends Toggle {
|
|||
]).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 {
|
||||
const allButtons = [];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: BaseUIElement = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||
let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html
|
||||
.SetClass("w-12 h-12 block relative");
|
||||
const presetInfo: PresetInfo = {
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
name: preset.title,
|
||||
description: preset.description,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
preciseInput: preset.preciseInput
|
||||
}
|
||||
|
||||
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
|
||||
|
|
|
@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement<string> {
|
|||
})
|
||||
|
||||
this.RegisterTriggers(element)
|
||||
element.style.overflow = "hidden"
|
||||
|
||||
return element;
|
||||
}
|
||||
|
|
35
UI/Input/InputElementWrapper.ts
Normal 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
|
@ -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
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -103,7 +103,7 @@ export class RadioButton<T> extends InputElement<T> {
|
|||
const block = document.createElement("div")
|
||||
block.appendChild(input)
|
||||
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)
|
||||
|
||||
form.appendChild(block)
|
||||
|
|
|
@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
|
|||
this.SetClass("form-text-field")
|
||||
let inputEl: HTMLElement
|
||||
if (options.htmlType === "area") {
|
||||
this.SetClass("w-full box-border max-w-full")
|
||||
const el = document.createElement("textarea")
|
||||
el.placeholder = placeholder
|
||||
el.rows = options.textAreaRows
|
||||
el.cols = 50
|
||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
||||
inputEl = el;
|
||||
} else {
|
||||
const el = document.createElement("input")
|
||||
|
|
|
@ -13,6 +13,8 @@ import {Utils} from "../../Utils";
|
|||
import Loc from "../../Models/Loc";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import LengthInput from "./LengthInput";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
|
||||
interface TextFieldDef {
|
||||
name: string,
|
||||
|
@ -21,14 +23,16 @@ interface TextFieldDef {
|
|||
reformat?: ((s: string, country?: () => string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||
location: [number, number],
|
||||
mapBackgroundLayer?: UIEventSource<any>
|
||||
mapBackgroundLayer?: UIEventSource<any>,
|
||||
args: (string | number | boolean)[]
|
||||
feature?: any
|
||||
}) => InputElement<string>,
|
||||
|
||||
inputmode?: string
|
||||
}
|
||||
|
||||
export default class ValidatedTextField {
|
||||
|
||||
public static bestLayerAt: (location: UIEventSource<Loc>, preferences: UIEventSource<string[]>) => any
|
||||
|
||||
public static tpList: TextFieldDef[] = [
|
||||
ValidatedTextField.tp(
|
||||
|
@ -63,6 +67,83 @@ export default class ValidatedTextField {
|
|||
return [year, month, day].join('-');
|
||||
},
|
||||
(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(
|
||||
"wikidata",
|
||||
"A wikidata identifier, e.g. Q42",
|
||||
|
@ -113,22 +194,6 @@ export default class ValidatedTextField {
|
|||
undefined,
|
||||
undefined,
|
||||
"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(
|
||||
"float",
|
||||
"A decimal",
|
||||
|
@ -222,6 +287,7 @@ export default class ValidatedTextField {
|
|||
* {string (typename) --> TextFieldDef}
|
||||
*/
|
||||
public static AllTypes = ValidatedTextField.allTypesDict();
|
||||
|
||||
public static InputForType(type: string, options?: {
|
||||
placeholder?: string | BaseUIElement,
|
||||
value?: UIEventSource<string>,
|
||||
|
@ -233,7 +299,9 @@ export default class ValidatedTextField {
|
|||
country?: () => string,
|
||||
location?: [number /*lat*/, number /*lon*/],
|
||||
mapBackgroundLayer?: UIEventSource<any>,
|
||||
unit?: Unit
|
||||
unit?: Unit,
|
||||
args?: (string | number | boolean)[] // Extra arguments for the inputHelper,
|
||||
feature?: any
|
||||
}): InputElement<string> {
|
||||
options = options ?? {};
|
||||
options.placeholder = options.placeholder ?? type;
|
||||
|
@ -247,7 +315,7 @@ export default class ValidatedTextField {
|
|||
if (str === undefined) {
|
||||
return false;
|
||||
}
|
||||
if(options.unit) {
|
||||
if (options.unit) {
|
||||
str = options.unit.stripUnitParts(str)
|
||||
}
|
||||
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.
|
||||
// This implies:
|
||||
// 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.SetStyle("width: min-content")
|
||||
unitDropDown.SetClass("w-min")
|
||||
|
||||
input = new CombinedInputElement(
|
||||
input,
|
||||
unitDropDown,
|
||||
// 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) => {
|
||||
// Take the value from OSM and feed it into the textfield and the dropdown
|
||||
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
|
||||
return [undefined, undefined]
|
||||
}
|
||||
const [strippedText, denom] = withDenom
|
||||
if(strippedText === undefined){
|
||||
if (strippedText === undefined) {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
return [strippedText, denom]
|
||||
|
@ -306,18 +373,20 @@ export default class ValidatedTextField {
|
|||
).SetClass("flex")
|
||||
}
|
||||
if (tp.inputHelper) {
|
||||
const helper = tp.inputHelper(input.GetValue(), {
|
||||
const helper = tp.inputHelper(input.GetValue(), {
|
||||
location: options.location,
|
||||
mapBackgroundLayer: options.mapBackgroundLayer
|
||||
|
||||
mapBackgroundLayer: options.mapBackgroundLayer,
|
||||
args: options.args,
|
||||
feature: options.feature
|
||||
})
|
||||
input = new CombinedInputElement(input, helper,
|
||||
(a, _) => a, // We can ignore b, as they are linked earlier
|
||||
a => [a, a]
|
||||
);
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static HelpText(): string {
|
||||
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
|
||||
|
@ -329,7 +398,9 @@ export default class ValidatedTextField {
|
|||
reformat?: ((s: string, country?: () => string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||
location: [number, number],
|
||||
mapBackgroundLayer: UIEventSource<any>
|
||||
mapBackgroundLayer: UIEventSource<any>,
|
||||
args: string[],
|
||||
feature: any
|
||||
}) => InputElement<string>,
|
||||
inputmode?: string): TextFieldDef {
|
||||
|
||||
|
|
|
@ -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");
|
||||
const titleIcons = new Combine(
|
||||
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")
|
||||
|
||||
|
|
|
@ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement {
|
|||
throw "Trying to generate a tagRenderingAnswer without configuration..."
|
||||
}
|
||||
super(tagsSource.map(tags => {
|
||||
if(tags === undefined){
|
||||
if (tags === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(configuration.condition){
|
||||
if(!configuration.condition.matchesProperties(tags)){
|
||||
|
||||
if (configuration.condition) {
|
||||
if (!configuration.condition.matchesProperties(tags)) {
|
||||
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;");
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
import InputElementWrapper from "../Input/InputElementWrapper";
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
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
|
||||
|
||||
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
||||
|
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
(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;
|
||||
if (freeform === undefined) {
|
||||
return undefined;
|
||||
|
@ -328,20 +329,34 @@ export default class TagRenderingQuestion extends Combine {
|
|||
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),
|
||||
country: () => tagsData._country,
|
||||
location: [tagsData._lat, tagsData._lon],
|
||||
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),
|
||||
pickString, toString
|
||||
);
|
||||
|
||||
if(freeform.inline){
|
||||
|
||||
inputTagsFilter.SetClass("w-16-imp")
|
||||
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
|
||||
inputTagsFilter.SetClass("block")
|
||||
|
||||
}
|
||||
|
||||
return inputTagsFilter;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -80,9 +80,7 @@ export default class ShowDataLayer {
|
|||
|
||||
if (zoomToFeatures) {
|
||||
try {
|
||||
|
||||
mp.fitBounds(geoLayer.getBounds())
|
||||
|
||||
mp.fitBounds(geoLayer.getBounds(), {animate: false})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
@ -148,7 +146,9 @@ export default class ShowDataLayer {
|
|||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
closeOnEscapeKey: true,
|
||||
closeButton: false
|
||||
closeButton: false,
|
||||
autoPanPaddingTopLeft: [15,15],
|
||||
|
||||
}, leafletLayer);
|
||||
|
||||
leafletLayer.bindPopup(popup);
|
||||
|
|
|
@ -39,7 +39,8 @@ export default class SpecialVisualizations {
|
|||
static constructMiniMap: (options?: {
|
||||
background?: UIEventSource<BaseLayer>,
|
||||
location?: UIEventSource<Loc>,
|
||||
allowMoving?: boolean
|
||||
allowMoving?: boolean,
|
||||
leafletOptions?: any
|
||||
}) => BaseUIElement;
|
||||
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
|
||||
public static specialVisualizations: SpecialVisualization[] =
|
||||
|
@ -369,7 +370,6 @@ export default class SpecialVisualizations {
|
|||
if (unit === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return unit.asHumanLongValue(value);
|
||||
|
||||
},
|
||||
|
@ -379,6 +379,7 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
|
||||
]
|
||||
|
||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||
private static GenHelpMessage() {
|
||||
|
||||
|
|
|
@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
|
|||
import {Utils} from "../Utils";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Combine from "./Base/Combine";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
|
||||
export class SubstitutedTranslation extends VariableUiElement {
|
||||
|
||||
public constructor(
|
||||
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(
|
||||
Locale.language.map(language => {
|
||||
const txt = translation.textFor(language)
|
||||
let txt = translation.textFor(language);
|
||||
if (txt === 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 => {
|
||||
if (proto.fixed !== undefined) {
|
||||
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
||||
|
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
})
|
||||
)
|
||||
|
||||
|
||||
this.SetClass("w-full")
|
||||
}
|
||||
|
||||
|
||||
public static ExtractSpecialComponents(template: string): {
|
||||
fixed?: string, special?: {
|
||||
public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
|
||||
fixed?: string,
|
||||
special?: {
|
||||
func: SpecialVisualization,
|
||||
args: 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'
|
||||
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
||||
if (matched != null) {
|
||||
|
||||
// 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 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 ?? "");
|
||||
if (argument.length > 0) {
|
||||
const realArgs = argument.split(",").map(str => str.trim());
|
||||
|
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
|
|||
}
|
||||
|
||||
let element;
|
||||
element = {special:{
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial
|
||||
}}
|
||||
element = {
|
||||
special: {
|
||||
args: args,
|
||||
style: style,
|
||||
func: knownSpecial
|
||||
}
|
||||
}
|
||||
return [...partBefore, element, ...partAfter]
|
||||
}
|
||||
}
|
||||
|
|
26
Utils.ts
|
@ -1,4 +1,5 @@
|
|||
import * as colors from "./assets/colors.json"
|
||||
import {TileRange} from "./Models/TileRange";
|
||||
|
||||
export class Utils {
|
||||
|
||||
|
@ -134,7 +135,7 @@ export class Utils {
|
|||
}
|
||||
return newArr;
|
||||
}
|
||||
|
||||
|
||||
public static MergeTags(a: any, b: any) {
|
||||
const t = {};
|
||||
for (const k in a) {
|
||||
|
@ -358,9 +359,12 @@ export class Utils {
|
|||
* @param contents
|
||||
* @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 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.download = fileName;
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "parking",
|
||||
"name": {
|
||||
"nl": "parking"
|
||||
"nl": "Parking"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"source": {
|
||||
|
@ -25,13 +25,13 @@
|
|||
{
|
||||
"if": "amenity=parking",
|
||||
"then": {
|
||||
"nl": "{name:nl}"
|
||||
"nl": "Auto Parking"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "amenity=motorcycle_parking",
|
||||
"then": {
|
||||
"nl": "{name}"
|
||||
"nl": "Motorfiets Parking"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -73,7 +73,10 @@
|
|||
},
|
||||
"tags": [
|
||||
"amenity=public_bookcase"
|
||||
]
|
||||
],
|
||||
"preciseInput": {
|
||||
"preferredBackground": "photo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
|
@ -139,7 +142,8 @@
|
|||
},
|
||||
"freeform": {
|
||||
"key": "capacity",
|
||||
"type": "nat"
|
||||
"type": "nat",
|
||||
"inline": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "watermill",
|
||||
"name": {
|
||||
"nl": "watermolens"
|
||||
"nl": "Watermolens"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"source": {
|
||||
|
|
3
assets/svg/checkbox-empty.svg
Normal 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 |
4
assets/svg/checkbox-filled.svg
Normal 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 |
83
assets/svg/crosshair-empty.svg
Normal 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 |
106
assets/svg/crosshair-locked.svg
Normal 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
|
@ -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 |
115
assets/svg/length-crosshair.svg
Normal 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 |
|
@ -49,6 +49,14 @@
|
|||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Hannah"
|
||||
],
|
||||
"path": "download.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [],
|
||||
"path": "ampersand.svg",
|
||||
|
@ -614,5 +622,635 @@
|
|||
"path": "filter.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Hannah Declerck"
|
||||
],
|
||||
"path": "checkbox-empty.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Hannah Declerck"
|
||||
],
|
||||
"path": "checkbox-filled.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Hannah Declerck"
|
||||
],
|
||||
"path": "arrow-left-thin.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "direction_masked.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "direction_outline.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "direction_stroke.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "SocialImageForeground.svg",
|
||||
"license": "CC-BY-SA",
|
||||
"sources": [
|
||||
"https://mapcomplete.osm.be"
|
||||
]
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "add.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [
|
||||
"Pieter Vander Vennet"
|
||||
],
|
||||
"path": "addSmall.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [],
|
||||
"path": "ampersand.svg",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"wikipedialink": {
|
||||
"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": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"wikipedia=",
|
||||
"wikidata~*"
|
||||
]
|
||||
},
|
||||
"if": "wikipedia=",
|
||||
"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>",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "id~=-",
|
||||
"then": "<span class='alert'>Uploading...</alert>"
|
||||
"if": "id~.*/-.*",
|
||||
"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]*"
|
||||
|
|
BIN
assets/themes/.DS_Store
vendored
|
@ -736,7 +736,7 @@
|
|||
"_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'])",
|
||||
"_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_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_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock",
|
||||
"_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id",
|
||||
"_embedding_features_with_rock:rock=JSON.parse(feat.properties._embedding_feature_with_rock ?? '{}')?.rock",
|
||||
"_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:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
|
||||
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"
|
||||
|
|
|
@ -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 |
|
@ -116,11 +116,5 @@
|
|||
"path": "birdhide.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
},
|
||||
{
|
||||
"authors": [],
|
||||
"path": "birdshelter.svg",
|
||||
"license": "CC0",
|
||||
"sources": []
|
||||
}
|
||||
]
|
|
@ -24,36 +24,58 @@
|
|||
"startZoom": 15,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "",
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"layers": [
|
||||
{
|
||||
"builtin": [
|
||||
"nature_reserve"
|
||||
],
|
||||
"#": "Nature reserve with geometry, z>=13",
|
||||
"builtin": "nature_reserve",
|
||||
"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": "13",
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/nature_reserve.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"visitor_information_centre"
|
||||
],
|
||||
"#": "Nature reserve overview from cache, points only, z < 13",
|
||||
"builtin": "nature_reserve",
|
||||
"override": {
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"+and": [
|
||||
"operator~.*[nN]atuurpunt.*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson"
|
||||
},
|
||||
"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",
|
||||
"icon": {
|
||||
|
@ -62,16 +84,17 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"trail"
|
||||
],
|
||||
"builtin": "trail",
|
||||
"override": {
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"+and": [
|
||||
"operator~.*[nN]atuurpunt.*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"minzoom": "13",
|
||||
"icon": {
|
||||
|
@ -90,11 +113,14 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"toilet"
|
||||
],
|
||||
"builtin": "toilet",
|
||||
"override": {
|
||||
"minzoom": "15",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/toilets.svg",
|
||||
"mappings": [
|
||||
|
@ -111,42 +137,49 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"birdhide"
|
||||
],
|
||||
"builtin": "birdhide",
|
||||
"override": {
|
||||
"minzoom": "15",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/birdhide.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"picnic_table"
|
||||
],
|
||||
"builtin": "picnic_table",
|
||||
"override": {
|
||||
"minzoom": "16",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/picnic_table.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"drinking_water"
|
||||
],
|
||||
"builtin": "drinking_water",
|
||||
"override": {
|
||||
"minzoom": "16",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/drips.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"parking"
|
||||
],
|
||||
"builtin": "parking",
|
||||
"override": {
|
||||
"minzoom": "16",
|
||||
"icon": {
|
||||
|
@ -173,33 +206,42 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"information_board"
|
||||
],
|
||||
"builtin": "information_board",
|
||||
"override": {
|
||||
"minzoom": "16",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/information_board.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"bench"
|
||||
],
|
||||
"builtin": "bench",
|
||||
"override": {
|
||||
"minzoom": "18",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/bench.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"builtin": [
|
||||
"watermill"
|
||||
],
|
||||
"builtin": "watermill",
|
||||
"override": {
|
||||
"minzoom": "18",
|
||||
"source": {
|
||||
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 12,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"icon": {
|
||||
"render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg"
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@
|
|||
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "generator:output:electricity"
|
||||
"key": "generator:output:electricity",
|
||||
"type": "pfloat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -85,7 +86,7 @@
|
|||
},
|
||||
"freeform": {
|
||||
"key": "height",
|
||||
"type": "float"
|
||||
"type": "pfloat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -179,6 +180,24 @@
|
|||
}
|
||||
],
|
||||
"eraseInvalidValues": true
|
||||
},
|
||||
{
|
||||
"appliesToKey": [
|
||||
"height",
|
||||
"rotor:diameter"
|
||||
],
|
||||
"applicableUnits": [
|
||||
{
|
||||
"canonicalDenomination": "m",
|
||||
"alternativeDenomination": [
|
||||
"meter"
|
||||
],
|
||||
"human": {
|
||||
"en": " meter",
|
||||
"nl": " meter"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Voyager"
|
||||
|
|
|
@ -105,11 +105,31 @@
|
|||
{
|
||||
"builtin": "slow_roads",
|
||||
"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": [
|
||||
"_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': ''"
|
||||
],
|
||||
"minzoom": 9,
|
||||
"minzoom": 18,
|
||||
"source": {
|
||||
"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",
|
||||
|
|
|
@ -64,7 +64,13 @@
|
|||
},
|
||||
"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:",
|
||||
|
|
|
@ -82,6 +82,10 @@ html, body {
|
|||
box-sizing: initial !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
display: block ruby;
|
||||
}
|
||||
|
||||
svg, img {
|
||||
box-sizing: content-box;
|
||||
width: 100%;
|
||||
|
@ -101,6 +105,10 @@ a {
|
|||
width: min-content;
|
||||
}
|
||||
|
||||
.w-16-imp {
|
||||
width: 4rem !important;
|
||||
}
|
||||
|
||||
.space-between{
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
3
index.ts
|
@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput";
|
|||
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||
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
|
||||
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
||||
DirectionInput.constructMinimap = options => new Minimap(options)
|
||||
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
|
||||
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
||||
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
|
|
|
@ -149,6 +149,10 @@
|
|||
"zoomInToSeeThisLayer": "Zoom in to see this layer",
|
||||
"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": {
|
||||
"abbreviations": {
|
||||
"monday": "Mon",
|
||||
|
|
|
@ -1185,9 +1185,6 @@
|
|||
},
|
||||
"1": {
|
||||
"then": "{name}"
|
||||
},
|
||||
"2": {
|
||||
"then": "Fietsenstalling"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -487,6 +487,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"0": {
|
||||
"title": "Обслуживание велосипедов/магазин"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defibrillator": {
|
||||
|
@ -1064,6 +1069,7 @@
|
|||
"1": {
|
||||
"question": "Вы хотите добавить описание?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Смотровая площадка"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,8 +122,10 @@
|
|||
"thanksForSharing": "Obrigado por compartilhar!",
|
||||
"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.",
|
||||
"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": {
|
||||
"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!",
|
||||
"name_required": "É necessário um nome para exibir e criar comentários",
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,27 @@
|
|||
"opening_hours": {
|
||||
"question": "Was sind die Öffnungszeiten von {name}?",
|
||||
"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}?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,20 @@
|
|||
"opening_hours": {
|
||||
"question": "Какое время работы у {name}?",
|
||||
"render": "<h3>Часы работы</h3>{opening_hours_table(opening_hours)}"
|
||||
},
|
||||
"level": {
|
||||
"mappings": {
|
||||
"2": {
|
||||
"then": "Расположено на первом этаже"
|
||||
},
|
||||
"1": {
|
||||
"then": "Расположено на первом этаже"
|
||||
},
|
||||
"0": {
|
||||
"then": "Расположено под землей"
|
||||
}
|
||||
},
|
||||
"render": "Расположено на {level}ом этаже"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1148,6 +1148,13 @@
|
|||
"human": " gigawatts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"applicableUnits": {
|
||||
"0": {
|
||||
"human": " meter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -956,6 +956,13 @@
|
|||
"human": " gigawatt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"applicableUnits": {
|
||||
"0": {
|
||||
"human": " meter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
920
package-lock.json
generated
|
@ -8,7 +8,7 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
|
||||
"start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** 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",
|
||||
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean",
|
||||
"add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"generate:layouts": "ts-node scripts/generateLayouts.ts",
|
||||
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
|
||||
"generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56",
|
||||
"generate: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:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
|
||||
"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",
|
||||
"i18next-client": "^1.11.4",
|
||||
"jquery": "^3.6.0",
|
||||
"jspdf": "^2.3.1",
|
||||
"latlon2country": "^1.1.3",
|
||||
"leaflet": "^1.7.1",
|
||||
"leaflet-providers": "^1.10.2",
|
||||
"leaflet-simple-map-screenshoter": "^0.4.4",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"libphonenumber": "0.0.10",
|
||||
"libphonenumber-js": "^1.7.55",
|
||||
|
@ -81,7 +83,6 @@
|
|||
"postcss": "^7.0.36",
|
||||
"prompt-sync": "^4.2.0",
|
||||
"sharp": "^0.27.0",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
||||
"tslint": "^6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -91,6 +92,7 @@
|
|||
"fs": "0.0.1-security",
|
||||
"marked": "^2.0.0",
|
||||
"read-file": "^0.2.0",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||
"ts-node": "^9.0.0",
|
||||
"ts-node-dev": "^1.0.0-pre.63",
|
||||
"tslint-no-circular-imports": "^0.7.0",
|
||||
|
|
|
@ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement";
|
|||
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;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* 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
|
||||
import {Overpass} from "../Logic/Osm/Overpass";
|
||||
|
@ -17,6 +17,8 @@ import MetaTagging from "../Logic/MetaTagging";
|
|||
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||
import {GeoOperations} from "../Logic/GeoOperations";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import * as fs from "fs";
|
||||
import {TileRange} from "../Models/TileRange";
|
||||
|
||||
|
||||
function createOverpassObject(theme: LayoutConfig) {
|
||||
|
@ -139,7 +141,7 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ {
|
|||
return allFeatures;
|
||||
}
|
||||
|
||||
async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) {
|
||||
function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) {
|
||||
let processed = 0;
|
||||
const layerIndex = theme.LayerIndex();
|
||||
for (let x = r.xstart; x <= r.xend; x++) {
|
||||
|
@ -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 generated = {} // layer --> x --> y[]
|
||||
for (let x = r.xstart; x <= r.xend; x++) {
|
||||
for (let y = r.ystart; y <= r.yend; y++) {
|
||||
const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8")
|
||||
|
@ -227,10 +230,8 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi
|
|||
.filter(f => f._matching_layer_id === layer.id)
|
||||
.filter(f => {
|
||||
const isShown = layer.isShown.GetRenderValue(f.properties).txt
|
||||
if (isShown === "no") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return isShown !== "no";
|
||||
|
||||
})
|
||||
const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z);
|
||||
console.log(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")")
|
||||
|
@ -239,18 +240,66 @@ async function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfi
|
|||
continue;
|
||||
}
|
||||
writeFileSync(new_path, JSON.stringify(geojson, null, " "))
|
||||
|
||||
if (generated[layer.id] === undefined) {
|
||||
generated[layer.id] = {}
|
||||
}
|
||||
if (generated[layer.id][x] === undefined) {
|
||||
generated[layer.id][x] = []
|
||||
}
|
||||
generated[layer.id][x].push(y)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of theme.layers) {
|
||||
const id = layer.id
|
||||
const loaded = generated[id]
|
||||
if(loaded === undefined){
|
||||
console.log("No features loaded for layer ",id)
|
||||
continue;
|
||||
}
|
||||
writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) {
|
||||
const allFeatures = []
|
||||
for (let x = r.xstart; x <= r.xend; x++) {
|
||||
for (let y = r.ystart; y <= r.yend; y++) {
|
||||
const read_path = geoJsonName(targetdir + "_" + layername, x, y, z);
|
||||
if (!fs.existsSync(read_path)) {
|
||||
continue;
|
||||
}
|
||||
const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features
|
||||
const pointsOnly = features.map(f => {
|
||||
|
||||
f.properties["_last_edit:timestamp"] = "1970-01-01"
|
||||
|
||||
if (f.geometry.type === "Point") {
|
||||
return f
|
||||
} else {
|
||||
return GeoOperations.centerpoint(f)
|
||||
}
|
||||
|
||||
})
|
||||
allFeatures.push(...pointsOnly)
|
||||
}
|
||||
}
|
||||
|
||||
const geojson = {
|
||||
"type": "FeatureCollection",
|
||||
"features": allFeatures
|
||||
}
|
||||
writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " "))
|
||||
}
|
||||
|
||||
async function main(args: string[]) {
|
||||
|
||||
if (args.length == 0) {
|
||||
console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1")
|
||||
console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name]")
|
||||
return;
|
||||
}
|
||||
const themeName = args[0]
|
||||
|
@ -285,8 +334,18 @@ async function main(args: string[]) {
|
|||
} while (failed > 0)
|
||||
|
||||
const extraFeatures = await downloadExtraData(theme);
|
||||
await postProcess(targetdir, tileRange, theme, extraFeatures)
|
||||
await splitPerLayer(targetdir, tileRange, theme)
|
||||
postProcess(targetdir, tileRange, theme, extraFeatures)
|
||||
splitPerLayer(targetdir, tileRange, theme)
|
||||
|
||||
if (args[7] === "--generate-point-overview") {
|
||||
const targetLayers = args[8].split(",")
|
||||
for (const targetLayer of targetLayers) {
|
||||
if (!theme.layers.some(l => l.id === targetLayer)) {
|
||||
throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",")
|
||||
}
|
||||
createOverview(targetdir, tileRange, zoomlevel, targetLayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import LayerConfig from "../Customizations/JSON/LayerConfig";
|
|||
import * as licenses from "../assets/generated/license_info.json"
|
||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
||||
import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson";
|
||||
import {Translation} from "../UI/i18n/Translation";
|
||||
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
|
||||
import AllKnownLayers from "../Customizations/AllKnownLayers";
|
||||
|
||||
|
@ -77,63 +76,6 @@ class LayerOverviewUtils {
|
|||
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[]) {
|
||||
|
||||
const lt = this.loadThemesAndLayers();
|
||||
|
@ -160,7 +102,6 @@ class LayerOverviewUtils {
|
|||
}
|
||||
|
||||
let themeErrorCount = []
|
||||
let missingTranslations = []
|
||||
for (const themeFile of themeFiles) {
|
||||
if (typeof themeFile.language === "string") {
|
||||
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 (!knownLayerIds.has(layer)) {
|
||||
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) {
|
||||
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 => l.builtin === undefined)
|
||||
|
||||
missingTranslations.push(...this.validateTranslationCompletenessOfObject(themeFile, themeFile.language, "Theme " + themeFile.id))
|
||||
|
||||
try {
|
||||
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) {
|
||||
console.log("All good!")
|
||||
|
||||
|
|
35
test.ts
|
@ -7,6 +7,11 @@ import {UIEventSource} from "./Logic/UIEventSource";
|
|||
import {Tag} from "./Logic/Tags/Tag";
|
||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||
import {Translation} from "./UI/i18n/Translation";
|
||||
import LocationInput from "./UI/Input/LocationInput";
|
||||
import Loc from "./Models/Loc";
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
import LengthInput from "./UI/Input/LengthInput";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
/*import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
||||
import Combine from "./UI/Base/Combine";
|
||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
||||
|
@ -148,19 +153,17 @@ function TestMiniMap() {
|
|||
featureSource.ping()
|
||||
}
|
||||
//*/
|
||||
QueryParameters.GetQueryParameter("test", "true").setData("true")
|
||||
State.state= new State(undefined)
|
||||
const id = "node/5414688303"
|
||||
State.state.allElements.addElementById(id, new UIEventSource<any>({id: id}))
|
||||
new Combine([
|
||||
new DeleteWizard(id, {
|
||||
noDeleteOptions: [
|
||||
{
|
||||
if:[ new Tag("access","private")],
|
||||
then: new Translation({
|
||||
en: "Very private! Delete now or me send lawfull lawyer"
|
||||
})
|
||||
}
|
||||
]
|
||||
}),
|
||||
]).AttachTo("maindiv")
|
||||
|
||||
const loc = new UIEventSource<Loc>({
|
||||
zoom: 24,
|
||||
lat: 51.21043,
|
||||
lon: 3.21389
|
||||
})
|
||||
const li = new LengthInput(
|
||||
AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")),
|
||||
loc
|
||||
)
|
||||
li.SetStyle("height: 30rem; background: aliceblue;")
|
||||
.AttachTo("maindiv")
|
||||
|
||||
new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv")
|
|
@ -15,7 +15,7 @@ export default class OsmConnectionSpec extends T {
|
|||
super("OsmConnectionSpec-test", [
|
||||
["login on dev",
|
||||
() => {
|
||||
const osmConn = new OsmConnection(false,
|
||||
const osmConn = new OsmConnection(false,false,
|
||||
new UIEventSource<string>(undefined),
|
||||
"Unit test",
|
||||
true,
|
||||
|
|
10
tslint.json
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-no-circular-imports"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {},
|
||||
"rulesDirectory": []
|
||||
}
|