From e11a5ca17babd47163a83ff7b269fc8bb928c83b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 13:47:53 +0200 Subject: [PATCH 01/60] Attempting to add in backend to the element --- Logic/Osm/Changes.ts | 50 ++++++++++++++++++++------------------ Logic/Osm/OsmConnection.ts | 4 +-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 4a4b00d35..7ebf4df9b 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -6,31 +6,31 @@ import Constants from "../../Models/Constants"; import FeatureSource from "../FeatureSource/FeatureSource"; import {TagsFilter} from "../Tags/TagsFilter"; import {Tag} from "../Tags/Tag"; +import {OsmConnection} from "./OsmConnection"; /** * 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: UIEventSource<{ elementId: string, key: string, value: string }[]> = + new UIEventSource<{ elementId: string; key: string; value: string }[]>([]); /** * 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 +49,7 @@ export class Changes implements FeatureSource{ return {k: key.trim(), v: value.trim()}; } - - + addTag(elementId: string, tagsFilter: TagsFilter, tags?: UIEventSource) { const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); @@ -59,7 +58,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 +75,17 @@ 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([]); } + /** * Create a new node element at the given lat/long. * An internal OsmObject is created to upload later on, a geojson represention is returned. @@ -118,33 +118,37 @@ 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.uploadAll([osmNode], changes); return geojson; } private uploadChangesWithLatestVersions( knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { const knownById = new Map(); - + knownElements.forEach(knownElement => { - console.log("Setting ",knownElement.type + knownElement.id, knownElement) + console.log("Setting ", knownElement.type + knownElement.id, knownElement) knownById.set(knownElement.type + "/" + knownElement.id, knownElement) }) - - + + // 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) { diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index a3df9be9f..decc8a09c 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -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', @@ -66,7 +66,7 @@ export class OsmConnection { osmConfiguration: "osm" | "osm-test" = 'osm' ) { 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; From 6732c12a0c212135a08e154d80c3f7dd6ab6bf2e Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 15:52:52 +0200 Subject: [PATCH 02/60] First version which caches changesets if not uploaded --- Logic/Osm/Changes.ts | 54 +++++++++++++++++++++------------ Logic/Osm/ChangesetHandler.ts | 21 ++++++------- Logic/Osm/OsmConnection.ts | 5 +-- Logic/Osm/OsmObject.ts | 19 ++++++++---- Logic/Web/LocalStorageSource.ts | 16 ++++++++++ Models/Constants.ts | 2 +- assets/tagRenderings/icons.json | 8 +++-- 7 files changed, 84 insertions(+), 41 deletions(-) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index de7c5dc0e..9da36b048 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -7,6 +7,7 @@ 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. @@ -24,8 +25,13 @@ export class Changes implements FeatureSource { /** * 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: UIEventSource<{ elementId: string, key: string, value: string }[]> = LocalStorageSource.GetParsed("pending-changes", []) + + /** + * All the pending new objects to upload + * @private + */ + private readonly newObjects: UIEventSource<{ id: number, lat: number, lon: number }[]> = LocalStorageSource.GetParsed("newObjects", []) /** * Adds a change to the pending changes @@ -82,8 +88,7 @@ export class Changes implements FeatureSource { if (flushreason !== undefined) { console.log(flushreason) } - this.uploadAll([], this.pending.data); - this.pending.setData([]); + this.uploadAll(); } /** @@ -93,12 +98,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 = { @@ -135,22 +140,32 @@ export class Changes implements FeatureSource { properties["_backend"] = State.state.osmConnection.userDetails.data.backend } - // this.uploadAll([osmNode], changes); + + 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(); - 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) { @@ -217,17 +232,19 @@ export class Changes implements FeatureSource { changes += ""; return changes; + }, + () => { + console.log("Upload successfull!") + this.newObjects.setData([]) + this.pending.setData([]); }); }; - 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; @@ -240,8 +257,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) }) } diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index ef9f5f717..98d45a790 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -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,8 @@ export class ChangesetHandler { public UploadChangeset( layout: LayoutConfig, allElements: ElementStorage, - generateChangeXML: (csid: string) => string) { + generateChangeXML: (csid: string) => string, + whenDone : (csId: string) => void) { if (this.userDetails.data.csCount == 0) { // The user became a contributor! @@ -80,6 +81,7 @@ export class ChangesetHandler { if (this._dryRun) { const changesetXML = generateChangeXML("123456"); console.log(changesetXML); + whenDone("123456") return; } @@ -93,8 +95,7 @@ export class ChangesetHandler { console.log(changeset); self.AddChange(csId, changeset, allElements, - () => { - }, + whenDone, (e) => { console.error("UPLOADING FAILED!", e) } @@ -107,14 +108,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); } ) @@ -244,7 +244,6 @@ 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") return; } else { continuation(response); @@ -265,7 +264,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 +279,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); }); } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index decc8a09c..cba332447 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -110,8 +110,9 @@ 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) { + this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone); } public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index e8f204759..09ee7137c 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -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>(); private static referencingWaysCache = new Map>(); @@ -37,15 +38,15 @@ export abstract class OsmObject { } static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource { - let src : UIEventSource; + let src: UIEventSource; 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(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); } ); diff --git a/Logic/Web/LocalStorageSource.ts b/Logic/Web/LocalStorageSource.ts index 050b12459..a89d2a556 100644 --- a/Logic/Web/LocalStorageSource.ts +++ b/Logic/Web/LocalStorageSource.ts @@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource"; * UIEventsource-wrapper around localStorage */ export class LocalStorageSource { + + static GetParsed(key: string, defaultValue : any){ + 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 { try { diff --git a/Models/Constants.ts b/Models/Constants.ts index 79bcb5ca0..33a52c40f 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.3d"; + public static vNumber = "0.8.4"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/assets/tagRenderings/icons.json b/assets/tagRenderings/icons.json index e0bde24da..5dbe8856a 100644 --- a/assets/tagRenderings/icons.json +++ b/assets/tagRenderings/icons.json @@ -59,8 +59,12 @@ "render": "", "mappings": [ { - "if": "id~=-", - "then": "Uploading..." + "if": "id~.*/-.*", + "then": "" + }, + { + "if": "_backend~*", + "then": "" } ], "condition": "id~(node|way|relation)/[0-9]*" From 7ba6a82b354ded009351080775661e6f87e1659c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 19:18:51 +0200 Subject: [PATCH 03/60] Remove translation completeness check from layer overview generation --- scripts/generateLayerOverview.ts | 62 -------------------------------- 1 file changed, 62 deletions(-) diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index b83b28be9..7da47f21c 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -77,63 +77,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(); @@ -198,11 +141,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!") From 3d7b8ab5648288997dc8d8acc27512fd68c8eb11 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 19:19:23 +0200 Subject: [PATCH 04/60] Remove translation completeness check from layer overview generation --- scripts/generateLayerOverview.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 7da47f21c..545ebf844 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -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"; @@ -108,10 +107,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) { @@ -129,7 +124,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") From fbffb367a72436c727859d66397a72d2ada61cef Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 19:21:27 +0200 Subject: [PATCH 05/60] Fix wikidata icon --- assets/tagRenderings/icons.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/tagRenderings/icons.json b/assets/tagRenderings/icons.json index 5dbe8856a..c1b9d5e27 100644 --- a/assets/tagRenderings/icons.json +++ b/assets/tagRenderings/icons.json @@ -1,15 +1,15 @@ { "wikipedialink": { "render": "WP", - "condition": "wikipedia~*", + "condition": { + "or": [ + "wikipedia~*", + "wikidata~*" + ] + }, "mappings": [ { - "if": { - "and": [ - "wikipedia=", - "wikidata~*" - ] - }, + "if": "wikipedia=", "then": "WD" } ] From 9bf78a62e49e282fa35c196e1b6256db7df1819c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 19:21:56 +0200 Subject: [PATCH 06/60] Remove unused file --- tslint.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tslint.json diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 6a204a045..000000000 --- a/tslint.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "defaultSeverity": "error", - "extends": [ - "tslint:recommended", - "tslint-no-circular-imports" - ], - "jsRules": {}, - "rules": {}, - "rulesDirectory": [] -} \ No newline at end of file From 80cb9efaf05314f1759f79ddafe5a0ee16de817c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 20:56:59 +0200 Subject: [PATCH 07/60] Translation sync --- Models/TileRange.ts | 9 + assets/layers/bench/bench.json | 66 ++-- assets/layers/bench_at_pt/bench_at_pt.json | 6 +- .../bicycle_library/bicycle_library.json | 3 +- assets/layers/bike_cafe/bike_cafe.json | 27 +- .../bike_monitoring_station.json | 3 +- assets/layers/bike_parking/bike_parking.json | 18 +- .../bike_repair_station.json | 12 +- assets/layers/bike_shop/bike_shop.json | 36 +- .../layers/defibrillator/defibrillator.json | 3 +- assets/layers/ghost_bike/ghost_bike.json | 6 +- assets/themes/campersites/campersites.json | 39 +- .../openwindpowermap/openwindpowermap.json | 344 +++++++++--------- langs/layers/fi.json | 172 ++++----- langs/layers/ru.json | 148 ++++---- langs/themes/en.json | 62 ++++ langs/themes/nl.json | 72 ++-- langs/themes/ru.json | 2 +- 18 files changed, 606 insertions(+), 422 deletions(-) create mode 100644 Models/TileRange.ts diff --git a/Models/TileRange.ts b/Models/TileRange.ts new file mode 100644 index 000000000..f3c59af23 --- /dev/null +++ b/Models/TileRange.ts @@ -0,0 +1,9 @@ +export interface TileRange { + xstart: number, + ystart: number, + xend: number, + yend: number, + total: number, + zoomlevel: number + +} \ No newline at end of file diff --git a/assets/layers/bench/bench.json b/assets/layers/bench/bench.json index 2ba7c0358..312b21cfc 100644 --- a/assets/layers/bench/bench.json +++ b/assets/layers/bench/bench.json @@ -12,7 +12,8 @@ "ru": "Скамейки", "zh_Hans": "长椅", "zh_Hant": "長椅", - "nb_NO": "Benker" + "nb_NO": "Benker", + "fi": "Penkit" }, "minzoom": 14, "source": { @@ -31,7 +32,8 @@ "ru": "Скамейка", "zh_Hans": "长椅", "zh_Hant": "長椅", - "nb_NO": "Benk" + "nb_NO": "Benk", + "fi": "Penkki" } }, "tagRenderings": [ @@ -49,7 +51,8 @@ "ru": "Спинка", "zh_Hans": "靠背", "zh_Hant": "靠背", - "nb_NO": "Rygglene" + "nb_NO": "Rygglene", + "fi": "Selkänoja" }, "freeform": { "key": "backrest" @@ -69,7 +72,8 @@ "ru": "Со спинкой", "zh_Hans": "靠背:有", "zh_Hant": "靠背:有", - "nb_NO": "Rygglene: Ja" + "nb_NO": "Rygglene: Ja", + "fi": "Selkänoja: kyllä" } }, { @@ -86,7 +90,8 @@ "ru": "Без спинки", "zh_Hans": "靠背:无", "zh_Hant": "靠背:無", - "nb_NO": "Rygglene: Nei" + "nb_NO": "Rygglene: Nei", + "fi": "Selkänoja: ei" } } ], @@ -149,7 +154,8 @@ "ru": "Материал: {material}", "zh_Hans": "材质: {material}", "zh_Hant": "材質:{material}", - "nb_NO": "Materiale: {material}" + "nb_NO": "Materiale: {material}", + "fi": "Materiaali: {material}" }, "freeform": { "key": "material", @@ -170,7 +176,8 @@ "zh_Hans": "材质:木", "nb_NO": "Materiale: tre", "zh_Hant": "材質:木頭", - "pt_BR": "Material: madeira" + "pt_BR": "Material: madeira", + "fi": "Materiaali: puu" } }, { @@ -203,7 +210,8 @@ "zh_Hans": "材质:石头", "nb_NO": "Materiale: stein", "zh_Hant": "材質:石頭", - "pt_BR": "Material: pedra" + "pt_BR": "Material: pedra", + "fi": "Materiaali: kivi" } }, { @@ -220,7 +228,8 @@ "zh_Hans": "材质:混凝土", "nb_NO": "Materiale: betong", "zh_Hant": "材質:水泥", - "pt_BR": "Material: concreto" + "pt_BR": "Material: concreto", + "fi": "Materiaali: betoni" } }, { @@ -237,7 +246,8 @@ "zh_Hans": "材质:塑料", "nb_NO": "Materiale: plastikk", "zh_Hant": "材質:塑膠", - "pt_BR": "Material: plástico" + "pt_BR": "Material: plástico", + "fi": "Materiaali: muovi" } }, { @@ -254,7 +264,8 @@ "zh_Hans": "材质:不锈钢", "nb_NO": "Materiale: stål", "zh_Hant": "材質:鋼鐵", - "pt_BR": "Material: aço" + "pt_BR": "Material: aço", + "fi": "Materiaali: teräs" } } ], @@ -313,7 +324,8 @@ "zh_Hans": "颜色: {colour}", "zh_Hant": "顏色:{colour}", "nb_NO": "Farge: {colour}", - "pt_BR": "Cor: {colour}" + "pt_BR": "Cor: {colour}", + "fi": "Väri: {colour}" }, "question": { "en": "Which colour does this bench have?", @@ -345,7 +357,8 @@ "zh_Hans": "颜色:棕", "zh_Hant": "顏色:棕色", "nb_NO": "Farge: brun", - "pt_BR": "Cor: marrom" + "pt_BR": "Cor: marrom", + "fi": "Väri: ruskea" } }, { @@ -361,7 +374,8 @@ "zh_Hans": "颜色:绿", "zh_Hant": "顏色:綠色", "nb_NO": "Farge: grønn", - "pt_BR": "Cor: verde" + "pt_BR": "Cor: verde", + "fi": "Väri: vihreä" } }, { @@ -377,7 +391,8 @@ "zh_Hans": "颜色:灰", "zh_Hant": "顏色:灰色", "nb_NO": "Farge: grå", - "pt_BR": "Cor: cinza" + "pt_BR": "Cor: cinza", + "fi": "Väri: harmaa" } }, { @@ -393,7 +408,8 @@ "zh_Hans": "颜色:白", "zh_Hant": "顏色:白色", "nb_NO": "Farge: hvit", - "pt_BR": "Cor: branco" + "pt_BR": "Cor: branco", + "fi": "Väri: valkoinen" } }, { @@ -409,7 +425,8 @@ "zh_Hans": "颜色:红", "zh_Hant": "顏色:紅色", "nb_NO": "Farge: rød", - "pt_BR": "Cor: vermelho" + "pt_BR": "Cor: vermelho", + "fi": "Väri: punainen" } }, { @@ -425,7 +442,8 @@ "zh_Hans": "颜色:黑", "zh_Hant": "顏色:黑色", "nb_NO": "Farge: svart", - "pt_BR": "Cor: preto" + "pt_BR": "Cor: preto", + "fi": "Väri: musta" } }, { @@ -441,7 +459,8 @@ "zh_Hans": "颜色:蓝", "zh_Hant": "顏色:藍色", "nb_NO": "Farge: blå", - "pt_BR": "Cor: azul" + "pt_BR": "Cor: azul", + "fi": "Väri: sininen" } }, { @@ -457,7 +476,8 @@ "zh_Hans": "颜色:黄", "zh_Hant": "顏色:黃色", "nb_NO": "Farge: gul", - "pt_BR": "Cor: amarelo" + "pt_BR": "Cor: amarelo", + "fi": "Väri: keltainen" } } ] @@ -528,7 +548,8 @@ "zh_Hans": "长椅", "nb_NO": "Benk", "zh_Hant": "長椅", - "pt_BR": "Banco" + "pt_BR": "Banco", + "fi": "Penkki" }, "description": { "en": "Add a new bench", @@ -542,7 +563,8 @@ "zh_Hans": "增加一个新的长椅", "nb_NO": "Legg til en ny benk", "zh_Hant": "新增長椅", - "pt_BR": "Adicionar um novo banco" + "pt_BR": "Adicionar um novo banco", + "fi": "Lisää uusi penkki" } } ] diff --git a/assets/layers/bench_at_pt/bench_at_pt.json b/assets/layers/bench_at_pt/bench_at_pt.json index 85cadb86e..bb2661e25 100644 --- a/assets/layers/bench_at_pt/bench_at_pt.json +++ b/assets/layers/bench_at_pt/bench_at_pt.json @@ -37,7 +37,8 @@ "zh_Hans": "长椅", "nb_NO": "Benk", "zh_Hant": "長椅", - "pt_BR": "Banco" + "pt_BR": "Banco", + "fi": "Penkki" }, "mappings": [ { @@ -96,7 +97,8 @@ "id": "{name}", "zh_Hans": "{name}", "zh_Hant": "{name}", - "pt_BR": "{name}" + "pt_BR": "{name}", + "fi": "{name}" }, "freeform": { "key": "name" diff --git a/assets/layers/bicycle_library/bicycle_library.json b/assets/layers/bicycle_library/bicycle_library.json index cb6cb2f2e..d56f3bc42 100644 --- a/assets/layers/bicycle_library/bicycle_library.json +++ b/assets/layers/bicycle_library/bicycle_library.json @@ -145,7 +145,8 @@ "fr": "Emprunter un vélo coûte 20 €/an et 20 € de garantie", "it": "Il prestito di una bicicletta costa 20 €/anno più 20 € di garanzia", "de": "Das Ausleihen eines Fahrrads kostet 20€ pro Jahr und 20€ Gebühr", - "zh_Hant": "租借單車價錢 €20/year 與 €20 保證金" + "zh_Hant": "租借單車價錢 €20/year 與 €20 保證金", + "ru": "Прокат велосипеда стоит €20/год и €20 залог" } } ] diff --git a/assets/layers/bike_cafe/bike_cafe.json b/assets/layers/bike_cafe/bike_cafe.json index 77b582395..30dcb1760 100644 --- a/assets/layers/bike_cafe/bike_cafe.json +++ b/assets/layers/bike_cafe/bike_cafe.json @@ -117,7 +117,8 @@ "de": "Dieses Fahrrad-Café bietet eine Fahrradpumpe an, die von jedem benutzt werden kann", "it": "Questo caffè in bici offre una pompa per bici liberamente utilizzabile", "zh_Hans": "这家自行车咖啡为每个人提供打气筒", - "zh_Hant": "這個單車咖啡廳有提供給任何人都能使用的單車打氣甬" + "zh_Hant": "這個單車咖啡廳有提供給任何人都能使用的單車打氣甬", + "ru": "В этом велосипедном кафе есть велосипедный насос для всеобщего использования" } }, { @@ -130,7 +131,8 @@ "de": "Dieses Fahrrad-Café bietet keine Fahrradpumpe an, die von jedem benutzt werden kann", "it": "Questo caffè in bici non offre una pompa per bici liberamente utilizzabile", "zh_Hans": "这家自行车咖啡不为每个人提供打气筒", - "zh_Hant": "這個單車咖啡廳並沒有為所有人提供單車打氣甬" + "zh_Hant": "這個單車咖啡廳並沒有為所有人提供單車打氣甬", + "ru": "В этом велосипедном кафе нет велосипедного насоса для всеобщего использования" } } ] @@ -144,7 +146,8 @@ "de": "Gibt es hier Werkzeuge, um das eigene Fahrrad zu reparieren?", "it": "Ci sono degli strumenti per riparare la propria bicicletta?", "zh_Hans": "这里有供你修车用的工具吗?", - "zh_Hant": "這裡是否有工具修理你的單車嗎?" + "zh_Hant": "這裡是否有工具修理你的單車嗎?", + "ru": "Есть ли здесь инструменты для починки вашего велосипеда?" }, "mappings": [ { @@ -157,7 +160,8 @@ "de": "Dieses Fahrrad-Café bietet Werkzeuge für die selbständige Reparatur an", "it": "Questo caffè in bici fornisce degli attrezzi per la riparazione fai-da-te", "zh_Hans": "这家自行车咖啡为DIY修理者提供工具", - "zh_Hant": "這個單車咖啡廳提供工具讓你修理" + "zh_Hant": "這個單車咖啡廳提供工具讓你修理", + "ru": "В этом велосипедном кафе есть инструменты для починки своего велосипеда" } }, { @@ -170,7 +174,8 @@ "de": "Dieses Fahrrad-Café bietet keine Werkzeuge für die selbständige Reparatur an", "it": "Questo caffè in bici non fornisce degli attrezzi per la riparazione fai-da-te", "zh_Hans": "这家自行车咖啡不为DIY修理者提供工具", - "zh_Hant": "這個單車咖啡廳並沒有提供工具讓你修理" + "zh_Hant": "這個單車咖啡廳並沒有提供工具讓你修理", + "ru": "В этом велосипедном кафе нет инструментов для починки своего велосипеда" } } ] @@ -184,7 +189,8 @@ "de": "Repariert dieses Fahrrad-Café Fahrräder?", "it": "Questo caffè in bici ripara le bici?", "zh_Hans": "这家自行车咖啡t提供修车服务吗?", - "zh_Hant": "這個單車咖啡廳是否能修理單車?" + "zh_Hant": "這個單車咖啡廳是否能修理單車?", + "ru": "Есть ли услуги ремонта велосипедов в этом велосипедном кафе?" }, "mappings": [ { @@ -197,7 +203,8 @@ "de": "Dieses Fahrrad-Café repariert Fahrräder", "it": "Questo caffè in bici ripara le bici", "zh_Hans": "这家自行车咖啡可以修车", - "zh_Hant": "這個單車咖啡廳修理單車" + "zh_Hant": "這個單車咖啡廳修理單車", + "ru": "В этом велосипедном кафе есть услуги ремонта велосипедов" } }, { @@ -210,7 +217,8 @@ "de": "Dieses Fahrrad-Café repariert keine Fahrräder", "it": "Questo caffè in bici non ripara le bici", "zh_Hans": "这家自行车咖啡不能修车", - "zh_Hant": "這個單車咖啡廳並不修理單車" + "zh_Hant": "這個單車咖啡廳並不修理單車", + "ru": "В этом велосипедном кафе нет услуг ремонта велосипедов" } } ] @@ -275,7 +283,8 @@ "fr": "Quand ce Café vélo est-t-il ouvert ?", "it": "Quando è aperto questo caffè in bici?", "zh_Hans": "这家自行车咖啡什么时候开门营业?", - "zh_Hant": "何時這個單車咖啡廳營運?" + "zh_Hant": "何時這個單車咖啡廳營運?", + "ru": "Каков режим работы этого велосипедного кафе?" }, "render": "{opening_hours_table(opening_hours)}", "freeform": { diff --git a/assets/layers/bike_monitoring_station/bike_monitoring_station.json b/assets/layers/bike_monitoring_station/bike_monitoring_station.json index 0f54f36de..34b4d1b63 100644 --- a/assets/layers/bike_monitoring_station/bike_monitoring_station.json +++ b/assets/layers/bike_monitoring_station/bike_monitoring_station.json @@ -5,7 +5,8 @@ "nl": "Telstation", "fr": "Stations de contrôle", "it": "Stazioni di monitoraggio", - "zh_Hant": "監視站" + "zh_Hant": "監視站", + "ru": "Станции мониторинга" }, "minzoom": 12, "source": { diff --git a/assets/layers/bike_parking/bike_parking.json b/assets/layers/bike_parking/bike_parking.json index 368931aa8..be24e0a6d 100644 --- a/assets/layers/bike_parking/bike_parking.json +++ b/assets/layers/bike_parking/bike_parking.json @@ -77,7 +77,8 @@ "de": "Dies ist ein Fahrrad-Parkplatz der Art: {bicycle_parking}", "hu": "Ez egy {bicycle_parking} típusú kerékpáros parkoló", "it": "È un parcheggio bici del tipo: {bicycle_parking}", - "zh_Hant": "這個單車停車場的類型是:{bicycle_parking}" + "zh_Hant": "這個單車停車場的類型是:{bicycle_parking}", + "ru": "Это велопарковка типа {bicycle_parking}" }, "freeform": { "key": "bicycle_parking", @@ -288,7 +289,8 @@ "fr": "Ce parking est couvert (il a un toit)", "hu": "A parkoló fedett", "it": "È un parcheggio coperto (ha un tetto)", - "zh_Hant": "這個停車場有遮蔽 (有屋頂)" + "zh_Hant": "這個停車場有遮蔽 (有屋頂)", + "ru": "Это крытая парковка (есть крыша/навес)" } }, { @@ -301,7 +303,8 @@ "fr": "Ce parking n'est pas couvert", "hu": "A parkoló nem fedett", "it": "Non è un parcheggio coperto", - "zh_Hant": "這個停車場沒有遮蔽" + "zh_Hant": "這個停車場沒有遮蔽", + "ru": "Это открытая парковка" } } ] @@ -324,7 +327,8 @@ "gl": "Lugar para {capacity} bicicletas", "de": "Platz für {capacity} Fahrräder", "it": "Posti per {capacity} bici", - "zh_Hant": "{capacity} 單車的地方" + "zh_Hant": "{capacity} 單車的地方", + "ru": "Место для {capacity} велосипеда(ов)" }, "freeform": { "key": "capacity", @@ -339,7 +343,8 @@ "fr": "Qui peut utiliser ce parking à vélo ?", "it": "Chi può usare questo parcheggio bici?", "de": "Wer kann diesen Fahrradparplatz nutzen?", - "zh_Hant": "誰可以使用這個單車停車場?" + "zh_Hant": "誰可以使用這個單車停車場?", + "ru": "Кто может пользоваться этой велопарковкой?" }, "render": { "en": "{access}", @@ -349,7 +354,8 @@ "it": "{access}", "ru": "{access}", "id": "{access}", - "zh_Hant": "{access}" + "zh_Hant": "{access}", + "fi": "{access}" }, "freeform": { "key": "access", diff --git a/assets/layers/bike_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index cc08a9acf..ef3adb308 100644 --- a/assets/layers/bike_repair_station/bike_repair_station.json +++ b/assets/layers/bike_repair_station/bike_repair_station.json @@ -218,7 +218,8 @@ "en": "When is this bicycle repair point open?", "fr": "Quand ce point de réparation de vélo est-il ouvert ?", "it": "Quando è aperto questo punto riparazione bici?", - "de": "Wann ist diese Fahrradreparaturstelle geöffnet?" + "de": "Wann ist diese Fahrradreparaturstelle geöffnet?", + "ru": "Когда работает эта точка обслуживания велосипедов?" }, "render": "{opening_hours_table()}", "freeform": { @@ -233,7 +234,8 @@ "en": "Always open", "fr": "Ouvert en permanence", "it": "Sempre aperto", - "de": "Immer geöffnet" + "de": "Immer geöffnet", + "ru": "Всегда открыто" } }, { @@ -512,7 +514,8 @@ "render": { "en": "./assets/layers/bike_repair_station/repair_station.svg", "ru": "./assets/layers/bike_repair_station/repair_station.svg", - "it": "./assets/layers/bike_repair_station/repair_station.svg" + "it": "./assets/layers/bike_repair_station/repair_station.svg", + "fi": "./assets/layers/bike_repair_station/repair_station.svg" }, "mappings": [ { @@ -584,7 +587,8 @@ "gl": "Bomba de ar", "de": "Fahrradpumpe", "it": "Pompa per bici", - "ru": "Велосипедный насос" + "ru": "Велосипедный насос", + "fi": "Pyöräpumppu" }, "tags": [ "amenity=bicycle_repair_station", diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json index 72ced485e..7f60ced89 100644 --- a/assets/layers/bike_shop/bike_shop.json +++ b/assets/layers/bike_shop/bike_shop.json @@ -6,7 +6,8 @@ "fr": "Magasin ou réparateur de vélo", "gl": "Tenda/arranxo de bicicletas", "de": "Fahrradwerkstatt/geschäft", - "it": "Venditore/riparatore bici" + "it": "Venditore/riparatore bici", + "ru": "Обслуживание велосипедов/магазин" }, "minzoom": 13, "source": { @@ -54,7 +55,8 @@ "fr": "Magasin ou réparateur de vélo", "gl": "Tenda/arranxo de bicicletas", "de": "Fahrradwerkstatt/geschäft", - "it": "Venditore/riparatore bici" + "it": "Venditore/riparatore bici", + "ru": "Обслуживание велосипедов/магазин" }, "mappings": [ { @@ -207,7 +209,8 @@ "fr": "Ce magasin s'appelle {name}", "gl": "Esta tenda de bicicletas chámase {name}", "de": "Dieses Fahrradgeschäft heißt {name}", - "it": "Questo negozio di biciclette è chiamato {name}" + "it": "Questo negozio di biciclette è chiamato {name}", + "ru": "Этот магазин велосипедов называется {name}" }, "freeform": { "key": "name" @@ -284,7 +287,8 @@ "fr": "Est-ce que ce magasin vend des vélos ?", "gl": "Esta tenda vende bicicletas?", "de": "Verkauft dieser Laden Fahrräder?", - "it": "Questo negozio vende bici?" + "it": "Questo negozio vende bici?", + "ru": "Продаются ли велосипеды в этом магазине?" }, "mappings": [ { @@ -368,7 +372,8 @@ "fr": "Ce magasin ne répare seulement des marques spécifiques", "gl": "Esta tenda só arranxa bicicletas dunha certa marca", "de": "Dieses Geschäft repariert nur Fahrräder einer bestimmten Marke", - "it": "Questo negozio ripara solo le biciclette di una certa marca" + "it": "Questo negozio ripara solo le biciclette di una certa marca", + "ru": "В этом магазине обслуживают велосипеды определённого бренда" } } ] @@ -466,7 +471,8 @@ "fr": "Est-ce que ce magasin offre une pompe en accès libre ?", "gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa?", "de": "Bietet dieses Geschäft eine Fahrradpumpe zur Benutzung für alle an?", - "it": "Questo negozio offre l’uso a chiunque di una pompa per bici?" + "it": "Questo negozio offre l’uso a chiunque di una pompa per bici?", + "ru": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?" }, "mappings": [ { @@ -477,7 +483,8 @@ "fr": "Ce magasin offre une pompe en acces libre", "gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa", "de": "Dieses Geschäft bietet eine Fahrradpumpe für alle an", - "it": "Questo negozio offre l’uso pubblico di una pompa per bici" + "it": "Questo negozio offre l’uso pubblico di una pompa per bici", + "ru": "В этом магазине есть велосипедный насос для всеобщего пользования" } }, { @@ -488,7 +495,8 @@ "fr": "Ce magasin n'offre pas de pompe en libre accès", "gl": "Esta tenda non ofrece unha bomba de ar para uso de calquera persoa", "de": "Dieses Geschäft bietet für niemanden eine Fahrradpumpe an", - "it": "Questo negozio non offre l’uso pubblico di una pompa per bici" + "it": "Questo negozio non offre l’uso pubblico di una pompa per bici", + "ru": "В этом магазине нет велосипедного насоса для всеобщего пользования" } }, { @@ -509,7 +517,8 @@ "fr": "Est-ce qu'il y a des outils pour réparer son vélo dans ce magasin ?", "gl": "Hai ferramentas aquí para arranxar a túa propia bicicleta?", "de": "Gibt es hier Werkzeuge, um das eigene Fahrrad zu reparieren?", - "it": "Sono presenti degli attrezzi per riparare la propria bici?" + "it": "Sono presenti degli attrezzi per riparare la propria bici?", + "ru": "Есть ли здесь инструменты для починки собственного велосипеда?" }, "mappings": [ { @@ -541,7 +550,8 @@ "nl": "Het gereedschap aan om je fiets zelf te herstellen is enkel voor als je de fiets er kocht of huurt", "fr": "Des outils d'auto-réparation sont disponibles uniquement si vous avez acheté ou loué le vélo dans ce magasin", "it": "Gli attrezzi per la riparazione fai-da-te sono disponibili solamente se hai acquistato/noleggiato la bici nel negozio", - "de": "Werkzeuge für die Selbstreparatur sind nur verfügbar, wenn Sie das Fahrrad im Laden gekauft/gemietet haben" + "de": "Werkzeuge für die Selbstreparatur sind nur verfügbar, wenn Sie das Fahrrad im Laden gekauft/gemietet haben", + "ru": "Инструменты для починки доступны только при покупке/аренде велосипеда в магазине" } } ] @@ -563,7 +573,8 @@ "nl": "Deze winkel biedt fietsschoonmaak aan", "fr": "Ce magasin lave les vélos", "it": "Questo negozio lava le biciclette", - "de": "Dieses Geschäft reinigt Fahrräder" + "de": "Dieses Geschäft reinigt Fahrräder", + "ru": "В этом магазине оказываются услуги мойки/чистки велосипедов" } }, { @@ -583,7 +594,8 @@ "nl": "Deze winkel biedt geen fietsschoonmaak aan", "fr": "Ce magasin ne fait pas le nettoyage de vélo", "it": "Questo negozio non offre la pulizia della bicicletta", - "de": "Dieser Laden bietet keine Fahrradreinigung an" + "de": "Dieser Laden bietet keine Fahrradreinigung an", + "ru": "В этом магазине нет услуг мойки/чистки велосипедов" } } ] diff --git a/assets/layers/defibrillator/defibrillator.json b/assets/layers/defibrillator/defibrillator.json index 04e3d2925..4074ee963 100644 --- a/assets/layers/defibrillator/defibrillator.json +++ b/assets/layers/defibrillator/defibrillator.json @@ -567,7 +567,8 @@ "nl": "Extra informatie voor OpenStreetMap experts: {fixme}", "fr": "Informations supplémentaires pour les experts d'OpenStreetMap : {fixme}", "it": "Informazioni supplementari per gli esperti di OpenStreetMap: {fixme}", - "de": "Zusätzliche Informationen für OpenStreetMap-Experten: {fixme}" + "de": "Zusätzliche Informationen für OpenStreetMap-Experten: {fixme}", + "ru": "Дополнительная информация для экспертов OpenStreetMap: {fixme}" }, "question": { "en": "Is there something wrong with how this is mapped, that you weren't able to fix here? (leave a note to OpenStreetMap experts)", diff --git a/assets/layers/ghost_bike/ghost_bike.json b/assets/layers/ghost_bike/ghost_bike.json index b7951da3d..2a2b8342d 100644 --- a/assets/layers/ghost_bike/ghost_bike.json +++ b/assets/layers/ghost_bike/ghost_bike.json @@ -76,7 +76,8 @@ "nl": "Ter nagedachtenis van {name}", "de": "Im Gedenken an {name}", "it": "In ricordo di {name}", - "fr": "En souvenir de {name}" + "fr": "En souvenir de {name}", + "ru": "В знак памяти о {name}" }, "freeform": { "key": "name" @@ -149,7 +150,8 @@ "nl": "Geplaatst op {start_date}", "en": "Placed on {start_date}", "it": "Piazzata in data {start_date}", - "fr": "Placé le {start_date}" + "fr": "Placé le {start_date}", + "ru": "Установлен {start_date}" }, "freeform": { "key": "start_date", diff --git a/assets/themes/campersites/campersites.json b/assets/themes/campersites/campersites.json index 76039862d..3c580c5fd 100644 --- a/assets/themes/campersites/campersites.json +++ b/assets/themes/campersites/campersites.json @@ -23,7 +23,8 @@ "it": "Questo sito raccoglie tutti i luoghi ufficiali dove sostare con il camper e aree dove è possibile scaricare acque grigie e nere. Puoi aggiungere dettagli riguardanti i servizi forniti e il loro costo. Aggiungi foto e recensioni. Questo è al contempo un sito web e una web app. I dati sono memorizzati su OpenStreetMap in modo tale che siano per sempre liberi e riutilizzabili da qualsiasi app.", "ru": "На этом сайте собраны все официальные места остановки кемперов и места, где можно сбросить серую и черную воду. Вы можете добавить подробную информацию о предоставляемых услугах и их стоимости. Добавлять фотографии и отзывы. Это веб-сайт и веб-приложение. Данные хранятся в OpenStreetMap, поэтому они будут бесплатными всегда и могут быть повторно использованы любым приложением.", "ja": "このWebサイトでは、すべてのキャンピングカーの公式停車場所と、汚水を捨てることができる場所を収集します。提供されるサービスとコストに関する詳細を追加できます。写真とレビューを追加します。これはウェブサイトとウェブアプリです。データはOpenStreetMapに保存されるので、永遠に無料で、どんなアプリからでも再利用できます。", - "zh_Hant": "這個網站收集所有官方露營地點,以及那邊能排放廢水。你可以加上詳細的服務項目與價格,加上圖片以及評價。這是網站與網路 app,資料則是存在開放街圖,因此會永遠免費,而且可以被所有 app 再利用。" + "zh_Hant": "這個網站收集所有官方露營地點,以及那邊能排放廢水。你可以加上詳細的服務項目與價格,加上圖片以及評價。這是網站與網路 app,資料則是存在開放街圖,因此會永遠免費,而且可以被所有 app 再利用。", + "nl": "Deze website verzamelt en toont alle officiële plaatsen waar een camper mag overnachten en afvalwater kan lozen. Ook jij kan extra gegevens toevoegen, zoals welke services er geboden worden en hoeveel dit kot, ook afbeeldingen en reviews kan je toevoegen. De data wordt op OpenStreetMap opgeslaan en is dus altijd gratis te hergebruiken, ook door andere applicaties." }, "language": [ "en", @@ -53,7 +54,8 @@ "ru": "Площадки для кемпинга", "ja": "キャンプサイト", "fr": "Campings", - "zh_Hant": "露營地" + "zh_Hant": "露營地", + "nl": "Camperplaatsen" }, "minzoom": 10, "source": { @@ -71,7 +73,8 @@ "ru": "Место для кемпинга {name}", "ja": "キャンプサイト {name}", "fr": "Camping {name}", - "zh_Hant": "露營地 {name}" + "zh_Hant": "露營地 {name}", + "nl": "Camperplaats {name}" }, "mappings": [ { @@ -86,7 +89,8 @@ "ru": "Место для кемпинга без названия", "ja": "無名のキャンプサイト", "fr": "Camping sans nom", - "zh_Hant": "沒有名稱的露營地" + "zh_Hant": "沒有名稱的露營地", + "nl": "Camper site" } } ] @@ -97,7 +101,8 @@ "ru": "площадки для кемпинга", "ja": "キャンプサイト", "fr": "campings", - "zh_Hant": "露營地" + "zh_Hant": "露營地", + "nl": "camperplaatsen" }, "tagRenderings": [ "images", @@ -108,7 +113,8 @@ "ru": "Это место называется {name}", "ja": "この場所は {name} と呼ばれています", "fr": "Cet endroit s'appelle {nom}", - "zh_Hant": "這個地方叫做 {name}" + "zh_Hant": "這個地方叫做 {name}", + "nl": "Deze plaats heet {name}" }, "question": { "en": "What is this place called?", @@ -117,7 +123,8 @@ "it": "Come viene chiamato questo luogo?", "ja": "ここは何というところですか?", "fr": "Comment s'appelle cet endroit ?", - "zh_Hant": "這個地方叫做什麼?" + "zh_Hant": "這個地方叫做什麼?", + "nl": "Wat is de naam van deze plaats?" }, "freeform": { "key": "name" @@ -130,7 +137,8 @@ "ru": "Взимается ли в этом месте плата?", "ja": "ここは有料ですか?", "fr": "Cet endroit est-il payant ?", - "zh_Hant": "這個地方收費嗎?" + "zh_Hant": "這個地方收費嗎?", + "nl": "Moet men betalen om deze camperplaats te gebruiken?" }, "mappings": [ { @@ -144,7 +152,8 @@ "it": "Devi pagare per usarlo", "ru": "За использование нужно платить", "ja": "使用料を支払う必要がある", - "zh_Hant": "你要付費才能使用" + "zh_Hant": "你要付費才能使用", + "nl": "Gebruik is betalend" } }, { @@ -162,7 +171,8 @@ "ja": "無料で使用可能", "fr": "Peut être utilisé gratuitement", "nb_NO": "Kan brukes gratis", - "zh_Hant": "可以免費使用" + "zh_Hant": "可以免費使用", + "nl": "Kan gratis gebruikt worden" } }, { @@ -179,7 +189,8 @@ "ru": "Это место взимает {charge}", "ja": "この場所は{charge} が必要", "nb_NO": "Dette stedet tar {charge}", - "zh_Hant": "這個地方收費 {charge}" + "zh_Hant": "這個地方收費 {charge}", + "nl": "Deze plaats vraagt {charge}" }, "question": { "en": "How much does this place charge?", @@ -188,7 +199,8 @@ "ja": "ここはいくらかかりますか?", "fr": "Combien coûte cet endroit ?", "nb_NO": "pø", - "zh_Hant": "這個地方收多少費用?" + "zh_Hant": "這個地方收多少費用?", + "nl": "Hoeveel kost deze plaats?" }, "freeform": { "key": "charge" @@ -774,7 +786,8 @@ "question": { "en": "Who can use this dump station?", "ja": "このゴミ捨て場は誰が使えるんですか?", - "it": "Chi può utilizzare questo luogo di sversamento?" + "it": "Chi può utilizzare questo luogo di sversamento?", + "ru": "Кто может использовать эту станцию утилизации?" }, "mappings": [ { diff --git a/assets/themes/openwindpowermap/openwindpowermap.json b/assets/themes/openwindpowermap/openwindpowermap.json index 39c2baa41..2330b885e 100644 --- a/assets/themes/openwindpowermap/openwindpowermap.json +++ b/assets/themes/openwindpowermap/openwindpowermap.json @@ -1,165 +1,185 @@ { - "id": "openwindpowermap", - "title": { - "en": "OpenWindPowerMap" - }, - "maintainer": "Seppe Santens", - "icon": "./assets/themes/openwindpowermap/wind_turbine.svg", - "description": { - "en": "A map for showing and editing wind turbines." - }, - "language": [ - "en" - ], - "version": "2021-06-18", - "startLat": 50.520, - "startLon": 4.643, - "startZoom": 8, - "clustering": { + "id": "openwindpowermap", + "title": { + "en": "OpenWindPowerMap" + }, + "maintainer": "Seppe Santens", + "icon": "./assets/themes/openwindpowermap/wind_turbine.svg", + "description": { + "en": "A map for showing and editing wind turbines." + }, + "language": [ + "en", + "nl" + ], + "version": "2021-06-18", + "startLat": 50.52, + "startLon": 4.643, + "startZoom": 8, + "clustering": { "maxZoom": 8 }, - "layers": [ - { - "id": "windturbine", - "name": { - "en": "wind turbine" - }, - "source": { - "osmTags": "generator:source=wind" - }, - "minzoom": 10, - "wayHandling": 1, - "title": { - "render": { - "en": "wind turbine" - }, - "mappings": [ - { - "if": "name~*", - "then": { - "en": "{name}" - } - } - ] - }, - "icon": "./assets/themes/openwindpowermap/wind_turbine.svg", - "iconSize": "40, 40, bottom", - "label": { - "mappings": [ - { - "if": "generator:output:electricity~^[0-9]+.*[W]$", - "then": "
{generator:output:electricity}
" - } - ] - }, - "tagRenderings": [ - { - "render": { - "en": "The power output of this wind turbine is {generator:output:electricity}." - }, - "question": { - "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" - }, - "freeform": { - "key": "generator:output:electricity" - } - }, - { - "render": { - "en": "This wind turbine is operated by {operator}." - }, - "question": { - "en": "Who operates this wind turbine?" - }, - "freeform": { - "key": "operator" - } - }, - { - "render": { - "en": "The total height (including rotor radius) of this wind turbine is {height} metres." - }, - "question": { - "en": "What is the total height of this wind turbine (including rotor radius), in metres?" - }, - "freeform": { - "key": "height", - "type": "float" - } - }, - { - "render": { - "en": "The rotor diameter of this wind turbine is {rotor:diameter} metres." - }, - "question": { - "en": "What is the rotor diameter of this wind turbine, in metres?" - }, - "freeform": { - "key": "rotor:diameter", - "type": "float" - } - }, - { - "render": { - "en": "This wind turbine went into operation on/in {start_date}." - }, - "question": { - "en": "When did this wind turbine go into operation?" - }, - "freeform": { - "key": "start_date", - "type": "date" - } - }, - "images" - ], - "presets": [ - { - "tags": [ - "power=generator", - "generator:source=wind" - ], - "title": { - "en": "wind turbine" - } - } - ] - } - ], - "units": [ - { - "appliesToKey": ["generator:output:electricity"], - "applicableUnits": [{ - "canonicalDenomination": "MW", - "alternativeDenomination": ["megawatts","megawatt"], - "human": { - "en": " megawatts", - "nl": " megawatt" - } - },{ - "canonicalDenomination": "kW", - "alternativeDenomination": ["kilowatts","kilowatt"], - "human": { - "en": " kilowatts", - "nl": " kilowatt" - } - },{ - "canonicalDenomination": "W", - "alternativeDenomination": ["watts","watt"], - "human": { - "en": " watts", - "nl": " watt" - } - },{ - "canonicalDenomination": "GW", - "alternativeDenomination": ["gigawatts","gigawatt"], - "human": { - "en": " gigawatts", - "nl": " gigawatt" - } - }], - "eraseInvalidValues": true - } - ], - "defaultBackgroundId": "CartoDB.Voyager" -} + "layers": [ + { + "id": "windturbine", + "name": { + "en": "wind turbine" + }, + "source": { + "osmTags": "generator:source=wind" + }, + "minzoom": 10, + "wayHandling": 1, + "title": { + "render": { + "en": "wind turbine" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "en": "{name}" + } + } + ] + }, + "icon": "./assets/themes/openwindpowermap/wind_turbine.svg", + "iconSize": "40, 40, bottom", + "label": { + "mappings": [ + { + "if": "generator:output:electricity~^[0-9]+.*[W]$", + "then": "
{generator:output:electricity}
" + } + ] + }, + "tagRenderings": [ + { + "render": { + "en": "The power output of this wind turbine is {generator:output:electricity}." + }, + "question": { + "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" + }, + "freeform": { + "key": "generator:output:electricity" + } + }, + { + "render": { + "en": "This wind turbine is operated by {operator}." + }, + "question": { + "en": "Who operates this wind turbine?" + }, + "freeform": { + "key": "operator" + } + }, + { + "render": { + "en": "The total height (including rotor radius) of this wind turbine is {height} metres." + }, + "question": { + "en": "What is the total height of this wind turbine (including rotor radius), in metres?" + }, + "freeform": { + "key": "height", + "type": "float" + } + }, + { + "render": { + "en": "The rotor diameter of this wind turbine is {rotor:diameter} metres." + }, + "question": { + "en": "What is the rotor diameter of this wind turbine, in metres?" + }, + "freeform": { + "key": "rotor:diameter", + "type": "float" + } + }, + { + "render": { + "en": "This wind turbine went into operation on/in {start_date}." + }, + "question": { + "en": "When did this wind turbine go into operation?" + }, + "freeform": { + "key": "start_date", + "type": "date" + } + }, + "images" + ], + "presets": [ + { + "tags": [ + "power=generator", + "generator:source=wind" + ], + "title": { + "en": "wind turbine" + } + } + ] + } + ], + "units": [ + { + "appliesToKey": [ + "generator:output:electricity" + ], + "applicableUnits": [ + { + "canonicalDenomination": "MW", + "alternativeDenomination": [ + "megawatts", + "megawatt" + ], + "human": { + "en": " megawatts", + "nl": " megawatt" + } + }, + { + "canonicalDenomination": "kW", + "alternativeDenomination": [ + "kilowatts", + "kilowatt" + ], + "human": { + "en": " kilowatts", + "nl": " kilowatt" + } + }, + { + "canonicalDenomination": "W", + "alternativeDenomination": [ + "watts", + "watt" + ], + "human": { + "en": " watts", + "nl": " watt" + } + }, + { + "canonicalDenomination": "GW", + "alternativeDenomination": [ + "gigawatts", + "gigawatt" + ], + "human": { + "en": " gigawatts", + "nl": " gigawatt" + } + } + ], + "eraseInvalidValues": true + } + ], + "defaultBackgroundId": "CartoDB.Voyager" +} \ No newline at end of file diff --git a/langs/layers/fi.json b/langs/layers/fi.json index 09fd6f9a8..71e6a3ca1 100644 --- a/langs/layers/fi.json +++ b/langs/layers/fi.json @@ -1,12 +1,86 @@ { - "bike_repair_station": { - "presets": { - "0": { - "title": "Pyöräpumppu" + "bench": { + "name": "Penkit", + "title": { + "render": "Penkki" + }, + "tagRenderings": { + "1": { + "render": "Selkänoja", + "mappings": { + "0": { + "then": "Selkänoja: kyllä" + }, + "1": { + "then": "Selkänoja: ei" + } + } + }, + "3": { + "render": "Materiaali: {material}", + "mappings": { + "0": { + "then": "Materiaali: puu" + }, + "2": { + "then": "Materiaali: kivi" + }, + "3": { + "then": "Materiaali: betoni" + }, + "4": { + "then": "Materiaali: muovi" + }, + "5": { + "then": "Materiaali: teräs" + } + } + }, + "5": { + "render": "Väri: {colour}", + "mappings": { + "0": { + "then": "Väri: ruskea" + }, + "1": { + "then": "Väri: vihreä" + }, + "2": { + "then": "Väri: harmaa" + }, + "3": { + "then": "Väri: valkoinen" + }, + "4": { + "then": "Väri: punainen" + }, + "5": { + "then": "Väri: musta" + }, + "6": { + "then": "Väri: sininen" + }, + "7": { + "then": "Väri: keltainen" + } + } } }, - "icon": { - "render": "./assets/layers/bike_repair_station/repair_station.svg" + "presets": { + "0": { + "title": "Penkki", + "description": "Lisää uusi penkki" + } + } + }, + "bench_at_pt": { + "title": { + "render": "Penkki" + }, + "tagRenderings": { + "1": { + "render": "{name}" + } } }, "bike_parking": { @@ -16,88 +90,14 @@ } } }, - "bench_at_pt": { - "tagRenderings": { - "1": { - "render": "{name}" - } + "bike_repair_station": { + "icon": { + "render": "./assets/layers/bike_repair_station/repair_station.svg" }, - "title": { - "render": "Penkki" - } - }, - "bench": { "presets": { "0": { - "description": "Lisää uusi penkki", - "title": "Penkki" + "title": "Pyöräpumppu" } - }, - "tagRenderings": { - "5": { - "mappings": { - "7": { - "then": "Väri: keltainen" - }, - "6": { - "then": "Väri: sininen" - }, - "5": { - "then": "Väri: musta" - }, - "4": { - "then": "Väri: punainen" - }, - "3": { - "then": "Väri: valkoinen" - }, - "2": { - "then": "Väri: harmaa" - }, - "1": { - "then": "Väri: vihreä" - }, - "0": { - "then": "Väri: ruskea" - } - }, - "render": "Väri: {colour}" - }, - "3": { - "mappings": { - "5": { - "then": "Materiaali: teräs" - }, - "4": { - "then": "Materiaali: muovi" - }, - "3": { - "then": "Materiaali: betoni" - }, - "2": { - "then": "Materiaali: kivi" - }, - "0": { - "then": "Materiaali: puu" - } - }, - "render": "Materiaali: {material}" - }, - "1": { - "mappings": { - "1": { - "then": "Selkänoja: ei" - }, - "0": { - "then": "Selkänoja: kyllä" - } - }, - "render": "Selkänoja" - } - }, - "title": { - "render": "Penkki" - }, - "name": "Penkit" + } } -} +} \ No newline at end of file diff --git a/langs/layers/ru.json b/langs/layers/ru.json index d8905f92f..4f74e8815 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -204,11 +204,33 @@ "2": { "question": "Есть ли в этом велосипедном кафе велосипедный насос для всеобщего использования?", "mappings": { - "1": { - "then": "В этом велосипедном кафе нет велосипедного насоса для всеобщего использования" - }, "0": { "then": "В этом велосипедном кафе есть велосипедный насос для всеобщего использования" + }, + "1": { + "then": "В этом велосипедном кафе нет велосипедного насоса для всеобщего использования" + } + } + }, + "3": { + "question": "Есть ли здесь инструменты для починки вашего велосипеда?", + "mappings": { + "0": { + "then": "В этом велосипедном кафе есть инструменты для починки своего велосипеда" + }, + "1": { + "then": "В этом велосипедном кафе нет инструментов для починки своего велосипеда" + } + } + }, + "4": { + "question": "Есть ли услуги ремонта велосипедов в этом велосипедном кафе?", + "mappings": { + "0": { + "then": "В этом велосипедном кафе есть услуги ремонта велосипедов" + }, + "1": { + "then": "В этом велосипедном кафе нет услуг ремонта велосипедов" } } }, @@ -223,28 +245,6 @@ }, "8": { "question": "Каков режим работы этого велосипедного кафе?" - }, - "4": { - "mappings": { - "1": { - "then": "В этом велосипедном кафе нет услуг ремонта велосипедов" - }, - "0": { - "then": "В этом велосипедном кафе есть услуги ремонта велосипедов" - } - }, - "question": "Есть ли услуги ремонта велосипедов в этом велосипедном кафе?" - }, - "3": { - "mappings": { - "1": { - "then": "В этом велосипедном кафе нет инструментов для починки своего велосипеда" - }, - "0": { - "then": "В этом велосипедном кафе есть инструменты для починки своего велосипеда" - } - }, - "question": "Есть ли здесь инструменты для починки вашего велосипеда?" } }, "presets": { @@ -253,6 +253,9 @@ } } }, + "bike_monitoring_station": { + "name": "Станции мониторинга" + }, "bike_parking": { "tagRenderings": { "1": { @@ -272,22 +275,22 @@ } } }, - "5": { - "render": "{access}", - "question": "Кто может пользоваться этой велопарковкой?" + "3": { + "mappings": { + "0": { + "then": "Это крытая парковка (есть крыша/навес)" + }, + "1": { + "then": "Это открытая парковка" + } + } }, "4": { "render": "Место для {capacity} велосипеда(ов)" }, - "3": { - "mappings": { - "1": { - "then": "Это открытая парковка" - }, - "0": { - "then": "Это крытая парковка (есть крыша/навес)" - } - } + "5": { + "question": "Кто может пользоваться этой велопарковкой?", + "render": "{access}" } } }, @@ -303,6 +306,14 @@ } }, "tagRenderings": { + "3": { + "question": "Когда работает эта точка обслуживания велосипедов?", + "mappings": { + "0": { + "then": "Всегда открыто" + } + } + }, "6": { "question": "Велосипедный насос все еще работает?", "mappings": { @@ -348,14 +359,6 @@ "then": "Есть манометр, но он сломан" } } - }, - "3": { - "question": "Когда работает эта точка обслуживания велосипедов?", - "mappings": { - "0": { - "then": "Всегда открыто" - } - } } }, "icon": { @@ -368,7 +371,9 @@ } }, "bike_shop": { + "name": "Обслуживание велосипедов/магазин", "title": { + "render": "Обслуживание велосипедов/магазин", "mappings": { "0": { "then": "Магазин спортивного инвентаря {name}" @@ -382,8 +387,7 @@ "4": { "then": "Магазин велосипедов {name}" } - }, - "render": "Обслуживание велосипедов/магазин" + } }, "description": "Магазин, специализирующийся на продаже велосипедов или сопутствующих товаров", "tagRenderings": { @@ -401,6 +405,7 @@ "question": "Какой адрес электронной почты у {name}?" }, "9": { + "question": "Продаются ли велосипеды в этом магазине?", "mappings": { "0": { "then": "В этом магазине продаются велосипеды" @@ -408,8 +413,7 @@ "1": { "then": "В этом магазине не продают велосипеды" } - }, - "question": "Продаются ли велосипеды в этом магазине?" + } }, "10": { "question": "В этом магазине ремонтируют велосипеды?", @@ -453,38 +457,37 @@ } } }, - "15": { - "question": "Здесь моют велосипеды?", - "mappings": { - "2": { - "then": "В этом магазине нет услуг мойки/чистки велосипедов" - }, - "0": { - "then": "В этом магазине оказываются услуги мойки/чистки велосипедов" - } - } - }, "13": { "question": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?", "mappings": { - "1": { - "then": "В этом магазине нет велосипедного насоса для всеобщего пользования" - }, "0": { "then": "В этом магазине есть велосипедный насос для всеобщего пользования" + }, + "1": { + "then": "В этом магазине нет велосипедного насоса для всеобщего пользования" } } }, "14": { + "question": "Есть ли здесь инструменты для починки собственного велосипеда?", "mappings": { "2": { "then": "Инструменты для починки доступны только при покупке/аренде велосипеда в магазине" } - }, - "question": "Есть ли здесь инструменты для починки собственного велосипеда?" + } + }, + "15": { + "question": "Здесь моют велосипеды?", + "mappings": { + "0": { + "then": "В этом магазине оказываются услуги мойки/чистки велосипедов" + }, + "2": { + "then": "В этом магазине нет услуг мойки/чистки велосипедов" + } + } } - }, - "name": "Обслуживание велосипедов/магазин" + } }, "defibrillator": { "name": "Дефибрилляторы", @@ -539,6 +542,9 @@ }, "ghost_bike": { "tagRenderings": { + "2": { + "render": "В знак памяти о {name}" + }, "3": { "render": "Доступна более подробная информация" }, @@ -547,9 +553,6 @@ }, "5": { "render": "Установлен {start_date}" - }, - "2": { - "render": "В знак памяти о {name}" } } }, @@ -885,8 +888,5 @@ "question": "Вы хотите добавить описание?" } } - }, - "bike_monitoring_station": { - "name": "Станции мониторинга" } -} +} \ No newline at end of file diff --git a/langs/themes/en.json b/langs/themes/en.json index ff61b8782..48852d5a4 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -1084,6 +1084,68 @@ "shortDescription": "This theme shows all (touristic) maps that OpenStreetMap knows of", "description": "On this map you can find all maps OpenStreetMap knows - typically a big map on an information board showing the area, city or region, e.g. a tourist map on the back of a billboard, a map of a nature reserve, a map of cycling networks in the region, ...)

