Merge branch 'develop'

This commit is contained in:
pietervdvn 2021-04-22 16:39:26 +02:00
commit aea55cf474
70 changed files with 3917 additions and 2656 deletions

View file

@ -102,7 +102,7 @@ export default class AllTranslationAssets {
getStartedNewAccount: new Translation( {"en":" or <a href='https://www.openstreetmap.org/user/new' target='_blank'>create a new account</a>","nl":" of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a> ","fr":" ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>enregistrez-vous</a>","es":" o <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea una nueva cuenta</a>","ca":" o <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea un nou compte</a>","gl":" ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea unha nova conta</a>","de":" oder <a href='https://www.openstreetmap.org/user/new' target='_blank'>ein neues Konto anlegen</a>"} ),
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":"<h3>Custom themes</h3>These are previously visited user-generated themes.","nl":"<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.","fr":"<h3>Thèmes personnalisés</h3>Vous avez déjà visité ces thèmes personnalisés.","gl":"<h3>Temas personalizados</h3>Estes son temas xerados por usuarios previamente visitados.","de":"<h3>Kundenspezifische Themen</h3>Dies sind zuvor besuchte benutzergenerierte Themen"} ),
aboutMapcomplete: new Translation( {"en":"<h3><b>About MapComplete</b></h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><p><b>Find out more</b><br/>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>* When embedded in a website, the iframe links to a full-screen MapComplete</li><li>* The full-screen version offers information about OpenStreetMap</li><li>* Viewing works without login, but editing requires an OSM login.</li><li>* If you are not logged in, you are asked to log in</li><li>* Once you answered a single question, you can add new points to the map</li><li>* After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a>.</p>","nl":"<h3><b>Over MapComplete</b></h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><p><b>Ontdek meer</b><br/>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>* Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>* De volledige versie heeft uitleg over OpenStreetMap</li><li>* Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>* Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>* Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>* Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a>.</p>","de":"<h3>Über MapComplete</h3><p>MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem <b>Einzelthema hinzuzufügen.</b></p><p>Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge <b>einfach und extrem benutzerfreundlich</b> 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.</p><p>Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer <b>den nächsten Schritt anzubieten</b>um mehr über OpenStreetMap zu erfahren:<ul><li>Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion</li><li>Die Vollbildversion bietet Informationen über OpenStreetMap</li><li>Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen</li><li>Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen</li><li>An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...</li></ul></p><p>Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>zum Quellcode</a> oder <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>zur Problemverfolgung</a>.</p>"} ),
aboutMapcomplete: new Translation( {"en":"<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a>.</p>","nl":"<h3>Over MapComplete</h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a>.</p>","de":"<h3>Über MapComplete</h3><p>MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem <b>Einzelthema hinzuzufügen.</b></p><p>Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge <b>einfach und extrem benutzerfreundlich</b> 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.</p><p>Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer <b>den nächsten Schritt anzubieten</b>um mehr über OpenStreetMap zu erfahren:<ul><li>Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion</li><li>Die Vollbildversion bietet Informationen über OpenStreetMap</li><li>Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen</li><li>Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen</li><li>An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...</li></ul></p><p>Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>zum Quellcode</a> oder <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>zur Problemverfolgung</a>.</p>"} ),
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"} ),

View file

@ -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;
}
}

View file

@ -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: "<custom overpass tags>"} 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:
* <div style="background: white; display: block">{name}</div>
*
* 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 ;

View file

@ -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 = "<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
const custom = [];
for (const layer of this.layers) {
custom.push(...layer.CustomCodeSnippets().map(code => code+"<br />"))
custom.push(...layer.CustomCodeSnippets().map(code => code + "<br />"))
}
if (custom.length === 0) {
return custom;
@ -184,8 +189,8 @@ export default class LayoutConfig {
custom.splice(0, 0, msg);
return custom;
}
public ExtractImages() : Set<string>{
public ExtractImages(): Set<string> {
const icons = new Set<string>()
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<string, string>()
// 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")
}
}

View file

@ -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;
/**

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields();
public static SharedIcons : Map<string, TagRenderingConfig> = SharedTagRenderings.generatedSharedFields(true);
private static generatedSharedFields(iconsOnly = false) {
const dict = {}
private static generatedSharedFields(iconsOnly = false) : Map<string, TagRenderingConfig>{
const dict = new Map<string, TagRenderingConfig>();
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)
}

View file

@ -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
* 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(';')\`

View file

@ -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"
}

View file

@ -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

View file

@ -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")

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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<string>, 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<any>(tags))
} else {
// We merge
console.warn("merging by OSM API UPDATE")
allElements.addOrGetById(id, tags)
}
})
})
}
}

View file

@ -5,14 +5,14 @@ import {UIEventSource} from "./UIEventSource";
export class ElementStorage {
private _elements = [];
private _elements = new Map<string, UIEventSource<any>>();
constructor() {
}
addElementById(id: string, eventSource: UIEventSource<any>) {
this._elements[id] = eventSource;
this._elements.set(id, eventSource);
}
/**
@ -23,41 +23,54 @@ export class ElementStorage {
*/
addOrGetElement(feature: any): UIEventSource<any> {
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<any>(feature.properties, "tags of " + feature.properties.id);
this._elements[feature.properties.id] = eventSource;
addOrGetById(elementId: string, newProperties: any): UIEventSource<any> {
if (!this._elements.has(elementId)) {
const eventSource = new UIEventSource<any>(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<any> {
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);
}
}

View file

@ -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 <b>feat</b> 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 <b>{ feat: GeoJSONFeature, overlap: number}</b>",
["...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 <b>feat</b> 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 <b>feat</b> 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 <b>feat</b> 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<string, any[]>, feat: any) => any;
private readonly _f: (params: {featuresPerLayer: Map<string, any[]>, relations: {role: string, relation: Relation}[]}, feat: any) => any;
constructor(name: string, doc: string, args: string[], f: ((featuresPerLayer: Map<string, any[]>, feat: any) => any)) {
constructor(name: string, doc: string, args: string[], f: ((params: {featuresPerLayer: Map<string, any[]>, relations: {role: string, relation: Relation}[]}, feat: any) => any)) {
this._name = name;
this._doc = doc;
this._args = args;
@ -127,9 +138,9 @@ Some advanced functions are available on <b>feat</b> as well:
}
public static FullPatchFeature(featuresPerLayer: Map<string, any[]>, feature) {
public static FullPatchFeature(featuresPerLayer: Map<string, any[]>,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 <b>feat</b> as well:
]);
}
public PatchFeature(featuresPerLayer: Map<string, any[]>, feature: any) {
feature[this._name] = this._f(featuresPerLayer, feature);
public PatchFeature(featuresPerLayer: Map<string, any[]>, relations: {role: string, relation: Relation}[], feature: any) {
feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature);
}
}

View file

@ -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 => {

View file

@ -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<Loc>) {
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,

View file

@ -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<string> = new Set<string>()
constructor(locationControl: UIEventSource<Loc>,
flayer: { isDisplayed: UIEventSource<boolean>, 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<string>();
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<string>();
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<string>) => {
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<boolean>, layerDef: LayerConfig }[], locationControl: UIEventSource<Loc>): GeoJsonSource[] {
const flayersPerSource = new Map<string, { isDisplayed: UIEventSource<boolean>, 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<boolean>(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))
}
}

View file

@ -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;
}

View file

@ -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);
});
}

