Merge branch 'develop' into feature/doctor

This commit is contained in:
Pieter Vander Vennet 2022-07-07 09:31:01 +02:00 committed by GitHub
commit ccb51e2175
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1625 additions and 241 deletions

14
.gitignore vendored
View file

@ -2,7 +2,6 @@ dist/*
node_modules
.cache/*
.idea/*
.vscode/*
scratch
assets/editor-layer-index.json
assets/generated/*
@ -24,3 +23,16 @@ index_*.ts
.~lock.*
*.doctest.ts
service-worker.js
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix

14
.gitpod.yml Normal file
View file

@ -0,0 +1,14 @@
tasks:
- init: npm run init
command: npm run start
ports:
- name: MapComplete Website
port: 1234
onOpen: open-browser
vscode:
extensions:
- "esbenp.prettier-vscode"
- "eamodio.gitlens",
- "GitHub.vscode-pull-request-github"

7
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"eamodio.gitlens",
"GitHub.vscode-pull-request-github"
]
}

21
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,21 @@
{
"json.schemas": [
{
"fileMatch": [
"/assets/layers/*/*.json",
"!/assets/layers/*/license_info.json"
],
"url": "./Docs/Schemas/LayerConfigJson.schema.json"
},
{
"fileMatch": [
"/assets/themes/*/*.json",
"!/assets/themes/*/license_info.json"
],
"url": "./Docs/Schemas/LayoutConfigJson.schema.json"
}
],
"editor.tabSize": 2,
"files.autoSave": "onFocusChange",
"search.useIgnoreFiles": true
}

14
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"path": "/",
"group": "build",
"problemMatcher": [],
"label": "MapComplete Dev",
"detail": "Run MapComplete Dev Server"
}
]
}

View file

@ -73,6 +73,11 @@ To use the WSL in Visual Studio Code:
To use WSL without Visual Studio Code you can replace steps 7 and 8 by opening up a WSL terminal
On mac
------
Install the `Command line tools for XCode which you can find [here](https://developer.apple.com/download/all/). You might need an apple dev account for this.
Automatic deployment
--------------------

View file

@ -39,6 +39,14 @@ export abstract class Conversion<TIn, TOut> {
return DesugaringStep.strict(fixed)
}
public convertJoin(json: TIn, context: string, errors: string[], warnings?: string[], information?: string[]): TOut {
const fixed = this.convert(json, context)
errors?.push(...(fixed.errors ?? []))
warnings?.push(...(fixed.warnings ?? []))
information?.push(...(fixed.information ?? []))
return fixed.result
}
public andThenF<X>(f: (tout:TOut) => X ): Conversion<TIn, X>{
return new Pipe(
this,

View file

@ -45,19 +45,67 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
}
}
export class DoesImageExist extends DesugaringStep<string> {
private readonly _knownImagePaths: Set<string>;
private readonly doesPathExist: (path: string) => boolean = undefined;
constructor(knownImagePaths: Set<string>, checkExistsSync: (path: string) => boolean = undefined) {
super("Checks if an image exists", [], "DoesImageExist");
this._knownImagePaths = knownImagePaths;
this.doesPathExist = checkExistsSync;
}
convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
const errors = []
const warnings = []
const information = []
if (image.indexOf("{") >= 0) {
information.push("Ignoring image with { in the path: " + image)
return {result: image}
}
if (image === "assets/SocialImage.png") {
return {result: image}
}
if (image.match(/[a-z]*/)) {
if (Svg.All[image + ".svg"] !== undefined) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
return {result: image};
}
}
if (this._knownImagePaths !== undefined && !this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) {
errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`)
} else if (!this.doesPathExist(image)) {
errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`)
} else {
errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`)
}
}
return {
result: image,
errors, warnings, information
}
}
}
class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
*/
private readonly _path?: string;
private readonly knownImagePaths: Set<string>;
private readonly _isBuiltin: boolean;
private _sharedTagRenderings: Map<string, any>;
private readonly _validateImage: DesugaringStep<string>;
constructor(knownImagePaths: Set<string>, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme");
this.knownImagePaths = knownImagePaths;
this._validateImage = doesImageExist;
this._path = path;
this._isBuiltin = isBuiltin;
this._sharedTagRenderings = sharedTagRenderings;
@ -89,26 +137,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.")
}
for (const image of images) {
if (image.indexOf("{") >= 0) {
information.push("Ignoring image with { in the path: " + image)
continue
}
if (image === "assets/SocialImage.png") {
continue
}
if (image.match(/[a-z]*/)) {
if (Svg.All[image + ".svg"] !== undefined) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
continue;// =>
}
}
if (this.knownImagePaths !== undefined && !this.knownImagePaths.has(image)) {
const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}`
errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}`)
}
this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information)
}
if (json.icon.endsWith(".svg")) {
@ -150,9 +179,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
if (theme.id !== filename) {
errors.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + this._path + ")")
}
if (!this.knownImagePaths.has(theme.icon)) {
errors.push("The theme image " + theme.icon + " is not attributed or not saved locally")
}
this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information);
const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"]))
if (dups.length > 0) {
errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`)
@ -166,16 +193,16 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
const targetLanguage = theme.title.SupportedLanguages()[0]
if(targetLanguage !== "en"){
if (targetLanguage !== "en") {
warnings.push(`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`)
}
// Official, public themes must have a full english translation
const checked = new ValidateLanguageCompleteness("en")
.convert(theme, theme.id)
errors.push(...checked.errors)
}
} catch (e) {
@ -192,10 +219,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
constructor(knownImagePaths: Set<string>, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map<string, any>) {
super("Validates a theme and the contained layers",
new ValidateTheme(knownImagePaths, path, isBuiltin, sharedTagRenderings),
new On("layers", new Each(new ValidateLayer(undefined, false, knownImagePaths)))
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
new On("layers", new Each(new ValidateLayer(undefined, false, doesImageExist)))
);
}
}
@ -354,7 +381,7 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
})
for (let j = 0; j < i; j++) {
const doesMatch = parsedConditions[j].matchesProperties(properties)
if(doesMatch && json.mappings[j].hideInAnswer === true && json.mappings[i].hideInAnswer !== true){
if (doesMatch && json.mappings[j].hideInAnswer === true && json.mappings[i].hideInAnswer !== true) {
warnings.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`)
} else if (doesMatch) {
// The current mapping is shadowed!
@ -385,14 +412,15 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
}
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
private knownImagePaths: Set<string>;
constructor(knownImagePaths: Set<string>) {
private readonly _doesImageExist: DoesImageExist;
constructor(doesImageExist: DoesImageExist) {
super("Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", [], "DetectMappingsWithImages");
this.knownImagePaths = knownImagePaths;
this._doesImageExist = doesImageExist;
}
/**
* const r = new DetectMappingsWithImages(new Set<string>()).convert({
* const r = new DetectMappingsWithImages(new DoesImageExist(new Set<string>())).convert({
* "mappings": [
* {
* "if": "bicycle_parking=stands",
@ -412,9 +440,9 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } {
const errors = []
const warnings = []
const information = []
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
if (json.mappings === undefined || json.mappings.length === 0) {
return {result: json}
}
@ -432,12 +460,10 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
information.push(`${ctx}: Ignored image ${images.join(", ")} in 'then'-clause of a mapping as this check has been disabled`)
for (const image of images) {
if (this.knownImagePaths !== undefined && !this.knownImagePaths.has(image)) {
const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}`
errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}`)
}
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information);
}
}
} else if (ignore) {
warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`)
@ -454,10 +480,10 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
}
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
constructor(layerConfig?: LayerConfigJson, knownImagePaths?: Set<string>) {
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
super("Various validation on tagRenderingConfigs",
new DetectShadowedMappings(layerConfig),
new DetectMappingsWithImages(knownImagePaths)
new DetectMappingsWithImages(doesImageExist)
);
}
}
@ -469,13 +495,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
*/
private readonly _path?: string;
private readonly _isBuiltin: boolean;
private knownImagePaths: Set<string> | undefined;
private readonly _doesImageExist: DoesImageExist;
constructor(path: string, isBuiltin: boolean, knownImagePaths: Set<string>) {
constructor(path: string, isBuiltin: boolean, doesImageExist: DoesImageExist) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer");
this._path = path;
this._isBuiltin = isBuiltin;
this.knownImagePaths = knownImagePaths
this._doesImageExist = doesImageExist
}
convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } {
@ -563,7 +589,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
if (json.tagRenderings !== undefined) {
const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this.knownImagePaths))).convert(json, context)
const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this._doesImageExist))).convert(json, context)
warnings.push(...(r.warnings ?? []))
errors.push(...(r.errors ?? []))
information.push(...(r.information ?? []))