If a map is missing, you can easily map this map on OpenStreetMap." }, + "openwindpowermap": { + "title": "OpenWindPowerMap", + "description": "A map for showing and editing wind turbines.", + "layers": { + "0": { + "name": "wind turbine", + "title": { + "render": "wind turbine", + "mappings": { + "0": { + "then": "{name}" + } + } + }, + "tagRenderings": { + "0": { + "render": "The power output of this wind turbine is {generator:output:electricity}.", + "question": "What is the power output of this wind turbine? (e.g. 2.3 MW)" + }, + "1": { + "render": "This wind turbine is operated by {operator}.", + "question": "Who operates this wind turbine?" + }, + "2": { + "render": "The total height (including rotor radius) of this wind turbine is {height} metres.", + "question": "What is the total height of this wind turbine (including rotor radius), in metres?" + }, + "3": { + "render": "The rotor diameter of this wind turbine is {rotor:diameter} metres.", + "question": "What is the rotor diameter of this wind turbine, in metres?" + }, + "4": { + "render": "This wind turbine went into operation on/in {start_date}.", + "question": "When did this wind turbine go into operation?" + } + }, + "presets": { + "0": { + "title": "wind turbine" + } + } + } + }, + "units": { + "0": { + "applicableUnits": { + "0": { + "human": " megawatts" + }, + "1": { + "human": " kilowatts" + }, + "2": { + "human": " watts" + }, + "3": { + "human": " gigawatts" + } + } + } + } + }, "personal": { "title": "Personal theme", "description": "Create a personal theme based on all the available layers of all themes" diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 6746eff92..ce6dfd8fc 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -241,42 +241,42 @@ "campersite": { "title": "Kampeersite", "shortDescription": "Vind locaties waar je de nacht kan doorbrengen met je mobilehome", + "description": "Deze website verzamelt en toont alle officiële plaatsen waar een camper mag overnachten en afvalwater kan lozen. Ook jij kan extra gegevens toevoegen, zoals welke services er geboden worden en hoeveel dit kot, ook afbeeldingen en reviews kan je toevoegen. De data wordt op OpenStreetMap opgeslaan en is dus altijd gratis te hergebruiken, ook door andere applicaties.", "layers": { "0": { "name": "Camperplaatsen", - "tagRenderings": { - "3": { - "question": "Hoeveel kost deze plaats?", - "render": "Deze plaats vraagt {charge}" - }, - "2": { - "mappings": { - "1": { - "then": "Kan gratis gebruikt worden" - }, - "0": { - "then": "Gebruik is betalend" - } - }, - "question": "Moet men betalen om deze camperplaats te gebruiken?" - }, - "1": { - "question": "Wat is de naam van deze plaats?", - "render": "Deze plaats heet {name}" - } - }, - "description": "camperplaatsen", "title": { + "render": "Camperplaats {name}", "mappings": { "0": { "then": "Camper site" } + } + }, + "description": "camperplaatsen", + "tagRenderings": { + "1": { + "render": "Deze plaats heet {name}", + "question": "Wat is de naam van deze plaats?" }, - "render": "Camperplaats {name}" + "2": { + "question": "Moet men betalen om deze camperplaats te gebruiken?", + "mappings": { + "0": { + "then": "Gebruik is betalend" + }, + "1": { + "then": "Kan gratis gebruikt worden" + } + } + }, + "3": { + "render": "Deze plaats vraagt {charge}", + "question": "Hoeveel kost deze plaats?" + } } } - }, - "description": "Deze website verzamelt en toont alle officiële plaatsen waar een camper mag overnachten en afvalwater kan lozen. Ook jij kan extra gegevens toevoegen, zoals welke services er geboden worden en hoeveel dit kot, ook afbeeldingen en reviews kan je toevoegen. De data wordt op OpenStreetMap opgeslaan en is dus altijd gratis te hergebruiken, ook door andere applicaties." + } }, "climbing": { "title": "Open Klimkaart", @@ -890,6 +890,26 @@ "shortDescription": "Deze kaart bevat informatie voor natuurliefhebbers", "description": "Op deze kaart vind je informatie voor natuurliefhebbers, zoals info over het natuurgebied waar je inzit, vogelkijkhutten, informatieborden, ..." }, + "openwindpowermap": { + "units": { + "0": { + "applicableUnits": { + "0": { + "human": " megawatt" + }, + "1": { + "human": " kilowatt" + }, + "2": { + "human": " watt" + }, + "3": { + "human": " gigawatt" + } + } + } + } + }, "personal": { "title": "Persoonlijk thema", "description": "Stel je eigen thema samen door lagen te combineren van alle andere themas" @@ -1023,4 +1043,4 @@ } } } -} +} \ No newline at end of file diff --git a/langs/themes/ru.json b/langs/themes/ru.json index c2d70c10f..cb0a7dbe7 100644 --- a/langs/themes/ru.json +++ b/langs/themes/ru.json @@ -525,4 +525,4 @@ "trees": { "title": "Деревья" } -} +} \ No newline at end of file From e594511e22fda8d5c48720c2f03fbf4c6b3fcd6a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:03:17 +0200 Subject: [PATCH 08/60] Better trimming of canonical values, no console output if not actually rewriting --- Customizations/JSON/Denomination.ts | 2 +- Logic/SimpleMetaTagger.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Customizations/JSON/Denomination.ts b/Customizations/JSON/Denomination.ts index 09c5ab977..8331d8adc 100644 --- a/Customizations/JSON/Denomination.ts +++ b/Customizations/JSON/Denomination.ts @@ -152,7 +152,7 @@ export class Denomination { if (stripped === null) { return null; } - return stripped + " " + this.canonical.trim() + return (stripped + " " + this.canonical.trim()).trim(); } /** diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index c6269e222..4453c2b4f 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -95,7 +95,10 @@ 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 === value){ + break; + } + console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) if(canonical === undefined && !unit.eraseInvalid) { break; } From 2dbdcaa2ba6694bdce3ec37b6b9a43bc7fe26081 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:03:41 +0200 Subject: [PATCH 09/60] Move TileRange to seperate file --- Models/TileRange.ts | 1 - Utils.ts | 12 ++---------- scripts/generateCache.ts | 3 ++- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Models/TileRange.ts b/Models/TileRange.ts index f3c59af23..e1dba5532 100644 --- a/Models/TileRange.ts +++ b/Models/TileRange.ts @@ -5,5 +5,4 @@ export interface TileRange { yend: number, total: number, zoomlevel: number - } \ No newline at end of file diff --git a/Utils.ts b/Utils.ts index 05ebcbaab..cb8835656 100644 --- a/Utils.ts +++ b/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) { @@ -449,12 +450,3 @@ export class Utils { } } -export interface TileRange { - xstart: number, - ystart: number, - xend: number, - yend: number, - total: number, - zoomlevel: number - -} \ No newline at end of file diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index 77dfa8653..f7651c4b4 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -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,7 @@ import MetaTagging from "../Logic/MetaTagging"; import LayerConfig from "../Customizations/JSON/LayerConfig"; import {GeoOperations} from "../Logic/GeoOperations"; import {UIEventSource} from "../Logic/UIEventSource"; +import {TileRange} from "../Models/TileRange"; function createOverpassObject(theme: LayoutConfig) { From 0306a2881100b8551c21b3e410d9db6ccbcf81f8 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:07:27 +0200 Subject: [PATCH 10/60] Disable zoom and fade animations on minimaps --- UI/Base/Minimap.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 647fade47..73bf2354a 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -44,7 +44,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() }); @@ -82,7 +81,9 @@ export default class Minimap extends BaseUIElement { scrollWheelZoom: this._allowMoving, doubleClickZoom: this._allowMoving, keyboard: this._allowMoving, - touchZoom: this._allowMoving + touchZoom: this._allowMoving, + zoomAnimation: this._allowMoving, + fadeAnimation: this._allowMoving }); map.setMaxBounds( From 1187d5b807040d0f6e2d738afd4e7a16cb411d7b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:07:44 +0200 Subject: [PATCH 11/60] Disable zoom animation --- UI/ShowDataLayer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 711f6b1c5..df45af45e 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -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) } From 58ac26b0b30d85fc3f909073ddfde2dcb97b1c71 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:23:32 +0200 Subject: [PATCH 12/60] Fix warnings in climbing theme --- assets/themes/climbing/climbing.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index aa8d11310..33463ccdb 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -727,7 +727,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" ] }, { @@ -1400,8 +1400,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" From 3ecfef9bc1e50ad3affd3ff07c758840ad0385bd Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 10 Jul 2021 21:41:56 +0200 Subject: [PATCH 13/60] Restore small icons in attribution, fix #413 --- Models/Constants.ts | 2 +- UI/BigComponents/Basemap.ts | 5 +++-- index.css | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 33a52c40f..48c52eebf 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.4"; + public static vNumber = "0.8.4-rc1"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index 2da6415b6..a4afd6ec8 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -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( - " | OpenStreetMap"); + "A"); - 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') } diff --git a/index.css b/index.css index 47178a06f..347c27a5d 100644 --- a/index.css +++ b/index.css @@ -82,6 +82,10 @@ html, body { box-sizing: initial !important; } +.leaflet-control-attribution { + display: block ruby; +} + svg, img { box-sizing: content-box; width: 100%; From a78e3866e04d40ac73926e4c0d379da1452eb403 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 11 Jul 2021 12:52:33 +0200 Subject: [PATCH 14/60] Add GetParsed to localStorageSource --- Logic/Osm/Changes.ts | 4 ++-- Logic/Web/LocalStorageSource.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 9da36b048..39b9f4e02 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -25,13 +25,13 @@ export class Changes implements FeatureSource { /** * All the pending changes */ - public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> = LocalStorageSource.GetParsed("pending-changes", []) + public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) /** * All the pending new objects to upload * @private */ - private readonly newObjects: UIEventSource<{ id: number, lat: number, lon: number }[]> = LocalStorageSource.GetParsed("newObjects", []) + private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) /** * Adds a change to the pending changes diff --git a/Logic/Web/LocalStorageSource.ts b/Logic/Web/LocalStorageSource.ts index a89d2a556..61009114a 100644 --- a/Logic/Web/LocalStorageSource.ts +++ b/Logic/Web/LocalStorageSource.ts @@ -5,7 +5,7 @@ import {UIEventSource} from "../UIEventSource"; */ export class LocalStorageSource { - static GetParsed(key: string, defaultValue : any){ + static GetParsed(key: string, defaultValue : T) : UIEventSource{ return LocalStorageSource.Get(key).map( str => { if(str === undefined){ From 6e3c39e4758d5bfa832216e73e08cfca91b4b256 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 11 Jul 2021 15:44:17 +0200 Subject: [PATCH 15/60] Add option to show text field inline with the rendering; add option to fill out a default value --- Customizations/JSON/TagRenderingConfig.ts | 4 ++ Customizations/JSON/TagRenderingConfigJson.ts | 14 ++++- UI/Input/InputElementWrapper.ts | 35 +++++++++++ UI/Input/TextField.ts | 2 +- UI/Input/ValidatedTextField.ts | 2 +- UI/Popup/TagRenderingQuestion.ts | 22 +++++-- UI/SpecialVisualizations.ts | 1 + UI/SubstitutedTranslation.ts | 59 ++++++++++++++----- .../public_bookcase/public_bookcase.json | 3 +- index.css | 4 ++ 10 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 UI/Input/InputElementWrapper.ts diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index d7e55ed8d..d3d440493 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -26,6 +26,8 @@ export default class TagRenderingConfig { readonly key: string, readonly type: string, readonly addExtraTags: TagsFilter[]; + readonly inline: boolean, + readonly default?: string }; readonly multiAnswer: boolean; @@ -73,6 +75,8 @@ 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 } diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 7dfaae82b..89871ec74 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -46,7 +46,19 @@ export interface TagRenderingConfigJson { **/ 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 }, /** diff --git a/UI/Input/InputElementWrapper.ts b/UI/Input/InputElementWrapper.ts new file mode 100644 index 000000000..765a0d3b4 --- /dev/null +++ b/UI/Input/InputElementWrapper.ts @@ -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 extends InputElement { + public readonly IsSelected: UIEventSource; + private readonly _inputElement: InputElement; + private readonly _renderElement: BaseUIElement + + constructor(inputElement: InputElement, translation: Translation, key: string, tags: UIEventSource) { + super() + this._inputElement = inputElement; + this.IsSelected = inputElement.IsSelected + const mapping = new Map() + + mapping.set(key, inputElement) + + this._renderElement = new SubstitutedTranslation(translation, tags, mapping) + } + + GetValue(): UIEventSource { + return this._inputElement.GetValue(); + } + + IsValid(t: T): boolean { + return this._inputElement.IsValid(t); + } + + protected InnerConstructElement(): HTMLElement { + return this._renderElement.ConstructElement(); + } + +} \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index 8f7d6ac44..da3073323 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -36,11 +36,11 @@ export class TextField extends InputElement { 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") diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 8ea3fb948..2eeff8a54 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -282,7 +282,7 @@ export default class ValidatedTextField { }) ) unitDropDown.GetValue().setData(unit.defaultDenom) - unitDropDown.SetStyle("width: min-content") + unitDropDown.SetClass("w-min") input = new CombinedInputElement( input, diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 52b2962d8..20c0b00d2 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -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 { + private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource): InputElement { const freeform = configuration.freeform; if (freeform === undefined) { return undefined; @@ -328,7 +329,8 @@ export default class TagRenderingQuestion extends Combine { return undefined; } - let input: InputElement = ValidatedTextField.InputForType(configuration.freeform.type, { + const tagsData = tags.data; + const input: InputElement = ValidatedTextField.InputForType(configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => tagsData._country, location: [tagsData._lat, tagsData._lon], @@ -336,12 +338,22 @@ export default class TagRenderingQuestion extends Combine { unit: applicableUnit }); - input.GetValue().setData(tagsData[configuration.freeform.key]); + input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); - return new InputElementMap( + let inputTagsFilter : InputElement = 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; } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 346e71e47..2bbcbbb34 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -379,6 +379,7 @@ export default class SpecialVisualizations { } ] + static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); private static GenHelpMessage() { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 03c7eb074..43352aa5b 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -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) { + tagsSource: UIEventSource, + mapping: Map = 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] } } diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 896f97cea..496b36ae1 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -137,7 +137,8 @@ }, "freeform": { "key": "capacity", - "type": "nat" + "type": "nat", + "inline": true } }, { diff --git a/index.css b/index.css index 347c27a5d..0bd790f55 100644 --- a/index.css +++ b/index.css @@ -105,6 +105,10 @@ a { width: min-content; } +.w-16-imp { + width: 4rem !important; +} + .space-between{ justify-content: space-between; } From 38231399c4bd92f033e9ef8abd483cc0ee742f58 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 11 Jul 2021 15:47:37 +0200 Subject: [PATCH 16/60] Use rounded-3xl for radio buttons instead of rounded-full --- UI/Input/RadioButton.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index fd5c006c2..2822b2166 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -103,7 +103,7 @@ export class RadioButton extends InputElement { 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) From 4eb131b3b103548c3be66daa1ea10357d1f12947 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 12 Jul 2021 00:05:56 +0200 Subject: [PATCH 17/60] Clip corners of the input-element-cone, which sometimes hid the save button resulting in unexpected click behaviour --- UI/Input/DirectionInput.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 12689d5d4..93d932c66 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -66,6 +66,7 @@ export default class DirectionInput extends InputElement { }) this.RegisterTriggers(element) + element.style.overflow = "hidden" return element; } From 4276d4a47d92f8ef4901a036ee7849ead24b8235 Mon Sep 17 00:00:00 2001 From: Ward Date: Mon, 12 Jul 2021 11:34:01 +0200 Subject: [PATCH 18/60] auto generated stuff --- assets/layers/picnic_table/picnic_table.json | 9 +- assets/layers/playground/playground.json | 36 ++- .../public_bookcase/public_bookcase.json | 29 +- assets/layers/sport_pitch/sport_pitch.json | 51 ++-- .../surveillance_camera.json | 15 +- assets/layers/toilet/toilet.json | 24 +- assets/layers/tree_node/tree_node.json | 33 +- .../charging_stations/charging_stations.json | 13 +- assets/themes/shops/shops.json | 37 ++- langs/layers/ru.json | 284 +++++++++--------- langs/themes/nl.json | 112 +++---- 11 files changed, 363 insertions(+), 280 deletions(-) diff --git a/assets/layers/picnic_table/picnic_table.json b/assets/layers/picnic_table/picnic_table.json index e5be9d077..672e41f07 100644 --- a/assets/layers/picnic_table/picnic_table.json +++ b/assets/layers/picnic_table/picnic_table.json @@ -26,7 +26,8 @@ "en": "The layer showing picnic tables", "nl": "Deze laag toont picnictafels", "it": "Il livello che mostra i tavoli da picnic", - "fr": "La couche montrant les tables de pique-nique" + "fr": "La couche montrant les tables de pique-nique", + "ru": "Слой, отображающий столы для пикника" }, "tagRenderings": [ { @@ -34,13 +35,15 @@ "en": "What material is this picnic table made of?", "nl": "Van welk materiaal is deze picnictafel gemaakt?", "it": "Di che materiale è fatto questo tavolo da picnic?", - "de": "Aus welchem Material besteht dieser Picknicktisch?" + "de": "Aus welchem Material besteht dieser Picknicktisch?", + "ru": "Из чего изготовлен этот стол для пикника?" }, "render": { "en": "This picnic table is made of {material}", "nl": "Deze picnictafel is gemaakt van {material}", "it": "Questo tavolo da picnic è fatto di {material}", - "de": "Dieser Picknicktisch besteht aus {material}" + "de": "Dieser Picknicktisch besteht aus {material}", + "ru": "Этот стол для пикника сделан из {material}" }, "freeform": { "key": "material" diff --git a/assets/layers/playground/playground.json b/assets/layers/playground/playground.json index 54afa9a4c..ec97eca2b 100644 --- a/assets/layers/playground/playground.json +++ b/assets/layers/playground/playground.json @@ -93,7 +93,8 @@ "nl": "De ondergrond bestaat uit houtsnippers", "en": "The surface consist of woodchips", "it": "La superficie consiste di trucioli di legno", - "de": "Die Oberfläche besteht aus Holzschnitzeln" + "de": "Die Oberfläche besteht aus Holzschnitzeln", + "ru": "Покрытие из щепы" } }, { @@ -154,7 +155,8 @@ "en": "Is this playground lit at night?", "it": "È illuminato di notte questo parco giochi?", "fr": "Ce terrain de jeux est-il éclairé la nuit ?", - "de": "Ist dieser Spielplatz nachts beleuchtet?" + "de": "Ist dieser Spielplatz nachts beleuchtet?", + "ru": "Эта игровая площадка освещается ночью?" }, "mappings": [ { @@ -163,7 +165,8 @@ "nl": "Deze speeltuin is 's nachts verlicht", "en": "This playground is lit at night", "it": "Questo parco giochi è illuminato di notte", - "de": "Dieser Spielplatz ist nachts beleuchtet" + "de": "Dieser Spielplatz ist nachts beleuchtet", + "ru": "Эта детская площадка освещается ночью" } }, { @@ -172,7 +175,8 @@ "nl": "Deze speeltuin is 's nachts niet verlicht", "en": "This playground is not lit at night", "it": "Questo parco giochi non è illuminato di notte", - "de": "Dieser Spielplatz ist nachts nicht beleuchtet" + "de": "Dieser Spielplatz ist nachts nicht beleuchtet", + "ru": "Эта детская площадка не освещается ночью" } } ] @@ -189,7 +193,8 @@ "nl": "Wat is de minimale leeftijd om op deze speeltuin te mogen?", "en": "What is the minimum age required to access this playground?", "it": "Qual è l’età minima per accedere a questo parco giochi?", - "fr": "Quel est l'âge minimal requis pour accéder à ce terrain de jeux ?" + "fr": "Quel est l'âge minimal requis pour accéder à ce terrain de jeux ?", + "ru": "С какого возраста доступна эта детская площадка?" }, "freeform": { "key": "min_age", @@ -201,7 +206,8 @@ "nl": "Toegankelijk tot {max_age}", "en": "Accessible to kids of at most {max_age}", "it": "Accessibile ai bambini di età inferiore a {max_age}", - "fr": "Accessible aux enfants de {max_age} au maximum" + "fr": "Accessible aux enfants de {max_age} au maximum", + "ru": "Доступно детям до {max_age}" }, "question": { "nl": "Wat is de maximaal toegestane leeftijd voor deze speeltuin?", @@ -340,7 +346,8 @@ "en": "Is this playground accessible to wheelchair users?", "fr": "Ce terrain de jeux est-il accessible aux personnes en fauteuil roulant ?", "de": "Ist dieser Spielplatz für Rollstuhlfahrer zugänglich?", - "it": "Il campetto è accessibile a persone in sedia a rotelle?" + "it": "Il campetto è accessibile a persone in sedia a rotelle?", + "ru": "Доступна ли детская площадка пользователям кресел-колясок?" }, "mappings": [ { @@ -350,7 +357,8 @@ "en": "Completely accessible for wheelchair users", "fr": "Entièrement accessible aux personnes en fauteuil roulant", "de": "Vollständig zugänglich für Rollstuhlfahrer", - "it": "Completamente accessibile in sedia a rotelle" + "it": "Completamente accessibile in sedia a rotelle", + "ru": "Полностью доступна пользователям кресел-колясок" } }, { @@ -360,7 +368,8 @@ "en": "Limited accessibility for wheelchair users", "fr": "Accessibilité limitée pour les personnes en fauteuil roulant", "de": "Eingeschränkte Zugänglichkeit für Rollstuhlfahrer", - "it": "Accesso limitato in sedia a rotelle" + "it": "Accesso limitato in sedia a rotelle", + "ru": "Частично доступна пользователям кресел-колясок" } }, { @@ -370,7 +379,8 @@ "en": "Not accessible for wheelchair users", "fr": "Non accessible aux personnes en fauteuil roulant", "de": "Nicht zugänglich für Rollstuhlfahrer", - "it": "Non accessibile in sedia a rotelle" + "it": "Non accessibile in sedia a rotelle", + "ru": "Недоступна пользователям кресел-колясок" } } ] @@ -385,7 +395,8 @@ "nl": "Op welke uren is deze speeltuin toegankelijk?", "en": "When is this playground accessible?", "fr": "Quand ce terrain de jeux est-il accessible ?", - "it": "Quando si può accedere a questo campetto?" + "it": "Quando si può accedere a questo campetto?", + "ru": "Когда открыта эта игровая площадка?" }, "mappings": [ { @@ -394,7 +405,8 @@ "nl": "Van zonsopgang tot zonsondergang", "en": "Accessible from sunrise till sunset", "fr": "Accessible du lever au coucher du soleil", - "it": "Si può accedere dall'alba al tramonto" + "it": "Si può accedere dall'alba al tramonto", + "ru": "Открыто от рассвета до заката" } }, { diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 496b36ae1..fdb223d06 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -13,7 +13,8 @@ "nl": "Een straatkastje met boeken voor iedereen", "de": "Ein Bücherschrank am Straßenrand mit Büchern, für jedermann zugänglich", "fr": "Une armoire ou une boite contenant des livres en libre accès", - "it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico" + "it": "Una vetrinetta ai bordi della strada contenente libri, aperta al pubblico", + "ru": "Уличный шкаф с книгами, доступными для всех" }, "source": { "osmTags": "amenity=public_bookcase" @@ -94,7 +95,7 @@ "nl": "Wat is de naam van dit boekenuilkastje?", "de": "Wie heißt dieser öffentliche Bücherschrank?", "fr": "Quel est le nom de cette microbibliothèque ?", - "ru": "Как называется общественный книжный шкаф?", + "ru": "Как называется этот общественный книжный шкаф?", "it": "Come si chiama questa microbiblioteca pubblica?" }, "freeform": { @@ -125,7 +126,8 @@ "nl": "Er passen {capacity} boeken", "de": "{capacity} Bücher passen in diesen Bücherschrank", "fr": "{capacity} livres peuvent entrer dans cette microbibliothèque", - "it": "Questa microbiblioteca può contenere fino a {capacity} libri" + "it": "Questa microbiblioteca può contenere fino a {capacity} libri", + "ru": "{capacity} книг помещается в этот книжный шкаф" }, "question": { "en": "How many books fit into this public bookcase?", @@ -147,7 +149,8 @@ "nl": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?", "de": "Welche Art von Büchern sind in diesem öffentlichen Bücherschrank zu finden?", "fr": "Quel type de livres peut-on dans cette microbibliothèque ?", - "it": "Che tipo di libri si possono trovare in questa microbiblioteca?" + "it": "Che tipo di libri si possono trovare in questa microbiblioteca?", + "ru": "Какие книги можно найти в этом общественном книжном шкафу?" }, "mappings": [ { @@ -179,7 +182,8 @@ "nl": "Boeken voor zowel kinderen als volwassenen", "de": "Sowohl Bücher für Kinder als auch für Erwachsene", "fr": "Livres pour enfants et adultes également", - "it": "Sia libri per l'infanzia, sia per l'età adulta" + "it": "Sia libri per l'infanzia, sia per l'età adulta", + "ru": "Книги и для детей, и для взрослых" } } ] @@ -232,7 +236,8 @@ "nl": "Is dit boekenruilkastje publiek toegankelijk?", "de": "Ist dieser öffentliche Bücherschrank frei zugänglich?", "fr": "Cette microbibliothèque est-elle librement accèssible ?", - "it": "Questa microbiblioteca è ad accesso libero?" + "it": "Questa microbiblioteca è ad accesso libero?", + "ru": "Имеется ли свободный доступ к этому общественному книжному шкафу?" }, "condition": "indoor=yes", "mappings": [ @@ -242,7 +247,8 @@ "nl": "Publiek toegankelijk", "de": "Öffentlich zugänglich", "fr": "Accèssible au public", - "it": "È ad accesso libero" + "it": "È ad accesso libero", + "ru": "Свободный доступ" }, "if": "access=yes" }, @@ -374,14 +380,16 @@ "nl": "Op welke dag werd dit boekenruilkastje geinstalleerd?", "de": "Wann wurde dieser öffentliche Bücherschrank installiert?", "fr": "Quand a été installée cette microbibliothèque ?", - "it": "Quando è stata inaugurata questa microbiblioteca?" + "it": "Quando è stata inaugurata questa microbiblioteca?", + "ru": "Когда был установлен этот общественный книжный шкаф?" }, "render": { "en": "Installed on {start_date}", "nl": "Geplaatst op {start_date}", "de": "Installiert am {start_date}", "fr": "Installée le {start_date}", - "it": "È stata inaugurata il {start_date}" + "it": "È stata inaugurata il {start_date}", + "ru": "Установлен {start_date}" }, "freeform": { "key": "start_date", @@ -402,7 +410,8 @@ "nl": "Is er een website over dit boekenruilkastje?", "de": "Gibt es eine Website mit weiteren Informationen über diesen öffentlichen Bücherschrank?", "fr": "Y a-t-il un site web avec plus d'informations sur cette microbibliothèque ?", - "it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?" + "it": "C'è un sito web con maggiori informazioni su questa microbiblioteca?", + "ru": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?" }, "freeform": { "key": "website", diff --git a/assets/layers/sport_pitch/sport_pitch.json b/assets/layers/sport_pitch/sport_pitch.json index efb49a0b2..140e222b2 100644 --- a/assets/layers/sport_pitch/sport_pitch.json +++ b/assets/layers/sport_pitch/sport_pitch.json @@ -32,7 +32,8 @@ "nl": "Een sportterrein", "fr": "Un terrain de sport", "en": "A sport pitch", - "it": "Un campo sportivo" + "it": "Un campo sportivo", + "ru": "Спортивная площадка" }, "tagRenderings": [ "images", @@ -64,7 +65,8 @@ "nl": "Hier kan men basketbal spelen", "fr": "Ici, on joue au basketball", "en": "Basketball is played here", - "it": "Qui si gioca a basket" + "it": "Qui si gioca a basket", + "ru": "Здесь можно играть в баскетбол" } }, { @@ -77,7 +79,8 @@ "nl": "Hier kan men voetbal spelen", "fr": "Ici, on joue au football", "en": "Soccer is played here", - "it": "Qui si gioca a calcio" + "it": "Qui si gioca a calcio", + "ru": "Здесь можно играть в футбол" } }, { @@ -104,7 +107,8 @@ "nl": "Hier kan men tennis spelen", "fr": "Ici, on joue au tennis", "en": "Tennis is played here", - "it": "Qui si gioca a tennis" + "it": "Qui si gioca a tennis", + "ru": "Здесь можно играть в теннис" } }, { @@ -117,7 +121,8 @@ "nl": "Hier kan men korfbal spelen", "fr": "Ici, on joue au korfball", "en": "Korfball is played here", - "it": "Qui si gioca a korfball" + "it": "Qui si gioca a korfball", + "ru": "Здесь можно играть в корфбол" } }, { @@ -130,7 +135,8 @@ "nl": "Hier kan men basketbal beoefenen", "fr": "Ici, on joue au basketball", "en": "Basketball is played here", - "it": "Qui si gioca a basket" + "it": "Qui si gioca a basket", + "ru": "Здесь можно играть в баскетбол" }, "hideInAnswer": true } @@ -141,7 +147,8 @@ "nl": "Wat is de ondergrond van dit sportveld?", "fr": "De quelle surface est fait ce terrain de sport ?", "en": "Which is the surface of this sport pitch?", - "it": "Qual è la superficie di questo campo sportivo?" + "it": "Qual è la superficie di questo campo sportivo?", + "ru": "Какое покрытие на этой спортивной площадке?" }, "render": { "nl": "De ondergrond is {surface}", @@ -211,7 +218,8 @@ "nl": "Is dit sportterrein publiek toegankelijk?", "fr": "Est-ce que ce terrain de sport est accessible au public ?", "en": "Is this sport pitch publicly accessible?", - "it": "Questo campo sportivo è aperto al pubblico?" + "it": "Questo campo sportivo è aperto al pubblico?", + "ru": "Есть ли свободный доступ к этой спортивной площадке?" }, "mappings": [ { @@ -220,7 +228,8 @@ "nl": "Publiek toegankelijk", "fr": "Accessible au public", "en": "Public access", - "it": "Aperto al pubblico" + "it": "Aperto al pubblico", + "ru": "Свободный доступ" } }, { @@ -229,7 +238,8 @@ "nl": "Beperkt toegankelijk (enkel na reservatie, tijdens bepaalde uren, ...)", "fr": "Accès limité (par exemple uniquement sur réservation, à certains horaires…)", "en": "Limited access (e.g. only with an appointment, during certain hours, ...)", - "it": "Accesso limitato (p.es. solo con prenotazione, in certi orari, ...)" + "it": "Accesso limitato (p.es. solo con prenotazione, in certi orari, ...)", + "ru": "Ограниченный доступ (напр., только по записи, в определённые часы, ...)" } }, { @@ -238,7 +248,8 @@ "nl": "Enkel toegankelijk voor leden van de bijhorende sportclub", "fr": "Accessible uniquement aux membres du club", "en": "Only accessible for members of the club", - "it": "Accesso limitato ai membri dell'associazione" + "it": "Accesso limitato ai membri dell'associazione", + "ru": "Доступ только членам клуба" } }, { @@ -257,7 +268,8 @@ "nl": "Moet men reserveren om gebruik te maken van dit sportveld?", "fr": "Doit-on réserver pour utiliser ce terrain de sport ?", "en": "Does one have to make an appointment to use this sport pitch?", - "it": "È necessario prenotarsi per usare questo campo sportivo?" + "it": "È necessario prenotarsi per usare questo campo sportivo?", + "ru": "Нужна ли предварительная запись для доступа на эту спортивную площадку?" }, "condition": { "and": [ @@ -282,7 +294,8 @@ "nl": "Reserveren is sterk aangeraden om gebruik te maken van dit sportterrein", "fr": "Il est recommendé de réserver pour utiliser ce terrain de sport", "en": "Making an appointment is recommended when using this sport pitch", - "it": "La prenotazione è consigliata per usare questo campo sportivo" + "it": "La prenotazione è consigliata per usare questo campo sportivo", + "ru": "Желательна предварительная запись для доступа на эту спортивную площадку" } }, { @@ -291,7 +304,8 @@ "nl": "Reserveren is mogelijk, maar geen voorwaarde", "fr": "Il est possible de réserver, mais ce n'est pas nécéssaire pour utiliser ce terrain de sport", "en": "Making an appointment is possible, but not necessary to use this sport pitch", - "it": "La prenotazione è consentita, ma non è obbligatoria per usare questo campo sportivo" + "it": "La prenotazione è consentita, ma non è obbligatoria per usare questo campo sportivo", + "ru": "Предварительная запись для доступа на эту спортивную площадку возможна, но не обязательна" } }, { @@ -300,7 +314,8 @@ "nl": "Reserveren is niet mogelijk", "fr": "On ne peut pas réserver", "en": "Making an appointment is not possible", - "it": "Non è possibile prenotare" + "it": "Non è possibile prenotare", + "ru": "Невозможна предварительная запись" } } ] @@ -336,7 +351,8 @@ "nl": "Wanneer is dit sportveld toegankelijk?", "fr": "Quand ce terrain est-il accessible ?", "en": "When is this pitch accessible?", - "it": "Quando è aperto questo campo sportivo?" + "it": "Quando è aperto questo campo sportivo?", + "ru": "В какое время доступна эта площадка?" }, "render": "Openingsuren: {opening_hours_table()}", "freeform": { @@ -446,7 +462,8 @@ "nl": "Ping-pong tafel", "fr": "Table de ping-pong", "en": "Tabletennis table", - "it": "Tavolo da tennistavolo" + "it": "Tavolo da tennistavolo", + "ru": "Стол для настольного тенниса" }, "tags": [ "leisure=pitch", diff --git a/assets/layers/surveillance_camera/surveillance_camera.json b/assets/layers/surveillance_camera/surveillance_camera.json index e5c872be2..20f1e77bf 100644 --- a/assets/layers/surveillance_camera/surveillance_camera.json +++ b/assets/layers/surveillance_camera/surveillance_camera.json @@ -39,7 +39,8 @@ "en": "What kind of camera is this?", "nl": "Wat voor soort camera is dit?", "fr": "Quel genre de caméra est-ce ?", - "it": "Di che tipo di videocamera si tratta?" + "it": "Di che tipo di videocamera si tratta?", + "ru": "Какая это камера?" }, "mappings": [ { @@ -65,7 +66,8 @@ "en": "A dome camera (which can turn)", "nl": "Een dome (bolvormige camera die kan draaien)", "fr": "Une caméra dôme (qui peut tourner)", - "it": "Una videocamera a cupola (che può ruotare)" + "it": "Una videocamera a cupola (che può ruotare)", + "ru": "Камера с поворотным механизмом" } }, { @@ -230,7 +232,8 @@ "en": "This camera is located outdoors", "nl": "Deze camera bevindt zich buiten", "fr": "Cette caméra est située à l'extérieur", - "it": "Questa videocamera si trova all'aperto" + "it": "Questa videocamera si trova all'aperto", + "ru": "Эта камера расположена снаружи" } }, { @@ -239,7 +242,8 @@ "en": "This camera is probably located outdoors", "nl": "Deze camera bevindt zich waarschijnlijk buiten", "fr": "Cette caméra est probablement située à l'extérieur", - "it": "Questa videocamera si trova probabilmente all'esterno" + "it": "Questa videocamera si trova probabilmente all'esterno", + "ru": "Возможно, эта камера расположена снаружи" }, "hideInAnswer": true } @@ -374,7 +378,8 @@ "en": "How is this camera placed?", "nl": "Hoe is deze camera geplaatst?", "fr": "Comment cette caméra est-elle placée ?", - "it": "Com'è posizionata questa telecamera?" + "it": "Com'è posizionata questa telecamera?", + "ru": "Как расположена эта камера?" }, "render": { "en": "Mounting method: {mount}", diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index 06ea04860..a33ddf742 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -57,7 +57,8 @@ "de": "Eine öffentlich zugängliche Toilette", "fr": "Des toilettes", "nl": "Een publieke toilet", - "it": "Servizi igienici aperti al pubblico" + "it": "Servizi igienici aperti al pubblico", + "ru": "Туалет или комната отдыха со свободным доступом" } }, { @@ -66,7 +67,8 @@ "de": "Toiletten mit rollstuhlgerechter Toilette", "fr": "Toilettes accessible aux personnes à mobilité réduite", "nl": "Een rolstoeltoegankelijke toilet", - "it": "Servizi igienici accessibili per persone in sedia a rotelle" + "it": "Servizi igienici accessibili per persone in sedia a rotelle", + "ru": "Туалет с доступом для пользователей кресел-колясок" }, "tags": [ "amenity=toilets", @@ -89,7 +91,8 @@ "de": "Sind diese Toiletten öffentlich zugänglich?", "fr": "Ces toilettes sont-elles accessibles au public ?", "nl": "Zijn deze toiletten publiek toegankelijk?", - "it": "Questi servizi igienici sono aperti al pubblico?" + "it": "Questi servizi igienici sono aperti al pubblico?", + "ru": "Есть ли свободный доступ к этим туалетам?" }, "render": { "en": "Access is {access}", @@ -112,7 +115,8 @@ "de": "Öffentlicher Zugang", "fr": "Accès publique", "nl": "Publiek toegankelijk", - "it": "Accesso pubblico" + "it": "Accesso pubblico", + "ru": "Свободный доступ" } }, { @@ -186,14 +190,16 @@ "de": "Wie viel muss man für diese Toiletten bezahlen?", "fr": "Quel est le prix d'accès de ces toilettes ?", "nl": "Hoeveel moet men betalen om deze toiletten te gebruiken?", - "it": "Quanto costa l'accesso a questi servizi igienici?" + "it": "Quanto costa l'accesso a questi servizi igienici?", + "ru": "Сколько стоит посещение туалета?" }, "render": { "en": "The fee is {charge}", "de": "Die Gebühr beträgt {charge}", "fr": "Le prix est {charge}", "nl": "De toiletten gebruiken kost {charge}", - "it": "La tariffa è {charge}" + "it": "La tariffa è {charge}", + "ru": "Стоимость {charge}" }, "condition": "fee=yes", "freeform": { @@ -227,7 +233,8 @@ "de": "Kein Zugang für Rollstuhlfahrer", "fr": "Non accessible aux personnes à mobilité réduite", "nl": "Niet toegankelijk voor rolstoelgebruikers", - "it": "Non accessibile in sedia a rotelle" + "it": "Non accessibile in sedia a rotelle", + "ru": "Недоступно пользователям кресел-колясок" } } ] @@ -238,7 +245,8 @@ "de": "Welche Art von Toiletten sind das?", "fr": "De quel type sont ces toilettes ?", "nl": "Welke toiletten zijn dit?", - "it": "Di che tipo di servizi igienici si tratta?" + "it": "Di che tipo di servizi igienici si tratta?", + "ru": "Какие это туалеты?" }, "mappings": [ { diff --git a/assets/layers/tree_node/tree_node.json b/assets/layers/tree_node/tree_node.json index a95bfdb01..1f3cc7fde 100644 --- a/assets/layers/tree_node/tree_node.json +++ b/assets/layers/tree_node/tree_node.json @@ -230,7 +230,8 @@ "question": { "nl": "Is deze boom groenblijvend of bladverliezend?", "en": "Is this tree evergreen or deciduous?", - "it": "È un sempreverde o caduco?" + "it": "È un sempreverde o caduco?", + "ru": "Это дерево вечнозелёное или листопадное?" }, "mappings": [ { @@ -242,7 +243,8 @@ "then": { "nl": "Bladverliezend: de boom is een periode van het jaar kaal.", "en": "Deciduous: the tree loses its leaves for some time of the year.", - "it": "Caduco: l’albero perde le sue foglie per un periodo dell’anno." + "it": "Caduco: l’albero perde le sue foglie per un periodo dell’anno.", + "ru": "Листопадное: у дерева опадают листья в определённое время года." } }, { @@ -255,7 +257,8 @@ "nl": "Groenblijvend.", "en": "Evergreen.", "it": "Sempreverde.", - "fr": "À feuilles persistantes." + "fr": "À feuilles persistantes.", + "ru": "Вечнозелёное." } } ], @@ -278,7 +281,8 @@ "nl": "Heeft de boom een naam?", "en": "Does the tree have a name?", "it": "L’albero ha un nome?", - "fr": "L'arbre a-t-il un nom ?" + "fr": "L'arbre a-t-il un nom ?", + "ru": "Есть ли у этого дерева название?" }, "freeform": { "key": "name", @@ -298,7 +302,8 @@ "nl": "De boom heeft geen naam.", "en": "The tree does not have a name.", "it": "L’albero non ha un nome.", - "fr": "L'arbre n'a pas de nom." + "fr": "L'arbre n'a pas de nom.", + "ru": "У этого дерева нет названия." } } ], @@ -399,7 +404,8 @@ "render": { "nl": "\"\"/ Onroerend Erfgoed-ID: {ref:OnroerendErfgoed}", "en": "\"\"/ Onroerend Erfgoed ID: {ref:OnroerendErfgoed}", - "it": "\"\"/ Onroerend Erfgoed ID: {ref:OnroerendErfgoed}" + "it": "\"\"/ Onroerend Erfgoed ID: {ref:OnroerendErfgoed}", + "ru": "\"\"/ Onroerend Erfgoed ID: {ref:OnroerendErfgoed}" }, "question": { "nl": "Wat is het ID uitgegeven door Onroerend Erfgoed Vlaanderen?", @@ -421,7 +427,8 @@ "render": { "nl": "\"\"/ Wikidata: {wikidata}", "en": "\"\"/ Wikidata: {wikidata}", - "it": "\"\"/ Wikidata: {wikidata}" + "it": "\"\"/ Wikidata: {wikidata}", + "ru": "\"\"/ Wikidata: {wikidata}" }, "question": { "nl": "Wat is het Wikidata-ID van deze boom?", @@ -484,7 +491,8 @@ "nl": "Loofboom", "en": "Broadleaved tree", "it": "Albero latifoglia", - "fr": "Arbre feuillu" + "fr": "Arbre feuillu", + "ru": "Лиственное дерево" }, "description": { "nl": "Een boom van een soort die blaadjes heeft, bijvoorbeeld eik of populier.", @@ -501,12 +509,14 @@ "title": { "nl": "Naaldboom", "en": "Needleleaved tree", - "it": "Albero aghifoglia" + "it": "Albero aghifoglia", + "ru": "Хвойное дерево" }, "description": { "nl": "Een boom van een soort met naalden, bijvoorbeeld den of spar.", "en": "A tree of a species with needles, such as pine or spruce.", - "it": "Un albero di una specie con aghi come il pino o l’abete." + "it": "Un albero di una specie con aghi come il pino o l’abete.", + "ru": "Дерево с хвоей (иглами), например, сосна или ель." } }, { @@ -524,7 +534,8 @@ "nl": "Wanneer je niet zeker bent of het nu een loof- of naaldboom is.", "en": "If you're not sure whether it's a broadleaved or needleleaved tree.", "it": "Qualora non si sia sicuri se si tratta di un albero latifoglia o aghifoglia.", - "fr": "Si vous n'êtes pas sûr(e) de savoir s'il s'agit d'un arbre à feuilles larges ou à aiguilles." + "fr": "Si vous n'êtes pas sûr(e) de savoir s'il s'agit d'un arbre à feuilles larges ou à aiguilles.", + "ru": "Если вы не уверены в том, лиственное это дерево или хвойное." } } ] diff --git a/assets/themes/charging_stations/charging_stations.json b/assets/themes/charging_stations/charging_stations.json index fc5040c6c..159cd5819 100644 --- a/assets/themes/charging_stations/charging_stations.json +++ b/assets/themes/charging_stations/charging_stations.json @@ -6,7 +6,8 @@ "ru": "Зарядные станции", "ja": "充電ステーション", "zh_Hant": "充電站", - "it": "Stazioni di ricarica" + "it": "Stazioni di ricarica", + "nl": "Oplaadpunten" }, "shortDescription": { "en": "A worldwide map of charging stations", @@ -29,6 +30,7 @@ "ja", "zh_Hant", "it", + "nl", "nb_NO" ], "maintainer": "", @@ -48,7 +50,8 @@ "ja": "充電ステーション", "zh_Hant": "充電站", "nb_NO": "Ladestasjoner", - "it": "Stazioni di ricarica" + "it": "Stazioni di ricarica", + "nl": "Oplaadpunten" }, "minzoom": 10, "source": { @@ -65,7 +68,8 @@ "ja": "充電ステーション", "zh_Hant": "充電站", "nb_NO": "Ladestasjon", - "it": "Stazione di ricarica" + "it": "Stazione di ricarica", + "nl": "Oplaadpunt" } }, "description": { @@ -74,7 +78,8 @@ "ja": "充電ステーション", "zh_Hant": "充電站", "nb_NO": "En ladestasjon", - "it": "Una stazione di ricarica" + "it": "Una stazione di ricarica", + "nl": "Een oplaadpunt" }, "tagRenderings": [ "images", diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json index e50029a62..3883a9ef8 100644 --- a/assets/themes/shops/shops.json +++ b/assets/themes/shops/shops.json @@ -23,6 +23,7 @@ "ja", "zh_Hant", "ru", + "nl", "ca", "id" ], @@ -41,7 +42,8 @@ "en": "Shop", "fr": "Magasin", "ru": "Магазин", - "ja": "店" + "ja": "店", + "nl": "Winkel" }, "minzoom": 16, "source": { @@ -56,7 +58,8 @@ "en": "Shop", "fr": "Magasin", "ru": "Магазин", - "ja": "店" + "ja": "店", + "nl": "Winkel" }, "mappings": [ { @@ -90,7 +93,8 @@ "description": { "en": "A shop", "fr": "Un magasin", - "ja": "ショップ" + "ja": "ショップ", + "nl": "Een winkel" }, "tagRenderings": [ "images", @@ -99,7 +103,8 @@ "en": "What is the name of this shop?", "fr": "Qu'est-ce que le nom de ce magasin?", "ru": "Как называется магазин?", - "ja": "このお店の名前は何ですか?" + "ja": "このお店の名前は何ですか?", + "nl": "Wat is de naam van deze winkel?" }, "render": "This shop is called {name}", "freeform": { @@ -143,7 +148,8 @@ "en": "Supermarket", "fr": "Supermarché", "ru": "Супермаркет", - "ja": "スーパーマーケット" + "ja": "スーパーマーケット", + "nl": "Supermarkt" } }, { @@ -169,7 +175,8 @@ "en": "Hairdresser", "fr": "Coiffeur", "ru": "Парикмахерская", - "ja": "理容師" + "ja": "理容師", + "nl": "Kapper" } }, { @@ -181,7 +188,8 @@ "then": { "en": "Bakery", "fr": "Boulangerie", - "ja": "ベーカリー" + "ja": "ベーカリー", + "nl": "Bakkerij" } }, { @@ -223,7 +231,8 @@ "question": { "en": "What is the phone number?", "fr": "Quel est le numéro de téléphone ?", - "ja": "電話番号は何番ですか?" + "ja": "電話番号は何番ですか?", + "nl": "Wat is het telefoonnummer?" }, "freeform": { "key": "phone", @@ -242,7 +251,8 @@ "question": { "en": "What is the website of this shop?", "fr": "Quel est le site internet de ce magasin ?", - "ja": "このお店のホームページは何ですか?" + "ja": "このお店のホームページは何ですか?", + "nl": "Wat is de website van deze winkel?" }, "freeform": { "key": "website", @@ -277,7 +287,8 @@ "question": { "en": "What are the opening hours of this shop?", "fr": "Quels sont les horaires d'ouverture de ce magasin ?", - "ja": "この店の営業時間は何時から何時までですか?" + "ja": "この店の営業時間は何時から何時までですか?", + "nl": "Wat zijn de openingsuren van deze winkel?" }, "freeform": { "key": "opening_hours", @@ -316,13 +327,15 @@ "en": "Shop", "fr": "Magasin", "ru": "Магазин", - "ja": "店" + "ja": "店", + "nl": "Winkel" }, "description": { "en": "Add a new shop", "fr": "Ajouter un nouveau magasin", "ru": "Добавить новый магазин", - "ja": "新しい店を追加する" + "ja": "新しい店を追加する", + "nl": "Voeg een nieuwe winkel toe" } } ], diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 55015961a..0cd328a6a 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -603,8 +603,11 @@ "title": { "render": "Стол для пикника" }, + "description": "Слой, отображающий столы для пикника", "tagRenderings": { "0": { + "question": "Из чего изготовлен этот стол для пикника?", + "render": "Этот стол для пикника сделан из {material}", "mappings": { "0": { "then": "Это деревянный стол для пикника" @@ -612,17 +615,14 @@ "1": { "then": "Это бетонный стол для пикника" } - }, - "render": "Этот стол для пикника сделан из {material}", - "question": "Из чего изготовлен этот стол для пикника?" + } } }, "presets": { "0": { "title": "Стол для пикника" } - }, - "description": "Слой, отображающий столы для пикника" + } }, "playground": { "name": "Детские площадки", @@ -645,6 +645,9 @@ "1": { "then": "Поверхность - песок" }, + "2": { + "then": "Покрытие из щепы" + }, "3": { "then": "Поверхность - брусчатка" }, @@ -653,9 +656,17 @@ }, "5": { "then": "Поверхность - бетон" + } + } + }, + "2": { + "question": "Эта игровая площадка освещается ночью?", + "mappings": { + "0": { + "then": "Эта детская площадка освещается ночью" }, - "2": { - "then": "Покрытие из щепы" + "1": { + "then": "Эта детская площадка не освещается ночью" } } }, @@ -663,6 +674,9 @@ "render": "Доступно для детей старше {min_age} лет", "question": "С какого возраста доступна эта детская площадка?" }, + "4": { + "render": "Доступно детям до {max_age}" + }, "6": { "mappings": { "4": { @@ -676,47 +690,33 @@ "8": { "render": "{phone}" }, - "10": { - "mappings": { - "1": { - "then": "Всегда доступен" - }, - "2": { - "then": "Всегда доступен" - }, - "0": { - "then": "Открыто от рассвета до заката" - } - }, - "question": "Когда открыта эта игровая площадка?" - }, "9": { + "question": "Доступна ли детская площадка пользователям кресел-колясок?", "mappings": { - "2": { - "then": "Недоступна пользователям кресел-колясок" + "0": { + "then": "Полностью доступна пользователям кресел-колясок" }, "1": { "then": "Частично доступна пользователям кресел-колясок" }, - "0": { - "then": "Полностью доступна пользователям кресел-колясок" + "2": { + "then": "Недоступна пользователям кресел-колясок" } - }, - "question": "Доступна ли детская площадка пользователям кресел-колясок?" + } }, - "4": { - "render": "Доступно детям до {max_age}" - }, - "2": { + "10": { + "question": "Когда открыта эта игровая площадка?", "mappings": { - "1": { - "then": "Эта детская площадка не освещается ночью" - }, "0": { - "then": "Эта детская площадка освещается ночью" + "then": "Открыто от рассвета до заката" + }, + "1": { + "then": "Всегда доступен" + }, + "2": { + "then": "Всегда доступен" } - }, - "question": "Эта игровая площадка освещается ночью?" + } } }, "presets": { @@ -727,6 +727,7 @@ }, "public_bookcase": { "name": "Книжные шкафы", + "description": "Уличный шкаф с книгами, доступными для всех", "title": { "render": "Книжный шкаф", "mappings": { @@ -751,10 +752,11 @@ } }, "3": { - "question": "Сколько книг помещается в этом общественном книжном шкафу?", - "render": "{capacity} книг помещается в этот книжный шкаф" + "render": "{capacity} книг помещается в этот книжный шкаф", + "question": "Сколько книг помещается в этом общественном книжном шкафу?" }, "4": { + "question": "Какие книги можно найти в этом общественном книжном шкафу?", "mappings": { "0": { "then": "В основном детские книги" @@ -765,27 +767,25 @@ "2": { "then": "Книги и для детей, и для взрослых" } - }, - "question": "Какие книги можно найти в этом общественном книжном шкафу?" - }, - "11": { - "render": "Более подробная информация на сайте", - "question": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?" - }, - "10": { - "render": "Установлен {start_date}", - "question": "Когда был установлен этот общественный книжный шкаф?" + } }, "6": { + "question": "Имеется ли свободный доступ к этому общественному книжному шкафу?", "mappings": { "0": { "then": "Свободный доступ" } - }, - "question": "Имеется ли свободный доступ к этому общественному книжному шкафу?" + } + }, + "10": { + "question": "Когда был установлен этот общественный книжный шкаф?", + "render": "Установлен {start_date}" + }, + "11": { + "render": "Более подробная информация на сайте", + "question": "Есть ли веб-сайт с более подробной информацией об этом общественном книжном шкафе?" } - }, - "description": "Уличный шкаф с книгами, доступными для всех" + } }, "slow_roads": { "tagRenderings": { @@ -819,30 +819,32 @@ "title": { "render": "Спортивная площадка" }, + "description": "Спортивная площадка", "tagRenderings": { "1": { "mappings": { - "2": { - "then": "Это стол для пинг-понга" - }, - "5": { + "0": { "then": "Здесь можно играть в баскетбол" }, - "4": { - "then": "Здесь можно играть в корфбол" - }, - "3": { - "then": "Здесь можно играть в теннис" - }, "1": { "then": "Здесь можно играть в футбол" }, - "0": { + "2": { + "then": "Это стол для пинг-понга" + }, + "3": { + "then": "Здесь можно играть в теннис" + }, + "4": { + "then": "Здесь можно играть в корфбол" + }, + "5": { "then": "Здесь можно играть в баскетбол" } } }, "2": { + "question": "Какое покрытие на этой спортивной площадке?", "render": "Поверхность - {surface}", "mappings": { "0": { @@ -860,55 +862,53 @@ "4": { "then": "Поверхность - бетон" } - }, - "question": "Какое покрытие на этой спортивной площадке?" - }, - "7": { - "mappings": { - "1": { - "then": "Всегда доступен" - } - }, - "question": "В какое время доступна эта площадка?" - }, - "4": { - "mappings": { - "1": { - "then": "Желательна предварительная запись для доступа на эту спортивную площадку" - }, - "3": { - "then": "Невозможна предварительная запись" - }, - "2": { - "then": "Предварительная запись для доступа на эту спортивную площадку возможна, но не обязательна" - } - }, - "question": "Нужна ли предварительная запись для доступа на эту спортивную площадку?" + } }, "3": { + "question": "Есть ли свободный доступ к этой спортивной площадке?", "mappings": { - "2": { - "then": "Доступ только членам клуба" + "0": { + "then": "Свободный доступ" }, "1": { "then": "Ограниченный доступ (напр., только по записи, в определённые часы, ...)" }, - "0": { - "then": "Свободный доступ" + "2": { + "then": "Доступ только членам клуба" } - }, - "question": "Есть ли свободный доступ к этой спортивной площадке?" + } + }, + "4": { + "question": "Нужна ли предварительная запись для доступа на эту спортивную площадку?", + "mappings": { + "1": { + "then": "Желательна предварительная запись для доступа на эту спортивную площадку" + }, + "2": { + "then": "Предварительная запись для доступа на эту спортивную площадку возможна, но не обязательна" + }, + "3": { + "then": "Невозможна предварительная запись" + } + } + }, + "7": { + "question": "В какое время доступна эта площадка?", + "mappings": { + "1": { + "then": "Всегда доступен" + } + } } }, "presets": { - "1": { - "title": "Спортивная площадка" - }, "0": { "title": "Стол для настольного тенниса" + }, + "1": { + "title": "Спортивная площадка" } - }, - "description": "Спортивная площадка" + } }, "surveillance_camera": { "name": "Камеры наблюдения", @@ -917,28 +917,28 @@ }, "tagRenderings": { "1": { + "question": "Какая это камера?", "mappings": { - "2": { - "then": "Панорамная камера" - }, "1": { "then": "Камера с поворотным механизмом" + }, + "2": { + "then": "Панорамная камера" } - }, - "question": "Какая это камера?" - }, - "8": { - "question": "Как расположена эта камера?" + } }, "5": { "mappings": { - "2": { - "then": "Возможно, эта камера расположена снаружи" - }, "1": { "then": "Эта камера расположена снаружи" + }, + "2": { + "then": "Возможно, эта камера расположена снаружи" } } + }, + "8": { + "question": "Как расположена эта камера?" } } }, @@ -958,15 +958,15 @@ }, "tagRenderings": { "1": { + "question": "Есть ли свободный доступ к этим туалетам?", "mappings": { - "2": { - "then": "Недоступно" - }, "0": { "then": "Свободный доступ" + }, + "2": { + "then": "Недоступно" } - }, - "question": "Есть ли свободный доступ к этим туалетам?" + } }, "2": { "mappings": { @@ -975,8 +975,9 @@ } } }, - "5": { - "question": "Какие это туалеты?" + "3": { + "question": "Сколько стоит посещение туалета?", + "render": "Стоимость {charge}" }, "4": { "mappings": { @@ -985,9 +986,8 @@ } } }, - "3": { - "render": "Стоимость {charge}", - "question": "Сколько стоит посещение туалета?" + "5": { + "question": "Какие это туалеты?" } } }, @@ -1010,44 +1010,44 @@ } } }, + "4": { + "question": "Это дерево вечнозелёное или листопадное?", + "mappings": { + "0": { + "then": "Листопадное: у дерева опадают листья в определённое время года." + }, + "1": { + "then": "Вечнозелёное." + } + } + }, "5": { "render": "Название: {name}", + "question": "Есть ли у этого дерева название?", "mappings": { "0": { "then": "У этого дерева нет названия." } - }, - "question": "Есть ли у этого дерева название?" - }, - "8": { - "render": "\"\"/ Wikidata: {wikidata}" + } }, "7": { "render": "\"\"/ Onroerend Erfgoed ID: {ref:OnroerendErfgoed}" }, - "4": { - "mappings": { - "1": { - "then": "Вечнозелёное." - }, - "0": { - "then": "Листопадное: у дерева опадают листья в определённое время года." - } - }, - "question": "Это дерево вечнозелёное или листопадное?" + "8": { + "render": "\"\"/ Wikidata: {wikidata}" } }, "presets": { + "0": { + "title": "Лиственное дерево" + }, + "1": { + "title": "Хвойное дерево", + "description": "Дерево с хвоей (иглами), например, сосна или ель." + }, "2": { "title": "Дерево", "description": "Если вы не уверены в том, лиственное это дерево или хвойное." - }, - "1": { - "description": "Дерево с хвоей (иглами), например, сосна или ель.", - "title": "Хвойное дерево" - }, - "0": { - "title": "Лиственное дерево" } } }, diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 6d7f84a0a..7966e9e2d 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -278,6 +278,18 @@ } } }, + "charging_stations": { + "title": "Oplaadpunten", + "layers": { + "0": { + "name": "Oplaadpunten", + "title": { + "render": "Oplaadpunt" + }, + "description": "Een oplaadpunt" + } + } + }, "climbing": { "title": "Open Klimkaart", "description": "Op deze kaart vind je verschillende klimgelegenheden, zoals klimzalen, bolderzalen en klimmen in de natuur", @@ -924,6 +936,50 @@ "shortDescription": "Een kaart met speeltuinen", "description": "Op deze kaart vind je speeltuinen en kan je zelf meer informatie en foto's toevoegen" }, + "shops": { + "layers": { + "0": { + "name": "Winkel", + "title": { + "render": "Winkel" + }, + "description": "Een winkel", + "tagRenderings": { + "1": { + "question": "Wat is de naam van deze winkel?" + }, + "2": { + "mappings": { + "1": { + "then": "Supermarkt" + }, + "3": { + "then": "Kapper" + }, + "4": { + "then": "Bakkerij" + } + } + }, + "3": { + "question": "Wat is het telefoonnummer?" + }, + "4": { + "question": "Wat is de website van deze winkel?" + }, + "6": { + "question": "Wat zijn de openingsuren van deze winkel?" + } + }, + "presets": { + "0": { + "title": "Winkel", + "description": "Voeg een nieuwe winkel toe" + } + } + } + } + }, "speelplekken": { "title": "Welkom bij de groendoener!", "shortDescription": "Speelplekken in de Antwerpse Zuidrand", @@ -1042,61 +1098,5 @@ } } } - }, - "charging_stations": { - "layers": { - "0": { - "description": "Een oplaadpunt", - "title": { - "render": "Oplaadpunt" - }, - "name": "Oplaadpunten" - } - }, - "title": "Oplaadpunten" - }, - "shops": { - "layers": { - "0": { - "tagRenderings": { - "4": { - "question": "Wat is de website van deze winkel?" - }, - "3": { - "question": "Wat is het telefoonnummer?" - }, - "2": { - "mappings": { - "4": { - "then": "Bakkerij" - }, - "3": { - "then": "Kapper" - }, - "1": { - "then": "Supermarkt" - } - } - }, - "1": { - "question": "Wat is de naam van deze winkel?" - }, - "6": { - "question": "Wat zijn de openingsuren van deze winkel?" - } - }, - "description": "Een winkel", - "title": { - "render": "Winkel" - }, - "name": "Winkel", - "presets": { - "0": { - "title": "Winkel", - "description": "Voeg een nieuwe winkel toe" - } - } - } - } } } \ No newline at end of file From ede32273891b66b0c4f12d932f4bdb8505bf7670 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 12 Jul 2021 13:16:08 +0200 Subject: [PATCH 19/60] Exclude a few files from start-script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d09ca2b7c..6f2603450 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "main": "index.js", "scripts": { "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", - "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** 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", From b9e15697f4a0dc9501663f4b6739c38ee2f8ddbd Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 12 Jul 2021 13:38:38 +0200 Subject: [PATCH 20/60] Robustify morescreen, remove unused var --- UI/BigComponents/MoreScreen.ts | 4 ++++ scripts/generateLayerOverview.ts | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index bfab0567d..91ab436b2 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -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( diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 545ebf844..d903867c8 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -98,7 +98,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") From 0c8387fffb2131a3e09e4fc71f33443cd3e57e3c Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Mon, 12 Jul 2021 14:48:22 +0200 Subject: [PATCH 21/60] =?UTF-8?q?=F0=9F=94=A7=20VS=20code=20line=20ending?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..37441beed --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "files.eol": "\n" +} \ No newline at end of file From e7dd70ee5d2f1a626bdcb85b0b552192793e0ea9 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Mon, 12 Jul 2021 14:49:17 +0200 Subject: [PATCH 22/60] =?UTF-8?q?=F0=9F=94=A7=20Basic=20Devcontainer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/Dockerfile | 16 ++++++++++++++++ .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..fcc5f3644 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile + +# [Choice] Node.js version: 16, 14, 12 +ARG VARIANT="16-buster" +FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install an additional version of node using nvm +# ARG EXTRA_NODE_VERSION=10 +# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" + +# [Optional] Uncomment if you want to install more global node packages +# RUN su node -c "npm install -g " diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f55f8d808 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node +{ + "name": "MapComplete", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick a Node version: 12, 14, 16 + "args": { + "VARIANT": "16" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": {}, + + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [1234], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install && npm run init", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "node" +} \ No newline at end of file From c26783187b411490b80327c82a6fb557648d4b18 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 13 Jul 2021 00:40:27 +0200 Subject: [PATCH 23/60] Attempt to fix #422 --- Logic/Osm/Changes.ts | 18 ++++++++--- Logic/Osm/ChangesetHandler.ts | 59 ++++++++++++++++++++++------------- Logic/Osm/OsmConnection.ts | 5 +-- Models/Constants.ts | 2 +- 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 39b9f4e02..286504174 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -29,10 +29,11 @@ export class Changes implements FeatureSource { /** * All the pending new objects to upload - * @private */ 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 */ @@ -190,6 +191,12 @@ export class Changes implements FeatureSource { console.log("No changes in any object"); 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 @@ -235,9 +242,12 @@ export class Changes implements FeatureSource { }, () => { console.log("Upload successfull!") - this.newObjects.setData([]) - this.pending.setData([]); - }); + self.newObjects.setData([]) + self.pending.setData([]); + self.isUploading.setData(false) + }, + () => self.isUploading.setData(false) + ); }; diff --git a/Logic/Osm/ChangesetHandler.ts b/Logic/Osm/ChangesetHandler.ts index 98d45a790..8fba43803 100644 --- a/Logic/Osm/ChangesetHandler.ts +++ b/Logic/Osm/ChangesetHandler.ts @@ -27,7 +27,7 @@ export class ChangesetHandler { } } - private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) : void{ + private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void { const nodes = response.getElementsByTagName("node"); // @ts-ignore for (const node of nodes) { @@ -70,7 +70,8 @@ export class ChangesetHandler { layout: LayoutConfig, allElements: ElementStorage, generateChangeXML: (csid: string) => string, - whenDone : (csId: string) => void) { + whenDone: (csId: string) => void, + onFail: () => void) { if (this.userDetails.data.csCount == 0) { // The user became a contributor! @@ -98,8 +99,11 @@ export class ChangesetHandler { whenDone, (e) => { console.error("UPLOADING FAILED!", e) + onFail() } ) + }, { + onFail: onFail }) } else { // There still exists an open changeset (or at least we hope so) @@ -114,8 +118,7 @@ export class ChangesetHandler { // 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, whenDone); - + 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,6 +256,9 @@ export class ChangesetHandler { }, function (err, response) { if (response === undefined) { console.log("err", err); + if(options.onFail){ + options.onFail() + } return; } else { continuation(response); diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index cba332447..37c8fa1d2 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -111,8 +111,9 @@ export class OsmConnection { layout: LayoutConfig, allElements: ElementStorage, generateChangeXML: (csid: string) => string, - whenDone: (csId: string) => void) { - this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone); + whenDone: (csId: string) => void, + onFail: () => {}) { + this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); } public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { diff --git a/Models/Constants.ts b/Models/Constants.ts index 48c52eebf..e54e0ea41 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.4-rc1"; + public static vNumber = "0.8.4-rc2"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From 8ddca45d242decde87421e2ca7032dc379f14006 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 13 Jul 2021 13:57:31 +0200 Subject: [PATCH 24/60] Add documentation about deployment in Windows --- Docs/Development_deployment.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Docs/Development_deployment.md b/Docs/Development_deployment.md index ffa69eab3..732336b57 100644 --- a/Docs/Development_deployment.md +++ b/Docs/Development_deployment.md @@ -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#` 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#` 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#` 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 -------------------- From 9ff02d2dba61ee15d465660dd94d02e04038d7da Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 13 Jul 2021 14:00:11 +0200 Subject: [PATCH 25/60] Remove npm run install (not needed) --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f55f8d808..2ea9a5be8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,7 @@ "forwardPorts": [1234], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install && npm run init", + "postCreateCommand": "npm run init", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node" From 0ff6ac4af9d9b7540101f2a7c00fef0291f5239e Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 13 Jul 2021 14:31:03 +0200 Subject: [PATCH 26/60] Clear pending changes if they are already applied --- Logic/Osm/Changes.ts | 4 +++- Models/Constants.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 286504174..ff50cc022 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -188,7 +188,9 @@ 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; diff --git a/Models/Constants.ts b/Models/Constants.ts index e54e0ea41..75423405e 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.4-rc2"; + public static vNumber = "0.8.4-rc3"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From ca1871262e3c680cc0538f7a9c4fac48eb51fa4d Mon Sep 17 00:00:00 2001 From: Ward Date: Tue, 13 Jul 2021 14:39:50 +0200 Subject: [PATCH 27/60] working location button --- Logic/Actors/GeoLocationHandler.ts | 433 +++++++++++++++-------------- 1 file changed, 222 insertions(+), 211 deletions(-) diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index e86c1baa7..18a62220d 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -1,244 +1,255 @@ import * as L from "leaflet"; -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; +import { UIEventSource } from "../UIEventSource"; +import { Utils } from "../../Utils"; 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 { VariableUiElement } from "../../UI/Base/VariableUIElement"; 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; - /** - * Wether or not the geolocation is active, aka the user requested the current location - * @private - */ - private readonly _isActive: UIEventSource; + /** + * 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; - /** - * The callback over the permission API - * @private - */ - private readonly _permission: UIEventSource; - /*** - * The marker on the map, in order to update it - * @private - */ - private _marker: L.Marker; - /** - * Literally: _currentGPSLocation.data != undefined - * @private - */ - private readonly _hasLocation: UIEventSource; - private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>; - /** - * Kept in order to update the marker - * @private - */ - private readonly _leafletMap: UIEventSource; - /** - * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs - * @private - */ - private _lastUserRequest: Date; - /** - * A small flag on localstorage. If the user previously granted the geolocation, it will be set. - * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. - * - * 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; - private readonly _layoutToUse: UIEventSource; + /** + * The callback over the permission API + * @private + */ + private readonly _permission: UIEventSource; + /*** + * The marker on the map, in order to update it + * @private + */ + private _marker: L.Marker; + /** + * Literally: _currentGPSLocation.data != undefined + * @private + */ + private readonly _hasLocation: UIEventSource; + private readonly _currentGPSLocation: UIEventSource<{ + latlng: any; + accuracy: number; + }>; + /** + * Kept in order to update the marker + * @private + */ + private readonly _leafletMap: UIEventSource; + /** + * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs + * @private + */ + private _lastUserRequest: Date; + /** + * A small flag on localstorage. If the user previously granted the geolocation, it will be set. + * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. + * + * 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; + private readonly _layoutToUse: UIEventSource; + constructor( + currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, + leafletMap: UIEventSource, + layoutToUse: UIEventSource + ) { + const hasLocation = currentGPSLocation.map( + (location) => location !== undefined + ); + const previousLocationGrant = LocalStorageSource.Get( + "geolocation-permissions" + ); + const isActive = new UIEventSource(false); - constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, - leafletMap: UIEventSource, - layoutToUse: UIEventSource) { + super( + hasLocation.map( + (hasLocation) => { + if (hasLocation) { + return Svg.crosshair_blue_ui(); + } + if (isActive.data) { + return Svg.crosshair_blue_center_ui(); + } + return Svg.crosshair_ui(); + }, + [isActive] + ) + ); + this._isActive = isActive; + this._isLocked = new UIEventSource(false); + this._permission = new UIEventSource(""); + this._previousLocationGrant = previousLocationGrant; + this._currentGPSLocation = currentGPSLocation; + this._leafletMap = leafletMap; + this._layoutToUse = layoutToUse; + this._hasLocation = hasLocation; + const self = this; - const hasLocation = currentGPSLocation.map((location) => location !== undefined); - const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions") - const isActive = new UIEventSource(false); + 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); + }); - super( - hasLocation.map(hasLocation => { + this.onClick(() => self.init(true)); + this.init(false); - if (hasLocation) { - return Svg.crosshair_blue_ui() - } - if (isActive.data) { - return Svg.crosshair_blue_center_ui(); - } - return Svg.crosshair_ui(); - }, [isActive]) + 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" ); - this._isActive = isActive; - this._permission = new UIEventSource("") - this._previousLocationGrant = previousLocationGrant; - this._currentGPSLocation = currentGPSLocation; - this._leafletMap = leafletMap; - this._layoutToUse = layoutToUse; - this._hasLocation = hasLocation; - const self = this; + } 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 currentPointer = this._isActive.map(isActive => { - if (isActive && !self._hasLocation.data) { - return "cursor-wait" - } - return "cursor-pointer" - }, [this._hasLocation]) - currentPointer.addCallbackAndRun(pointerClass => { - self.SetClass(pointerClass); - }) + const map = self._leafletMap.data; + console.log("check for map", map); + const newMarker = L.marker(location.latlng, { icon: icon }); + newMarker.addTo(map); - this.onClick(() => self.init(true)) - this.init(false) + if (self._marker !== undefined) { + map.removeLayer(self._marker); + } + self._marker = newMarker; + }); + } + private init(askPermission: boolean) { + const self = this; + + if (self._isActive.data) { + self.MoveToCurrentLoction(16); + return; } - private init(askPermission: boolean) { - - const self = this; - const map = this._leafletMap.data; - - this._currentGPSLocation.addCallback((location) => { - self._previousLocationGrant.setData("granted"); - - const timeSinceRequest = (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; - if (timeSinceRequest < 30) { - self.MoveToCurrentLoction(16) - } - - 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(""); + 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); } - private locate() { - const self = this; - const map: any = this._leafletMap.data; + if (askPermission) { + self.StartGeolocating(true); + } else if (this._previousLocationGrant.data === "granted") { + this._previousLocationGrant.setData(""); + self.StartGeolocating(false); + } + } - 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?: number) { + 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; } - private MoveToCurrentLoction(targetZoom = 16) { - const location = this._currentGPSLocation.data; - this._lastUserRequest = undefined; + // 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"); - 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 - ); - } + 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); } - private StartGeolocating(zoomToGPS = true) { - const self = this; - console.log("Starting geolocation") + console.log("Searching location using GPS"); - this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); - if (self._permission.data === "denied") { - self._previousLocationGrant.setData(""); - return ""; - } - if (this._currentGPSLocation.data !== undefined) { - this.MoveToCurrentLoction(16) - } - - - console.log("Searching location using GPS") - this.locate(); - - - if (!self._isActive.data) { - self._isActive.setData(true); - Utils.DoEvery(60000, () => { - - if (document.visibilityState !== "visible") { - console.log("Not starting gps: document not visible") - return; - } - this.locate(); - }) - } + if (self._isActive.data) { + return; } + self._isActive.setData(true); -} \ No newline at end of file + 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"); + } + ); + } +} From 4c4b0356c18ae5859aef7eeb33a04a216d547a9f Mon Sep 17 00:00:00 2001 From: Ward Date: Tue, 13 Jul 2021 14:57:51 +0200 Subject: [PATCH 28/60] location tracking centering screen --- Logic/Actors/GeoLocationHandler.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 18a62220d..f8e38f50c 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -72,23 +72,26 @@ export default class GeoLocationHandler extends VariableUiElement { "geolocation-permissions" ); const isActive = new UIEventSource(false); + const isLocked = new UIEventSource(false); super( hasLocation.map( - (hasLocation) => { - if (hasLocation) { + (hasLocationData) => { + if (isLocked.data) { + return Svg.up_ui(); + } else if (hasLocationData) { return Svg.crosshair_blue_ui(); - } - if (isActive.data) { + } else if (isActive.data) { return Svg.crosshair_blue_center_ui(); + } else { + return Svg.crosshair_ui(); } - return Svg.crosshair_ui(); }, - [isActive] + [isActive, isLocked] ) ); this._isActive = isActive; - this._isLocked = new UIEventSource(false); + this._isLocked = isLocked; this._permission = new UIEventSource(""); this._previousLocationGrant = previousLocationGrant; this._currentGPSLocation = currentGPSLocation; @@ -110,7 +113,12 @@ export default class GeoLocationHandler extends VariableUiElement { self.SetClass(pointerClass); }); - this.onClick(() => self.init(true)); + this.onClick(() => { + self.init(true); + if (self._isActive.data) { + self._isLocked.setData(!self._isLocked.data); + } + }); this.init(false); this._currentGPSLocation.addCallback((location) => { @@ -139,7 +147,6 @@ export default class GeoLocationHandler extends VariableUiElement { }); const map = self._leafletMap.data; - console.log("check for map", map); const newMarker = L.marker(location.latlng, { icon: icon }); newMarker.addTo(map); From 54a084d81c653d5a27d7a480a75c330c8e8557ec Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 13 Jul 2021 18:34:20 +0200 Subject: [PATCH 29/60] Add units to height of windmills --- .../openwindpowermap/openwindpowermap.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/assets/themes/openwindpowermap/openwindpowermap.json b/assets/themes/openwindpowermap/openwindpowermap.json index 2330b885e..0f938fbd4 100644 --- a/assets/themes/openwindpowermap/openwindpowermap.json +++ b/assets/themes/openwindpowermap/openwindpowermap.json @@ -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" } }, { @@ -128,6 +129,23 @@ } ], "units": [ + { + "appliesToKey": [ + "height","rotor:diameter" + ], + "applicableUnits": [ + { + "canonicalDenomination": "m", + "alternativeDenomination": [ + "meter" + ], + "human": { + "en": " meter", + "nl": " meter" + } + } + ] + }, { "appliesToKey": [ "generator:output:electricity" From f5cc180eeaf90dd5b1e3b7c6149af03d25a86a7c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 13 Jul 2021 18:36:31 +0200 Subject: [PATCH 30/60] Add new units to the bottom as not to break translations --- .../openwindpowermap/openwindpowermap.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/assets/themes/openwindpowermap/openwindpowermap.json b/assets/themes/openwindpowermap/openwindpowermap.json index 0f938fbd4..28201b9e5 100644 --- a/assets/themes/openwindpowermap/openwindpowermap.json +++ b/assets/themes/openwindpowermap/openwindpowermap.json @@ -129,24 +129,7 @@ } ], "units": [ - { - "appliesToKey": [ - "height","rotor:diameter" - ], - "applicableUnits": [ - { - "canonicalDenomination": "m", - "alternativeDenomination": [ - "meter" - ], - "human": { - "en": " meter", - "nl": " meter" - } - } - ] - }, - { + { "appliesToKey": [ "generator:output:electricity" ], @@ -197,6 +180,23 @@ } ], "eraseInvalidValues": true + }, + { + "appliesToKey": [ + "height","rotor:diameter" + ], + "applicableUnits": [ + { + "canonicalDenomination": "m", + "alternativeDenomination": [ + "meter" + ], + "human": { + "en": " meter", + "nl": " meter" + } + } + ] } ], "defaultBackgroundId": "CartoDB.Voyager" From 42d0071b26b3b619cf6561a5f1003b61176b3f6b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 13 Jul 2021 18:52:02 +0200 Subject: [PATCH 31/60] Use canonical rendering in windpowermap --- Customizations/JSON/Denomination.ts | 6 +++++- Customizations/JSON/LayoutConfigJson.ts | 4 ++++ Logic/SimpleMetaTagger.ts | 5 +++++ UI/SpecialVisualizations.ts | 1 - assets/themes/openwindpowermap/openwindpowermap.json | 6 +++--- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Customizations/JSON/Denomination.ts b/Customizations/JSON/Denomination.ts index 8331d8adc..2b9779f94 100644 --- a/Customizations/JSON/Denomination.ts +++ b/Customizations/JSON/Denomination.ts @@ -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; @@ -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) diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index 8ced24bd7..374de70e0 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -225,6 +225,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 * diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 4453c2b4f..a704e2259 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -84,6 +84,7 @@ export default class SimpleMetaTagger { }, (feature => { const units = State.state.layoutToUse.data.units ?? []; + let rewritten = false; for (const key in feature.properties) { if (!feature.properties.hasOwnProperty(key)) { continue; @@ -104,10 +105,14 @@ export default class SimpleMetaTagger { } feature.properties[key] = canonical; + rewritten = true; break; } } + if(rewritten){ + State.state.allElements.getEventSourceById(feature.id).ping(); + } }) ) diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 2bbcbbb34..309060b36 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -369,7 +369,6 @@ export default class SpecialVisualizations { if (unit === undefined) { return value; } - return unit.asHumanLongValue(value); }, diff --git a/assets/themes/openwindpowermap/openwindpowermap.json b/assets/themes/openwindpowermap/openwindpowermap.json index 28201b9e5..b1e4ecaa7 100644 --- a/assets/themes/openwindpowermap/openwindpowermap.json +++ b/assets/themes/openwindpowermap/openwindpowermap.json @@ -56,7 +56,7 @@ "tagRenderings": [ { "render": { - "en": "The power output of this wind turbine is {generator:output:electricity}." + "en": "The power output of this wind turbine is {canonical(generator:output:electricity)}." }, "question": { "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" @@ -79,7 +79,7 @@ }, { "render": { - "en": "The total height (including rotor radius) of this wind turbine is {height} metres." + "en": "The total height (including rotor radius) of this wind turbine is {canonical(height)}." }, "question": { "en": "What is the total height of this wind turbine (including rotor radius), in metres?" @@ -91,7 +91,7 @@ }, { "render": { - "en": "The rotor diameter of this wind turbine is {rotor:diameter} metres." + "en": "The rotor diameter of this wind turbine is {canonical(rotor:diameter)}." }, "question": { "en": "What is the rotor diameter of this wind turbine, in metres?" From 315e2f7fd1f91ba7520e001b8e8daf70297469a2 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 00:17:15 +0200 Subject: [PATCH 32/60] Add precise input method, enabled for public bookcases --- Customizations/JSON/LayerConfig.ts | 16 ++- Customizations/JSON/LayerConfigJson.ts | 10 ++ Logic/Actors/AvailableBaseLayers.ts | 21 ++-- Models/BaseLayer.ts | 2 + Svg.ts | 7 +- UI/BigComponents/SimpleAddUI.ts | 95 +++++++++------ UI/Input/LocationInput.ts | 112 ++++++++++++++++++ .../public_bookcase/public_bookcase.json | 5 +- assets/svg/crosshair-empty.svg | 83 +++++++++++++ test.ts | 31 +++-- 10 files changed, 318 insertions(+), 64 deletions(-) create mode 100644 UI/Input/LocationInput.ts create mode 100644 assets/svg/crosshair-empty.svg diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 0acf8198f..48e96f4a2 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -54,6 +54,7 @@ export default class LayerConfig { title: Translation, tags: Tag[], description?: Translation, + preciseInput?: {preferredBackground?: string} }[]; tagRenderings: TagRenderingConfig []; @@ -130,12 +131,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`) - })) + 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 diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index d81307fd9..6739e33fd 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -217,6 +217,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" + } }[], /** diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index 2fd679571..52b1c12de 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -1,11 +1,13 @@ 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"; +import {isBoolean} from "util"; /** * Calculates which layers are available at the current location @@ -24,14 +26,16 @@ 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; - constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) { + constructor(location: UIEventSource) { const self = this; this.availableEditorLayers = location.map( @@ -140,7 +144,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 +158,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; diff --git a/Models/BaseLayer.ts b/Models/BaseLayer.ts index 01eb8e9d7..84556fc69 100644 --- a/Models/BaseLayer.ts +++ b/Models/BaseLayer.ts @@ -7,4 +7,6 @@ export default interface BaseLayer { max_zoom: number, min_zoom: number; feature: any, + isBest?: boolean, + category?: "map" | "osmbasedmap" | "photo" | "historicphoto" | string } \ No newline at end of file diff --git a/Svg.ts b/Svg.ts index 9a5c94b8f..89baa7bec 100644 --- a/Svg.ts +++ b/Svg.ts @@ -89,6 +89,11 @@ export default class Svg { public static crosshair_blue_svg() { return new Img(Svg.crosshair_blue, true);} public static crosshair_blue_ui() { return new FixedUiElement(Svg.crosshair_blue_img);} + public static crosshair_empty = " image/svg+xml " + public static crosshair_empty_img = Img.AsImageElement(Svg.crosshair_empty) + public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);} + public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);} + public static crosshair = " image/svg+xml " public static crosshair_img = Img.AsImageElement(Svg.crosshair) public static crosshair_svg() { return new Img(Svg.crosshair, true);} @@ -334,4 +339,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 75dd3e403..05fb52c64 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -16,6 +16,9 @@ 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"; /* * The SimpleAddUI is a single panel, which can have multiple states: @@ -25,14 +28,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 + }, + preciseInput?: { + preferredBackground?: string } } @@ -48,18 +55,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(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 +73,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 +91,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 +108,41 @@ 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 = undefined + if (preset.preciseInput !== undefined) { + preciseInput = new LocationInput({ + preferCategory: preset.preciseInput.preferredBackground ?? State.state.backgroundLayer, + centerLocation: + new UIEventSource({ + lat: location.data.lat, + lon: location.data.lon, + zoom: 19 + }) + }) + 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 +152,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 +164,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 +204,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 +218,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): 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(tags), false).icon.html + let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource(tags), false).icon.html .SetClass("w-12 h-12 block relative"); const presetInfo: PresetInfo = { tags: preset.tags, layerToAddTo: layer, name: preset.title, description: preset.description, - icon: icon + icon: icon, + preciseInput: preset.preciseInput } const button = SimpleAddUI.CreatePresetSelectButton(presetInfo); diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts new file mode 100644 index 000000000..cee2f5cbf --- /dev/null +++ b/UI/Input/LocationInput.ts @@ -0,0 +1,112 @@ +import {InputElement} from "./InputElement"; +import Loc from "../../Models/Loc"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Minimap from "../Base/Minimap"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import BaseLayer from "../../Models/BaseLayer"; +import Combine from "../Base/Combine"; +import Svg from "../../Svg"; + +export default class LocationInput extends InputElement { + + IsSelected: UIEventSource = new UIEventSource(false); + private _centerLocation: UIEventSource; + private readonly preferCategory; + + constructor(options?: { + centerLocation?: UIEventSource, + preferCategory?: string | UIEventSource, + }) { + super(); + options = options ?? {} + options.centerLocation = options.centerLocation ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) + this._centerLocation = options.centerLocation; + + if(typeof options.preferCategory === "string"){ + options.preferCategory = new UIEventSource(options.preferCategory); + } + this.preferCategory = options.preferCategory ?? new UIEventSource(undefined) + this.SetClass("block h-full") + } + + GetValue(): UIEventSource { + return this._centerLocation; + } + + IsValid(t: Loc): boolean { + return t !== undefined; + } + + protected InnerConstructElement(): HTMLElement { + const layer: UIEventSource = new AvailableBaseLayers(this._centerLocation).availableEditorLayers.map(allLayers => { + // First float all 'best layers' to the top + allLayers.sort((a, b) => { + if (a.isBest && b.isBest) { + return 0; + } + if (!a.isBest) { + return 1 + } + + return -1; + } + ) + if (this.preferCategory) { + const self = this; + //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top + allLayers.sort((a, b) => { + const preferred = self.preferCategory.data + if (a.category === preferred && b.category === preferred) { + return 0; + } + if (a.category !== preferred) { + return 1 + } + + return -1; + } + ) + } + return allLayers[0] + }, [this.preferCategory] + ) + layer.addCallbackAndRunD(layer => console.log(layer)) + const map = new Minimap( + { + location: this._centerLocation, + background: layer + } + ) + map.leafletMap.addCallbackAndRunD(leaflet => { + console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15)) + leaflet.setMaxBounds( + leaflet.getBounds().pad(0.15) + ) + }) + + layer.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(); + } + +} \ No newline at end of file diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index fdb223d06..1efe04b3a 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -73,7 +73,10 @@ }, "tags": [ "amenity=public_bookcase" - ] + ], + "preciseInput": { + "preferredBackground": "photo" + } } ], "tagRenderings": [ diff --git a/assets/svg/crosshair-empty.svg b/assets/svg/crosshair-empty.svg new file mode 100644 index 000000000..36a6e18f8 --- /dev/null +++ b/assets/svg/crosshair-empty.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/test.ts b/test.ts index eb29b9921..5d077d354 100644 --- a/test.ts +++ b/test.ts @@ -7,6 +7,9 @@ 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 ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -148,19 +151,15 @@ 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({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 li = new LocationInput({ + preferCategory:"photo", + centerLocation: + new UIEventSource({ + lat: 51.21576, lon: 3.22001, zoom: 19 + }) +}) + li.SetStyle("height: 20rem") + .AttachTo("maindiv") + +new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv") \ No newline at end of file From a7024cc1faf4735bbe9f5e45732fd56525931547 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 00:23:32 +0200 Subject: [PATCH 33/60] Add designated lock icon --- Logic/Actors/GeoLocationHandler.ts | 4 ++-- Svg.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index f8e38f50c..ed944ed0a 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -78,7 +78,7 @@ export default class GeoLocationHandler extends VariableUiElement { hasLocation.map( (hasLocationData) => { if (isLocked.data) { - return Svg.up_ui(); + return Svg.crosshair_locked_ui(); } else if (hasLocationData) { return Svg.crosshair_blue_ui(); } else if (isActive.data) { @@ -114,10 +114,10 @@ export default class GeoLocationHandler extends VariableUiElement { }); this.onClick(() => { - self.init(true); if (self._isActive.data) { self._isLocked.setData(!self._isLocked.data); } + self.init(true); }); this.init(false); diff --git a/Svg.ts b/Svg.ts index 89baa7bec..0266e43e8 100644 --- a/Svg.ts +++ b/Svg.ts @@ -94,6 +94,11 @@ export default class Svg { public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);} public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);} + public static crosshair_locked = " image/svg+xml " + public static crosshair_locked_img = Img.AsImageElement(Svg.crosshair_locked) + public static crosshair_locked_svg() { return new Img(Svg.crosshair_locked, true);} + public static crosshair_locked_ui() { return new FixedUiElement(Svg.crosshair_locked_img);} + public static crosshair = " image/svg+xml " public static crosshair_img = Img.AsImageElement(Svg.crosshair) public static crosshair_svg() { return new Img(Svg.crosshair, true);} @@ -339,4 +344,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} From 196894cca7813f2abb5a353e033e390b37a2b86a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 00:25:09 +0200 Subject: [PATCH 34/60] Add designated lock icon --- assets/svg/crosshair-locked.svg | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 assets/svg/crosshair-locked.svg diff --git a/assets/svg/crosshair-locked.svg b/assets/svg/crosshair-locked.svg new file mode 100644 index 000000000..b1a741c28 --- /dev/null +++ b/assets/svg/crosshair-locked.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + From 99512724e011ef13a2a503be36b701b3d8cb1626 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 10:05:10 +0200 Subject: [PATCH 35/60] Fix build --- Customizations/JSON/LayerConfigJson.ts | 2 +- Logic/Actors/GeoLocationHandler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index 6739e33fd..ca272ecb0 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -225,7 +225,7 @@ export interface LayerConfigJson { * If 'preferredBackgroundCategory' is set, the element will attempt to pick a background layer of that category. */ preciseInput?: true | { - preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" + preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string } }[], diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index ed944ed0a..6d37e6f4e 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -114,7 +114,7 @@ export default class GeoLocationHandler extends VariableUiElement { }); this.onClick(() => { - if (self._isActive.data) { + if (self._hasLocation.data) { self._isLocked.setData(!self._isLocked.data); } self.init(true); From 583d1e137f080d3b9bd9189c28b8866ac946e2c7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 16:05:50 +0200 Subject: [PATCH 36/60] Refactoring of AvailableBaseLayer --- InitUiElements.ts | 2 +- Logic/Actors/AvailableBaseLayers.ts | 95 ++++++++++++++++++++--------- UI/Base/Minimap.ts | 2 +- UI/BigComponents/SimpleAddUI.ts | 22 ++++--- UI/Input/LocationInput.ts | 46 ++------------ 5 files changed, 89 insertions(+), 78 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index 0f1143eba..0dbc7eaac 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -341,7 +341,7 @@ export class InitUiElements { private static InitBaseMap() { - State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; + State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index 52b1c12de..eceadde89 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -7,7 +7,6 @@ import {UIEventSource} from "../UIEventSource"; import {GeoOperations} from "../GeoOperations"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; -import {isBoolean} from "util"; /** * Calculates which layers are available at the current location @@ -31,42 +30,82 @@ export default class AvailableBaseLayers { category: "osmbasedmap" } - public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); - public availableEditorLayers: UIEventSource; - constructor(location: UIEventSource) { - const self = this; - this.availableEditorLayers = - location.map( - (currentLocation) => { + public static AvailableLayersAt(location: UIEventSource): UIEventSource { + 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, preferedCategory: UIEventSource): UIEventSource { + 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 === preferedCategory && b.category === preferedCategory) { + return 0; + } + if (a.category !== preferedCategory) { + 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) { @@ -146,7 +185,7 @@ export default class AvailableBaseLayers { layer: leafletLayer, feature: layer, isBest: props.best ?? false, - category: props.category + category: props.category }); } return layers; diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 73bf2354a..2c38e8b74 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -52,7 +52,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 05fb52c64..9d1fd1475 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -19,6 +19,7 @@ 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: @@ -115,14 +116,21 @@ export default class SimpleAddUI extends Toggle { let location = State.state.LastClickLocation; let preciseInput: InputElement = 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(preset.preciseInput.preferredBackground)) + } + preciseInput = new LocationInput({ - preferCategory: preset.preciseInput.preferredBackground ?? State.state.backgroundLayer, - centerLocation: - new UIEventSource({ - lat: location.data.lat, - lon: location.data.lon, - zoom: 19 - }) + mapBackground: backgroundLayer, + centerLocation:locationSrc + }) preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") } diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index cee2f5cbf..c306153e3 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -2,30 +2,27 @@ import {InputElement} from "./InputElement"; import Loc from "../../Models/Loc"; import {UIEventSource} from "../../Logic/UIEventSource"; import Minimap from "../Base/Minimap"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import BaseLayer from "../../Models/BaseLayer"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import State from "../../State"; export default class LocationInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private _centerLocation: UIEventSource; - private readonly preferCategory; + private readonly mapBackground : UIEventSource; constructor(options?: { + mapBackground?: UIEventSource, centerLocation?: UIEventSource, - preferCategory?: string | UIEventSource, }) { super(); options = options ?? {} options.centerLocation = options.centerLocation ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) this._centerLocation = options.centerLocation; - if(typeof options.preferCategory === "string"){ - options.preferCategory = new UIEventSource(options.preferCategory); - } - this.preferCategory = options.preferCategory ?? new UIEventSource(undefined) + this.mapBackground = options.mapBackground ?? State.state.backgroundLayer this.SetClass("block h-full") } @@ -38,43 +35,10 @@ export default class LocationInput extends InputElement { } protected InnerConstructElement(): HTMLElement { - const layer: UIEventSource = new AvailableBaseLayers(this._centerLocation).availableEditorLayers.map(allLayers => { - // First float all 'best layers' to the top - allLayers.sort((a, b) => { - if (a.isBest && b.isBest) { - return 0; - } - if (!a.isBest) { - return 1 - } - - return -1; - } - ) - if (this.preferCategory) { - const self = this; - //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top - allLayers.sort((a, b) => { - const preferred = self.preferCategory.data - if (a.category === preferred && b.category === preferred) { - return 0; - } - if (a.category !== preferred) { - return 1 - } - - return -1; - } - ) - } - return allLayers[0] - }, [this.preferCategory] - ) - layer.addCallbackAndRunD(layer => console.log(layer)) const map = new Minimap( { location: this._centerLocation, - background: layer + background: this.mapBackground } ) map.leafletMap.addCallbackAndRunD(leaflet => { From 4cc42c0842af3a3faf923a73b9b508410ec4c448 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 16:06:34 +0200 Subject: [PATCH 37/60] WIP: length picker --- Svg.ts | 7 +- UI/Input/LengthInput.ts | 117 ++++++++++++++++++++++++++++++++ assets/svg/length-crosshair.svg | 73 ++++++++++++++++++++ test.ts | 9 +-- 4 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 UI/Input/LengthInput.ts create mode 100644 assets/svg/length-crosshair.svg diff --git a/Svg.ts b/Svg.ts index 0266e43e8..8b1e29307 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,6 +184,11 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) + public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} + public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} + public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) public static logo_svg() { return new Img(Svg.logo, true);} @@ -344,4 +349,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"length-crosshair.svg": Svg.length_crosshair,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts new file mode 100644 index 000000000..82b79ee0f --- /dev/null +++ b/UI/Input/LengthInput.ts @@ -0,0 +1,117 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import Svg from "../../Svg"; +import BaseUIElement from "../BaseUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; +import Minimap from "../Base/Minimap"; + + +/** + * Selects a length after clicking on the minimap, in meters + */ +export default class LengthInput extends InputElement { + private readonly _location: UIEventSource; + + public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource; + private background; + + constructor(mapBackground: UIEventSource, + location: UIEventSource, + value?: UIEventSource) { + super(); + this._location = location; + this.value = value ?? new UIEventSource(undefined); + this.background = mapBackground; + } + + GetValue(): UIEventSource { + return this.value; + } + + IsValid(str: string): boolean { + const t = Number(str); + return !isNaN(t) && t >= 0 && t <= 360; + } + + protected InnerConstructElement(): HTMLElement { + + let map: BaseUIElement = new FixedUiElement("") + if (!Utils.runningFromConsole) { + map = new Minimap({ + background: this.background, + allowMoving: true, + location: this._location + }) + } + + const element = new Combine([ + Svg.direction_stroke_svg().SetStyle( + `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) + .SetClass("direction-svg relative") + .SetStyle("z-index: 1000"), + map.SetClass("w-full h-full absolute top-0 left-O rounded-full overflow-hidden"), + ]) + .SetStyle("position:relative;display:block;width: min(100%, 25em); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .ConstructElement() + + + this.value.addCallbackAndRunD(rotation => { + const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement + cone.style.transform = `rotate(${rotation}deg)`; + + }) + + this.RegisterTriggers(element) + element.style.overflow = "hidden" + + return element; + } + + private RegisterTriggers(htmlElement: HTMLElement) { + const self = this; + + function onPosChange(x: number, y: number) { + const rect = htmlElement.getBoundingClientRect(); + const dx = -(rect.left + rect.right) / 2 + x; + const dy = (rect.top + rect.bottom) / 2 - y; + const angle = 180 * Math.atan2(dy, dx) / Math.PI; + const angleGeo = Math.floor((450 - angle) % 360); + self.value.setData("" + angleGeo) + } + + + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + ev.preventDefault(); + } + + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + } + + let isDown = false; + + htmlElement.onmousedown = (ev: MouseEvent) => { + isDown = true; + onPosChange(ev.clientX, ev.clientY); + ev.preventDefault(); + } + + htmlElement.onmouseup = (ev) => { + isDown = false; + ev.preventDefault(); + } + + htmlElement.onmousemove = (ev: MouseEvent) => { + if (isDown) { + onPosChange(ev.clientX, ev.clientY); + } + ev.preventDefault(); + } + } + +} \ No newline at end of file diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg new file mode 100644 index 000000000..6db2cf72b --- /dev/null +++ b/assets/svg/length-crosshair.svg @@ -0,0 +1,73 @@ + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + + + diff --git a/test.ts b/test.ts index 5d077d354..23da820f3 100644 --- a/test.ts +++ b/test.ts @@ -10,6 +10,7 @@ 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 ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -152,13 +153,7 @@ function TestMiniMap() { } //*/ -const li = new LocationInput({ - preferCategory:"photo", - centerLocation: - new UIEventSource({ - lat: 51.21576, lon: 3.22001, zoom: 19 - }) -}) +const li = new LengthInput() li.SetStyle("height: 20rem") .AttachTo("maindiv") From 7deb9b5d53bdc594b5d04ea76148fa7e64bb2766 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 14 Jul 2021 16:05:50 +0200 Subject: [PATCH 38/60] Refactoring of AvailableBaseLayer --- InitUiElements.ts | 2 +- Logic/Actors/AvailableBaseLayers.ts | 95 ++++++++++++++++++++--------- UI/Base/Minimap.ts | 2 +- UI/BigComponents/SimpleAddUI.ts | 22 ++++--- UI/Input/LocationInput.ts | 48 ++------------- 5 files changed, 90 insertions(+), 79 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index 0f1143eba..0dbc7eaac 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -341,7 +341,7 @@ export class InitUiElements { private static InitBaseMap() { - State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; + State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index 52b1c12de..e84ecc635 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -7,7 +7,6 @@ import {UIEventSource} from "../UIEventSource"; import {GeoOperations} from "../GeoOperations"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; -import {isBoolean} from "util"; /** * Calculates which layers are available at the current location @@ -31,42 +30,82 @@ export default class AvailableBaseLayers { category: "osmbasedmap" } - public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); - public availableEditorLayers: UIEventSource; - constructor(location: UIEventSource) { - const self = this; - this.availableEditorLayers = - location.map( - (currentLocation) => { + public static AvailableLayersAt(location: UIEventSource): UIEventSource { + 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, preferedCategory: UIEventSource): UIEventSource { + 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) { @@ -146,7 +185,7 @@ export default class AvailableBaseLayers { layer: leafletLayer, feature: layer, isBest: props.best ?? false, - category: props.category + category: props.category }); } return layers; diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 73bf2354a..2c38e8b74 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -52,7 +52,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 05fb52c64..9d1fd1475 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -19,6 +19,7 @@ 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: @@ -115,14 +116,21 @@ export default class SimpleAddUI extends Toggle { let location = State.state.LastClickLocation; let preciseInput: InputElement = 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(preset.preciseInput.preferredBackground)) + } + preciseInput = new LocationInput({ - preferCategory: preset.preciseInput.preferredBackground ?? State.state.backgroundLayer, - centerLocation: - new UIEventSource({ - lat: location.data.lat, - lon: location.data.lon, - zoom: 19 - }) + mapBackground: backgroundLayer, + centerLocation:locationSrc + }) preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") } diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index cee2f5cbf..d568e4443 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -2,30 +2,27 @@ import {InputElement} from "./InputElement"; import Loc from "../../Models/Loc"; import {UIEventSource} from "../../Logic/UIEventSource"; import Minimap from "../Base/Minimap"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import BaseLayer from "../../Models/BaseLayer"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import State from "../../State"; export default class LocationInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private _centerLocation: UIEventSource; - private readonly preferCategory; + private readonly mapBackground : UIEventSource; constructor(options?: { + mapBackground?: UIEventSource, centerLocation?: UIEventSource, - preferCategory?: string | UIEventSource, }) { super(); options = options ?? {} options.centerLocation = options.centerLocation ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) this._centerLocation = options.centerLocation; - if(typeof options.preferCategory === "string"){ - options.preferCategory = new UIEventSource(options.preferCategory); - } - this.preferCategory = options.preferCategory ?? new UIEventSource(undefined) + this.mapBackground = options.mapBackground ?? State.state.backgroundLayer this.SetClass("block h-full") } @@ -38,43 +35,10 @@ export default class LocationInput extends InputElement { } protected InnerConstructElement(): HTMLElement { - const layer: UIEventSource = new AvailableBaseLayers(this._centerLocation).availableEditorLayers.map(allLayers => { - // First float all 'best layers' to the top - allLayers.sort((a, b) => { - if (a.isBest && b.isBest) { - return 0; - } - if (!a.isBest) { - return 1 - } - - return -1; - } - ) - if (this.preferCategory) { - const self = this; - //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top - allLayers.sort((a, b) => { - const preferred = self.preferCategory.data - if (a.category === preferred && b.category === preferred) { - return 0; - } - if (a.category !== preferred) { - return 1 - } - - return -1; - } - ) - } - return allLayers[0] - }, [this.preferCategory] - ) - layer.addCallbackAndRunD(layer => console.log(layer)) const map = new Minimap( { location: this._centerLocation, - background: layer + background: this.mapBackground } ) map.leafletMap.addCallbackAndRunD(leaflet => { @@ -84,7 +48,7 @@ export default class LocationInput extends InputElement { ) }) - layer.map(layer => { + this.mapBackground.map(layer => { const leaflet = map.leafletMap.data if (leaflet === undefined || layer === undefined) { From b68bf7b95029a3161b5471daebf12997c767349f Mon Sep 17 00:00:00 2001 From: Arno Deceuninck Date: Thu, 15 Jul 2021 09:34:00 +0200 Subject: [PATCH 39/60] Download cached features as geojson --- InitUiElements.ts | 2 ++ State.ts | 2 ++ UI/BigComponents/LayerSelection.ts | 5 +++++ UI/GeoJsonExport.ts | 15 +++++++++++++++ Utils.ts | 8 ++++++++ 5 files changed, 32 insertions(+) create mode 100644 UI/GeoJsonExport.ts diff --git a/InitUiElements.ts b/InitUiElements.ts index 0f1143eba..97dd2b02c 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -427,6 +427,8 @@ export class InitUiElements { state.locationControl, state.selectedElement); + State.state.featurePipeline = source; + new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse); const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource); diff --git a/State.ts b/State.ts index 8e4322d65..7793485aa 100644 --- a/State.ts +++ b/State.ts @@ -19,6 +19,7 @@ import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import {Relation} from "./Logic/Osm/ExtractRelations"; import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; +import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; /** * Contains the global state: a bunch of UI-event sources @@ -95,6 +96,7 @@ export default class State { public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; + public readonly featurePipeline: FeaturePipeline; /** diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index e28294709..11237fa32 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -7,6 +7,8 @@ import Translations from "../i18n/Translations"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import BaseUIElement from "../BaseUIElement"; import {Translation} from "../i18n/Translation"; +import {SubtleButton} from "../Base/SubtleButton"; +import {exportAsGeoJson} from "../GeoJsonExport"; /** * Shows the panel with all layers and a toggle for each of them @@ -74,6 +76,9 @@ export default class LayerSelection extends Combine { ); } + const downloadButton = new SubtleButton("./assets/svg/floppy.svg", "Download visible data as geojson") + downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline)) // TODO: Define this + checkboxes.push(downloadButton) super(checkboxes) this.SetStyle("display:flex;flex-direction:column;") diff --git a/UI/GeoJsonExport.ts b/UI/GeoJsonExport.ts new file mode 100644 index 000000000..c18baed17 --- /dev/null +++ b/UI/GeoJsonExport.ts @@ -0,0 +1,15 @@ +import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; +import {Utils} from "../Utils"; + +export function exportAsGeoJson(featurePipeline: FeaturePipeline, options?: {metadata?: boolean}) { + let defaults = { + metadata: false + } + options = Utils.setDefaults(options, defaults); + // Select all features, ignore the freshness and other data + // TODO: Remove mapcomplete metadata (starting with underscore) + let featureList: JSON[] = featurePipeline? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"]; + let geojson = {type: "FeatureCollection", features: featureList} + + Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "Geodata.json"); +} diff --git a/Utils.ts b/Utils.ts index 05ebcbaab..418f1d111 100644 --- a/Utils.ts +++ b/Utils.ts @@ -447,6 +447,14 @@ export class Utils { b: parseInt(hex.substr(5, 2), 16), } } + + public static setDefaults(options, defaults){ + let result = {}; + for (let key of defaults){ + result[key] = key in options ? options[key] : defaults[key]; + } + return result; + } } export interface TileRange { From bd07eed4824afb03d09ebf98c2fab4ce7a8b7f91 Mon Sep 17 00:00:00 2001 From: Arno Deceuninck Date: Thu, 15 Jul 2021 09:58:17 +0200 Subject: [PATCH 40/60] Remove MapComplete metadata from featurelist --- UI/GeoJsonExport.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/UI/GeoJsonExport.ts b/UI/GeoJsonExport.ts index c18baed17..68037eee3 100644 --- a/UI/GeoJsonExport.ts +++ b/UI/GeoJsonExport.ts @@ -1,14 +1,31 @@ import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; import {Utils} from "../Utils"; -export function exportAsGeoJson(featurePipeline: FeaturePipeline, options?: {metadata?: boolean}) { +export function exportAsGeoJson(featurePipeline: FeaturePipeline, options?: { metadata?: boolean }) { let defaults = { metadata: false } options = Utils.setDefaults(options, defaults); // Select all features, ignore the freshness and other data - // TODO: Remove mapcomplete metadata (starting with underscore) - let featureList: JSON[] = featurePipeline? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"]; + let featureList: JSON[] = featurePipeline ? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"]; + + function removeMetaData(featureList: JSON[]) { + 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 featureList; + } + + // Remove the metadata of MapComplete (all properties starting with an underscore) + if (!options.metadata) { + removeMetaData(featureList); + } + let geojson = {type: "FeatureCollection", features: featureList} Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "Geodata.json"); From e7ef2fb6d8116fae7edfea16b0488b00d56395c8 Mon Sep 17 00:00:00 2001 From: Arno Deceuninck Date: Thu, 15 Jul 2021 10:56:30 +0200 Subject: [PATCH 41/60] Updated documentation --- UI/BigComponents/LayerSelection.ts | 2 +- UI/GeoJsonExport.ts | 21 ++++++++++++++------- Utils.ts | 7 +++---- tslint.json | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 11237fa32..900be9348 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -77,7 +77,7 @@ export default class LayerSelection extends Combine { } const downloadButton = new SubtleButton("./assets/svg/floppy.svg", "Download visible data as geojson") - downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline)) // TODO: Define this + downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline)) checkboxes.push(downloadButton) super(checkboxes) diff --git a/UI/GeoJsonExport.ts b/UI/GeoJsonExport.ts index 68037eee3..d3e5dc5d6 100644 --- a/UI/GeoJsonExport.ts +++ b/UI/GeoJsonExport.ts @@ -1,14 +1,25 @@ import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; import {Utils} from "../Utils"; -export function exportAsGeoJson(featurePipeline: FeaturePipeline, options?: { metadata?: boolean }) { +/** + * 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 + */ +export function exportAsGeoJson(featurePipeline: FeaturePipeline, options: { metadata?: boolean} = {}) { let defaults = { - metadata: false + metadata: false, } options = Utils.setDefaults(options, defaults); + // Select all features, ignore the freshness and other data let featureList: JSON[] = featurePipeline ? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"]; + /** + * Removes the metadata of MapComplete (all properties starting with an underscore) + * @param featureList JsonList containing features, output object + */ function removeMetaData(featureList: JSON[]) { for (let i=0; i < featureList.length; i++) { let feature = featureList[i]; @@ -18,13 +29,9 @@ export function exportAsGeoJson(featurePipeline: FeaturePipeline, options?: { me } } } - return featureList; } - // Remove the metadata of MapComplete (all properties starting with an underscore) - if (!options.metadata) { - removeMetaData(featureList); - } + if (!options.metadata) removeMetaData(featureList); let geojson = {type: "FeatureCollection", features: featureList} diff --git a/Utils.ts b/Utils.ts index 418f1d111..683e7a8f9 100644 --- a/Utils.ts +++ b/Utils.ts @@ -449,11 +449,10 @@ export class Utils { } public static setDefaults(options, defaults){ - let result = {}; - for (let key of defaults){ - result[key] = key in options ? options[key] : defaults[key]; + for (let key in defaults){ + if (!(key in options)) options[key] = defaults[key]; } - return result; + return options; } } diff --git a/tslint.json b/tslint.json index 6a204a045..85c7437ac 100644 --- a/tslint.json +++ b/tslint.json @@ -1,5 +1,5 @@ { - "defaultSeverity": "error", + "defaultSeverity": "warn", "extends": [ "tslint:recommended", "tslint-no-circular-imports" From 62925a89ba333eb654ac67bb5084788e466050f0 Mon Sep 17 00:00:00 2001 From: Arno Deceuninck Date: Thu, 15 Jul 2021 11:13:00 +0200 Subject: [PATCH 42/60] Text in translation file + refactor --- {UI => Logic/FeatureSource}/GeoJsonExport.ts | 4 ++-- UI/BigComponents/LayerSelection.ts | 4 ++-- langs/en.json | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) rename {UI => Logic/FeatureSource}/GeoJsonExport.ts (93%) diff --git a/UI/GeoJsonExport.ts b/Logic/FeatureSource/GeoJsonExport.ts similarity index 93% rename from UI/GeoJsonExport.ts rename to Logic/FeatureSource/GeoJsonExport.ts index d3e5dc5d6..541e10373 100644 --- a/UI/GeoJsonExport.ts +++ b/Logic/FeatureSource/GeoJsonExport.ts @@ -1,5 +1,5 @@ -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; -import {Utils} from "../Utils"; +import FeaturePipeline from "./FeaturePipeline"; +import {Utils} from "../../Utils"; /** * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index 900be9348..c48b36163 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -8,7 +8,7 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig"; import BaseUIElement from "../BaseUIElement"; import {Translation} from "../i18n/Translation"; import {SubtleButton} from "../Base/SubtleButton"; -import {exportAsGeoJson} from "../GeoJsonExport"; +import {exportAsGeoJson} from "../../Logic/FeatureSource/GeoJsonExport"; /** * Shows the panel with all layers and a toggle for each of them @@ -76,7 +76,7 @@ export default class LayerSelection extends Combine { ); } - const downloadButton = new SubtleButton("./assets/svg/floppy.svg", "Download visible data as geojson") + const downloadButton = new SubtleButton("./assets/svg/floppy.svg", Translations.t.general.layerSelection.downloadGeojson.Clone()) downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline)) checkboxes.push(downloadButton) diff --git a/langs/en.json b/langs/en.json index 7f327653c..b604850f8 100644 --- a/langs/en.json +++ b/langs/en.json @@ -147,7 +147,8 @@ "loginOnlyNeededToEdit": "if you want to edit the map", "layerSelection": { "zoomInToSeeThisLayer": "Zoom in to see this layer", - "title": "Select layers" + "title": "Select layers", + "downloadGeojson": "Download layer features as geojson" }, "weekdays": { "abbreviations": { From 46d57edea0dd7a3f4bab5b5febbda033eb2f43e4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 15 Jul 2021 21:01:10 +0200 Subject: [PATCH 43/60] Translation sync --- assets/themes/artwork/artwork.json | 2 +- assets/themes/bicycle_library/bicycle_library.json | 6 ++++-- assets/themes/openwindpowermap/openwindpowermap.json | 11 ++++++----- langs/themes/de.json | 8 ++++---- langs/themes/en.json | 7 +++++++ langs/themes/nl.json | 7 +++++++ 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/assets/themes/artwork/artwork.json b/assets/themes/artwork/artwork.json index 7b8cc876c..c3b7b7c55 100644 --- a/assets/themes/artwork/artwork.json +++ b/assets/themes/artwork/artwork.json @@ -374,7 +374,7 @@ "en": "Is there a website with more information about this artwork?", "nl": "Op welke website kan men meer informatie vinden over dit kunstwerk?", "fr": "Sur quel site web pouvons-nous trouver plus d'informations sur cette œuvre d'art?", - "de": "Auf welcher Website gibt es mehr Informationen über dieses Kunstwerk?", + "de": "Gibt es eine Website mit weiteren Informationen über dieses Kunstwerk?", "it": "Esiste un sito web con maggiori informazioni su quest’opera?", "ru": "Есть ли сайт с более подробной информацией об этой работе?", "ja": "この作品についての詳しい情報はどのウェブサイトにありますか?", diff --git a/assets/themes/bicycle_library/bicycle_library.json b/assets/themes/bicycle_library/bicycle_library.json index 437020a3f..6d0cf61e2 100644 --- a/assets/themes/bicycle_library/bicycle_library.json +++ b/assets/themes/bicycle_library/bicycle_library.json @@ -10,7 +10,8 @@ "ja", "fr", "zh_Hant", - "nb_NO" + "nb_NO", + "de" ], "title": { "en": "Bicycle libraries", @@ -20,7 +21,8 @@ "ja": "自転車ライブラリ", "fr": "Vélothèques", "zh_Hant": "單車圖書館", - "nb_NO": "Sykkelbibliotek" + "nb_NO": "Sykkelbibliotek", + "de": "Fahrradbibliothek" }, "description": { "nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.", diff --git a/assets/themes/openwindpowermap/openwindpowermap.json b/assets/themes/openwindpowermap/openwindpowermap.json index b1e4ecaa7..22bba27ef 100644 --- a/assets/themes/openwindpowermap/openwindpowermap.json +++ b/assets/themes/openwindpowermap/openwindpowermap.json @@ -56,7 +56,7 @@ "tagRenderings": [ { "render": { - "en": "The power output of this wind turbine is {canonical(generator:output:electricity)}." + "en": "The power output of this wind turbine is {generator:output:electricity}." }, "question": { "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" @@ -79,7 +79,7 @@ }, { "render": { - "en": "The total height (including rotor radius) of this wind turbine is {canonical(height)}." + "en": "The total height (including rotor radius) of this wind turbine is {height} metres." }, "question": { "en": "What is the total height of this wind turbine (including rotor radius), in metres?" @@ -91,7 +91,7 @@ }, { "render": { - "en": "The rotor diameter of this wind turbine is {canonical(rotor:diameter)}." + "en": "The rotor diameter of this wind turbine is {rotor:diameter} metres." }, "question": { "en": "What is the rotor diameter of this wind turbine, in metres?" @@ -129,7 +129,7 @@ } ], "units": [ - { + { "appliesToKey": [ "generator:output:electricity" ], @@ -183,7 +183,8 @@ }, { "appliesToKey": [ - "height","rotor:diameter" + "height", + "rotor:diameter" ], "applicableUnits": [ { diff --git a/langs/themes/de.json b/langs/themes/de.json index 95abb60b1..6a0b29fad 100644 --- a/langs/themes/de.json +++ b/langs/themes/de.json @@ -87,6 +87,9 @@ "shortDescription": "Eine Karte aller Sitzbänke", "description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Einzeln stehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Account können Sie neue Bänke eintragen oder Detailinformationen existierender Bänke bearbeiten." }, + "bicyclelib": { + "title": "Fahrradbibliothek" + }, "bookcases": { "title": "Öffentliche Bücherschränke Karte", "description": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen." @@ -327,8 +330,5 @@ "toilets": { "title": "Offene Toilette Karte", "description": "Eine Karte der öffentlichen Toiletten" - }, - "bicyclelib": { - "title": "Fahrradbibliothek" } -} +} \ No newline at end of file diff --git a/langs/themes/en.json b/langs/themes/en.json index 48852d5a4..f5db488b9 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -1143,6 +1143,13 @@ "human": " gigawatts" } } + }, + "1": { + "applicableUnits": { + "0": { + "human": " meter" + } + } } } }, diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 7966e9e2d..44bf09b5f 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -919,6 +919,13 @@ "human": " gigawatt" } } + }, + "1": { + "applicableUnits": { + "0": { + "human": " meter" + } + } } } }, From 294bdbfd9247ef99f5798881cbb5fadb76f0bad4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 00:49:50 +0200 Subject: [PATCH 44/60] Add missing license info --- assets/svg/license_info.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index 1ef8f94c8..db044b683 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -582,5 +582,15 @@ "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" + ] } ] \ No newline at end of file From 0af3a91fde5c9a72457d565d5a061145eb112eaa Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 00:57:33 +0200 Subject: [PATCH 45/60] Fix licenses, small improvement to icon --- assets/svg/crosshair-locked.svg | 69 ++++++++++++++++----------------- assets/svg/license_info.json | 14 ++++++- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/assets/svg/crosshair-locked.svg b/assets/svg/crosshair-locked.svg index b1a741c28..d8d04340c 100644 --- a/assets/svg/crosshair-locked.svg +++ b/assets/svg/crosshair-locked.svg @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="2.8284271" - inkscape:cx="67.47399" - inkscape:cy="29.788021" + inkscape:zoom="5.6568542" + inkscape:cx="27.044982" + inkscape:cy="77.667126" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -68,40 +68,39 @@ inkscape:groupmode="layer" id="layer1" transform="translate(0,-270.54165)"> - - - - - + id="g827"> + + 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" /> + + + + diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index 1ef8f94c8..f82b5fb71 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -162,6 +162,18 @@ "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" @@ -204,7 +216,7 @@ "license": "CC0", "sources": [ "https://commons.wikimedia.org/wiki/File:Media-floppy.svg", - " http://tango.freedesktop.org/Tango_Desktop_Project" + "http://tango.freedesktop.org/Tango_Desktop_Project" ] }, { From abd7db100dd290e9f5a826532bfe7cffb717c116 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 01:42:09 +0200 Subject: [PATCH 46/60] Finish the export functionality: move logic around a bit, add license information for reusers, wire the functionality as feature switch --- Customizations/JSON/LayoutConfig.ts | 2 ++ Customizations/JSON/LayoutConfigJson.ts | 2 ++ Logic/FeatureSource/FeatureSource.ts | 38 +++++++++++++++++++++++- Logic/FeatureSource/GeoJsonExport.ts | 39 ------------------------- State.ts | 10 +++++-- Svg.ts | 2 +- UI/BigComponents/ExportDataButton.ts | 21 +++++++++++++ UI/BigComponents/LayerControlPanel.ts | 36 ++++++++++++++--------- UI/BigComponents/LayerSelection.ts | 6 ---- langs/en.json | 7 +++-- 10 files changed, 98 insertions(+), 65 deletions(-) delete mode 100644 Logic/FeatureSource/GeoJsonExport.ts create mode 100644 UI/BigComponents/ExportDataButton.ts diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 12b9d5f76..e76c68ac8 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -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) diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index 374de70e0..d36a8463d 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -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. * @@ -335,4 +336,5 @@ export interface LayoutConfigJson { enableGeolocation?: boolean; enableBackgroundLayerSelection?: boolean; enableShowAllQuestions?: boolean; + enableExportButton?: boolean; } diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index ba568271e..171db39f6 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -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} + + + } + + } \ No newline at end of file diff --git a/Logic/FeatureSource/GeoJsonExport.ts b/Logic/FeatureSource/GeoJsonExport.ts deleted file mode 100644 index 541e10373..000000000 --- a/Logic/FeatureSource/GeoJsonExport.ts +++ /dev/null @@ -1,39 +0,0 @@ -import FeaturePipeline from "./FeaturePipeline"; -import {Utils} from "../../Utils"; - -/** - * 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 - */ -export function exportAsGeoJson(featurePipeline: FeaturePipeline, options: { metadata?: boolean} = {}) { - let defaults = { - metadata: false, - } - options = Utils.setDefaults(options, defaults); - - // Select all features, ignore the freshness and other data - let featureList: JSON[] = featurePipeline ? featurePipeline.features.data.map((feature) => feature.feature) : ["I'm empty"]; - - /** - * Removes the metadata of MapComplete (all properties starting with an underscore) - * @param featureList JsonList containing features, output object - */ - function removeMetaData(featureList: JSON[]) { - 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]; - } - } - } - } - - if (!options.metadata) removeMetaData(featureList); - - let geojson = {type: "FeatureCollection", features: featureList} - - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "Geodata.json"); -} diff --git a/State.ts b/State.ts index 7793485aa..90289bcab 100644 --- a/State.ts +++ b/State.ts @@ -96,6 +96,10 @@ export default class State { public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; + public readonly featureSwitchEnableExport: UIEventSource; + + + public readonly featurePipeline: FeaturePipeline; @@ -127,7 +131,7 @@ export default class State { public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); - + constructor(layoutToUse: LayoutConfig) { const self = this; @@ -201,6 +205,8 @@ export default class State { "Disables/Enables the geolocation button"); this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, "Always show all questions"); + this.featureSwitchEnableExport = featSw("fs-export",(layoutToUse) => layoutToUse?.enableExportButton ?? false, + "If set, enables the 'download'-button to download everything as geojson") this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false", "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org") @@ -212,7 +218,7 @@ export default class State { 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 diff --git a/Svg.ts b/Svg.ts index 0266e43e8..5684fc4b5 100644 --- a/Svg.ts +++ b/Svg.ts @@ -94,7 +94,7 @@ export default class Svg { public static crosshair_empty_svg() { return new Img(Svg.crosshair_empty, true);} public static crosshair_empty_ui() { return new FixedUiElement(Svg.crosshair_empty_img);} - public static crosshair_locked = " image/svg+xml " + public static crosshair_locked = " image/svg+xml " public static crosshair_locked_img = Img.AsImageElement(Svg.crosshair_locked) public static crosshair_locked_svg() { return new Img(Svg.crosshair_locked, true);} public static crosshair_locked_ui() { return new FixedUiElement(Svg.crosshair_locked_img);} diff --git a/UI/BigComponents/ExportDataButton.ts b/UI/BigComponents/ExportDataButton.ts new file mode 100644 index 000000000..9a161de9f --- /dev/null +++ b/UI/BigComponents/ExportDataButton.ts @@ -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")]) + } +} \ No newline at end of file diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index 42a3eda12..c8837fbcc 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -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, "
", 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") } } \ No newline at end of file diff --git a/UI/BigComponents/LayerSelection.ts b/UI/BigComponents/LayerSelection.ts index c48b36163..3c7f108e8 100644 --- a/UI/BigComponents/LayerSelection.ts +++ b/UI/BigComponents/LayerSelection.ts @@ -7,8 +7,6 @@ import Translations from "../i18n/Translations"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import BaseUIElement from "../BaseUIElement"; import {Translation} from "../i18n/Translation"; -import {SubtleButton} from "../Base/SubtleButton"; -import {exportAsGeoJson} from "../../Logic/FeatureSource/GeoJsonExport"; /** * Shows the panel with all layers and a toggle for each of them @@ -76,10 +74,6 @@ export default class LayerSelection extends Combine { ); } - const downloadButton = new SubtleButton("./assets/svg/floppy.svg", Translations.t.general.layerSelection.downloadGeojson.Clone()) - downloadButton.onClick(() => exportAsGeoJson(State.state.featurePipeline)) - checkboxes.push(downloadButton) - super(checkboxes) this.SetStyle("display:flex;flex-direction:column;") diff --git a/langs/en.json b/langs/en.json index b604850f8..aac35335c 100644 --- a/langs/en.json +++ b/langs/en.json @@ -147,8 +147,11 @@ "loginOnlyNeededToEdit": "if you want to edit the map", "layerSelection": { "zoomInToSeeThisLayer": "Zoom in to see this layer", - "title": "Select layers", - "downloadGeojson": "Download layer features as geojson" + "title": "Select layers" + }, + "download": { + "downloadGeojson": "Download visible data as geojson", + "licenseInfo": "

Copyright notice

The provided is available under ODbL. Reusing this data is free for any purpose, but
  • the attribution © OpenStreetMap contributors
  • Any change to this data must be republished under the same license
. Please see the full copyright notice for details" }, "weekdays": { "abbreviations": { From 3bcd2553111e3cb91d470c75abefc389b097c361 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 02:06:33 +0200 Subject: [PATCH 47/60] Add fake user switch to mimick a logged in user; fixes #432 --- Docs/URL_Parameters.md | 248 +++++++++++++++++++++---------------- Logic/Osm/OsmConnection.ts | 20 ++- State.ts | 39 +++--- preferences.ts | 2 +- test/OsmConnection.spec.ts | 2 +- 5 files changed, 182 insertions(+), 129 deletions(-) diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md index 6f299adcf..5c3158fd7 100644 --- a/Docs/URL_Parameters.md +++ b/Docs/URL_Parameters.md @@ -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- ------------------ diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 37c8fa1d2..92a0823f6 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -47,6 +47,7 @@ export class OsmConnection { public auth; public userDetails: UIEventSource; public isLoggedIn: UIEventSource + private fakeUser: boolean; _dryRun: boolean; public preferencesHandler: OsmPreferences; public changesetHandler: ChangesetHandler; @@ -59,12 +60,15 @@ export class OsmConnection { url: string }; - constructor(dryRun: boolean, oauth_token: UIEventSource, + constructor(dryRun: boolean, + fakeUser: boolean, + oauth_token: UIEventSource, // 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; console.debug("Using backend", this._oauth_config.url) @@ -72,7 +76,15 @@ export class OsmConnection { this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; this.userDetails = new UIEventSource(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){ @@ -138,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(); diff --git a/State.ts b/State.ts index 90289bcab..a5bad6706 100644 --- a/State.ts +++ b/State.ts @@ -59,8 +59,8 @@ export default class State { public favouriteLayers: UIEventSource; public layerUpdater: OverpassFeatureSource; - - public osmApiFeatureSource : OsmApiFeatureSource ; + + public osmApiFeatureSource: OsmApiFeatureSource; public filteredLayers: UIEventSource<{ @@ -81,7 +81,7 @@ export default class State { * Keeps track of relations: which way is part of which other way? * Set by the overpass-updater; used in the metatagging */ - public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships") + public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships") public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; @@ -96,8 +96,8 @@ export default class State { public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; - public readonly featureSwitchEnableExport: UIEventSource; - + public readonly featureSwitchEnableExport: UIEventSource; + public readonly featureSwitchFakeUser: UIEventSource; public readonly featurePipeline: FeaturePipeline; @@ -131,7 +131,7 @@ export default class State { public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); - + constructor(layoutToUse: LayoutConfig) { const self = this; @@ -205,20 +205,24 @@ export default class State { "Disables/Enables the geolocation button"); this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, "Always show all questions"); - this.featureSwitchEnableExport = featSw("fs-export",(layoutToUse) => layoutToUse?.enableExportButton ?? false, + this.featureSwitchEnableExport = featSw("fs-export", (layoutToUse) => layoutToUse?.enableExportButton ?? false, "If set, enables the 'download'-button to download everything as geojson") this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false", "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org") .map(str => str === "true", [], b => "" + b); - - this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug","false", + + this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false", + "If true, 'dryrun' mode is activated and a fake user account is loaded") + .map(str => str === "true", [], b => "" + b); + + this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug", "false", "If true, shows some extra debugging help such as all the available tags on every object") .map(str => str === "true", [], b => "" + b) - this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","osm", + 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 @@ -229,18 +233,19 @@ export default class State { this.backgroundLayerId = QueryParameters.GetQueryParameter("background", - layoutToUse?.defaultBackgroundId ?? "osm", - "The id of the background layer to start with") + layoutToUse?.defaultBackgroundId ?? "osm", + "The id of the background layer to start with") } - - - if(Utils.runningFromConsole){ + + + 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, @@ -253,7 +258,7 @@ export default class State { this.allElements = new ElementStorage(); this.changes = new Changes(); this.osmApiFeatureSource = new OsmApiFeatureSource() - + new PendingChangesUploader(this.changes, this.selectedElement); this.mangroveIdentity = new MangroveIdentity( diff --git a/preferences.ts b/preferences.ts index 1c1773a14..a7ae07ded 100644 --- a/preferences.ts +++ b/preferences.ts @@ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement"; import Table from "./UI/Base/Table"; -const connection = new OsmConnection(false, new UIEventSource(undefined), ""); +const connection = new OsmConnection(false, false, new UIEventSource(undefined), ""); let rendered = false; diff --git a/test/OsmConnection.spec.ts b/test/OsmConnection.spec.ts index ffcb4840c..2253e56c3 100644 --- a/test/OsmConnection.spec.ts +++ b/test/OsmConnection.spec.ts @@ -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(undefined), "Unit test", true, From d0293fb2320123548c10fb6dc555fc2880887852 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 02:13:41 +0200 Subject: [PATCH 48/60] Fix chrome styling bug --- UI/Popup/FeatureInfoBox.ts | 2 +- UI/Popup/TagRenderingAnswer.ts | 38 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index f35f73ceb..b456c0ab9 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -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") diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 6c8fd257e..c8953dd01 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -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;"); } From d8287ba1c5b95dde22de0b11089ea00433eb86ed Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 16 Jul 2021 02:24:27 +0200 Subject: [PATCH 49/60] Fix minimaps --- UI/Base/Minimap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 2c38e8b74..a7066c9ee 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -82,7 +82,7 @@ export default class Minimap extends BaseUIElement { doubleClickZoom: this._allowMoving, keyboard: this._allowMoving, touchZoom: this._allowMoving, - zoomAnimation: this._allowMoving, + // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, fadeAnimation: this._allowMoving }); From 973cd7ada381a6b7cd2b4e9ab351a367694476aa Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 18 Jul 2021 20:07:08 +0200 Subject: [PATCH 50/60] Fix speelplekken-temp --- .../speelplekken/speelplekken_temp.json | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/assets/themes/speelplekken/speelplekken_temp.json b/assets/themes/speelplekken/speelplekken_temp.json index cf5eedfe5..052c219ba 100644 --- a/assets/themes/speelplekken/speelplekken_temp.json +++ b/assets/themes/speelplekken/speelplekken_temp.json @@ -121,20 +121,21 @@ "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 => \"\" + r.relation.tags.name + \"\"))).join(', ')", - "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" - ], - "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", - "geoJsonZoomLevel": 14, - "isOsmCache": true - } + ] + }, + "calculatedTags": [ + "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"\" + r.relation.tags.name + \"\"))).join(', ')", + "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" + ], + "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", + "geoJsonZoomLevel": 14, + "isOsmCache": true } - }, + } + }, { "id": "walking_routes", "name": { From 76fa147670a596c718ebaa5298513c6489ac6df3 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 18 Jul 2021 20:16:28 +0200 Subject: [PATCH 51/60] Fix deployment (once more) --- assets/tagRenderings/questions.json | 2 +- .../speelplekken/speelplekken_temp.json | 38 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 3fabc4deb..66d034cdc 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -142,7 +142,7 @@ "if": "location=underground", "then": { "en": "Located underground", - "nl": "Bevindt zich ondergrounds" + "nl": "Bevindt zich ondergronds" }, "hideInAnswer": true }, diff --git a/assets/themes/speelplekken/speelplekken_temp.json b/assets/themes/speelplekken/speelplekken_temp.json index 052c219ba..bf3975dc3 100644 --- a/assets/themes/speelplekken/speelplekken_temp.json +++ b/assets/themes/speelplekken/speelplekken_temp.json @@ -105,24 +105,26 @@ { "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" - } - ] - }, + "+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 => \"\" + r.relation.tags.name + \"\"))).join(', ')", "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" From 186405784e789d230f83b22b5f35b21225376ce7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 18 Jul 2021 20:24:08 +0200 Subject: [PATCH 52/60] Add translation file --- langs/shared-questions/nl.json | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 langs/shared-questions/nl.json diff --git a/langs/shared-questions/nl.json b/langs/shared-questions/nl.json new file mode 100644 index 000000000..fd5a2a9b6 --- /dev/null +++ b/langs/shared-questions/nl.json @@ -0,0 +1,35 @@ +{ + "undefined": { + "phone": { + "question": "Wat is het telefoonnummer van {name}?" + }, + "email": { + "question": "Wat is het email-adres van {name}?" + }, + "website": { + "question": "Wat is de website van {name}?" + }, + "description": { + "question": "Zijn er extra zaken die je niet in de bovenstaande vragen kwijt kon? Zet deze in de descriptionHerhaal geen antwoorden die je reeds gaf" + }, + "opening_hours": { + "question": "Wat zijn de openingsuren van {name}?", + "render": "

Openingsuren

{opening_hours_table(opening_hours)}" + }, + "level": { + "question": "Op welke verdieping bevindt dit punt zich?", + "render": "Bevindt zich op de {level}de verdieping", + "mappings": { + "0": { + "then": "Bevindt zich ondergronds" + }, + "1": { + "then": "Bevindt zich gelijkvloers" + }, + "2": { + "then": "Bevindt zich op de eerste verdieping" + } + } + } + } +} \ No newline at end of file From 219895191d1ef3742961804a27f439a9d396dcc9 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 18 Jul 2021 21:48:11 +0200 Subject: [PATCH 53/60] Disable adding new elements-popup if the userbadge is disabled; add small cosmetic changes --- Logic/Actors/StrayClickHandler.ts | 7 ++++++- State.ts | 6 ++++++ UI/ShowDataLayer.ts | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index b4d630070..3e7609fd4 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -47,7 +47,12 @@ export default class StrayClickHandler { popupAnchor: [0, -45] }) }); - const popup = L.popup().setContent("
"); + const popup = L.popup({ + autoPan: true, + autoPanPaddingTopLeft: [15,15], + closeOnEscapeKey: true, + autoClose: true + }).setContent("
"); self._lastMarker.addTo(leafletMap.data); self._lastMarker.bindPopup(popup); diff --git a/State.ts b/State.ts index a5bad6706..418657266 100644 --- a/State.ts +++ b/State.ts @@ -193,6 +193,12 @@ export default class State { "Disables/Enables the layer control"); this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"); + this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { + if (!userbadge) { + this.featureSwitchAddNew.setData(false) + } + }) + this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, "Disables/enables the help menu or welcome message"); this.featureSwitchIframe = featSw("fs-iframe", () => false, diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index df45af45e..59225640f 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -146,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); From 4fa9159da10c4b214d975740e413321d450a8425 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 20 Jul 2021 01:33:58 +0200 Subject: [PATCH 54/60] First working version of a width measurment tool --- Customizations/JSON/TagRenderingConfig.ts | 17 +- Customizations/JSON/TagRenderingConfigJson.ts | 6 + Svg.ts | 2 +- UI/Base/Minimap.ts | 22 ++- UI/Input/LengthInput.ts | 160 +++++++++++++----- UI/Input/ValidatedTextField.ts | 127 ++++++++++---- UI/Popup/TagRenderingQuestion.ts | 5 +- UI/SpecialVisualizations.ts | 3 +- assets/svg/length-crosshair.svg | 44 +++-- assets/themes/widths/width.json | 8 +- index.ts | 3 + test.ts | 13 +- 12 files changed, 300 insertions(+), 110 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index d3d440493..7b36dae44 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -27,7 +27,8 @@ export default class TagRenderingConfig { readonly type: string, readonly addExtraTags: TagsFilter[]; readonly inline: boolean, - readonly default?: string + readonly default?: string, + readonly helperArgs?: (string | number | boolean)[] }; readonly multiAnswer: boolean; @@ -76,8 +77,8 @@ export default class TagRenderingConfig { addExtraTags: json.freeform.addExtraTags?.map((tg, i) => FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], inline: json.freeform.inline ?? false, - default: json.freeform.default - + default: json.freeform.default, + helperArgs: json.freeform.helperArgs } if (json.freeform["extraTags"] !== undefined) { @@ -336,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; - } + } } \ No newline at end of file diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 89871ec74..843889525 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -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,6 +41,11 @@ 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' diff --git a/Svg.ts b/Svg.ts index 26c5505ed..a3fe46cd7 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,7 +184,7 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index a7066c9ee..6ebf37a75 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.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; private _isInited = false; private _allowMoving: boolean; + private readonly _leafletoptions: any; constructor(options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any } ) { super() @@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement { this._location = options?.location ?? new UIEventSource(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; @@ -52,7 +56,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet @@ -71,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,9 +86,13 @@ export default class Minimap extends BaseUIElement { doubleClickZoom: this._allowMoving, keyboard: this._allowMoving, touchZoom: this._allowMoving, - // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: 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]] diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index 82b79ee0f..ea7530ce3 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -2,11 +2,12 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; -import BaseUIElement from "../BaseUIElement"; -import {FixedUiElement} from "../Base/FixedUiElement"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; -import Minimap from "../Base/Minimap"; +import {GeoOperations} from "../../Logic/GeoOperations"; +import DirectionInput from "./DirectionInput"; +import {RadioButton} from "./RadioButton"; +import {FixedInputElement} from "./FixedInputElement"; /** @@ -19,13 +20,15 @@ export default class LengthInput extends InputElement { private readonly value: UIEventSource; private background; - constructor(mapBackground: UIEventSource, + constructor(mapBackground: UIEventSource, location: UIEventSource, value?: UIEventSource) { super(); this._location = location; this.value = value ?? new UIEventSource(undefined); this.background = mapBackground; + this.SetClass("block") + } GetValue(): UIEventSource { @@ -33,83 +36,150 @@ export default class LengthInput extends InputElement { } IsValid(str: string): boolean { - const t = Number(str); + const t = Number(str) return !isNaN(t) && t >= 0 && t <= 360; } protected InnerConstructElement(): HTMLElement { - - let map: BaseUIElement = new FixedUiElement("") + const modeElement = new RadioButton([ + new FixedInputElement("Measure", "measure"), + new FixedInputElement("Move", "move") + ]) + // @ts-ignore + let map = undefined if (!Utils.runningFromConsole) { - map = new Minimap({ + map = DirectionInput.constructMinimap({ background: this.background, - allowMoving: true, - location: this._location + allowMoving: false, + location: this._location, + leafletOptions: { + tap: true + } }) } - const element = new Combine([ - Svg.direction_stroke_svg().SetStyle( - `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) - .SetClass("direction-svg relative") - .SetStyle("z-index: 1000"), - map.SetClass("w-full h-full absolute top-0 left-O rounded-full overflow-hidden"), + new Combine([Svg.length_crosshair_ui().SetStyle( + `visibility: hidden; position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) + ]) + .SetClass("block length-crosshair-svg relative") + .SetStyle("z-index: 1000"), + map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), ]) - .SetStyle("position:relative;display:block;width: min(100%, 25em); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden") .ConstructElement() - this.value.addCallbackAndRunD(rotation => { - const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement - cone.style.transform = `rotate(${rotation}deg)`; - - }) - - this.RegisterTriggers(element) + this.RegisterTriggers(element, map?.leafletMap) element.style.overflow = "hidden" - - return element; + element.style.display = "block" + + return element } - private RegisterTriggers(htmlElement: HTMLElement) { - const self = this; + private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource) { + + 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; + } - function onPosChange(x: number, y: number) { const rect = htmlElement.getBoundingClientRect(); - const dx = -(rect.left + rect.right) / 2 + x; - const dy = (rect.top + rect.bottom) / 2 - y; - const angle = 180 * Math.atan2(dy, dx) / Math.PI; - const angleGeo = Math.floor((450 - angle) % 360); - self.value.setData("" + angleGeo) + // 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 = 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) { + console.log(firstClickXY, [dx, dy], "pixel origin", leaflet.getPixelOrigin()) + const first = leaflet.layerPointToLatLng(firstClickXY) + const last = leaflet.layerPointToLatLng([dx, dy]) + console.log(first, last) + const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 + console.log("First", first, "last", last, "d", geoDist) + self.value.setData("" + geoDist) + } + + } + } - htmlElement.ontouchmove = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); ev.preventDefault(); } - htmlElement.ontouchstart = (ev: TouchEvent) => { - onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); + ev.preventDefault(); } - let isDown = false; + htmlElement.ontouchend = (ev: TouchEvent) => { + onPosChange(undefined, undefined, false, true); + ev.preventDefault(); + } htmlElement.onmousedown = (ev: MouseEvent) => { - isDown = true; - onPosChange(ev.clientX, ev.clientY); + onPosChange(ev.clientX, ev.clientY, true); ev.preventDefault(); } htmlElement.onmouseup = (ev) => { - isDown = false; + onPosChange(ev.clientX, ev.clientY, false, true); ev.preventDefault(); } htmlElement.onmousemove = (ev: MouseEvent) => { - if (isDown) { - onPosChange(ev.clientX, ev.clientY); - } + onPosChange(ev.clientX, ev.clientY, false); ev.preventDefault(); } } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 2eeff8a54..809ff025f 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -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, options?: { location: [number, number], - mapBackgroundLayer?: UIEventSource + mapBackgroundLayer?: UIEventSource, + args: (string | number | boolean)[] + feature?: any }) => InputElement, - inputmode?: string } export default class ValidatedTextField { + public static bestLayerAt: (location: UIEventSource, preferences: UIEventSource) => any public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( @@ -63,6 +67,79 @@ 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({ + 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(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){ + } + options.feature + + const location = new UIEventSource({ + 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(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 +190,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({ - lat: options.location[0], - lon: options.location[1], - zoom: 19 - }),value); - }, - "numeric" - ), ValidatedTextField.tp( "float", "A decimal", @@ -222,6 +283,7 @@ export default class ValidatedTextField { * {string (typename) --> TextFieldDef} */ public static AllTypes = ValidatedTextField.allTypesDict(); + public static InputForType(type: string, options?: { placeholder?: string | BaseUIElement, value?: UIEventSource, @@ -233,7 +295,9 @@ export default class ValidatedTextField { country?: () => string, location?: [number /*lat*/, number /*lon*/], mapBackgroundLayer?: UIEventSource, - unit?: Unit + unit?: Unit, + args?: (string | number | boolean)[] // Extra arguments for the inputHelper, + feature?: any }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -247,7 +311,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 +332,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 @@ -288,17 +352,16 @@ export default class ValidatedTextField { 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 +369,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 +394,9 @@ export default class ValidatedTextField { reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { location: [number, number], - mapBackgroundLayer: UIEventSource + mapBackgroundLayer: UIEventSource, + args: string[], + feature: any }) => InputElement, inputmode?: string): TextFieldDef { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 20c0b00d2..c72375959 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -330,12 +330,15 @@ export default class TagRenderingQuestion extends Combine { } const tagsData = tags.data; + const feature = State.state.allElements.ContainingFeatures.get(tagsData.id) const input: InputElement = 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[freeform.key] ?? freeform.default); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 309060b36..5a38e8184 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -39,7 +39,8 @@ export default class SpecialVisualizations { static constructMiniMap: (options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any }) => BaseUIElement; static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource, layoutToUse: UIEventSource, enablePopups?: boolean, zoomToFeatures?: boolean) => any; public static specialVisualizations: SpecialVisualization[] = diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg index 6db2cf72b..cb83789fb 100644 --- a/assets/svg/length-crosshair.svg +++ b/assets/svg/length-crosshair.svg @@ -26,17 +26,17 @@ guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:window-width="1680" - inkscape:window-height="1009" + inkscape:window-width="1920" + inkscape:window-height="999" id="namedview16" showgrid="false" showguides="true" inkscape:guide-bbox="true" - inkscape:zoom="0.25" - inkscape:cx="-448.31847" - inkscape:cy="144.08448" + inkscape:zoom="0.5" + inkscape:cx="108.3764" + inkscape:cy="623.05359" inkscape:window-x="0" - inkscape:window-y="15" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg14" inkscape:snap-smooth-nodes="true" /> @@ -54,20 +54,36 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 + ry="427.81949" + transform="rotate(-90)" /> + + + diff --git a/assets/themes/widths/width.json b/assets/themes/widths/width.json index 48a1e883a..298b9a128 100644 --- a/assets/themes/widths/width.json +++ b/assets/themes/widths/width.json @@ -64,7 +64,13 @@ }, "tagRenderings": [ { - "render": "Deze straat is {width:carriageway}m breed" + "render": "Deze straat is {width:carriageway}m breed", + "question": "Hoe breed is deze straat?", + "freeform": { + "key": "width:carriageway", + "type": "length", + "helperArgs": [21, "map"] + } }, { "render": "Deze straat heeft {_width:difference}m te weinig:", diff --git a/index.ts b/index.ts index 70b06bf30..634ad8533 100644 --- a/index.ts +++ b/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, diff --git a/test.ts b/test.ts index 23da820f3..21ca94b74 100644 --- a/test.ts +++ b/test.ts @@ -11,6 +11,7 @@ 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"; @@ -153,8 +154,16 @@ function TestMiniMap() { } //*/ -const li = new LengthInput() - li.SetStyle("height: 20rem") +const loc = new UIEventSource({ + zoom: 24, + lat: 51.21043, + lon: 3.21389 +}) +const li = new LengthInput( + AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource("map","photo")), + loc +) + li.SetStyle("height: 30rem; background: aliceblue;") .AttachTo("maindiv") new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv") \ No newline at end of file From aa9045fd131a87e9ae77f9b9304a75d3ecc7b73f Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 20 Jul 2021 01:59:19 +0200 Subject: [PATCH 55/60] Automatically move the map onto the feature, add arguments to helpers --- Logic/GeoOperations.ts | 8 +++++ Svg.ts | 2 +- UI/Input/LengthInput.ts | 10 +++--- UI/Input/ValidatedTextField.ts | 4 +++ assets/svg/length-crosshair.svg | 56 ++++++++++++++++++++++++--------- 5 files changed, 58 insertions(+), 22 deletions(-) diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 31cb88ad2..768a5fe24 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -273,6 +273,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"}); + } } diff --git a/Svg.ts b/Svg.ts index a3fe46cd7..88e921626 100644 --- a/Svg.ts +++ b/Svg.ts @@ -184,7 +184,7 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index ea7530ce3..0558069b2 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -58,11 +58,11 @@ export default class LengthInput extends InputElement { }) } const element = new Combine([ - new Combine([Svg.length_crosshair_ui().SetStyle( - `visibility: hidden; position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) + 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"), + .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") @@ -119,6 +119,7 @@ export default class LengthInput extends InputElement { const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement + const measurementCrosshairInner: HTMLElement = measurementCrosshair.firstChild if (firstClickXY === undefined) { measurementCrosshair.style.visibility = "hidden" @@ -139,12 +140,9 @@ export default class LengthInput extends InputElement { const leaflet = leafletMap?.data if (leaflet) { - console.log(firstClickXY, [dx, dy], "pixel origin", leaflet.getPixelOrigin()) const first = leaflet.layerPointToLatLng(firstClickXY) const last = leaflet.layerPointToLatLng([dx, dy]) - console.log(first, last) const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 - console.log("First", first, "last", last, "d", geoDist) self.value.setData("" + geoDist) } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 809ff025f..ec3aa62ce 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -121,6 +121,10 @@ export default class ValidatedTextField { // 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 diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg index cb83789fb..0446f22c4 100644 --- a/assets/svg/length-crosshair.svg +++ b/assets/svg/length-crosshair.svg @@ -33,8 +33,8 @@ showguides="true" inkscape:guide-bbox="true" inkscape:zoom="0.5" - inkscape:cx="108.3764" - inkscape:cy="623.05359" + inkscape:cx="307.56567" + inkscape:cy="-35.669379" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -53,21 +53,26 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 - + @@ -75,15 +80,36 @@ Created by potrace 1.15, written by Peter Selinger 2001-2017 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:11.99999975,11.99999975;stroke-dashoffset:0" /> + 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.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" /> + + From cdccfa27bedd7d1299620af0759b81fc44ef1f76 Mon Sep 17 00:00:00 2001 From: Vinicius Date: Tue, 20 Jul 2021 01:54:42 +0000 Subject: [PATCH 56/60] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.3% (165 of 166 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/pt_BR/ --- langs/pt_BR.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/langs/pt_BR.json b/langs/pt_BR.json index 638ab0d39..268c8e4e6 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -122,8 +122,10 @@ "thanksForSharing": "Obrigado por compartilhar!", "copiedToClipboard": "Link copiado para a área de transferência", "addToHomeScreen": "

