forked from MapComplete/MapComplete
		
	Merge branch 'feature/doctor' of https://github.com/pietervdvn/MapComplete into feature/doctor
This commit is contained in:
		
						commit
						7fd6a89c27
					
				
					 30 changed files with 1625 additions and 241 deletions
				
			
		
							
								
								
									
										14
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -2,7 +2,6 @@ dist/* | ||||||
| node_modules | node_modules | ||||||
| .cache/* | .cache/* | ||||||
| .idea/* | .idea/* | ||||||
| .vscode/* |  | ||||||
| scratch | scratch | ||||||
| assets/editor-layer-index.json | assets/editor-layer-index.json | ||||||
| assets/generated/* | assets/generated/* | ||||||
|  | @ -24,3 +23,16 @@ index_*.ts | ||||||
| .~lock.* | .~lock.* | ||||||
| *.doctest.ts | *.doctest.ts | ||||||
| service-worker.js | 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
									
								
							
							
						
						
									
										14
									
								
								.gitpod.yml
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										7
									
								
								.vscode/extensions.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | { | ||||||
|  |     "recommendations": [ | ||||||
|  |         "esbenp.prettier-vscode", | ||||||
|  |         "eamodio.gitlens", | ||||||
|  |         "GitHub.vscode-pull-request-github" | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										14
									
								
								.vscode/tasks.json
									
										
									
									
										vendored
									
									
										Normal 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" | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | @ -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 | 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 | Automatic deployment | ||||||
| -------------------- | -------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -39,6 +39,14 @@ export abstract class Conversion<TIn, TOut> { | ||||||
|         return DesugaringStep.strict(fixed) |         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>{ |     public andThenF<X>(f: (tout:TOut) => X ): Conversion<TIn, X>{ | ||||||
|         return new Pipe( |         return new Pipe( | ||||||
|             this, |             this, | ||||||
|  |  | ||||||
|  | @ -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> { | class ValidateTheme extends DesugaringStep<LayoutConfigJson> { | ||||||
|     /** |     /** | ||||||
|      * The paths where this layer is originally saved. Triggers some extra checks |      * The paths where this layer is originally saved. Triggers some extra checks | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private readonly _path?: string; |     private readonly _path?: string; | ||||||
|     private readonly knownImagePaths: Set<string>; |  | ||||||
|     private readonly _isBuiltin: boolean; |     private readonly _isBuiltin: boolean; | ||||||
|     private _sharedTagRenderings: Map<string, any>; |     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"); |         super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme"); | ||||||
|         this.knownImagePaths = knownImagePaths; |         this._validateImage = doesImageExist; | ||||||
|         this._path = path; |         this._path = path; | ||||||
|         this._isBuiltin = isBuiltin; |         this._isBuiltin = isBuiltin; | ||||||
|         this._sharedTagRenderings = sharedTagRenderings; |         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.") |                 errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.") | ||||||
|             } |             } | ||||||
|             for (const image of images) { |             for (const image of images) { | ||||||
|                 if (image.indexOf("{") >= 0) { |                 this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information) | ||||||
|                     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}`) |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (json.icon.endsWith(".svg")) { |             if (json.icon.endsWith(".svg")) { | ||||||
|  | @ -150,9 +179,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> { | ||||||
|             if (theme.id !== filename) { |             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 + ")") |                 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)) { |             this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information); | ||||||
|                 errors.push("The theme image " + theme.icon + " is not attributed or not saved locally") |  | ||||||
|             } |  | ||||||
|             const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"])) |             const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"])) | ||||||
|             if (dups.length > 0) { |             if (dups.length > 0) { | ||||||
|                 errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`) |                 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
 |                 // 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] |                 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`) |                     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
 |                 // Official, public themes must have a full english translation
 | ||||||
|                 const checked = new ValidateLanguageCompleteness("en") |                 const checked = new ValidateLanguageCompleteness("en") | ||||||
|                     .convert(theme, theme.id) |                     .convert(theme, theme.id) | ||||||
|                 errors.push(...checked.errors) |                 errors.push(...checked.errors) | ||||||
|                  | 
 | ||||||
|                | 
 | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  | @ -192,10 +219,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ValidateThemeAndLayers extends Fuse<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", |         super("Validates a theme and the contained layers", | ||||||
|             new ValidateTheme(knownImagePaths, path, isBuiltin, sharedTagRenderings), |             new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings), | ||||||
|             new On("layers", new Each(new ValidateLayer(undefined, false, knownImagePaths))) |             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++) { |             for (let j = 0; j < i; j++) { | ||||||
|                 const doesMatch = parsedConditions[j].matchesProperties(properties) |                 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.`) |                     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) { |                 } else if (doesMatch) { | ||||||
|                     // The current mapping is shadowed!
 |                     // The current mapping is shadowed!
 | ||||||
|  | @ -385,14 +412,15 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> { | export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> { | ||||||
|     private knownImagePaths: Set<string>; |     private readonly _doesImageExist: DoesImageExist; | ||||||
|     constructor(knownImagePaths: Set<string>) { | 
 | ||||||
|  |     constructor(doesImageExist: DoesImageExist) { | ||||||
|         super("Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", [], "DetectMappingsWithImages"); |         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": [ |      *     "mappings": [ | ||||||
|      *         { |      *         { | ||||||
|      *             "if": "bicycle_parking=stands", |      *             "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
 |      * 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[] } { |     convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } { | ||||||
|         const errors = [] |         const errors: string[] = [] | ||||||
|         const warnings = [] |         const warnings: string[] = [] | ||||||
|         const information = [] |         const information: string[] = [] | ||||||
|         if (json.mappings === undefined || json.mappings.length === 0) { |         if (json.mappings === undefined || json.mappings.length === 0) { | ||||||
|             return {result: json} |             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`) |                     information.push(`${ctx}: Ignored image ${images.join(", ")} in 'then'-clause of a mapping as this check has been disabled`) | ||||||
| 
 | 
 | ||||||
|                     for (const image of images) { |                     for (const image of images) { | ||||||
|                         if (this.knownImagePaths !== undefined && !this.knownImagePaths.has(image)) { |                         this._doesImageExist.convertJoin(image, ctx, errors, warnings, information); | ||||||
|                             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}`) |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                      | 
 | ||||||
|                 } |                 } | ||||||
|             } else if (ignore) { |             } else if (ignore) { | ||||||
|                 warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`) |                 warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`) | ||||||
|  | @ -454,10 +480,10 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> { | export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> { | ||||||
|     constructor(layerConfig?: LayerConfigJson, knownImagePaths?: Set<string>) { |     constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) { | ||||||
|         super("Various validation on tagRenderingConfigs", |         super("Various validation on tagRenderingConfigs", | ||||||
|             new DetectShadowedMappings(layerConfig), |             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 _path?: string; | ||||||
|     private readonly _isBuiltin: boolean; |     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"); |         super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer"); | ||||||
|         this._path = path; |         this._path = path; | ||||||
|         this._isBuiltin = isBuiltin; |         this._isBuiltin = isBuiltin; | ||||||
|         this.knownImagePaths = knownImagePaths |         this._doesImageExist = doesImageExist | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } { |     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) { |             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 ?? [])) |                 warnings.push(...(r.warnings ?? [])) | ||||||
|                 errors.push(...(r.errors ?? [])) |                 errors.push(...(r.errors ?? [])) | ||||||
|                 information.push(...(r.information ?? [])) |                 information.push(...(r.information ?? [])) | ||||||
|  |  | ||||||
|  | @ -31,6 +31,7 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement"; | ||||||
| 
 | 
 | ||||||
| export default class LayerConfig extends WithContextLoader { | export default class LayerConfig extends WithContextLoader { | ||||||
| 
 | 
 | ||||||
|  |     public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const; | ||||||
|     public readonly id: string; |     public readonly id: string; | ||||||
|     public readonly name: Translation; |     public readonly name: Translation; | ||||||
|     public readonly description: Translation; |     public readonly description: Translation; | ||||||
|  | @ -44,10 +45,8 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|     public readonly maxzoom: number; |     public readonly maxzoom: number; | ||||||
|     public readonly title?: TagRenderingConfig; |     public readonly title?: TagRenderingConfig; | ||||||
|     public readonly titleIcons: TagRenderingConfig[]; |     public readonly titleIcons: TagRenderingConfig[]; | ||||||
| 
 |  | ||||||
|     public readonly mapRendering: PointRenderingConfig[] |     public readonly mapRendering: PointRenderingConfig[] | ||||||
|     public readonly lineRendering: LineRenderingConfig[] |     public readonly lineRendering: LineRenderingConfig[] | ||||||
| 
 |  | ||||||
|     public readonly units: Unit[]; |     public readonly units: Unit[]; | ||||||
|     public readonly deletion: DeleteConfig | null; |     public readonly deletion: DeleteConfig | null; | ||||||
|     public readonly allowMove: MoveConfig | null |     public readonly allowMove: MoveConfig | null | ||||||
|  | @ -57,15 +56,11 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|      * In seconds |      * In seconds | ||||||
|      */ |      */ | ||||||
|     public readonly maxAgeOfCache: number |     public readonly maxAgeOfCache: number | ||||||
| 
 |  | ||||||