View file

@ -31,6 +31,7 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement";
export default class LayerConfig extends WithContextLoader {
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const;
public readonly id: string;
public readonly name: Translation;
public readonly description: Translation;
@ -44,10 +45,8 @@ export default class LayerConfig extends WithContextLoader {
public readonly maxzoom: number;
public readonly title?: TagRenderingConfig;
public readonly titleIcons: TagRenderingConfig[];
public readonly mapRendering: PointRenderingConfig[]
public readonly lineRendering: LineRenderingConfig[]
public readonly units: Unit[];
public readonly deletion: DeleteConfig | null;
public readonly allowMove: MoveConfig | null
@ -57,15 +56,11 @@ export default class LayerConfig extends WithContextLoader {
* In seconds
*/
public readonly maxAgeOfCache: number
public readonly presets: PresetConfig[];
public readonly tagRenderings: TagRenderingConfig[];
public readonly filters: FilterConfig[];
public readonly filterIsSameAs: string;
public readonly forceLoad: boolean;
public static readonly syncSelectionAllowed = ["no" , "local" , "theme-only" , "global"] as const;
public readonly syncSelection: (typeof LayerConfig.syncSelectionAllowed)[number] // this is a trick to conver a constant array of strings into a type union of these values
constructor(
@ -74,18 +69,24 @@ export default class LayerConfig extends WithContextLoader {
official: boolean = true
) {
context = context + "." + json.id;
const translationContext = "layers:"+json.id
const translationContext = "layers:" + json.id
super(json, context)
this.id = json.id;
if (typeof json === "string") {
throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})`
}
if (json.id === undefined) {
throw "Not a valid layer: id is undefined: " + JSON.stringify(json)
throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})`
}
if (json.source === undefined) {
throw "Layer " + this.id + " does not define a source section (" + context + ")"
}
if (json.source.osmTags === undefined) {
throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")"
}
@ -98,8 +99,8 @@ export default class LayerConfig extends WithContextLoader {
}
this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30
if(json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0){
throw context+ " Invalid sync-selection: must be one of "+LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ")+" but got '"+json.syncSelection+"'"
if (json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0) {
throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'"
}
this.syncSelection = json.syncSelection ?? "no";
const osmTags = TagUtils.Tag(
@ -107,10 +108,10 @@ export default class LayerConfig extends WithContextLoader {
context + "source.osmTags"
);
if(Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()){
throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t"+osmTags.asHumanString(false, false, {});
if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) {
throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + osmTags.asHumanString(false, false, {});
}
if (json.source["geoJsonSource"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geoJsonSource'";
}
@ -118,7 +119,7 @@ export default class LayerConfig extends WithContextLoader {
if (json.source["geojson"] !== undefined) {
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)";
}
this.source = new SourceConfig(
{
@ -138,8 +139,8 @@ export default class LayerConfig extends WithContextLoader {
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, translationContext + ".name");
if(json.units!==undefined && !Array.isArray(json.units)){
throw "At "+context+".units: the 'units'-section should be a list; you probably have an object there"
if (json.units !== undefined && !Array.isArray(json.units)) {
throw "At " + context + ".units: the 'units'-section should be a list; you probably have an object there"
}
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
@ -167,8 +168,8 @@ export default class LayerConfig extends WithContextLoader {
const index = kv.indexOf("=");
let key = kv.substring(0, index).trim();
const r = "[a-z_][a-z0-9:]*"
if(key.match(r) === null){
throw "At "+context+" invalid key for calculated tag: "+key+"; it should match "+r
if (key.match(r) === null) {
throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r
}
const isStrict = key.endsWith(':')
if (isStrict) {
@ -343,14 +344,14 @@ export default class LayerConfig extends WithContextLoader {
}
public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map<string, string[]>, dependencies: {
context?: string;
reason: string;
neededLayer: string;
}[] = []
, addedByDefault = false, canBeIncluded = true): BaseUIElement {
context?: string;
reason: string;
neededLayer: string;
}[] = []
, addedByDefault = false, canBeIncluded = true): BaseUIElement {
const extraProps = []
extraProps.push("This layer is shown at zoomlevel **"+this.minzoom+"** and higher")
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
if (canBeIncluded) {
if (addedByDefault) {
@ -440,7 +441,7 @@ export default class LayerConfig extends WithContextLoader {
let overpassLink: BaseUIElement = undefined;
if (Constants.priviliged_layers.indexOf(this.id) < 0) {
try {
overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(<TagsFilter> new And(neededTags).optimize()))
overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(<TagsFilter>new And(neededTags).optimize()))
} catch (e) {
console.error("Could not generate overpasslink for " + this.id)
}

View file

@ -352,7 +352,7 @@ export default class TagRenderingConfig {
}
if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) {
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. If a contributor were to pick this as an option, MapComplete wouldn't be able to determine which tags to add.\n Either change it or set 'hideInAnswer'`
}
}

View file

@ -0,0 +1,12 @@
[
{
"path": "shelter.svg",
"license": "MIT",
"authors": [
"Diemen Design"
],
"sources": [
"https://icon-icons.com/icon/map-shelter/158301"
]
}
]

View file

@ -0,0 +1,88 @@
{
"id": "shelter",
"name": {
"en": "Shelter"
},
"description": {
"en": "Layer showing shelter structures"
},
"source": {
"osmTags": {
"and": [
"amenity=shelter"
]
}
},
"minzoom": 13,
"title": {
"render": {
"en": "Shelter"
}
},
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": "./assets/layers/shelter/shelter.svg"
}
],
"tagRenderings": [
{
"id": "shelter-type",
"mappings": [
{
"if": "shelter_type=public_transport",
"then": {
"en": "This is a shelter at a public transport stop."
}
},
{
"if": "shelter_type=picnic_shelter",
"then": {
"en": "This is a shelter protecting from rain at a picnic site."
}
},
{
"if": "shelter_type=gazebo",
"then": {
"en": "This is a gazebo."
}
},
{
"if": "shelter_type=weather_shelter",
"then": {
"en": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads."
}
},
{
"if": "shelter_type=lean_to",
"then": {
"en": "This is a shed with 3 walls, primarily intended for camping."
}
},
{
"if": "shelter_type=pavilion",
"then": {
"en": "This is a pavilion"
}
},
{
"if": "shelter_type=basic_hut",
"then": "This is a basic hut, providing basic shelter and sleeping facilities."
}
],
"question": {
"en": "What kind of shelter is this?"
},
"render": {
"en": "Shelter type: {shelter_type}"
},
"freeform": {
"key": "shelter_type",
"type": "string"
}
}
]
}

View file

@ -0,0 +1 @@
<svg role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="M 4.4352679,0.99999571 3.578125,2.7143027 l 0.4017857,0 0.8973214,-1.71430699 -0.4419642,0 z m 3.4285714,0 -0.8571429,1.71430699 0.4017857,0 0.8973215,-1.71430699 -0.4419643,0 z m 3.4285707,0 -0.857143,1.71430699 0.401786,0 0.897322,-1.71430699 -0.441965,0 z M 1.8571429,2.7143027 1,4.4286098 l 0.4017857,0 0.8989955,-1.7143071 -0.4436383,0 z m 3.4352678,0 -0.8571428,1.7143071 0.4017857,0 0.8973214,-1.7143071 -0.4419643,0 z m 3.4285714,0 -0.8571428,1.7143071 0.4017857,0 0.8973214,-1.7143071 -0.4419643,0 z m 3.4285709,0 -0.857143,1.7143071 0.401786,0 0.897322,-1.7143071 -0.441965,0 z m -9.0066959,1.7143071 -0.8571428,1.714307 0.4017857,0 0.8989955,-1.714307 -0.4436384,0 z m 6.842076,0 -0.8571429,1.714307 0.4017857,0 0.8989951,-1.714307 -0.4436379,0 z M 7,5.2857633 l -6,3.428614 1.7142857,0 0.8571429,-0.3431962 0,4.1853199 0,0.01507 a 0.42857143,0.42857675 0 0 0 0.2059152,0.366634 0.42857143,0.42857675 0 0 0 0.4319196,0.0067 0.42857143,0.42857675 0 0 0 0.219308,-0.373322 l 0,-4.5435832 L 7,7.0000703 l 2.5714286,1.0279145 0,4.5435832 a 0.42857143,0.42857675 0 0 0 0.2059148,0.366634 0.42857143,0.42857675 0 0 0 0.4319206,0.0067 0.42857143,0.42857675 0 0 0 0.219307,-0.37332 l 0,-4.2003869 0.857143,0.3431962 1.714286,0 -6,-3.428614 z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,141 @@
{
"id": "transit_routes",
"name": {
"en": "Bus lines"
},
"description": {
"en": "Layer showing bus lines"
},
"source": {
"osmTags": {
"and": [
"type=route",
"route=bus"
]
}
},
"minzoom": 15,
"title": {
"render": {
"en": "Bus line"
},
"mappings": [
{
"if": "name~*",
"then": {
"en": "{name}"
}
}
]
},
"mapRendering": [
{
"color": {
"render": {
"en": "#ff0000"
},
"mappings": [
{
"if": "colour~*",
"then": "{colour}"
}
]
}
}
],
"tagRenderings": [
{
"id": "name",
"freeform": {
"key": "name",
"type": "string",
"placeholder": "Bus XX: From => Via => To"
},
"render": "{name}",
"question": {
"en": "What is the name for this bus line? (i.e. Bus XX: From => Via => To)"
}
},
{
"id": "from",
"freeform": {
"key": "from",
"type": "string",
"placeholder": "City, Stop Name"
},
"render": {
"en": "This bus line begins at {from}"
},
"question": {
"en": "What is the starting point for this bus line?"
}
},
{
"id": "via",
"freeform": {
"key": "via",
"type": "string",
"placeholder": "City, Stop Name"
},
"render": {
"en": "This bus line goes via {via}"
},
"question": {
"en": "What is the via point for this bus line?"
}
},
{
"id": "to",
"freeform": {
"key": "to",
"type": "string",
"placeholder": "City, Stop Name"
},
"render": {
"en": "This bus line ends at {to}"
},
"question": {
"en": "What is the ending point for this bus line?"
}
},
{
"id": "colour",
"freeform": {
"key": "colour",
"type": "color"
},
"render": {
"en": "This bus line has the color {colour}"
},
"question": {
"en": "What is the colour for this bus line?"
}
},
{
"id": "network",
"freeform": {
"key": "network",
"type": "string"
},
"render": {
"en": "This bus line is part of the {network} network"
},
"question": {
"en": "What network does this bus line belong to?"
}
},
{
"id": "operator",
"freeform": {
"key": "operator",
"type": "string"
},
"render": {
"en": "This bus line is operated by {operator}"
},
"question": {
"en": "What company operates this bus line?"
}
}
]
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
viewBox="0 0 500 500"
version="1.1"
id="svg4"
sodipodi:docname="bus_stop.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview6"
showgrid="false"
inkscape:zoom="25.986174"
inkscape:cx="-2.629287"
inkscape:cy="10.208516"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M 100,0 50,50 v 400 h 32.8125 c 0,0 5.057445,50 40.6375,50 34.45648,0 39.05,-50 39.05,-50 h 175 c 0,0 5.7171,50 39.05,50 40.82343,0 40.6375,-50 40.6375,-50 H 450 V 50 L 400,0 Z m 50,50 h 200 v 50 H 150 Z M 100,150 H 400 V 300 H 100 Z m 0,200 h 50 v 50 h -50 z m 250,0 h 50 v 50 h -50 z"
id="path2"
style="stroke-width:50" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,15 @@
[
{
"path": "bus_stop.svg",
"license": "CC0",
"authors": [
"Andy Allan",
"Michael Glanznig",
"Paul Norman",
"Paul Dicker"
],
"sources": [
"https://github.com/gravitystorm/openstreetmap-carto/blob/master/symbols/highway/bus_stop.svg"
]
}
]

View file

@ -0,0 +1,260 @@
{
"id": "transit_stops",
"name": {
"en": "Transit Stops"
},
"description": {
"en": "Layer showing different types of transit stops."
},
"source": {
"osmTags": {
"or": [
"highway=bus_stop"
]
}
},
"minzoom": 15,
"title": {
"render": {
"en": "Transit Stop"
},
"mappings": [
{
"if": "name~*",
"then": {
"en": "Stop {name}"
}
}
]
},
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": {
"render": "./assets/layers/transit_stops/bus_stop.svg",
"mappings": []
},
"label": "<div style=\"background: white; display: block\">{name}</div>"
}
],
"calculatedTags": [
"_routes=feat.memberships()",
"_contained_routes_properties=feat.memberships().map(p => {return {id: p.relation.id, name: p.relation.properties.name} }).filter((v,i,a)=>a.findIndex(t=>(JSON.stringify(t) === JSON.stringify(v)))===i)",
"_contained_route_ids=JSON.parse(feat.properties._contained_routes_properties ?? '[]').map(p => p.id)",
"_contained_routes=JSON.parse(feat.properties._contained_routes_properties ?? '[]').map(p => `<li><a href='#relation/${p.id}'>${p.name ?? 'bus route'}</a></li>`).join('')",
"_contained_routes_count=JSON.parse(feat.properties._contained_routes_properties ?? '[]').length"
],
"tagRenderings": [
{
"id": "stop_name",
"render": {
"en": "This stop is called <b>{name}</b>"
},
"freeform": {
"key": "name",
"type": "string",
"addExtraTags": [
"noname="
],
"placeholder": {
"en": "Name of the stop"
}
},
"mappings": [
{
"if": {
"and": [
"noname=yes",
"name="
]
},
"then": {
"en": "This stop has no name"
}
}
],
"question": {
"en": "What is the name of this stop?"
},
"placeholder": "Name of the stop"
},
"images",
{
"id": "shelter",
"mappings": [
{
"if": "shelter=yes",
"then": {
"en": "This stop has a shelter"
}
},
{
"if": "shelter=no",
"then": {
"en": "This stop does not have a shelter"
}
},
{
"if": "shelter=separate",
"then": {
"en": "This stop has a shelter, that's separately mapped"
},
"hideInAnswer": true
}
],
"question": {
"en": "Does this stop have a shelter?"
}
},
{
"id": "bench",
"mappings": [
{
"if": "bench=yes",
"then": {
"en": "This stop has a bench"
}
},
{
"if": "bench=no",
"then": {
"en": "This stop does not have a bench"
}
},
{
"if": "bench=separate",
"then": {
"en": "This stop has a bench, that's separately mapped"
},
"hideInAnswer": true
}
],
"question": {
"en": "Does this stop have a bench?"
}
},
{
"id": "bin",
"mappings": [
{
"if": "bin=yes",
"then": {
"en": "This stop has a bin"
}
},
{
"if": "bin=no",
"then": {
"en": "This stop does not have a bin"
}
},
{
"if": "bin=separate",
"then": {
"en": "This stop has a bin, that's separately mapped"
},
"hideInAnswer": true
}
],
"question": {
"en": "Does this stop have a bin?"
}
},
"wheelchair-access",
{
"id": "tactile_paving",
"mappings": [
{
"if": "tactile_paving=yes",
"then": {
"en": "This stop has tactile paving"
}
},
{
"if": "tactile_paving=no",
"then": {
"en": "This stop does not have tactile paving"
}
}
],
"question": {
"en": "Does this stop have tactile paving?"
}
},
{
"id": "lit",
"mappings": [
{
"if": "lit=yes",
"then": {
"en": "This stop is lit"
}
},
{
"if": "lit=no",
"then": {
"en": "This stop is not lit"
}
}
],
"question": {
"en": "Is this stop lit?"
}
},
{
"id": "departures_board",
"mappings": [
{
"if": "departures_board=yes",
"then": {
"en": "This stop has a departures board of unknown type"
},
"hideInAnswer": true
},
{
"if": "departures_board=realtime",
"then": {
"en": "This stop has a board showing realtime departure information"
}
},
{
"if": "passenger_information_display=yes",
"then": {
"en": "This stop has a board showing realtime departure information"
},
"hideInAnswer": true
},
{
"if": "departures_board=timetable",
"then": {
"en": "This stop has a timetable showing regular departures"
}
},
{
"if": "departures_board=interval",
"then": {
"en": "This stop has a timetable containing just the interval between departures"
}
},
{
"if": "departures_board=no",
"then": {
"en": "This stop does not have a departures board"
}
}
]
},
{
"render": {
"en": "<h3>{_contained_routes_count} routes stop at this stop</h3> <ul>{_contained_routes}</ul>"
},
"condition": "_contained_routes~*",
"id": "contained_routes"
}
],
"filter": [],
"allowMove": false
}

View file

@ -311,6 +311,10 @@
"if": "theme=toilets",
"then": "./assets/themes/toilets/toilets.svg"
},
{
"if": "theme=transit",
"then": "./assets/layers/transit_stops/bus_stop.svg"
},
{
"if": "theme=trees",
"then": "./assets/themes/trees/logo.svg"

View file

@ -0,0 +1,51 @@
{
"id": "transit",
"maintainer": "Robin van der Linde",
"version": "20220406",
"title": {
"en": "Bus routes"
},
"description": {
"en": "Plan your trip with the help of the public transport system."
},
"icon": "./assets/layers/transit_stops/bus_stop.svg",
"startZoom": 20,
"startLat": 53.21333,
"startLon": 6.56963,
"layers": [
"transit_stops",
"transit_routes",
{
"builtin": "bike_parking",
"override": {
"minzoom": 19,
"minzoomVisible": 19
}
},
{
"builtin": "parking",
"override": {
"minzoom": 19,
"minzoomVisible": 19
}
},
{
"builtin": "shelter",
"override": {
"minzoom": 19,
"minzoomVisible": 19,
"source": {
"osmTags": {
"and": [
"amenity=shelter",
"shelter_type=public_transport"
]
}
}
},
"hideTagRenderingsWithLabels": [
"shelter-type"
]
}
]
}

View file

@ -3568,6 +3568,11 @@
"0": {
"explanation": "{title()} has closed down permanently"
}
},
"nonDeleteMappings": {
"0": {
"then": "This is actually a pub"
}
}
},
"description": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)",
@ -5232,6 +5237,39 @@
"render": "School <i>{name}</i>"
}
},
"shelter": {
"description": "Layer showing shelter structures",
"name": "Shelter",
"tagRenderings": {
"shelter-type": {
"mappings": {
"0": {
"then": "This is a shelter at a public transport stop."
},
"1": {
"then": "This is a shelter protecting from rain at a picnic site."
},
"2": {
"then": "This is a gazebo."
},
"3": {
"then": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads."
},
"4": {
"then": "This is a shed with 3 walls, primarily intended for camping."
},
"5": {
"then": "This is a pavilion"
}
},
"question": "What kind of shelter is this?",
"render": "Shelter type: {shelter_type}"
}
},
"title": {
"render": "Shelter"
}
},
"shops": {
"deletion": {
"extraDeleteReasons": {
@ -5279,11 +5317,13 @@
}
},
"tagRenderings": {
"2": {
"override": {
"question": "What kind of shop is this?"
}
},
"shops-name": {
"question": "What is the name of this shop?"
},
"shops-type-from-id": {
"question": "What kind of shop is this?"
}
},
"title": {
@ -5962,6 +6002,169 @@
"render": "Trail"
}
},
"transit_routes": {
"description": "Layer showing bus lines",
"mapRendering": {
"0": {
"color": {
"render": "#ff0000"
}
}
},
"name": "Bus lines",
"tagRenderings": {
"colour": {
"question": "What is the colour for this bus line?",
"render": "This bus line has the color {colour}"
},
"from": {
"question": "What is the starting point for this bus line?",
"render": "This bus line begins at {from}"
},
"name": {
"question": "What is the name for this bus line? (i.e. Bus XX: From => Via => To)"
},
"network": {
"question": "What network does this bus line belong to?",
"render": "This bus line is part of the {network} network"
},
"operator": {
"question": "What company operates this bus line?",
"render": "This bus line is operated by {operator}"
},
"to": {
"question": "What is the ending point for this bus line?",
"render": "This bus line ends at {to}"
},
"via": {
"question": "What is the via point for this bus line?",
"render": "This bus line goes via {via}"
}
},
"title": {
"mappings": {
"0": {
"then": "{name}"
}
},
"render": "Bus line"
}
},
"transit_stops": {
"description": "Layer showing different types of transit stops.",
"name": "Transit Stops",
"tagRenderings": {
"bench": {
"mappings": {
"0": {
"then": "This stop has a bench"
},
"1": {
"then": "This stop does not have a bench"
},
"2": {
"then": "This stop has a bench, that's separately mapped"
}
},
"question": "Does this stop have a bench?"
},
"bin": {
"mappings": {
"0": {
"then": "This stop has a bin"
},
"1": {
"then": "This stop does not have a bin"
},
"2": {
"then": "This stop has a bin, that's separately mapped"
}
},
"question": "Does this stop have a bin?"
},
"contained_routes": {
"render": "<h3>{_contained_routes_count} routes stop at this stop</h3> <ul>{_contained_routes}</ul>"
},
"departures_board": {
"mappings": {
"0": {
"then": "This stop has a departures board of unknown type"
},
"1": {
"then": "This stop has a board showing realtime departure information"
},
"2": {
"then": "This stop has a board showing realtime departure information"
},
"3": {
"then": "This stop has a timetable showing regular departures"
},
"4": {
"then": "This stop has a timetable containing just the interval between departures"
},
"5": {
"then": "This stop does not have a departures board"
}
}
},
"lit": {
"mappings": {
"0": {
"then": "This stop is lit"
},
"1": {
"then": "This stop is not lit"
}
},
"question": "Is this stop lit?"
},
"shelter": {
"mappings": {
"0": {
"then": "This stop has a shelter"
},
"1": {
"then": "This stop does not have a shelter"
},
"2": {
"then": "This stop has a shelter, that's separately mapped"
}
},
"question": "Does this stop have a shelter?"
},
"stop_name": {
"freeform": {
"placeholder": "Name of the stop"
},
"mappings": {
"0": {
"then": "This stop has no name"
}
},
"question": "What is the name of this stop?",
"render": "This stop is called <b>{name}</b>"
},
"tactile_paving": {
"mappings": {
"0": {
"then": "This stop has tactile paving"
},
"1": {
"then": "This stop does not have tactile paving"
}
},
"question": "Does this stop have tactile paving?"
}
},
"title": {
"mappings": {
"0": {
"then": "Stop {name}"
}
},
"render": "Transit Stop"
}
},
"tree_node": {
"description": "A layer showing trees",
"name": "Tree",

View file

@ -1756,6 +1756,218 @@
"render": "Microbiblioteca"
}
},
"recycling": {
"description": "Un livello con i contenitori e centri per la raccolta rifiuti riciclabili",
"filter": {
"0": {
"options": {
"0": {
"question": "Aperto ora"
}
}
},
"1": {
"options": {
"0": {
"question": "Tutti i tipi di rifiuti"
},
"1": {
"question": "Riciclo di batterie"
},
"2": {
"question": "Riciclo di confezioni per bevande"
},
"3": {
"question": "Riciclo di lattine"
},
"4": {
"question": "Riciclo di abiti"
},
"5": {
"question": "Riciclo di olio da cucina"
},
"6": {
"question": "Riciclo di olio da motore"
},
"7": {
"question": "Riciclo di umido"
},
"8": {
"question": "Riciclo di bottiglie di vetro"
},
"9": {
"question": "Riciclo di vetro"
},
"10": {
"question": "Riciclo di giornali"
},
"11": {
"question": "Riciclo di carta"
},
"12": {
"question": "Riciclo di bottiglie di plastica"
},
"13": {
"question": "Riciclo di confezioni di plastica"
},
"14": {
"question": "Riciclo di plastica"
},
"15": {
"question": "Riciclo di rottami metallici"
},
"16": {
"question": "Riciclo di piccoli elettrodomestici"
},
"17": {
"question": "Riciclo di secco"
}
}
}
},
"name": "Riciclo",
"presets": {
"0": {
"title": "un contenitore per il riciclo"
},
"1": {
"title": "un centro di riciclo"
}
},
"tagRenderings": {
"container-location": {
"mappings": {
"0": {
"then": "E' un contenitore sotterraneo"
},
"1": {
"then": "Questo contenitore è al chiuso"
},
"2": {
"then": "Questo contenitore è all'aperto"
}
},
"question": "Dove si trova questo contenitore?"
},
"opening_hours": {
"mappings": {
"0": {
"then": "24/7"
}
},
"question": "Quali sono gli orari di apertura di questo impianto di raccolta e riciclo?"
},
"operator": {
"question": "Quale azienda gestisce questo impianto di raccolta e riciclo?",
"render": "Questa struttura di raccola e riciclo è gestita da {operator}"
},
"recycling-accepts": {
"mappings": {
"0": {
"then": "Batterie"
},
"1": {
"then": "Cartoni per bevande"
},
"2": {
"then": "Lattine"
},
"3": {
"then": "Abiti"
},
"4": {
"then": "Olio da cucina"
},
"5": {
"then": "Olio di motore"
},
"6": {
"then": "Verde"
},
"7": {
"then": "Umido"
},
"8": {
"then": "Bottiglie di vetro"
},
"9": {
"then": "Vetro"
},
"10": {
"then": "Giornali"
},
"11": {
"then": "Carta"
},
"12": {
"then": "Bottiglie di platica"
},
"13": {
"then": "Confezioni di plastica"
},
"14": {
"then": "Plastica"
},
"15": {
"then": "Rottami metallici"
},
"16": {
"then": "Scarpe"
},
"17": {
"then": "Piccoli elettrodomestici"
},
"18": {
"then": "Piccoli elettrodomestici"
},
"19": {
"then": "Aghi e oggetti appuntiti"
},
"20": {
"then": "Secco"
}
},
"question": "Cosa si può riciclare qui?"
},
"recycling-centre-name": {
"mappings": {
"0": {
"then": "Questo centro raccolta e riciclo rifiuti non ha un nome specifico"
}
},
"question": "Come si chiama questo centro raccolta e riciclo rifiuti?",
"render": "Questo centro raccolta e riciclo rifiuti si chiama <b>{name}</b>"
},
"recycling-type": {
"mappings": {
"0": {
"then": "Questo è un contenitore per il riciclo di rifiuti"
},
"1": {
"then": "Questo è un centro per la raccola e riciclo di rifiuti"
},
"2": {
"then": "Contenitore per lo smaltimento del secco"
}
},
"question": "Che tipo di raccolta è questo?"
}
},
"title": {
"mappings": {
"0": {
"then": "Centro di riciclo rifiuti"
},
"1": {
"then": "Centro di riciclo rifiuti"
},
"2": {
"then": "Contenitore per il riciclo"
}
},
"render": "Impianti di riciclo"
}
},
"slow_roads": {
"tagRenderings": {
"slow_roads-surface": {
@ -2259,6 +2471,158 @@
"render": "Punto panoramico"
}
},
"waste_basket": {
"description": "Questo è un cestino dei rifiuti pubblico, un bidone della spazzatura, dove puoi buttare via la tua spazzatura",
"filter": {
"0": {
"options": {
"0": {
"question": "Tutti i tipi"
},
"1": {
"question": "Cestino per sigarette"
},
"2": {
"question": "Cestino per medicinali"
},
"3": {
"question": "Cestino per escrementi dei cani"
},
"4": {
"question": "Cestino per la spazzatura"
},
"5": {
"question": "Cestino dei rifiuti per oggetti taglienti"
},
"6": {
"question": "Cestino per la plastica"
}
}
},
"1": {
"options": {
"0": {
"question": "Cestino per rifiuti con dispenser per sacchetti per escrementi dei cani"
}
}
}
},
"mapRendering": {
"0": {
"iconSize": {
"mappings": {
"0": {
"then": "Cestino dei rifiuti"
}
}
}
}
},
"name": "Cestino dei rifiuti",
"presets": {
"0": {
"title": "un cestino dei rifiuti"
}
},
"tagRenderings": {
"dispensing_dog_bags": {
"mappings": {
"0": {
"then": "Questo cestino ha un distributore di sacchetti per escrementi dei cani"
},
"1": {
"then": "Questo cestino <b>non</b> ha un distributore di sacchetti per escrementi dei cani"
},
"2": {
"then": "Questo cestino <b>non</b> ha un distributore di sacchetti per escrementi dei cani"
}
},
"question": "Questo cestino ha un distributore di sacchetti per escrementi dei cani?"
},
"waste-basket-waste-types": {
"mappings": {
"0": {
"then": "Un cestino rifiuti per uso generico"
},
"1": {
"then": "Un cestino rifiuti per uso generico"
},
"2": {
"then": "Un cestino rifiuti per escrementi di cani"
},
"3": {
"then": "Un cestino rifiuti per sigarette"
},
"4": {
"then": "Un cestino rifiuti per medicinali"
},
"5": {
"then": "Un cestino rifiuti per aghi e altri oggetti appuntiti"
},
"6": {
"then": "Un cestino rifiuti per la plastica"
}
},
"question": "Che tipo di cestino dei rifiuti è questo?"
}
},
"title": {
"render": "Cestino dei rifiuti"
}
},
"waste_disposal": {
"description": "Cestino per lo smaltimento dei rifiuti, contenitore di dimensioni medio grandi per lo smaltimento dei rifiuti (domestici)",
"filter": {
"0": {
"options": {
"0": {
"question": "Solo accesso pubblico"
}
}
}
},
"name": "Contenitori per la raccolta differenziata",
"presets": {
"0": {
"description": "Cestino di dimensioni medio-grandi per lo smaltimento dei rifiuti (domestici)",
"title": "un raccoglitore per lo smaltimento rifiuti"
}
},
"tagRenderings": {
"access": {
"mappings": {
"0": {
"then": "Questo cestino può essere usato da chiunque"
},
"1": {
"then": "Questo cestino è privato"
},
"2": {
"then": "Questo cestino è solo per residenti"
}
},
"question": "Chi può utilizzare questo cestino per lo smaltimento dei rifiuti?",
"render": "Accesso: {access}"
},
"disposal-location": {
"mappings": {
"0": {
"then": "Questo è un contenitore sotterraneo"
},
"1": {
"then": "Questo contenitore è al chiuso"
},
"2": {
"then": "Questo contenitore è all'aperto"
}
},
"question": "Dove si trova questo contenitore?"
}
},
"title": {
"render": "Smaltimento rifiuti"
}
},
"windturbine": {
"name": "pala eolica",
"presets": {

View file

@ -5183,11 +5183,13 @@
}
},
"tagRenderings": {
"2": {
"override": {
"question": "Wat voor soort winkel is dit?"
}
},
"shops-name": {
"question": "Wat is de naam van deze winkel?"
},
"shops-type-from-id": {
"question": "Wat voor soort winkel is dit?"
}
},
"title": {

View file

@ -953,6 +953,10 @@
"description": "A map of public toilets",
"title": "Open Toilet Map"
},
"transit": {
"description": "Plan your trip with the help of the public transport system.",
"title": "Bus routes"
},
"trees": {
"description": "Map all the trees!",
"shortDescription": "Map all the trees",

View file

@ -577,6 +577,10 @@
"shortDescription": "Mappa tutti gli alberi",
"title": "Alberi"
},
"waste": {
"description": "Mappa dei cestini per i rifiuti e i centri di raccolta e riciclo rifiuti.",
"title": "Rifiuti"
},
"waste_basket": {
"description": "In questa cartina troverai i cestini dei rifiuti nei tuoi paraggi. Se manca un cestino, puoi inserirlo tu stesso",
"shortDescription": "Una cartina dei cestini dei rifiuti",

View file

@ -28,7 +28,7 @@
"generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../MapComplete-data/speelplekken_cache/ 51.20 4.35 51.09 4.56",
"generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../MapComplete-data/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:cache:natuurpunt:mini": "ts-node scripts/generateCache.ts natuurpunt 12 ../../git/MapComplete-data/natuurpunt_cache_mini/ 51.00792239979105 4.497699737548828 51.0353492224462554 4.539070129394531 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:layeroverview": "ts-node scripts/generateLayerOverview.ts --no-fail",
"generate:layeroverview": "ts-node scripts/generateLayerOverview.ts",
"generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
"generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",
@ -36,7 +36,7 @@
"generate:schemas": "ts2json-schema -p Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && ts-node scripts/fixSchemas.ts ",
"generate:service-worker": "tsc service-worker.ts && git_hash=$(git rev-parse HEAD) && sed -i \"s/GITHUB-COMMIT/$git_hash/\" service-worker.js",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json",
"reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json && rm ./asssets/generated/layers/* && rm ./assets/generated/themes/*",
"generate": "mkdir -p ./assets/generated; npm run reset:layeroverview; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run generate:licenses; npm run generate:layeroverview; npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && ts-node csvToJson.ts && cd -",
"prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh",

View file

@ -45,13 +45,103 @@ export default class ScriptUtils {
})
}
private static async DownloadJSON(url: string, headers?: any): Promise<any>{
public static erasableLog(...text) {
process.stdout.write("\r " + text.join(" ") + " \r")
}
public static sleep(ms) {
if (ms <= 0) {
process.stdout.write("\r \r")
return;
}
return new Promise((resolve) => {
process.stdout.write("\r Sleeping for " + (ms / 1000) + "s \r")
setTimeout(resolve, 1000);
}).then(() => ScriptUtils.sleep(ms - 1000));
}
public static getLayerPaths(): string[] {
return ScriptUtils.readDirRecSync("./assets/layers")
.filter(path => path.indexOf(".json") > 0)
.filter(path => path.indexOf(".proto.json") < 0)
.filter(path => path.indexOf("license_info.json") < 0)
}
public static getLayerFiles(): { parsed: LayerConfigJson, path: string }[] {
return ScriptUtils.readDirRecSync("./assets/layers")
.filter(path => path.indexOf(".json") > 0)
.filter(path => path.indexOf(".proto.json") < 0)
.filter(path => path.indexOf("license_info.json") < 0)
.map(path => {
try {
const contents = readFileSync(path, "UTF8")
if (contents === "") {
throw "The file " + path + " is empty, did you properly save?"
}
const parsed = JSON.parse(contents);
return {parsed, path}
} catch (e) {
console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e)
throw e
}
})
}
public static getThemePaths(): string[] {
return ScriptUtils.readDirRecSync("./assets/themes")
.filter(path => path.endsWith(".json") && !path.endsWith(".proto.json"))
.filter(path => path.indexOf("license_info.json") < 0)
}
public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] {
return this.getThemePaths()
.map(path => {
try {
const contents = readFileSync(path, "UTF8");
if (contents === "") {
throw "The file " + path + " is empty, did you properly save?"
}
const parsed = JSON.parse(contents);
return {parsed: parsed, path: path}
} catch (e) {
console.error("Could not read file ", path, "due to ", e)
throw e
}
});
}
public static TagInfoHistogram(key: string): Promise<{
data: { count: number, value: string, fraction: number }[]
}> {
const url = `https://taginfo.openstreetmap.org/api/4/key/values?key=${key}&filter=all&lang=en&sortname=count&sortorder=desc&page=1&rp=17&qtype=value`
return ScriptUtils.DownloadJSON(url)
}
public static async ReadSvg(path: string): Promise<any> {
if (!existsSync(path)) {
throw "File not found: " + path
}
const root = await xml2js.parseStringPromise(readFileSync(path, "UTF8"))
return root.svg
}
public static async ReadSvgSync(path: string, callback: ((svg: any) => void)): Promise<any> {
xml2js.parseString(readFileSync(path, "UTF8"), {async: false}, (err, root) => {
if (err) {
throw err
}
callback(root["svg"]);
})
}
private static async DownloadJSON(url: string, headers?: any): Promise<any> {
const data = await ScriptUtils.Download(url, headers);
return JSON.parse(data.content)
}
private static Download(url, headers?: any): Promise<{content: string}> {
private static Download(url, headers?: any): Promise<{ content: string }> {
return new Promise((resolve, reject) => {
try {
headers = headers ?? {}
@ -83,84 +173,4 @@ export default class ScriptUtils {
}
public static erasableLog(...text) {
process.stdout.write("\r " + text.join(" ") + " \r")
}
public static sleep(ms) {
if (ms <= 0) {
process.stdout.write("\r \r")
return;
}
return new Promise((resolve) => {
process.stdout.write("\r Sleeping for " + (ms / 1000) + "s \r")
setTimeout(resolve, 1000);
}).then(() => ScriptUtils.sleep(ms - 1000));
}
public static getLayerFiles(): { parsed: LayerConfigJson, path: string }[] {
return ScriptUtils.readDirRecSync("./assets/layers")
.filter(path => path.indexOf(".json") > 0)
.filter(path => path.indexOf(".proto.json") < 0)
.filter(path => path.indexOf("license_info.json") < 0)
.map(path => {
try {
const contents = readFileSync(path, "UTF8")
if (contents === "") {
throw "The file " + path + " is empty, did you properly save?"
}
const parsed = JSON.parse(contents);
return {parsed, path}
} catch (e) {
console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e)
throw e
}
})
}
public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] {
return ScriptUtils.readDirRecSync("./assets/themes")
.filter(path => path.endsWith(".json") && !path.endsWith(".proto.json"))
.filter(path => path.indexOf("license_info.json") < 0)
.map(path => {
try {
const contents = readFileSync(path, "UTF8");
if (contents === "") {
throw "The file " + path + " is empty, did you properly save?"
}
const parsed = JSON.parse(contents);
return {parsed: parsed, path: path}
} catch (e) {
console.error("Could not read file ", path, "due to ", e)
throw e
}
});
}
public static TagInfoHistogram(key: string): Promise<{
data: { count: number, value: string, fraction: number }[]
}> {
const url = `https://taginfo.openstreetmap.org/api/4/key/values?key=${key}&filter=all&lang=en&sortname=count&sortorder=desc&page=1&rp=17&qtype=value`
return ScriptUtils.DownloadJSON(url)
}
public static async ReadSvg(path: string): Promise<any>{
if(!existsSync(path)){
throw "File not found: "+path
}
const root = await xml2js.parseStringPromise(readFileSync(path, "UTF8"))
return root.svg
}
public static async ReadSvgSync(path: string, callback: ((svg: any) => void)): Promise<any>{
xml2js.parseString(readFileSync(path, "UTF8"),{async: false} , (err, root) => {
if(err){
throw err
}
callback(root["svg"]);
})
}
}

View file

@ -10,9 +10,10 @@ mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index
npm run generate:editor-layer-index &&
npm run reset:layeroverview
npm run generate &&
npm run generate:layeroverview && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise
npm run generate:layeroverview --force && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise; first time happens in 'npm run generate'
npm run test &&
npm run generate:layouts

View file

@ -1,10 +1,11 @@
import ScriptUtils from "./ScriptUtils";
import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs";
import {existsSync, mkdirSync, readFileSync, statSync, writeFileSync} from "fs";
import * as licenses from "../assets/generated/license_info.json"
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import Constants from "../Models/Constants";
import {
DoesImageExist,
PrevalidateTheme,
ValidateLayer,
ValidateTagRenderings,
@ -25,6 +26,51 @@ import {Utils} from "../Utils";
class LayerOverviewUtils {
public static readonly layerPath = "./assets/generated/layers/"
public static readonly themePath = "./assets/generated/themes/"
private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set<string> {
const publicThemes = [].concat(...themefiles
.filter(th => !th.hideFromOverview))
return new Set([].concat(...publicThemes.map(th => this.extractLayerIdsFrom(th))))
}
private static extractLayerIdsFrom(themeFile: LayoutConfigJson, includeInlineLayers = true): string[] {
const publicLayerIds = []
for (const publicLayer of themeFile.layers) {
if (typeof publicLayer === "string") {
publicLayerIds.push(publicLayer)
continue
}
if (publicLayer["builtin"] !== undefined) {
const bi = publicLayer["builtin"]
if (typeof bi === "string") {
publicLayerIds.push(bi)
continue
}
bi.forEach(id => publicLayerIds.push(id))
continue
}
if (includeInlineLayers) {
publicLayerIds.push(publicLayer["id"])
}
}
return publicLayerIds
}
shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean {
if (!existsSync(targetfile)) {
return true;
}
const targetModified = statSync(targetfile).mtime
if (typeof sourcefile === "string") {
sourcefile = [sourcefile]
}
return sourcefile.some(sourcefile => statSync(sourcefile).mtime > targetModified)
}
writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean, mustHaveLanguage: boolean, layers: (LayerConfigJson | string | { builtin })[] }[]) {
const perId = new Map<string, any>();
for (const theme of themes) {
@ -69,23 +115,23 @@ class LayerOverviewUtils {
}
writeTheme(theme: LayoutConfigJson) {
if (!existsSync("./assets/generated/themes")) {
mkdirSync("./assets/generated/themes");
if (!existsSync(LayerOverviewUtils.themePath)) {
mkdirSync(LayerOverviewUtils.themePath);
}
writeFileSync(`./assets/generated/themes/${theme.id}.json`, JSON.stringify(theme, null, " "), "UTF8");
writeFileSync(`${LayerOverviewUtils.themePath}${theme.id}.json`, JSON.stringify(theme, null, " "), "UTF8");
}
writeLayer(layer: LayerConfigJson) {
if (!existsSync("./assets/generated/layers")) {
mkdirSync("./assets/generated/layers");
if (!existsSync(LayerOverviewUtils.layerPath)) {
mkdirSync(LayerOverviewUtils.layerPath);
}
writeFileSync(`./assets/generated/layers/${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8");
writeFileSync(`${LayerOverviewUtils.layerPath}${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8");
}
getSharedTagRenderings(knownImagePaths: Set<string>): Map<string, TagRenderingConfigJson> {
getSharedTagRenderings(doesImageExist: DoesImageExist): Map<string, TagRenderingConfigJson> {
const dict = new Map<string, TagRenderingConfigJson>();
const validator = new ValidateTagRenderings(undefined, knownImagePaths);
const validator = new ValidateTagRenderings(undefined, doesImageExist);
for (const key in questions["default"]) {
if (key === "id") {
continue
@ -93,7 +139,7 @@ class LayerOverviewUtils {
questions[key].id = key;
questions[key]["source"] = "shared-questions"
const config = <TagRenderingConfigJson>questions[key]
validator.convertStrict(config, "generate-layer-overview:tagRenderings/questions.json:"+key)
validator.convertStrict(config, "generate-layer-overview:tagRenderings/questions.json:" + key)
dict.set(key, config)
}
for (const key in icons["default"]) {
@ -104,9 +150,9 @@ class LayerOverviewUtils {
continue
}
icons[key].id = key;
const config = <TagRenderingConfigJson>icons[key]
validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:"+key)
dict.set(key,config)
const config = <TagRenderingConfigJson>icons[key]
validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:" + key)
dict.set(key, config)
}
dict.forEach((value, key) => {
@ -149,16 +195,18 @@ class LayerOverviewUtils {
}
}
main(_: string[]) {
main(args: string[]) {
const forceReload = args.some(a => a == "--force")
const licensePaths = new Set<string>()
for (const i in licenses) {
licensePaths.add(licenses[i].path)
}
const sharedLayers = this.buildLayerIndex(licensePaths);
const sharedThemes = this.buildThemeIndex(licensePaths, sharedLayers)
const doesImageExist = new DoesImageExist(licensePaths, existsSync)
const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload);
const recompiledThemes : string[] = []
const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers, recompiledThemes, forceReload)
writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({
"layers": Array.from(sharedLayers.values()),
@ -168,7 +216,7 @@ class LayerOverviewUtils {
writeFileSync("./assets/generated/known_layers.json", JSON.stringify({layers: Array.from(sharedLayers.values())}))
{
if(recompiledThemes.length > 0) {
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
const iconsPerTheme =
Array.from(sharedThemes.values()).map(th => ({
@ -188,28 +236,42 @@ class LayerOverviewUtils {
console.log(green("All done!"))
}
private buildLayerIndex(knownImagePaths: Set<string>): Map<string, LayerConfigJson> {
private buildLayerIndex(doesImageExist: DoesImageExist, forceReload: boolean): Map<string, LayerConfigJson> {
// First, we expand and validate all builtin layers. These are written to assets/generated/layers
// At the same time, an index of available layers is built.
console.log(" ---------- VALIDATING BUILTIN LAYERS ---------")
const sharedTagRenderings = this.getSharedTagRenderings(knownImagePaths);
const layerFiles = ScriptUtils.getLayerFiles();
const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist);
const sharedLayers = new Map<string, LayerConfigJson>()
const state: DesugaringContext = {
tagRenderings: sharedTagRenderings,
sharedLayers
}
const prepLayer = new PrepareLayer(state);
for (const sharedLayerJson of layerFiles) {
const context = "While building builtin layer " + sharedLayerJson.path
const fixed = prepLayer.convertStrict(sharedLayerJson.parsed, context)
const skippedLayers: string[] = []
const recompiledLayers: string[] = []
for (const sharedLayerPath of ScriptUtils.getLayerPaths()) {
{
const targetPath = LayerOverviewUtils.layerPath + sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/"))
if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) {
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
sharedLayers.set(sharedLayer.id, sharedLayer)
skippedLayers.push(sharedLayer.id)
continue;
}
if(fixed.source.osmTags["and"] === undefined){
}
const parsed = JSON.parse(readFileSync(sharedLayerPath, "utf8"))
const context = "While building builtin layer " + sharedLayerPath
const fixed = prepLayer.convertStrict(parsed, context)
if (fixed.source.osmTags["and"] === undefined) {
fixed.source.osmTags = {"and": [fixed.source.osmTags]}
}
const validator = new ValidateLayer(sharedLayerJson.path, true, knownImagePaths);
const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist);
validator.convertStrict(fixed, context)
if (sharedLayers.has(fixed.id)) {
@ -217,39 +279,18 @@ class LayerOverviewUtils {
}
sharedLayers.set(fixed.id, fixed)
recompiledLayers.push(fixed.id)
this.writeLayer(fixed)
}
console.log("Recompiled layers " + recompiledLayers.join(", ") + " and skipped " + skippedLayers.length + " layers")
return sharedLayers;
}
private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set<string> {
const publicLayers = [].concat(...themefiles
.filter(th => !th.hideFromOverview)
.map(th => th.layers))
const publicLayerIds = new Set<string>()
for (const publicLayer of publicLayers) {
if (typeof publicLayer === "string") {
publicLayerIds.add(publicLayer)
continue
}
if (publicLayer["builtin"] !== undefined) {
const bi = publicLayer["builtin"]
if (typeof bi === "string") {
publicLayerIds.add(bi)
continue
}
bi.forEach(id => publicLayerIds.add(id))
continue
}
publicLayerIds.add(publicLayer.id)
}
return publicLayerIds
}
private buildThemeIndex(knownImagePaths: Set<string>, sharedLayers: Map<string, LayerConfigJson>): Map<string, LayoutConfigJson> {
private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map<string, LayerConfigJson>, recompiledThemes: string[], forceReload: boolean): Map<string, LayoutConfigJson> {
console.log(" ---------- VALIDATING BUILTIN THEMES ---------")
const themeFiles = ScriptUtils.getThemeFiles();
const fixed = new Map<string, LayoutConfigJson>();
@ -258,23 +299,33 @@ class LayerOverviewUtils {
const convertState: DesugaringContext = {
sharedLayers,
tagRenderings: this.getSharedTagRenderings(knownImagePaths),
tagRenderings: this.getSharedTagRenderings(doesImageExist),
publicLayers
}
const nonDefaultLanguages : {theme: string, language: string}[] = []
const skippedThemes: string[] = []
for (const themeInfo of themeFiles) {
const themePath = themeInfo.path;
let themeFile = themeInfo.parsed
const themePath = themeInfo.path
{
const targetPath = LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
const usedLayers = Array.from(LayerOverviewUtils.extractLayerIdsFrom(themeFile, false))
.map(id => LayerOverviewUtils.layerPath + id + ".json")
if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) {
fixed.set(themeFile.id, JSON.parse(readFileSync(LayerOverviewUtils.themePath+themeFile.id+".json", 'utf8')))
skippedThemes.push(themeFile.id)
continue;
}
recompiledThemes.push(themeFile.id)
}
new PrevalidateTheme().convertStrict(themeFile, themePath)
try {
themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath)
if (knownImagePaths === undefined) {
throw "Could not load known images/licenses"
}
new ValidateThemeAndLayers(knownImagePaths, themePath, true, convertState.tagRenderings)
new ValidateThemeAndLayers(doesImageExist, themePath, true, convertState.tagRenderings)
.convertStrict(themeFile, themePath)
this.writeTheme(themeFile)
@ -293,6 +344,9 @@ class LayerOverviewUtils {
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
}
}));
console.log("Recompiled themes " + recompiledThemes.join(", ") + " and skipped " + skippedThemes.length + " themes")
return fixed;
}

View file

@ -4,6 +4,7 @@ import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion";
import {UIEventSource} from "../../../Logic/UIEventSource";
import { expect } from 'chai';
import Locale from "../../../UI/i18n/Locale";
describe("TagRenderingQuestion", () => {
@ -27,6 +28,7 @@ describe("TagRenderingQuestion", () => {
it("should have a freeform text field with a type explanation", () => {
Locale.language.setData("en")
const configJson = <TagRenderingConfigJson>{
id: "test-tag-rendering",
question: "Question?",

View file

@ -28,23 +28,28 @@ function initDownloads(query: string){
describe("GenerateCache", () => {
it("should generate a cached file for the Natuurpunt-theme", async () => {
if (existsSync("/tmp/np-cache")) {
ScriptUtils.readDirRecSync("/tmp/np-cache").forEach(p => unlinkSync(p))
rmdirSync("/tmp/np-cache")
// We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this
const dir = "/var/tmp/"
if(!existsSync(dir)){
console.log("Not executing caching test: no temp directory found")
}
mkdirSync("/tmp/np-cache")
if (existsSync(dir+"/np-cache")) {
ScriptUtils.readDirRecSync(dir+"np-cache").forEach(p => unlinkSync(p))
rmdirSync(dir+"np-cache")
}
mkdirSync(dir+"np-cache")
initDownloads(
"(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B"
);
await main([
"natuurpunt",
"12",
"/tmp/np-cache",
dir+"np-cache",
"51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285",
"--generate-point-overview", "nature_reserve,visitor_information_centre"
])
await ScriptUtils.sleep(500)
const birdhides = JSON.parse(readFileSync("/tmp/np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8"))
const birdhides = JSON.parse(readFileSync(dir+"np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8"))
expect(birdhides.features.length).deep.equal(5)
expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true