Adicionar à sua tela inicial

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

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:" - } + "intro": "

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:", + "embedIntro": "

Incorpore em seu site

Por favor, incorpore este mapa em seu site.
Nós o encorajamos a fazer isso - você nem precisa pedir permissão.
É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará." + }, + "aboutMapcomplete": "

Sobre o MapComplete

Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre umúnico tema.Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! Omantenedor do temadefine elementos, questões e linguagens para o tema.

Saiba mais

MapComplete sempreoferece a próxima etapapara saber mais sobre o OpenStreetMap.

  • Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira
  • A versão em tela inteira oferece informações sobre o OpenStreetMap
  • A visualização funciona sem login, mas a edição requer um login do OSM.
  • Se você não estiver conectado, será solicitado que você faça o login
  • Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa
  • Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki


Você percebeuum problema? Você tem umasolicitação de recurso ? Querajudar a traduzir? Acesse o código-fonteou rastreador de problemas.

Quer verseu progresso? Siga a contagem de edição emOsmCha.

" }, "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 o TOS e a política de privacidade de Mangrove.reviews ", + "affiliated_reviewer_warning": "(Revisão de afiliados)" }, "favourite": { "reload": "Recarregar dados", - "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais" + "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais", + "loginNeeded": "

Entrar

Um layout pessoal está disponível apenas para usuários do OpenStreetMap" } } From a8dcbaca193fe30094639471638a3c32a4681238 Mon Sep 17 00:00:00 2001 From: Jan Zabel Date: Mon, 19 Jul 2021 10:55:51 +0000 Subject: [PATCH 57/60] Translated using Weblate (German) Currently translated at 90.9% (10 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/de/ --- langs/shared-questions/de.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/de.json b/langs/shared-questions/de.json index ff0b97af8..6faff774e 100644 --- a/langs/shared-questions/de.json +++ b/langs/shared-questions/de.json @@ -6,6 +6,27 @@ "opening_hours": { "question": "Was sind die Öffnungszeiten von {name}?", "render": "