View file

@ -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)
}
}
})
}
}

View file

@ -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
}
}

View file

@ -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<string, any[]>,
memberships: Map<string, { role: string, relation: Relation }[]>
}
/**
* 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<string, { role: string, relation: Relation }[]>, 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<string, ((featursPerLayer: Map<string, any[]>, feature: any) => void)>();
const layerFuncs = new Map<string, ((params: Params, feature: any) => 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<string, any[]>, 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<string, any[]>, 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<string, any[]>, 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)

View file

@ -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});
}
}

View file

@ -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<string, { role: string, relation: Relation }[]> {
const memberships = new Map<string, { role: string, relation: Relation }[]>()
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
}
}

View file

@ -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 += ' <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\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 += ' <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\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[];

View file

@ -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) {

View file

@ -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([
"<h3>", metatag.keys.join(", "), "</h3>",

View file

@ -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}[]

View file

@ -21,6 +21,7 @@ export class UIEventSource<T> {
for (let i = 0; i < 10; i++) {
console.log(copy[i].tag, copy[i]);
}
return UIEventSource.allSources;
}
return [];
}

View file

@ -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.

View file

@ -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.

View file

@ -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<any>(undefined, "Selected element")
/**
* Keeps track of relations: which way is part of which other way?
* Set by the overpass-updater; used in the metatagging
*/
public readonly knownRelations = new UIEventSource<Map<string, {role: string, relation: Relation}[]>>(undefined, "Relation memberships")
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>;
@ -246,6 +253,7 @@ export default class State {
new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements);
new UpdateTagsFromOsmAPI(this.selectedElement.map(el => el?.properties?.id), this.allElements)
}

View file

@ -28,11 +28,11 @@ export default class AttributionPanel extends Combine {
"<br/>",
new Attribution(undefined, undefined, State.state.layoutToUse, undefined),
"<br/>",
Translations.t.general.attribution.iconAttribution.title.Clone().SetClass("font-bold pt-12 pb-3"),
"<h3>",Translations.t.general.attribution.iconAttribution.title.Clone().SetClass("pt-6 pb-3"),"</h3>",
...Utils.NoNull(Array.from(layoutToUse.data.ExtractImages()))
.map(AttributionPanel.IconAttribution)
]);
this.SetClass("flex flex-col")
this.SetClass("flex flex-col link-underline")
}
private static IconAttribution(iconPath: string) {

View file

@ -47,7 +47,7 @@ export default class FullWelcomePaneWithTabs extends UIElement {
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
{
header: Svg.osm_logo_img,
content: Translations.t.general.openStreetMapIntro as UIElement
content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline") as UIElement
},
]
@ -71,7 +71,7 @@ export default class FullWelcomePaneWithTabs extends UIElement {
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
return ""
}
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).Render();
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).SetClass("link-underline").Render();
}, [Locale.language]))
}
);

View file

@ -47,9 +47,12 @@ export default class LayerSelection extends UIElement {
.SetClass("single-layer-selection-toggle")
.SetStyle("opacity:0.2;");
const name = Translations.WT(layer.layerDef.name).Clone()
.SetStyle("font-size:large;margin-left: 0.5em;");
const name = Translations.WT(layer.layerDef.name)?.Clone()
?.SetStyle("font-size:large;margin-left: 0.5em;");
if((name ?? "") === ""){
continue
}
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
if (location.zoom < layer.layerDef.minzoom) {

View file

@ -31,7 +31,7 @@ export default class ShareScreen extends UIElement {
const optionCheckboxes: UIElement[] = []
const optionParts: (UIEventSource<string>)[] = [];
this.SetClass("link-underline")
function check() {
return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
}

View file

@ -41,6 +41,7 @@ export default class ThemeIntroductionPanel extends UIElement {
}
)
)
this.SetClass("link-underline")
}
InnerRender(): string {

View file

@ -206,7 +206,7 @@ export default class OpeningHoursVisualization extends UIElement {
}
return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed").Render()
}
const moment = `${opensAtDate.getDay()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}`
const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}`
return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed").Render()
}

View file

@ -189,7 +189,11 @@ export class OH {
if(spl.length != 2){
return null;
}
return {hours: Number(spl[0].trim()), minutes: Number(spl[1].trim())};
const hm = {hours: Number(spl[0].trim()), minutes: Number(spl[1].trim())};
if(isNaN(hm.hours) || isNaN(hm.minutes) ){
return null;
}
return hm;
}
public static parseHHMMRange(hhmmhhmm: string): {

View file

@ -9,6 +9,8 @@ import State from "../../State";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import {Tag} from "../../Logic/Tags/Tag";
import Constants from "../../Models/Constants";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
export default class FeatureInfoBox extends ScrollableFullScreen {
@ -19,7 +21,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig),
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
tags.data.id);
if (layerConfig === undefined) {
throw "Undefined layerconfig";
}
@ -49,6 +51,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig): UIElement {
let questionBox: UIElement = undefined;
if (State.state.featureSwitchUserbadge.data) {
questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
}
@ -60,15 +63,21 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
questionBoxIsUsed = true;
return questionBox;
}
return new EditableTagRendering(tags, tr);
return new EditableTagRendering(tags, tr);
});
if (!questionBoxIsUsed) {
renderings.push(questionBox);
}
if(State.state.featureSwitchIsDebugging.data){
const config: TagRenderingConfig = new TagRenderingConfig({render:"{all_tags()}"}, new Tag("id",""), "");
renderings.push(new TagRenderingAnswer(tags,config ))
if (State.state.osmConnection.userDetails.data.csCount >= Constants.userJourney.historyLinkVisible ||
State.state.featureSwitchIsDebugging.data == true ||
State.state.featureSwitchIsTesting.data == true) {
renderings.push(new TagRenderingAnswer( tags, SharedTagRenderings.SharedTagRendering.get("last_edit")))
}
if (State.state.featureSwitchIsDebugging.data) {
const config: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, new Tag("id", ""), "");
renderings.push(new TagRenderingAnswer(tags, config))
}
return new Combine(renderings).SetClass("block")

View file

@ -26,7 +26,7 @@ export default class TagRenderingAnswer extends UIElement {
if (configuration === undefined) {
throw "Trying to generate a tagRenderingAnswer without configuration..."
}
this.SetClass("flex items-center flex-row text-lg")
this.SetClass("flex items-center flex-row text-lg link-underline")
this.SetStyle("word-wrap: anywhere;");
}
@ -46,7 +46,7 @@ export default class TagRenderingAnswer extends UIElement {
if (this._configuration.multiAnswer) {
let freeformKeyUsed = this._configuration.freeform?.key === undefined; // If it is undefined, it is "used" already, or at least we don't have to check for it anymore
const applicableThens: Translation[] = Utils.NoNull(this._configuration.mappings.map(mapping => {
const applicableThens: Translation[] = Utils.NoNull(this._configuration.mappings?.map(mapping => {
if (mapping.if === undefined) {
return mapping.then;
}
@ -59,7 +59,7 @@ export default class TagRenderingAnswer extends UIElement {
return mapping.then;
}
return undefined;
}))
}) ?? [])
if (!freeformKeyUsed
&& tags[this._configuration.freeform.key] !== undefined) {

View file

@ -88,7 +88,7 @@ export default class ShowDataLayer {
marker.openPopup();
const popup = marker.getPopup();
const tags = State.state.allElements.addOrGetElement(selected);
const tags = State.state.allElements.getEventSourceById(selected.properties.id);
const layer: LayerConfig = this._layerDict[selected._matching_layer_id];
const infoBox = FeatureInfoBox.construct(tags, layer);
@ -118,7 +118,7 @@ export default class ShowDataLayer {
// We have to convert them to the appropriate icon
// Click handling is done in the next step
const tagSource = State.state.allElements.addOrGetElement(feature)
const tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
if (layer === undefined) {

View file

@ -73,10 +73,18 @@ export class Translation extends UIElement {
let rtext: string = "";
if (typeof (el) === "string") {
rtext = el;
} else if(typeof(el) === "number") {
// HUH? Where did that number come from?
} else if (typeof (el) === "number") {
// HUH? Where did that number come from? It might be a version number or something calculated
rtext = "" + el;
}else {
} else if (el["toISOString"] != undefined) {
// This is a date, probably the timestamp of the object
// @ts-ignore
const date: Date = el;
rtext = date.toLocaleString();
} else if (el.InnerRender === undefined) {
console.error("InnerREnder is not defined", el);
throw "Hmmm, el.InnerRender is not defined?"
} else {
Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
rtext = el.InnerRender();

View file

@ -156,8 +156,6 @@ export class Utils {
}
static Merge(source: any, target: any) {
target = JSON.parse(JSON.stringify(target));
source = JSON.parse(JSON.stringify(source));
for (const key in source) {
const sourceV = source[key];
const targetV = target[key]
@ -203,6 +201,26 @@ export class Utils {
static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } {
return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z}
}
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1:number, lon1: number) : TileRange{
const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel)
const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel)
const xstart = Math.min(t0.x, t1.x)
const xend = Math.max(t0.x, t1.x)
const ystart = Math.min(t0.y, t1.y)
const yend = Math.max(t0.y, t1.y)
const total = (1 + xend - xstart) * (1 + yend - ystart)
return {
xstart: xstart,
xend: xend,
ystart: ystart,
yend: yend,
total: total,
zoomlevel: zoomlevel
}
}
public static MinifyJSON(stringified: string): string {
stringified = stringified.replace(/\|/g, "||");
@ -257,3 +275,13 @@ export class Utils {
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
}
}
export interface TileRange{
xstart: number,
ystart: number,
xend: number,
yend: number,
total: number,
zoomlevel: number
}

View file

@ -159,32 +159,34 @@
"width": {
"render": "4"
},
"dashArray": "2 10 6 10",
"color": {
"render": "#bb2",
"dashArray": {
"render": "",
"mappings": [
{
"if": "highway=cycleway",
"then": "#00c"
"then": ""
},
{
"if": "highway=path",
"then": "#bb2"
"then": "10 3"
},
{
"if": "highway=footway",
"then": "#c30"
"then": "10 10"
},
{
"if": "highway=pedestrian",
"then": "#3c3"
"then": "10 10"
},
{
"if": "highway=living_street",
"then": "#ccc"
"then": "10 5 3 5"
}
]
},
"color": {
"render": "#eaba2a"
},
"presets": [
]
}

View file

@ -250,7 +250,8 @@
"en": "Always accessible"
}
}
]
],
"condition": "access~*"
},
"questions",
{

View file

@ -77,5 +77,10 @@
"key": "opening_hours",
"type": "opening_hours"
}
},
"last_edit": {
"#": "Gives some metainfo about the last edit and who did edit it - rendering only",
"#condition": "_last_edit:contributor~*",
"render": "<div class='subtle' style='font-size: small'><a href='https://www.openStreetMap.org/changeset/{_last_edit:changeset}' target='_blank'>Last edited on {_last_edit:timestamp}</a> by <a href='https://www.openStreetMap.org/user/{_last_edit:contributor}' target='_blank'>{_last_edit:contributor}</a></div>"
}
}

View file

@ -1,77 +1,91 @@
[
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_gym.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_icon.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_no_rope.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_rope.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_route.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "climbing_unknown.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]
},
{
"authors": [
"Polarbear w",
"Christian Neumann"
],
"path": "club.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Climbing_icon_no_rope.svg",
"https://utopicode.de/",
"https://github.com/chrneumann/MapComplete"
]

View file

@ -4,7 +4,7 @@
"Klimaan VZW"
],
"path": "bloei.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -14,7 +14,7 @@
"Klimaan VZW"
],
"path": "bodembedekker.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -24,7 +24,7 @@
"Klimaan VZW"
],
"path": "eetbaar.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -34,7 +34,7 @@
"Klimaan VZW"
],
"path": "gevelton.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -44,7 +44,7 @@
"Klimaan VZW"
],
"path": "geveltuin.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -54,7 +54,7 @@
"Klimaan VZW"
],
"path": "halfzon.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -64,7 +64,7 @@
"Klimaan VZW"
],
"path": "klimplant.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -74,7 +74,7 @@
"Klimaan VZW"
],
"path": "schaduw.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -84,7 +84,7 @@
"Klimaan VZW"
],
"path": "struik.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]
@ -94,7 +94,7 @@
"Klimaan VZW"
],
"path": "zon.svg",
"license": "CC-BY-SA 3.0",
"license": "CC0",
"sources": [
"https://klimaan.be/"
]

View file

@ -224,6 +224,35 @@
}
]
},
{
"question": {
"nl": "Als je je eigen container (bv. kookpot of kleine potjes voor saus) meeneemt, gebruikt de frituur deze dan om je bestelling in te doen?",
"en": "If you bring your own container (such as a cooking pot and small pots), is it used to package your order?<br/>"
},
"mappings": [
{
"if": "bulk_purchase=yes",
"then": {
"nl": "Je mag je <b>eigen containers</b> meenemen om je bestelling in mee te nemen en zo minder afval te maken",
"en": "You can bring <b>your own containers</b> to get your order, saving on single-use packaging material and thus waste"
}
},
{
"if": "bulk_purchase=no",
"then": {
"nl": "Je mag <b>geen</b> eigen containers meenemen om je bestelling in mee te nemen",
"en": "Bringing your own container is <b>not allowed</b>"
}
},
{
"if": "bulk_purchase=only",
"then": {
"nl": "Je <b>moet</b> je eigen containers meenemen om je bestelling in mee te nemen.",
"en": "You <b>must</b> bring your own container to order here."
}
}
]
},
"questions",
"reviews"
],

View file

@ -19,6 +19,7 @@
"startLon": 3.231,
"startZoom": 14,
"widenFactor": 0.05,
"cacheTimeout": 3600,
"socialImage": "",
"layers": [
{

View file

@ -195,7 +195,8 @@
}
}
]
},"images"
},
"images"
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
@ -392,7 +393,7 @@
},
{
"question": {
"en": "How is the station operator classified??"
"en": "How is the station operator classified?"
},
"render": {
"en": "The operator is a(n) {operator:type} entity."
@ -442,7 +443,8 @@
}
}
]
},"images"
},
"images"
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
@ -540,7 +542,7 @@
},
{
"question": {
"en": "How is the station operator classified??"
"en": "How is the station operator classified?"
},
"render": {
"en": "The operator is a(n) {operator:type} entity."
@ -590,7 +592,8 @@
}
}
]
},"images"
},
"images"
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {

View file

@ -15,7 +15,7 @@
"maintainer": "MapComplete",
"icon": "./assets/layers/play_forest/icon.svg",
"hideFromOverview": true,
"lockLocation": false,
"lockLocation": true,
"version": "0",
"startLat": 51.17174,
"startLon": 4.449462,
@ -24,13 +24,176 @@
"socialImage": "",
"defaultBackgroundId": "CartoDB.Positron",
"layers": [
"play_forest",
"playground",
"sport_pitch",
"slow_roads",
"grass_in_parks",
"village_green"
{
"builtin": "play_forest",
"override": {
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
}
}
},
{
"builtin": "playground",
"override": {
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
}
}
},
{
"builtin": "sport_pitch",
"override": {
"minzoom": 15,
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
}
}
},
{
"builtin": "slow_roads",
"override": {
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
},
"calculatedTags": [
"_part_of_walking_routes=feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\").join(', ')"
]
}
},
{
"builtin": "grass_in_parks",
"override": {
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
}
}
},
{
"builtin": "village_green",
"override": {
"source": {
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
}
}
},
{
"id": "walking_routes",
"name": {
"nl": "Wandelroutes van provincie Antwerpen"
},
"description": "Walking routes by 'provincie Antwerpen'",
"source": {
"osmTags": {
"and": [
"type=route",
"route=foot",
"operator=provincie Antwerpen"
]
},
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14
},
"title": {
"render": "Wandeling <i>{name}</i>",
"mappings": [
{
"if": "name~.*wandeling.*",
"then": "{name}"
}
]
},
"tagRenderings": [
{
"render": {
"nl": "Deze wandeling is <b>{_length:km}km</b> lang"
}
},
{
"mappings": [
{
"if": "route=iwn",
"then": {
"nl": "Dit is een internationale wandelroute"
}
},
{
"if": "route=nwn",
"then": {
"nl": "Dit is een nationale wandelroute"
}
},
{
"if": "route=rwn",
"then": {
"nl": "Dit is een regionale wandelroute"
}
},
{
"if": "route=lwn",
"then": {
"nl": "Dit is een lokale wandelroute"
}
}
]
},
{
"render": {
"nl": "<h3>Korte beschrijving:</h3>{description}"
},
"question": "Geef een korte beschrijving van de wandeling (max 255 tekens)",
"freeform": {
"key": "description",
"type": "text"
}
},
{
"question": {
"nl": "Wie beheert deze wandeling en plaatst dus de signalisatiebordjes?"
},
"render": "Signalisatie geplaatst door {operator}",
"freeform": {
"key": "operator"
}
},
{
"question": {
"nl": "Naar wie kan men emailen bij problemen rond signalisatie?"
},
"render": {
"nl": "Bij problemen met signalisatie kan men emailen naar <a href='mailto:{operator:email}'>{operator:email}</a>"
},
"freeform": {
"key": "operator:email",
"type": "email"
}
},
"questions",
"reviews"
],
"color": {
"render": "#6d6",
"mappings": [
{
"if": "color~*",
"then": "{color}"
}
]
},
"width": {
"render": "3"
}
}
],
"roamingRenderings": []
"roamingRenderings": [
{
"render": "Maakt deel uit van {_part_of_walking_routes}",
"condition": "_part_of_walking_routes~*"
}
]
}

View file

@ -782,8 +782,8 @@
"de": "<h3>Kundenspezifische Themen</h3>Dies sind zuvor besuchte benutzergenerierte Themen"
},
"aboutMapcomplete": {
"en": "<h3><b>About MapComplete</b></h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><p><b>Find out more</b><br/>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>* When embedded in a website, the iframe links to a full-screen MapComplete</li><li>* The full-screen version offers information about OpenStreetMap</li><li>* Viewing works without login, but editing requires an OSM login.</li><li>* If you are not logged in, you are asked to log in</li><li>* Once you answered a single question, you can add new points to the map</li><li>* After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a>.</p>",
"nl": "<h3><b>Over MapComplete</b></h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><p><b>Ontdek meer</b><br/>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>* Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>* De volledige versie heeft uitleg over OpenStreetMap</li><li>* Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>* Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>* Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>* Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a>.</p>",
"en": "<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >OsmCha</a>.</p>",
"nl": "<h3>Over MapComplete</h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' target='_blank' >op OsmCha</a>.</p>",
"de": "<h3>Über MapComplete</h3><p>MapComplete ist ein OpenStreetMap-Editor, der jedem helfen soll, auf einfache Weise Informationen zu einem <b>Einzelthema hinzuzufügen.</b></p><p>Nur Merkmale, die für ein einzelnes Thema relevant sind, werden mit einigen vordefinierten Fragen gezeigt, um die Dinge <b>einfach und extrem benutzerfreundlich</b> 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.</p><p>Ein weiterer wichtiger Teil von MapComplete ist jedoch, immer <b>den nächsten Schritt anzubieten</b>um mehr über OpenStreetMap zu erfahren:<ul><li>Ein iframe ohne UI-Elemente verlinkt zu einer Vollbildversion</li><li>Die Vollbildversion bietet Informationen über OpenStreetMap</li><li>Wenn Sie nicht eingeloggt sind, werden Sie gebeten, sich einzuloggen</li><li>Wenn Sie eine einzige Frage beantwortet haben, dürfen Sie Punkte hinzufügen</li><li>An einem bestimmten Punkt erscheinen die tatsächlich hinzugefügten Tags, die später mit dem Wiki verlinkt werden...</li></ul></p><p>Fällt Ihnen ein Problem mit MapComplete auf? Haben Sie einen Feature-Wunsch? Wollen Sie beim Übersetzen helfen? Gehen Sie <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>zum Quellcode</a> oder <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>zur Problemverfolgung</a>.</p>"
},
"backgroundMap": {

View file

@ -0,0 +1,58 @@
[
{
"path": "hydrant_pillar.svg",
"license": "",
"authors": [],
"sources": [
"https://mapcomplete.braindeaddev.com/assets/layers/fire/hydrant_pillar.svg"
]
},
{
"path": "hydrant_unknown.svg",
"license": "",
"authors": [],
"sources": [
"https://mapcomplete.braindeaddev.com/assets/layers/fire/hydrant_unknown.svg"
]
},
{
"path": "hydrant_underground.svg",
"license": "",
"authors": [],
"sources": [
"https://mapcomplete.braindeaddev.com/assets/layers/fire/hydrant_underground.svg"
]
},
{
"path": "Hydrant_02.svg",
"license": "",
"authors": [],
"sources": [
"https://upload.wikimedia.org/wikipedia/commons/6/66/Hydrant_02.svg"
]
},
{
"path": "MUTCD_RS-090.svg",
"license": "",
"authors": [],
"sources": [
"https://upload.wikimedia.org/wikipedia/commons/e/e8/MUTCD_RS-090.svg"
]
},
{
"path": "British_Columbia_W-318-L.svg",
"license": "",
"authors": [],
"sources": [
"https://upload.wikimedia.org/wikipedia/commons/8/84/British_Columbia_W-318-L.svg"
]
},
{
"path": "Emojione_1F6A8.svg",
"license": "",
"authors": [],
"sources": [
"https://upload.wikimedia.org/wikipedia/commons/1/1e/Emojione_1F6A8.svg"
]
}
]

View file

@ -101,6 +101,7 @@ svg, img {
box-sizing: content-box;
width: 100%;
height: 100%;
display: unset;
}
@ -108,6 +109,46 @@ a {
color: var(--foreground-color);
}
.link-underline a {
text-decoration: underline 1px #0078a855;;
color: #0078A8;
}
li {
margin-left: 0.5em;
padding-left: 0.2em;
margin-top: 0.1em;
}
h2 {
font-size: large;
margin-top: 0.5em;
margin-bottom: 0.3em;
font-weight: bold;
}
h3 {
font-size: larger;
margin-top: 0.6em;
margin-bottom: 0;
font-weight: bold;
}
h3 {
font-size: larger;
margin-top: 0.6em;
margin-bottom: 0;
font-weight: bolder;
}
p {
padding-top: 0.1em;
}
li::marker {
content: "•"
}
.subtle-background {
background: var(--subtle-detail-color);
color: var(--subtle-detail-color-contrast);
@ -242,6 +283,12 @@ a {
color: #999;
}
.link-underline .subtle a {
color: var(--foreground-color);
text-decoration: underline 1px #7193bb88;
color: #7193bb;
}
.bold {
font-weight: bold;
}

View file

@ -67,7 +67,9 @@ defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout, "The
let layoutToUse: LayoutConfig = AllKnownLayouts.allKnownLayouts.get(defaultLayout.toLowerCase());
// WOrkaround/legacy
const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false", "If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: \n\n- The hash of the URL contains a base64-encoded .json-file containing the theme definition\n- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator\n- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme");
// Workaround/legacy to keep the old paramters working as I renamed some of them
if(layoutToUse?.id === "cyclofix"){
QueryParameters.GetQueryParameter("layer-bike_shops", "true", "Legacy - keep De Fietsambassade working").syncWith(
QueryParameters.GetQueryParameter("layer-bike_shop","true","Legacy - keep De Fietsambassade working")
@ -78,7 +80,6 @@ if(layoutToUse?.id === "cyclofix"){
}
const userLayoutParam = QueryParameters.GetQueryParameter("userlayout", "false");
const layoutFromBase64 = decodeURIComponent(userLayoutParam.data);
document.getElementById('centermessage').innerText = '';
document.getElementById("decoration-desktop").remove();
@ -102,7 +103,7 @@ if (layoutFromBase64.startsWith("http")) {
}
// Overwrite the id to the wiki:value
parsed.id = link;
const layout = new LayoutConfig(parsed, false);
const layout = new LayoutConfig(parsed, false).patchImages(link, data);
InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data));
} catch (e) {
new FixedUiElement(`<a href="${link}">${link}</a> is invalid:<br/>${e}<br/> <a href='https://${window.location.host}/'>Go back</a>`)

4691
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,19 +8,21 @@
"main": "index.js",
"scripts": {
"increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096",
"start": "ts-node scripts/generateLayerOverview.ts && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts && ts-node test/ImageAttribution.spec.ts",
"start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts && ts-node test/ImageAttribution.spec.ts && ts-node test/Theme.spec.ts",
"generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json",
"generate:images": "ts-node scripts/generateIncludedImages.ts",
"generate:translations": "ts-node scripts/generateTranslations.ts",
"generate:layouts": "ts-node scripts/generateLayouts.ts",
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
"generate:cache:speelplekken": "ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.37 51.11 4.51",
"generate:layeroverview": "ts-node scripts/generateLayerOverview.ts --no-fail",
"generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"validate:layeroverview": "ts-node scripts/generateLayerOverview.ts --report",
"validate:licenses": "ts-node scripts/generateLicenseInfo.ts --report",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"generate": "mkdir -p ./assets/generated && echo '{\"layers\":[], \"themes\":[]}' > ./assets/generated/known_layers_and_themes.json && npm run generate:images && npm run generate:translations && npm run generate:licenses && npm run generate:licenses && npm run generate:layeroverview",
"reset:layeroverview": "echo '{\"layers\":[], \"themes\":[]}' > ./assets/generated/known_layers_and_themes.json",
"generate": "mkdir -p ./assets/generated && npm run reset:layeroverview && npm run generate:images && npm run generate:translations && npm run generate:licenses && npm run generate:licenses && npm run generate:layeroverview",
"build": "rm -rf dist/ && npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"prepare-deploy": "npm run generate && npm run test && npm run generate:editor-layer-index && npm run generate:layouts && npm run generate:layeroverview && npm run build && rm -rf .cache && npm run generate:docs",
"deploy:staging": "npm run prepare-deploy && rm -rf /home/pietervdvn/git/pietervdvn.github.io/Staging/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/Staging/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean",
@ -38,7 +40,11 @@
"dependencies": {
"@babel/preset-env": "7.13.8",
"@tailwindcss/postcss7-compat": "^2.0.2",
"@turf/buffer": "^6.3.0",
"@turf/collect": "^6.3.0",
"@turf/distance": "^6.3.0",
"@turf/length": "^6.3.0",
"@turf/turf": "^6.3.0",
"@types/jquery": "^3.5.5",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0",

View file

@ -1,4 +1,5 @@
import {lstatSync, readdirSync} from "fs";
import * as https from "https";
export default class ScriptUtils {
public static readDirRecSync(path): string[] {
@ -16,5 +17,30 @@ export default class ScriptUtils {
}
return result;
}
public static DownloadJSON(url, continuation : (parts : string []) => void){
https.get(url, (res) => {
console.log("Got response!")
const parts : string[] = []
res.setEncoding('utf8');
res.on('data', function (chunk) {
// @ts-ignore
parts.push(chunk)
});
res.addListener('end', function () {
continuation(parts)
});
})
}
public static sleep(ms) {
return new Promise((resolve) => {
console.debug("Sleeping for", ms)
setTimeout(resolve, ms);
});
}
}

70
scripts/fixTheme.ts Normal file
View file

@ -0,0 +1,70 @@
/*
* This script attempt to automatically fix some basic issues when a theme from the custom generator is loaded
*/
import {Utils} from "../Utils"
Utils.runningFromConsole = true;
import {readFileSync, writeFileSync} from "fs";
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
import {Layer} from "leaflet";
import LayerConfig from "../Customizations/JSON/LayerConfig";
import SmallLicense from "../Models/smallLicense";
if(process.argv.length == 2){
console.log("USAGE: ts-node scripts/fixTheme <path to theme>")
throw "No path specified"
}
const path = process.argv[2]
const dir = path.substring(0, path.lastIndexOf("/"))
console.log("Fixing up ", path)
const themeConfigJson : LayoutConfigJson = JSON.parse(readFileSync(path, "UTF8"))
const linuxHints = []
const licenses : SmallLicense[] = []
const replacements: {source: string, destination: string}[] = []
for (const layerConfigJson of themeConfigJson.layers) {
if(typeof (layerConfigJson) === "string"){
continue;
}
if(layerConfigJson["overpassTags"] !== undefined){
const tags = layerConfigJson["overpassTags"];
layerConfigJson["overpassTags"] = undefined;
layerConfigJson["source"] = { osmTags : tags}
}
// @ts-ignore
const layerConfig = new LayerConfig(layerConfigJson, true)
const images : string[] = Array.from(layerConfig.ExtractImages())
const remoteImages = images.filter(img => img.startsWith("http"))
for (const remoteImage of remoteImages) {
linuxHints.push("wget " + remoteImage)
const imgPath = remoteImage.substring(remoteImage.lastIndexOf("/") + 1)
licenses.push({
path: imgPath,
license: "",
authors: [],
sources: [remoteImage]
})
replacements.push({source: remoteImage, destination: `${dir}/${imgPath}`})
}
}
let fixedThemeJson = JSON.stringify(themeConfigJson, null , " ")
for (const replacement of replacements) {
fixedThemeJson = fixedThemeJson.replace(new RegExp(replacement.source, "g"), replacement.destination)
}
const fixScriptPath = dir + "/fix_script_"+path.replace(/\//g,"_")+".sh"
writeFileSync(dir + "/generated.license_info.json", JSON.stringify(licenses, null, " "))
writeFileSync(fixScriptPath, linuxHints.join("\n"))
writeFileSync(path+".autofixed.json", fixedThemeJson)
console.log(`IMPORTANT:
1) run ${fixScriptPath}
2) Copy generated.license_info.json over into license_info.json and add the missing attributions and authors
3) Verify ${path}.autofixed.json as theme, and rename it to ${path}
4) Delete the fix script and other unneeded files`)

225
scripts/generateCache.ts Normal file
View file

@ -0,0 +1,225 @@
/**
* Generates a collection of geojson files based on an overpass query for a given theme
*/
import {TileRange, Utils} from "../Utils";
Utils.runningFromConsole = true
import {Overpass} from "../Logic/Osm/Overpass";
import {writeFileSync, existsSync, readFileSync} from "fs";
import {TagsFilter} from "../Logic/Tags/TagsFilter";
import {Or} from "../Logic/Tags/Or";
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import ScriptUtils from "./ScriptUtils";
import ExtractRelations from "../Logic/Osm/ExtractRelations";
import * as OsmToGeoJson from "osmtogeojson";
import {Script} from "vm";
import MetaTagging from "../Logic/MetaTagging";
import State from "../State";
import {createEvalAwarePartialHost} from "ts-node/dist/repl";
import {fail} from "assert";
function createOverpassObject(theme: LayoutConfig) {
let filters: TagsFilter[] = [];
let extraScripts: string[] = [];
for (const layer of theme.layers) {
if (typeof (layer) === "string") {
throw "A layer was not expanded!"
}
if (layer.doNotDownload) {
continue;
}
if (layer.source.geojsonSource !== undefined) {
// We download these anyway - we are building the cache after all!
//continue;
}
// Check if data for this layer has already been loaded
if (layer.source.overpassScript !== undefined) {
extraScripts.push(layer.source.overpassScript)
} else {
filters.push(layer.source.osmTags);
}
}
filters = Utils.NoNull(filters)
extraScripts = Utils.NoNull(extraScripts)
if (filters.length + extraScripts.length === 0) {
throw "Nothing to download! The theme doesn't declare anything to download"
}
return new Overpass(new Or(filters), extraScripts);
}
function saveResponse(chunks: string[], targetDir: string): boolean {
const contents = chunks.join("")
if (contents.startsWith("<?xml")) {
// THis is an error message
console.error("Failed to create ", targetDir, "probably over quota: ", contents)
return false;
}
writeFileSync(targetDir, contents)
return true
}
function rawJsonName(targetDir: string, x: number, y: number, z: number): string {
return targetDir + "_" + z + "_" + x + "_" + y + ".json"
}
function geoJsonName(targetDir: string, x: number, y: number, z: number): string {
return targetDir + "_" + z + "_" + x + "_" + y + ".geojson"
}
function metaJsonName(targetDir: string, x: number, y: number, z: number): string {
return targetDir + "_" + z + "_" + x + "_" + y + ".meta.json"
}
async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/* : {failed: number, skipped :number} */ {
let downloaded = 0
let failed = 0
let skipped = 0
for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) {
downloaded++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
if (existsSync(filename)) {
console.log("Already exists: ", filename)
skipped++
continue;
}
console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total, "failed: ", failed, "skipped: ", skipped)
const boundsArr = Utils.tile_bounds(r.zoomlevel, x, y)
const bounds = {
north: Math.max(boundsArr[0][0], boundsArr[1][0]),
south: Math.min(boundsArr[0][0], boundsArr[1][0]),
east: Math.max(boundsArr[0][1], boundsArr[1][1]),
west: Math.min(boundsArr[0][1], boundsArr[1][1])
}
const url = overpass.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]")
let gotResponse = false
let success = false;
ScriptUtils.DownloadJSON(url,
chunks => {
gotResponse = true;
success = saveResponse(chunks, filename)
})
while (!gotResponse) {
await ScriptUtils.sleep(10000)
console.debug("Waking up")
if (!gotResponse) {
console.log("Didn't get an answer yet - waiting more")
}
}
if(!success){
failed++;
console.log("Hit the rate limit - waiting 90s")
for (let i = 0; i < 90; i++) {
console.log(90 - i)
await ScriptUtils.sleep(1000)
}
}
}
}
return {failed: failed, skipped: skipped}
}
async function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig) {
let processed = 0;
for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) {
processed++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
console.log(" Post processing", processed, "/", r.total, filename)
if (!existsSync(filename)) {
throw "Not found - and not downloaded. Run this script again!: " + filename
}
// We read the raw OSM-file and convert it to a geojson
const rawOsm = JSON.parse(readFileSync(filename, "UTF8"))
// Create and save the geojson file - which is the main chunk of the data
const geojson = OsmToGeoJson.default(rawOsm);
const osmTime = new Date(rawOsm.osm3s.timestamp_osm_base);
for (const feature of geojson.features) {
for (const layer of theme.layers) {
if (layer.source.osmTags.matchesProperties(feature.properties)) {
feature["_matching_layer_id"] = layer.id;
break;
}
}
}
const featuresFreshness = geojson.features.map(feature => ({
freshness: osmTime,
feature: feature
}));
// Extract the relationship information
const relations = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(rawOsm))
MetaTagging.addMetatags(featuresFreshness, relations, theme.layers);
writeFileSync(geoJsonName(targetdir, x, y, r.zoomlevel), JSON.stringify(geojson))
const meta = {
freshness: osmTime,
relations: relations
}
writeFileSync(
metaJsonName(targetdir, x, y, r.zoomlevel),
JSON.stringify(meta)
)
}
}
}
async function main(args: string[]) {
if (args.length == 0) {
console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1")
return;
}
const themeName = args[0]
const zoomlevel = Number(args[1])
const targetdir = args[2] + "/" + themeName
const lat0 = Number(args[3])
const lon0 = Number(args[4])
const lat1 = Number(args[5])
const lon1 = Number(args[6])
const tileRange = Utils.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1)
const theme = AllKnownLayouts.allKnownLayouts.get(themeName)
if (theme === undefined) {
const keys = []
AllKnownLayouts.allKnownLayouts.forEach((_, key) => {
keys.push(key)
})
console.error("The theme " + theme + " was not found; try one of ", keys);
return
}
const overpass = createOverpassObject(theme)
let failed = 0;
do {
const cachingResult = await downloadRaw(targetdir, tileRange, overpass)
failed = cachingResult.failed
if (failed > 0) {
ScriptUtils.sleep(30000)
}
} while (failed > 0)
await postProcess(targetdir, tileRange, theme)
}
let args = [...process.argv]
args.splice(0, 2)
main(args);

View file

@ -27,7 +27,7 @@ const layerFiles = ScriptUtils.readDirRecSync("./assets/layers")
const parsed = JSON.parse(readFileSync(path, "UTF8"));
return {parsed: parsed, path: path}
} catch (e) {
console.error("Could not parse file ", "./assets/layers/"+ path, "due to ", e)
console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e)
}
})
const themeFiles: any[] = ScriptUtils.readDirRecSync("./assets/themes")
@ -42,7 +42,7 @@ writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify(
}))
console.log("Discovered ", layerFiles.length, "layers and ", themeFiles.length, "themes\n")
console.log("Discovered", layerFiles.length, "layers and", themeFiles.length, "themes\n")
console.log(" ---------- VALIDATING ---------")
// ------------- VALIDATION --------------
const licensePaths = []
@ -64,12 +64,11 @@ function validateLayer(layerJson: LayerConfigJson, path: string, context?: strin
errorCount.push("Found a remote image: " + remoteImage + " in layer " + layer.id + ", please download it.")
const path = remoteImage.substring(remoteImage.lastIndexOf("/") + 1)
}
const expected : string = `assets/layers/${layer.id}/${layer.id}.json`
if(path!=undefined && path.indexOf(expected)< 0){
errorCount.push("Layer is in an incorrect place. The path is "+path+", but expected "+expected)
const expected: string = `assets/layers/${layer.id}/${layer.id}.json`
if (path != undefined && path.indexOf(expected) < 0) {
errorCount.push("Layer is in an incorrect place. The path is " + path + ", but expected " + expected)
}
for (const image of images) {
if (!knownPaths.has(image)) {
const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}`
@ -106,7 +105,7 @@ for (const themeFile of themeFiles) {
}
} else {
// layer.builtin contains layer overrides - we can skip those
layerErrorCount.push(...validateLayer(layer,undefined, themeFile.id))
layerErrorCount.push(...validateLayer(layer, undefined, themeFile.id))
}
}
}

