diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts
index 661082eb9f..78be5c6748 100644
--- a/AllTranslationAssets.ts
+++ b/AllTranslationAssets.ts
@@ -102,7 +102,7 @@ export default class AllTranslationAssets {
getStartedNewAccount: new Translation( {"en":" or create a new account","nl":" of maak een nieuwe account aan ","fr":" ou enregistrez-vous","es":" o crea una nueva cuenta","ca":" o crea un nou compte","gl":" ou crea unha nova conta","de":" oder ein neues Konto anlegen"} ),
noTagsSelected: new Translation( {"en":"No tags selected","es":"No se han seleccionado etiquetas","ca":"No s'han seleccionat etiquetes","gl":"Non se seleccionaron etiquetas","nl":"Geen tags geselecteerd","fr":"Aucune balise sélectionnée","de":"Keine Tags ausgewählt"} ),
customThemeIntro: new Translation( {"en":"
Custom themes
These are previously visited user-generated themes.","nl":"Onofficiële thema's
De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.","fr":"Thèmes personnalisés
Vous avez déjà visité ces thèmes personnalisés.","gl":"Temas personalizados
Estes son temas xerados por usuarios previamente visitados.","de":"Kundenspezifische Themen
Dies sind zuvor besuchte benutzergenerierte Themen"} ),
- aboutMapcomplete: new Translation( {"en":"About MapComplete
With MapComplete you can enrich OpenStreetMap with information on a single theme. Answer a few questions, and within minutes your contributions will be available around the globe! The theme maintainer defines elements, questions and languages for the theme.
Find out more
MapComplete always offers the next step to learn more about OpenStreetMap.
- * When embedded in a website, the iframe links to a full-screen MapComplete
- * The full-screen version offers information about OpenStreetMap
- * Viewing works without login, but editing requires an OSM login.
- * If you are not logged in, you are asked to log in
- * Once you answered a single question, you can add new points to the map
- * After a while, actual OSM-tags are shown, later linking to the wiki
Did you notice an issue? Do you have a feature request? Want to help translate? Head over to the source code or issue tracker.
Want to see your progress? Follow the edit count on OsmCha.
","nl":"Over MapComplete
Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De maker van het thema bepaalt de elementen, vragen en taalversies voor het thema.
Ontdek meer
MapComplete biedt altijd de volgende stap naar meer OpenStreetMap:
- * Indien ingebed in een website linkt het iframe naar de volledige MapComplete
- * De volledige versie heeft uitleg over OpenStreetMap
- * Bekijken kan altijd, maar wijzigen vereist een OSM-account
- * Als je niet aangemeld bent, wordt je gevraagd dit te doen
- * Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen
- * Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki
Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker.
Wil je je vorderingen zien? Volg de edits op OsmCha.","de":"Über MapComplete
MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem Einzelthema hinzuzufügen.
Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge einfach und extrem benutzerfreundlich zu halten.Der Themen-Betreuer kann auch eine Sprache für die Schnittstelle wählen, Elemente deaktivieren oder sogar in eine andere Website ohne jegliches UI-Element einbetten.
Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer den nächsten Schritt anzubietenum mehr über OpenStreetMap zu erfahren:
- Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion
- Die Vollbildversion bietet Informationen über OpenStreetMap
- Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen
- Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen
- An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...
Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie zum Quellcode oder zur Problemverfolgung.
"} ),
+ aboutMapcomplete: new Translation( {"en":"About MapComplete
With MapComplete you can enrich OpenStreetMap with information on a single theme. Answer a few questions, and within minutes your contributions will be available around the globe! The theme maintainer defines elements, questions and languages for the theme.
Find out more
MapComplete always offers the next step to learn more about OpenStreetMap.
- When embedded in a website, the iframe links to a full-screen MapComplete
- The full-screen version offers information about OpenStreetMap
- Viewing works without login, but editing requires an OSM login.
- If you are not logged in, you are asked to log in
- Once you answered a single question, you can add new points to the map
- After a while, actual OSM-tags are shown, later linking to the wiki
Did you notice an issue? Do you have a feature request? Want to help translate? Head over to the source code or issue tracker.
Want to see your progress? Follow the edit count on OsmCha.
","nl":"Over MapComplete
Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De maker van het thema bepaalt de elementen, vragen en taalversies voor het thema.
Ontdek meer
MapComplete biedt altijd de volgende stap naar meer OpenStreetMap:
- Indien ingebed in een website linkt het iframe naar de volledige MapComplete
- De volledige versie heeft uitleg over OpenStreetMap
- Bekijken kan altijd, maar wijzigen vereist een OSM-account
- Als je niet aangemeld bent, wordt je gevraagd dit te doen
- Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen
- Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki
Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker.
Wil je je vorderingen zien? Volg de edits op OsmCha.","de":"Über MapComplete
MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem Einzelthema hinzuzufügen.
Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge einfach und extrem benutzerfreundlich zu halten.Der Themen-Betreuer kann auch eine Sprache für die Schnittstelle wählen, Elemente deaktivieren oder sogar in eine andere Website ohne jegliches UI-Element einbetten.
Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer den nächsten Schritt anzubietenum mehr über OpenStreetMap zu erfahren:
- Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion
- Die Vollbildversion bietet Informationen über OpenStreetMap
- Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen
- Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen
- An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...
Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie zum Quellcode oder zur Problemverfolgung.
"} ),
backgroundMap: new Translation( {"en":"Background map","ca":"Mapa de fons","es":"Mapa de fondo","nl":"Achtergrondkaart","fr":"Carte de fonds","de":"Hintergrundkarte"} ),
layerSelection: { zoomInToSeeThisLayer: new Translation( {"en":"Zoom in to see this layer","ca":"Amplia per veure aquesta capa","es":"Amplía para ver esta capa","nl":"Vergroot de kaart om deze laag te zien","fr":"Aggrandissez la carte pour voir cette couche","de":"Vergrößern, um diese Ebene zu sehen"} ),
title: new Translation( {"en":"Select layers","nl":"Selecteer lagen"} ),
diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts
index c65a55655d..e77f31001d 100644
--- a/Customizations/JSON/LayerConfig.ts
+++ b/Customizations/JSON/LayerConfig.ts
@@ -85,6 +85,7 @@ export default class LayerConfig {
this.source = new SourceConfig({
osmTags: osmTags,
geojsonSource: json.source["geoJson"],
+ geojsonSourceLevel: json.source["geoJsonZoomLevel"],
overpassScript: json.source["overpassScript"],
});
} else {
@@ -136,7 +137,7 @@ export default class LayerConfig {
return new TagRenderingConfig(deflt, self.source.osmTags, `${context}.${key}.default value`);
}
if (typeof v === "string") {
- const shared = SharedTagRenderings.SharedTagRendering[v];
+ const shared = SharedTagRenderings.SharedTagRendering.get(v);
if (shared) {
return shared;
}
@@ -159,14 +160,14 @@ export default class LayerConfig {
if (renderingJson === "questions") {
if (readOnly) {
- throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}`
+ throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(renderingJson)}`
}
return new TagRenderingConfig("questions", undefined)
}
- const shared = SharedTagRenderings.SharedTagRendering[renderingJson];
+ const shared = SharedTagRenderings.SharedTagRendering.get(renderingJson);
if (shared !== undefined) {
return shared;
}
@@ -176,7 +177,7 @@ export default class LayerConfig {
});
}
- this.tagRenderings = trs(json.tagRenderings, this.source.geojsonSource !== undefined);
+ this.tagRenderings = trs(json.tagRenderings, false);
const titleIcons = [];
@@ -196,8 +197,8 @@ export default class LayerConfig {
this.icon = tr("icon", "");
this.iconOverlays = (json.iconOverlays ?? []).map((overlay, i) => {
let tr = new TagRenderingConfig(overlay.then, self.source.osmTags, `iconoverlays.${i}`);
- if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons[overlay.then] !== undefined) {
- tr = SharedTagRenderings.SharedIcons[overlay.then];
+ if (typeof overlay.then === "string" && SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined) {
+ tr = SharedTagRenderings.SharedIcons.get(overlay.then);
}
return {
if: FromJSON.Tag(overlay.if),
@@ -410,13 +411,19 @@ export default class LayerConfig {
htmlParts.push(badgesComponent)
}
- if(sourceParts.length ==0){iconH = 0}
+ if (sourceParts.length == 0) {
+ iconH = 0
+ }
+ try {
- const label = self.label.GetRenderValue(tgs)?.Subs(tgs)
- .SetClass("block w-min text-center")
- .SetStyle("margin-top: "+(iconH + 2) +"px")
- if (label !== undefined) {
- htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center"))
+ const label = self.label?.GetRenderValue(tgs)?.Subs(tgs)
+ ?.SetClass("block w-min text-center")
+ ?.SetStyle("margin-top: " + (iconH + 2) + "px")
+ if (label !== undefined) {
+ htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center"))
+ }
+ } catch (e) {
+ console.error(e, tgs)
}
return new Combine(htmlParts).Render();
})
@@ -456,5 +463,4 @@ export default class LayerConfig {
return allIcons;
}
-
}
\ No newline at end of file
diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts
index 34d0a8c056..66af91221c 100644
--- a/Customizations/JSON/LayerConfigJson.ts
+++ b/Customizations/JSON/LayerConfigJson.ts
@@ -29,7 +29,8 @@ export interface LayerConfigJson {
* There are some options:
*
* source: {osmTags: "key=value"} will fetch all objects with given tags from OSM. Currently, this will create a query to overpass and fetch the data - in the future this might fetch from the OSM API
- * source: {geoJsonSource: "https://my.source.net/some-geo-data.geojson"} to fetch a geojson from a third party source
+ * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} to fetch a geojson from a third party source
+ * source: {geoJson: "https://my.source.net/some-tile-geojson-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted
*
* source: {overpassScript: ""} when you want to do special things. _This should be really rare_.
* This means that the data will be pulled from overpass with this script, and will ignore the osmTags for the query
@@ -40,7 +41,7 @@ export interface LayerConfigJson {
* While still supported, this is considered deprecated
*/
source: { osmTags: AndOrTagConfigJson | string } |
- { osmTags: AndOrTagConfigJson | string, geoJson: string } |
+ { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number } |
{ osmTags: AndOrTagConfigJson | string, overpassScript: string }
/**
@@ -130,8 +131,11 @@ export interface LayerConfigJson {
*/
rotation?: string | TagRenderingConfigJson;
/**
- * A HTML-fragment that is shown at the center of the icon, for example:
+ * A HTML-fragment that is shown below the icon, for example:
* {name}
+ *
+ * If the icon is undefined, then the label is shown in the center of the feature.
+ * Note that, if the wayhandling hides the icon then no label is shown as well.
*/
label?: string | TagRenderingConfigJson ;
diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts
index 64cfc84c64..734f254901 100644
--- a/Customizations/JSON/LayoutConfig.ts
+++ b/Customizations/JSON/LayoutConfig.ts
@@ -40,11 +40,15 @@ export default class LayoutConfig {
public readonly enableLayers: boolean;
public readonly enableSearch: boolean;
public readonly enableGeolocation: boolean;
- private readonly _official : boolean;
public readonly enableBackgroundLayerSelection: boolean;
public readonly customCss?: string;
+ /*
+ How long is the cache valid, in seconds?
+ */
+ public readonly cacheTimeout?: number;
+ private readonly _official: boolean;
- constructor(json: LayoutConfigJson, official=true, context?: string) {
+ constructor(json: LayoutConfigJson, official = true, context?: string) {
this._official = official;
this.id = json.id;
context = (context ?? "") + "." + this.id;
@@ -79,8 +83,8 @@ export default class LayoutConfig {
this.widenFactor = json.widenFactor ?? 0.05;
this.roamingRenderings = (json.roamingRenderings ?? []).map((tr, i) => {
if (typeof tr === "string") {
- if (SharedTagRenderings.SharedTagRendering[tr] !== undefined) {
- return SharedTagRenderings.SharedTagRendering[tr];
+ if (SharedTagRenderings.SharedTagRendering.get(tr) !== undefined) {
+ return SharedTagRenderings.SharedTagRendering.get(tr);
}
}
return new TagRenderingConfig(tr, undefined, `${this.id}.roaming_renderings[${i}]`);
@@ -104,11 +108,11 @@ export default class LayoutConfig {
throw "Unkown fixed layer " + name;
}
// @ts-ignore
- layer = Utils.Merge(layer.override, shared);
+ layer = Utils.Merge(layer.override, JSON.parse(JSON.stringify(shared))); // We make a deep copy of the shared layer, in order to protect it from changes
}
// @ts-ignore
- return new LayerConfig(layer,`${this.id}.layers[${i}]`, official)
+ return new LayerConfig(layer, `${this.id}.layers[${i}]`, official)
});
// ALl the layers are constructed, let them share tags in now!
@@ -167,16 +171,17 @@ export default class LayoutConfig {
this.enableAddNewPoints = json.enableAddNewPoints ?? true;
this.enableBackgroundLayerSelection = json.enableBackgroundLayerSelection ?? true;
this.customCss = json.customCss;
+ this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60)
}
public CustomCodeSnippets(): string[] {
- if(this._official){
+ if (this._official) {
return [];
}
const msg = "
This layout uses custom javascript, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:
"
const custom = [];
for (const layer of this.layers) {
- custom.push(...layer.CustomCodeSnippets().map(code => code+"
"))
+ custom.push(...layer.CustomCodeSnippets().map(code => code + "
"))
}
if (custom.length === 0) {
return custom;
@@ -184,8 +189,8 @@ export default class LayoutConfig {
custom.splice(0, 0, msg);
return custom;
}
-
- public ExtractImages() : Set{
+
+ public ExtractImages(): Set {
const icons = new Set()
for (const layer of this.layers) {
layer.ExtractImages().forEach(icons.add, icons)
@@ -194,4 +199,53 @@ export default class LayoutConfig {
icons.add(this.socialImage)
return icons
}
+
+ /**
+ * Replaces all the relative image-urls with a fixed image url
+ * This is to fix loading from external sources
+ *
+ * It should be passed the location where the original theme file is hosted.
+ *
+ * If no images are rewritten, the same object is returned, otherwise a new copy is returned
+ */
+ public patchImages(originalURL: string, originalJson: string): LayoutConfig {
+ const allImages = Array.from(this.ExtractImages())
+ const rewriting = new Map()
+
+ // Needed for absolute urls: note that it doesn't contain a trailing slash
+ const origin = new URL(originalURL).origin
+ let path = new URL(originalURL).href
+ path = path.substring(0, path.lastIndexOf("/"))
+ for (const image of allImages) {
+ if(image == "" || image == undefined){
+ continue
+ }
+ if (image.startsWith("http://") || image.startsWith("https://")) {
+ continue
+ }
+ if (image.startsWith("/")) {
+ // This is an absolute path
+ rewriting.set(image, origin + image)
+ } else if (image.startsWith("./assets/themes")) {
+ // Legacy workaround
+ rewriting.set(image, path + image.substring(image.lastIndexOf("/")))
+ } else if (image.startsWith("./")) {
+ // This is a relative url
+ rewriting.set(image, path + image.substring(1))
+ } else {
+ // This is a relative URL with only the path
+ rewriting.set(image, path + image)
+ }
+ }
+ if (rewriting.size == 0) {
+ return this;
+ }
+ rewriting.forEach((value, key) => {
+ console.log("Rewriting",key, "==>", value)
+
+ originalJson = originalJson.replace(new RegExp(key, "g"), value)
+ })
+ return new LayoutConfig(JSON.parse(originalJson), false, "Layout rewriting")
+ }
+
}
\ No newline at end of file
diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts
index 0b63c727b5..e70677e269 100644
--- a/Customizations/JSON/LayoutConfigJson.ts
+++ b/Customizations/JSON/LayoutConfigJson.ts
@@ -118,6 +118,25 @@ export interface LayoutConfigJson {
* The id of the default background. BY default: vanilla OSM
*/
defaultBackgroundId?: string;
+
+ /**
+ * The number of seconds that a feature is allowed to stay in the cache.
+ * The caching flow is as following:
+ *
+ * 1. The application is opened the first time
+ * 2. An overpass query is run
+ * 3. The result is saved to local storage
+ *
+ * On the next opening:
+ *
+ * 1. The application is opened
+ * 2. Data is loaded from cache and displayed
+ * 3. An overpass query is run
+ * 4. All data (both from overpass ánd local storage) are saved again to local storage (except when to old)
+ *
+ * Default value: 60 days
+ */
+ cacheTimout?: number;
/**
diff --git a/Customizations/JSON/SourceConfig.ts b/Customizations/JSON/SourceConfig.ts
index f354ed33de..336826275a 100644
--- a/Customizations/JSON/SourceConfig.ts
+++ b/Customizations/JSON/SourceConfig.ts
@@ -5,11 +5,13 @@ export default class SourceConfig {
osmTags?: TagsFilter;
overpassScript?: string;
geojsonSource?: string;
+ geojsonZoomLevel?: number;
constructor(params: {
osmTags?: TagsFilter,
overpassScript?: string,
- geojsonSource?: string
+ geojsonSource?: string,
+ geojsonSourceLevel?: number
}) {
let defined = 0;
@@ -28,5 +30,6 @@ export default class SourceConfig {
this.osmTags = params.osmTags;
this.overpassScript = params.overpassScript;
this.geojsonSource = params.geojsonSource;
+ this.geojsonZoomLevel = params.geojsonSourceLevel;
}
}
\ No newline at end of file
diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts
index 05661b737c..d5d237a145 100644
--- a/Customizations/JSON/TagRenderingConfig.ts
+++ b/Customizations/JSON/TagRenderingConfig.ts
@@ -77,7 +77,8 @@ export default class TagRenderingConfig {
throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}`
}
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
- throw `Freeform.key ${this.freeform.key} is an invalid type`
+ const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", ");
+ throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
}
if (this.freeform.addExtraTags) {
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
@@ -204,7 +205,7 @@ export default class TagRenderingConfig {
return true;
}
if (this.multiAnswer) {
- for (const m of this.mappings) {
+ for (const m of this.mappings ?? []) {
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
return true;
}
diff --git a/Customizations/SharedTagRenderings.ts b/Customizations/SharedTagRenderings.ts
index 772bd97bba..3d3a05208b 100644
--- a/Customizations/SharedTagRenderings.ts
+++ b/Customizations/SharedTagRenderings.ts
@@ -4,15 +4,15 @@ import * as icons from "../assets/tagRenderings/icons.json";
export default class SharedTagRenderings {
- public static SharedTagRendering = SharedTagRenderings.generatedSharedFields();
- public static SharedIcons = SharedTagRenderings.generatedSharedFields(true);
+ public static SharedTagRendering : Map = SharedTagRenderings.generatedSharedFields();
+ public static SharedIcons : Map = SharedTagRenderings.generatedSharedFields(true);
- private static generatedSharedFields(iconsOnly = false) {
- const dict = {}
+ private static generatedSharedFields(iconsOnly = false) : Map{
+ const dict = new Map();
function add(key, store) {
try {
- dict[key] = new TagRenderingConfig(store[key], key)
+ dict.set(key, new TagRenderingConfig(store[key], key))
} catch (e) {
console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e)
}
diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md
index d531ca393e..38fa2f762d 100644
--- a/Docs/CalculatedTags.md
+++ b/Docs/CalculatedTags.md
@@ -11,6 +11,10 @@ The latitude and longitude of the point (or centerpoint in the case of a way/are
The surface area of the feature, in square meters and in hectare. Not set on points and ways
+### \_length, \_length:km
+
+The total length of a feature in meters (and in kilometers, rounded to one decimal for '\_length:km'). For a surface, the length of the perimeter
+
### \_country
The country code of the property (with latlon2country)
@@ -53,6 +57,7 @@ The above code will be executed for every feature in the layer. The feature is a
* distanceTo
* overlapWith
* closest
+* memberships
### distanceTo
@@ -71,4 +76,8 @@ Gives a list of features from the specified layer which this feature overlaps wi
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.
-* list of features
\ No newline at end of file
+* list of features
+
+### memberships
+
+Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. For example: \`\_part\_of\_walking\_routes=feat.memberships().map(r => r.relation.tags.name).join(';')\`
\ No newline at end of file
diff --git a/Docs/Tools/csvGrapher.py b/Docs/Tools/csvGrapher.py
index 8e7ea7a2b5..985f588423 100644
--- a/Docs/Tools/csvGrapher.py
+++ b/Docs/Tools/csvGrapher.py
@@ -358,6 +358,7 @@ theme_remappings = {
"wiki-User-joost_schouppe-campersite": "campersite",
"wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes",
"wiki:User:joost_schouppe/campersite": "campersite",
+ "arbres":"arbres_leffia",
"https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes"
}
diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md
index 49526404dd..0d71e0b712 100644
--- a/Docs/URL_Parameters.md
+++ b/Docs/URL_Parameters.md
@@ -1,3 +1,24 @@
+URL-parameters and URL-hash
+============================
+
+This document gives an overview of which URL-parameters can be used to influence MapComplete.
+
+What is a URL parameter?
+------------------------
+
+URL-parameters are extra parts of the URL used to set the state.
+
+For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`,
+the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all seperated by `&`, namely:
+
+- The url-parameter `lat` is `51.0` in this instance
+- The url-parameter `lon` is `4.3` in this instance
+- The url-parameter `z` is `5` in this instance
+- The url-parameter `test` is `true` in this instance
+
+Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case.
+
+
custom-css
------------
If specified, the custom css from the given link will be loaded additionaly
@@ -14,6 +35,13 @@ The layout to load into MapComplete
userlayout
------------
+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_
+
The default value is _false_
layer-control-toggle
diff --git a/InitUiElements.ts b/InitUiElements.ts
index 8593398c23..d2e4b9e53c 100644
--- a/InitUiElements.ts
+++ b/InitUiElements.ts
@@ -247,7 +247,6 @@ export class InitUiElements {
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg());
- // .SetClass("open-welcome-button block");
new CheckBox(
fullOptions
.SetClass("welcomeMessage")
diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts
index 93bb4bb452..f03961ae1d 100644
--- a/Logic/Actors/TitleHandler.ts
+++ b/Logic/Actors/TitleHandler.ts
@@ -42,10 +42,11 @@ class TitleElement extends UIElement {
continue;
}
if (layer.source.osmTags.matchesProperties(properties)) {
- const title = new TagRenderingAnswer(
- this._allElementsStorage.addOrGetElement(feature),
- layer.title
- )
+ const tags = this._allElementsStorage.getEventSourceById(feature.properties.id);
+ if (tags == undefined) {
+ return defaultTitle;
+ }
+ const title = new TagRenderingAnswer(tags, layer.title)
return new Combine([defaultTitle, " | ", title]).Render();
}
}
diff --git a/Logic/Actors/UpdateFromOverpass.ts b/Logic/Actors/UpdateFromOverpass.ts
index c75239a939..370da705bd 100644
--- a/Logic/Actors/UpdateFromOverpass.ts
+++ b/Logic/Actors/UpdateFromOverpass.ts
@@ -125,7 +125,12 @@ export default class UpdateFromOverpass implements FeatureSource {
private update(): void {
if (this.runningQuery.data) {
- console.log("Still running a query, skip");
+ console.log("Still running a query, not updating");
+ return;
+ }
+
+ if(this.timeout.data > 0){
+ console.log("Still in timeout - not updating")
return;
}
diff --git a/Logic/Actors/UpdateTagsFromOsmAPI.ts b/Logic/Actors/UpdateTagsFromOsmAPI.ts
new file mode 100644
index 0000000000..cdf0863b06
--- /dev/null
+++ b/Logic/Actors/UpdateTagsFromOsmAPI.ts
@@ -0,0 +1,54 @@
+import {UIEventSource} from "../UIEventSource";
+import {ElementStorage} from "../ElementStorage";
+import {OsmObject, OsmObjectMeta} from "../Osm/OsmObject";
+import SimpleMetaTagger from "../SimpleMetaTagger";
+
+export default class UpdateTagsFromOsmAPI {
+
+
+ public static readonly metaTagger = new SimpleMetaTagger(
+ ["_last_edit:contributor",
+ "_last_edit:contributor:uid",
+ "_last_edit:changeset",
+ "_last_edit:timestamp",
+ "_version_number"],
+ "Information about the last edit of this object. \n\nIMPORTANT: this data is _only_ loaded when the popup is added. This means it should _not_ be used to render icons!",
+ (feature: any, index: number, freshness: Date) => {/*Do nothing - this is only added for documentation reasons*/
+ }
+ )
+
+ /***
+ * This actor downloads the element from the OSM-API and updates the corresponding tags in the UI-updater.
+ */
+ constructor(idToDownload: UIEventSource, allElements: ElementStorage) {
+
+ idToDownload.addCallbackAndRun(id => {
+ if (id === undefined) {
+ return;
+ }
+
+ OsmObject.DownloadObject(id, (element: OsmObject, meta: OsmObjectMeta) => {
+ console.log("Updating element from OSM-API: ", element)
+
+
+ const tags = element.tags;
+ tags["_last_edit:contributor"] = meta["_last_edit:contributor"]
+ tags["_last_edit:contributor:uid"] = meta["_last_edit:contributor:uid"]
+ tags["_last_edit:changeset"] = meta["_last_edit:changeset"]
+ tags["_last_edit:timestamp"] = meta["_last_edit:timestamp"].toLocaleString()
+ tags["_version_number"] = meta._version_number
+ if (!allElements.has(id)) {
+ console.warn("Adding element by id")
+ allElements.addElementById(id, new UIEventSource(tags))
+ } else {
+ // We merge
+ console.warn("merging by OSM API UPDATE")
+ allElements.addOrGetById(id, tags)
+ }
+ })
+ })
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts
index d301d794e2..88e4ab1513 100644
--- a/Logic/ElementStorage.ts
+++ b/Logic/ElementStorage.ts
@@ -5,14 +5,14 @@ import {UIEventSource} from "./UIEventSource";
export class ElementStorage {
- private _elements = [];
+ private _elements = new Map>();
constructor() {
}
addElementById(id: string, eventSource: UIEventSource) {
- this._elements[id] = eventSource;
+ this._elements.set(id, eventSource);
}
/**
@@ -23,41 +23,54 @@ export class ElementStorage {
*/
addOrGetElement(feature: any): UIEventSource {
const elementId = feature.properties.id;
- if (elementId in this._elements) {
- const es = this._elements[elementId];
- if (es.data == feature.properties) {
- // Reference comparison gives the same object! we can just return the event source
- return es;
- }
+ const newProperties = feature.properties;
+ const es = this.addOrGetById(elementId, newProperties)
- const keptKeys = es.data;
- // The element already exists
- // We add all the new keys to the old keys
- let somethingChanged = false;
- for (const k in feature.properties) {
- const v = feature.properties[k];
- if (keptKeys[k] !== v) {
- keptKeys[k] = v;
- somethingChanged = true;
- }
- }
- if (somethingChanged) {
- es.ping();
- }
+ // At last, we overwrite the tag of the new feature to use the tags in the already existing event source
+ feature.properties = es.data
+ return es;
+ }
- return es;
- } else {
- const eventSource = new UIEventSource(feature.properties, "tags of " + feature.properties.id);
- this._elements[feature.properties.id] = eventSource;
+ addOrGetById(elementId: string, newProperties: any): UIEventSource {
+ if (!this._elements.has(elementId)) {
+ const eventSource = new UIEventSource(newProperties, "tags of " + elementId);
+ this._elements.set(elementId, eventSource);
return eventSource;
}
+
+
+ const es = this._elements.get(elementId);
+ if (es.data == newProperties) {
+ // Reference comparison gives the same object! we can just return the event source
+ return es;
+ }
+ const keptKeys = es.data;
+ // The element already exists
+ // We use the new feature to overwrite all the properties in the already existing eventsource
+ let somethingChanged = false;
+ for (const k in newProperties) {
+ const v = newProperties[k];
+ if (keptKeys[k] !== v) {
+ keptKeys[k] = v;
+ somethingChanged = true;
+ }
+ }
+ if (somethingChanged) {
+ es.ping();
+ }
+ return es;
}
getEventSourceById(elementId): UIEventSource {
- if (elementId in this._elements) {
- return this._elements[elementId];
+ if (this._elements.has(elementId)) {
+ return this._elements.get(elementId);
}
console.error("Can not find eventsource with id ", elementId);
+ return undefined;
+ }
+
+ has(id) {
+ return this._elements.has(id);
}
}
\ No newline at end of file
diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts
index 1689e67d14..cfb4ed8eef 100644
--- a/Logic/ExtraFunction.ts
+++ b/Logic/ExtraFunction.ts
@@ -1,6 +1,8 @@
import {GeoOperations} from "./GeoOperations";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
+import State from "../State";
+import {Relation} from "./Osm/ExtractRelations";
export class ExtraFunction {
@@ -35,15 +37,15 @@ The above code will be executed for every feature in the layer. The feature is a
Some advanced functions are available on feat as well:
`
- private static OverlapFunc = new ExtraFunction(
+ private static readonly OverlapFunc = new ExtraFunction(
"overlapWith",
"Gives a list of features from the specified layer which this feature overlaps with, the amount of overlap in m². The returned value is { feat: GeoJSONFeature, overlap: number}",
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
- (featuresPerLayer, feat) => {
+ (params, feat) => {
return (...layerIds: string[]) => {
const result = []
for (const layerId of layerIds) {
- const otherLayer = featuresPerLayer.get(layerId);
+ const otherLayer = params.featuresPerLayer.get(layerId);
if (otherLayer === undefined) {
continue;
}
@@ -56,38 +58,38 @@ Some advanced functions are available on feat as well:
}
}
)
- private static DistanceToFunc = new ExtraFunction(
+ private static readonly DistanceToFunc = new ExtraFunction(
"distanceTo",
"Calculates the distance between the feature and a specified point",
["longitude", "latitude"],
(featuresPerLayer, feature) => {
return (arg0, lat) => {
- if(typeof arg0 === "number"){
+ if (typeof arg0 === "number") {
const lon = arg0
// Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
- }else{
+ } else {
// arg0 is probably a feature
- return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0),[feature._lon, feature._lat])
+ return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat])
}
-
+
}
}
)
- private static ClosestObjectFunc = new ExtraFunction(
+ private static readonly ClosestObjectFunc = new ExtraFunction(
"closest",
"Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.",
["list of features"],
- (featuresPerLayer, feature) => {
+ (params, feature) => {
return (features) => {
if (typeof features === "string") {
- features = featuresPerLayer.get(features)
+ features = params.featuresPerLayer.get(features)
}
let closestFeature = undefined;
let closestDistance = undefined;
for (const otherFeature of features) {
- if(otherFeature == feature){
+ if (otherFeature == feature) {
continue; // We ignore self
}
let distance = undefined;
@@ -99,10 +101,10 @@ Some advanced functions are available on feat as well:
[feature._lon, feature._lat]
)
}
- if(distance === undefined){
+ if (distance === undefined) {
throw "Undefined distance!"
}
- if(closestFeature === undefined || distance < closestDistance){
+ if (closestFeature === undefined || distance < closestDistance) {
closestFeature = otherFeature
closestDistance = distance;
}
@@ -113,13 +115,22 @@ Some advanced functions are available on feat as well:
)
- private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc];
+ private static readonly Memberships = new ExtraFunction(
+ "memberships",
+ "Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. \n\nFor example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
+ [],
+ (params, feature) => {
+ return () => params.relations ?? [];
+ }
+ )
+
+ private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, ExtraFunction.Memberships];
private readonly _name: string;
private readonly _args: string[];
private readonly _doc: string;
- private readonly _f: (featuresPerLayer: Map, feat: any) => any;
+ private readonly _f: (params: {featuresPerLayer: Map, relations: {role: string, relation: Relation}[]}, feat: any) => any;
- constructor(name: string, doc: string, args: string[], f: ((featuresPerLayer: Map, feat: any) => any)) {
+ constructor(name: string, doc: string, args: string[], f: ((params: {featuresPerLayer: Map, relations: {role: string, relation: Relation}[]}, feat: any) => any)) {
this._name = name;
this._doc = doc;
this._args = args;
@@ -127,9 +138,9 @@ Some advanced functions are available on feat as well:
}
- public static FullPatchFeature(featuresPerLayer: Map, feature) {
+ public static FullPatchFeature(featuresPerLayer: Map,relations: {role: string, relation: Relation}[], feature) {
for (const func of ExtraFunction.allFuncs) {
- func.PatchFeature(featuresPerLayer, feature);
+ func.PatchFeature(featuresPerLayer, relations, feature);
}
}
@@ -155,7 +166,8 @@ Some advanced functions are available on feat as well:
]);
}
- public PatchFeature(featuresPerLayer: Map, feature: any) {
- feature[this._name] = this._f(featuresPerLayer, feature);
+ public PatchFeature(featuresPerLayer: Map, relations: {role: string, relation: Relation}[], feature: any) {
+
+ feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature);
}
}
\ No newline at end of file
diff --git a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
index a58534cf9e..5ca687012c 100644
--- a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
+++ b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
@@ -11,6 +11,10 @@ import LayerConfig from "../../Customizations/JSON/LayerConfig";
export default class FeatureDuplicatorPerLayer implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
+
+ public static GetMatchingLayerId(){
+
+ }
constructor(layers: UIEventSource<{ layerDef: LayerConfig }[]>, upstream: FeatureSource) {
this.features = upstream.features.map(features => {
diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts
index f6cca66ff3..81fe3e20dc 100644
--- a/Logic/FeatureSource/FeaturePipeline.ts
+++ b/Logic/FeatureSource/FeaturePipeline.ts
@@ -12,6 +12,7 @@ import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import Loc from "../../Models/Loc";
import GeoJsonSource from "./GeoJsonSource";
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
+import RegisteringFeatureSource from "./RegisteringFeatureSource";
export default class FeaturePipeline implements FeatureSource {
@@ -24,33 +25,33 @@ export default class FeaturePipeline implements FeatureSource {
locationControl: UIEventSource) {
const amendedOverpassSource =
- new RememberingSource(new FeatureDuplicatorPerLayer(flayers,
- new LocalStorageSaver(updater, layout))
- );
-
- const geojsonSources: GeoJsonSource [] = []
- for (const flayer of flayers.data) {
- const sourceUrl = flayer.layerDef.source.geojsonSource
- if (sourceUrl !== undefined) {
- geojsonSources.push(
- new GeoJsonSource(flayer.layerDef.id, sourceUrl))
- }
- }
+ new RememberingSource(
+ new LocalStorageSaver(
+ new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too
+ new RegisteringFeatureSource(
+ new FeatureDuplicatorPerLayer(flayers,
+ updater)
+ )), layout));
+ const geojsonSources: FeatureSource [] = GeoJsonSource
+ .ConstructMultiSource(flayers.data, locationControl)
+ .map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)));
+
const amendedLocalStorageSource =
- new RememberingSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
- );
+ new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
+ ));
- newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
+ newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers,
+ new RegisteringFeatureSource(newPoints)));
const merged =
- new MetaTaggingFeatureSource(
- new FeatureSourceMerger([
- amendedOverpassSource,
- amendedLocalStorageSource,
- newPoints,
- ...geojsonSources
- ]));
+
+ new FeatureSourceMerger([
+ amendedOverpassSource,
+ amendedLocalStorageSource,
+ newPoints,
+ ...geojsonSources
+ ]);
const source =
new WayHandlingApplyingFeatureSource(flayers,
diff --git a/Logic/FeatureSource/GeoJsonSource.ts b/Logic/FeatureSource/GeoJsonSource.ts
index dea7356477..7f17fd8fa6 100644
--- a/Logic/FeatureSource/GeoJsonSource.ts
+++ b/Logic/FeatureSource/GeoJsonSource.ts
@@ -1,51 +1,195 @@
import FeatureSource from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import * as $ from "jquery";
+import {control} from "leaflet";
+import zoom = control.zoom;
+import Loc from "../../Models/Loc";
+import State from "../../State";
+import {Utils} from "../../Utils";
+import LayerConfig from "../../Customizations/JSON/LayerConfig";
+
/**
* Fetches a geojson file somewhere and passes it along
*/
export default class GeoJsonSource implements FeatureSource {
+
features: UIEventSource<{ feature: any; freshness: Date }[]>;
- constructor(layerId: string, url: string, onFail: ((errorMsg: any) => void) = undefined) {
- if (onFail === undefined) {
- onFail = errorMsg => {
- console.warn(`Could not load geojson layer from`, url, "due to", errorMsg)
+ private readonly onFail: ((errorMsg: any, url: string) => void) = undefined;
+
+ private readonly layerId: string;
+
+ private readonly seenids: Set = new Set()
+
+ constructor(locationControl: UIEventSource,
+ flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig },
+ onFail?: ((errorMsg: any) => void)) {
+ this.layerId = flayer.layerDef.id;
+ let url = flayer.layerDef.source.geojsonSource;
+ const zoomLevel = flayer.layerDef.source.geojsonZoomLevel;
+
+ this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
+
+ if (zoomLevel === undefined) {
+ // This is a classic, static geojson layer
+ if (onFail === undefined) {
+ onFail = errorMsg => {
+ console.warn(`Could not load geojson layer from`, url, "due to", errorMsg)
+ }
}
+ this.onFail = onFail;
+
+ this.LoadJSONFrom(url)
+ } else {
+ // This is a dynamic template with a fixed zoom level
+ url = url.replace("{z}", "" + zoomLevel)
+ const loadedTiles = new Set();
+ const self = this;
+ this.onFail = (msg, url) => {
+ console.warn(`Could not load geojson layer from`, url, "due to", msg)
+ loadedTiles.delete(url)
+ }
+
+ const neededTiles = locationControl.map(
+ location => {
+
+ if (!flayer.isDisplayed.data) {
+ return undefined;
+ }
+
+ // Yup, this is cheating to just get the bounds here
+ const bounds = State.state.leafletMap.data.getBounds()
+ const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
+ const needed = new Set();
+ for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
+ for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
+ let neededUrl = url.replace("{x}", "" + x).replace("{y}", "" + y);
+ needed.add(neededUrl)
+ }
+ }
+ return needed;
+ }
+ );
+ neededTiles.stabilized(250).addCallback((needed: Set) => {
+ if (needed === undefined) {
+ return;
+ }
+ needed.forEach(neededTile => {
+ if (loadedTiles.has(neededTile)) {
+ return;
+ }
+
+ loadedTiles.add(neededTile)
+ self.LoadJSONFrom(neededTile)
+
+ })
+ })
+
}
- this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
+ }
+
+ /**
+ * Merges together the layers which have the same source
+ * @param flayers
+ * @param locationControl
+ * @constructor
+ */
+ public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource, layerDef: LayerConfig }[], locationControl: UIEventSource): GeoJsonSource[] {
+
+ const flayersPerSource = new Map, layerDef: LayerConfig }[]>();
+ for (const flayer of flayers) {
+ const url = flayer.layerDef.source.geojsonSource
+ if (url === undefined) {
+ continue;
+ }
+
+ if (!flayersPerSource.has(url)) {
+ flayersPerSource.set(url, [])
+ }
+ flayersPerSource.get(url).push(flayer)
+ }
+
+ console.log("SOURCES", flayersPerSource)
+
+ const sources: GeoJsonSource[] = []
+
+ flayersPerSource.forEach((flayers, key) => {
+ if (flayers.length == 1) {
+ sources.push(new GeoJsonSource(locationControl, flayers[0]));
+ return;
+ }
+
+ const zoomlevels = Utils.Dedup(flayers.map(flayer => "" + (flayer.layerDef.source.geojsonZoomLevel ?? "")))
+ if (zoomlevels.length > 1) {
+ throw "Multiple zoomlevels defined for same geojson source " + key
+ }
+
+ let isShown = new UIEventSource(true, "IsShown for multiple layers: or of multiple values");
+ for (const flayer of flayers) {
+ flayer.isDisplayed.addCallbackAndRun(() => {
+ let value = false;
+ for (const flayer of flayers) {
+ value = flayer.isDisplayed.data || value;
+ }
+ isShown.setData(value);
+ });
+
+ }
+
+ const source = new GeoJsonSource(locationControl, {
+ isDisplayed: isShown,
+ layerDef: flayers[0].layerDef // We only care about the source info here
+ })
+ sources.push(source)
+
+ })
+ return sources;
+
+ }
+
+ private LoadJSONFrom(url: string) {
const eventSource = this.features;
+ const self = this;
$.getJSON(url, function (json, status) {
if (status !== "success") {
console.log("Fetching geojson failed failed")
- onFail(status);
+ self.onFail(status, url);
return;
}
if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) {
console.log("Timeout or other runtime error");
- onFail("Runtime error (timeout)")
+ self.onFail("Runtime error (timeout)", url)
return;
}
const time = new Date();
- const features: { feature: any, freshness: Date } [] = []
+ const newFeatures: { feature: any, freshness: Date } [] = []
let i = 0;
+ let skipped = 0;
for (const feature of json.features) {
if (feature.properties.id === undefined) {
feature.properties.id = url + "/" + i;
feature.id = url + "/" + i;
i++;
}
- feature._matching_layer_id = layerId;
- features.push({feature: feature, freshness: time})
+ if (self.seenids.has(feature.properties.id)) {
+ skipped++;
+ continue;
+ }
+ self.seenids.add(feature.properties.id)
+
+ newFeatures.push({feature: feature, freshness: time})
}
- console.log("Loaded features are", features)
- eventSource.setData(features)
-
- }).fail(onFail)
+ console.log("Downloaded "+newFeatures.length+" new features and "+skipped+" already seen features from "+ url);
+
+ if(newFeatures.length == 0){
+ return;
+ }
+
+ eventSource.setData(eventSource.data.concat(newFeatures))
+ }).fail(msg => self.onFail(msg, url))
}
-
}
\ No newline at end of file
diff --git a/Logic/FeatureSource/LocalStorageSaver.ts b/Logic/FeatureSource/LocalStorageSaver.ts
index 6dc4630981..5a66cef056 100644
--- a/Logic/FeatureSource/LocalStorageSaver.ts
+++ b/Logic/FeatureSource/LocalStorageSaver.ts
@@ -18,6 +18,11 @@ export default class LocalStorageSaver implements FeatureSource {
if (features === undefined) {
return;
}
+
+ const now = new Date().getTime()
+ features = features.filter(f => layout.data.cacheTimeout > Math.abs(now - f.freshness.getTime())/1000)
+
+
if(features.length == 0){
return;
}
diff --git a/Logic/FeatureSource/MetaTaggingFeatureSource.ts b/Logic/FeatureSource/MetaTaggingFeatureSource.ts
index fd2472203c..3cc1c2877a 100644
--- a/Logic/FeatureSource/MetaTaggingFeatureSource.ts
+++ b/Logic/FeatureSource/MetaTaggingFeatureSource.ts
@@ -3,6 +3,7 @@ import {UIEventSource} from "../UIEventSource";
import State from "../../State";
import Hash from "../Web/Hash";
import MetaTagging from "../MetaTagging";
+import ExtractRelations from "../Osm/ExtractRelations";
export default class MetaTaggingFeatureSource implements FeatureSource {
features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{feature: any; freshness: Date}[]>(undefined);
@@ -15,14 +16,13 @@ export default class MetaTaggingFeatureSource implements FeatureSource {
}
featuresFreshness.forEach(featureFresh => {
const feature = featureFresh.feature;
- State.state.allElements.addOrGetElement(feature);
-
+
if (Hash.hash.data === feature.properties.id) {
State.state.selectedElement.setData(feature);
}
})
- MetaTagging.addMetatags(featuresFreshness, State.state.layoutToUse.data.layers);
+ MetaTagging.addMetatags(featuresFreshness, State.state.knownRelations.data, State.state.layoutToUse.data.layers);
self.features.setData(featuresFreshness);
});
}
diff --git a/Logic/FeatureSource/RegisteringFeatureSource.ts b/Logic/FeatureSource/RegisteringFeatureSource.ts
new file mode 100644
index 0000000000..9e5d19dc15
--- /dev/null
+++ b/Logic/FeatureSource/RegisteringFeatureSource.ts
@@ -0,0 +1,19 @@
+import FeatureSource from "./FeatureSource";
+import {UIEventSource} from "../UIEventSource";
+import State from "../../State";
+
+export default class RegisteringFeatureSource implements FeatureSource {
+ features: UIEventSource<{ feature: any; freshness: Date }[]>;
+
+ constructor(source: FeatureSource) {
+ this.features = source.features;
+ this.features.addCallbackAndRun(features => {
+ for (const feature of features ?? []) {
+ if (!State.state.allElements.has(feature.feature.properties.id)) {
+ State.state.allElements.addOrGetElement(feature.feature)
+ }
+ }
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts
index a687dee553..43dd45ae9c 100644
--- a/Logic/GeoOperations.ts
+++ b/Logic/GeoOperations.ts
@@ -1,4 +1,4 @@
-import * as turf from 'turf'
+import * as turf from '@turf/turf'
export class GeoOperations {
@@ -15,7 +15,8 @@ export class GeoOperations {
return newFeature;
}
- static centerpointCoordinates(feature: any){
+ static centerpointCoordinates(feature: any): [number, number]{
+ // @ts-ignore
return turf.center(feature).geometry.coordinates;
}
@@ -118,6 +119,9 @@ export class GeoOperations {
return inside;
};
+ static lengthInMeters(feature: any) {
+ return turf.length(feature) * 1000
+ }
}
diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts
index b405947f20..1a646b26a9 100644
--- a/Logic/MetaTagging.ts
+++ b/Logic/MetaTagging.ts
@@ -1,6 +1,14 @@
import LayerConfig from "../Customizations/JSON/LayerConfig";
import SimpleMetaTagger from "./SimpleMetaTagger";
import {ExtraFunction} from "./ExtraFunction";
+import State from "../State";
+import {Relation} from "./Osm/ExtractRelations";
+
+
+interface Params {
+ featuresPerLayer: Map,
+ memberships: Map
+}
/**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
@@ -14,7 +22,8 @@ export default class MetaTagging {
* An actor which adds metatags on every feature in the given object
* The features are a list of geojson-features, with a "properties"-field and geometry
*/
- static addMetatags(features: { feature: any; freshness: Date }[], layers: LayerConfig[]) {
+ static addMetatags(features: { feature: any; freshness: Date }[],
+ relations: Map, layers: LayerConfig[]) {
for (const metatag of SimpleMetaTagger.metatags) {
try {
@@ -26,7 +35,7 @@ export default class MetaTagging {
}
// The functions - per layer - which add the new keys
- const layerFuncs = new Map, feature: any) => void)>();
+ const layerFuncs = new Map void)>();
for (const layer of layers) {
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
}
@@ -48,27 +57,26 @@ export default class MetaTagging {
if (f === undefined) {
continue;
}
-
- f(featuresPerLayer, feature.feature)
+ f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature)
}
}
- private static createRetaggingFunc(layer: LayerConfig): ((featuresPerLayer: Map, feature: any) => void) {
+ private static createRetaggingFunc(layer: LayerConfig):
+ ((params: Params, feature: any) => void) {
const calculatedTags: [string, string][] = layer.calculatedTags;
if (calculatedTags === undefined) {
return undefined;
}
- const functions: ((featuresPerLayer: Map, feature: any) => void)[] = [];
+ const functions: ((params: Params, feature: any) => void)[] = [];
for (const entry of calculatedTags) {
const key = entry[0]
const code = entry[1];
if (code === undefined) {
continue;
}
-
const func = new Function("feat", "return " + code + ";");
const f = (featuresPerLayer, feature: any) => {
@@ -76,16 +84,17 @@ export default class MetaTagging {
}
functions.push(f)
}
- return (featuresPerLayer: Map, feature) => {
+ return (params: Params, feature) => {
const tags = feature.properties
if (tags === undefined) {
return;
}
- ExtraFunction.FullPatchFeature(featuresPerLayer, feature);
+ const relations = params.memberships.get(feature.properties.id)
+ ExtraFunction.FullPatchFeature(params.featuresPerLayer, relations, feature);
try {
for (const f of functions) {
- f(featuresPerLayer, feature);
+ f(params, feature);
}
} catch (e) {
console.error("While calculating a tag value: ", e)
diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts
index caf0afe664..0471a49cd1 100644
--- a/Logic/Osm/Changes.ts
+++ b/Logic/Osm/Changes.ts
@@ -57,9 +57,11 @@ 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;
+ console.log("Applied ", change.k, "=", change.v)
this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v});
}
}
diff --git a/Logic/Osm/ExtractRelations.ts b/Logic/Osm/ExtractRelations.ts
new file mode 100644
index 0000000000..24eefefd9e
--- /dev/null
+++ b/Logic/Osm/ExtractRelations.ts
@@ -0,0 +1,63 @@
+import State from "../../State";
+
+export interface Relation {
+ id: number,
+ type: "relation"
+ members: {
+ type: ("way" | "node" | "relation"),
+ ref: number,
+ role: string
+ }[],
+ tags: any,
+ // Alias for tags; tags == properties
+ properties: any
+}
+
+export default class ExtractRelations {
+
+ public static RegisterRelations(overpassJson: any) : void{
+ const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
+ State.state.knownRelations.setData(memberships)
+ }
+
+ /**
+ * Gets an overview of the relations - except for multipolygons. We don't care about those
+ * @param overpassJson
+ * @constructor
+ */
+ public static GetRelationElements(overpassJson: any): Relation[] {
+ const relations = overpassJson.elements
+ .filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
+ for (const relation of relations) {
+ relation.properties = relation.tags
+ }
+ return relations
+ }
+
+ /**
+ * Build a mapping of {memberId --> {role in relation, id of relation} }
+ * @param relations
+ * @constructor
+ */
+ public static BuildMembershipTable(relations: Relation[]): Map {
+ const memberships = new Map()
+
+ for (const relation of relations) {
+ for (const member of relation.members) {
+
+ const role = {
+ role: member.role,
+ relation: relation
+ }
+ const key = member.type + "/" + member.ref
+ if (!memberships.has(key)) {
+ memberships.set(key, [])
+ }
+ memberships.get(key).push(role)
+ }
+ }
+
+ return memberships
+ }
+
+}
\ No newline at end of file
diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts
index 86a64eb917..b1f6d2024f 100644
--- a/Logic/Osm/OsmObject.ts
+++ b/Logic/Osm/OsmObject.ts
@@ -15,18 +15,16 @@ export abstract class OsmObject {
this.type = type;
}
- static DownloadObject(id, continuation: ((element: OsmObject) => void)) {
+ static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
-
- const newContinuation = (element: OsmObject) => {
-
- console.log("Received: ",element);
-
- continuation(element);
+
+ const newContinuation = (element: OsmObject, meta :OsmObjectMeta) => {
+ console.log("Received: ", element, "with meta", meta);
+ continuation(element, meta);
}
-
+
switch (type) {
case("node"):
return new OsmNode(idN).Download(newContinuation);
@@ -38,66 +36,7 @@ export abstract class OsmObject {
}
}
-
- abstract SaveExtraData(element);
-
-
- /**
- * Generates the changeset-XML for tags
- * @constructor
- */
- TagsXML(): string {
- let tags = "";
- for (const key in this.tags) {
- const v = this.tags[key];
- if (v !== "") {
- tags += ' \n'
- }
- }
- return tags;
- }
-
-
- Download(continuation: ((element: OsmObject) => void)) {
- const self = this;
- $.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id,
- function (data) {
- const element = data.elements[0];
- self.tags = element.tags;
- self.version = element.version;
- self.SaveExtraData(element);
- continuation(self);
- }
- );
- return this;
- }
-
- public addTag(k: string, v: string): void {
- if (k in this.tags) {
- const oldV = this.tags[k];
- if (oldV == v) {
- return;
- }
- console.log("WARNING: overwriting ",oldV, " with ", v," for key ",k)
- }
- this.tags[k] = v;
- if(v === undefined || v === ""){
- delete this.tags[k];
- }
- this.changed = true;
- }
-
- protected VersionXML(){
- if(this.version === undefined){
- return "";
- }
- return 'version="'+this.version+'"';
- }
- abstract ChangesetXML(changesetId: string): string;
-
-
-
- public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects : any) => void)) {
+ public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) {
// local function which downloads all the objects one by one
// this is one big loop, running one download, then rerunning the entire function
if (neededIds.length == 0) {
@@ -115,10 +54,71 @@ export abstract class OsmObject {
OsmObject.DownloadObject(neededId,
function (element) {
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
- OsmObject.DownloadAll(neededIds,knownElements, continuation);
+ OsmObject.DownloadAll(neededIds, knownElements, continuation);
}
);
}
+
+ abstract SaveExtraData(element);
+
+ /**
+ * Generates the changeset-XML for tags
+ * @constructor
+ */
+ TagsXML(): string {
+ let tags = "";
+ for (const key in this.tags) {
+ const v = this.tags[key];
+ if (v !== "") {
+ tags += ' \n'
+ }
+ }
+ return tags;
+ }
+
+ Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
+ const self = this;
+ $.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id,
+ function (data) {
+ const element = data.elements[0];
+ self.tags = element.tags;
+ self.version = element.version;
+ self.SaveExtraData(element);
+ continuation(self, {
+ "_last_edit:contributor": element.user,
+ "_last_edit:contributor:uid": element.uid,
+ "_last_edit:changeset": element.changeset,
+ "_last_edit:timestamp": new Date(element.timestamp),
+ "_version_number": element.version
+ });
+ }
+ );
+ return this;
+ }
+
+ public addTag(k: string, v: string): void {
+ if (k in this.tags) {
+ const oldV = this.tags[k];
+ if (oldV == v) {
+ return;
+ }
+ console.log("WARNING: overwriting ", oldV, " with ", v, " for key ", k)
+ }
+ this.tags[k] = v;
+ if (v === undefined || v === "") {
+ delete this.tags[k];
+ }
+ this.changed = true;
+ }
+
+ abstract ChangesetXML(changesetId: string): string;
+
+ protected VersionXML() {
+ if (this.version === undefined) {
+ return "";
+ }
+ return 'version="' + this.version + '"';
+ }
}
@@ -149,6 +149,15 @@ export class OsmNode extends OsmObject {
}
}
+export interface OsmObjectMeta{
+ "_last_edit:contributor": string,
+ "_last_edit:contributor:uid": number,
+ "_last_edit:changeset": number,
+ "_last_edit:timestamp": Date,
+ "_version_number": number
+
+}
+
export class OsmWay extends OsmObject {
nodes: number[];
diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts
index 401d96bd01..007cf9f4ec 100644
--- a/Logic/Osm/Overpass.ts
+++ b/Logic/Osm/Overpass.ts
@@ -1,7 +1,8 @@
import * as $ from "jquery"
import * as OsmToGeoJson from "osmtogeojson";
import Bounds from "../../Models/Bounds";
-import {TagsFilter} from "../TagsFilter";
+import {TagsFilter} from "../Tags/TagsFilter";
+import ExtractRelations from "./ExtractRelations";
/**
* Interfaces overpass to get all the latest data
@@ -38,16 +39,16 @@ export class Overpass {
return;
}
+ ExtractRelations.RegisterRelations(json)
// @ts-ignore
const geojson = OsmToGeoJson.default(json);
- console.log("Received geojson", geojson)
const osmTime = new Date(json.osm3s.timestamp_osm_base);
continuation(geojson, osmTime);
}).fail(onFail)
}
- private buildQuery(bbox: string): string {
+ buildQuery(bbox: string): string {
const filters = this._filter.asOverpass()
let filter = ""
for (const filterOr of filters) {
diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts
index ae77b7b2cf..6b1acd4831 100644
--- a/Logic/SimpleMetaTagger.ts
+++ b/Logic/SimpleMetaTagger.ts
@@ -7,6 +7,8 @@ import {Utils} from "../Utils";
import opening_hours from "opening_hours";
import {UIElement} from "../UI/UIElement";
import Combine from "../UI/Base/Combine";
+import UpdateTagsFromOsmAPI from "./Actors/UpdateTagsFromOsmAPI";
+
export default class SimpleMetaTagger {
public readonly keys: string[];
@@ -52,6 +54,18 @@ export default class SimpleMetaTagger {
feature.area = sqMeters;
})
);
+
+ private static lngth = new SimpleMetaTagger(
+ ["_length", "_length:km"], "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter",
+ (feature => {
+ const l = GeoOperations.lengthInMeters(feature)
+ feature.properties["_length"] = "" + l
+ const km = Math.floor(l / 1000)
+ const kmRest = Math.round((l - km * 1000) / 100)
+ feature.properties["_length:km"] = "" + km+ "." + kmRest
+ })
+ )
+
private static country = new SimpleMetaTagger(
["_country"], "The country code of the property (with latlon2country)",
feature => {
@@ -64,7 +78,7 @@ export default class SimpleMetaTagger {
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
try {
feature.properties["_country"] = countries[0].trim().toLowerCase();
- const tagsSource = State.state.allElements.addOrGetElement(feature);
+ const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
tagsSource.ping();
} catch (e) {
console.warn(e)
@@ -76,8 +90,13 @@ export default class SimpleMetaTagger {
["_isOpen", "_isOpen:description"],
"If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
(feature => {
-
- const tagsSource = State.state.allElements.addOrGetElement(feature);
+ if(Utils.runningFromConsole){
+ // We are running from console, thus probably creating a cache
+ // isOpen is irrelevant
+ return
+ }
+
+ const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id);
tagsSource.addCallbackAndRun(tags => {
if (tags.opening_hours === undefined || tags._country === undefined) {
return;
@@ -294,6 +313,7 @@ export default class SimpleMetaTagger {
public static metatags = [
SimpleMetaTagger.latlon,
SimpleMetaTagger.surfaceArea,
+ SimpleMetaTagger.lngth,
SimpleMetaTagger.country,
SimpleMetaTagger.isOpen,
SimpleMetaTagger.carriageWayWidth,
@@ -303,7 +323,7 @@ export default class SimpleMetaTagger {
];
static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) {
- SimpleMetaTagger.coder.GetCountryCodeFor(lon, lat, callback)
+ SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
}
static HelpText(): UIElement {
@@ -317,7 +337,7 @@ export default class SimpleMetaTagger {
];
- for (const metatag of SimpleMetaTagger.metatags) {
+ for (const metatag of SimpleMetaTagger.metatags.concat(UpdateTagsFromOsmAPI.metaTagger)) {
subElements.push(
new Combine([
"", metatag.keys.join(", "), "
",
diff --git a/Logic/Tags/TagsFilter.ts b/Logic/Tags/TagsFilter.ts
index e34b052a60..1c6a5d32e9 100644
--- a/Logic/Tags/TagsFilter.ts
+++ b/Logic/Tags/TagsFilter.ts
@@ -14,7 +14,9 @@ export abstract class TagsFilter {
/**
* Converts the tagsFilter into a list of key-values that should be uploaded to OSM.
- * Throws an error if not applicable
+ * Throws an error if not applicable.
+ *
+ * Note: properties are the already existing tags-object. It is only used in the substituting tag
*/
abstract asChange(properties:any): {k: string, v:string}[]
diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts
index 8f604fdce2..0c92d314f5 100644
--- a/Logic/UIEventSource.ts
+++ b/Logic/UIEventSource.ts
@@ -21,6 +21,7 @@ export class UIEventSource {
for (let i = 0; i < 10; i++) {
console.log(copy[i].tag, copy[i]);
}
+ return UIEventSource.allSources;
}
return [];
}
diff --git a/Models/Constants.ts b/Models/Constants.ts
index 02aa7e8149..ffd796a687 100644
--- a/Models/Constants.ts
+++ b/Models/Constants.ts
@@ -2,20 +2,22 @@ import { Utils } from "../Utils";
export default class Constants {
- public static vNumber = "0.6.10";
+ public static vNumber = "0.6.11";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
addNewPointsUnlock: 0,
moreScreenUnlock: 1,
personalLayoutUnlock: 15,
+ historyLinkVisible: 20,
tagsVisibleAt: 25,
mapCompleteHelpUnlock: 50,
tagsVisibleAndWikiLinked: 30,
themeGeneratorReadOnlyUnlock: 50,
themeGeneratorFullUnlock: 500,
addNewPointWithUnreadMessagesUnlock: 500,
- minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19)
+ minZoomLevelToAddNewPoints: (Constants.isRetina() ? 18 : 19),
+
};
/**
* Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes.
diff --git a/README.md b/README.md
index cce053ce30..308293909c 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
> Let a thousand flowers bloom
-
-MapComplete attempts to be a webversion crossover of StreetComplete and MapContrib. It tries to be just as easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle infrastructure, ...)
+MapComplete is an OpenStreetMap viewer and editor. It shows map features on a certain topic, and allows to see, edit and add new features to the map.
+It can be seen as a webversion crossover of StreetComplete and MapContrib. It tries to be just as easy to use as StreetComplete, but it allows to focus on one single theme per instance (e.g. nature, bicycle infrastructure, ...)
The design goals of MapComplete are to be:
@@ -155,12 +155,10 @@ Geolocation is available on mobile only throught hte device's GPS location (so n
TODO: erase cookies of third party websites and API's
-# Attribution
+# Attribution and Copyright
-Data from OpenStreetMap
+The code is available under GPL; all map data comes from OpenStreetMap (both foreground and background maps).
Background layer selection: curated by https://github.com/osmlab/editor-layer-index
Icons are attributed in various 'license_info.json'-files and can be found in the app.
-
-
diff --git a/State.ts b/State.ts
index 91912f0a23..9da68bd13d 100644
--- a/State.ts
+++ b/State.ts
@@ -17,6 +17,8 @@ import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass";
import LayerConfig from "./Customizations/JSON/LayerConfig";
import TitleHandler from "./Logic/Actors/TitleHandler";
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
+import {Relation} from "./Logic/Osm/ExtractRelations";
+import UpdateTagsFromOsmAPI from "./Logic/Actors/UpdateTagsFromOsmAPI";
/**
* Contains the global state: a bunch of UI-event sources
@@ -76,6 +78,11 @@ export default class State {
*/
public readonly selectedElement = new UIEventSource(undefined, "Selected element")
+ /**
+ * Keeps track of relations: which way is part of which other way?
+ * Set by the overpass-updater; used in the metatagging
+ */
+ public readonly knownRelations = new UIEventSource