Merge branch 'master' into master

This commit is contained in:
Pieter Vander Vennet 2021-03-15 15:57:45 +01:00 committed by GitHub
commit 3bfa3dbfc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 4967 additions and 542 deletions

3
.gitignore vendored
View file

@ -9,3 +9,6 @@ assets/generated/*
/*.html
!/index.html
.parcel-cache
Docs/Tools/stats.*.json
Docs/Tools/stats.csv

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

BIN
Docs/Tools/Contributors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

6
Docs/Tools/compileStats.sh Executable file
View file

@ -0,0 +1,6 @@
#! /bin/bash
./fetchStats.sh
./csvPerChange.sh
python csvGrapher.py

318
Docs/Tools/csvGrapher.py Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ export default class LazyElement extends UIElement {
}
self.Update();
// @ts-ignore
if(this._content.Activate){
if (this._content.Activate) {
// THis is ugly - I know
// @ts-ignore
this._content.Activate();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ export default class SpecialVisualizations {
public static specialVisualizations: {
funcName: string,
constr: ((state: State,tagSource: UIEventSource<any>, argument: string[]) => UIElement),
constr: ((state: State, tagSource: UIEventSource<any>, argument: string[]) => UIElement),
docs: string,
example?: string,
args: { name: string, defaultValue?: string, doc: string }[]
@ -32,7 +32,7 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: ((state: State,tags: UIEventSource<any>) => {
constr: ((state: State, tags: UIEventSource<any>) => {
return new VariableUiElement(tags.map(tags => {
const parts = [];
for (const key in tags) {
@ -56,10 +56,10 @@ export default class SpecialVisualizations {
defaultValue: "true",
doc: "Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary"
}],
constr: (state: State,tags, args) => {
constr: (state: State, tags, args) => {
const imagePrefix = args[0];
const loadSpecial = args[1].toLowerCase() === "true";
const searcher : UIEventSource<{ key: string, url: string }[]> = new ImageSearcher(tags, imagePrefix, loadSpecial);
const searcher: UIEventSource<{ key: string, url: string }[]> = new ImageSearcher(tags, imagePrefix, loadSpecial);
return new ImageCarousel(searcher, tags);
}
@ -73,25 +73,28 @@ export default class SpecialVisualizations {
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "image"
}],
constr: (state: State,tags, args) => {
constr: (state: State, tags, args) => {
return new ImageUploadFlow(tags, args[0])
}
},
{
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) => {
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),
@ -111,7 +114,7 @@ export default class SpecialVisualizations {
defaultValue: "opening_hours",
doc: "The tagkey from which the table is constructed."
}],
constr: (state: State,tagSource: UIEventSource<any>, args) => {
constr: (state: State, tagSource: UIEventSource<any>, args) => {
let keyname = args[0];
if (keyname === undefined || keyname === "") {
keyname = keyname ?? "opening_hours"
@ -132,7 +135,7 @@ export default class SpecialVisualizations {
}, {
name: "path", doc: "The path (or shorthand) that should be returned"
}],
constr: (state: State,tagSource: UIEventSource<any>, args) => {
constr: (state: State, tagSource: UIEventSource<any>, args) => {
const url = args[0];
const shorthands = args[1];
const neededValue = args[2];
@ -147,10 +150,10 @@ 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) => {
constr: (state: State, tagSource: UIEventSource<any>, args) => {
if (window.navigator.share) {
const title = state.layoutToUse.data.title.txt;
let name = tagSource.data.name;
@ -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
]

View file

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

View file

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

View file

@ -46,7 +46,7 @@ export class Utils {
}
public static Round(i: number) {
if(i < 0){
if (i < 0) {
return "-" + Utils.Round(-i);
}
const j = "" + Math.floor(i * 10);
@ -87,7 +87,7 @@ export class Utils {
return ls;
}
public static NoEmpty(array: string[]): string[]{
public static NoEmpty(array: string[]): string[] {
const ls: string[] = [];
for (const t of array) {
if (t === "") {
@ -98,18 +98,18 @@ export class Utils {
return ls;
}
public static EllipsesAfter(str : string, l : number = 100){
if(str === undefined){
public static EllipsesAfter(str: string, l: number = 100) {
if (str === undefined) {
return undefined;
}
if(str.length <= l){
if (str.length <= l) {
return str;
}
return str.substr(0, l - 3)+"...";
return str.substr(0, l - 3) + "...";
}
public static Dedup(arr: string[]):string[]{
if(arr === undefined){
public static Dedup(arr: string[]): string[] {
if (arr === undefined) {
return undefined;
}
const newArr = [];
@ -141,7 +141,7 @@ export class Utils {
}
// Date will be undefined on failure
public static LoadCustomCss(location: string){
public static LoadCustomCss(location: string) {
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.id = "customCss";
@ -150,26 +150,72 @@ export class Utils {
link.href = location;
link.media = 'all';
head.appendChild(link);
console.log("Added custom layout ",location)
console.log("Added custom layout ", location)
}
static Merge(source: any, target: any){
static Merge(source: any, target: any) {
target = JSON.parse(JSON.stringify(target));
source = JSON.parse(JSON.stringify(source));
for (const key in source) {
const sourceV = source[key];
const targetV = target[key]
if(typeof sourceV === "object"){
if(targetV === undefined){
if (typeof sourceV === "object") {
if (targetV === undefined) {
target[key] = sourceV;
}else{
} else {
Utils.Merge(sourceV, targetV);
}
}else{
} else {
target[key] = sourceV;
}
}
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)));
}
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

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

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

View file

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

View 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": [
]
}

View 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"
]
}
]
}

View 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

View 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": []
}

View file

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

View 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": []
}

View 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": []
}

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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", (() => {