|     public readonly presets: PresetConfig[]; |     public readonly presets: PresetConfig[]; | ||||||
| 
 |  | ||||||
|     public readonly tagRenderings: TagRenderingConfig[]; |     public readonly tagRenderings: TagRenderingConfig[]; | ||||||
|     public readonly filters: FilterConfig[]; |     public readonly filters: FilterConfig[]; | ||||||
|     public readonly filterIsSameAs: string; |     public readonly filterIsSameAs: string; | ||||||
|     public readonly forceLoad: boolean; |     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
 |     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( |     constructor( | ||||||
|  | @ -74,18 +69,24 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         official: boolean = true |         official: boolean = true | ||||||
|     ) { |     ) { | ||||||
|         context = context + "." + json.id; |         context = context + "." + json.id; | ||||||
|         const translationContext = "layers:"+json.id |         const translationContext = "layers:" + json.id | ||||||
|         super(json, context) |         super(json, context) | ||||||
|         this.id = json.id; |         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) { |         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) { |         if (json.source === undefined) { | ||||||
|             throw "Layer " + this.id + " does not define a source section (" + context + ")" |             throw "Layer " + this.id + " does not define a source section (" + context + ")" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         if (json.source.osmTags === undefined) { |         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 + ")" |             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 |         this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30 | ||||||
|         if(json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0){ |         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+"'" |             throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'" | ||||||
|         } |         } | ||||||
|         this.syncSelection = json.syncSelection ?? "no"; |         this.syncSelection = json.syncSelection ?? "no"; | ||||||
|         const osmTags = TagUtils.Tag( |         const osmTags = TagUtils.Tag( | ||||||
|  | @ -107,10 +108,10 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|             context + "source.osmTags" |             context + "source.osmTags" | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if(Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()){ |         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, {}); |             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) { |         if (json.source["geoJsonSource"] !== undefined) { | ||||||
|             throw context + "Use 'geoJson' instead of 'geoJsonSource'"; |             throw context + "Use 'geoJson' instead of 'geoJsonSource'"; | ||||||
|         } |         } | ||||||
|  | @ -118,7 +119,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         if (json.source["geojson"] !== undefined) { |         if (json.source["geojson"] !== undefined) { | ||||||
|             throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; |             throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
| 
 | 
 | ||||||
|         this.source = new SourceConfig( |         this.source = new SourceConfig( | ||||||
|             { |             { | ||||||
|  | @ -138,8 +139,8 @@ export default class LayerConfig extends WithContextLoader { | ||||||
| 
 | 
 | ||||||
|         this.allowSplit = json.allowSplit ?? false; |         this.allowSplit = json.allowSplit ?? false; | ||||||
|         this.name = Translations.T(json.name, translationContext + ".name"); |         this.name = Translations.T(json.name, translationContext + ".name"); | ||||||
|         if(json.units!==undefined && !Array.isArray(json.units)){ |         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" |             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}]`))) |         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("="); |                 const index = kv.indexOf("="); | ||||||
|                 let key = kv.substring(0, index).trim(); |                 let key = kv.substring(0, index).trim(); | ||||||
|                 const r = "[a-z_][a-z0-9:]*" |                 const r = "[a-z_][a-z0-9:]*" | ||||||
|                 if(key.match(r) === null){ |                 if (key.match(r) === null) { | ||||||
|                     throw "At "+context+" invalid key for calculated tag: "+key+"; it should match "+r |                     throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r | ||||||
|                 } |                 } | ||||||
|                 const isStrict = key.endsWith(':') |                 const isStrict = key.endsWith(':') | ||||||
|                 if (isStrict) { |                 if (isStrict) { | ||||||
|  | @ -343,14 +344,14 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map<string, string[]>, dependencies: { |     public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map<string, string[]>, dependencies: { | ||||||
|         context?: string; |                                      context?: string; | ||||||
|         reason: string; |                                      reason: string; | ||||||
|         neededLayer: string; |                                      neededLayer: string; | ||||||
|     }[] = [] |                                  }[] = [] | ||||||
|                                  , addedByDefault = false, canBeIncluded = true): BaseUIElement { |         , addedByDefault = false, canBeIncluded = true): BaseUIElement { | ||||||
|         const extraProps = [] |         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 (canBeIncluded) { | ||||||
|             if (addedByDefault) { |             if (addedByDefault) { | ||||||
|  | @ -440,7 +441,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|         let overpassLink: BaseUIElement = undefined; |         let overpassLink: BaseUIElement = undefined; | ||||||
|         if (Constants.priviliged_layers.indexOf(this.id) < 0) { |         if (Constants.priviliged_layers.indexOf(this.id) < 0) { | ||||||
|             try { |             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) { |             } catch (e) { | ||||||
|                 console.error("Could not generate overpasslink for " + this.id) |                 console.error("Could not generate overpasslink for " + this.id) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -352,7 +352,7 @@ export default class TagRenderingConfig { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) { |             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'` | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								assets/layers/shelter/license_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								assets/layers/shelter/license_info.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | [ | ||||||
|  |   { | ||||||
|  |     "path": "shelter.svg", | ||||||
|  |     "license": "MIT", | ||||||
|  |     "authors": [ | ||||||
|  |       "Diemen Design" | ||||||
|  |     ], | ||||||
|  |     "sources": [ | ||||||
|  |       "https://icon-icons.com/icon/map-shelter/158301" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | ] | ||||||
							
								
								
									
										88
									
								
								assets/layers/shelter/shelter.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								assets/layers/shelter/shelter.json
									
										
									
									
									
										Normal 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" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								assets/layers/shelter/shelter.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/layers/shelter/shelter.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										141
									
								
								assets/layers/transit_routes/transit_routes.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								assets/layers/transit_routes/transit_routes.json
									
										
									
									
									
										Normal 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?" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								assets/layers/transit_stops/bus_stop.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								assets/layers/transit_stops/bus_stop.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										15
									
								
								assets/layers/transit_stops/license_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								assets/layers/transit_stops/license_info.json
									
										
									
									
									
										Normal 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" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | ] | ||||||
							
								
								
									
										260
									
								
								assets/layers/transit_stops/transit_stops.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								assets/layers/transit_stops/transit_stops.json
									
										
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
|  | @ -311,6 +311,10 @@ | ||||||
|                 "if": "theme=toilets", |                 "if": "theme=toilets", | ||||||
|                 "then": "./assets/themes/toilets/toilets.svg" |                 "then": "./assets/themes/toilets/toilets.svg" | ||||||
|               }, |               }, | ||||||
|  |               { | ||||||
|  |                 "if": "theme=transit", | ||||||
|  |                 "then": "./assets/layers/transit_stops/bus_stop.svg" | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 "if": "theme=trees", |                 "if": "theme=trees", | ||||||
|                 "then": "./assets/themes/trees/logo.svg" |                 "then": "./assets/themes/trees/logo.svg" | ||||||
|  |  | ||||||
							
								
								
									
										51
									
								
								assets/themes/transit/transit.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								assets/themes/transit/transit.json
									
										
									
									
									
										Normal 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" | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | @ -3568,6 +3568,11 @@ | ||||||
|                 "0": { |                 "0": { | ||||||
|                     "explanation": "{title()} has closed down permanently" |                     "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)", |         "description": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)", | ||||||
|  | @ -5232,6 +5237,39 @@ | ||||||
|             "render": "School <i>{name}</i>" |             "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": { |     "shops": { | ||||||
|         "deletion": { |         "deletion": { | ||||||
|             "extraDeleteReasons": { |             "extraDeleteReasons": { | ||||||
|  | @ -5279,11 +5317,13 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "tagRenderings": { |         "tagRenderings": { | ||||||
|  |             "2": { | ||||||
|  |                 "override": { | ||||||
|  |                     "question": "What kind of shop is this?" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "shops-name": { |             "shops-name": { | ||||||
|                 "question": "What is the name of this shop?" |                 "question": "What is the name of this shop?" | ||||||
|             }, |  | ||||||
|             "shops-type-from-id": { |  | ||||||
|                 "question": "What kind of shop is this?" |  | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "title": { |         "title": { | ||||||
|  | @ -5962,6 +6002,169 @@ | ||||||
|             "render": "Trail" |             "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": { |     "tree_node": { | ||||||
|         "description": "A layer showing trees", |         "description": "A layer showing trees", | ||||||
|         "name": "Tree", |         "name": "Tree", | ||||||
|  |  | ||||||
|  | @ -1756,6 +1756,218 @@ | ||||||
|             "render": "Microbiblioteca" |             "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": { |     "slow_roads": { | ||||||
|         "tagRenderings": { |         "tagRenderings": { | ||||||
|             "slow_roads-surface": { |             "slow_roads-surface": { | ||||||
|  | @ -2259,6 +2471,158 @@ | ||||||
|             "render": "Punto panoramico" |             "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": { |     "windturbine": { | ||||||
|         "name": "pala eolica", |         "name": "pala eolica", | ||||||
|         "presets": { |         "presets": { | ||||||
|  |  | ||||||
|  | @ -5183,11 +5183,13 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "tagRenderings": { |         "tagRenderings": { | ||||||
|  |             "2": { | ||||||
|  |                 "override": { | ||||||
|  |                     "question": "Wat voor soort winkel is dit?" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             "shops-name": { |             "shops-name": { | ||||||
|                 "question": "Wat is de naam van deze winkel?" |                 "question": "Wat is de naam van deze winkel?" | ||||||
|             }, |  | ||||||
|             "shops-type-from-id": { |  | ||||||
|                 "question": "Wat voor soort winkel is dit?" |  | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "title": { |         "title": { | ||||||
|  |  | ||||||
|  | @ -953,6 +953,10 @@ | ||||||
|         "description": "A map of public toilets", |         "description": "A map of public toilets", | ||||||
|         "title": "Open Toilet Map" |         "title": "Open Toilet Map" | ||||||
|     }, |     }, | ||||||
|  |     "transit": { | ||||||
|  |         "description": "Plan your trip with the help of the public transport system.", | ||||||
|  |         "title": "Bus routes" | ||||||
|  |     }, | ||||||
|     "trees": { |     "trees": { | ||||||
|         "description": "Map all the trees!", |         "description": "Map all the trees!", | ||||||
|         "shortDescription": "Map all the trees", |         "shortDescription": "Map all the trees", | ||||||
|  |  | ||||||
|  | @ -577,6 +577,10 @@ | ||||||
|         "shortDescription": "Mappa tutti gli alberi", |         "shortDescription": "Mappa tutti gli alberi", | ||||||
|         "title": "Alberi" |         "title": "Alberi" | ||||||
|     }, |     }, | ||||||
|  |     "waste": { | ||||||
|  |         "description": "Mappa dei cestini per i rifiuti e i centri di raccolta e riciclo rifiuti.", | ||||||
|  |         "title": "Rifiuti" | ||||||
|  |     }, | ||||||
|     "waste_basket": { |     "waste_basket": { | ||||||
|         "description": "In questa cartina troverai i cestini dei rifiuti nei tuoi paraggi. Se manca un cestino, puoi inserirlo tu stesso", |         "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", |         "shortDescription": "Una cartina dei cestini dei rifiuti", | ||||||
|  |  | ||||||
|  | @ -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: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": "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: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", |     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", | ||||||
|     "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", |     "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", | ||||||
|     "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", |     "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: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", |     "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'", |     "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": "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 -", |     "generate:charging-stations": "cd ./assets/layers/charging_station && ts-node csvToJson.ts && cd -", | ||||||
|     "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", |     "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", | ||||||
|  |  | ||||||
|  | @ -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); |         const data = await ScriptUtils.Download(url, headers); | ||||||
|         return JSON.parse(data.content) |         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) => { |         return new Promise((resolve, reject) => { | ||||||
|             try { |             try { | ||||||
|                 headers = headers ?? {} |                 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"]); |  | ||||||
|          }) |  | ||||||
|     } |  | ||||||
|      |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,9 +10,10 @@ mkdir dist 2> /dev/null | ||||||
| mkdir dist/assets 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 | # 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 && | ||||||
| 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 test && | ||||||
| npm run generate:layouts  | npm run generate:layouts  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import ScriptUtils from "./ScriptUtils"; | 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 * as licenses from "../assets/generated/license_info.json" | ||||||
| import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; | ||||||
| import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import Constants from "../Models/Constants"; | import Constants from "../Models/Constants"; | ||||||
| import { | import { | ||||||
|  |     DoesImageExist, | ||||||
|     PrevalidateTheme, |     PrevalidateTheme, | ||||||
|     ValidateLayer, |     ValidateLayer, | ||||||
|     ValidateTagRenderings, |     ValidateTagRenderings, | ||||||
|  | @ -25,6 +26,51 @@ import {Utils} from "../Utils"; | ||||||
| 
 | 
 | ||||||
| class LayerOverviewUtils { | 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 })[] }[]) { |     writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean, mustHaveLanguage: boolean, layers: (LayerConfigJson | string | { builtin })[] }[]) { | ||||||
|         const perId = new Map<string, any>(); |         const perId = new Map<string, any>(); | ||||||
|         for (const theme of themes) { |         for (const theme of themes) { | ||||||
|  | @ -69,23 +115,23 @@ class LayerOverviewUtils { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     writeTheme(theme: LayoutConfigJson) { |     writeTheme(theme: LayoutConfigJson) { | ||||||
|         if (!existsSync("./assets/generated/themes")) { |         if (!existsSync(LayerOverviewUtils.themePath)) { | ||||||
|             mkdirSync("./assets/generated/themes"); |             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) { |     writeLayer(layer: LayerConfigJson) { | ||||||
|         if (!existsSync("./assets/generated/layers")) { |         if (!existsSync(LayerOverviewUtils.layerPath)) { | ||||||
|             mkdirSync("./assets/generated/layers"); |             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 dict = new Map<string, TagRenderingConfigJson>(); | ||||||
|          | 
 | ||||||
|         const validator = new ValidateTagRenderings(undefined, knownImagePaths); |         const validator = new ValidateTagRenderings(undefined, doesImageExist); | ||||||
|         for (const key in questions["default"]) { |         for (const key in questions["default"]) { | ||||||
|             if (key === "id") { |             if (key === "id") { | ||||||
|                 continue |                 continue | ||||||
|  | @ -93,7 +139,7 @@ class LayerOverviewUtils { | ||||||
|             questions[key].id = key; |             questions[key].id = key; | ||||||
|             questions[key]["source"] = "shared-questions" |             questions[key]["source"] = "shared-questions" | ||||||
|             const config = <TagRenderingConfigJson>questions[key] |             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) |             dict.set(key, config) | ||||||
|         } |         } | ||||||
|         for (const key in icons["default"]) { |         for (const key in icons["default"]) { | ||||||
|  | @ -104,9 +150,9 @@ class LayerOverviewUtils { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             icons[key].id = key; |             icons[key].id = key; | ||||||
|             const config =  <TagRenderingConfigJson>icons[key] |             const config = <TagRenderingConfigJson>icons[key] | ||||||
|             validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:"+key) |             validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:" + key) | ||||||
|             dict.set(key,config) |             dict.set(key, config) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         dict.forEach((value, key) => { |         dict.forEach((value, key) => { | ||||||
|  | @ -149,16 +195,18 @@ class LayerOverviewUtils { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     main(args: string[]) { | ||||||
|     main(_: string[]) { |          | ||||||
|  |         const forceReload = args.some(a => a == "--force") | ||||||
| 
 | 
 | ||||||
|         const licensePaths = new Set<string>() |         const licensePaths = new Set<string>() | ||||||
|         for (const i in licenses) { |         for (const i in licenses) { | ||||||
|             licensePaths.add(licenses[i].path) |             licensePaths.add(licenses[i].path) | ||||||
|         } |         } | ||||||
| 
 |         const doesImageExist = new DoesImageExist(licensePaths, existsSync) | ||||||
|         const sharedLayers = this.buildLayerIndex(licensePaths); |         const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload); | ||||||
|         const sharedThemes = this.buildThemeIndex(licensePaths, sharedLayers) |         const recompiledThemes : string[] = [] | ||||||
|  |         const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers, recompiledThemes, forceReload) | ||||||
| 
 | 
 | ||||||
|         writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ |         writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ | ||||||
|             "layers": Array.from(sharedLayers.values()), |             "layers": Array.from(sharedLayers.values()), | ||||||
|  | @ -168,7 +216,7 @@ class LayerOverviewUtils { | ||||||
|         writeFileSync("./assets/generated/known_layers.json", JSON.stringify({layers: Array.from(sharedLayers.values())})) |         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
 |             // mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
 | ||||||
|             const iconsPerTheme = |             const iconsPerTheme = | ||||||
|                 Array.from(sharedThemes.values()).map(th => ({ |                 Array.from(sharedThemes.values()).map(th => ({ | ||||||
|  | @ -188,28 +236,42 @@ class LayerOverviewUtils { | ||||||
|         console.log(green("All done!")) |         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
 |         // 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.
 |         // At the same time, an index of available layers is built.
 | ||||||
|         console.log("   ---------- VALIDATING BUILTIN LAYERS ---------") |         console.log("   ---------- VALIDATING BUILTIN LAYERS ---------") | ||||||
| 
 | 
 | ||||||
|         const sharedTagRenderings = this.getSharedTagRenderings(knownImagePaths); |         const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist); | ||||||
|         const layerFiles = ScriptUtils.getLayerFiles(); |  | ||||||
|         const sharedLayers = new Map<string, LayerConfigJson>() |         const sharedLayers = new Map<string, LayerConfigJson>() | ||||||
|         const state: DesugaringContext = { |         const state: DesugaringContext = { | ||||||
|             tagRenderings: sharedTagRenderings, |             tagRenderings: sharedTagRenderings, | ||||||
|             sharedLayers |             sharedLayers | ||||||
|         } |         } | ||||||
|         const prepLayer = new PrepareLayer(state); |         const prepLayer = new PrepareLayer(state); | ||||||
|         for (const sharedLayerJson of layerFiles) { |         const skippedLayers: string[] = [] | ||||||
|             const context = "While building builtin layer " + sharedLayerJson.path |         const recompiledLayers: string[] = [] | ||||||
|             const fixed = prepLayer.convertStrict(sharedLayerJson.parsed, context) |         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]} |                 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) |             validator.convertStrict(fixed, context) | ||||||
| 
 | 
 | ||||||
|             if (sharedLayers.has(fixed.id)) { |             if (sharedLayers.has(fixed.id)) { | ||||||
|  | @ -217,39 +279,18 @@ class LayerOverviewUtils { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             sharedLayers.set(fixed.id, fixed) |             sharedLayers.set(fixed.id, fixed) | ||||||
|  |             recompiledLayers.push(fixed.id) | ||||||
| 
 | 
 | ||||||
|             this.writeLayer(fixed) |             this.writeLayer(fixed) | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         console.log("Recompiled layers " + recompiledLayers.join(", ") + " and skipped " + skippedLayers.length + " layers") | ||||||
|  | 
 | ||||||
|         return sharedLayers; |         return sharedLayers; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set<string> { |     private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map<string, LayerConfigJson>, recompiledThemes: string[], forceReload: boolean): Map<string, LayoutConfigJson> { | ||||||
|         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> { |  | ||||||
|         console.log("   ---------- VALIDATING BUILTIN THEMES ---------") |         console.log("   ---------- VALIDATING BUILTIN THEMES ---------") | ||||||
|         const themeFiles = ScriptUtils.getThemeFiles(); |         const themeFiles = ScriptUtils.getThemeFiles(); | ||||||
|         const fixed = new Map<string, LayoutConfigJson>(); |         const fixed = new Map<string, LayoutConfigJson>(); | ||||||
|  | @ -258,23 +299,33 @@ class LayerOverviewUtils { | ||||||
| 
 | 
 | ||||||
|         const convertState: DesugaringContext = { |         const convertState: DesugaringContext = { | ||||||
|             sharedLayers, |             sharedLayers, | ||||||
|             tagRenderings: this.getSharedTagRenderings(knownImagePaths), |             tagRenderings: this.getSharedTagRenderings(doesImageExist), | ||||||
|             publicLayers |             publicLayers | ||||||
|         } |         } | ||||||
|         const nonDefaultLanguages : {theme: string, language: string}[] = [] |         const skippedThemes: string[] = [] | ||||||
|         for (const themeInfo of themeFiles) { |         for (const themeInfo of themeFiles) { | ||||||
|  | 
 | ||||||
|  |             const themePath = themeInfo.path; | ||||||
|             let themeFile = themeInfo.parsed |             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) |             new PrevalidateTheme().convertStrict(themeFile, themePath) | ||||||
|             try { |             try { | ||||||
| 
 | 
 | ||||||
|                 themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath) |                 themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath) | ||||||
| 
 | 
 | ||||||
|                 if (knownImagePaths === undefined) { |                 new ValidateThemeAndLayers(doesImageExist, themePath, true, convertState.tagRenderings) | ||||||
|                     throw "Could not load known images/licenses" |  | ||||||
|                 } |  | ||||||
|                 new ValidateThemeAndLayers(knownImagePaths, themePath, true, convertState.tagRenderings) |  | ||||||
|                     .convertStrict(themeFile, themePath) |                     .convertStrict(themeFile, themePath) | ||||||
| 
 | 
 | ||||||
|                 this.writeTheme(themeFile) |                 this.writeTheme(themeFile) | ||||||
|  | @ -293,6 +344,9 @@ class LayerOverviewUtils { | ||||||
|                 mustHaveLanguage: t.mustHaveLanguage?.length > 0, |                 mustHaveLanguage: t.mustHaveLanguage?.length > 0, | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
|  | 
 | ||||||
|  |         console.log("Recompiled themes " + recompiledThemes.join(", ") + " and skipped " + skippedThemes.length + " themes") | ||||||
|  | 
 | ||||||
|         return fixed; |         return fixed; | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; | ||||||
| import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion"; | import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion"; | ||||||
| import {UIEventSource} from "../../../Logic/UIEventSource"; | import {UIEventSource} from "../../../Logic/UIEventSource"; | ||||||
| import { expect } from 'chai'; | import { expect } from 'chai'; | ||||||
|  | import Locale from "../../../UI/i18n/Locale"; | ||||||
| 
 | 
 | ||||||
| describe("TagRenderingQuestion", () => { | describe("TagRenderingQuestion", () => { | ||||||
| 
 | 
 | ||||||
|  | @ -27,6 +28,7 @@ describe("TagRenderingQuestion", () => { | ||||||
| 
 | 
 | ||||||
|     it("should have a freeform text field with a type explanation", () => { |     it("should have a freeform text field with a type explanation", () => { | ||||||
| 
 | 
 | ||||||
|  |         Locale.language.setData("en") | ||||||
|         const configJson = <TagRenderingConfigJson>{ |         const configJson = <TagRenderingConfigJson>{ | ||||||
|             id: "test-tag-rendering", |             id: "test-tag-rendering", | ||||||
|             question: "Question?", |             question: "Question?", | ||||||
|  |  | ||||||
|  | @ -28,23 +28,28 @@ function initDownloads(query: string){ | ||||||
| describe("GenerateCache", () => { | describe("GenerateCache", () => { | ||||||
|      |      | ||||||
|         it("should generate a cached file for the Natuurpunt-theme", async () => { |         it("should generate a cached file for the Natuurpunt-theme", async () => { | ||||||
|             if (existsSync("/tmp/np-cache")) { |             // We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this
 | ||||||
|                 ScriptUtils.readDirRecSync("/tmp/np-cache").forEach(p => unlinkSync(p)) |             const dir = "/var/tmp/" | ||||||
|                 rmdirSync("/tmp/np-cache") |             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( |             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" |                 "(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([ |             await main([ | ||||||
|                 "natuurpunt", |                 "natuurpunt", | ||||||
|                 "12", |                 "12", | ||||||
|                 "/tmp/np-cache", |                 dir+"np-cache", | ||||||
|                 "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", |                 "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", | ||||||
|                 "--generate-point-overview", "nature_reserve,visitor_information_centre" |                 "--generate-point-overview", "nature_reserve,visitor_information_centre" | ||||||
|             ]) |             ]) | ||||||
|             await ScriptUtils.sleep(500) |             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.length).deep.equal(5) | ||||||
|             expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true |             expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue