Merge branch 'master' into master
3
.gitignore
vendored
|
@ -9,3 +9,6 @@ assets/generated/*
|
|||
/*.html
|
||||
!/index.html
|
||||
.parcel-cache
|
||||
Docs/Tools/stats.*.json
|
||||
Docs/Tools/stats.csv
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ export default class AllTranslationAssets {
|
|||
|
||||
public static t = { image: { addPicture: new Translation( {"en":"Add picture","es":"Añadir foto","ca":"Afegir foto","nl":"Voeg foto toe","fr":"Ajoutez une photo","gl":"Engadir imaxe","de":"Bild hinzufügen"} ),
|
||||
uploadingPicture: new Translation( {"en":"Uploading your picture...","nl":"Bezig met een foto te uploaden...","es":"Subiendo tu imagen ...","ca":"Pujant la teva imatge ...","fr":"Mise en ligne de votre photo...","gl":"Subindo a túa imaxe...","de":"Ihr Bild hochladen..."} ),
|
||||
uploadingMultiple: new Translation( {"en":"Uploading {count} of your picture...","nl":"Bezig met {count} foto's te uploaden...","ca":"Pujant {count} de la teva imatge...","es":"Subiendo {count} de tus fotos...","fr":"Mise en ligne de {count} photos...","gl":"Subindo {count} das túas imaxes...","de":"{count} Ihrer Bilder hochgeladen..."} ),
|
||||
uploadingMultiple: new Translation( {"en":"Uploading {count} pictures...","nl":"Bezig met {count} foto's te uploaden...","ca":"Pujant {count} de la teva imatge...","es":"Subiendo {count} de tus fotos...","fr":"Mise en ligne de {count} photos...","gl":"Subindo {count} das túas imaxes...","de":"{count} Ihrer Bilder hochgeladen..."} ),
|
||||
pleaseLogin: new Translation( {"en":"Please login to add a picure","nl":"Gelieve je aan te melden om een foto toe te voegen","es":"Entra para subir una foto","ca":"Entra per pujar una foto","fr":"Connectez-vous pour mettre une photo en ligne","gl":"Inicia a sesión para subir unha imaxe","de":"Bitte einloggen, um ein Bild hinzuzufügen"} ),
|
||||
willBePublished: new Translation( {"en":"Your picture will be published: ","es":"Tu foto será publicada: ","ca":"La teva foto serà publicada: ","nl":"Jouw foto wordt gepubliceerd: ","fr":"Votre photo va être publiée: ","gl":"A túa imaxe será publicada: ","de":"Ihr Bild wird veröffentlicht: "} ),
|
||||
cco: new Translation( {"en":"in the public domain","ca":"en domini públic","es":"en dominio público","nl":"in het publiek domein","fr":"sur le domaine publique","gl":"no dominio público","de":"in die Public Domain"} ),
|
||||
|
|
|
@ -22,6 +22,10 @@ import * as tree_nodes from "../assets/layers/trees/tree_nodes.json"
|
|||
import * as benches from "../assets/layers/benches/benches.json"
|
||||
import * as benches_at_pt from "../assets/layers/benches/benches_at_pt.json"
|
||||
import * as picnic_tables from "../assets/layers/benches/picnic_tables.json"
|
||||
import * as play_forest from "../assets/layers/play_forest/play_forest.json"
|
||||
import * as playground from "../assets/layers/playground/playground.json"
|
||||
import * as sport_pitch from "../assets/layers/sport_pitch/sport_pitch.json"
|
||||
import * as slow_roads from "../assets/layers/slow_roads/slow_roads.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
import {LayerConfigJson} from "./JSON/LayerConfigJson";
|
||||
|
||||
|
@ -52,7 +56,11 @@ export default class AllKnownLayers {
|
|||
tree_nodes,
|
||||
benches,
|
||||
benches_at_pt,
|
||||
picnic_tables
|
||||
picnic_tables,
|
||||
play_forest,
|
||||
playground,
|
||||
sport_pitch,
|
||||
slow_roads
|
||||
];
|
||||
|
||||
// Must be below the list...
|
||||
|
|
|
@ -22,6 +22,9 @@ import * as personal from "../assets/themes/personalLayout/personalLayout.json"
|
|||
import * as playgrounds from "../assets/themes/playgrounds/playgrounds.json"
|
||||
import * as bicycle_lib from "../assets/themes/bicycle_library/bicycle_library.json"
|
||||
import * as campersites from "../assets/themes/campersites/campersites.json"
|
||||
import * as play_forests from "../assets/themes/play_forests/play_forests.json"
|
||||
import * as speelplekken from "../assets/themes/speelplekken/speelplekken.json"
|
||||
import * as sport_pitches from "../assets/themes/sport_pitches/sport_pitches.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
import LayoutConfig from "./JSON/LayoutConfig";
|
||||
import AllKnownLayers from "./AllKnownLayers";
|
||||
|
@ -71,7 +74,10 @@ export class AllKnownLayouts {
|
|||
new LayoutConfig(climbing),
|
||||
new LayoutConfig(playgrounds),
|
||||
new LayoutConfig(trees),
|
||||
new LayoutConfig(campersites)
|
||||
new LayoutConfig(campersites),
|
||||
new LayoutConfig(play_forests) ,
|
||||
new LayoutConfig(speelplekken),
|
||||
new LayoutConfig(sport_pitches)
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -49,24 +49,26 @@ export default class LayerConfig {
|
|||
|
||||
tagRenderings: TagRenderingConfig [];
|
||||
|
||||
private readonly configuration_warnings : string[] = []
|
||||
|
||||
constructor(json: LayerConfigJson,
|
||||
context?: string) {
|
||||
context = context + "." + json.id;
|
||||
const self = this;
|
||||
this.id = json.id;
|
||||
this.name = Translations.T(json.name);
|
||||
this.description = Translations.T(json.description);
|
||||
this.name = Translations.T(json.name, context+".name");
|
||||
this.description = Translations.T(json.description, context+".description");
|
||||
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
|
||||
this.doNotDownload = json.doNotDownload ?? false,
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
this.minzoom = json.minzoom;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
||||
this.presets = (json.presets ?? []).map(pr =>
|
||||
this.presets = (json.presets ?? []).map((pr, i) =>
|
||||
({
|
||||
title: Translations.T(pr.title),
|
||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
|
||||
description: Translations.T(pr.description)
|
||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`)
|
||||
}))
|
||||
|
||||
|
||||
|
|
|
@ -140,8 +140,20 @@ export interface LayerConfigJson {
|
|||
* NB: if no presets are defined, the popup to add new points doesn't show up at all
|
||||
*/
|
||||
presets?: {
|
||||
/**
|
||||
* The title - shown on the 'add-new'-button.
|
||||
*/
|
||||
title: string | any,
|
||||
/**
|
||||
* The tags to add. It determines the icon too
|
||||
*/
|
||||
tags: string[],
|
||||
/**
|
||||
* The _first sentence_ of the description is shown on the button of the `add` menu.
|
||||
* The full description is shown in the confirmation dialog.
|
||||
*
|
||||
* (The first sentence is until the first '.'-character in the description)
|
||||
*/
|
||||
description?: string | any,
|
||||
}[],
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ export default class LayoutConfig {
|
|||
};
|
||||
|
||||
public readonly hideFromOverview: boolean;
|
||||
public readonly lockLocation: boolean | [[number,number],[number, number]];
|
||||
public readonly enableUserBadge: boolean;
|
||||
public readonly enableShareScreen: boolean;
|
||||
public readonly enableMoreQuests: boolean;
|
||||
|
@ -53,6 +54,9 @@ export default class LayoutConfig {
|
|||
} else {
|
||||
this.language = json.language;
|
||||
}
|
||||
if(this.language.length == 0){
|
||||
throw "No languages defined. Define at least one language"
|
||||
}
|
||||
if (json.title === undefined) {
|
||||
throw "Title not defined in " + this.id;
|
||||
}
|
||||
|
@ -62,7 +66,7 @@ export default class LayoutConfig {
|
|||
this.title = new Translation(json.title, context + ".title");
|
||||
this.description = new Translation(json.description, context + ".description");
|
||||
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription");
|
||||
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context) : new Translation(json.descriptionTail, context + ".descriptionTail");
|
||||
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context+".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail");
|
||||
this.icon = json.icon;
|
||||
this.socialImage = json.socialImage;
|
||||
this.startZoom = json.startZoom;
|
||||
|
@ -103,7 +107,7 @@ export default class LayoutConfig {
|
|||
return new LayerConfig(layer, `${this.id}.layers[${i}]`)
|
||||
});
|
||||
|
||||
// ALl the layers are constructed, let them share tags in piece now!
|
||||
// ALl the layers are constructed, let them share tags in now!
|
||||
const roaming : {r, source: LayerConfig}[] = []
|
||||
for (const layer of this.layers) {
|
||||
roaming.push({r: layer.GetRoamingRenderings(), source:layer});
|
||||
|
@ -118,6 +122,17 @@ export default class LayoutConfig {
|
|||
}
|
||||
}
|
||||
|
||||
for(const layer of this.layers) {
|
||||
layer.AddRoamingRenderings(
|
||||
{
|
||||
titleIcons:[],
|
||||
iconOverlays: [],
|
||||
tagRenderings: this.roamingRenderings
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
this.clustering = {
|
||||
maxZoom: 16,
|
||||
minNeededElements: 250
|
||||
|
@ -135,7 +150,7 @@ export default class LayoutConfig {
|
|||
}
|
||||
|
||||
this.hideFromOverview = json.hideFromOverview ?? false;
|
||||
|
||||
this.lockLocation = json.lockLocation ?? false;
|
||||
this.enableUserBadge = json.enableUserBadge ?? true;
|
||||
this.enableShareScreen = json.enableShareScreen ?? true;
|
||||
this.enableMoreQuests = json.enableMoreQuests ?? true;
|
||||
|
|
|
@ -158,6 +158,13 @@ export interface LayoutConfigJson {
|
|||
*/
|
||||
hideFromOverview?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, the basemap will not scroll outside of the area visible on initial zoom.
|
||||
* If set to [[lat0, lon0], [lat1, lon1]], the map will not scroll outside of those bounds.
|
||||
* Off by default, which will enable panning to the entire world
|
||||
*/
|
||||
lockLocation?: boolean | [[number, number], [number, number]];
|
||||
|
||||
enableUserBadge?: boolean;
|
||||
enableShareScreen?: boolean;
|
||||
enableMoreQuests?: boolean;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {And, TagsFilter} from "../../Logic/Tags";
|
||||
import {And, TagsFilter, TagUtils} from "../../Logic/Tags";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {FromJSON} from "./FromJSON";
|
||||
|
@ -16,6 +16,8 @@ export default class TagRenderingConfig {
|
|||
readonly question?: Translation;
|
||||
readonly condition?: TagsFilter;
|
||||
|
||||
readonly configuration_warnings : string[] = []
|
||||
|
||||
readonly freeform?: {
|
||||
readonly key: string,
|
||||
readonly type: string,
|
||||
|
@ -45,13 +47,13 @@ export default class TagRenderingConfig {
|
|||
throw "Initing a TagRenderingConfig with undefined in " + context;
|
||||
}
|
||||
if (typeof json === "string") {
|
||||
this.render = Translations.T(json);
|
||||
this.render = Translations.T(json, context+".render");
|
||||
this.multiAnswer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.render = Translations.T(json.render);
|
||||
this.question = Translations.T(json.question);
|
||||
this.render = Translations.T(json.render, context+".render");
|
||||
this.question = Translations.T(json.question, context+".question");
|
||||
this.roaming = json.roaming ?? false;
|
||||
const condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`);
|
||||
if (this.roaming && conditionIfRoaming !== undefined) {
|
||||
|
@ -66,15 +68,27 @@ export default class TagRenderingConfig {
|
|||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? []
|
||||
}
|
||||
if(this.freeform.key === undefined || this.freeform.key === ""){
|
||||
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`
|
||||
}
|
||||
if(this.freeform.addExtraTags){
|
||||
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
|
||||
if(usedKeys.indexOf(this.freeform.key) >= 0){
|
||||
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.multiAnswer = json.multiAnswer ?? false
|
||||
if (json.mappings) {
|
||||
|
||||
|
||||
this.mappings = json.mappings.map((mapping, i) => {
|
||||
|
||||
|
||||
if (mapping.then === undefined) {
|
||||
throw `${context}.mapping[${i}]: Invalid mapping: if without body`
|
||||
}
|
||||
|
@ -87,14 +101,15 @@ export default class TagRenderingConfig {
|
|||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = FromJSON.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
|
||||
}
|
||||
const mappingContext = `${context}.mapping[${i}]`
|
||||
const mp = {
|
||||
if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}].if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${context}.mapping[${i}].ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then),
|
||||
if: FromJSON.Tag(mapping.if, `${mappingContext}.if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${mappingContext}.ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then, `{mappingContext}.then`),
|
||||
hideInAnswer: hideInAnswer
|
||||
};
|
||||
if (this.question) {
|
||||
if (hideInAnswer !== true && !mp.if.isUsableAsAnswer()) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
|
||||
|
@ -115,6 +130,36 @@ export default class TagRenderingConfig {
|
|||
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
|
||||
}
|
||||
|
||||
if(this.render && this.question && this.freeform === undefined){
|
||||
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}`
|
||||
}
|
||||
|
||||
if(!json.multiAnswer && this.mappings !== undefined && this.question !== undefined){
|
||||
let keys = []
|
||||
for (let i = 0; i < this.mappings.length; i++){
|
||||
const mapping = this.mappings[i];
|
||||
if(mapping.if === undefined){
|
||||
throw `${context}.mappings[${i}].if is undefined`
|
||||
}
|
||||
keys.push(...mapping.if.usedKeys())
|
||||
}
|
||||
keys = Utils.Dedup(keys)
|
||||
for (let i = 0; i < this.mappings.length; i++){
|
||||
const mapping = this.mappings[i];
|
||||
if(mapping.hideInAnswer){
|
||||
continue
|
||||
}
|
||||
|
||||
const usedKeys = mapping.if.usedKeys();
|
||||
for (const expectedKey of keys) {
|
||||
if(usedKeys.indexOf(expectedKey) < 0){
|
||||
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}`
|
||||
this.configuration_warnings.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.question !== undefined && json.multiAnswer) {
|
||||
if ((this.mappings?.length ?? 0) === 0) {
|
||||
throw `${context} MultiAnswer is set, but no mappings are defined`
|
||||
|
@ -139,6 +184,40 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if it is known or not shown, false if the question should be asked
|
||||
* @constructor
|
||||
*/
|
||||
public IsKnown(tags: any): boolean {
|
||||
if (this.condition &&
|
||||
!this.condition.matchesProperties(tags)) {
|
||||
// Filtered away by the condition
|
||||
return true;
|
||||
}
|
||||
if(this.multiAnswer){
|
||||
for (const m of this.mappings) {
|
||||
if(TagUtils.MatchesMultiAnswer(m.if, tags)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const free = this.freeform?.key
|
||||
if(free !== undefined){
|
||||
return tags[free] !== undefined
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
if (this.GetRenderValue(tags) !== undefined) {
|
||||
// This value is known and can be rendered
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct rendering value (or undefined if not known)
|
||||
* @constructor
|
||||
|
|
|
@ -49,14 +49,64 @@ export interface TagRenderingConfigJson {
|
|||
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
|
||||
*/
|
||||
mappings?: {
|
||||
/**
|
||||
* If this condition is met, then the text under `then` will be shown.
|
||||
* If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM.
|
||||
*/
|
||||
if: AndOrTagConfigJson | string,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This tag is applied if the respective checkbox is unset
|
||||
* If the condition `if` is met, the text `then` will be rendered.
|
||||
* If not known yet, the user will be presented with `then` as an option
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string,
|
||||
then: string | any
|
||||
hideInAnswer?: boolean
|
||||
then: string | any,
|
||||
/**
|
||||
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
|
||||
*
|
||||
* In the latter case, a correct text should be shown, but only a single, canonical tagging should be selectable by the user.
|
||||
* In this case, one of the mappings can be hiden by setting this flag.
|
||||
*
|
||||
* To demonstrate an example making a default assumption:
|
||||
*
|
||||
* mappings: [
|
||||
* {
|
||||
* if: "access=", -- no access tag present, we assume accessible
|
||||
* then: "Accessible to the general public",
|
||||
* hideInAnswer: true
|
||||
* },
|
||||
* {
|
||||
* if: "access=yes",
|
||||
* then: "Accessible to the general public", -- the user selected this, we add that to OSM
|
||||
* },
|
||||
* {
|
||||
* if: "access=no",
|
||||
* then: "Not accessible to the public"
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* For example, for an operator, we have `operator=Agentschap Natuur en Bos`, which is often abbreviated to `operator=ANB`.
|
||||
* Then, we would add two mappings:
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos" -- the non-abbreviated version which should be uploaded
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* },
|
||||
* {
|
||||
* if: "operator=ANB", -- we don't want to upload abbreviations
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* hideInAnswer: true
|
||||
* }
|
||||
*/
|
||||
hideInAnswer?: boolean,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This is for situations such as:
|
||||
* `accepts:coins=no` where one can select all the possible payment methods. However, we want to make explicit that some options _were not_ selected.
|
||||
* This can be done with `ifnot`
|
||||
* Note that we can not explicitly render this negative case to the user, we cannot show `does _not_ accept coins`.
|
||||
* If this is important to your usecase, consider using multiple radiobutton-fields without `multiAnswer`
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string
|
||||
|
||||
}[]
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,7 @@ Every field is documented in the source code itself - you can find them here:
|
|||
- [The top level `LayoutConfig`](https://github.com/pietervdvn/MapComplete/blob/master/Customizations/JSON/LayoutConfigJson.ts)
|
||||
- [A layer object `LayerConfig`](https://github.com/pietervdvn/MapComplete/blob/master/Customizations/JSON/LayerConfigJson.ts)
|
||||
- [The `TagRendering`](https://github.com/pietervdvn/MapComplete/blob/master/Customizations/JSON/TagRenderingConfigJson.ts)
|
||||
- At last, the exact semantics of tags is documented [here](Docs/Tags_format.md)
|
||||
|
||||
## Some pitfalls
|
||||
|
||||
|
|
61
Docs/SpecialRenderings.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
### Special tag renderings
|
||||
|
||||
In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.General usage is **{func\_name()}** or **{func\_name(arg, someotherarg)}**. Note that you _do not_ need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args
|
||||
|
||||
### all\_tags
|
||||
|
||||
Prints all key-value pairs of the object - used for debugging
|
||||
|
||||
**Example usage:** {all\_tags()}
|
||||
|
||||
### image\_carousel
|
||||
|
||||
Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)
|
||||
|
||||
1. **image key/prefix**: The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Default: image
|
||||
2. **smart search**: Also include images given via 'Wikidata', 'wikimedia\_commons' and 'mapillary Default: true
|
||||
|
||||
**Example usage:** {image\_carousel(image,true)}
|
||||
|
||||
### image\_upload
|
||||
|
||||
Creates a button where a user can upload an image to IMGUR
|
||||
|
||||
1. **image-key**: Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added) Default: image
|
||||
|
||||
**Example usage:** {image\_upload(image)}
|
||||
|
||||
### reviews
|
||||
|
||||
Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten
|
||||
|
||||
1. **subjectKey**: The key to use to determine the subject. If specified, the subject will be **tags\[subjectKey\]** Default: name
|
||||
2. **fallback**: The identifier to use, if _tags\[subjectKey\]_ as specified above is not available. This is effectively a fallback value
|
||||
|
||||
**Example usage:** **{reviews()} **for a vanilla review, **{reviews(name, play\_forest)}** to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play\_forest' is used****
|
||||
|
||||
### ****opening\_hours\_table****
|
||||
|
||||
****Creates an opening-hours table. Usage: {opening\_hours\_table(opening\_hours)} to create a table of the tag 'opening\_hours'.
|
||||
|
||||
1. **key**: The tagkey from which the table is constructed. Default: opening\_hours
|
||||
|
||||
**Example usage:** {opening\_hours\_table(opening\_hours)}
|
||||
|
||||
### live
|
||||
|
||||
Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json\[x\]\[y\]\[z\], other: json\[a\]\[b\]\[c\] out of it and will return 'other' or 'json\[a\]\[b\]\[c\]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed\_value)}
|
||||
|
||||
1. **Url**: The URL to load
|
||||
2. **Shorthands**: A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;
|
||||
3. **path**: The path (or shorthand) that should be returned
|
||||
|
||||
**Example usage:** {live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour\_cnt;day:data.day\_cnt;year:data.year\_cnt,hour)}
|
||||
|
||||
### share\_link
|
||||
|
||||
Creates a link that (attempts to) open the native 'share'-screen
|
||||
|
||||
1. **url**: The url to share (default: current URL)
|
||||
|
||||
**Example usage:** {share\_link()} to share the current page, {share\_link()} to share the given url****
|
31
Docs/Stats.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
Statistics
|
||||
==========
|
||||
|
||||
There are some fancy statistics available about MapComplete use. The most important once are listed below, some more graphs (and the scripts to generate them) are [in the tools directory](Tools/)
|
||||
|
||||
All Time usage
|
||||
--------------
|
||||
|
||||
![](Tools/CumulativeContributors.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20contributor.png)
|
||||
|
||||
Note: in 2020, MapComplete would still make one changeset per answered question. This heavily skews the below graphs towards `buurtnatuur` and `cyclofìx`, two heavily used themes at the beginning.
|
||||
|
||||
![](Tools/Cumulative%20changesets%20per%20theme.png)
|
||||
![](Tools/Theme%20distribution.png)
|
||||
|
||||
2020
|
||||
----
|
||||
|
||||
![](Tools/CumulativeContributors%20in%202020.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20contributor%20in%202020.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20theme%20in%202020.png)
|
||||
![](Tools/Theme%20distribution%20in%202020.png)
|
||||
|
||||
2021
|
||||
----
|
||||
|
||||
![](Tools/CumulativeContributors%20in%202021.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20contributor%20in%202021.png)
|
||||
![](Tools/Cumulative%20changesets%20per%20theme%20in%202021.png)
|
||||
![](Tools/Theme%20distribution%20in%202021.png)
|
37
Docs/Tags_format.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
Tags format
|
||||
=============
|
||||
|
||||
When creating the `json` file describing your layer or theme, you'll have to add a few tags to describe what you want. This document gives an overview of what every expression means and how it behaves in edge cases.
|
||||
|
||||
Strict equality
|
||||
---------------
|
||||
|
||||
Strict equality is denoted by `key=value`. This key matches __only if__ the keypair is present exactly as stated.
|
||||
|
||||
**Only normal tags (eventually in an `and`) can be used in places where they are uploaded**. Normal tags are used in the `mappings` of a [TagRendering] (unless `hideInAnswer` is specified), they are used in `addExtraTags` of [Freeform] and are used in the `tags`-list of a preset.
|
||||
|
||||
If a different kind of tag specification is given, your theme will fail to parse.
|
||||
|
||||
### If key is not present
|
||||
|
||||
If you want to check if a key is not present, use `key=` (pronounce as *key is empty*). A tag collection will match this if `key` is missing or if `key` is a literal empty value.
|
||||
|
||||
### Removing a key
|
||||
|
||||
If a key should be deleted in the OpenStreetMap-database, specify `key=` as well. This can be used e.g. to remove a fixme or value from another mapping if another field is filled out.
|
||||
|
||||
Strict not equals
|
||||
-----------------
|
||||
|
||||
To check if a key does _not_ equal a certain value, use `key!=value`. This is converted behind the scenes to `key!~^value$`
|
||||
|
||||
### If key is present
|
||||
|
||||
This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not empty.
|
||||
|
||||
Regex equals
|
||||
------------
|
||||
|
||||
A tag can also be tested against a regex with `key~regex`. Note that this regex __must match__ the entire value. If the value is allowed to appear anywhere as substring, use `key~.*regex.*`
|
||||
|
||||
Equivalently, `key!~regex` can be used if you _don't_ want to match the regex in order to appear.
|
BIN
Docs/Tools/Changesets per theme in 2020.png
Normal file
After Width: | Height: | Size: 259 KiB |
BIN
Docs/Tools/Changesets per theme in 2021.png
Normal file
After Width: | Height: | Size: 631 KiB |
BIN
Docs/Tools/Changesets per theme.png
Normal file
After Width: | Height: | Size: 258 KiB |
BIN
Docs/Tools/Changesets per version number in 2020.png
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
Docs/Tools/Changesets per version number in 2021.png
Normal file
After Width: | Height: | Size: 428 KiB |
BIN
Docs/Tools/Contributors in 2020.png
Normal file
After Width: | Height: | Size: 581 KiB |
BIN
Docs/Tools/Contributors in 2021.png
Normal file
After Width: | Height: | Size: 499 KiB |
BIN
Docs/Tools/Contributors.png
Normal file
After Width: | Height: | Size: 649 KiB |
BIN
Docs/Tools/Cumulative changesets per contributor in 2020.png
Normal file
After Width: | Height: | Size: 704 KiB |
BIN
Docs/Tools/Cumulative changesets per contributor in 2021.png
Normal file
After Width: | Height: | Size: 212 KiB |
BIN
Docs/Tools/Cumulative changesets per contributor.png
Normal file
After Width: | Height: | Size: 600 KiB |
BIN
Docs/Tools/Cumulative changesets per host in 2020.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
Docs/Tools/Cumulative changesets per host in 2021.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
Docs/Tools/Cumulative changesets per host.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
Docs/Tools/Cumulative changesets per language in 2020.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
Docs/Tools/Cumulative changesets per language in 2021.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
Docs/Tools/Cumulative changesets per language.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
Docs/Tools/Cumulative changesets per theme in 2020.png
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
Docs/Tools/Cumulative changesets per theme in 2021.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
Docs/Tools/Cumulative changesets per theme.png
Normal file
After Width: | Height: | Size: 308 KiB |
BIN
Docs/Tools/Cumulative changesets per version number in 2020.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
Docs/Tools/Cumulative changesets per version number in 2021.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
Docs/Tools/Cumulative changesets per version number.png
Normal file
After Width: | Height: | Size: 250 KiB |
BIN
Docs/Tools/CumulativeContributors in 2020.png
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
Docs/Tools/CumulativeContributors in 2021.png
Normal file
After Width: | Height: | Size: 270 KiB |
BIN
Docs/Tools/CumulativeContributors.png
Normal file
After Width: | Height: | Size: 273 KiB |
BIN
Docs/Tools/Theme distribution in 2020.png
Normal file
After Width: | Height: | Size: 594 KiB |
BIN
Docs/Tools/Theme distribution in 2021.png
Normal file
After Width: | Height: | Size: 555 KiB |
BIN
Docs/Tools/Theme distribution.png
Normal file
After Width: | Height: | Size: 756 KiB |
6
Docs/Tools/compileStats.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#! /bin/bash
|
||||
|
||||
./fetchStats.sh
|
||||
./csvPerChange.sh
|
||||
python csvGrapher.py
|
||||
|
318
Docs/Tools/csvGrapher.py
Normal file
|
@ -0,0 +1,318 @@
|
|||
import csv
|
||||
from datetime import datetime
|
||||
|
||||
from matplotlib import pyplot
|
||||
|
||||
|
||||
def counts(lst):
|
||||
counts = {}
|
||||
for v in lst:
|
||||
if not v in counts:
|
||||
counts[v] = 0
|
||||
counts[v] += 1
|
||||
return counts
|
||||
|
||||
|
||||
class Hist:
|
||||
|
||||
def __init__(self, firstcolumn):
|
||||
self.key = "\"" + firstcolumn + "\""
|
||||
self.dictionary = {}
|
||||
self.key = ""
|
||||
|
||||
def add(self, key, value):
|
||||
if not key in self.dictionary:
|
||||
self.dictionary[key] = []
|
||||
self.dictionary[key].append(value)
|
||||
|
||||
def values(self):
|
||||
allV = []
|
||||
for v in self.dictionary.values():
|
||||
allV += list(set(v))
|
||||
return list(set(allV))
|
||||
|
||||
def keys(self):
|
||||
return self.dictionary.keys()
|
||||
|
||||
def get(self, key):
|
||||
if key in self.dictionary:
|
||||
return self.dictionary[key]
|
||||
return None
|
||||
|
||||
# Returns (keys, values.map(f)). To be used with e.g. pyplot.plot
|
||||
def map(self, f):
|
||||
vals = []
|
||||
keys = self.keys()
|
||||
for key in keys:
|
||||
vals.append(f(self.get(key)))
|
||||
return vals
|
||||
|
||||
def mapcumul(self, f, add, zero):
|
||||
vals = []
|
||||
running_value = zero
|
||||
keys = self.keys()
|
||||
for key in keys:
|
||||
v = f(self.get(key))
|
||||
running_value = add(running_value, v)
|
||||
vals.append(running_value)
|
||||
return vals
|
||||
|
||||
def csv(self):
|
||||
csv = self.key + "," + ",".join(self.values())
|
||||
header = self.values()
|
||||
for k in self.dictionary.keys():
|
||||
csv += k
|
||||
values = counts(self.dictionary[k])
|
||||
for head in header:
|
||||
if head in values:
|
||||
csv += "," + str(values[head])
|
||||
else:
|
||||
csv += ",0"
|
||||
csv += "\n"
|
||||
return csv
|
||||
|
||||
def __str__(self):
|
||||
return str(self.dictionary)
|
||||
|
||||
|
||||
def build_hist(stats, keyIndex, valueIndex):
|
||||
hist = Hist("date")
|
||||
c = 0
|
||||
for row in stats:
|
||||
c += 1
|
||||
hist.add(row[keyIndex], row[valueIndex])
|
||||
return hist
|
||||
|
||||
|
||||
def as_date(str):
|
||||
return datetime.strptime(str, "%Y-%m-%d")
|
||||
|
||||
|
||||
def cumulative_users(stats):
|
||||
users_hist = build_hist(stats, 0, 1)
|
||||
all_users_per_day = users_hist.mapcumul(
|
||||
lambda users: set(users),
|
||||
lambda a, b: a.union(b),
|
||||
set([])
|
||||
)
|
||||
cumul_uniq = list(map(len, all_users_per_day))
|
||||
unique_per_day = users_hist.map(lambda users: len(set(users)))
|
||||
new_users = [0]
|
||||
for i in range(len(cumul_uniq) - 1):
|
||||
new_users.append(cumul_uniq[i + 1] - cumul_uniq[i])
|
||||
dates = map(as_date, users_hist.keys())
|
||||
return list(dates), cumul_uniq, list(unique_per_day), list(new_users)
|
||||
|
||||
|
||||
def pyplot_init():
|
||||
pyplot.figure(figsize=(14, 8), dpi=200)
|
||||
pyplot.xticks(rotation='vertical')
|
||||
pyplot.tight_layout()
|
||||
|
||||
|
||||
def create_usercount_graphs(stats, extra_text=""):
|
||||
print("Creating usercount graphs " + extra_text)
|
||||
dates, cumul_uniq, unique_per_day, new_users = cumulative_users(stats)
|
||||
total = cumul_uniq[-1]
|
||||
|
||||
pyplot_init()
|
||||
pyplot.fill_between(dates, unique_per_day, label='Unique contributors')
|
||||
pyplot.fill_between(dates, new_users, label='First time contributor via MapComplete')
|
||||
pyplot.legend()
|
||||
pyplot.title("Unique contributors" + extra_text + ' with MapComplete (' + str(total) + ' contributors)')
|
||||
pyplot.ylabel("Number of unique contributors")
|
||||
pyplot.xlabel("Date")
|
||||
pyplot.savefig("Contributors" + extra_text + ".png", dpi=400, facecolor='w', edgecolor='w', bbox_inches='tight')
|
||||
|
||||
pyplot_init()
|
||||
pyplot.plot(dates, cumul_uniq, label='Cumulative unique contributors')
|
||||
pyplot.legend()
|
||||
pyplot.title("Cumulative unique contributors" + extra_text + " with MapComplete - " + str(total) + " contributors")
|
||||
pyplot.ylabel("Number of unique contributors")
|
||||
pyplot.xlabel("Date")
|
||||
pyplot.savefig("CumulativeContributors" + extra_text + ".png", dpi=400, facecolor='w', edgecolor='w',
|
||||
bbox_inches='tight')
|
||||
|
||||
|
||||
def create_theme_breakdown(stats, fileExtra="", cutoff=5):
|
||||
print("Creating theme breakdown " + fileExtra)
|
||||
themeCounts = {}
|
||||
for row in stats:
|
||||
theme = row[3].lower()
|
||||
if theme in theme_remappings:
|
||||
theme = theme_remappings[theme]
|
||||
if theme in themeCounts:
|
||||
themeCounts[theme] += 1
|
||||
else:
|
||||
themeCounts[theme] = 1
|
||||
themes = list(themeCounts.items())
|
||||
if len(themes) == 0:
|
||||
print("No entries found for theme breakdown (extra: " + str(fileExtra) + ")")
|
||||
return
|
||||
themes.sort(key=lambda kv: kv[1], reverse=True)
|
||||
other_count = sum([theme[1] for theme in themes if theme[1] < cutoff])
|
||||
themes_filtered = [theme for theme in themes if theme[1] >= cutoff]
|
||||
keys = list(map(lambda kv: kv[0] + " (" + str(kv[1]) + ")", themes_filtered))
|
||||
values = list(map(lambda kv: kv[1], themes_filtered))
|
||||
total = sum(map(lambda kv: kv[1], themes))
|
||||
first_pct = themes[0][1] / total;
|
||||
if other_count > 0:
|
||||
keys.append("other")
|
||||
values.append(other_count)
|
||||
pyplot_init()
|
||||
pyplot.pie(values, labels=keys, startangle=(90 - 360 * first_pct / 2))
|
||||
pyplot.title("MapComplete changes per theme" + fileExtra + " - " + str(total) + " total changes")
|
||||
pyplot.savefig("Theme distribution" + fileExtra + ".png", dpi=400, facecolor='w', edgecolor='w',
|
||||
bbox_inches='tight')
|
||||
return themes
|
||||
|
||||
|
||||
def cumulative_changes_per(contents, index, subject, filenameextra="", cutoff=5, cumulative=True, sort=True):
|
||||
print("Creating graph about " + subject + filenameextra)
|
||||
themes = Hist("date")
|
||||
dates_per_theme = Hist("theme")
|
||||
all_themes = set()
|
||||
for row in contents:
|
||||
th = row[index]
|
||||
all_themes.add(th)
|
||||
themes.add(as_date(row[0]), th)
|
||||
dates_per_theme.add(th, row[0])
|
||||
per_theme_count = list(zip(dates_per_theme.keys(), dates_per_theme.map(len)))
|
||||
# PerThemeCount gives the most popular theme first
|
||||
if sort == True:
|
||||
per_theme_count.sort(key=lambda kv: kv[1], reverse=False)
|
||||
elif sort is not None:
|
||||
per_theme_count.sort(key=sort)
|
||||
values_to_show = [] # (theme name, value to fill between - this is stacked, with the first layer to print last)
|
||||
running_totals = None
|
||||
other_total = 0
|
||||
other_theme_count = 0
|
||||
other_cumul = None
|
||||
|
||||
for kv in per_theme_count:
|
||||
theme = kv[0]
|
||||
total_for_this_theme = kv[1]
|
||||
if cumulative:
|
||||
edits_per_day_cumul = themes.mapcumul(
|
||||
lambda themes_for_date: len([x for x in themes_for_date if theme == x]),
|
||||
lambda a, b: a + b, 0)
|
||||
else:
|
||||
edits_per_day_cumul = themes.map(lambda themes_for_date: len([x for x in themes_for_date if theme == x]))
|
||||
|
||||
if (not cumulative) or (running_totals is None):
|
||||
running_totals = edits_per_day_cumul
|
||||
else:
|
||||
running_totals = list(map(lambda ab: ab[0] + ab[1], zip(running_totals, edits_per_day_cumul)))
|
||||
|
||||
if total_for_this_theme >= cutoff:
|
||||
values_to_show.append((theme, running_totals))
|
||||
else:
|
||||
other_total += total_for_this_theme
|
||||
other_theme_count += 1
|
||||
if other_cumul is None:
|
||||
other_cumul = edits_per_day_cumul
|
||||
else:
|
||||
other_cumul = list(map(lambda ab: ab[0] + ab[1], zip(other_cumul, edits_per_day_cumul)))
|
||||
|
||||
keys = list(themes.keys())
|
||||
values_to_show.reverse()
|
||||
values_to_show.append(("other", other_cumul))
|
||||
totals = dict(per_theme_count)
|
||||
total = sum(totals.values())
|
||||
totals["other"] = other_total
|
||||
|
||||
pyplot_init()
|
||||
for kv in values_to_show:
|
||||
if kv[1] is None:
|
||||
continue # No 'other' graph
|
||||
msg = kv[0] + " (" + str(totals[kv[0]]) + ")"
|
||||
if kv[0] == "other":
|
||||
msg = str(other_theme_count) + " small " + subject + "s (" + str(other_total) + " changes)"
|
||||
if cumulative:
|
||||
pyplot.fill_between(keys, kv[1], label=msg)
|
||||
else:
|
||||
pyplot.plot(keys, kv[1], label=msg)
|
||||
|
||||
if cumulative:
|
||||
cumulative_txt = "Cumulative changesets"
|
||||
else:
|
||||
cumulative_txt = "Changesets"
|
||||
pyplot.title(cumulative_txt + " per " + subject + filenameextra + " (" + str(total) + " changesets)")
|
||||
pyplot.legend(loc="upper left", ncol=3)
|
||||
pyplot.savefig(cumulative_txt + " per " + subject + filenameextra + ".png")
|
||||
|
||||
|
||||
def contents_where(contents, index, starts_with, invert=False):
|
||||
for row in contents:
|
||||
if row[index].startswith(starts_with) is not invert:
|
||||
yield row
|
||||
|
||||
|
||||
def create_graphs(contents):
|
||||
create_usercount_graphs(contents)
|
||||
create_theme_breakdown(contents)
|
||||
cumulative_changes_per(contents, 3, "theme", cutoff=10)
|
||||
cumulative_changes_per(contents, 3, "theme", cutoff=10, cumulative=False)
|
||||
cumulative_changes_per(contents, 1, "contributor", cutoff=15)
|
||||
cumulative_changes_per(contents, 2, "language", cutoff=1)
|
||||
cumulative_changes_per(contents, 4, "version number", cutoff=1, sort=lambda kv : kv[0])
|
||||
cumulative_changes_per(contents, 8, "host", cutoff=1)
|
||||
|
||||
currentYear = datetime.now().year
|
||||
for year in range(2020, currentYear + 1):
|
||||
contents_filtered = list(contents_where(contents, 0, str(year)))
|
||||
extratext = " in " + str(year)
|
||||
create_usercount_graphs(contents_filtered, extratext)
|
||||
create_theme_breakdown(contents_filtered, extratext)
|
||||
cumulative_changes_per(contents_filtered, 3, "theme", extratext, cutoff=5)
|
||||
cumulative_changes_per(contents_filtered, 3, "theme", extratext, cutoff=5, cumulative=False)
|
||||
cumulative_changes_per(contents_filtered, 1, "contributor", extratext, cutoff=10)
|
||||
cumulative_changes_per(contents_filtered, 2, "language", extratext, cutoff=1)
|
||||
cumulative_changes_per(contents_filtered, 4, "version number", extratext, cutoff=1, cumulative=False, sort=lambda kv : kv[0])
|
||||
cumulative_changes_per(contents_filtered, 4, "version number", extratext, cutoff=1, sort=lambda kv : kv[0])
|
||||
cumulative_changes_per(contents_filtered, 8, "host", extratext, cutoff=1)
|
||||
|
||||
|
||||
theme_remappings = {
|
||||
"metamap": "maps",
|
||||
"groen": "buurtnatuur",
|
||||
"wiki:mapcomplete/fritures": "fritures",
|
||||
"wiki:MapComplete/Fritures": "fritures",
|
||||
"lits": "lit",
|
||||
"pomp": "cyclofix",
|
||||
"wiki:user:joost_schouppe/campersite": "campersite",
|
||||
"wiki-user-joost_schouppe-geveltuintjes": "geveltuintjes",
|
||||
"wiki-user-joost_schouppe-campersite": "campersites",
|
||||
"wiki-User-joost_schouppe-campersite": "campersites",
|
||||
"wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes",
|
||||
"wiki:User:joost_schouppe/campersite": "campersites",
|
||||
"https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes"
|
||||
}
|
||||
|
||||
|
||||
def clean_input(contents):
|
||||
for row in contents:
|
||||
theme = row[3].strip().strip("\"").lower()
|
||||
if theme == "null":
|
||||
# The theme metadata has only been set later on - we fetch this from the comment
|
||||
i = row[7].rfind("#")
|
||||
theme = row[7][i + 1:-1].lower()
|
||||
if theme in theme_remappings:
|
||||
theme = theme_remappings[theme]
|
||||
row[3] = theme
|
||||
row[4] = row[4].strip().strip("\"")[0:len("MapComplete x.x.x")]
|
||||
yield [data.strip().strip("\"") for data in row]
|
||||
|
||||
|
||||
def main():
|
||||
print("Creating graphs...")
|
||||
with open('stats.csv', newline='') as csvfile:
|
||||
stats = list(clean_input(csv.reader(csvfile, delimiter=',', quotechar='"')))
|
||||
print("Found " + str(len(stats)) + " changesets")
|
||||
create_graphs(stats)
|
||||
print("All done!")
|
||||
|
||||
|
||||
# pyplot.fill_between(range(0,5), [1,2,3,3,2],)
|
||||
# pyplot.show()
|
||||
main()
|
21
Docs/Tools/csvPerChange.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#! /bin/bash
|
||||
|
||||
if [[ ! -e stats.1.json ]]
|
||||
then
|
||||
echo "No stats found - not compiling"
|
||||
exit
|
||||
fi
|
||||
|
||||
rm stats.csv
|
||||
# echo "date, username, language, theme, editor, creations, changes" > stats.csv
|
||||
echo "" > tmp.csv
|
||||
|
||||
for f in stats.*.json
|
||||
do
|
||||
echo $f
|
||||
jq ".features[].properties | [.date, .user, .metadata.language, .metadata.theme, .editor, .create, .modify, .comment, .metadata.host]" "$f" | tr -d "\n" | sed "s/]\[/\n/g" | tr -d "][" >> tmp.csv
|
||||
echo "" >> tmp.csv
|
||||
done
|
||||
|
||||
sed "/^$/d" tmp.csv | sed "s/^ //" | sed "s/ / /g" | sed "s/\"\(....-..-..\)T........./\"\1/" | sort >> stats.csv
|
||||
rm tmp.csv
|
18
Docs/Tools/fetchStats.sh
Executable file
|
@ -0,0 +1,18 @@
|
|||
DATE=$(date +"%Y-%m-%d%%20%H%%3A%M")
|
||||
COUNTER=1
|
||||
if [[ $1 != "" ]]
|
||||
then
|
||||
echo "Starting at $1"
|
||||
COUNTER="$1"
|
||||
fi
|
||||
|
||||
NEXT_URL=$(echo "https://osmcha.org/api/v1/changesets/?date__gte=2020-07-05&date__lte=$DATE&editor=mapcomplete&page=$COUNTER&page_size=1000")
|
||||
rm stats.*.json
|
||||
while [[ "$NEXT_URL" != "" ]]
|
||||
do
|
||||
echo "$COUNTER '$NEXT_URL'"
|
||||
curl "$NEXT_URL" -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Referer: https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222020-07-05%22%2C%22value%22%3A%222020-07-05%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D' -H 'Content-Type: application/json' -H 'Authorization: Token 6e422e2afedb79ef66573982012000281f03dc91' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'TE: Trailers' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' -o stats.$COUNTER.json
|
||||
|
||||
NEXT_URL=$(jq ".next" stats.$COUNTER.json | sed "s/\"//g")
|
||||
let COUNTER++
|
||||
done;
|
2880
Docs/Tools/stats.csv
Normal file
|
@ -257,7 +257,7 @@ export class InitUiElements {
|
|||
isOpened.setData(false);
|
||||
}
|
||||
})
|
||||
isOpened.setData(true)
|
||||
isOpened.setData(Hash.hash.data === undefined)
|
||||
|
||||
|
||||
}
|
||||
|
@ -280,7 +280,8 @@ export class InitUiElements {
|
|||
Translations.t.general.attribution.attributionContent,
|
||||
"<br/>",
|
||||
new Attribution(undefined, undefined, State.state.layoutToUse, undefined)
|
||||
])
|
||||
]),
|
||||
"copyright"
|
||||
)
|
||||
|
||||
;
|
||||
|
@ -334,6 +335,7 @@ export class InitUiElements {
|
|||
|
||||
const attr = new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse,
|
||||
State.state.leafletMap);
|
||||
|
||||
const bm = new Basemap("leafletDiv",
|
||||
State.state.locationControl,
|
||||
State.state.backgroundLayer,
|
||||
|
@ -341,6 +343,22 @@ export class InitUiElements {
|
|||
attr
|
||||
);
|
||||
State.state.leafletMap.setData(bm.map);
|
||||
const layout = State.state.layoutToUse.data
|
||||
if (layout.lockLocation) {
|
||||
const tile = Utils.embedded_tile(layout.startLat, layout.startLon, layout.startZoom - 1)
|
||||
const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y)
|
||||
// We use the bounds to get a sense of distance for this zoom level
|
||||
const latDiff = bounds[0][0] - bounds[1][0]
|
||||
const lonDiff = bounds[0][1] - bounds[1][1]
|
||||
console.warn("Locking the bounds to ", bounds)
|
||||
bm.map.setMaxBounds(
|
||||
[[ layout.startLat - latDiff, layout.startLon - lonDiff ],
|
||||
[ layout.startLat + latDiff, layout.startLon + lonDiff ],
|
||||
]
|
||||
);
|
||||
bm.map.setMinZoom(layout.startZoom)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static InitLayers() {
|
||||
|
@ -420,7 +438,8 @@ export class InitUiElements {
|
|||
|
||||
const addNewPoint = new ScrollableFullScreen(
|
||||
() => Translations.t.general.add.title.Clone(),
|
||||
() => new SimpleAddUI());
|
||||
() => new SimpleAddUI(),
|
||||
"new");
|
||||
|
||||
addNewPoint.isShown.addCallback(isShown => {
|
||||
if (!isShown) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {UIElement} from "../../UI/UIElement";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
|
||||
/**
|
||||
|
@ -18,9 +17,7 @@ export default class SelectedFeatureHandler {
|
|||
this._featureSource = featureSource;
|
||||
const self = this;
|
||||
hash.addCallback(h => {
|
||||
console.log("Hash is now ", h)
|
||||
if (h === undefined || h === "") {
|
||||
console.error("Deselecting feature...")
|
||||
selectedFeature.setData(undefined);
|
||||
}else{
|
||||
self.selectFeature();
|
||||
|
@ -30,7 +27,10 @@ export default class SelectedFeatureHandler {
|
|||
featureSource.features.addCallback(_ => self.selectFeature());
|
||||
|
||||
selectedFeature.addCallback(feature => {
|
||||
hash.setData(feature?.properties?.id ?? "");
|
||||
const h = feature?.properties?.id;
|
||||
if(h !== undefined){
|
||||
hash.setData(h)
|
||||
}
|
||||
})
|
||||
|
||||
this.selectFeature();
|
||||
|
@ -51,7 +51,6 @@ export default class SelectedFeatureHandler {
|
|||
if(hash === undefined || hash === "" || hash === "#"){
|
||||
return;
|
||||
}
|
||||
console.log("Selecting a feature from the hash...")
|
||||
for (const feature of features) {
|
||||
const id = feature.feature?.properties?.id;
|
||||
if(id === hash){
|
||||
|
|
|
@ -84,13 +84,13 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
layers.addCallback(update);
|
||||
|
||||
const registered = new Set<UIEventSource<boolean>>();
|
||||
layers.addCallback(layers => {
|
||||
layers.addCallbackAndRun(layers => {
|
||||
for (const layer of layers) {
|
||||
if(registered.has(layer.isDisplayed)){
|
||||
continue;
|
||||
}
|
||||
registered.add(layer.isDisplayed);
|
||||
layer.isDisplayed.addCallback(update);
|
||||
layer.isDisplayed.addCallback(() => update());
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ export default class MetaTagging {
|
|||
tags["_isOpen:oldvalue"] = tags.opening_hours
|
||||
window.setTimeout(
|
||||
() => {
|
||||
console.log("Updating the _isOpen tag for ", tags.id);
|
||||
console.log("Updating the _isOpen tag for ", tags.id, ", it's timer expired after", timeout);
|
||||
updateTags();
|
||||
},
|
||||
timeout
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {Utils} from "../Utils";
|
||||
import {type} from "os";
|
||||
|
||||
export abstract class TagsFilter {
|
||||
abstract matches(tags: { k: string, v: string }[]): boolean
|
||||
|
@ -26,12 +25,14 @@ export class RegexTag extends TagsFilter {
|
|||
private readonly key: RegExp | string;
|
||||
private readonly value: RegExp | string;
|
||||
private readonly invert: boolean;
|
||||
private readonly matchesEmpty: boolean
|
||||
|
||||
constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) {
|
||||
super();
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.invert = invert;
|
||||
this.matchesEmpty = RegexTag.doesMatch("", this.value);
|
||||
}
|
||||
|
||||
private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
|
||||
|
@ -65,6 +66,10 @@ export class RegexTag extends TagsFilter {
|
|||
return RegexTag.doesMatch(tag.v, this.value) != this.invert;
|
||||
}
|
||||
}
|
||||
if(this.matchesEmpty){
|
||||
// The value is 'empty'
|
||||
return !this.invert;
|
||||
}
|
||||
// The matching key was not found
|
||||
return this.invert;
|
||||
}
|
||||
|
@ -244,7 +249,7 @@ export class Or extends TagsFilter {
|
|||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
return [].concat(this.or.map(subkeys => subkeys.usedKeys()));
|
||||
return [].concat(...this.or.map(subkeys => subkeys.usedKeys()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,7 +368,7 @@ export class And extends TagsFilter {
|
|||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
return [].concat(this.and.map(subkeys => subkeys.usedKeys()));
|
||||
return [].concat(...this.and.map(subkeys => subkeys.usedKeys()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
/**
|
||||
* Wrapper around the hash to create an UIEventSource from it
|
||||
*/
|
||||
export default class Hash {
|
||||
|
||||
public static hash: UIEventSource<string> = Hash.Get();
|
||||
|
@ -33,6 +36,7 @@ export default class Hash {
|
|||
return;
|
||||
}
|
||||
|
||||
history.pushState({}, "")
|
||||
window.location.hash = "#" + h;
|
||||
});
|
||||
|
||||
|
@ -45,6 +49,14 @@ export default class Hash {
|
|||
hash.setData(newValue)
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', _ => {
|
||||
let newValue = window.location.hash.substr(1);
|
||||
if (newValue === "") {
|
||||
newValue = undefined;
|
||||
}
|
||||
hash.setData(newValue)
|
||||
})
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,9 @@ export default class MangroveReviews {
|
|||
this._name = name;
|
||||
this._mangroveIdentity = identity;
|
||||
this._dryRun = dryRun;
|
||||
if(dryRun){
|
||||
console.warn("Mangrove reviews will _not_ be saved as dryrun is specified")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -170,7 +173,6 @@ export default class MangroveReviews {
|
|||
console.warn("DRYRUNNING mangrove reviews: ", payload);
|
||||
if (callback) {
|
||||
if (callback) {
|
||||
console.log("Calling callback")
|
||||
callback();
|
||||
}
|
||||
this._reviews.data.push(r);
|
||||
|
@ -180,7 +182,6 @@ export default class MangroveReviews {
|
|||
} else {
|
||||
mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => {
|
||||
if (callback) {
|
||||
console.log("Calling callback")
|
||||
callback();
|
||||
}
|
||||
this._reviews.data.push(r);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.5.5";
|
||||
public static vNumber = "0.5.11";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
# MapComplete
|
||||
|
||||
> Let a thousand flowers bloom
|
||||
|
@ -242,3 +243,5 @@ Urinal icon: https://thenounproject.com/term/urinal/1307984/
|
|||
24/7 icon: https://www.vecteezy.com/vector-art/1394992-24-7-service-and-support-icon-set
|
||||
|
||||
Translation-icon: https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg
|
||||
|
||||
PingPong-table icon: Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
|
2
State.ts
|
@ -76,7 +76,7 @@ export default class State {
|
|||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(undefined)
|
||||
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
|
||||
|
||||
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
|
||||
|
|
|
@ -4,6 +4,7 @@ import Combine from "./Combine";
|
|||
import Ornament from "./Ornament";
|
||||
import {FixedUiElement} from "./FixedUiElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
|
||||
/**
|
||||
* Wraps some contents into a panel that scrolls the content _under_ the title
|
||||
|
@ -13,21 +14,28 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
public isShown: UIEventSource<boolean>;
|
||||
private _component: UIElement;
|
||||
private _fullscreencomponent: UIElement;
|
||||
private static readonly _actor = ScrollableFullScreen.InitActor();
|
||||
private _hashToSet: string;
|
||||
private static _currentlyOpen : ScrollableFullScreen;
|
||||
|
||||
constructor(title: (() => UIElement), content: (() => UIElement), isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)) {
|
||||
constructor(title: ((mode: string) => UIElement), content: ((mode: string) => UIElement),
|
||||
hashToSet: string,
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
) {
|
||||
super();
|
||||
this.isShown = isShown;
|
||||
this._hashToSet = hashToSet;
|
||||
|
||||
this._component = this.BuildComponent(title(), content(), isShown);
|
||||
this._fullscreencomponent = this.BuildComponent(title(), content(), isShown);
|
||||
this._component = this.BuildComponent(title("desktop"), content("desktop"), isShown)
|
||||
.SetClass("hidden md:block");
|
||||
this._fullscreencomponent = this.BuildComponent(title("mobile"), content("mobile"), isShown);
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
isShown.addCallback(isShown => {
|
||||
if (isShown) {
|
||||
self.Activate();
|
||||
} else {
|
||||
self.clear();
|
||||
|
||||
ScrollableFullScreen.clear();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -39,7 +47,11 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
Activate(): void {
|
||||
this.isShown.setData(true)
|
||||
this._fullscreencomponent.AttachTo("fullscreen");
|
||||
if(this._hashToSet != undefined){
|
||||
Hash.hash.setData(this._hashToSet)
|
||||
}
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen = this;
|
||||
fs.classList.remove("hidden")
|
||||
}
|
||||
|
||||
|
@ -68,11 +80,21 @@ export default class ScrollableFullScreen extends UIElement {
|
|||
|
||||
}
|
||||
|
||||
private clear() {
|
||||
private static clear() {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen");
|
||||
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
|
||||
fs.classList.add("hidden")
|
||||
Hash.hash.setData(undefined);
|
||||
}
|
||||
|
||||
private static InitActor(){
|
||||
Hash.hash.addCallback(hash => {
|
||||
if(hash === undefined || hash === ""){
|
||||
ScrollableFullScreen.clear()
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -75,8 +75,6 @@ export class Basemap {
|
|||
this.map.on("contextmenu", function (e) {
|
||||
// @ts-ignore
|
||||
lastClickLocation.setData({lat: e.latlng.lat, lon: e.latlng.lng});
|
||||
// @ts-ignore
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class FullWelcomePaneWithTabs extends UIElement {
|
|||
this._component = new ScrollableFullScreen(
|
||||
() => layoutToUse.title.Clone(),
|
||||
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails),
|
||||
isShown
|
||||
"welcome" ,isShown
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
export default class LayerControlPanel extends ScrollableFullScreen {
|
||||
|
||||
constructor(isShown: UIEventSource<boolean>) {
|
||||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, isShown);
|
||||
super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown);
|
||||
}
|
||||
|
||||
private static GenTitle(): UIElement {
|
||||
|
|
|
@ -22,10 +22,7 @@ export default class ThemeIntroductionPanel extends UIElement {
|
|||
this.languagePicker = LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.data.language, Translations.t.general.pickLanguage);
|
||||
const layout = State.state.layoutToUse.data;
|
||||
|
||||
this.description = new Combine([
|
||||
"<h3>", layout.title, "</h3>",
|
||||
layout.description
|
||||
])
|
||||
this.description = layout.description
|
||||
this.plzLogIn =
|
||||
Translations.t.general.loginWithOpenStreetMap
|
||||
.onClick(() => {
|
||||
|
|
|
@ -53,13 +53,8 @@ export default class EditableTagRendering extends UIElement {
|
|||
if (this._editMode.data) {
|
||||
return this._question.Render();
|
||||
}
|
||||
if (this._configuration.multiAnswer) {
|
||||
const atLeastOneMatch = this._configuration.mappings.some(mp =>TagUtils.MatchesMultiAnswer(mp.if, this._tags.data));
|
||||
if (!atLeastOneMatch) {
|
||||
return "";
|
||||
}
|
||||
} else if (this._configuration.GetRenderValue(this._tags.data) === undefined) {
|
||||
return "";
|
||||
if(!this._configuration.IsKnown(this._tags.data)){
|
||||
return ""
|
||||
}
|
||||
|
||||
return new Combine([this._answer,
|
||||
|
|
|
@ -8,6 +8,7 @@ import TagRenderingAnswer from "./TagRenderingAnswer";
|
|||
import State from "../../State";
|
||||
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||
private static featureInfoboxCache: Map<LayerConfig, Map<UIEventSource<any>, FeatureInfoBox>> = new Map<LayerConfig, Map<UIEventSource<any>, FeatureInfoBox>>();
|
||||
|
@ -16,7 +17,10 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
tags: UIEventSource<any>,
|
||||
layerConfig: LayerConfig
|
||||
) {
|
||||
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig),() => FeatureInfoBox.GenerateContent(tags, layerConfig));
|
||||
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig),
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
|
||||
tags.data.id);
|
||||
|
||||
if (layerConfig === undefined) {
|
||||
throw "Undefined layerconfig";
|
||||
}
|
||||
|
@ -24,18 +28,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
}
|
||||
|
||||
static construct(tags: UIEventSource<any>, layer: LayerConfig): FeatureInfoBox {
|
||||
let innerMap = FeatureInfoBox.featureInfoboxCache.get(layer);
|
||||
if (innerMap === undefined) {
|
||||
innerMap = new Map<UIEventSource<any>, FeatureInfoBox>();
|
||||
FeatureInfoBox.featureInfoboxCache.set(layer, innerMap);
|
||||
}
|
||||
|
||||
let featureInfoBox = innerMap.get(tags);
|
||||
if (featureInfoBox === undefined) {
|
||||
featureInfoBox = new FeatureInfoBox(tags, layer);
|
||||
innerMap.set(tags, featureInfoBox);
|
||||
}
|
||||
return featureInfoBox;
|
||||
let innerMap = Utils.getOrSetDefault(FeatureInfoBox.featureInfoboxCache, layer,() => new Map<UIEventSource<any>, FeatureInfoBox>())
|
||||
return Utils.getOrSetDefault(innerMap, tags, () => new FeatureInfoBox(tags, layer));
|
||||
}
|
||||
|
||||
private static GenerateTitleBar(tags: UIEventSource<any>,
|
||||
|
@ -56,15 +50,13 @@ 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);
|
||||
}
|
||||
|
||||
let questionBoxIsUsed = false;
|
||||
const renderings = layerConfig.tagRenderings.map(tr => {
|
||||
const renderings = layerConfig.tagRenderings.map((tr,i) => {
|
||||
if (tr.question === null) {
|
||||
// This is the question box!
|
||||
questionBoxIsUsed = true;
|
||||
|
@ -75,13 +67,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
if (!questionBoxIsUsed) {
|
||||
renderings.push(questionBox);
|
||||
}
|
||||
const tail = new Combine([]).SetClass("only-on-mobile");
|
||||
|
||||
return new Combine([
|
||||
...renderings,
|
||||
tail.SetClass("featureinfobox-tail")
|
||||
]
|
||||
).SetClass("block")
|
||||
return new Combine(renderings).SetClass("block")
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -46,37 +46,11 @@ export default class QuestionBox extends UIElement {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if it is known or not shown, false if the question should be asked
|
||||
* @constructor
|
||||
*/
|
||||
IsKnown(tagRendering: TagRenderingConfig): boolean {
|
||||
if (tagRendering.condition &&
|
||||
!tagRendering.condition.matchesProperties(this._tags.data)) {
|
||||
// Filtered away by the condition
|
||||
return true;
|
||||
}
|
||||
if(tagRendering.multiAnswer){
|
||||
for (const m of tagRendering.mappings) {
|
||||
if(TagUtils.MatchesMultiAnswer(m.if, this._tags.data)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tagRendering.GetRenderValue(this._tags.data) !== undefined) {
|
||||
// This value is known and can be rendered
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
|
||||
let tagRendering = this._tagRenderings[i];
|
||||
|
||||
if(this.IsKnown(tagRendering)){
|
||||
if(tagRendering.IsKnown(this._tags.data)){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {Utils} from "../../Utils";
|
|||
import Combine from "../Base/Combine";
|
||||
import {TagUtils} from "../../Logic/Tags";
|
||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
|
||||
/***
|
||||
* Displays the correct value for a known tagrendering
|
||||
|
@ -43,21 +44,37 @@ export default class TagRenderingAnswer extends UIElement {
|
|||
|
||||
// The render value doesn't work well with multi-answers (checkboxes), so we have to check for them manually
|
||||
if (this._configuration.multiAnswer) {
|
||||
const applicableThens = Utils.NoNull(this._configuration.mappings.map(mapping => {
|
||||
|
||||
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 => {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping.then;
|
||||
}
|
||||
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
|
||||
if(!freeformKeyUsed){
|
||||
if(mapping.if.usedKeys().indexOf(this._configuration.freeform.key) >= 0){
|
||||
freeformKeyUsed = true;
|
||||
}
|
||||
}
|
||||
return mapping.then;
|
||||
}
|
||||
return undefined;
|
||||
}))
|
||||
if (applicableThens.length >= 0) {
|
||||
if (applicableThens.length === 1) {
|
||||
this._content = applicableThens[0];
|
||||
|
||||
if (!freeformKeyUsed
|
||||
&& tags[this._configuration.freeform.key] !== undefined) {
|
||||
applicableThens.push(this._configuration.render)
|
||||
}
|
||||
|
||||
const self = this
|
||||
const valuesToRender: UIElement[] = applicableThens.map(tr => SubstitutedTranslation.construct(tr, self._tags))
|
||||
|
||||
if (valuesToRender.length >= 0) {
|
||||
if (valuesToRender.length === 1) {
|
||||
this._content = valuesToRender[0];
|
||||
} else {
|
||||
this._content = new Combine(["<ul>",
|
||||
...applicableThens.map(tr => new Combine(["<li>", tr, "</li>"]))
|
||||
...valuesToRender.map(tr => new Combine(["<li>", tr, "</li>"]))
|
||||
,
|
||||
"</ul>"
|
||||
])
|
||||
|
|
|
@ -38,7 +38,8 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
constructor(tags: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
afterSave?: () => void,
|
||||
cancelButton?: UIElement) {
|
||||
cancelButton?: UIElement
|
||||
) {
|
||||
super(tags);
|
||||
this._tags = tags;
|
||||
this._configuration = configuration;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/**
|
||||
* Shows the reviews and scoring base on mangrove.reviesw
|
||||
* Shows the reviews and scoring base on mangrove.reviews
|
||||
* The middle element is some other component shown in the middle, e.g. the review input element
|
||||
*/
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Review} from "../../Logic/Web/Review";
|
||||
|
@ -11,7 +12,7 @@ import SingleReview from "./SingleReview";
|
|||
export default class ReviewElement extends UIElement {
|
||||
private readonly _reviews: UIEventSource<Review[]>;
|
||||
private readonly _subject: string;
|
||||
private _middleElement: UIElement;
|
||||
private readonly _middleElement: UIElement;
|
||||
|
||||
constructor(subject: string, reviews: UIEventSource<Review[]>, middleElement: UIElement) {
|
||||
super(reviews);
|
||||
|
@ -33,7 +34,7 @@ export default class ReviewElement extends UIElement {
|
|||
const avg = (revs.map(review => review.rating).reduce((a, b) => a + b, 0) / revs.length);
|
||||
elements.push(
|
||||
new Combine([
|
||||
SingleReview.GenStars(avg).SetClass("stars flex"),
|
||||
SingleReview.GenStars(avg),
|
||||
`<a target="_blank" href='https://mangrove.reviews/search?sub=${encodeURIComponent(this._subject)}'>`,
|
||||
revs.length === 1 ? Translations.t.reviews.title_singular :
|
||||
Translations.t.reviews.title
|
||||
|
@ -42,7 +43,7 @@ export default class ReviewElement extends UIElement {
|
|||
"</a>"
|
||||
])
|
||||
|
||||
.SetClass("review-title"));
|
||||
.SetClass("font-2xl flex justify-between items-center pl-2 pr-2"));
|
||||
|
||||
elements.push(this._middleElement);
|
||||
|
||||
|
@ -55,7 +56,7 @@ export default class ReviewElement extends UIElement {
|
|||
|
||||
.SetClass("review-attribution"))
|
||||
|
||||
return new Combine(elements).SetClass("review").Render();
|
||||
return new Combine(elements).SetClass("block").Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -56,7 +56,7 @@ export default class ReviewForm extends InputElement<Review> {
|
|||
onSave(this._value.data, () => {
|
||||
self._saveButton = Translations.t.reviews.saved.SetClass("thanks");
|
||||
});
|
||||
})
|
||||
}).SetClass("break-normal")
|
||||
|
||||
this._isAffiliated = new CheckBoxes([t.i_am_affiliated])
|
||||
|
||||
|
|
|
@ -22,9 +22,9 @@ export default class SingleReview extends UIElement{
|
|||
}
|
||||
const scoreTen = Math.round(rating / 10);
|
||||
return new Combine([
|
||||
"<img src='./assets/svg/star.svg' />".repeat(Math.floor(scoreTen / 2)),
|
||||
scoreTen % 2 == 1 ? "<img src='./assets/svg/star_half.svg' />" : ""
|
||||
])
|
||||
"<img src='./assets/svg/star.svg' class='h-8 md:h-12'/>".repeat(Math.floor(scoreTen / 2)),
|
||||
scoreTen % 2 == 1 ? "<img src='./assets/svg/star_half.svg' class='h-8 md:h-12'/>" : ""
|
||||
]).SetClass("flex w-max")
|
||||
}
|
||||
InnerRender(): string {
|
||||
const d = this._review.date;
|
||||
|
@ -33,25 +33,23 @@ export default class SingleReview extends UIElement{
|
|||
[
|
||||
new Combine([
|
||||
SingleReview.GenStars(review.rating)
|
||||
.SetClass("review-rating"),
|
||||
new FixedUiElement(review.comment).SetClass("review-comment")
|
||||
]).SetClass("review-stars-comment"),
|
||||
|
||||
]),
|
||||
new FixedUiElement(review.comment),
|
||||
new Combine([
|
||||
new Combine([
|
||||
|
||||
new FixedUiElement(review.author).SetClass("review-author"),
|
||||
new Combine(["<b>",review.author,"</b>"]),
|
||||
review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "",
|
||||
]).SetStyle("margin-right: 0.5em"),
|
||||
new FixedUiElement(`${d.getFullYear()}-${Utils.TwoDigits(d.getMonth() + 1)}-${Utils.TwoDigits(d.getDate())} ${Utils.TwoDigits(d.getHours())}:${Utils.TwoDigits(d.getMinutes())}`)
|
||||
.SetClass("review-date")
|
||||
]).SetClass("review-author-date")
|
||||
.SetClass("subtle-lighter")
|
||||
]).SetClass("flex mb-4 justify-end")
|
||||
|
||||
]
|
||||
);
|
||||
el.SetClass("review-element");
|
||||
if(review.made_by_user){
|
||||
el.SetClass("review-by-current-user")
|
||||
el.SetClass("block p-2 m-1 rounded-xl subtle-background review-element");
|
||||
if(review.made_by_user.data){
|
||||
el.SetClass("border-attention-catch")
|
||||
}
|
||||
return el.Render();
|
||||
}
|
||||
|
|
|
@ -80,18 +80,21 @@ export default class SpecialVisualizations {
|
|||
|
||||
{
|
||||
funcName: "reviews",
|
||||
docs: "Adds an overview of the mangrove-reviews of this object. IMPORTANT: the _name_ of the object should be defined for this to work!",
|
||||
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
|
||||
example: "<b>{reviews()}<b> for a vanilla review, <b>{reviews(name, play_forest)}</b> to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
|
||||
args: [{
|
||||
name: "subject",
|
||||
doc: "The identifier used for this value; by default the name of the reviewed object"
|
||||
name: "subjectKey",
|
||||
defaultValue: "name",
|
||||
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>"
|
||||
}, {
|
||||
name: "fallback",
|
||||
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value"
|
||||
}],
|
||||
constr: (state: State, tags, args) => {
|
||||
const tgs = tags.data;
|
||||
let subject = tgs.name ?? "";
|
||||
if (args[0] !== undefined && args[0] !== "") {
|
||||
subject = args[0];
|
||||
}
|
||||
if (subject === "") {
|
||||
const key = args[0] ?? "name"
|
||||
let subject = tgs[key] ?? args[1];
|
||||
if (subject === undefined || subject === "") {
|
||||
return Translations.t.reviews.name_required;
|
||||
}
|
||||
const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat),
|
||||
|
@ -147,7 +150,7 @@ export default class SpecialVisualizations {
|
|||
args: [
|
||||
{
|
||||
name: "url",
|
||||
doc: "The url to share (defualt: current URL)",
|
||||
doc: "The url to share (default: current URL)",
|
||||
}
|
||||
],
|
||||
constr: (state: State, tagSource: UIEventSource<any>, args) => {
|
||||
|
@ -204,7 +207,9 @@ export default class SpecialVisualizations {
|
|||
|
||||
|
||||
return new Combine([
|
||||
"<h3>Special tag renderings</h3>",
|
||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||
"General usage is <b>{func_name()}</b> or <b>{func_name(arg, someotherarg)}</b>. Note that you <i>do not</i> need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args",
|
||||
...helpTexts
|
||||
|
||||
]
|
||||
|
|
|
@ -6,9 +6,11 @@ import Combine from "./Base/Combine";
|
|||
import State from "../State";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import SpecialVisualizations from "./SpecialVisualizations";
|
||||
import {Utils} from "../Utils";
|
||||
|
||||
export class SubstitutedTranslation extends UIElement {
|
||||
private static cachedTranslations: Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>> = new Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>();
|
||||
private static cachedTranslations:
|
||||
Map<string, Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>> = new Map<string, Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>>();
|
||||
private readonly tags: UIEventSource<any>;
|
||||
private readonly translation: Translation;
|
||||
private content: UIElement[];
|
||||
|
@ -32,20 +34,26 @@ export class SubstitutedTranslation extends UIElement {
|
|||
this.SetClass("w-full")
|
||||
}
|
||||
|
||||
private static GenerateMap(){
|
||||
return new Map<UIEventSource<any>, SubstitutedTranslation>()
|
||||
}
|
||||
private static GenerateSubCache(){
|
||||
return new Map<Translation, Map<UIEventSource<any>, SubstitutedTranslation>>();
|
||||
}
|
||||
|
||||
public static construct(
|
||||
translation: Translation,
|
||||
tags: UIEventSource<any>): SubstitutedTranslation {
|
||||
if (!this.cachedTranslations.has(translation)) {
|
||||
this.cachedTranslations.set(translation, new Map<UIEventSource<any>, SubstitutedTranslation>());
|
||||
}
|
||||
const innerMap = this.cachedTranslations.get(translation);
|
||||
|
||||
/* let cachedTranslations = Utils.getOrSetDefault(SubstitutedTranslation.cachedTranslations, SubstitutedTranslation.GenerateSubCache);
|
||||
const innerMap = Utils.getOrSetDefault(cachedTranslations, translation, SubstitutedTranslation.GenerateMap);
|
||||
|
||||
const cachedTranslation = innerMap.get(tags);
|
||||
if (cachedTranslation !== undefined) {
|
||||
return cachedTranslation;
|
||||
}
|
||||
}*/
|
||||
const st = new SubstitutedTranslation(translation, tags);
|
||||
innerMap.set(tags, st);
|
||||
// innerMap.set(tags, st);
|
||||
return st;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,19 +18,19 @@ export default class Translations {
|
|||
}
|
||||
|
||||
|
||||
static T(t: string | any): Translation {
|
||||
static T(t: string | any, context = undefined): Translation {
|
||||
if(t === undefined){
|
||||
return undefined;
|
||||
}
|
||||
if(typeof t === "string"){
|
||||
return new Translation({"*":t});
|
||||
return new Translation({"*":t}, context);
|
||||
}
|
||||
if(t.render !== undefined){
|
||||
const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly"
|
||||
console.error(msg, t);
|
||||
throw msg
|
||||
}
|
||||
return new Translation(t);
|
||||
return new Translation(t, context);
|
||||
}
|
||||
|
||||
private static wtcache = {}
|
||||
|
|
46
Utils.ts
|
@ -152,6 +152,7 @@ export class Utils {
|
|||
head.appendChild(link);
|
||||
console.log("Added custom layout ", location)
|
||||
}
|
||||
|
||||
static Merge(source: any, target: any) {
|
||||
target = JSON.parse(JSON.stringify(target));
|
||||
source = JSON.parse(JSON.stringify(source));
|
||||
|
@ -172,4 +173,49 @@ export class Utils {
|
|||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) {
|
||||
let found = dict.get(k);
|
||||
if (found !== undefined) {
|
||||
return found;
|
||||
}
|
||||
dict.set(k, v());
|
||||
return dict.get(k);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tile bounds of the
|
||||
* @param z
|
||||
* @param x
|
||||
* @param y
|
||||
* @returns [[lat, lon], [lat, lon]]
|
||||
*/
|
||||
static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return x, y of the tile containing (lat, lon) on the given zoom level
|
||||
*/
|
||||
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}
|
||||
}
|
||||
|
||||
private static tile2long(x, z) {
|
||||
return (x / Math.pow(2, z) * 360 - 180);
|
||||
}
|
||||
|
||||
private static tile2lat(y, z) {
|
||||
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||||
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
|
||||
}
|
||||
|
||||
private static lon2tile(lon, zoom) {
|
||||
return (Math.floor((lon + 180) / 360 * Math.pow(2, zoom)));
|
||||
}
|
||||
|
||||
private static lat2tile(lat, zoom) {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,13 @@
|
|||
"fr": "Bancs"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"amenity=bench"
|
||||
]
|
||||
},
|
||||
"overpassTags": "amenity=bench",
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Bench",
|
||||
"de": "Sitzbank",
|
||||
"fr": "Banc"
|
||||
},
|
||||
"mappings": []
|
||||
}
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
|
@ -28,16 +23,11 @@
|
|||
"fr": "Dossier"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "backrest",
|
||||
"addExtraTags": []
|
||||
"key": "backrest"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"backrest=yes"
|
||||
]
|
||||
},
|
||||
"if": "backrest=yes",
|
||||
"then": {
|
||||
"en": "Backrest: Yes",
|
||||
"de": "Rückenlehne: Ja",
|
||||
|
@ -45,11 +35,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"backrest=no"
|
||||
]
|
||||
},
|
||||
"if": "backrest=no",
|
||||
"then": {
|
||||
"en": "Backrest: No",
|
||||
"de": "Rückenlehne: Nein",
|
||||
|
@ -73,7 +59,6 @@
|
|||
"key": "seats",
|
||||
"type": "nat"
|
||||
},
|
||||
"mappings": [],
|
||||
"question": {
|
||||
"en": "How many seats does this bench have?",
|
||||
"de": "Wie viele Sitzplätze hat diese Bank?",
|
||||
|
@ -92,11 +77,7 @@
|
|||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=wood"
|
||||
]
|
||||
},
|
||||
"if": "material=wood",
|
||||
"then": {
|
||||
"en": "Material: wood",
|
||||
"de": "Material: Holz",
|
||||
|
@ -104,11 +85,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=metal"
|
||||
]
|
||||
},
|
||||
"if": "material=metal",
|
||||
"then": {
|
||||
"en": "Material: metal",
|
||||
"de": "Material: Metall",
|
||||
|
@ -116,11 +93,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=stone"
|
||||
]
|
||||
},
|
||||
"if": "material=stone",
|
||||
"then": {
|
||||
"en": "Material: stone",
|
||||
"de": "Material: Stein",
|
||||
|
@ -128,11 +101,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=concrete"
|
||||
]
|
||||
},
|
||||
"if": "material=concrete",
|
||||
"then": {
|
||||
"en": "Material: concrete",
|
||||
"de": "Material: Beton",
|
||||
|
@ -140,11 +109,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=plastic"
|
||||
]
|
||||
},
|
||||
"if": "material=plastic",
|
||||
"then": {
|
||||
"en": "Material: plastic",
|
||||
"de": "Material: Kunststoff",
|
||||
|
@ -152,11 +117,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=steel"
|
||||
]
|
||||
},
|
||||
"if": "material=steel",
|
||||
"then": {
|
||||
"en": "Material: steel",
|
||||
"de": "Material: Stahl",
|
||||
|
@ -200,11 +161,7 @@
|
|||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=brown"
|
||||
]
|
||||
},
|
||||
"if": "colour=brown",
|
||||
"then": {
|
||||
"en": "Colour: brown",
|
||||
"de": "Farbe: braun",
|
||||
|
@ -212,11 +169,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=green"
|
||||
]
|
||||
},
|
||||
"if": "colour=green",
|
||||
"then": {
|
||||
"en": "Colour: green",
|
||||
"de": "Farbe: grün",
|
||||
|
@ -224,11 +177,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=gray"
|
||||
]
|
||||
},
|
||||
"if": "colour=gray",
|
||||
"then": {
|
||||
"en": "Colour: gray",
|
||||
"de": "Farbe: grau",
|
||||
|
@ -236,11 +185,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=white"
|
||||
]
|
||||
},
|
||||
"if": "colour=white",
|
||||
"then": {
|
||||
"en": "Colour: white",
|
||||
"de": "Farbe: weiß",
|
||||
|
@ -248,11 +193,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=red"
|
||||
]
|
||||
},
|
||||
"if": "colour=red",
|
||||
"then": {
|
||||
"en": "Colour: red",
|
||||
"de": "Farbe: rot",
|
||||
|
@ -260,11 +201,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=black"
|
||||
]
|
||||
},
|
||||
"if": "colour=black",
|
||||
"then": {
|
||||
"en": "Colour: black",
|
||||
"de": "Farbe: schwarz",
|
||||
|
@ -272,11 +209,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=blue"
|
||||
]
|
||||
},
|
||||
"if": "colour=blue",
|
||||
"then": {
|
||||
"en": "Colour: blue",
|
||||
"de": "Farbe: blau",
|
||||
|
@ -284,11 +217,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"colour=yellow"
|
||||
]
|
||||
},
|
||||
"if": "colour=yellow",
|
||||
"then": {
|
||||
"en": "Colour: yellow",
|
||||
"de": "Farbe: gelb",
|
||||
|
|
|
@ -5,11 +5,7 @@
|
|||
"nl": "Picnictafels"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=picnic_table"
|
||||
]
|
||||
},
|
||||
"overpassTags": "leisure=picnic_table",
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Picnic table",
|
||||
|
@ -35,22 +31,14 @@
|
|||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=wood"
|
||||
]
|
||||
},
|
||||
"if": "material=wood",
|
||||
"then": {
|
||||
"en": "This is a wooden picnic table",
|
||||
"nl": "Deze picnictafel is gemaakt uit hout"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"material=concrete"
|
||||
]
|
||||
},
|
||||
"if": "material=concrete",
|
||||
"then": {
|
||||
"en": "This is a concrete picnic table",
|
||||
"nl": "Deze picnictafel is gemaakt uit beton"
|
||||
|
|
BIN
assets/layers/play_forest/icon.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
62
assets/layers/play_forest/icon.svg
Normal file
After Width: | Height: | Size: 21 KiB |
114
assets/layers/play_forest/play_forest.json
Normal file
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"id": "play_forest",
|
||||
"name": {
|
||||
"nl": "Speelbossen"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"playground=forest"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Speelbos"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~Speelbos.*",
|
||||
"then": {
|
||||
"nl": "{name}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "name~*",
|
||||
"then": {
|
||||
"nl": "Speelbos {name}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"nl": "Een speelbos is een vrij toegankelijke zone in een bos"
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": "Wie beheert dit gebied?",
|
||||
"render": "Dit gebied wordt beheerd door {operator}",
|
||||
"freeform": {
|
||||
"key": "operator"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "operator~[aA][nN][bB]",
|
||||
"then": "Dit gebied wordt beheerd door het <a href='https://www.natuurenbos.be/spelen'>Agentschap Natuur en Bos</a>",
|
||||
"hideInAnswer": true
|
||||
},
|
||||
{
|
||||
"if": "operator=Agenstchap Natuur en Bos",
|
||||
"then": "Dit gebied wordt beheerd door het <a href='https://www.natuurenbos.be/spelen'>Agentschap Natuur en Bos</a>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Wanneer is deze speelzone toegankelijk?",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "opening_hours=08:00-22:00",
|
||||
"then": "Het hele jaar door overdag toegankelijk (van 08:00 tot 22:00)"
|
||||
},
|
||||
{
|
||||
"if": "opening_hours=Jul-Aug 08:00-22:00",
|
||||
"then": "Enkel in de <b>zomervakantie</b> en overdag toegankelijk (van 1 juli tot 31 augustus, van 08:00 tot 22:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Wie kan men emailen indien er problemen zijn met de speelzone?",
|
||||
"render": "De bevoegde dienst kan bereikt worden via {email}",
|
||||
"freeform": {
|
||||
"key": "email",
|
||||
"type": "email"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": "Wie kan men bellen indien er problemen zijn met de speelzone?",
|
||||
"render": "De bevoegde dienst kan getelefoneerd worden via {phone}",
|
||||
"freeform": {
|
||||
"key": "phone",
|
||||
"type": "phone"
|
||||
}
|
||||
},
|
||||
"questions",
|
||||
{
|
||||
"render": "{reviews(name, play_forest)}"
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"hideFromOverview": false,
|
||||
"icon": {
|
||||
"render": "./assets/layers/play_forest/icon.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "8"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#2d2"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
"title": "Speelbos",
|
||||
"tags": [
|
||||
"leisure=playground",
|
||||
"playground=forest",
|
||||
"fixme=Toegevoegd met MapComplete, geometry nog uit te tekenen"
|
||||
],
|
||||
"description": "Een zone in het bos, duidelijk gemarkeerd als speelzone met de overeenkomstige borden.<br/><img src='./assets/layers/play_forest/icon.svg'/>"
|
||||
}
|
||||
],
|
||||
"wayHandling": 2
|
||||
}
|
261
assets/layers/playground/playground.json
Normal file
|
@ -0,0 +1,261 @@
|
|||
{
|
||||
"id": "playground",
|
||||
"name": {
|
||||
"nl": "Speeltuinen",
|
||||
"en": "Playgrounds"
|
||||
},
|
||||
"minzoom": 13,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=playground",
|
||||
"playground!=forest"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"nl": "Speeltuinen",
|
||||
"en": "Playgrounds"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Speeltuin",
|
||||
"en": "Playground"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~*",
|
||||
"then": {
|
||||
"nl": "Speeltuin <i>{name}</i>",
|
||||
"en": "Playground <i>{name}</i>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": {
|
||||
"nl": "Wat is de ondergrond van deze speeltuin?<br/><i>Indien er verschillende ondergronden zijn, neem de meest voorkomende</i>",
|
||||
"en": "Which is the surface of this playground?<br/><i>If there are multiple, select the most occuring one</i>"
|
||||
},
|
||||
"render": {
|
||||
"nl": "De ondergrond is <b>{surface}</b>",
|
||||
"en": "The surface is <b>{surface}</b>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "surface"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "surface=grass",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>gras</b>",
|
||||
"en": "The surface is <b>grass</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=sand",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>zand</b>",
|
||||
"en": "The surface is <b>sand</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=paving_stones",
|
||||
"then": {
|
||||
"nl": "De ondergrond bestaat uit <b>stoeptegels</b>",
|
||||
"en": "The surface is <b>paving stones</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=asphalt",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>asfalt</b>",
|
||||
"en": "The surface is <b>asphalt</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=concrete",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>beton</b>",
|
||||
"en": "The surface is <b>concrete</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=unpaved",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>onverhard</b>",
|
||||
"en": "The surface is <b>unpaved</b>"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
},
|
||||
{
|
||||
"if": "surface=paved",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>verhard</b>",
|
||||
"en": "The surface is <b>paved</b>"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Is deze speeltuin 's nachts verlicht?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "lit=yes",
|
||||
"then": "Deze speeltuin is 's nachts verlicht"
|
||||
},
|
||||
{
|
||||
"if": "lit=no",
|
||||
"then": "Deze speeltuin is 's nachts niet verlicht"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
"nl": "Toegankelijk vanaf {min_age} jaar oud"
|
||||
},
|
||||
"question": {
|
||||
"nl": "Wat is de minimale leeftijd om op deze speeltuin te mogen?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "min_age",
|
||||
"type": "pnat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
"nl": "Toegankelijk tot {max_age}"
|
||||
},
|
||||
"question": {
|
||||
"nl": "Wat is de maximaal toegestane leeftijd voor deze speeltuin?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "max_age",
|
||||
"type": "pnat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": "Wie beheert deze speeltuin?",
|
||||
"render": "Beheer door {operator}",
|
||||
"freeform": {
|
||||
"key": "operator"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": "Is deze speeltuin vrij toegankelijk voor het publiek?",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "access=",
|
||||
"then": "Vrij toegankelijk voor het publiek",
|
||||
"hideInAnswer": true
|
||||
},
|
||||
{
|
||||
"if": "access=yes",
|
||||
"then": "Vrij toegankelijk voor het publiek"
|
||||
},
|
||||
{
|
||||
"if": "access=customers",
|
||||
"then": "Enkel toegankelijk voor klanten van de bijhorende zaak"
|
||||
},
|
||||
{
|
||||
"if": "access=students",
|
||||
"then": "Vrij toegankelijk voor scholieren van de school"
|
||||
},
|
||||
{
|
||||
"if": "access=private",
|
||||
"then": "Niet vrij toegankelijk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Wie kan men emailen indien er problemen zijn met de speeltuin?",
|
||||
"render": "De bevoegde dienst kan bereikt worden via {email}",
|
||||
"freeform": {
|
||||
"key": "email",
|
||||
"type": "email"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": "Wie kan men bellen indien er problemen zijn met de speeltuin?",
|
||||
"render": "De bevoegde dienst kan getelefoneerd worden via {phone}",
|
||||
"freeform": {
|
||||
"key": "phone",
|
||||
"type": "phone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Is deze speeltuin toegankelijk voor rolstoelgebruikers?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "wheelchair=yes",
|
||||
"then": {
|
||||
"nl": "Geheel toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "wheelchair=limited",
|
||||
"then": {
|
||||
"nl": "Beperkt toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "wheelchair=no",
|
||||
"then": {
|
||||
"nl": "Niet toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"freeform": {
|
||||
"key": "opening_hours",
|
||||
"type": "opening_hours"
|
||||
},
|
||||
"render": "{opening_hours_table(opening_hours)}",
|
||||
"question": {
|
||||
"nl": "Op welke uren is deze speeltuin toegankelijk?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "opening_hours=sunrise-sunset",
|
||||
"then": {
|
||||
"nl": "Van zonsopgang tot zonsondergang"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions",
|
||||
{
|
||||
"render": "{reviews(name, playground)}"
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "https://upload.wikimedia.org/wikipedia/commons/0/00/Map_icons_by_Scott_de_Jonge_-_playground.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "3"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#0c3"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"leisure=playground"
|
||||
],
|
||||
"title": {
|
||||
"nl": "Speeltuin"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wayHandling": 2
|
||||
}
|
|
@ -256,7 +256,9 @@
|
|||
"de": "Teil des Netzwerks 'Little Free Library'",
|
||||
"fr": "Fait partie du réseau 'Little Free Library'"
|
||||
},
|
||||
"if": "brand=Little Free Library"
|
||||
"if":{
|
||||
"and": ["brand=Little Free Library","nobrand="]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
|
|
178
assets/layers/slow_roads/slow_roads.json
Normal file
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"id": "slow_roads",
|
||||
"name": {
|
||||
"nl": "Trage wegen"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"or": [
|
||||
"highway=pedestrian",
|
||||
"highway=cycleway",
|
||||
"highway=footway",
|
||||
"highway=path",
|
||||
"highway=bridleway",
|
||||
"highway=living_street",
|
||||
"highway=track"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Trage weg"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~*",
|
||||
"then": {
|
||||
"nl": "{name}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "highway=footway",
|
||||
"then": {
|
||||
"nl": "Voetpad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "highway=cycleway",
|
||||
"then": {
|
||||
"nl": "Fietspad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "highway=pedestrian",
|
||||
"then": {
|
||||
"nl": "Voetgangersstraat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "highway=living_street",
|
||||
"then": {
|
||||
"nl": "Woonerf"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": {
|
||||
"nl": "Wat is de wegverharding van dit pad?"
|
||||
},
|
||||
"render": {
|
||||
"nl": "De ondergrond is <b>{surface}</b>",
|
||||
"en": "The surface is <b>{surface}</b>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "surface"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "surface=grass",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>gras</b>",
|
||||
"en": "The surface is <b>grass</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=ground",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>aarde</b>",
|
||||
"en": "The surface is <b>ground</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=unpaved",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>onverhard</b>",
|
||||
"en": "The surface is <b>unpaved</b>"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
},
|
||||
{
|
||||
"if": "surface=sand",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>zand</b>",
|
||||
"en": "The surface is <b>sand</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=paving_stones",
|
||||
"then": {
|
||||
"nl": "De ondergrond bestaat uit <b>stoeptegels</b>",
|
||||
"en": "The surface is <b>paving stones</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=asphalt",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>asfalt</b>",
|
||||
"en": "The surface is <b>asphalt</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=concrete",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>beton</b>",
|
||||
"en": "The surface is <b>concrete</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=paved",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>verhard</b>",
|
||||
"en": "The surface is <b>paved</b>"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Is deze weg 's nachts verlicht?",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "lit=yes",
|
||||
"then": "'s nachts verlicht"
|
||||
},
|
||||
{
|
||||
"if": "lit=no",
|
||||
"then": "Niet verlicht"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/svg/bug.svg",
|
||||
"mappings": []
|
||||
},
|
||||
"width": {
|
||||
"render": "4"
|
||||
},
|
||||
"color": {
|
||||
"render": "#bb2",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "highway=cycleway",
|
||||
"then": "#00c"
|
||||
},
|
||||
{
|
||||
"if": "highway=path",
|
||||
"then": "#bb2"
|
||||
},
|
||||
{
|
||||
"if": "highway=footway",
|
||||
"then": "#c30"
|
||||
},
|
||||
{
|
||||
"if": "highway=pedestrian",
|
||||
"then": "#3c3"
|
||||
},
|
||||
{
|
||||
"if": "highway=living_street",
|
||||
"then": "#ccc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"presets": [
|
||||
]
|
||||
}
|
242
assets/layers/sport_pitch/sport_pitch.json
Normal file
|
@ -0,0 +1,242 @@
|
|||
{
|
||||
"id": "sport_pitch",
|
||||
"name": {
|
||||
"nl": "Sportterrein"
|
||||
},
|
||||
"wayHandling": 2,
|
||||
"minzoom": 12,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=pitch"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Sportterrein"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"nl": "Een sportterrein"
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"render": {
|
||||
"nl": "Hier kan men {sport} beoefenen"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "sport"
|
||||
},
|
||||
"question": "Welke sporten kan men hier beoefenen?",
|
||||
"multiAnswer": true,
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=basketball"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hier kan men basketbal spelen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=soccer"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hier kan men voetbal spelen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=table_tennis"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Dit is een pingpongtafel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=tennis"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hier kan men tennis spelen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=korfball"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hier kan men korfbal spelen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"sport=basket"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Hier kan men basketbal beoefenen"
|
||||
}
|
||||
}
|
||||
]
|
||||
},{
|
||||
"question": {
|
||||
"nl": "Wat is de ondergrond van dit sportveld?",
|
||||
"en": "Which is the surface of this sport pitch?"
|
||||
},
|
||||
"render": {
|
||||
"nl": "De ondergrond is <b>{surface}</b>",
|
||||
"en": "The surface is <b>{surface}</b>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "surface"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "surface=grass",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>gras</b>",
|
||||
"en": "The surface is <b>grass</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=sand",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>zand</b>",
|
||||
"en": "The surface is <b>sand</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=paving_stones",
|
||||
"then": {
|
||||
"nl": "De ondergrond bestaat uit <b>stoeptegels</b>",
|
||||
"en": "The surface is <b>paving stones</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=asphalt",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>asfalt</b>",
|
||||
"en": "The surface is <b>asphalt</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "surface=concrete",
|
||||
"then": {
|
||||
"nl": "De ondergrond is <b>beton</b>",
|
||||
"en": "The surface is <b>concrete</b>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Is dit sportterrein publiek toegankelijk?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "access=public",
|
||||
"then": "Publiek toegankelijk"
|
||||
},
|
||||
{"if": "access=limited",
|
||||
"then": "Beperkt toegankelijk (enkel na reservatie, tijdens bepaalde uren, ...)"
|
||||
},
|
||||
{
|
||||
"if": "access=members",
|
||||
"then": "Enkel toegankelijk voor leden van de bijhorende sportclub"
|
||||
},
|
||||
{
|
||||
"if": "access=private",
|
||||
"then": "Privaat en niet toegankelijk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Moet men reserveren om gebruik te maken van dit sportveld?"
|
||||
},
|
||||
"condition": {
|
||||
"and": [ "access!=public", "access!=private", "access!=members"]},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "reservation=required",
|
||||
"then": "Reserveren is verplicht om gebruik te maken van dit sportterrein"
|
||||
},
|
||||
{
|
||||
"if": "reservation=recommended",
|
||||
"then": "Reserveren is sterk aangeraden om gebruik te maken van dit sportterrein"
|
||||
},
|
||||
{"if": "reservation=yes",
|
||||
"then": "Reserveren is mogelijk, maar geen voorwaarde"
|
||||
},
|
||||
{
|
||||
"if": "reservation=no",
|
||||
"then": "Reserveren is niet mogelijk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Wat is het telefoonnummer van de bevoegde dienst of uitbater?",
|
||||
"freeform": {
|
||||
"key": "phone",
|
||||
"type": "phone"
|
||||
},
|
||||
"render": "<a href='tel:{phone}'>{phone}</a>"
|
||||
},
|
||||
{
|
||||
"question": "Wat is het email-adres van de bevoegde dienst of uitbater?",
|
||||
"freeform": {
|
||||
"key": "email",
|
||||
"type": "email"
|
||||
},
|
||||
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>"
|
||||
},
|
||||
"questions",
|
||||
{"render":"{reviews(name, sportpitch)}"}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "./assets/layers/sport_pitch/tabletennis.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "8"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#00f"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
"title": {
|
||||
"nl": "Ping-pong tafel"
|
||||
},
|
||||
"tags": [
|
||||
"leisure=pitch",
|
||||
"sport=table_tennis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"nl": "Sportterrein"
|
||||
},
|
||||
"tags": [
|
||||
"leisure=pitch",
|
||||
"fixme=Toegevoegd met MapComplete, geometry nog uit te tekenen"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
assets/layers/sport_pitch/tabletennis.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M496.2 296.5C527.7 218.7 512 126.2 449 63.1 365.1-21 229-21 145.1 63.1l-56 56.1 211.5 211.5c46.1-62.1 131.5-77.4 195.6-34.2zm-217.9 79.7L57.9 155.9c-27.3 45.3-21.7 105 17.3 144.1l34.5 34.6L6.7 424c-8.6 7.5-9.1 20.7-1 28.8l53.4 53.5c8 8.1 21.2 7.6 28.7-1L177.1 402l35.7 35.7c19.7 19.7 44.6 30.5 70.3 33.3-7.1-17-11-35.6-11-55.1-.1-13.8 2.5-27 6.2-39.7zM416 320c-53 0-96 43-96 96s43 96 96 96 96-43 96-96-43-96-96-96z"/></svg>
|
||||
<!--
|
||||
Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
-->
|
After Width: | Height: | Size: 669 B |
28
assets/themes/play_forests/play_forests.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"id": "play_forests",
|
||||
"title": {
|
||||
"nl": "Speelbossen"
|
||||
},
|
||||
"shortDescription": {
|
||||
"nl": "Deze kaart toont speelbossen"
|
||||
},
|
||||
"description": {
|
||||
"nl": "Een speelbos is een zone in een bos die vrij toegankelijk is voor spelende kinderen. Deze wordt in bossen van het Agentschap Natuur en bos altijd aangeduid met het overeenkomstige bord."
|
||||
},
|
||||
"language": [
|
||||
"nl"
|
||||
],
|
||||
"maintainer": "",
|
||||
"icon": "./assets/layers/play_forest/icon.svg",
|
||||
"version": "0",
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"hideInOverview": true,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "",
|
||||
"layers": [
|
||||
"play_forest"
|
||||
],
|
||||
"roamingRenderings": []
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"id": "playgrounds",
|
||||
"title": {
|
||||
"nl": "Speelzones"
|
||||
"nl": "Speelplekken"
|
||||
},
|
||||
"shortDescription": {
|
||||
"nl": "Speelzones en speeltuinen"
|
||||
"nl": "Speelplekken en sportplekken"
|
||||
},
|
||||
"description": {
|
||||
"nl": "Deze kaart toont speelzones in het groen"
|
||||
"nl": "Op deze kaart vind je speelplekken zoals speeltuinen, speelbossen en sportterreinen"
|
||||
},
|
||||
"language": [
|
||||
"nl"
|
||||
|
@ -17,157 +17,11 @@
|
|||
"version": "0",
|
||||
"startLat": 50.535,
|
||||
"startLon": 4.399,
|
||||
"startZoom": 10,
|
||||
"startZoom": 13,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "",
|
||||
"hideFromOverview": true,
|
||||
"layers": [
|
||||
{
|
||||
"id": "playgrounds",
|
||||
"name": {
|
||||
"nl": "Speeltuinen"
|
||||
},
|
||||
"minzoom": 14,
|
||||
"overpassTags": {
|
||||
"and": [
|
||||
"leisure=playground"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"nl": "Speeltuin"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"name~*"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Speeltuin <i>{name}</i>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"nl": "Alle speeltuinen"
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": {
|
||||
"nl": "Is deze speeltuin toegankelijk voor rolstoelgebruikers?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"wheelchair=yes"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Geheel toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"wheelchair=limited"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Beperkt toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"wheelchair=no"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Niet toegankelijk voor rolstoelgebruikers"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"freeform": {
|
||||
"key": "opening_hours",
|
||||
"type": "opening_hours"
|
||||
},
|
||||
"render": "{opening_hours_table(opening_hours)}",
|
||||
"question": {
|
||||
"nl": "Op welke uren is deze speeltuin toegankelijk?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"and": [
|
||||
"opening_hours=sunrise-sunset"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"nl": "Van zonsopgang tot zonsondergang"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
"nl": "Toegankelijk vanaf {min_age} jaar oud"
|
||||
},
|
||||
"question": {
|
||||
"nl": "Wat is de minimale leeftijd om op deze speeltuin te mogen?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "min_age",
|
||||
"type": "pnat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
"nl": "Toegankelijk tot {max_age}"
|
||||
},
|
||||
"question": {
|
||||
"nl": "Wat is de maximaal toegestane leeftijd voor deze speeltuin?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "max_age",
|
||||
"type": "pnat"
|
||||
}
|
||||
},
|
||||
"questions",
|
||||
{
|
||||
"render": "{reviews(leisure=playground)}"
|
||||
}
|
||||
],
|
||||
"hideUnderlayingFeaturesMinPercentage": 0,
|
||||
"icon": {
|
||||
"render": "https://upload.wikimedia.org/wikipedia/commons/0/00/Map_icons_by_Scott_de_Jonge_-_playground.svg"
|
||||
},
|
||||
"width": {
|
||||
"render": "3"
|
||||
},
|
||||
"iconSize": {
|
||||
"render": "40,40,center"
|
||||
},
|
||||
"color": {
|
||||
"render": "#0c3"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"leisure=playground"
|
||||
],
|
||||
"title": {
|
||||
"nl": "Speeltuin"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wayHandling": 2
|
||||
}
|
||||
"playground"
|
||||
],
|
||||
"roamingRenderings": []
|
||||
}
|
35
assets/themes/speelplekken/speelplekken.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"id": "speelplekken",
|
||||
"title": {
|
||||
"nl": "Speelplekken in de Antwerpse Zuidrand"
|
||||
},
|
||||
"shortDescription": {
|
||||
"nl": "Speelplekken in de Antwerpse Zuidrand"
|
||||
},
|
||||
"description": {
|
||||
"nl": "Speelplekken in de Antwerpse Zuidrand. Een project van Provincie Antwerpen, in samenwerking met Createlli, Sportpret en OpenStreetMap België"
|
||||
},
|
||||
"language": [
|
||||
"nl"
|
||||
],
|
||||
"maintainer": "MapComplete",
|
||||
"icon": "./assets/layers/play_forest/icon.svg",
|
||||
"hideInOverview": true,
|
||||
"lockLocation": true,
|
||||
"version": "0",
|
||||
"startLat": 51.17174,
|
||||
"startLon": 4.449462,
|
||||
"startZoom": 12,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "",
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"layers": [
|
||||
"play_forest",
|
||||
"playground",
|
||||
"sport_pitch",
|
||||
"slow_roads",
|
||||
"drinking_water",
|
||||
"toilets"
|
||||
],
|
||||
"roamingRenderings": []
|
||||
}
|
27
assets/themes/sport_pitches/sport_pitches.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"id": "sport_pitches",
|
||||
"title": {
|
||||
"nl": "Sportvelden"
|
||||
},
|
||||
"shortDescription": {
|
||||
"nl": "Deze kaart toont sportvelden"
|
||||
},
|
||||
"description": {
|
||||
"nl": "Een sportveld is een ingerichte plaats met infrastructuur om een sport te beoefenen"
|
||||
},
|
||||
"language": [
|
||||
"nl"
|
||||
],
|
||||
"maintainer": "",
|
||||
"icon": "./assets/layers/sport_pitch/tabletennis.svg",
|
||||
"version": "0",
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 0.05,
|
||||
"socialImage": "",
|
||||
"layers": [
|
||||
"sport_pitch"
|
||||
],
|
||||
"roamingRenderings": []
|
||||
}
|
|
@ -1,70 +1,3 @@
|
|||
.review {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.review-title {
|
||||
font-size: x-large;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.review-title img {
|
||||
max-width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
.review-rating img {
|
||||
max-width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.review-by-current-user {
|
||||
border: 5px solid var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.review-rating {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 5em;
|
||||
margin-right: 0.5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-date {
|
||||
color: var(--subtle-detail-color-light-contrast);
|
||||
}
|
||||
|
||||
.review-stars-comment {
|
||||
display: flex;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.review-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.review-author-date {
|
||||
display: flex;
|
||||
margin-bottom: 0.5em;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
.review-element {
|
||||
padding: 1em;
|
||||
margin: 0.5em;
|
||||
display: block;
|
||||
border-radius: 1em;
|
||||
background-color: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.review-attribution {
|
||||
display: flex;
|
||||
|
|
|
@ -110,7 +110,15 @@ a {
|
|||
|
||||
.subtle-background {
|
||||
background: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.subtle-lighter {
|
||||
color: var(--subtle-detail-color-light-contrast);
|
||||
}
|
||||
|
||||
.border-attention-catch{ border: 5px solid var(--catch-detail-color);}
|
||||
|
||||
.slick-prev:before, .slick-next:before {
|
||||
/*Slideshow workaround*/
|
||||
color:black !important;
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<link rel="stylesheet" href="vendor/MarkerCluster.Default.css"/>
|
||||
<!-- $$$CUSTOM-CSS -->
|
||||
<!-- $$$MANIFEST -->
|
||||
<link rel="manifest" href="./index.webmanifest">
|
||||
<link rel="manifest" href="./index.manifest">
|
||||
|
||||
<link rel="icon" href="assets/svg/add.svg" sizes="any" type="image/svg+xml">
|
||||
|
||||
|
|
66
index.manifest
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "index",
|
||||
"short_name": "M",
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"description": "M",
|
||||
"orientation": "portrait-primary, landscape-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/generated/svg_mapcomplete_logo512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/svg/mapcomplete_logo.svg",
|
||||
"sizes": "513x513",
|
||||
"type": "image/svg"
|
||||
}
|
||||
]
|
||||
}
|
49
package-lock.json
generated
|
@ -5686,6 +5686,11 @@
|
|||
"domelementtype": "1"
|
||||
}
|
||||
},
|
||||
"domino": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
|
||||
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||
|
@ -5763,23 +5768,23 @@
|
|||
"integrity": "sha512-2fvco0F2bBIgqzO8GRP0Jt/91pdrf9KfZ5FsmkYkjERmIJG585cFeFZV4+CO6oTmU3HmCTgfcZuEa7kW8VUh3A=="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
"brorand": "^1.0.1",
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
"hash.js": "^1.0.0",
|
||||
"hmac-drbg": "^1.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"minimalistic-crypto-utils": "^1.0.0"
|
||||
"hmac-drbg": "^1.0.1",
|
||||
"inherits": "^2.0.4",
|
||||
"minimalistic-assert": "^1.0.1",
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -11176,9 +11181,9 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
|
||||
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
|
||||
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
|
@ -11206,9 +11211,9 @@
|
|||
"integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"postcss-selector-parser": {
|
||||
"version": "6.0.4",
|
||||
|
@ -12114,6 +12119,14 @@
|
|||
"turf-inside": "^3.0.12"
|
||||
}
|
||||
},
|
||||
"turndown": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.0.0.tgz",
|
||||
"integrity": "sha512-G1FfxfR0mUNMeGjszLYl3kxtopC4O9DRRiMlMDDVHvU1jaBkGFg4qxIyjIk2aiKLHyDyZvZyu4qBO2guuYBy3Q==",
|
||||
"requires": {
|
||||
"domino": "^2.1.6"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"build": "rm -rf dist/ && npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
|
||||
"prepare-deploy": "npm run test && npm run generate:editor-layer-index && npm run generate:layouts && npm run generate && npm run build && rm -rf .cache",
|
||||
"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",
|
||||
"deploy:pietervdvn": "npm run prepare-deploy && rm -rf /home/pietervdvn/git/pietervdvn.github.io/MapComplete/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/MapComplete/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean",
|
||||
"deploy:production": "rm -rf ./assets/generated && npm run prepare-deploy && npm run optimize-images && rm -rf /home/pietervdvn/git/mapcomplete.github.io/* && cp -r dist/* /home/pietervdvn/git/mapcomplete.github.io/ && cd /home/pietervdvn/git/mapcomplete.github.io/ && echo \"mapcomplete.osm.be\" > CNAME && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean",
|
||||
"clean": "rm -rf .cache/ && (find *.html | grep -v \"\\(index\\|land\\|test\\|preferences\\|customGenerator\\).html\" | xargs rm) && rm *.webmanifest"
|
||||
},
|
||||
|
@ -68,6 +69,7 @@
|
|||
"read-file": "^0.2.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"ts-node-dev": "^1.0.0-pre.63",
|
||||
"turndown": "^7.0.0",
|
||||
"typescript": "^3.9.7",
|
||||
"write-file": "^1.0.0"
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ async function createLandingPage(layout: LayoutConfig) {
|
|||
}
|
||||
|
||||
let output = template
|
||||
.replace("./index.webmanifest", `${enc(layout.id)}.webmanifest`)
|
||||
.replace("./index.manifest", `${enc(layout.id)}.webmanifest`)
|
||||
.replace("<!-- $$$OG-META -->", og)
|
||||
.replace(/<title>.+?<\/title>/, `<title>${ogTitle}</title>`)
|
||||
.replace("Loading MapComplete, hang on...", `Loading MapComplete theme <i>${ogTitle}</i>...`)
|
||||
|
@ -294,7 +294,7 @@ writeFile(generatedDir + "/wikiIndex", wikiPage, (err) => {
|
|||
description:"MapComplete as a map viewer and editor which show thematic POI based on OpenStreetMap"
|
||||
}),"").then(manifObj => {
|
||||
const manif = JSON.stringify(manifObj, undefined, 2);
|
||||
writeFileSync("index.webmanifest",manif)
|
||||
writeFileSync("index.manifest",manif)
|
||||
})
|
||||
|
||||
console.log("Counting all translations")
|
||||
|
|
11
scripts/generateDocs.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {Utils} from "../Utils";
|
||||
Utils.runningFromConsole = true;
|
||||
import SpecialVisualizations from "../UI/SpecialVisualizations";
|
||||
import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs";
|
||||
|
||||
|
||||
const html = SpecialVisualizations.HelpMessage.InnerRender();
|
||||
var TurndownService = require('turndown')
|
||||
const md = new TurndownService().turndown(html);
|
||||
writeFileSync("./Docs/SpecialRenderings.md", md)
|
||||
console.log("Generated docs")
|
|
@ -27,6 +27,26 @@ new T("Tags", [
|
|||
const tag = FromJSON.Tag("key=value") as Tag;
|
||||
equal(tag.key, "key");
|
||||
equal(tag.value, "value");
|
||||
equal(tag.matches([{k:"key",v:"value"}]), true)
|
||||
equal(tag.matches([{k:"key",v:"z"}]), false)
|
||||
equal(tag.matches([{k:"key",v:""}]), false)
|
||||
equal(tag.matches([{k:"other_key",v:""}]), false)
|
||||
equal(tag.matches([{k:"other_key",v:"value"}]), false)
|
||||
|
||||
const isEmpty = FromJSON.Tag("key=") as Tag;
|
||||
equal(isEmpty.matches([{k:"key",v:"value"}]), false)
|
||||
equal(isEmpty.matches([{k:"key",v:""}]), true)
|
||||
equal(isEmpty.matches([{k:"other_key",v:""}]), true)
|
||||
equal(isEmpty.matches([{k:"other_key",v:"value"}]), true)
|
||||
|
||||
const isNotEmpty = FromJSON.Tag("key!=");
|
||||
equal(isNotEmpty.matches([{k:"key",v:"value"}]), true)
|
||||
equal(isNotEmpty.matches([{k:"key",v:"other_value"}]), true)
|
||||
equal(isNotEmpty.matches([{k:"key",v:""}]), false)
|
||||
equal(isNotEmpty.matches([{k:"other_key",v:""}]), false)
|
||||
equal(isNotEmpty.matches([{k:"other_key",v:"value"}]), false)
|
||||
|
||||
|
||||
|
||||
const and = FromJSON.Tag({"and": ["key=value", "x=y"]}) as And;
|
||||
equal((and.and[0] as Tag).key, "key");
|
||||
|
@ -37,10 +57,8 @@ new T("Tags", [
|
|||
equal(notReg.matches([{k:"x",v:"y"}]), false)
|
||||
equal(notReg.matches([{k:"x",v:"z"}]), true)
|
||||
equal(notReg.matches([{k:"x",v:""}]), true)
|
||||
|
||||
equal(notReg.matches([]), true)
|
||||
|
||||
|
||||
const noMatch = FromJSON.Tag("key!=value") as Tag;
|
||||
equal(noMatch.matches([{k:"key",v:"value"}]), false)
|
||||
equal(noMatch.matches([{k:"key",v:"otherValue"}]), true)
|
||||
|
@ -54,6 +72,12 @@ new T("Tags", [
|
|||
equal(multiMatch.matches([{k:"vending",v:"bicycle_tube;something"}]), true)
|
||||
equal(multiMatch.matches([{k:"vending",v:"xyz;bicycle_tube;something"}]), true)
|
||||
|
||||
const nameStartsWith = FromJSON.Tag("name~[sS]peelbos.*")
|
||||
equal(nameStartsWith.matches([{k:"name",v: "Speelbos Sint-Anna"}]), true)
|
||||
equal(nameStartsWith.matches([{k:"name",v: "speelbos Sint-Anna"}]), true)
|
||||
equal(nameStartsWith.matches([{k:"name",v: "Sint-Anna"}]), false)
|
||||
equal(nameStartsWith.matches([{k:"name",v: ""}]), false)
|
||||
|
||||
})],
|
||||
["Is equivalent test", (() => {
|
||||
|
||||
|
|