View file

@ -234,6 +234,7 @@ if (!existsSync(generatedDir)) {
}
const blacklist = ["", "test", ".", "..", "manifest", "index", "land", "preferences", "account", "openstreetmap", "custom"]
// @ts-ignore
const all : LayoutConfigJson[] = all_known_layouts.themes;
for (const i in all) {

View file

@ -193,17 +193,18 @@ writeFileSync("./assets/generated/license_info.json", JSON.stringify(licenseInfo
const artwork = contents.filter(pth => pth.match(/(.svg|.png|.jpg)$/i) != null)
const missingLicenses = missingLicenseInfos(licenseInfos, artwork)
const invalidLicenses = licenseInfos.filter(l => (l.license ?? "") === "").map(l => `License for artwork ${l.path} is empty string or undefined`)
if (process.argv.indexOf("--prompt") >= 0 || process.argv.indexOf("--query") >= 0) {
queryMissingLicenses(missingLicenses)
}
if (missingLicenses.length > 0) {
const msg = `There are ${missingLicenses.length} licenses missing.`
const msg = `There are ${missingLicenses.length} licenses missing and ${invalidLicenses.length} invalid licenses.`
console.log( missingLicenses.concat(invalidLicenses).join("\n"))
console.error(msg)
if (process.argv.indexOf("--report") >= 0) {
console.log("Writing report!")
writeFileSync("missing_licenses.txt", missingLicenses.join("\n"))
writeFileSync("missing_licenses.txt", missingLicenses.concat(invalidLicenses).join("\n"))
}
if (process.argv.indexOf("--no-fail") < 0) {
throw msg

View file

@ -1,6 +1,6 @@
export default class T {
constructor(testsuite: string, tests: [string, () => void ][]) {
let failures : string []= [];
for (const [name, test] of tests) {

48
test/Theme.spec.ts Normal file
View file

@ -0,0 +1,48 @@
import T from "./TestHelper";
import {Utils} from "../Utils";
Utils.runningFromConsole = true;
import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion";
import {UIEventSource} from "../Logic/UIEventSource";
import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig";
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson";
import * as assert from "assert";
new T("Theme tests",
[
["Nested overrides work", () => {
const themeConfigJson : LayoutConfigJson = {
description: "Descr",
icon: "",
language: ["en"],
layers: [
{
builtin: "public_bookcase",
override: {
source:{
geoJson: "xyz"
}
}
}
],
maintainer: "",
startLat: 0,
startLon: 0,
startZoom: 0,
title: {
en: "Title"
},
version: "",
id: "test"
}
const themeConfig = new LayoutConfig(themeConfigJson);
assert.equal("xyz", themeConfig.layers[0].source.geojsonSource)
}]
]
);