Öffnungszeiten

{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.
Bitte keine bereits erhobenen Informationen." + }, + "website": { + "question": "Was ist die Website von {name}?" + }, + "email": { + "question": "Was ist die Mail-Adresse von {name}?" } } -} \ No newline at end of file +} From 4211b9c3ee4f599084ed554b39a71f02aeb1ad9d Mon Sep 17 00:00:00 2001 From: Rodrigo Tavares Date: Tue, 20 Jul 2021 10:17:39 +0000 Subject: [PATCH 58/60] Translated using Weblate (Portuguese (Brazil)) Currently translated at 72.7% (8 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/pt_BR/ --- langs/shared-questions/pt_BR.json | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/pt_BR.json b/langs/shared-questions/pt_BR.json index 0967ef424..9c577c396 100644 --- a/langs/shared-questions/pt_BR.json +++ b/langs/shared-questions/pt_BR.json @@ -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}?" + } + } +} From f6fca5e800b074d5930002d55cb9c07a1ecabf3e Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 18 Jul 2021 22:19:55 +0000 Subject: [PATCH 59/60] Translated using Weblate (Russian) Currently translated at 90.9% (10 of 11 strings) Translation: MapComplete/shared-questions Translate-URL: https://hosted.weblate.org/projects/mapcomplete/shared-questions/ru/ --- langs/shared-questions/ru.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/langs/shared-questions/ru.json b/langs/shared-questions/ru.json index a06bc7607..93c56dc44 100644 --- a/langs/shared-questions/ru.json +++ b/langs/shared-questions/ru.json @@ -15,6 +15,20 @@ "opening_hours": { "question": "Какое время работы у {name}?", "render": "

Часы работы

{opening_hours_table(opening_hours)}" + }, + "level": { + "mappings": { + "2": { + "then": "Расположено на первом этаже" + }, + "1": { + "then": "Расположено на первом этаже" + }, + "0": { + "then": "Расположено под землей" + } + }, + "render": "Расположено на {level}ом этаже" } } -} \ No newline at end of file +} From f6ebdc2b32fe45543da4de301d64731f1617cf27 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 18 Jul 2021 20:59:27 +0000 Subject: [PATCH 60/60] Translated using Weblate (Russian) Currently translated at 54.7% (305 of 557 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layer-translations/ru/ --- langs/layers/ru.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 0cd328a6a..c0f535334 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -487,6 +487,11 @@ } } } + }, + "presets": { + "0": { + "title": "Обслуживание велосипедов/магазин" + } } }, "defibrillator": { @@ -1064,6 +1069,7 @@ "1": { "question": "Вы хотите добавить описание?" } - } + }, + "name": "Смотровая площадка" } -} \ No newline at end of file +}