forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						ccf9c4b5f6
					
				
					 50 changed files with 1427 additions and 766 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								Docs/FilterFunctionality.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Docs/FilterFunctionality.gif
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7.3 MiB | 
							
								
								
									
										
											BIN
										
									
								
								Docs/FilteredByDepth.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Docs/FilteredByDepth.gif
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.3 MiB | 
|  | @ -4,6 +4,11 @@ Making your own theme | ||||||
| In MapComplete, it is relatively simple to make your own theme. This guide will give some information on how you can do | In MapComplete, it is relatively simple to make your own theme. This guide will give some information on how you can do | ||||||
| this. | this. | ||||||
| 
 | 
 | ||||||
|  | Table of contents: | ||||||
|  | 
 | ||||||
|  | 1. [Requirements](#requirements) which lists what you should know before starting to create a theme | ||||||
|  | 2. [What is a good theme?](#what-is-a-good-theme) | ||||||
|  | 
 | ||||||
| Requirements | Requirements | ||||||
| ------------ | ------------ | ||||||
| 
 | 
 | ||||||
|  | @ -15,10 +20,213 @@ Before you start, you should have the following qualifications: | ||||||
| - You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to | - You are in contact with your local OpenStreetMap community and do know some other members to discuss tagging and to | ||||||
|   help testing |   help testing | ||||||
| 
 | 
 | ||||||
| If you do not have those qualifications, reach out to the MapComplete community channel | Please, do reach out to the MapComplete community channel | ||||||
| on [Telegram](https://t.me/MapComplete) | on [Telegram](https://t.me/MapComplete) | ||||||
| or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org). | or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org). | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | What is a good theme? | ||||||
|  | --------------------- | ||||||
|  | 
 | ||||||
|  | A **theme** (or _layout_) is a single map showing one or more layers. | ||||||
|  | The layers should work together in such a way that they serve a certain **audience**. | ||||||
|  | You should be able to state in a few sentences whom would be the user of such a map, e.g.  | ||||||
|  | 
 | ||||||
|  | - a cyclist searching for bike repair | ||||||
|  | - a thirsty person who needs water | ||||||
|  | - someone who wants to know what their street is named after | ||||||
|  | - ... | ||||||
|  | 
 | ||||||
|  | Some layers will be useful for many themes (e.g. _drinking water_, _toilets_, _shops_, ...). Due to this, MapComplete supports to reuse already existing official layers into a theme. | ||||||
|  | 
 | ||||||
|  | To include an already existing layer, simply type the layer id, e.g.: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "id": "my-theme", | ||||||
|  |   "title": "My theme for xyz", | ||||||
|  |   "...": "...", | ||||||
|  |   "layers": [ | ||||||
|  |     { | ||||||
|  |       "id": "my super-awesome new layer" | ||||||
|  |     }, | ||||||
|  |     "bench", | ||||||
|  |     "shops", | ||||||
|  |     "drinking_water", | ||||||
|  |     "toilet" | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Note that it is good practice to use an existing layer and to tweak it: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "id": "my super awesome theme", | ||||||
|  |   "...": "...", | ||||||
|  |   "layers": [ | ||||||
|  |     { | ||||||
|  |       "builtin": [ | ||||||
|  |         "toilet", | ||||||
|  |         "bench" | ||||||
|  |       ], | ||||||
|  |       "override": { | ||||||
|  |         "#": "Override is a section which copies all the keys here and 'pastes' them into the existing layers. For example, the 'minzoom' defined here will redifine the minzoom of 'toilet' and 'bench'", | ||||||
|  |         "minzoom": 17, | ||||||
|  |         "#0": "Appending to lists is supported to, e.g. to add an extra question", | ||||||
|  |         "tagRenderings+": [ | ||||||
|  |           { | ||||||
|  |             "id": "new-question", | ||||||
|  |             "question": "What is <some property>?", | ||||||
|  |             "render": "{property}", | ||||||
|  |             "...": "..." | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "#1": "Note that paths will be followed: the below block will add/change the icon of the layer, without changing the other properties of the first tag rendering. (Assumption: the first mapRendering is the icon rendering)", | ||||||
|  |         "mapRendering": [ | ||||||
|  |           { | ||||||
|  |             "icon": { | ||||||
|  |               "render": "new-icon.svg" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### What is a good layer? | ||||||
|  | 
 | ||||||
|  | A good layer is layer which shows **all** objects of a certain type, e.g. **all** shops, **all** restaurants, ... | ||||||
|  | 
 | ||||||
|  | It asks some relevant questions, with the most important and easiests questions first. | ||||||
|  | 
 | ||||||
|  | #### Don't: use a layer to filter | ||||||
|  | 
 | ||||||
|  | **Do not define a layer which filters on an attribute**, such as <del>all restaurants with a vegetarian diet</del>, <del>all shops which accept bitcoin</del>. | ||||||
|  | This makes _addition_ of new points difficult as information might not yet be known. Conser the following situation: | ||||||
|  | 
 | ||||||
|  | 1. A theme defines a layer `vegetarian restaurants`, which matches `amenity=restaurant` & `diet:vegetarian=yes`. | ||||||
|  | 2. An object exists in OSM with `amenity=restaurant`;`name=Fancy Food`;`diet:vegan=yes`;`phone=...`;... | ||||||
|  | 3. A contributor visits the themes and will notice that _Fancy Food_ is missing | ||||||
|  | 4. The contributor will add _Fancy Food_ | ||||||
|  | 5. There are now **two** Fancy Food objects in OSM. | ||||||
|  | 
 | ||||||
|  | Instead, use the filter functionality instead. This can be used from the layer to hide some objects based on their properties. | ||||||
|  | When the contributor wants to add a new point, they'll be notified that some features might be hidden and only be allowed to add a new point when the points are shown. | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "id": "my awesome layer", | ||||||
|  |   "tagRenderings": "... some relevant attributes and questions ...", | ||||||
|  |   "mapRenderings": "... display on the map ... ", | ||||||
|  |   "filter": [ | ||||||
|  |     { | ||||||
|  |       "id": "vegetarian", | ||||||
|  |       "options": [ | ||||||
|  |         { | ||||||
|  |           "question": { | ||||||
|  |             "en": "Has a vegetarian menu" | ||||||
|  |           }, | ||||||
|  |           "osmTags": { | ||||||
|  |             "or": [ | ||||||
|  |               "diet:vegetarian=yes", | ||||||
|  |               "diet:vegetarian=only", | ||||||
|  |               "diet:vegan=yes", | ||||||
|  |               "diet:vegan=only" | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | If you want to show only features of a certain type, there is a workaround. | ||||||
|  | For example, the [fritures map](https://mapcomplete.osm.be/fritures.html?z=1&welcome-control-toggle=true) will show french fries shop, aka every `amenity~fast_food|restaurant` with `cuisine=friture`. | ||||||
|  | However, quite a few fritures are already mapped as fastfood but have their `cuisine`-tag missing (or misspelled). | ||||||
|  | 
 | ||||||
|  | There is a workaround for this: show **all** food related items at zoomlevel 19 (or higher), and only show the fritures when zoomed out. | ||||||
|  | 
 | ||||||
|  | In order to achieve this: | ||||||
|  | 
 | ||||||
|  | 1. The layer 'food' is defined in a separate file and reused | ||||||
|  | 2. The layer food is imported in the theme 'fritures'. With 'override', some properties are changed, namely: | ||||||
|  |    - The `osmTags` are overwritten: `cuisine=friture` is now required | ||||||
|  |    - The presets are overwritten and _disabled_  | ||||||
|  |    - The _id_ and _name_ of the layer are changed | ||||||
|  | 3. The layer `food` is imported _a second time_, but now the minzoom is set to `19`. This will show _all_ restaurants. | ||||||
|  | 
 | ||||||
|  | In case of a friture which is already added as fastfood, they'll see the fastfood popup instead of adding a new item: | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "layers": [ | ||||||
|  |     { | ||||||
|  |       "builtin": "food", | ||||||
|  |       "override": { | ||||||
|  |         "id": "friture", | ||||||
|  |         "name": { | ||||||
|  |           "en": "Fries shop" | ||||||
|  |         }, | ||||||
|  |         "=presets": [], | ||||||
|  |         "source": { | ||||||
|  |           "=osmTags": { | ||||||
|  |             "and": [ | ||||||
|  |               "cuisine=friture", | ||||||
|  |               { | ||||||
|  |                 "or": [ | ||||||
|  |                   "amenity=fast_food", | ||||||
|  |                   "amenity=restaurant" | ||||||
|  |                 ] | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "builtin": "food", | ||||||
|  |       "override": { | ||||||
|  |         "minzoom": 19, | ||||||
|  |         "filter": null, | ||||||
|  |         "name": null | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### What is a good question and tagrendering? | ||||||
|  | 
 | ||||||
|  | A tagrendering maps an attribute onto a piece of human readable text.  | ||||||
|  | These should be **full sentences**, e.g. `"render": "The maximum speed of this road is {maxspeed} km/h"` | ||||||
|  | 
 | ||||||
|  | In some cases, there might be some predifined special values as mappings, such as `"mappings": [{"if": "maxspeed=30", "then": "The maxspeed is 30km/h"}]` | ||||||
|  | 
 | ||||||
|  | The question then follows logically: `{"question": "What is the maximum allowed speed for this road, in km/h?"}` | ||||||
|  | At last, you'll also want to say that the user can type an answer too and that it has to be a number: `"freeform":{"key": "maxspeed","type":"pnat"}`. | ||||||
|  | 
 | ||||||
|  | The entire tagRendering will thus be: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "question": "What is the maximum allowed speed for this road, in km/h?", | ||||||
|  |   "render": "The maximum speed of this road is {maxspeed} km/h", | ||||||
|  |   "freeform":{"key": "maxspeed","type":"pnat"}, | ||||||
|  |   "mappings": [{"if": "maxspeed=30", "then": "The maxspeed is 30km/h"}] | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| The template | The template | ||||||
| ------------ | ------------ | ||||||
| 
 | 
 | ||||||
|  | @ -229,16 +437,10 @@ disregarding other properties. | ||||||
| 
 | 
 | ||||||
| One should not make one layer for benches with a backrest and one layer for benches without. This is confusing for users | One should not make one layer for benches with a backrest and one layer for benches without. This is confusing for users | ||||||
| and poses problems: what if the backrest status is unknown? What if it is some weird value? Also, it isn't possible to ' | and poses problems: what if the backrest status is unknown? What if it is some weird value? Also, it isn't possible to ' | ||||||
| move' an attribute to another layer. | move' a feature to another layer. | ||||||
| 
 | 
 | ||||||
| Instead, make one layer for one kind of object and change the icon based on attributes. | Instead, make one layer for one kind of object and change the icon based on attributes. | ||||||
| 
 | 
 | ||||||
| ### Using layers as filters |  | ||||||
| 
 |  | ||||||
| Using layers as filters - this doesn't work! |  | ||||||
| 
 |  | ||||||
| Use the `filter`-functionality instead. |  | ||||||
| 
 |  | ||||||
| ### Not reading the theme JSON specs | ### Not reading the theme JSON specs | ||||||
| 
 | 
 | ||||||
| There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for | There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for | ||||||
|  |  | ||||||
|  | @ -22,9 +22,8 @@ | ||||||
|   "#": "For more options and configuration, see the documentation in LayoutConfig.json", |   "#": "For more options and configuration, see the documentation in LayoutConfig.json", | ||||||
|   "#layers": "The list of layers is where most of the content will be. Either reuse an already existing layer by simply calling it's ID or define a whole new layer. An overview of builtin layers is at https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md#normal-layers", |   "#layers": "The list of layers is where most of the content will be. Either reuse an already existing layer by simply calling it's ID or define a whole new layer. An overview of builtin layers is at https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md#normal-layers", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "bench", |  | ||||||
|     { |     { | ||||||
|       "id": "a singular nound describing the feature, in english", |       "id": "a singular noun describing the feature, in english", | ||||||
|       "source": { |       "source": { | ||||||
|         "osmTags": { |         "osmTags": { | ||||||
|           "#": "For a description on which tags are possible, see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md", |           "#": "For a description on which tags are possible, see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md", | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
| import {QueryParameters} from "../Web/QueryParameters"; | import {QueryParameters} from "../Web/QueryParameters"; | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource"; | ||||||
| import {BBox} from "../BBox"; | import {BBox} from "../BBox"; | ||||||
|  | import Constants from "../../Models/Constants"; | ||||||
| 
 | 
 | ||||||
| export interface GeoLocationPointProperties { | export interface GeoLocationPointProperties { | ||||||
|     id: "gps", |     id: "gps", | ||||||
|  | @ -25,13 +26,11 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wether or not the geolocation is active, aka the user requested the current location |      * Wether or not the geolocation is active, aka the user requested the current location | ||||||
|      * @private |  | ||||||
|      */ |      */ | ||||||
|     private readonly _isActive: UIEventSource<boolean>; |     private readonly _isActive: UIEventSource<boolean>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user |      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user | ||||||
|      * @private |  | ||||||
|      */ |      */ | ||||||
|     private readonly _isLocked: UIEventSource<boolean>; |     private readonly _isLocked: UIEventSource<boolean>; | ||||||
| 
 | 
 | ||||||
|  | @ -54,9 +53,8 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs |      * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||||
|      * @private |  | ||||||
|      */ |      */ | ||||||
|     private _lastUserRequest: Date; |     private _lastUserRequest: UIEventSource<Date>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * A small flag on localstorage. If the user previously granted the geolocation, it will be set. |      * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||||
|  | @ -80,6 +78,8 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|     ) { |     ) { | ||||||
|         const currentGPSLocation = new UIEventSource<Coordinates>(undefined, "GPS-coordinate") |         const currentGPSLocation = new UIEventSource<Coordinates>(undefined, "GPS-coordinate") | ||||||
|         const leafletMap = state.leafletMap |         const leafletMap = state.leafletMap | ||||||
|  |         const initedAt = new Date() | ||||||
|  |         let autozoomDone = false; | ||||||
|         const hasLocation = currentGPSLocation.map( |         const hasLocation = currentGPSLocation.map( | ||||||
|             (location) => location !== undefined |             (location) => location !== undefined | ||||||
|         ); |         ); | ||||||
|  | @ -97,13 +97,28 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 |             const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 | ||||||
|             return timeDiff <= 3 |             return timeDiff <= 3 | ||||||
|         }) |         }) | ||||||
|  | 
 | ||||||
|  |         const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") | ||||||
|  |         const willFocus = lastClick.map(lastUserRequest => { | ||||||
|  |             const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000 | ||||||
|  |             if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             if (lastUserRequest === undefined) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000 | ||||||
|  |             return timeDiff <= Constants.zoomToLocationTimeout | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|         lastClick.addCallbackAndRunD(_ => { |         lastClick.addCallbackAndRunD(_ => { | ||||||
|             window.setTimeout(() => { |             window.setTimeout(() => { | ||||||
|                 if (lastClickWithinThreeSecs.data) { |                 if (lastClickWithinThreeSecs.data || willFocus.data) { | ||||||
|                     lastClick.ping() |                     lastClick.ping() | ||||||
|                 } |                 } | ||||||
|             }, 500) |             }, 500) | ||||||
|         }) |         }) | ||||||
|  | 
 | ||||||
|         super( |         super( | ||||||
|             hasLocation.map( |             hasLocation.map( | ||||||
|                 (hasLocationData) => { |                 (hasLocationData) => { | ||||||
|  | @ -116,7 +131,8 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|                     } |                     } | ||||||
|                     if (!hasLocationData) { |                     if (!hasLocationData) { | ||||||
|                         // Position not yet found but we are active: we spin to indicate activity
 |                         // Position not yet found but we are active: we spin to indicate activity
 | ||||||
|                         const icon = Svg.location_empty_svg() |                         // If will focus is active too, we indicate this differently
 | ||||||
|  |                         const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg() | ||||||
|                         icon.SetStyle("animation: spin 4s linear infinite;") |                         icon.SetStyle("animation: spin 4s linear infinite;") | ||||||
|                         return icon; |                         return icon; | ||||||
|                     } |                     } | ||||||
|  | @ -130,7 +146,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|                     // We have a location, so we show a dot in the center
 |                     // We have a location, so we show a dot in the center
 | ||||||
|                     return Svg.location_svg(); |                     return Svg.location_svg(); | ||||||
|                 }, |                 }, | ||||||
|                 [isActive, isLocked, permission, lastClickWithinThreeSecs] |                 [isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus] | ||||||
|             ) |             ) | ||||||
|         ); |         ); | ||||||
|         this.SetClass("mapcontrol") |         this.SetClass("mapcontrol") | ||||||
|  | @ -142,6 +158,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|         this._leafletMap = leafletMap; |         this._leafletMap = leafletMap; | ||||||
|         this._layoutToUse = state.layoutToUse; |         this._layoutToUse = state.layoutToUse; | ||||||
|         this._hasLocation = hasLocation; |         this._hasLocation = hasLocation; | ||||||
|  |         this._lastUserRequest = lastClick | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         const currentPointer = this._isActive.map( |         const currentPointer = this._isActive.map( | ||||||
|  | @ -183,7 +200,6 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             self.init(true, true); |             self.init(true, true); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") |  | ||||||
| 
 | 
 | ||||||
|         const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined |         const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined | ||||||
|         this.init(false, doAutoZoomToLocation); |         this.init(false, doAutoZoomToLocation); | ||||||
|  | @ -221,8 +237,12 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) |             self.currentLocation?.features?.setData([{feature, freshness: new Date()}]) | ||||||
| 
 | 
 | ||||||
|             const timeSinceRequest = |             const timeSinceRequest = | ||||||
|                 (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; |                 (new Date().getTime() - (self._lastUserRequest.data?.getTime() ?? 0)) / 1000; | ||||||
|             if (timeSinceRequest < 30) { | 
 | ||||||
|  |             if (willFocus.data) { | ||||||
|  |                 console.log("Zooming to user location: willFocus is set") | ||||||
|  |                 willFocus.setData(false) | ||||||
|  |                 autozoomDone = true; | ||||||
|                 self.MoveToCurrentLocation(16); |                 self.MoveToCurrentLocation(16); | ||||||
|             } else if (self._isLocked.data) { |             } else if (self._isLocked.data) { | ||||||
|                 self.MoveToCurrentLocation(); |                 self.MoveToCurrentLocation(); | ||||||
|  | @ -239,8 +259,8 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             self.MoveToCurrentLocation(16); |             self.MoveToCurrentLocation(16); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         if(typeof navigator === "undefined"){ |         if (typeof navigator === "undefined") { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -271,7 +291,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Moves to the currently loaded location. |      * Moves to the currently loaded location. | ||||||
|      *  |      * | ||||||
|      * // Should move to any location
 |      * // Should move to any location
 | ||||||
|      * let resultingLocation = undefined |      * let resultingLocation = undefined | ||||||
|      * let resultingzoom = 1 |      * let resultingzoom = 1 | ||||||
|  | @ -321,7 +341,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|      */ |      */ | ||||||
|     private MoveToCurrentLocation(targetZoom?: number) { |     private MoveToCurrentLocation(targetZoom?: number) { | ||||||
|         const location = this._currentGPSLocation.data; |         const location = this._currentGPSLocation.data; | ||||||
|         this._lastUserRequest = undefined; |         this._lastUserRequest.setData(undefined); | ||||||
| 
 | 
 | ||||||
|         if ( |         if ( | ||||||
|             this._currentGPSLocation.data.latitude === 0 && |             this._currentGPSLocation.data.latitude === 0 && | ||||||
|  | @ -341,14 +361,9 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (!inRange) { |         if (!inRange) { | ||||||
|             console.log( |             console.log("Not zooming to GPS location: out of bounds", b, location); | ||||||
|                 "Not zooming to GPS location: out of bounds", |  | ||||||
|                 b, |  | ||||||
|                 location |  | ||||||
|             ); |  | ||||||
|         } else { |         } else { | ||||||
|             const currentZoom = this._leafletMap.data.getZoom() |             const currentZoom = this._leafletMap.data.getZoom() | ||||||
| 
 |  | ||||||
|             this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom)); |             this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom)); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -356,7 +371,7 @@ export default class GeoLocationHandler extends VariableUiElement { | ||||||
|     private StartGeolocating(zoomToGPS = true) { |     private StartGeolocating(zoomToGPS = true) { | ||||||
|         const self = this; |         const self = this; | ||||||
| 
 | 
 | ||||||
|         this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); |         this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0)) | ||||||
|         if (self._permission.data === "denied") { |         if (self._permission.data === "denied") { | ||||||
|             self._previousLocationGrant.setData(""); |             self._previousLocationGrant.setData(""); | ||||||
|             self._isActive.setData(false) |             self._isActive.setData(false) | ||||||
|  |  | ||||||
|  | @ -76,9 +76,9 @@ export class OsmPreferences { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         function updateData(l: number) { |         function updateData(l: number) { | ||||||
|             if (l === undefined) { |             if(Object.keys(self.preferences.data).length === 0){ | ||||||
|                 source.setData(undefined); |                 // The preferences are still empty - they are not yet updated, so we delay updating for now 
 | ||||||
|                 return; |                 return | ||||||
|             } |             } | ||||||
|             const prefsCount = Number(l); |             const prefsCount = Number(l); | ||||||
|             if (prefsCount > 100) { |             if (prefsCount > 100) { | ||||||
|  | @ -86,7 +86,11 @@ export class OsmPreferences { | ||||||
|             } |             } | ||||||
|             let str = ""; |             let str = ""; | ||||||
|             for (let i = 0; i < prefsCount; i++) { |             for (let i = 0; i < prefsCount; i++) { | ||||||
|                 str += self.GetPreference(allStartWith + "-" + i, "").data; |                 const key = allStartWith + "-" + i | ||||||
|  |                 if(self.preferences.data[key] === undefined){ | ||||||
|  |                     console.warn("Detected a broken combined preference:", key, "is undefined", self.preferences) | ||||||
|  |                 } | ||||||
|  |                 str += self.preferences.data[key] ?? ""; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             source.setData(str); |             source.setData(str); | ||||||
|  | @ -95,7 +99,9 @@ export class OsmPreferences { | ||||||
|         length.addCallback(l => { |         length.addCallback(l => { | ||||||
|             updateData(Number(l)); |             updateData(Number(l)); | ||||||
|         }); |         }); | ||||||
|         updateData(Number(length.data)); |         this.preferences.addCallbackAndRun(_ => { | ||||||
|  |             updateData(Number(length.data)); | ||||||
|  |         }) | ||||||
| 
 | 
 | ||||||
|         return source; |         return source; | ||||||
|     } |     } | ||||||
|  | @ -127,7 +133,8 @@ export class OsmPreferences { | ||||||
|     public ClearPreferences() { |     public ClearPreferences() { | ||||||
|         let isRunning = false; |         let isRunning = false; | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.preferences.addCallbackAndRun(prefs => { |         this.preferences.addCallback(prefs => { | ||||||
|  |             console.log("Cleaning preferences...") | ||||||
|             if (Object.keys(prefs).length == 0) { |             if (Object.keys(prefs).length == 0) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  | @ -135,19 +142,17 @@ export class OsmPreferences { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             isRunning = true |             isRunning = true | ||||||
|             const prefixes = ["mapcomplete-installed-theme", "mapcomplete-installed-themes-", "mapcomplete-current-open-changeset", "mapcomplete-personal-theme-layer"] |             const prefixes = ["mapcomplete-"] | ||||||
|             for (const key in prefs) { |             for (const key in prefs) { | ||||||
|                 for (const prefix of prefixes) { |                 const matches = prefixes.some(prefix => key.startsWith(prefix)) | ||||||
|                     if (key.startsWith(prefix)) { |                 if (matches) { | ||||||
|                         console.log("Clearing ", key) |                     console.log("Clearing ", key) | ||||||
|                         self.GetPreference(key, "").setData("") |                     self.GetPreference(key, "").setData("") | ||||||
| 
 | 
 | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             isRunning = false; |             isRunning = false; | ||||||
|             return true; |             return; | ||||||
| 
 |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -173,7 +178,6 @@ export class OsmPreferences { | ||||||
|             // For differing values, the server overrides local changes
 |             // For differing values, the server overrides local changes
 | ||||||
|             self.preferenceSources.forEach((preference, key) => { |             self.preferenceSources.forEach((preference, key) => { | ||||||
|                 const osmValue = self.preferences.data[key] |                 const osmValue = self.preferences.data[key] | ||||||
|                     console.log("Sending value to osm:", key," osm has: ", osmValue, " local has: ", preference.data) |  | ||||||
|                 if(osmValue === undefined && preference.data !== undefined){ |                 if(osmValue === undefined && preference.data !== undefined){ | ||||||
|                     // OSM doesn't know this value yet
 |                     // OSM doesn't know this value yet
 | ||||||
|                     self.UploadPreference(key, preference.data) |                     self.UploadPreference(key, preference.data) | ||||||
|  |  | ||||||
|  | @ -43,29 +43,34 @@ export default class ElementsState extends FeatureSwitchState { | ||||||
| 
 | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
|  |          | ||||||
|  |          | ||||||
|  |             function localStorageSynced(key: string, deflt: number, docs: string ): UIEventSource<number>{ | ||||||
|  |                 const localStorage = LocalStorageSource.Get(key) | ||||||
|  |                 const previousValue = localStorage.data | ||||||
|  |                 const src = UIEventSource.asFloat( | ||||||
|  |                     QueryParameters.GetQueryParameter( | ||||||
|  |                         key, | ||||||
|  |                         "" + deflt, | ||||||
|  |                         docs | ||||||
|  |                     ).syncWith(localStorage) | ||||||
|  |                 ); | ||||||
|  |                  | ||||||
|  |                 if(src.data === deflt){ | ||||||
|  |                     const prev = Number(previousValue) | ||||||
|  |                     if(!isNaN(prev)){ | ||||||
|  |                         src.setData(prev) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 return src; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             // -- Location control initialization
 |             // -- Location control initialization
 | ||||||
|             const zoom = UIEventSource.asFloat( |             const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level") | ||||||
|                 QueryParameters.GetQueryParameter( |             const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude") | ||||||
|                     "z", |             const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app") | ||||||
|                     "" + (layoutToUse?.startZoom ?? 1), | 
 | ||||||
|                     "The initial/current zoom level" |  | ||||||
|                 ).syncWith(LocalStorageSource.Get("zoom")) |  | ||||||
|             ); |  | ||||||
|             const lat = UIEventSource.asFloat( |  | ||||||
|                 QueryParameters.GetQueryParameter( |  | ||||||
|                     "lat", |  | ||||||
|                     "" + (layoutToUse?.startLat ?? 0), |  | ||||||
|                     "The initial/current latitude" |  | ||||||
|                 ).syncWith(LocalStorageSource.Get("lat")) |  | ||||||
|             ); |  | ||||||
|             const lon = UIEventSource.asFloat( |  | ||||||
|                 QueryParameters.GetQueryParameter( |  | ||||||
|                     "lon", |  | ||||||
|                     "" + (layoutToUse?.startLon ?? 0), |  | ||||||
|                     "The initial/current longitude of the app" |  | ||||||
|                 ).syncWith(LocalStorageSource.Get("lon")) |  | ||||||
|             ); |  | ||||||
| 
 | 
 | ||||||
|             this.locationControl.setData({ |             this.locationControl.setData({ | ||||||
|                 zoom: Utils.asFloat(zoom.data), |                 zoom: Utils.asFloat(zoom.data), | ||||||
|  | @ -73,7 +78,7 @@ export default class ElementsState extends FeatureSwitchState { | ||||||
|                 lon: Utils.asFloat(lon.data), |                 lon: Utils.asFloat(lon.data), | ||||||
|             }) |             }) | ||||||
|             this.locationControl.addCallback((latlonz) => { |             this.locationControl.addCallback((latlonz) => { | ||||||
|                 // Sync th location controls
 |                 // Sync the location controls
 | ||||||
|                 zoom.setData(latlonz.zoom); |                 zoom.setData(latlonz.zoom); | ||||||
|                 lat.setData(latlonz.lat); |                 lat.setData(latlonz.lat); | ||||||
|                 lon.setData(latlonz.lon); |                 lon.setData(latlonz.lon); | ||||||
|  |  | ||||||
|  | @ -8,6 +8,13 @@ export class And extends TagsFilter { | ||||||
|         super(); |         super(); | ||||||
|         this.and = and |         this.and = and | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     public static construct(and: TagsFilter[]): TagsFilter{ | ||||||
|  |         if(and.length === 1){ | ||||||
|  |             return and[0] | ||||||
|  |         } | ||||||
|  |         return new And(and) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private static combine(filter: string, choices: string[]): string[] { |     private static combine(filter: string, choices: string[]): string[] { | ||||||
|         const values = []; |         const values = []; | ||||||
|  | @ -45,7 +52,7 @@ export class And extends TagsFilter { | ||||||
|      * import {RegexTag} from "./RegexTag"; |      * import {RegexTag} from "./RegexTag"; | ||||||
|      *  |      *  | ||||||
|      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) |      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) | ||||||
|      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]" ]
 |      * and.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]" ]
 | ||||||
|      */ |      */ | ||||||
|     asOverpass(): string[] { |     asOverpass(): string[] { | ||||||
|         let allChoices: string[] = null; |         let allChoices: string[] = null; | ||||||
|  | @ -87,17 +94,17 @@ export class And extends TagsFilter { | ||||||
|      * ]) |      * ]) | ||||||
|      * const t1 = new And([new Tag("valves", "A")]) |      * const t1 = new And([new Tag("valves", "A")]) | ||||||
|      * const t2 = new And([new Tag("valves", "B")]) |      * const t2 = new And([new Tag("valves", "B")]) | ||||||
|      * t0.isEquivalent(t0) // => true
 |      * t0.shadows(t0) // => true
 | ||||||
|      * t1.isEquivalent(t1) // => true
 |      * t1.shadows(t1) // => true
 | ||||||
|      * t2.isEquivalent(t2) // => true
 |      * t2.shadows(t2) // => true
 | ||||||
|      * t0.isEquivalent(t1) // => false
 |      * t0.shadows(t1) // => false
 | ||||||
|      * t0.isEquivalent(t2) // => false
 |      * t0.shadows(t2) // => false
 | ||||||
|      * t1.isEquivalent(t0) // => false
 |      * t1.shadows(t0) // => false
 | ||||||
|      * t1.isEquivalent(t2) // => false
 |      * t1.shadows(t2) // => false
 | ||||||
|      * t2.isEquivalent(t0) // => false
 |      * t2.shadows(t0) // => false
 | ||||||
|      * t2.isEquivalent(t1) // => false
 |      * t2.shadows(t1) // => false
 | ||||||
|      */ |      */ | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof And)) { |         if (!(other instanceof And)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -105,7 +112,7 @@ export class And extends TagsFilter { | ||||||
|         for (const selfTag of this.and) { |         for (const selfTag of this.and) { | ||||||
|             let matchFound = false; |             let matchFound = false; | ||||||
|             for (const otherTag of other.and) { |             for (const otherTag of other.and) { | ||||||
|                 matchFound = selfTag.isEquivalent(otherTag); |                 matchFound = selfTag.shadows(otherTag); | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|  | @ -118,7 +125,7 @@ export class And extends TagsFilter { | ||||||
|         for (const otherTag of other.and) { |         for (const otherTag of other.and) { | ||||||
|             let matchFound = false; |             let matchFound = false; | ||||||
|             for (const selfTag of this.and) { |             for (const selfTag of this.and) { | ||||||
|                 matchFound = selfTag.isEquivalent(otherTag); |                 matchFound = selfTag.shadows(otherTag); | ||||||
|                 if (matchFound) { |                 if (matchFound) { | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|  | @ -148,23 +155,90 @@ export class And extends TagsFilter { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * IN some contexts, some expressions can be considered true, e.g. | ||||||
|  |      * (X=Y | (A=B & X=Y)) | ||||||
|  |      *        ^---------^ | ||||||
|  |      * When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise. | ||||||
|  |      * This means that the entire 'AND' is considered FALSE | ||||||
|  |      *  | ||||||
|  |      * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
 | ||||||
|  |      * new And([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
 | ||||||
|  |      * new And([ new RegexTag("key",/^..*$/) ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // => new Tag("other_key","value")
 | ||||||
|  |      * new And([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 | ||||||
|  |      *  | ||||||
|  |      * // should remove 'club~*' if we know that 'club=climbing'
 | ||||||
|  |      * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) | ||||||
|  |      * expr.removePhraseConsideredKnown(new Tag("club","climbing"), true) // => new Tag("sport","climbing")
 | ||||||
|  |      *  | ||||||
|  |      * const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} ) | ||||||
|  |      * expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
 | ||||||
|  |      */ | ||||||
|  |     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||||
|  |         const newAnds: TagsFilter[] = [] | ||||||
|  |         for (const tag of this.and) { | ||||||
|  |             if(tag instanceof And){ | ||||||
|  |                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||||
|  |             } | ||||||
|  |             if(tag instanceof Or){ | ||||||
|  |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|  |                 if(r === true){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(r === false){ | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 newAnds.push(r) | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(value && knownExpression.shadows(tag)){ | ||||||
|  |                 /** | ||||||
|  |                  * At this point, we do know that 'knownExpression' is true in every case | ||||||
|  |                  * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, | ||||||
|  |                  * we can be sure that 'tag' is true as well. | ||||||
|  |                  *  | ||||||
|  |                  * "True" is the neutral element in an AND, so we can skip the tag | ||||||
|  |                  */ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(!value && tag.shadows(knownExpression)){ | ||||||
|  | 
 | ||||||
|  |                 /** | ||||||
|  |                  * We know that knownExpression is unmet. | ||||||
|  |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  |                  * then tag CANNOT be met too, as known expression is not met. | ||||||
|  |                  *  | ||||||
|  |                  * This implies that 'tag' must be false too! | ||||||
|  |                  */ | ||||||
|  | 
 | ||||||
|  |                 // false is the element which absorbs all
 | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             newAnds.push(tag) | ||||||
|  |         } | ||||||
|  |         if(newAnds.length === 0){ | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         return And.construct(newAnds) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|         if(this.and.length === 0){ |         if(this.and.length === 0){ | ||||||
|             return true |             return true | ||||||
|         } |         } | ||||||
|         const optimized = this.and.map(t => t.optimize()) |         const optimizedRaw = this.and.map(t => t.optimize()) | ||||||
|  |             .filter(t => t !== true /* true is the neutral element in an AND, we drop them*/ ) | ||||||
|  |         if(optimizedRaw.some(t => t === false)){ | ||||||
|  |             // We have an AND with a contained false: this is always 'false'
 | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         const optimized = <TagsFilter[]> optimizedRaw; | ||||||
|          |          | ||||||
|         const newAnds : TagsFilter[] = [] |         const newAnds : TagsFilter[] = [] | ||||||
|          |          | ||||||
|         let containedOrs : Or[] = [] |         let containedOrs : Or[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if(tf === false){ |  | ||||||
|                 return false |  | ||||||
|             } |  | ||||||
|             if(tf === true){ |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             if(tf instanceof And){ |             if(tf instanceof And){ | ||||||
|                 newAnds.push(...tf.and) |                 newAnds.push(...tf.and) | ||||||
|             }else if(tf instanceof Or){ |             }else if(tf instanceof Or){ | ||||||
|  | @ -173,27 +247,56 @@ export class And extends TagsFilter { | ||||||
|                 newAnds.push(tf) |                 newAnds.push(tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |          | ||||||
|         containedOrs = containedOrs.filter(ca => { |         { | ||||||
|             for (const element of ca.or) { |             let dirty = false; | ||||||
|                 if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){ |             do { | ||||||
|                     // At least one part of the 'OR' is matched by the outer or, so this means that this OR isn't needed at all
 |                 const cleanedContainedOrs : Or[] = [] | ||||||
|                     // XY & (XY | AB) === XY
 |                 outer: for (let containedOr of containedOrs) { | ||||||
|                     return false |                     for (const known of newAnds) { | ||||||
|  |                         // input for optimazation: (K=V & (X=Y | K=V))
 | ||||||
|  |                         // containedOr: (X=Y | K=V)
 | ||||||
|  |                         // newAnds (and thus known): (K=V) --> true
 | ||||||
|  |                         const cleaned = containedOr.removePhraseConsideredKnown(known, true) | ||||||
|  |                         if (cleaned === true) { | ||||||
|  |                             // The neutral element within an AND
 | ||||||
|  |                             continue outer // skip addition too
 | ||||||
|  |                         } | ||||||
|  |                         if (cleaned === false) { | ||||||
|  |                             // zero element
 | ||||||
|  |                             return false | ||||||
|  |                         } | ||||||
|  |                         if (cleaned instanceof Or) { | ||||||
|  |                             containedOr = cleaned | ||||||
|  |                             continue | ||||||
|  |                         } | ||||||
|  |                         // the 'or' dissolved into a normal tag -> it has to be added to the newAnds
 | ||||||
|  |                         newAnds.push(cleaned) | ||||||
|  |                         dirty = true; // rerun this algo later on
 | ||||||
|  |                         continue outer; | ||||||
|  |                     } | ||||||
|  |                     cleanedContainedOrs.push(containedOr) | ||||||
|                 } |                 } | ||||||
|             } |                 containedOrs = cleanedContainedOrs | ||||||
|             return true; |             } while(dirty) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |          | ||||||
|  |         containedOrs = containedOrs.filter(ca => { | ||||||
|  |             const isShadowed = TagUtils.containsEquivalents(newAnds, ca.or) | ||||||
|  |             // If 'isShadowed', then at least one part of the 'OR' is matched by the outer and, so this means that this OR isn't needed at all
 | ||||||
|  |             // XY & (XY | AB) === XY
 | ||||||
|  |             return !isShadowed; | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         // Extract common keys from the OR
 |         // Extract common keys from the OR
 | ||||||
|         if(containedOrs.length === 1){ |         if(containedOrs.length === 1){ | ||||||
|             newAnds.push(containedOrs[0]) |             newAnds.push(containedOrs[0]) | ||||||
|         } |         }else if(containedOrs.length > 1){ | ||||||
|         if(containedOrs.length > 1){ |  | ||||||
|             let commonValues : TagsFilter [] = containedOrs[0].or |             let commonValues : TagsFilter [] = containedOrs[0].or | ||||||
|             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){ |             for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++){ | ||||||
|                 const containedOr = containedOrs[i]; |                 const containedOr = containedOrs[i]; | ||||||
|                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.isEquivalent(cv))) |                 commonValues = commonValues.filter(cv => containedOr.or.some(candidate => candidate.shadows(cv))) | ||||||
|             } |             } | ||||||
|             if(commonValues.length === 0){ |             if(commonValues.length === 0){ | ||||||
|                 newAnds.push(...containedOrs) |                 newAnds.push(...containedOrs) | ||||||
|  | @ -201,19 +304,11 @@ export class And extends TagsFilter { | ||||||
|                 const newOrs: TagsFilter[] = [] |                 const newOrs: TagsFilter[] = [] | ||||||
|                 for (const containedOr of containedOrs) { |                 for (const containedOr of containedOrs) { | ||||||
|                     const elements = containedOr.or |                     const elements = containedOr.or | ||||||
|                         .filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate))) |                         .filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||||
|                     const or = new Or(elements).optimize() |                     newOrs.push(Or.construct(elements)) | ||||||
|                     if(or === true){ |  | ||||||
|                         // neutral element
 |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     if(or === false){ |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
|                     newOrs.push(or) |  | ||||||
|                 } |                 } | ||||||
|                  |                  | ||||||
|                 commonValues.push(new And(newOrs)) |                 commonValues.push(And.construct(newOrs)) | ||||||
|                 const result = new Or(commonValues).optimize() |                 const result = new Or(commonValues).optimize() | ||||||
|                 if(result === false){ |                 if(result === false){ | ||||||
|                     return false |                     return false | ||||||
|  | @ -224,16 +319,22 @@ export class And extends TagsFilter { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |         if(newAnds.length === 0){ | ||||||
|         if(newAnds.length === 1){ |             return true | ||||||
|             return newAnds[0] |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if(TagUtils.ContainsOppositeTags(newAnds)){ | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |          | ||||||
|         TagUtils.sortFilters(newAnds, true) |         TagUtils.sortFilters(newAnds, true) | ||||||
|          |          | ||||||
|         return new And(newAnds) |         return And.construct(newAnds) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return !this.and.some(t => !t.isNegative()); |         return !this.and.some(t => !t.isNegative()); | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |      | ||||||
| } | } | ||||||
|  | @ -23,7 +23,7 @@ export default class ComparingTag implements TagsFilter { | ||||||
|         throw "A comparable tag can not be used as overpass filter" |         throw "A comparable tag can not be used as overpass filter" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         return other === this; |         return other === this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										156
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							
							
						
						
									
										156
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							|  | @ -11,6 +11,14 @@ export class Or extends TagsFilter { | ||||||
|         this.or = or; |         this.or = or; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static construct(or: TagsFilter[]): TagsFilter{ | ||||||
|  |         if(or.length === 1){ | ||||||
|  |             return or[0] | ||||||
|  |         } | ||||||
|  |         return new Or(or) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         for (const tagsFilter of this.or) { |         for (const tagsFilter of this.or) { | ||||||
|             if (tagsFilter.matchesProperties(properties)) { |             if (tagsFilter.matchesProperties(properties)) { | ||||||
|  | @ -28,7 +36,7 @@ export class Or extends TagsFilter { | ||||||
|      *  |      *  | ||||||
|      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) |      * const and = new And([new Tag("boundary","protected_area"), new RegexTag("protect_class","98",true)]) | ||||||
|      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) |      * const or = new Or([and, new Tag("leisure", "nature_reserve"]) | ||||||
|      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!~\"^98$\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 |      * or.asOverpass() // => [ "[\"boundary\"=\"protected_area\"][\"protect_class\"!=\"98\"]", "[\"leisure\"=\"nature_reserve\"]" ]
 | ||||||
|      *  |      *  | ||||||
|      * // should fuse nested ors into a single list
 |      * // should fuse nested ors into a single list
 | ||||||
|      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) |      * const or = new Or([new Tag("key","value"), new Or([new Tag("key1","value1"), new Tag("key2","value2")])]) | ||||||
|  | @ -51,14 +59,14 @@ export class Or extends TagsFilter { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof Or) { |         if (other instanceof Or) { | ||||||
| 
 | 
 | ||||||
|             for (const selfTag of this.or) { |             for (const selfTag of this.or) { | ||||||
|                 let matchFound = false; |                 let matchFound = false; | ||||||
|                 for (let i = 0; i < other.or.length && !matchFound; i++) { |                 for (let i = 0; i < other.or.length && !matchFound; i++) { | ||||||
|                     let otherTag = other.or[i]; |                     let otherTag = other.or[i]; | ||||||
|                     matchFound = selfTag.isEquivalent(otherTag); |                     matchFound = selfTag.shadows(otherTag); | ||||||
|                 } |                 } | ||||||
|                 if (!matchFound) { |                 if (!matchFound) { | ||||||
|                     return false; |                     return false; | ||||||
|  | @ -85,45 +93,127 @@ export class Or extends TagsFilter { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * IN some contexts, some expressions can be considered true, e.g. | ||||||
|  |      * (X=Y & (A=B | X=Y)) | ||||||
|  |      *        ^---------^ | ||||||
|  |      * When the evaluation hits (A=B | X=Y), we know _for sure_ that X=Y _does match, as it would have failed the first clause otherwise. | ||||||
|  |      * This means we can safely ignore this in the OR | ||||||
|  |      *  | ||||||
|  |      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), true) // =>true
 | ||||||
|  |      * new Or([ new Tag("key","value") ,new Tag("other_key","value")]).removePhraseConsideredKnown(new Tag("key","value"), false) // => new Tag("other_key","value")
 | ||||||
|  |      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), true) // => true
 | ||||||
|  |      * new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
 | ||||||
|  |      * new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")]).removePhraseConsideredKnown(new Tag("foo","bar"), false) // => new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")])
 | ||||||
|  |      */ | ||||||
|  |     removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): TagsFilter | boolean { | ||||||
|  |         const newOrs: TagsFilter[] = [] | ||||||
|  |         for (const tag of this.or) { | ||||||
|  |             if(tag instanceof Or){ | ||||||
|  |                 throw "Optimize expressions before using removePhraseConsideredKnown" | ||||||
|  |             } | ||||||
|  |             if(tag instanceof And){ | ||||||
|  |                 const r = tag.removePhraseConsideredKnown(knownExpression, value) | ||||||
|  |                 if(r === false){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(r === true){ | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |                 newOrs.push(r) | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if(value && knownExpression.shadows(tag)){ | ||||||
|  |                 /** | ||||||
|  |                  * At this point, we do know that 'knownExpression' is true in every case | ||||||
|  |                  * As `shadows` does define that 'tag' MUST be true if 'knownExpression' is true, | ||||||
|  |                  * we can be sure that 'tag' is true as well. | ||||||
|  |                  * | ||||||
|  |                  * "True" is the absorbing element in an OR, so we can return true | ||||||
|  |                  */ | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |             if(!value && tag.shadows(knownExpression)){ | ||||||
|  | 
 | ||||||
|  |                 /** | ||||||
|  |                  * We know that knownExpression is unmet. | ||||||
|  |                  * if the tag shadows 'knownExpression' (which is the case when control flows gets here), | ||||||
|  |                  * then tag CANNOT be met too, as known expression is not met. | ||||||
|  |                  * | ||||||
|  |                  * This implies that 'tag' must be false too! | ||||||
|  |                  * false is the neutral element in an OR | ||||||
|  |                  */ | ||||||
|  |                continue | ||||||
|  |             } | ||||||
|  |             newOrs.push(tag) | ||||||
|  |         } | ||||||
|  |         if(newOrs.length === 0){ | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         return Or.construct(newOrs) | ||||||
|  |     } | ||||||
|  |      | ||||||
|     optimize(): TagsFilter | boolean { |     optimize(): TagsFilter | boolean { | ||||||
|          |          | ||||||
|         if(this.or.length === 0){ |         if(this.or.length === 0){ | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         const optimized = this.or.map(t => t.optimize()) |         const optimizedRaw = this.or.map(t => t.optimize()) | ||||||
|  |             .filter(t => t !== false /* false is the neutral element in an OR, we drop them*/ ) | ||||||
|  |         if(optimizedRaw.some(t => t === true)){ | ||||||
|  |             // We have an OR with a contained true: this is always 'true'
 | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         const optimized = <TagsFilter[]> optimizedRaw; | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
|         const newOrs : TagsFilter[] = [] |         const newOrs : TagsFilter[] = [] | ||||||
| 
 |  | ||||||
|         let containedAnds : And[] = [] |         let containedAnds : And[] = [] | ||||||
|         for (const tf of optimized) { |         for (const tf of optimized) { | ||||||
|             if(tf === true){ |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|             if(tf === false){ |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if(tf instanceof Or){ |             if(tf instanceof Or){ | ||||||
|  |                 // expand all the nested ors...
 | ||||||
|                 newOrs.push(...tf.or) |                 newOrs.push(...tf.or) | ||||||
|             }else if(tf instanceof And){ |             }else if(tf instanceof And){ | ||||||
|  |                 // partition of all the ands
 | ||||||
|                 containedAnds.push(tf) |                 containedAnds.push(tf) | ||||||
|             } else { |             } else { | ||||||
|                 newOrs.push(tf) |                 newOrs.push(tf) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         containedAnds = containedAnds.filter(ca => { |         { | ||||||
|             for (const element of ca.and) { |             let dirty = false; | ||||||
|                 if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){ |             do { | ||||||
|                     // At least one part of the 'AND' is matched by the outer or, so this means that this OR isn't needed at all
 |                 const cleanedContainedANds : And[] = [] | ||||||
|                     // XY | (XY & AB) === XY
 |                 outer: for (let containedAnd of containedAnds) { | ||||||
|                     return false |                     for (const known of newOrs) { | ||||||
|  |                         // input for optimazation: (K=V | (X=Y & K=V))
 | ||||||
|  |                         // containedAnd: (X=Y & K=V)
 | ||||||
|  |                         // newOrs (and thus known): (K=V) --> false
 | ||||||
|  |                         const cleaned = containedAnd.removePhraseConsideredKnown(known, false) | ||||||
|  |                         if (cleaned === false) { | ||||||
|  |                             // The neutral element within an OR
 | ||||||
|  |                             continue outer // skip addition too
 | ||||||
|  |                         } | ||||||
|  |                         if (cleaned === true) { | ||||||
|  |                             // zero element
 | ||||||
|  |                             return true | ||||||
|  |                         } | ||||||
|  |                         if (cleaned instanceof And) { | ||||||
|  |                             containedAnd = cleaned | ||||||
|  |                             continue // clean up with the other known values
 | ||||||
|  |                         } | ||||||
|  |                         // the 'and' dissolved into a normal tag -> it has to be added to the newOrs
 | ||||||
|  |                         newOrs.push(cleaned) | ||||||
|  |                         dirty = true; // rerun this algo later on
 | ||||||
|  |                         continue outer; | ||||||
|  |                     } | ||||||
|  |                     cleanedContainedANds.push(containedAnd) | ||||||
|                 } |                 } | ||||||
|             } |                 containedAnds = cleanedContainedANds | ||||||
|             return true; |             } while(dirty) | ||||||
|         }) |         } | ||||||
| 
 |  | ||||||
|         // Extract common keys from the ANDS
 |         // Extract common keys from the ANDS
 | ||||||
|         if(containedAnds.length === 1){ |         if(containedAnds.length === 1){ | ||||||
|             newOrs.push(containedAnds[0]) |             newOrs.push(containedAnds[0]) | ||||||
|  | @ -131,40 +221,46 @@ export class Or extends TagsFilter { | ||||||
|             let commonValues : TagsFilter [] = containedAnds[0].and |             let commonValues : TagsFilter [] = containedAnds[0].and | ||||||
|             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ |             for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++){ | ||||||
|                 const containedAnd = containedAnds[i]; |                 const containedAnd = containedAnds[i]; | ||||||
|                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.isEquivalent(cv))) |                 commonValues = commonValues.filter(cv => containedAnd.and.some(candidate => candidate.shadows(cv))) | ||||||
|             } |             } | ||||||
|             if(commonValues.length === 0){ |             if(commonValues.length === 0){ | ||||||
|                 newOrs.push(...containedAnds) |                 newOrs.push(...containedAnds) | ||||||
|             }else{ |             }else{ | ||||||
|                 const newAnds: TagsFilter[] = [] |                 const newAnds: TagsFilter[] = [] | ||||||
|                 for (const containedAnd of containedAnds) { |                 for (const containedAnd of containedAnds) { | ||||||
|                     const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate))) |                     const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate))) | ||||||
|                     newAnds.push(new And(elements)) |                     newAnds.push(And.construct(elements)) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 commonValues.push(new Or(newAnds)) |                 commonValues.push(Or.construct(newAnds)) | ||||||
|                 const result = new And(commonValues).optimize() |                 const result = new And(commonValues).optimize() | ||||||
|                 if(result === true){ |                 if(result === true){ | ||||||
|                     return true |                     return true | ||||||
|                 }else if(result === false){ |                 }else if(result === false){ | ||||||
|                     // neutral element: skip
 |                     // neutral element: skip
 | ||||||
|                 }else{ |                 }else{ | ||||||
|                     newOrs.push(new And(commonValues)) |                     newOrs.push(And.construct(commonValues)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(newOrs.length === 1){ |         if(newOrs.length === 0){ | ||||||
|             return newOrs[0] |             return false | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if(TagUtils.ContainsOppositeTags(newOrs)){ | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |          | ||||||
|         TagUtils.sortFilters(newOrs, false) |         TagUtils.sortFilters(newOrs, false) | ||||||
| 
 | 
 | ||||||
|         return new Or(newOrs) |         return Or.construct(newOrs) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     isNegative(): boolean { |     isNegative(): boolean { | ||||||
|         return this.or.some(t => t.isNegative()); |         return this.or.some(t => t.isNegative()); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,13 +10,6 @@ export class RegexTag extends TagsFilter { | ||||||
|     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { |     constructor(key: string | RegExp, value: RegExp | string, invert: boolean = false) { | ||||||
|         super(); |         super(); | ||||||
|         this.key = key; |         this.key = key; | ||||||
|         if (typeof value === "string") { |  | ||||||
|             if (value.indexOf("^") < 0 && value.indexOf("$") < 0) { |  | ||||||
|                 value = "^" + value + "$" |  | ||||||
|             } |  | ||||||
|             value = new RegExp(value) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.value = value; |         this.value = value; | ||||||
|         this.invert = invert; |         this.invert = invert; | ||||||
|         this.matchesEmpty = RegexTag.doesMatch("", this.value); |         this.matchesEmpty = RegexTag.doesMatch("", this.value); | ||||||
|  | @ -79,14 +72,14 @@ export class RegexTag extends TagsFilter { | ||||||
|     /**  |     /**  | ||||||
|      * Checks if this tag matches the given properties |      * Checks if this tag matches the given properties | ||||||
|      *  |      *  | ||||||
|      * const isNotEmpty = new RegexTag("key","^$", true); |      * const isNotEmpty = new RegexTag("key",/^$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => true
 | ||||||
|      * isNotEmpty.matchesProperties({"key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": ""}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"other_key": "value"}) // => false
 | ||||||
|      *  |      *  | ||||||
|      * const isNotEmpty = new RegexTag("key","^..*$", true); |      * const isNotEmpty = new RegexTag("key",/^..*$/, true); | ||||||
|      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "value"}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 |      * isNotEmpty.matchesProperties({"key": "other_value"}) // => false
 | ||||||
|      * isNotEmpty.matchesProperties({"key": ""}) // => true
 |      * isNotEmpty.matchesProperties({"key": ""}) // => true
 | ||||||
|  | @ -121,6 +114,9 @@ export class RegexTag extends TagsFilter { | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 |      * importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
 | ||||||
|      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 |      * importMatch.matchesProperties({"tags": "amenity=bench"}) // =>false
 | ||||||
|  |      *  | ||||||
|  |      * new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false
 | ||||||
|  |      * new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true
 | ||||||
|      */ |      */ | ||||||
|     matchesProperties(tags: any): boolean { |     matchesProperties(tags: any): boolean { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|  | @ -147,17 +143,87 @@ export class RegexTag extends TagsFilter { | ||||||
| 
 | 
 | ||||||
|     asHumanString() { |     asHumanString() { | ||||||
|         if (typeof this.key === "string") { |         if (typeof this.key === "string") { | ||||||
|             return `${this.key}${this.invert ? "!" : ""}~${RegexTag.source(this.value)}`; |             const oper = typeof this.value === "string" ? "=" : "~" | ||||||
|  |             return `${this.key}${this.invert ? "!" : ""}${oper}${RegexTag.source(this.value)}`; | ||||||
|         } |         } | ||||||
|         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` |         return `${this.key.source}${this.invert ? "!" : ""}~~${RegexTag.source(this.value)}` | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     /** | ||||||
|  |      *  | ||||||
|  |      * new RegexTag("key","value").shadows(new Tag("key","value")) // => true
 | ||||||
|  |      * new RegexTag("key",/value/).shadows(new RegexTag("key","value")) // => true
 | ||||||
|  |      * new RegexTag("key",/^..*$/).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key",/^..*$/).shadows(new Tag("other_key","value")) // => false
 | ||||||
|  |      * new RegexTag("key", /^a+$/).shadows(new Tag("key", "a")) // => false
 | ||||||
|  |      *  | ||||||
|  |      *  | ||||||
|  |      * // should not shadow too eagerly: the first tag might match 'key=abc', the second won't
 | ||||||
|  |      *  new RegexTag("key", /^..*$/).shadows(new Tag("key", "some_value")) // => false
 | ||||||
|  |      *  | ||||||
|  |      * // should handle 'invert'
 | ||||||
|  |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true
 | ||||||
|  |      * new RegexTag("key","value", true).shadows(new Tag("key","value")) // => false
 | ||||||
|  |      * new RegexTag("key","value", true).shadows(new Tag("key","some_other_value")) // => false
 | ||||||
|  |      */ | ||||||
|  |     shadows(other: TagsFilter): boolean { | ||||||
|         if (other instanceof RegexTag) { |         if (other instanceof RegexTag) { | ||||||
|             return other.asHumanString() == this.asHumanString(); |             if((other.key["source"] ?? other.key) !== (this.key["source"] ?? this.key) ){ | ||||||
|  |                 // Keys don't match, never shadowing
 | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |             if((other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) && this.invert  == other.invert ){ | ||||||
|  |                 // Values (and inverts) match
 | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             if(typeof other.value ==="string"){ | ||||||
|  |                 const valuesMatch = RegexTag.doesMatch(other.value, this.value) | ||||||
|  |                 if(!this.invert && !other.invert){ | ||||||
|  |                     // this: key~value, other: key=value
 | ||||||
|  |                     return valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(this.invert && !other.invert){ | ||||||
|  |                     // this: key!~value, other: key=value
 | ||||||
|  |                     return !valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(!this.invert && other.invert){ | ||||||
|  |                     // this: key~value, other: key!=value
 | ||||||
|  |                     return !valuesMatch | ||||||
|  |                 } | ||||||
|  |                 if(!this.invert && !other.invert){ | ||||||
|  |                     // this: key!~value, other: key!=value
 | ||||||
|  |                     return valuesMatch | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|         if (other instanceof Tag) { |         if (other instanceof Tag) { | ||||||
|             return RegexTag.doesMatch(other.key, this.key) && RegexTag.doesMatch(other.value, this.value); |             if(!RegexTag.doesMatch(other.key, this.key)){ | ||||||
|  |                 // Keys don't match
 | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |              | ||||||
|  |             if(this.value["source"] === "^..*$") { | ||||||
|  |                 if(this.invert){ | ||||||
|  |                     return other.value === "" | ||||||
|  |                 } | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if (this.invert) { | ||||||
|  |                 /* | ||||||
|  |                  * this: "a!=b" | ||||||
|  |                  * other: "a=c" | ||||||
|  |                  * actual property: a=x | ||||||
|  |                  * In other words: shadowing will never occur here | ||||||
|  |                  */ | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             // Unless the values are the same, it is pretty hard to figure out if they are shadowing. This is future work
 | ||||||
|  |             return (this.value["source"] ?? this.value) === other.value; | ||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ export default class SubstitutingTag implements TagsFilter { | ||||||
|         throw "A variable with substitution can not be used to query overpass" |         throw "A variable with substitution can not be used to query overpass" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     shadows(other: TagsFilter): boolean { | ||||||
|         if (!(other instanceof SubstitutingTag)) { |         if (!(other instanceof SubstitutingTag)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -88,14 +88,23 @@ export class Tag extends TagsFilter { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEquivalent(other: TagsFilter): boolean { |     /** | ||||||
|         if (other instanceof Tag) { |      * // should handle advanced regexes
 | ||||||
|             return this.key === other.key && this.value === other.value; |      * new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("key", /^..*$/, true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new Tag("key","value")) // => true
 | ||||||
|  |      * new Tag("key","some_other_value").shadows(new RegexTag("key", "value", true)) // => true
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("key", "value", true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", true)) // => false
 | ||||||
|  |      * new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
 | ||||||
|  |      */ | ||||||
|  |     shadows(other: TagsFilter): boolean { | ||||||
|  |         if(other["key"] !== undefined){ | ||||||
|  |             if(other["key"] !== this.key){ | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         if (other instanceof RegexTag) { |         return other.matchesProperties({[this.key]: this.value}); | ||||||
|             other.isEquivalent(this); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     usedKeys(): string[] { |     usedKeys(): string[] { | ||||||
|  |  | ||||||
|  | @ -200,15 +200,16 @@ export class TagUtils { | ||||||
|      * |      * | ||||||
|      * TagUtils.Tag("key=value") // => new Tag("key", "value")
 |      * TagUtils.Tag("key=value") // => new Tag("key", "value")
 | ||||||
|      * TagUtils.Tag("key=") // => new Tag("key", "")
 |      * TagUtils.Tag("key=") // => new Tag("key", "")
 | ||||||
|      * TagUtils.Tag("key!=") // => new RegexTag("key", "^..*$")
 |      * TagUtils.Tag("key!=") // => new RegexTag("key", /^..*$/)
 | ||||||
|      * TagUtils.Tag("key!=value") // => new RegexTag("key", /^value$/, true)
 |      * TagUtils.Tag("key~*") // => new RegexTag("key", /^..*$/)
 | ||||||
|  |      * TagUtils.Tag("key!=value") // => new RegexTag("key", "value", true)
 | ||||||
|      * TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
 |      * TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
 | ||||||
|      * TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/, true)
 |      * TagUtils.Tag("x!~y") // => new RegexTag("x", /^y$/, true)
 | ||||||
|      * TagUtils.Tag({"and": ["key=value", "x=y"]}) // => new And([new Tag("key","value"), new Tag("x","y")])
 |      * TagUtils.Tag({"and": ["key=value", "x=y"]}) // => new And([new Tag("key","value"), new Tag("x","y")])
 | ||||||
|      * TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
 |      * TagUtils.Tag("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
 | ||||||
|      * TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
 |      * TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
 | ||||||
|      * TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true)
 |      * TagUtils.Tag("xyz!~\\[\\]") // => new RegexTag("xyz", /^\[\]$/, true)
 | ||||||
|      * TagUtils.Tag("tags~(^|.*;)amenity=public_bookcase($|;.*)") // => new RegexTag("tags", /(^|.*;)amenity=public_bookcase($|;.*)/)
 |      * TagUtils.Tag("tags~(.*;)?amenity=public_bookcase(;.*)?") // => new RegexTag("tags", /^(.*;)?amenity=public_bookcase(;.*)?$/)
 | ||||||
|      * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/)
 |      * TagUtils.Tag("service:bicycle:.*~~*") // => new RegexTag(/^service:bicycle:.*$/, /^..*$/)
 | ||||||
|      * |      * | ||||||
|      * TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
 |      * TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
 | ||||||
|  | @ -306,7 +307,7 @@ export class TagUtils { | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1], |                 new RegExp("^"+ split[1]+"$"), | ||||||
|                 true |                 true | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  | @ -338,17 +339,6 @@ export class TagUtils { | ||||||
|                 split[1] = "..*" |                 split[1] = "..*" | ||||||
|                 return new RegexTag(split[0], /^..*$/) |                 return new RegexTag(split[0], /^..*$/) | ||||||
|             } |             } | ||||||
|             return new RegexTag( |  | ||||||
|                 split[0], |  | ||||||
|                 new RegExp("^" + split[1] + "$"), |  | ||||||
|                 true |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         if (tag.indexOf("!~") >= 0) { |  | ||||||
|             const split = Utils.SplitFirst(tag, "!~"); |  | ||||||
|             if (split[1] === "*") { |  | ||||||
|                 split[1] = "..*" |  | ||||||
|             } |  | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1], |                 split[1], | ||||||
|  | @ -357,15 +347,18 @@ export class TagUtils { | ||||||
|         } |         } | ||||||
|         if (tag.indexOf("~") >= 0) { |         if (tag.indexOf("~") >= 0) { | ||||||
|             const split = Utils.SplitFirst(tag, "~"); |             const split = Utils.SplitFirst(tag, "~"); | ||||||
|  |             let value : string | RegExp = split[1] | ||||||
|             if (split[1] === "") { |             if (split[1] === "") { | ||||||
|                 throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" |                 throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" | ||||||
|             } |             } | ||||||
|             if (split[1] === "*") { |             if (value === "*") { | ||||||
|                 split[1] = "..*" |                 value = /^..*$/ | ||||||
|  |             }else { | ||||||
|  |                 value = new RegExp("^"+value+"$") | ||||||
|             } |             } | ||||||
|             return new RegexTag( |             return new RegexTag( | ||||||
|                 split[0], |                 split[0], | ||||||
|                 split[1] |                 value | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|         if (tag.indexOf("=") >= 0) { |         if (tag.indexOf("=") >= 0) { | ||||||
|  | @ -431,4 +424,94 @@ export class TagUtils { | ||||||
|         return " (" + joined + ") " |         return " (" + joined + ") " | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns 'true' is opposite tags are detected. | ||||||
|  |      * Note that this method will never work perfectly | ||||||
|  |      *  | ||||||
|  |      * // should be false for some simple cases
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new Tag("key0", "value")]) // => false
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new Tag("key", "value0")]) // => false
 | ||||||
|  |      *  | ||||||
|  |      * // should detect simple cases
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", "value", true)]) // => true
 | ||||||
|  |      * TagUtils.ContainsOppositeTags([new Tag("key", "value"), new RegexTag("key", /value/, true)]) // => true
 | ||||||
|  |      */ | ||||||
|  |     public static ContainsOppositeTags(tags: (TagsFilter)[]) : boolean{ | ||||||
|  |         for (let i = 0; i < tags.length; i++){ | ||||||
|  |             const tag = tags[i]; | ||||||
|  |             if(!(tag instanceof Tag || tag instanceof RegexTag)){ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             for (let j = i + 1; j < tags.length; j++){ | ||||||
|  |                 const guard = tags[j]; | ||||||
|  |                 if(!(guard instanceof Tag || guard instanceof RegexTag)){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if(guard.key !== tag.key) { | ||||||
|  |                     // Different keys: they can _never_ be opposites
 | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if((guard.value["source"] ?? guard.value) !== (tag.value["source"] ?? tag.value)){ | ||||||
|  |                     // different values: the can _never_ be opposites
 | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 if( (guard["invert"] ?? false) !== (tag["invert"] ?? false) ) { | ||||||
|  |                     // The 'invert' flags are opposite, the key and value is the same for both
 | ||||||
|  |                     // This means we have found opposite tags!
 | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns a filtered version of 'listToFilter'. | ||||||
|  |      * For a list [t0, t1, t2], If `blackList` contains an equivalent (or broader) match of any `t`, this respective `t` is dropped from the returned list | ||||||
|  |      * Ignores nested ORS and ANDS | ||||||
|  |      *  | ||||||
|  |      * TagUtils.removeShadowedElementsFrom([new Tag("key","value")],  [new Tag("key","value"), new Tag("other_key","value")]) // => [new Tag("other_key","value")]
 | ||||||
|  |      */ | ||||||
|  |     public static removeShadowedElementsFrom(blacklist: TagsFilter[], listToFilter: TagsFilter[] ) : TagsFilter[] { | ||||||
|  |         return listToFilter.filter(tf => !blacklist.some(guard => guard.shadows(tf))) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns a filtered version of 'listToFilter', where no duplicates and no equivalents exists. | ||||||
|  |      *  | ||||||
|  |      * TagUtils.removeEquivalents([new RegexTag("key", /^..*$/), new Tag("key","value")]) // => [new Tag("key", "value")]
 | ||||||
|  |      */ | ||||||
|  |     public static removeEquivalents( listToFilter: (Tag | RegexTag)[]) : TagsFilter[] { | ||||||
|  |         const result: TagsFilter[] = [] | ||||||
|  |         outer: for (let i = 0; i < listToFilter.length; i++){ | ||||||
|  |             const tag = listToFilter[i]; | ||||||
|  |             for (let j = 0; j < listToFilter.length; j++){ | ||||||
|  |                 if(i === j){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const guard = listToFilter[j]; | ||||||
|  |                 if(guard.shadows(tag)) { | ||||||
|  |                     // the guard 'kills' the tag: we continue the outer loop without adding the tag
 | ||||||
|  |                     continue outer; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             result.push(tag) | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Returns `true` if at least one element of the 'guards' shadows one element of the 'listToFilter'. | ||||||
|  |      * | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [new Tag("key","value"), new Tag("other_key","value")]) // => true
 | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("other_key","value")]) // => false
 | ||||||
|  |      * TagUtils.containsEquivalents([new Tag("key","value")],  [ new Tag("key","other_value")]) // => false
 | ||||||
|  |      */ | ||||||
|  |     public static containsEquivalents( guards: TagsFilter[], listToFilter: TagsFilter[] ) : boolean { | ||||||
|  |         return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |    | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -4,7 +4,11 @@ export abstract class TagsFilter { | ||||||
| 
 | 
 | ||||||
|     abstract isUsableAsAnswer(): boolean; |     abstract isUsableAsAnswer(): boolean; | ||||||
| 
 | 
 | ||||||
|     abstract isEquivalent(other: TagsFilter): boolean; |     /** | ||||||
|  |      * Indicates some form of equivalency: | ||||||
|  |      * if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties | ||||||
|  |      */ | ||||||
|  |     abstract shadows(other: TagsFilter): boolean; | ||||||
| 
 | 
 | ||||||
|     abstract matchesProperties(properties: any): boolean; |     abstract matchesProperties(properties: any): boolean; | ||||||
| 
 | 
 | ||||||
|  | @ -30,7 +34,7 @@ export abstract class TagsFilter { | ||||||
|      * Returns an optimized version (or self) of this tagsFilter |      * Returns an optimized version (or self) of this tagsFilter | ||||||
|      */ |      */ | ||||||
|     abstract optimize(): TagsFilter | boolean; |     abstract optimize(): TagsFilter | boolean; | ||||||
| 
 |      | ||||||
|     /** |     /** | ||||||
|      * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). |      * Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries). | ||||||
|      *  |      *  | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import {Utils} from "../Utils"; | ||||||
| 
 | 
 | ||||||
| export default class Constants { | export default class Constants { | ||||||
| 
 | 
 | ||||||
|     public static vNumber = "0.18.1"; |     public static vNumber = "0.18.2"; | ||||||
|      |      | ||||||
|     public static ImgurApiKey = '7070e7167f0a25a' |     public static ImgurApiKey = '7070e7167f0a25a' | ||||||
|     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" |     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" | ||||||
|  | @ -62,6 +62,13 @@ export default class Constants { | ||||||
|      */ |      */ | ||||||
|     static distanceToChangeObjectBins = [25, 50, 100, 500, 1000, 5000, Number.MAX_VALUE] |     static distanceToChangeObjectBins = [25, 50, 100, 500, 1000, 5000, Number.MAX_VALUE] | ||||||
|     static themeOrder = ["personal", "cyclofix", "waste" , "etymology",  "food","cafes_and_pubs", "playgrounds", "hailhydrant", "toilets", "aed", "bookcases"]; |     static themeOrder = ["personal", "cyclofix", "waste" , "etymology",  "food","cafes_and_pubs", "playgrounds", "hailhydrant", "toilets", "aed", "bookcases"]; | ||||||
|  |     /** | ||||||
|  |      * Upon initialization, the GPS will search the location. | ||||||
|  |      * If the location is found within the given timout, it'll automatically fly to it. | ||||||
|  |      *  | ||||||
|  |      * In seconds | ||||||
|  |      */ | ||||||
|  |     static zoomToLocationTimeout = 60; | ||||||
| 
 | 
 | ||||||
|     private static isRetina(): boolean { |     private static isRetina(): boolean { | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|  |  | ||||||
|  | @ -127,6 +127,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|                 idKey: json.source["idKey"] |                 idKey: json.source["idKey"] | ||||||
| 
 | 
 | ||||||
|             }, |             }, | ||||||
|  |             Constants.priviliged_layers.indexOf(this.id) > 0, | ||||||
|             json.id |             json.id | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
| import {RegexTag} from "../../Logic/Tags/RegexTag"; | import {RegexTag} from "../../Logic/Tags/RegexTag"; | ||||||
|  | import {param} from "jquery"; | ||||||
| 
 | 
 | ||||||
| export default class SourceConfig { | export default class SourceConfig { | ||||||
| 
 | 
 | ||||||
|  | @ -19,7 +20,7 @@ export default class SourceConfig { | ||||||
|         isOsmCache?: boolean, |         isOsmCache?: boolean, | ||||||
|         geojsonSourceLevel?: number, |         geojsonSourceLevel?: number, | ||||||
|         idKey?: string |         idKey?: string | ||||||
|     }, context?: string) { |     }, isSpecialLayer: boolean, context?: string) { | ||||||
| 
 | 
 | ||||||
|         let defined = 0; |         let defined = 0; | ||||||
|         if (params.osmTags) { |         if (params.osmTags) { | ||||||
|  | @ -43,6 +44,15 @@ export default class SourceConfig { | ||||||
|                 throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` |                 throw `Source defines a geojson-zoomLevel, but does not specify {x} nor {y} (or equivalent), this is probably a bug (in context ${context})` | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         if(params.osmTags !== undefined && !isSpecialLayer){ | ||||||
|  |             const optimized = params.osmTags.optimize() | ||||||
|  |             if(optimized === false){ | ||||||
|  |                 throw "Error at "+context+": the specified tags are conflicting with each other: they will never match anything at all" | ||||||
|  |             } | ||||||
|  |             if(optimized === true){ | ||||||
|  |                        throw "Error at "+context+": the specified tags are very wide: they will always match everything" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|         this.osmTags = params.osmTags ?? new RegexTag("id", /.*/); |         this.osmTags = params.osmTags ?? new RegexTag("id", /.*/); | ||||||
|         this.overpassScript = params.overpassScript; |         this.overpassScript = params.overpassScript; | ||||||
|         this.geojsonSource = params.geojsonSource; |         this.geojsonSource = params.geojsonSource; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {Translation} from "../../UI/i18n/Translation"; | import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
| import Translations from "../../UI/i18n/Translations"; | import Translations from "../../UI/i18n/Translations"; | ||||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||||
|  | @ -22,8 +22,8 @@ export default class TagRenderingConfig { | ||||||
| 
 | 
 | ||||||
|     public readonly id: string; |     public readonly id: string; | ||||||
|     public readonly group: string; |     public readonly group: string; | ||||||
|     public readonly render?: Translation; |     public readonly render?: TypedTranslation<object>; | ||||||
|     public readonly question?: Translation; |     public readonly question?: TypedTranslation<object>; | ||||||
|     public readonly condition?: TagsFilter; |     public readonly condition?: TagsFilter; | ||||||
| 
 | 
 | ||||||
|     public readonly configuration_warnings: string[] = [] |     public readonly configuration_warnings: string[] = [] | ||||||
|  | @ -43,7 +43,7 @@ export default class TagRenderingConfig { | ||||||
|     public readonly mappings?: { |     public readonly mappings?: { | ||||||
|         readonly if: TagsFilter, |         readonly if: TagsFilter, | ||||||
|         readonly ifnot?: TagsFilter, |         readonly ifnot?: TagsFilter, | ||||||
|         readonly then: Translation, |         readonly then: TypedTranslation<object>, | ||||||
|         readonly icon: string, |         readonly icon: string, | ||||||
|         readonly iconClass: string |         readonly iconClass: string | ||||||
|         readonly hideInAnswer: boolean | TagsFilter |         readonly hideInAnswer: boolean | TagsFilter | ||||||
|  | @ -110,12 +110,13 @@ export default class TagRenderingConfig { | ||||||
|             } |             } | ||||||
|            const type = json.freeform.type ?? "string" |            const type = json.freeform.type ?? "string" | ||||||
| 
 | 
 | ||||||
|             let placeholder = Translations.T(json.freeform.placeholder) |             let placeholder: Translation = Translations.T(json.freeform.placeholder) | ||||||
|             if (placeholder === undefined) { |             if (placeholder === undefined) { | ||||||
|                 const typeDescription = Translations.t.validation[type]?.description |                 const typeDescription = Translations.t.validation[type]?.description | ||||||
|                 placeholder = Translations.T(json.freeform.key+" ("+type+")") |  | ||||||
|                 if(typeDescription !== undefined){ |                 if(typeDescription !== undefined){ | ||||||
|                     placeholder = placeholder.Subs({[type]: typeDescription}) |                     placeholder = Translations.T(json.freeform.key+" ("+type+")").Subs({[type]: typeDescription}) | ||||||
|  |                 }else{ | ||||||
|  |                     placeholder = Translations.T(json.freeform.key+" ("+type+")") | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -383,7 +384,7 @@ export default class TagRenderingConfig { | ||||||
|         let freeformKeyDefined = this.freeform?.key !== undefined; |         let freeformKeyDefined = this.freeform?.key !== undefined; | ||||||
|         let usedFreeformValues = new Set<string>() |         let usedFreeformValues = new Set<string>() | ||||||
|         // We run over all the mappings first, to check if the mapping matches
 |         // We run over all the mappings first, to check if the mapping matches
 | ||||||
|         const applicableMappings: { then: Translation, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => { |         const applicableMappings: { then: TypedTranslation<any>, img?: string }[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => { | ||||||
|             if (mapping.if === undefined) { |             if (mapping.if === undefined) { | ||||||
|                 return mapping; |                 return mapping; | ||||||
|             } |             } | ||||||
|  | @ -404,7 +405,7 @@ export default class TagRenderingConfig { | ||||||
|             const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v)) |             const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v)) | ||||||
|             for (const leftover of leftovers) { |             for (const leftover of leftovers) { | ||||||
|                 applicableMappings.push({then:  |                 applicableMappings.push({then:  | ||||||
|                         this.render.replace("{"+this.freeform.key+"}", leftover) |                         new TypedTranslation<object>(this.render.replace("{"+this.freeform.key+"}", leftover).translations)  | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -412,7 +413,7 @@ export default class TagRenderingConfig { | ||||||
|         return applicableMappings |         return applicableMappings | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetRenderValue(tags: any, defltValue: any = undefined): Translation { |     public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation<any> { | ||||||
|         return this.GetRenderValueWithImage(tags, defltValue).then |         return this.GetRenderValueWithImage(tags, defltValue).then | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -421,7 +422,7 @@ export default class TagRenderingConfig { | ||||||
|      * Not compatible with multiAnswer - use GetRenderValueS instead in that case |      * Not compatible with multiAnswer - use GetRenderValueS instead in that case | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: Translation, icon?: string } { |     public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation<any>, icon?: string } { | ||||||
|         if (this.mappings !== undefined && !this.multiAnswer) { |         if (this.mappings !== undefined && !this.multiAnswer) { | ||||||
|             for (const mapping of this.mappings) { |             for (const mapping of this.mappings) { | ||||||
|                 if (mapping.if === undefined) { |                 if (mapping.if === undefined) { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import Link from "./Link"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| 
 | 
 | ||||||
| export default class LinkToWeblate extends VariableUiElement { | export default class LinkToWeblate extends VariableUiElement { | ||||||
|  |     private static URI: any; | ||||||
|     constructor(context: string, availableTranslations: object) { |     constructor(context: string, availableTranslations: object) { | ||||||
|         super( Locale.language.map(ln => { |         super( Locale.language.map(ln => { | ||||||
|             if (Locale.showLinkToWeblate.data === false) { |             if (Locale.showLinkToWeblate.data === false) { | ||||||
|  | @ -36,4 +37,10 @@ export default class LinkToWeblate extends VariableUiElement { | ||||||
|         const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/" |         const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/" | ||||||
|         return baseUrl + category + "/" + language + "/?offset=1&q=context%3A%3D%22" + key + "%22" |         return baseUrl + category + "/" + language + "/?offset=1&q=context%3A%3D%22" + key + "%22" | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static hrefToWeblateZen(language: string, category: string, searchKey: string): string{ | ||||||
|  |         const baseUrl = "https://hosted.weblate.org/zen/mapcomplete/" | ||||||
|  |         // ?offset=1&q=+state%3A%3Ctranslated+context%3Acampersite&sort_by=-priority%2Cposition&checksum=
 | ||||||
|  |         return baseUrl + category + "/" + language + "?offset=1&q=+state%3A%3Ctranslated+context%3A"+encodeURIComponent(searchKey)+"&sort_by=-priority%2Cposition&checksum=" | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -49,7 +49,7 @@ export default abstract class BaseUIElement { | ||||||
|      */ |      */ | ||||||
|     public SetClass(clss: string) { |     public SetClass(clss: string) { | ||||||
|         if (clss == undefined) { |         if (clss == undefined) { | ||||||
|             return |             return this | ||||||
|         } |         } | ||||||
|         const all = clss.split(" ").map(clsName => clsName.trim()); |         const all = clss.split(" ").map(clsName => clsName.trim()); | ||||||
|         let recordedChange = false; |         let recordedChange = false; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ export default class AddNewMarker extends Combine { | ||||||
|             let last = undefined; |             let last = undefined; | ||||||
|             for (const filteredLayer of filteredLayers) { |             for (const filteredLayer of filteredLayers) { | ||||||
|                 const layer = filteredLayer.layerDef; |                 const layer = filteredLayer.layerDef; | ||||||
|                 if(layer.name === undefined){ |                 if(layer.name === undefined && !filteredLayer.isDisplayed.data){ | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 for (const preset of filteredLayer.layerDef.presets) { |                 for (const preset of filteredLayer.layerDef.presets) { | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import ContributorCount from "../../Logic/ContributorCount"; | import ContributorCount from "../../Logic/ContributorCount"; | ||||||
| import Img from "../Base/Img"; | import Img from "../Base/Img"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {TypedTranslation} from "../i18n/Translation"; | ||||||
| import TranslatorsPanel from "./TranslatorsPanel"; | import TranslatorsPanel from "./TranslatorsPanel"; | ||||||
| 
 | 
 | ||||||
| export class OpenIdEditor extends VariableUiElement { | export class OpenIdEditor extends VariableUiElement { | ||||||
|  | @ -198,7 +198,7 @@ export default class CopyrightPanel extends Combine { | ||||||
|         this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem") |         this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static CodeContributors(contributors, translation: Translation): BaseUIElement { |     private static CodeContributors(contributors, translation: TypedTranslation<{contributors, hiddenCount}>): BaseUIElement { | ||||||
| 
 | 
 | ||||||
|         const total = contributors.contributors.length; |         const total = contributors.contributors.length; | ||||||
|         let filtered = [...contributors.contributors] |         let filtered = [...contributors.contributors] | ||||||
|  |  | ||||||
|  | @ -90,10 +90,10 @@ export default class MoreScreen extends Combine { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let hash = "" |         let hash = "" | ||||||
|         if(layout.definition !== undefined){ |         if (layout.definition !== undefined) { | ||||||
|             hash = "#"+btoa(JSON.stringify(layout.definition)) |             hash = "#" + btoa(JSON.stringify(layout.definition)) | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const linkText = currentLocation?.map(currentLocation => { |         const linkText = currentLocation?.map(currentLocation => { | ||||||
|             const params = [ |             const params = [ | ||||||
|                 ["z", currentLocation?.zoom], |                 ["z", currentLocation?.zoom], | ||||||
|  | @ -106,11 +106,10 @@ export default class MoreScreen extends Combine { | ||||||
|         }) ?? new UIEventSource<string>(`${linkPrefix}`) |         }) ?? new UIEventSource<string>(`${linkPrefix}`) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         return new SubtleButton(layout.icon, |         return new SubtleButton(layout.icon, | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 `<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`, |                 `<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`, | ||||||
|                 new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:"+layout.id+".title" : undefined), |                 new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined), | ||||||
|                 `</dt>`, |                 `</dt>`, | ||||||
|                 `<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`, |                 `<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`, | ||||||
|                 new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", |                 new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", | ||||||
|  | @ -128,15 +127,13 @@ export default class MoreScreen extends Combine { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createUnofficialButtonFor(state: UserRelatedState, id: string): BaseUIElement { |     private static createUnofficialButtonFor(state: UserRelatedState, id: string): BaseUIElement { | ||||||
|         const allPreferences = state.osmConnection.preferencesHandler.preferences.data; |         const pref = state.osmConnection.GetLongPreference(id) | ||||||
|         const length = Number(allPreferences[id + "-length"]) |         const str = pref.data | ||||||
|         let str = ""; |         if (str === undefined || str === "undefined" || str === "") { | ||||||
|         for (let i = 0; i < length; i++) { |             pref.setData(null) | ||||||
|             str += allPreferences[id + "-" + i] |  | ||||||
|         } |  | ||||||
|         if(str === undefined || str === "undefined"){ |  | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
|             const value: { |             const value: { | ||||||
|                 id: string |                 id: string | ||||||
|  | @ -149,7 +146,8 @@ export default class MoreScreen extends Combine { | ||||||
|             value.isOfficial = false |             value.isOfficial = false | ||||||
|             return MoreScreen.createLinkButton(state, value, true) |             return MoreScreen.createLinkButton(state, value, true) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.debug("Could not parse unofficial theme information for " + id, "The json is: ", str, e) |             console.warn("Removing theme " + id + " as it could not be parsed from the preferences") | ||||||
|  |             pref.setData(null) | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -163,16 +161,14 @@ export default class MoreScreen extends Combine { | ||||||
| 
 | 
 | ||||||
|                 for (const key in allPreferences) { |                 for (const key in allPreferences) { | ||||||
|                     if (key.startsWith(prefix) && key.endsWith("-combined-length")) { |                     if (key.startsWith(prefix) && key.endsWith("-combined-length")) { | ||||||
|                         const id = key.substring(0, key.length - "-length".length) |                         const id = key.substring("mapcomplete-".length, key.length - "-combined-length".length) | ||||||
|                         ids.push(id) |                         ids.push(id) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 return ids |                 return ids | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|         var stableIds = UIEventSource.ListStabilized<string>(currentIds) |         var stableIds = UIEventSource.ListStabilized<string>(currentIds) | ||||||
| 
 |  | ||||||
|         return new VariableUiElement( |         return new VariableUiElement( | ||||||
|             stableIds.map(ids => { |             stableIds.map(ids => { | ||||||
|                 const allThemes: BaseUIElement[] = [] |                 const allThemes: BaseUIElement[] = [] | ||||||
|  | @ -182,12 +178,11 @@ export default class MoreScreen extends Combine { | ||||||
|                         allThemes.push(link.SetClass(buttonClass)) |                         allThemes.push(link.SetClass(buttonClass)) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 if (allThemes.length <= 0) { |                 if (allThemes.length <= 0) { | ||||||
|                     return undefined; |                     return undefined; | ||||||
|                 } |                 } | ||||||
|                 return new Combine([ |                 return new Combine([ | ||||||
|                     Translations.t.general.customThemeIntro.Clone(), |                     Translations.t.general.customThemeIntro, | ||||||
|                     new Combine(allThemes).SetClass(themeListClasses) |                     new Combine(allThemes).SetClass(themeListClasses) | ||||||
|                 ]); |                 ]); | ||||||
|             })); |             })); | ||||||
|  |  | ||||||
|  | @ -208,15 +208,20 @@ export default class SimpleAddUI extends Toggle { | ||||||
|         const allButtons = []; |         const allButtons = []; | ||||||
|         for (const layer of state.filteredLayers.data) { |         for (const layer of state.filteredLayers.data) { | ||||||
| 
 | 
 | ||||||
|             if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) { |             if (layer.isDisplayed.data === false) { | ||||||
|                 // The layer is not displayed and we cannot enable the layer control -> we skip
 |                 // The layer is not displayed...
 | ||||||
|                 continue; |                 if(!state.featureSwitchFilter.data){ | ||||||
|  |                     // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
 | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (layer.layerDef.name === undefined) { | ||||||
|  |                     // this layer can never be toggled on in any case, so we skip the presets
 | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (layer.layerDef.name === undefined) { | 
 | ||||||
|                 // this is a parlty hidden layer
 |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             const presets = layer.layerDef.presets; |             const presets = layer.layerDef.presets; | ||||||
|             for (const preset of presets) { |             for (const preset of presets) { | ||||||
|  |  | ||||||
|  | @ -14,7 +14,9 @@ import Title from "../Base/Title"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {SubtleButton} from "../Base/SubtleButton"; | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| 
 | import * as native_languages from "../../assets/language_native.json" | ||||||
|  | import * as used_languages from "../../assets/generated/used_languages.json" | ||||||
|  | import BaseUIElement from "../BaseUIElement"; | ||||||
| 
 | 
 | ||||||
| class TranslatorsPanelContent extends Combine { | class TranslatorsPanelContent extends Combine { | ||||||
|     constructor(layout: LayoutConfig, isTranslator: UIEventSource<boolean>) { |     constructor(layout: LayoutConfig, isTranslator: UIEventSource<boolean>) { | ||||||
|  | @ -24,36 +26,53 @@ class TranslatorsPanelContent extends Combine { | ||||||
| 
 | 
 | ||||||
|         const seed = t.completeness |         const seed = t.completeness | ||||||
|         for (const ln of Array.from(completeness.keys())) { |         for (const ln of Array.from(completeness.keys())) { | ||||||
|             if(ln === "*"){ |             if (ln === "*") { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|             if (seed.translations[ln] === undefined) { |             if (seed.translations[ln] === undefined) { | ||||||
|                 seed.translations[ln] = seed.translations["en"] |                 seed.translations[ln] = seed.translations["en"] | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          | 
 | ||||||
|         const completenessTr = {} |         const completenessTr = {} | ||||||
|         const completenessPercentage = {} |         const completenessPercentage = {} | ||||||
|         seed.SupportedLanguages().forEach(ln => { |         seed.SupportedLanguages().forEach(ln => { | ||||||
|             completenessTr[ln] = ""+(completeness.get(ln) ?? 0) |             completenessTr[ln] = "" + (completeness.get(ln) ?? 0) | ||||||
|             completenessPercentage[ln] = ""+Math.round(100 * (completeness.get(ln) ?? 0) / total) |             completenessPercentage[ln] = "" + Math.round(100 * (completeness.get(ln) ?? 0) / total) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const missingTranslationsFor = (ln: string) => Utils.NoNull(untranslated.get(ln) ?? []) |         function missingTranslationsFor(language: string): BaseUIElement[] { | ||||||
|             .filter(ctx => ctx.indexOf(":") >= 0) |             // e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description
 | ||||||
|             .map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import")) |             const missingKeys = Utils.NoNull(untranslated.get(language) ?? []) | ||||||
|             .map(context => new Link(context, LinkToWeblate.hrefToWeblate(ln, context), true)) |                 .filter(ctx => ctx.indexOf(":") >= 0) | ||||||
|  |                 .map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import")) | ||||||
|  | 
 | ||||||
|  |             const hasMissingTheme = missingKeys.some(k => k.startsWith("themes:")) | ||||||
|  |             const missingLayers = Utils.Dedup( missingKeys.filter(k => k.startsWith("layers:")) | ||||||
|  |                 .map(k => k.slice("layers:".length).split(".")[0])) | ||||||
|  | 
 | ||||||
|  |             console.log("Getting untranslated string for",language,"raw:",missingKeys,"hasMissingTheme:",hasMissingTheme,"missingLayers:",missingLayers) | ||||||
|  |             return [ | ||||||
|  |                 hasMissingTheme ? new Link("themes:" + layout.id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id), true) : undefined, | ||||||
|  |                 ...missingLayers.map(id => new Link("layer:" + id + ".* (zen mode)", LinkToWeblate.hrefToWeblateZen(language, "layers", id), true)), | ||||||
|  |                 ...missingKeys.map(context => new Link(context, LinkToWeblate.hrefToWeblate(language, context), true)) | ||||||
|  |             ] | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |         //
 | ||||||
|  |         // 
 | ||||||
|         // "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
 |         // "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
 | ||||||
|         const translated = seed.Subs({total, theme: layout.title, |         const translated = seed.Subs({ | ||||||
|  |             total, theme: layout.title, | ||||||
|             percentage: new Translation(completenessPercentage), |             percentage: new Translation(completenessPercentage), | ||||||
|             translated: new Translation(completenessTr) |             translated: new Translation(completenessTr), | ||||||
|  |             language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng) | ||||||
|         }) |         }) | ||||||
|          | 
 | ||||||
|         super([ |         super([ | ||||||
|             new Title( |             new Title( | ||||||
|             Translations.t.translations.activateButton, |                 Translations.t.translations.activateButton, | ||||||
|             ), |             ), | ||||||
|             new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator), |             new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator), | ||||||
|             t.help, |             t.help, | ||||||
|  | @ -63,15 +82,18 @@ class TranslatorsPanelContent extends Combine { | ||||||
|                 .onClick(() => { |                 .onClick(() => { | ||||||
|                     Locale.showLinkToWeblate.setData(false) |                     Locale.showLinkToWeblate.setData(false) | ||||||
|                 }), |                 }), | ||||||
|              |  | ||||||
|             new VariableUiElement(Locale.language.map(ln => { |  | ||||||
| 
 | 
 | ||||||
|  |             new VariableUiElement(Locale.language.map(ln => { | ||||||
|                 const missing = missingTranslationsFor(ln) |                 const missing = missingTranslationsFor(ln) | ||||||
|                 if (missing.length === 0) { |                 if (missing.length === 0) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|  |                 let title = Translations.t.translations.allMissing; | ||||||
|  |                 if(untranslated.get(ln) !== undefined){ | ||||||
|  |                     title = Translations.t.translations.missing.Subs({count: untranslated.get(ln).length}) | ||||||
|  |                 } | ||||||
|                 return new Toggleable( |                 return new Toggleable( | ||||||
|                     new Title(Translations.t.translations.missing.Subs({count: missing.length})), |                     new Title(title), | ||||||
|                     new Combine(missing).SetClass("flex flex-col") |                     new Combine(missing).SetClass("flex flex-col") | ||||||
|                 ) |                 ) | ||||||
|             })) |             })) | ||||||
|  | @ -83,38 +105,37 @@ class TranslatorsPanelContent extends Combine { | ||||||
| 
 | 
 | ||||||
| export default class TranslatorsPanel extends Toggle { | export default class TranslatorsPanel extends Toggle { | ||||||
| 
 | 
 | ||||||
|      | 
 | ||||||
|     constructor(state: { layoutToUse: LayoutConfig, isTranslator: UIEventSource<boolean> }, iconStyle?: string) { |     constructor(state: { layoutToUse: LayoutConfig, isTranslator: UIEventSource<boolean> }, iconStyle?: string) { | ||||||
|         const t = Translations.t.translations |         const t = Translations.t.translations | ||||||
|         super( |         super( | ||||||
|                 new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator) |             new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator) | ||||||
|             ).SetClass("flex flex-col"), |             ).SetClass("flex flex-col"), | ||||||
|             new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => Locale.showLinkToWeblate.setData(true)), |             new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => Locale.showLinkToWeblate.setData(true)), | ||||||
|             Locale.showLinkToWeblate  |             Locale.showLinkToWeblate | ||||||
|         ) |         ) | ||||||
|         this.SetClass("hidden-on-mobile") |         this.SetClass("hidden-on-mobile") | ||||||
|          | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public static MissingTranslationsFor(layout: LayoutConfig) : {completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number} { |     public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } { | ||||||
|         let total = 0 |         let total = 0 | ||||||
|         const completeness = new Map<string, number>() |         const completeness = new Map<string, number>() | ||||||
|         const untranslated = new Map<string, string[]>() |         const untranslated = new Map<string, string[]>() | ||||||
|  | 
 | ||||||
|         Utils.WalkObject(layout, (o, path) => { |         Utils.WalkObject(layout, (o, path) => { | ||||||
|             const translation = <Translation><any>o; |             const translation = <Translation><any>o; | ||||||
|             if(translation.translations["*"] !== undefined){ |             if (translation.translations["*"] !== undefined) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             if(translation.context === undefined || translation.context.indexOf(":") < 0){ |             if (translation.context === undefined || translation.context.indexOf(":") < 0) { | ||||||
|                 // no source given - lets ignore
 |                 // no source given - lets ignore
 | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
| 
 |              | ||||||
|             for (const lang of translation.SupportedLanguages()) { |             total ++ | ||||||
|                 completeness.set(lang, 1 + (completeness.get(lang) ?? 0)) |             used_languages.languages.forEach(ln => { | ||||||
|             } |  | ||||||
|             layout.title.SupportedLanguages().forEach(ln => { |  | ||||||
|                 const trans = translation.translations |                 const trans = translation.translations | ||||||
|                 if (trans["*"] !== undefined) { |                 if (trans["*"] !== undefined) { | ||||||
|                     return; |                     return; | ||||||
|  | @ -124,11 +145,11 @@ export default class TranslatorsPanel extends Toggle { | ||||||
|                         untranslated.set(ln, []) |                         untranslated.set(ln, []) | ||||||
|                     } |                     } | ||||||
|                     untranslated.get(ln).push(translation.context) |                     untranslated.get(ln).push(translation.context) | ||||||
|  |                 }else{ | ||||||
|  |                     completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             if(translation.translations["*"] === undefined){ |             | ||||||
|                 total++ |  | ||||||
|             } |  | ||||||
|         }, o => { |         }, o => { | ||||||
|             if (o === undefined || o === null) { |             if (o === undefined || o === null) { | ||||||
|                 return false; |                 return false; | ||||||
|  |  | ||||||
|  | @ -248,7 +248,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         const inputEl = new InputElementMap<number[], TagsFilter>( |         const inputEl = new InputElementMap<number[], TagsFilter>( | ||||||
|             checkBoxes, |             checkBoxes, | ||||||
|             (t0, t1) => { |             (t0, t1) => { | ||||||
|                 return t0?.isEquivalent(t1) ?? false |                 return t0?.shadows(t1) ?? false | ||||||
|             }, |             }, | ||||||
|             (indices) => { |             (indices) => { | ||||||
|                 if (indices.length === 0) { |                 if (indices.length === 0) { | ||||||
|  | @ -370,7 +370,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         return new FixedInputElement( |         return new FixedInputElement( | ||||||
|             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), |             TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), | ||||||
|             tagging, |             tagging, | ||||||
|             (t0, t1) => t1.isEquivalent(t0)); |             (t0, t1) => t1.shadows(t0)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateMappingContent(mapping: { |     private static GenerateMappingContent(mapping: { | ||||||
|  | @ -450,7 +450,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( |         let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap( | ||||||
|             input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), |             input, (a, b) => a === b || (a?.shadows(b) ?? false), | ||||||
|             pickString, toString |             pickString, toString | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ export default class ReviewElement extends VariableUiElement { | ||||||
|                         SingleReview.GenStars(avg), |                         SingleReview.GenStars(avg), | ||||||
|                         new Link( |                         new Link( | ||||||
|                             revs.length === 1 ? Translations.t.reviews.title_singular.Clone() : |                             revs.length === 1 ? Translations.t.reviews.title_singular.Clone() : | ||||||
|                                 Translations.t.reviews.title.Clone() |                                 Translations.t.reviews.title | ||||||
|                                     .Subs({count: "" + revs.length}), |                                     .Subs({count: "" + revs.length}), | ||||||
|                             `https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`, |                             `https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`, | ||||||
|                             true |                             true | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; | import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata"; | ||||||
| import {Translation} from "../i18n/Translation"; | import {Translation, TypedTranslation} from "../i18n/Translation"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import Loading from "../Base/Loading"; | import Loading from "../Base/Loading"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | @ -22,7 +22,7 @@ export default class WikidataPreviewBox extends VariableUiElement { | ||||||
|     private static extraProperties: { |     private static extraProperties: { | ||||||
|         requires?: { p: number, q?: number }[], |         requires?: { p: number, q?: number }[], | ||||||
|         property: string, |         property: string, | ||||||
|         display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * })  */> |         display: TypedTranslation<{value}> | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * })  */> | ||||||
|     }[] = [ |     }[] = [ | ||||||
|         { |         { | ||||||
|             requires: WikidataPreviewBox.isHuman, |             requires: WikidataPreviewBox.isHuman, | ||||||
|  |  | ||||||
|  | @ -1,9 +1,6 @@ | ||||||
| import Locale from "./Locale"; | import Locale from "./Locale"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import Link from "../Base/Link"; |  | ||||||
| import Svg from "../../Svg"; |  | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; |  | ||||||
| import LinkToWeblate from "../Base/LinkToWeblate"; | import LinkToWeblate from "../Base/LinkToWeblate"; | ||||||
| 
 | 
 | ||||||
| export class Translation extends BaseUIElement { | export class Translation extends BaseUIElement { | ||||||
|  | @ -164,25 +161,7 @@ export class Translation extends BaseUIElement { | ||||||
|     public AllValues(): string[] { |     public AllValues(): string[] { | ||||||
|         return this.SupportedLanguages().map(lng => this.translations[lng]); |         return this.SupportedLanguages().map(lng => this.translations[lng]); | ||||||
|     } |     } | ||||||
| 
 |      | ||||||
|     /** |  | ||||||
|      * Substitutes text in a translation. |  | ||||||
|      * If a translation is passed, it'll be fused |  | ||||||
|      *  |  | ||||||
|      * // Should replace simple keys
 |  | ||||||
|      * new Translation({"en": "Some text {key}"}).Subs({key: "xyz"}).textFor("en") // => "Some text xyz"
 |  | ||||||
|      *  |  | ||||||
|      * // Should fuse translations
 |  | ||||||
|      * const subpart = new Translation({"en": "subpart","nl":"onderdeel"}) |  | ||||||
|      * const tr = new Translation({"en": "Full sentence with {part}", nl: "Volledige zin met {part}"}) |  | ||||||
|      * const subbed = tr.Subs({part: subpart}) |  | ||||||
|      * subbed.textFor("en") // => "Full sentence with subpart"
 |  | ||||||
|      * subbed.textFor("nl") // => "Volledige zin met onderdeel"
 |  | ||||||
|      */ |  | ||||||
|     public Subs(text: any, context?: string): Translation { |  | ||||||
|         return this.OnEveryLanguage((template, lang) => Utils.SubstituteKeys(template, text, lang), context) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation { |     public OnEveryLanguage(f: (s: string, language: string) => string, context?: string): Translation { | ||||||
|         const newTranslations = {}; |         const newTranslations = {}; | ||||||
|         for (const lang in this.translations) { |         for (const lang in this.translations) { | ||||||
|  | @ -278,5 +257,28 @@ export class Translation extends BaseUIElement { | ||||||
|         return this.txt |         return this.txt | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|  | export class TypedTranslation<T> extends Translation { | ||||||
|  |     constructor(translations: object, context?: string) { | ||||||
|  |         super(translations, context); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Substitutes text in a translation. | ||||||
|  |      * If a translation is passed, it'll be fused | ||||||
|  |      * | ||||||
|  |      * // Should replace simple keys
 | ||||||
|  |      * new TypedTranslation<object>({"en": "Some text {key}"}).Subs({key: "xyz"}).textFor("en") // => "Some text xyz"
 | ||||||
|  |      * | ||||||
|  |      * // Should fuse translations
 | ||||||
|  |      * const subpart = new Translation({"en": "subpart","nl":"onderdeel"}) | ||||||
|  |      * const tr = new TypedTranslation<object>({"en": "Full sentence with {part}", nl: "Volledige zin met {part}"}) | ||||||
|  |      * const subbed = tr.Subs({part: subpart}) | ||||||
|  |      * subbed.textFor("en") // => "Full sentence with subpart"
 | ||||||
|  |      * subbed.textFor("nl") // => "Volledige zin met onderdeel"
 | ||||||
|  |      */ | ||||||
|  |     Subs(text: T, context?: string): Translation { | ||||||
|  |         return this.OnEveryLanguage((template, lang) => Utils.SubstituteKeys(template, text, lang), context) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| import {Translation} from "./Translation"; | import {Translation, TypedTranslation} from "./Translation"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import * as known_languages from "../../assets/generated/used_languages.json" | import * as known_languages from "../../assets/generated/used_languages.json" | ||||||
| import CompiledTranslations from "../../assets/generated/CompiledTranslations"; | import CompiledTranslations from "../../assets/generated/CompiledTranslations"; | ||||||
|  | @ -22,7 +22,7 @@ export default class Translations { | ||||||
|         return s; |         return s; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static T(t: string | any, context = undefined): Translation { |     static T(t: string | any, context = undefined): TypedTranslation<object> { | ||||||
|         if (t === undefined || t === null) { |         if (t === undefined || t === null) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|  | @ -30,17 +30,17 @@ export default class Translations { | ||||||
|             t = "" + t |             t = "" + t | ||||||
|         } |         } | ||||||
|         if (typeof t === "string") { |         if (typeof t === "string") { | ||||||
|             return new Translation({"*": t}, context); |             return new TypedTranslation({"*": t}, context); | ||||||
|         } |         } | ||||||
|         if (t.render !== undefined) { |         if (t.render !== undefined) { | ||||||
|             const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly" |             const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly" | ||||||
|             console.error(msg, t); |             console.error(msg, t); | ||||||
|             throw msg |             throw msg | ||||||
|         } |         } | ||||||
|         if (t instanceof Translation) { |         if (t instanceof TypedTranslation) { | ||||||
|             return t; |             return t; | ||||||
|         } |         } | ||||||
|         return new Translation(t, context); |         return new TypedTranslation(t, context); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import {readFileSync, writeFileSync} from "fs"; | import {readFileSync, writeFileSync} from "fs"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import {TagRenderingConfigJson} from "../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; |  | ||||||
| import ScriptUtils from "../../../scripts/ScriptUtils"; | import ScriptUtils from "../../../scripts/ScriptUtils"; | ||||||
| import {LayerConfigJson} from "../../../Models/ThemeConfig/Json/LayerConfigJson"; | import {LayerConfigJson} from "../../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import FilterConfigJson from "../../../Models/ThemeConfig/Json/FilterConfigJson"; | import FilterConfigJson from "../../../Models/ThemeConfig/Json/FilterConfigJson"; | ||||||
|  | @ -8,7 +7,7 @@ import {QuestionableTagRenderingConfigJson} from "../../../Models/ThemeConfig/Js | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| function colonSplit(value: string): string[] { | function colonSplit(value: string): string[] { | ||||||
|     return value.split(";").map(v => v.replace(/"/g, '').trim().toLowerCase()).filter(s => s !== ""); |     return value.split(";").map(v => v.replace(/"/g, '').trim()).filter(s => s !== ""); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function loadCsv(file): { | function loadCsv(file): { | ||||||
|  |  | ||||||
|  | @ -51,7 +51,8 @@ | ||||||
|   ], |   ], | ||||||
|   "overrideAll": { |   "overrideAll": { | ||||||
|     "allowMove": { |     "allowMove": { | ||||||
|       "improveAccuracy": true |       "enableRelocation": false, | ||||||
|  |       "enableImproveAccuracy": true | ||||||
|     }, |     }, | ||||||
|     "+titleIcons": [ |     "+titleIcons": [ | ||||||
|       { |       { | ||||||
|  |  | ||||||
|  | @ -276,17 +276,17 @@ | ||||||
|         "willBePublished": "La teva foto serà publicada: " |         "willBePublished": "La teva foto serà publicada: " | ||||||
|     }, |     }, | ||||||
|     "importHelper": { |     "importHelper": { | ||||||
|         "allAttributesSame": "Totes les funcions a importar tenen aquesta etiqueta", |         "introduction": { | ||||||
|         "description": "L'ajudant d'importació converteix un conjunt de dades extern en notes. El conjunt de dades extern ha de coincidir amb una de les capes MapComplete existents. Per a cada article que introdueixes a l'importador, es crearà una nota única. Aquestes notes es mostraran juntament amb les característiques rellevants en aquests mapes per afegir-les fàcilment.", |             "description": "L'ajudant d'importació converteix un conjunt de dades extern en notes. El conjunt de dades extern ha de coincidir amb una de les capes MapComplete existents. Per a cada article que introdueixes a l'importador, es crearà una nota única. Aquestes notes es mostraran juntament amb les característiques rellevants en aquests mapes per afegir-les fàcilment.", | ||||||
|         "importFormat": "Un text d'una nota ha de tenir el format següent per poder ser recollit: <br><div class=\"literal-code\">[Una petita introducció]<br>https://mapcomplete.osm.be /[themename].html?[paràmetres com ara lat i lon]#import<br>[totes les etiquetes de la funció] </div>", |             "importFormat": "Un text d'una nota ha de tenir el format següent per poder ser recollit: <br><div class=\"literal-code\">[Una petita introducció]<br>https://mapcomplete.osm.be /[themename].html?[paràmetres com ara lat i lon]#import<br>[totes les etiquetes de la funció] </div>" | ||||||
|         "inspectDataTitle": "Inspecciona les dades de {count} funcions per importar", |         }, | ||||||
|         "inspectDidAutoDected": "La capa es va seleccionar automàticament", |         "login": { | ||||||
|         "inspectLooksCorrect": "Aquests valors semblen correctes", |             "lockNotice": "Aquesta pàgina està bloquejada. Necessites {importHelperUnlock} conjunts de canvis per poder accedir aquí.", | ||||||
|         "lockNotice": "Aquesta pàgina està bloquejada. Necessites {importHelperUnlock} conjunts de canvis per poder accedir aquí.", |             "loggedInWith": "Actualment has entrat com a <b>{name}</b> i has fet {csCount} conjunts de canvis", | ||||||
|         "locked": "Necessites almenys {importHelperUnlock} per utilitzar l'ajudant d'importació", |             "loginIsCorrect": "<b>{name}</b> és el compte correcte per crear les notes d'importació.", | ||||||
|         "loggedInWith": "Actualment has entrat com a <b>{name}</b> i has fet {csCount} conjunts de canvis", |             "loginRequired": "Has d'entrar per continuar", | ||||||
|         "loginIsCorrect": "<b>{name}</b> és el compte correcte per crear les notes d'importació.", |             "userAccountTitle": "Seleccionar compte d'usuari" | ||||||
|         "loginRequired": "Has d'entrar per continuar", |         }, | ||||||
|         "mapPreview": { |         "mapPreview": { | ||||||
|             "autodetected": "La capa es va deduir automàticament en funció de les propietats", |             "autodetected": "La capa es va deduir automàticament en funció de les propietats", | ||||||
|             "confirm": "Les característiques es troben a la ubicació correcta del mapa", |             "confirm": "Les característiques es troben a la ubicació correcta del mapa", | ||||||
|  | @ -294,6 +294,12 @@ | ||||||
|             "selectLayer": "Amb quina capa coincideix aquesta importació?", |             "selectLayer": "Amb quina capa coincideix aquesta importació?", | ||||||
|             "title": "Vista prèvia del mapa" |             "title": "Vista prèvia del mapa" | ||||||
|         }, |         }, | ||||||
|  |         "previewAttributes": { | ||||||
|  |             "allAttributesSame": "Totes les funcions a importar tenen aquesta etiqueta", | ||||||
|  |             "inspectDataTitle": "Inspecciona les dades de {count} funcions per importar", | ||||||
|  |             "inspectLooksCorrect": "Aquests valors semblen correctes", | ||||||
|  |             "someHaveSame": "{count} característiques per importar tenen aquesta etiqueta, això és un {percentage}% del total" | ||||||
|  |         }, | ||||||
|         "selectFile": { |         "selectFile": { | ||||||
|             "description": "Seleccionar un fitxer .csv o .geojson per començar", |             "description": "Seleccionar un fitxer .csv o .geojson per començar", | ||||||
|             "errDuplicate": "Algunes columnes tenen el mateix nom", |             "errDuplicate": "Algunes columnes tenen el mateix nom", | ||||||
|  | @ -308,11 +314,7 @@ | ||||||
|             "noFilesLoaded": "No s'ha carregat cap arxiu", |             "noFilesLoaded": "No s'ha carregat cap arxiu", | ||||||
|             "title": "Seleccionar arxiu" |             "title": "Seleccionar arxiu" | ||||||
|         }, |         }, | ||||||
|         "selectLayer": "Seleccionar capa...", |         "title": "Ajuda de l'importador" | ||||||
|         "someHaveSame": "{count} característiques per importar tenen aquesta etiqueta, això és un {percentage}% del total", |  | ||||||
|         "title": "Ajuda de l'importador", |  | ||||||
|         "userAccountTitle": "Seleccionar compte d'usuari", |  | ||||||
|         "validateDataTitle": "Validar dades" |  | ||||||
|     }, |     }, | ||||||
|     "importInspector": { |     "importInspector": { | ||||||
|         "title": "Inspeccionar i controlar notes d'importació" |         "title": "Inspeccionar i controlar notes d'importació" | ||||||
|  |  | ||||||
|  | @ -408,11 +408,10 @@ | ||||||
|             "title": "Thema auswählen", |             "title": "Thema auswählen", | ||||||
|             "unmatchedTitle": "Die folgenden Elemente stimmen mit keiner Voreinstellung überein" |             "unmatchedTitle": "Die folgenden Elemente stimmen mit keiner Voreinstellung überein" | ||||||
|         }, |         }, | ||||||
|         "someHaveSame": "{count} der zu importierenden Objekte haben dieses Tag, das sind {percentage}% der Gesamtzahl", |  | ||||||
|         "testMode": "Testmodus - Notizen werden nicht importiert", |         "testMode": "Testmodus - Notizen werden nicht importiert", | ||||||
|         "title": "Import-Helfer", |  | ||||||
|         "userAccountTitle": "Wähle einen Benutzeraccount", |         "userAccountTitle": "Wähle einen Benutzeraccount", | ||||||
|         "validateDataTitle": "Bestätige Daten" |         "validateDataTitle": "Bestätige Daten", | ||||||
|  |         "title": "Import-Helfer" | ||||||
|     }, |     }, | ||||||
|     "importInspector": { |     "importInspector": { | ||||||
|         "title": "Importhinweise überprüfen und verwalten" |         "title": "Importhinweise überprüfen und verwalten" | ||||||
|  |  | ||||||
|  | @ -619,6 +619,7 @@ | ||||||
|     }, |     }, | ||||||
|     "translations": { |     "translations": { | ||||||
|         "activateButton": "Help to translate MapComplete", |         "activateButton": "Help to translate MapComplete", | ||||||
|  |         "allMissing": "No translations yet", | ||||||
|         "completeness": "Translations for {theme} in {language} are at {percentage}%: {translated} strings out of {total} are translated", |         "completeness": "Translations for {theme} in {language} are at {percentage}%: {translated} strings out of {total} are translated", | ||||||
|         "deactivate": "Disable translation buttons", |         "deactivate": "Disable translation buttons", | ||||||
|         "help": "Click the 'translate'-icon next to a string to enter or update a piece of text. You need a Weblate-account for this. Create one with your OSM-username to automatically unlock translation mode.", |         "help": "Click the 'translate'-icon next to a string to enter or update a piece of text. You need a Weblate-account for this. Create one with your OSM-username to automatically unlock translation mode.", | ||||||
|  |  | ||||||
|  | @ -275,8 +275,7 @@ | ||||||
|         "selectFile": { |         "selectFile": { | ||||||
|             "title": "Seleccionar archivo" |             "title": "Seleccionar archivo" | ||||||
|         }, |         }, | ||||||
|         "title": "Ayudante de importación", |         "title": "Ayudante de importación" | ||||||
|         "validateDataTitle": "Validar datos" |  | ||||||
|     }, |     }, | ||||||
|     "importLayer": { |     "importLayer": { | ||||||
|         "layerName": "Posible {title}", |         "layerName": "Posible {title}", | ||||||
|  |  | ||||||
|  | @ -42,11 +42,6 @@ | ||||||
|         "getStartedLogin": "Entra no OpenStreetMap para comezar", |         "getStartedLogin": "Entra no OpenStreetMap para comezar", | ||||||
|         "getStartedNewAccount": " ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea unha nova conta</a>", |         "getStartedNewAccount": " ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea unha nova conta</a>", | ||||||
|         "goToInbox": "Abrir mensaxes", |         "goToInbox": "Abrir mensaxes", | ||||||
|         "index": { |  | ||||||
|             "intro": "O MapComplete é un visor e editor do OpenStreetMap, que te amosa información sobre un tema específico.", |  | ||||||
|             "pickTheme": "Escolle un tema para comezar.", |  | ||||||
|             "title": "Benvido ao MapComplete" |  | ||||||
|         }, |  | ||||||
|         "layerSelection": { |         "layerSelection": { | ||||||
|             "title": "Seleccionar capas", |             "title": "Seleccionar capas", | ||||||
|             "zoomInToSeeThisLayer": "Achégate para ver esta capa" |             "zoomInToSeeThisLayer": "Achégate para ver esta capa" | ||||||
|  |  | ||||||
|  | @ -267,8 +267,12 @@ | ||||||
|         "willBePublished": "A képed így lesz közzétéve: " |         "willBePublished": "A képed így lesz közzétéve: " | ||||||
|     }, |     }, | ||||||
|     "importHelper": { |     "importHelper": { | ||||||
|         "allAttributesSame": "Ez a címke minden importálandó objektumon szerepel", |         "introduction": { | ||||||
|         "description": "Az importálási segédprogram egy külső adatkészletet konvertál OSM-jegyzetekké. A külső adatkészletnek meg kell felelnie a MapComplete egyik meglévő rétegének. Az importálóba helyezett minden egyes elemhez egyetlen jegyzet fog létrejönni. Ezek a jegyzetek a megfelelő objektumokkal együtt fognak megjelenni ezeken a térképekben, hogy könnyen fel lehessen rajzolni őket a térképre." |             "description": "Az importálási segédprogram egy külső adatkészletet konvertál OSM-jegyzetekké. A külső adatkészletnek meg kell felelnie a MapComplete egyik meglévő rétegének. Az importálóba helyezett minden egyes elemhez egyetlen jegyzet fog létrejönni. Ezek a jegyzetek a megfelelő objektumokkal együtt fognak megjelenni ezeken a térképekben, hogy könnyen fel lehessen rajzolni őket a térképre." | ||||||
|  |         }, | ||||||
|  |         "previewAttributes": { | ||||||
|  |             "allAttributesSame": "Ez a címke minden importálandó objektumon szerepel" | ||||||
|  |         } | ||||||
|     }, |     }, | ||||||
|     "index": { |     "index": { | ||||||
|         "#": "Ezek a szövegek akkor jelennek meg a témagombok felett, ha nincs betöltve téma", |         "#": "Ezek a szövegek akkor jelennek meg a témagombok felett, ha nincs betöltve téma", | ||||||
|  |  | ||||||
|  | @ -97,12 +97,6 @@ | ||||||
|         "backToMapcomplete": "Terug naar het themaoverzicht", |         "backToMapcomplete": "Terug naar het themaoverzicht", | ||||||
|         "backgroundMap": "Achtergrondkaart", |         "backgroundMap": "Achtergrondkaart", | ||||||
|         "cancel": "Annuleren", |         "cancel": "Annuleren", | ||||||
|         "centerMessage": { |  | ||||||
|             "loadingData": "Data wordt geladen…", |  | ||||||
|             "ready": "Klaar!", |  | ||||||
|             "retrying": "Data inladen mislukt. Opnieuw proberen over {count} seconden…", |  | ||||||
|             "zoomIn": "Zoom in om de data te zien en te bewerken" |  | ||||||
|         }, |  | ||||||
|         "confirm": "Bevestigen", |         "confirm": "Bevestigen", | ||||||
|         "customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.", |         "customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.", | ||||||
|         "download": { |         "download": { | ||||||
|  | @ -134,12 +128,6 @@ | ||||||
|         "histogram": { |         "histogram": { | ||||||
|             "error_loading": "Kan het histogram niet laden" |             "error_loading": "Kan het histogram niet laden" | ||||||
|         }, |         }, | ||||||
|         "index": { |  | ||||||
|             "#": "Deze teksten worden getoond boven de themaknoppen als er geen thema is geladen", |  | ||||||
|             "intro": "MapComplete is een OpenStreetMap applicatie waar informatie over een specifiek thema bekeken en aangepast kan worden.", |  | ||||||
|             "pickTheme": "Kies hieronder een thema om te beginnen.", |  | ||||||
|             "title": "Welkom bij MapComplete" |  | ||||||
|         }, |  | ||||||
|         "layerSelection": { |         "layerSelection": { | ||||||
|             "title": "Selecteer lagen", |             "title": "Selecteer lagen", | ||||||
|             "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien" |             "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien" | ||||||
|  | @ -202,22 +190,6 @@ | ||||||
|         "readYourMessages": "Gelieve eerst je berichten op OpenStreetMap te lezen alvorens nieuwe punten toe te voegen.", |         "readYourMessages": "Gelieve eerst je berichten op OpenStreetMap te lezen alvorens nieuwe punten toe te voegen.", | ||||||
|         "removeLocationHistory": "Verwijder de geschiedenis aan locaties", |         "removeLocationHistory": "Verwijder de geschiedenis aan locaties", | ||||||
|         "returnToTheMap": "Ga terug naar de kaart", |         "returnToTheMap": "Ga terug naar de kaart", | ||||||
|         "reviews": { |  | ||||||
|             "affiliated_reviewer_warning": "(Review door betrokkene)", |  | ||||||
|             "attribution": "De beoordelingen worden voorzien door <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> en zijn beschikbaar onder de<a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0-licentie</a>.", |  | ||||||
|             "i_am_affiliated": "<span>Ik ben persoonlijk betrokken</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span>", |  | ||||||
|             "name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken", |  | ||||||
|             "no_rating": "Geen score bekend", |  | ||||||
|             "no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!", |  | ||||||
|             "plz_login": "Meld je aan om een beoordeling te geven", |  | ||||||
|             "posting_as": "Ingelogd als", |  | ||||||
|             "saved": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>", |  | ||||||
|             "saving_review": "Opslaan…", |  | ||||||
|             "title": "{count} beoordelingen", |  | ||||||
|             "title_singular": "Eén beoordeling", |  | ||||||
|             "tos": "Als je je review publiceert, ga je akkoord met de <a href='https://mangrove.reviews/terms' target='_blank'>de gebruiksvoorwaarden en privacy policy van Mangrove.reviews</a>", |  | ||||||
|             "write_a_comment": "Schrijf een beoordeling…" |  | ||||||
|         }, |  | ||||||
|         "save": "Opslaan", |         "save": "Opslaan", | ||||||
|         "screenToSmall": "Open {theme} in een nieuw venster", |         "screenToSmall": "Open {theme} in een nieuw venster", | ||||||
|         "search": { |         "search": { | ||||||
|  | @ -304,17 +276,17 @@ | ||||||
|         "willBePublished": "Jouw foto wordt gepubliceerd " |         "willBePublished": "Jouw foto wordt gepubliceerd " | ||||||
|     }, |     }, | ||||||
|     "importHelper": { |     "importHelper": { | ||||||
|         "allAttributesSame": "Alle kaart-objecten om te importeren hebben deze tag", |         "introduction": { | ||||||
|         "description": "De importeer-helper converteert een externe dataset in OSM-kaartnotas. De externe data moet overeenkomen met een bestaande MapComplete-laag. Voor elk item wordt er een kaartnota gemaakt. Deze notas worden dan samen met de relevante POI getoond en kunnen dan (via MapComplete) snel en eenvoudig toegevoegd worden.", |             "description": "De importeer-helper converteert een externe dataset in OSM-kaartnotas. De externe data moet overeenkomen met een bestaande MapComplete-laag. Voor elk item wordt er een kaartnota gemaakt. Deze notas worden dan samen met de relevante POI getoond en kunnen dan (via MapComplete) snel en eenvoudig toegevoegd worden.", | ||||||
|         "importFormat": "Een kaartnota moet het volgende formaat hebben om gedetecteerd te worden binnen een laag: <br><div class=\"literal-code\">[Een introductietekst]<br>https://mapcomplete.osm.be/[themename].html?[parameters waaronder lon en lat]#import<br>[alle tags van het te importeren object] </div>", |             "importFormat": "Een kaartnota moet het volgende formaat hebben om gedetecteerd te worden binnen een laag: <br><div class=\"literal-code\">[Een introductietekst]<br>https://mapcomplete.osm.be/[themename].html?[parameters waaronder lon en lat]#import<br>[alle tags van het te importeren object] </div>" | ||||||
|         "inspectDataTitle": "Bekijk de data van {count} te importeren objecten", |         }, | ||||||
|         "inspectDidAutoDected": "Deze laag werd automatisch gekozen", |         "login": { | ||||||
|         "inspectLooksCorrect": "Deze waardes zien er correct uit", |             "lockNotice": "Deze pagina is afgeschermd. Je hebt minstens {importHelperUnlock} changesets nodig voor je deze pagina mag gebruiken.", | ||||||
|         "lockNotice": "Deze pagina is afgeschermd. Je hebt minstens {importHelperUnlock} changesets nodig voor je deze pagina mag gebruiken.", |             "loggedInWith": "Je bent momenteel aangemeld als <b>{name}</b> and maakte {csCount} eerdere wijzigingen", | ||||||
|         "locked": "Je hebt minstens {importHelperUnlock} changesets nodig om de import helper te gebruiken", |             "loginIsCorrect": "<b>{name}</b> is de correcte account om de import-nota's mee te maken.", | ||||||
|         "loggedInWith": "Je bent momenteel aangemeld als <b>{name}</b> and maakte {csCount} eerdere wijzigingen", |             "loginRequired": "Je moet ingelogd zijn om verder te gaan", | ||||||
|         "loginIsCorrect": "<b>{name}</b> is de correcte account om de import-nota's mee te maken.", |             "userAccountTitle": "Selecteer een account" | ||||||
|         "loginRequired": "Je moet ingelogd zijn om verder te gaan", |         }, | ||||||
|         "mapPreview": { |         "mapPreview": { | ||||||
|             "autodetected": "Deze laag was automatisch gekozen gebaseerd op de aanwezige eigenschappen", |             "autodetected": "Deze laag was automatisch gekozen gebaseerd op de aanwezige eigenschappen", | ||||||
|             "confirm": "De objecten bevinden zich op de juiste locatie", |             "confirm": "De objecten bevinden zich op de juiste locatie", | ||||||
|  | @ -322,6 +294,12 @@ | ||||||
|             "selectLayer": "Met welke laag komt deze te importeren dataset overeen?", |             "selectLayer": "Met welke laag komt deze te importeren dataset overeen?", | ||||||
|             "title": "Voorbeeldkaart" |             "title": "Voorbeeldkaart" | ||||||
|         }, |         }, | ||||||
|  |         "previewAttributes": { | ||||||
|  |             "allAttributesSame": "Alle kaart-objecten om te importeren hebben deze tag", | ||||||
|  |             "inspectDataTitle": "Bekijk de data van {count} te importeren objecten", | ||||||
|  |             "inspectLooksCorrect": "Deze waardes zien er correct uit", | ||||||
|  |             "someHaveSame": "{count} te importeren objecten hebben dit attribuut, dit is {percentage}% van het totaal" | ||||||
|  |         }, | ||||||
|         "selectFile": { |         "selectFile": { | ||||||
|             "description": "Selecteer een .csv of .geojson-bestand", |             "description": "Selecteer een .csv of .geojson-bestand", | ||||||
|             "errDuplicate": "Sommige kolommen hebben dezelfde naam", |             "errDuplicate": "Sommige kolommen hebben dezelfde naam", | ||||||
|  | @ -336,11 +314,7 @@ | ||||||
|             "noFilesLoaded": "Geen bestand ingeladen op dit moment", |             "noFilesLoaded": "Geen bestand ingeladen op dit moment", | ||||||
|             "title": "Selecteer bestand" |             "title": "Selecteer bestand" | ||||||
|         }, |         }, | ||||||
|         "selectLayer": "Selecteer een laag...", |         "title": "Importeer-helper" | ||||||
|         "someHaveSame": "{count} te importeren objecten hebben dit attribuut, dit is {percentage}% van het totaal", |  | ||||||
|         "title": "Importeer-helper", |  | ||||||
|         "userAccountTitle": "Selecteer een account", |  | ||||||
|         "validateDataTitle": "Valideer data" |  | ||||||
|     }, |     }, | ||||||
|     "importInspector": { |     "importInspector": { | ||||||
|         "title": "Inspecteer en beheer importeer-notas" |         "title": "Inspecteer en beheer importeer-notas" | ||||||
|  |  | ||||||
							
								
								
									
										112
									
								
								langs/pl.json
									
										
									
									
									
								
							
							
						
						
									
										112
									
								
								langs/pl.json
									
										
									
									
									
								
							|  | @ -35,121 +35,9 @@ | ||||||
|         "cancel": "Anuluj", |         "cancel": "Anuluj", | ||||||
|         "customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.", |         "customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.", | ||||||
|         "fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.", |         "fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.", | ||||||
|         "general": { |  | ||||||
|             "about": "Łatwo edytuj i dodaj OpenStreetMap dla określonego motywu", |  | ||||||
|             "aboutMapcomplete": "<h3>O MapComplete</h3><p>Dzięki MapComplete możesz wzbogacić OpenStreetMap o informacje na <b>pojedynczy temat.</b> Odpowiedz na kilka pytań, a w ciągu kilku minut Twój wkład będzie dostępny na całym świecie! Opiekun <b>tematu</b> definiuje elementy, pytania i języki dla tematu.</p><h3>Dowiedz się więcej</h3><p>MapComplete zawsze <b>oferuje następny krok</b>, by dowiedzieć się więcej o OpenStreetMap.</p><ul><li>Po osadzeniu na stronie internetowej, element iframe łączy się z pełnoekranowym MapComplete</li><li>Wersja pełnoekranowa oferuje informacje o OpenStreetMap</li><li>Przeglądanie działa bez logowania, ale edycja wymaga loginu OSM.</li><li>Jeżeli nie jesteś zalogowany, zostaniesz poproszony o zalogowanie się</li><li>Po udzieleniu odpowiedzi na jedno pytanie, możesz dodać nowe punkty do mapy</li><li>Po chwili wyświetlane są rzeczywiste tagi OSM, które później linkują do wiki</li></ul><p></p><br><p>Zauważyłeś <b>problem</b>? Czy masz <b>prośbę o dodanie jakiejś funkcji</b>? Chcesz <b>pomóc w tłumaczeniu</b>? Udaj się do <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">kodu źródłowego</a> lub <a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">issue trackera.</a> </p><p> Chcesz zobaczyć <b>swoje postępy</b>? Śledź liczbę edycji na <a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>.</p>", |  | ||||||
|             "add": { |  | ||||||
|                 "addNew": "Dodaj nową {category} tutaj", |  | ||||||
|                 "confirmButton": "Dodaj tutaj {category}.<br><div class=\"alert\">Twój dodatek jest widoczny dla wszystkich</div>", |  | ||||||
|                 "confirmIntro": "<h3>Czy dodać tutaj {title}?</h3> Punkt, który tutaj utworzysz, będzie <b>widoczny dla wszystkich</b>. Proszę, dodawaj rzeczy do mapy tylko wtedy, gdy naprawdę istnieją. Wiele aplikacji korzysta z tych danych.", |  | ||||||
|                 "intro": "Kliknąłeś gdzieś, gdzie nie są jeszcze znane żadne dane.<br>", |  | ||||||
|                 "layerNotEnabled": "Warstwa {layer} nie jest włączona. Włącz tę warstwę, aby dodać punkt", |  | ||||||
|                 "openLayerControl": "Otwórz okno sterowania warstwą", |  | ||||||
|                 "pleaseLogin": "<a class=\"activate-osm-authentication\">Zaloguj się, aby dodać nowy punkt</a>", |  | ||||||
|                 "stillLoading": "Dane wciąż się ładują. Poczekaj chwilę, zanim dodasz nowy punkt.", |  | ||||||
|                 "title": "Czy dodać nowy punkt?", |  | ||||||
|                 "zoomInFurther": "Powiększ jeszcze bardziej, aby dodać punkt." |  | ||||||
|             }, |  | ||||||
|             "backgroundMap": "Tło mapy", |  | ||||||
|             "cancel": "Anuluj", |  | ||||||
|             "customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.", |  | ||||||
|             "fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.", |  | ||||||
|             "getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć", |  | ||||||
|             "getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>", |  | ||||||
|             "goToInbox": "Otwórz skrzynkę odbiorczą", |  | ||||||
|             "layerSelection": { |  | ||||||
|                 "title": "Wybierz warstwy", |  | ||||||
|                 "zoomInToSeeThisLayer": "Powiększ, aby zobaczyć tę warstwę" |  | ||||||
|             }, |  | ||||||
|             "loginToStart": "Zaloguj się, aby odpowiedzieć na to pytanie", |  | ||||||
|             "loginWithOpenStreetMap": "Zaloguj z OpenStreetMap", |  | ||||||
|             "nameInlineQuestion": "Nazwa tej {category} to $$$", |  | ||||||
|             "noNameCategory": "{category} bez nazwy", |  | ||||||
|             "noTagsSelected": "Nie wybrano tagów", |  | ||||||
|             "number": "numer", |  | ||||||
|             "oneSkippedQuestion": "Jedno pytanie zostało pominięte", |  | ||||||
|             "opening_hours": { |  | ||||||
|                 "closed_permanently": "Zamknięte na nieokreślony czas", |  | ||||||
|                 "closed_until": "Zamknięte do {date}", |  | ||||||
|                 "error_loading": "Błąd: nie można zwizualizować tych godzin otwarcia.", |  | ||||||
|                 "not_all_rules_parsed": "Godziny otwarcia tego sklepu są skomplikowane. Następujące reguły są ignorowane w elemencie wejściowym:", |  | ||||||
|                 "openTill": "do", |  | ||||||
|                 "open_24_7": "Otwarte przez całą dobę", |  | ||||||
|                 "open_during_ph": "W czasie świąt państwowych udogodnienie to jest", |  | ||||||
|                 "opensAt": "z", |  | ||||||
|                 "ph_closed": "zamknięte", |  | ||||||
|                 "ph_not_known": " ", |  | ||||||
|                 "ph_open": "otwarte" |  | ||||||
|             }, |  | ||||||
|             "osmLinkTooltip": "Zobacz ten obiekt na OpenStreetMap, aby uzyskać historię i więcej opcji edycji", |  | ||||||
|             "pickLanguage": "Wybierz język: ", |  | ||||||
|             "questions": { |  | ||||||
|                 "emailIs": "Adres e-mail {category} to <a href=\"mailto:{email}\" target=\"_blank\">{email}</a>", |  | ||||||
|                 "emailOf": "Jaki jest adres e-mail {category}?", |  | ||||||
|                 "phoneNumberIs": "Numer telefonu {category} to <a target=\"_blank\">{phone}</a>", |  | ||||||
|                 "phoneNumberOf": "Jaki jest numer telefonu do {category}?", |  | ||||||
|                 "websiteIs": "Strona internetowa: <a href=\"{website}\" target=\"_blank\">{website}</a>", |  | ||||||
|                 "websiteOf": "Jaka jest strona internetowa {category}?" |  | ||||||
|             }, |  | ||||||
|             "readYourMessages": "Przeczytaj wszystkie wiadomości OpenStreetMap przed dodaniem nowego punktu.", |  | ||||||
|             "returnToTheMap": "Wróć do mapy", |  | ||||||
|             "save": "Zapisz", |  | ||||||
|             "search": { |  | ||||||
|                 "error": "Coś poszło nie tak…", |  | ||||||
|                 "nothing": "Nic nie znaleziono…", |  | ||||||
|                 "search": "Wyszukaj lokalizację", |  | ||||||
|                 "searching": "Szukanie…" |  | ||||||
|             }, |  | ||||||
|             "sharescreen": { |  | ||||||
|                 "addToHomeScreen": "<h3> Dodaj do ekranu głównego</h3>Możesz łatwo dodać tę stronę do ekranu głównego smartfona, aby poczuć się jak w domu. Kliknij przycisk \"Dodaj do ekranu głównego\" na pasku adresu URL, aby to zrobić.", |  | ||||||
|                 "copiedToClipboard": "Link został skopiowany do schowka", |  | ||||||
|                 "editThemeDescription": "Dodaj lub zmień pytania do tego motywu mapy", |  | ||||||
|                 "editThisTheme": "Edytuj ten motyw", |  | ||||||
|                 "embedIntro": "<h3>Umieść na swojej stronie internetowej</h3>Proszę, umieść tę mapę na swojej stronie internetowej. <br>Zachęcamy cię do tego - nie musisz nawet pytać o zgodę. <br>Jest ona darmowa i zawsze będzie. Im więcej osób jej używa, tym bardziej staje się wartościowa.", |  | ||||||
|                 "fsAddNew": "Włącz przycisk \"Dodaj nowe POI\"", |  | ||||||
|                 "fsGeolocation": "Włącz przycisk „Zlokalizuj mnie” (tylko na urządzeniach mobilnych)", |  | ||||||
|                 "fsIncludeCurrentBackgroundMap": "Dołącz bieżący wybór tła <b>{name}</b>", |  | ||||||
|                 "fsIncludeCurrentLayers": "Uwzględnij wybór bieżącej warstwy", |  | ||||||
|                 "fsIncludeCurrentLocation": "Uwzględnij bieżącą lokalizację", |  | ||||||
|                 "fsLayerControlToggle": "Zacznij od rozwiniętej kontroli warstw", |  | ||||||
|                 "fsLayers": "Włącz kontrolę warstw", |  | ||||||
|                 "fsSearch": "Włącz pasek wyszukiwania", |  | ||||||
|                 "fsUserbadge": "Włącz przycisk logowania", |  | ||||||
|                 "fsWelcomeMessage": "Pokaż wyskakujące okienko wiadomości powitalnej i powiązane zakładki", |  | ||||||
|                 "intro": "<h3> Udostępnij tę mapę</h3> Udostępnij tę mapę, kopiując poniższy link i wysyłając ją do przyjaciół i rodziny:", |  | ||||||
|                 "thanksForSharing": "Dzięki za udostępnienie!" |  | ||||||
|             }, |  | ||||||
|             "skip": "Pomiń to pytanie", |  | ||||||
|             "skippedQuestions": "Niektóre pytania są pominięte", |  | ||||||
|             "weekdays": { |  | ||||||
|                 "abbreviations": { |  | ||||||
|                     "friday": "Pt", |  | ||||||
|                     "monday": "Pn", |  | ||||||
|                     "saturday": "Sob", |  | ||||||
|                     "sunday": "Niedz", |  | ||||||
|                     "thursday": "Czw", |  | ||||||
|                     "tuesday": "Wt", |  | ||||||
|                     "wednesday": "Śr" |  | ||||||
|                 }, |  | ||||||
|                 "friday": "Piątek", |  | ||||||
|                 "monday": "Poniedziałek", |  | ||||||
|                 "saturday": "Sobota", |  | ||||||
|                 "sunday": "Niedziela", |  | ||||||
|                 "thursday": "Czwartek", |  | ||||||
|                 "tuesday": "Wtorek", |  | ||||||
|                 "wednesday": "Środa" |  | ||||||
|             }, |  | ||||||
|             "welcomeBack": "Jesteś zalogowany, witaj z powrotem!" |  | ||||||
|         }, |  | ||||||
|         "getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć", |         "getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć", | ||||||
|         "getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>", |         "getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>", | ||||||
|         "goToInbox": "Otwórz skrzynkę odbiorczą", |         "goToInbox": "Otwórz skrzynkę odbiorczą", | ||||||
|         "index": { |  | ||||||
|             "#": "Te teksty są wyświetlane nad przyciskami motywu, gdy nie jest załadowany żaden motyw", |  | ||||||
|             "intro": "MapComplete to przeglądarka i edytor OpenStreetMap, który pokazuje informacje podzielone według tematu.", |  | ||||||
|             "pickTheme": "Wybierz temat poniżej, aby rozpocząć.", |  | ||||||
|             "title": "Witaj w MapComplete" |  | ||||||
|         }, |  | ||||||
|         "layerSelection": { |         "layerSelection": { | ||||||
|             "title": "Wybierz warstwy", |             "title": "Wybierz warstwy", | ||||||
|             "zoomInToSeeThisLayer": "Powiększ, aby zobaczyć tę warstwę" |             "zoomInToSeeThisLayer": "Powiększ, aby zobaczyć tę warstwę" | ||||||
|  |  | ||||||
|  | @ -90,130 +90,6 @@ | ||||||
|             "title": "下載可視的資料" |             "title": "下載可視的資料" | ||||||
|         }, |         }, | ||||||
|         "fewChangesBefore": "請先回答有關既有節點的問題再來新增新節點。", |         "fewChangesBefore": "請先回答有關既有節點的問題再來新增新節點。", | ||||||
|         "general": { |  | ||||||
|             "about": "相當容易編輯,而且能為開放街圖新增特定主題", |  | ||||||
|             "aboutMapcomplete": "<h3>關於 MapComplete</h3><p>使用 MapComplete 你可以藉由<b>單一主題</b>豐富開放街圖的圖資。回答幾個問題,然後幾分鐘之內你的貢獻立刻就傳遍全球!<b>主題維護者</b>定議主題的元素、問題與語言。</p><h3>發現更多</h3><p>MapComplete 總是提供學習更多開放街圖<b>下一步的知識</b>。</p><ul><li>當你內嵌網站,網頁內嵌會連結到全螢幕的 MapComplete</li><li>全螢幕的版本提供關於開放街圖的資訊</li><li>不登入檢視成果,但是要編輯則需登入 OSM。</li><li>如果你沒有登入,你會被要求先登入</li><li>當你回答單一問題時,你可以在地圖新增新的節點</li><li>過了一陣子,實際的 OSM-標籤會顯示,之後會連結到 wiki</li></ul><p></p><br><p>你有注意到<b>問題</b>嗎?你想請求<b>功能</b>嗎?想要<b>幫忙翻譯</b>嗎?來到<a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">原始碼</a>或是<a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">問題追蹤器。</a></p><p>想要看到<b>你的進度</b>嗎?到<a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>追蹤編輯數。</p>", |  | ||||||
|             "add": { |  | ||||||
|                 "addNew": "在這裡新增新的 {category}", |  | ||||||
|                 "confirmButton": "在此新增 {category}。<br><div class=\"alert\">大家都可以看到您新增的內容</div>", |  | ||||||
|                 "confirmIntro": "<h3>在這裡新增 {title} ?</h3>你在這裡新增的節點<b>所有人都看得到</b>。請只有在確定有物件存在的情形下才新增上去,許多應用程式都使用這份資料。", |  | ||||||
|                 "intro": "您點擊處目前未有已知的資料。<br>", |  | ||||||
|                 "layerNotEnabled": "圖層 {layer} 目前無法使用,請先啟用這圖層再加新的節點", |  | ||||||
|                 "openLayerControl": "開啟圖層控制框", |  | ||||||
|                 "pleaseLogin": "<a class=\"activate-osm-authentication\">請先登入來新增節點</a>", |  | ||||||
|                 "stillLoading": "目前仍在載入資料,請稍後再來新增節點。", |  | ||||||
|                 "title": "新增新的節點?", |  | ||||||
|                 "zoomInFurther": "放大來新增新的節點。" |  | ||||||
|             }, |  | ||||||
|             "attribution": { |  | ||||||
|                 "attributionContent": "<p>所有資料由<a href=\"https://osm.org\" target=\"_blank\">開放街圖</a>提供,在<a href=\"https://osm.org/copyright\" target=\"_blank\">開放資料庫授權條款</a>之下自由再利用。</p>", |  | ||||||
|                 "attributionTitle": "署名通知", |  | ||||||
|                 "codeContributionsBy": "MapComplete 是由 {contributors} 和其他 <a href=\"https://github.com/pietervdvn/MapComplete/graphs/contributors\" target=\"_blank\">{hiddenCount} 位貢獻者</a>構建而成", |  | ||||||
|                 "iconAttribution": { |  | ||||||
|                     "title": "使用的圖示" |  | ||||||
|                 }, |  | ||||||
|                 "mapContributionsBy": "目前檢視的資料由 {contributors} 貢獻編輯", |  | ||||||
|                 "mapContributionsByAndHidden": "目前顯到的資料是由 {contributors} 和其他 {hiddenCount} 位貢獻者編輯貢獻", |  | ||||||
|                 "themeBy": "由 {author} 維護主題" |  | ||||||
|             }, |  | ||||||
|             "backgroundMap": "背景地圖", |  | ||||||
|             "cancel": "取消", |  | ||||||
|             "customThemeIntro": "<h3>客製化主題</h3>觀看這些先前使用者創造的主題。", |  | ||||||
|             "fewChangesBefore": "請先回答有關既有節點的問題再來新增新節點。", |  | ||||||
|             "getStartedLogin": "登入開放街圖帳號來開始", |  | ||||||
|             "getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>", |  | ||||||
|             "goToInbox": "開啟訊息框", |  | ||||||
|             "layerSelection": { |  | ||||||
|                 "title": "選擇圖層", |  | ||||||
|                 "zoomInToSeeThisLayer": "放大來看這個圖層" |  | ||||||
|             }, |  | ||||||
|             "loginToStart": "登入之後來回答這問題", |  | ||||||
|             "loginWithOpenStreetMap": "用開放街圖帳號登入", |  | ||||||
|             "morescreen": { |  | ||||||
|                 "createYourOwnTheme": "從零開始建立你的 MapComplete 主題", |  | ||||||
|                 "intro": "<h3>看更多主題地圖?</h3>您喜歡蒐集地理資料嗎?<br>還有更多主題。", |  | ||||||
|                 "requestATheme": "如果你有客製化要求,請到問題追踪器那邊提出要求", |  | ||||||
|                 "streetcomplete": "行動裝置另有類似的應用程式 <a class=\"underline hover:text-blue-800\" href=\"https://play.google.com/store/apps/details?id=de.westnordost.streetcomplete\" target=\"_blank\">StreetComplete</a>。" |  | ||||||
|             }, |  | ||||||
|             "nameInlineQuestion": "這個 {category} 的名稱是 $$$", |  | ||||||
|             "noNameCategory": "{category} 沒有名稱", |  | ||||||
|             "noTagsSelected": "沒有選取標籤", |  | ||||||
|             "number": "號碼", |  | ||||||
|             "oneSkippedQuestion": "跳過一個問題", |  | ||||||
|             "openStreetMapIntro": "<h3>開放的地圖</h3><p>如果有一份地圖,任何人都能自由使用與編輯,單一的地圖能夠儲存所有地理相關資訊?這樣不就很酷嗎?接著,所有的網站使用不同的、範圍小的,不相容的地圖 (通常也都過時了),也就不再需要了。</p><p><b><a href=\"https://OpenStreetMap.org\" target=\"_blank\">開放街圖</a></b>就是這樣的地圖,人人都能免費這些圖資 (只要<a href=\"https://osm.org/copyright\" target=\"_blank\">署名與公開變動這資料</a>)。只要遵循這些,任何人都能自由新增新資料與修正錯誤,這些網站也都使用開放街圖,資料也都來自開放街圖,你的答案與修正也會加到開放街圖上面。</p><p>許多人與應用程式已經採用開放街圖了:<a href=\"https://organicmaps.app//\" target=\"_blank\">Organic Maps</a>、<a href=\"https://osmAnd.net\" target=\"_blank\">OsmAnd</a>,還有 Facebook、Instagram,蘋果地圖、Bing 地圖(部分)採用開放街圖。如果你在開放街圖上變動資料,也會同時影響這些應用 - 在他們下次更新資料之後!</p>", |  | ||||||
|             "opening_hours": { |  | ||||||
|                 "closed_permanently": "不清楚關閉多久了", |  | ||||||
|                 "closed_until": "{date} 起關閉", |  | ||||||
|                 "error_loading": "錯誤:無法視覺化開放時間。", |  | ||||||
|                 "not_all_rules_parsed": "這間店的開放時間相當複雜,在輸入元素時忽略接下來的規則:", |  | ||||||
|                 "openTill": "結束時間", |  | ||||||
|                 "open_24_7": "24小時營業", |  | ||||||
|                 "open_during_ph": "國定假日的時候,這個場所是", |  | ||||||
|                 "opensAt": "開始時間", |  | ||||||
|                 "ph_closed": "無營業", |  | ||||||
|                 "ph_not_known": " ", |  | ||||||
|                 "ph_open": "有營業" |  | ||||||
|             }, |  | ||||||
|             "osmLinkTooltip": "在開放街圖歷史和更多編輯選項下面來檢視這物件", |  | ||||||
|             "pickLanguage": "選擇語言: ", |  | ||||||
|             "questions": { |  | ||||||
|                 "emailIs": "{category} 的電子郵件地址是<a href=\"mailto:{email}\" target=\"_blank\">{email}</a>", |  | ||||||
|                 "emailOf": "{category} 的電子郵件地址是?", |  | ||||||
|                 "phoneNumberIs": "此 {category} 的電話號碼為 <a target=\"_blank\">{phone}</a>", |  | ||||||
|                 "phoneNumberOf": "{category} 的電話號碼是?", |  | ||||||
|                 "websiteIs": "網站:<a href=\"{website}\" target=\"_blank\">{website}</a>", |  | ||||||
|                 "websiteOf": "{category} 的網站網址是?" |  | ||||||
|             }, |  | ||||||
|             "readYourMessages": "請先閱讀開放街圖訊息之前再來新增新節點。", |  | ||||||
|             "returnToTheMap": "回到地圖", |  | ||||||
|             "save": "儲存", |  | ||||||
|             "search": { |  | ||||||
|                 "error": "有狀況發生了…", |  | ||||||
|                 "nothing": "沒有找到…", |  | ||||||
|                 "search": "搜尋地點", |  | ||||||
|                 "searching": "搜尋中…" |  | ||||||
|             }, |  | ||||||
|             "sharescreen": { |  | ||||||
|                 "addToHomeScreen": "<h3>新增到您的主畫面</h3>您可以輕易將這網站新增到您智慧型手機的主畫面,在網址列點選「新增到主畫面按鈕」來做這件事情。", |  | ||||||
|                 "copiedToClipboard": "複製連結到簡貼簿", |  | ||||||
|                 "editThemeDescription": "新增或改變這個地圖的問題", |  | ||||||
|                 "editThisTheme": "編輯這個主題", |  | ||||||
|                 "embedIntro": "<h3>嵌入到你的網站</h3>請考慮將這份地圖嵌入您的網站。<br>地圖毋須額外授權,非常歡迎您多加利用。<br>一切都是免費的,而且之後也是免費的,越有更多人使用,則越顯得它的價值。", |  | ||||||
|                 "fsAddNew": "啟用'新增新的興趣點'按鈕", |  | ||||||
|                 "fsGeolocation": "啟用'地理定位自身'按鈕 (只有行動版本)", |  | ||||||
|                 "fsIncludeCurrentBackgroundMap": "包含目前背景選擇<b>{name}</b>", |  | ||||||
|                 "fsIncludeCurrentLayers": "包含目前選擇圖層", |  | ||||||
|                 "fsIncludeCurrentLocation": "包含目前位置", |  | ||||||
|                 "fsLayerControlToggle": "開始時擴展圖層控制", |  | ||||||
|                 "fsLayers": "啟用圖層控制", |  | ||||||
|                 "fsSearch": "啟用搜尋列", |  | ||||||
|                 "fsUserbadge": "啟用登入按鈕", |  | ||||||
|                 "fsWelcomeMessage": "顯示歡迎訊息以及相關頁籤", |  | ||||||
|                 "intro": "<h3>分享這地圖</h3>複製下面的連結來向朋友與家人分享這份地圖:", |  | ||||||
|                 "thanksForSharing": "感謝分享!" |  | ||||||
|             }, |  | ||||||
|             "skip": "跳過這問題", |  | ||||||
|             "skippedQuestions": "有些問題已經跳過了", |  | ||||||
|             "weekdays": { |  | ||||||
|                 "abbreviations": { |  | ||||||
|                     "friday": "星期五", |  | ||||||
|                     "monday": "星期一", |  | ||||||
|                     "saturday": "星期六", |  | ||||||
|                     "sunday": "星期日", |  | ||||||
|                     "thursday": "星期四", |  | ||||||
|                     "tuesday": "星期二", |  | ||||||
|                     "wednesday": "星期三" |  | ||||||
|                 }, |  | ||||||
|                 "friday": "星期五", |  | ||||||
|                 "monday": "星期一", |  | ||||||
|                 "saturday": "星期六", |  | ||||||
|                 "sunday": "星期日", |  | ||||||
|                 "thursday": "星期四", |  | ||||||
|                 "tuesday": "星期二", |  | ||||||
|                 "wednesday": "星期三" |  | ||||||
|             }, |  | ||||||
|             "welcomeBack": "你已經登入了,歡迎回來!" |  | ||||||
|         }, |  | ||||||
|         "getStartedLogin": "登入開放街圖帳號來開始", |         "getStartedLogin": "登入開放街圖帳號來開始", | ||||||
|         "getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>", |         "getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>", | ||||||
|         "goToInbox": "開啟訊息框", |         "goToInbox": "開啟訊息框", | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -43,7 +43,7 @@ | ||||||
|         "libphonenumber-js": "^1.7.55", |         "libphonenumber-js": "^1.7.55", | ||||||
|         "lz-string": "^1.4.4", |         "lz-string": "^1.4.4", | ||||||
|         "mangrove-reviews": "^0.1.3", |         "mangrove-reviews": "^0.1.3", | ||||||
|         "moment": "^2.29.0", |         "moment": "^2.29.2", | ||||||
|         "opening_hours": "^3.6.0", |         "opening_hours": "^3.6.0", | ||||||
|         "osm-auth": "^1.0.2", |         "osm-auth": "^1.0.2", | ||||||
|         "osmtogeojson": "^3.0.0-beta.4", |         "osmtogeojson": "^3.0.0-beta.4", | ||||||
|  | @ -9738,9 +9738,9 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/moment": { |     "node_modules/moment": { | ||||||
|       "version": "2.29.1", |       "version": "2.29.2", | ||||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", |       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", | ||||||
|       "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", |       "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": "*" |         "node": "*" | ||||||
|       } |       } | ||||||
|  | @ -24294,9 +24294,9 @@ | ||||||
|       "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" |       "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" | ||||||
|     }, |     }, | ||||||
|     "moment": { |     "moment": { | ||||||
|       "version": "2.29.1", |       "version": "2.29.2", | ||||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", |       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", | ||||||
|       "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" |       "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" | ||||||
|     }, |     }, | ||||||
|     "monotone-convex-hull-2d": { |     "monotone-convex-hull-2d": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|  |  | ||||||
|  | @ -89,7 +89,7 @@ | ||||||
|     "libphonenumber-js": "^1.7.55", |     "libphonenumber-js": "^1.7.55", | ||||||
|     "lz-string": "^1.4.4", |     "lz-string": "^1.4.4", | ||||||
|     "mangrove-reviews": "^0.1.3", |     "mangrove-reviews": "^0.1.3", | ||||||
|     "moment": "^2.29.0", |     "moment": "^2.29.2", | ||||||
|     "opening_hours": "^3.6.0", |     "opening_hours": "^3.6.0", | ||||||
|     "osm-auth": "^1.0.2", |     "osm-auth": "^1.0.2", | ||||||
|     "osmtogeojson": "^3.0.0-beta.4", |     "osmtogeojson": "^3.0.0-beta.4", | ||||||
|  |  | ||||||
							
								
								
									
										61
									
								
								scripts/automoveTranslations.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								scripts/automoveTranslations.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | import * as languages from "../assets/generated/used_languages.json" | ||||||
|  | import {readFileSync, writeFileSync} from "fs"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Moves values around in 'section'. Section will be changed | ||||||
|  |  * @param section | ||||||
|  |  * @param referenceSection | ||||||
|  |  * @param language | ||||||
|  |  */ | ||||||
|  | function fixSection(section, referenceSection, language: string) { | ||||||
|  |     if(section === undefined){ | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |     outer: for (const key of Object.keys(section)) { | ||||||
|  |         const v = section[key] | ||||||
|  |         if(typeof v ==="string" && referenceSection[key] === undefined){ | ||||||
|  |             // Not found in reference, search for a subsection with this key
 | ||||||
|  |             for (const subkey of Object.keys(referenceSection)) { | ||||||
|  |                 const subreference = referenceSection[subkey] | ||||||
|  |                 if(subreference[key] !== undefined){ | ||||||
|  |                     if(section[subkey] !== undefined &&  section[subkey][key] !== undefined) { | ||||||
|  |                         console.log(`${subkey}${key} is already defined... Looking furhter`) | ||||||
|  |                         continue | ||||||
|  |                     } | ||||||
|  |                     if(typeof section[subkey] === "string"){ | ||||||
|  |                         console.log(`NOT overwriting '${section[subkey]}' for ${subkey} (needed for ${key})`) | ||||||
|  |                     }else{ | ||||||
|  |                         // apply fix
 | ||||||
|  |                         if(section[subkey] === undefined){ | ||||||
|  |                             section[subkey] = {} | ||||||
|  |                         } | ||||||
|  |                         section[subkey][key] = section[key] | ||||||
|  |                         delete  section[key] | ||||||
|  |                         console.log(`Rewritten key: ${key} --> ${subkey}.${key} in language ${language}`) | ||||||
|  |                         continue outer | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             console.log("No solution found for "+key) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | function main(args:string[]):void{ | ||||||
|  |     const sectionName = args[0] | ||||||
|  |     const l = args[1] | ||||||
|  |     if(sectionName === undefined){ | ||||||
|  |         console.log("Tries to automatically move translations to a new subsegment. Usage: 'sectionToCheck' 'language'") | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |     const reference = JSON.parse( readFileSync("./langs/en.json","UTF8")) | ||||||
|  |     const path = `./langs/${l}.json` | ||||||
|  |     const file = JSON.parse( readFileSync(path,"UTF8")) | ||||||
|  |     fixSection(file[sectionName], reference[sectionName], l) | ||||||
|  |     writeFileSync(path, JSON.stringify(file, null, "    ")+"\n") | ||||||
|  |      | ||||||
|  |      | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | main(process.argv.slice(2)) | ||||||
|  | @ -297,7 +297,20 @@ function transformTranslation(obj: any, path: string[] = [], languageWhitelist : | ||||||
|                 } |                 } | ||||||
|                 value = nv; |                 value = nv; | ||||||
|             } |             } | ||||||
|             values += `${Utils.Times((_) => "  ", path.length + 1)}get ${key}() { return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}") },
 |              | ||||||
|  | 
 | ||||||
|  |             if(value["en"] === undefined){ | ||||||
|  |                 throw `At ${path.join(".")}: Missing 'en' translation at path ${path.join(".")}.${key}\n\tThe translations in other languages are ${JSON.stringify(value)}` | ||||||
|  |             } | ||||||
|  |             const subParts : string[] = value["en"].match(/{[^}]*}/g) | ||||||
|  |             let expr = `return new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")` | ||||||
|  |             if(subParts !== null){ | ||||||
|  |                 // convert '{to_substitute}' into 'to_substitute'
 | ||||||
|  |                 const types = Utils.Dedup( subParts.map(tp => tp.substring(1, tp.length - 1))) | ||||||
|  |                 expr = `return new TypedTranslation<{ ${types.join(", ")} }>(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")` | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             values += `${Utils.Times((_) => "  ", path.length + 1)}get ${key}() { ${expr} },
 | ||||||
| ` | ` | ||||||
|         } else { |         } else { | ||||||
|             values += (Utils.Times((_) => "  ", path.length + 1)) + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) + ",\n" |             values += (Utils.Times((_) => "  ", path.length + 1)) + key + ": " + transformTranslation(value, [...path, key], languageWhitelist) + ",\n" | ||||||
|  | @ -340,7 +353,7 @@ function genTranslations() { | ||||||
|     const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8")) |     const translations = JSON.parse(fs.readFileSync("./assets/generated/translations.json", "utf-8")) | ||||||
|     const transformed =  transformTranslation(translations); |     const transformed =  transformTranslation(translations); | ||||||
| 
 | 
 | ||||||
|     let module = `import {Translation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`; |     let module = `import {Translation, TypedTranslation} from "../../UI/i18n/Translation"\n\nexport default class CompiledTranslations {\n\n`; | ||||||
|     module += " public static t = " + transformed; |     module += " public static t = " + transformed; | ||||||
|     module += "\n    }" |     module += "\n    }" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										347
									
								
								test/Logic/Tags/OptimizeTags.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								test/Logic/Tags/OptimizeTags.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,347 @@ | ||||||
|  | import {describe} from 'mocha' | ||||||
|  | import {expect} from 'chai' | ||||||
|  | import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; | ||||||
|  | import {And} from "../../../Logic/Tags/And"; | ||||||
|  | import {Tag} from "../../../Logic/Tags/Tag"; | ||||||
|  | import {TagUtils} from "../../../Logic/Tags/TagUtils"; | ||||||
|  | import {Or} from "../../../Logic/Tags/Or"; | ||||||
|  | import {RegexTag} from "../../../Logic/Tags/RegexTag"; | ||||||
|  | 
 | ||||||
|  | describe("Tag optimalization", () => { | ||||||
|  | 
 | ||||||
|  |     describe("And", () => { | ||||||
|  |         it("with condition and nested and should be flattened", () => { | ||||||
|  |             const t = new And( | ||||||
|  |                 [ | ||||||
|  |                     new And([ | ||||||
|  |                         new Tag("x", "y") | ||||||
|  |                     ]), | ||||||
|  |                     new Tag("a", "b") | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq(`a=b&x=y`) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should be 'true' if no conditions are given", () => { | ||||||
|  |             const t = new And( | ||||||
|  |                 [] | ||||||
|  |             ) | ||||||
|  |             const opt = t.optimize() | ||||||
|  |             expect(opt).eq(true) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and common property should be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             const t = new And([ | ||||||
|  |                 new Tag("foo", "bar"), | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y"), | ||||||
|  |                     new Tag("a", "b") | ||||||
|  |                 ]), | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y"), | ||||||
|  |                     new Tag("c", "d") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and common regextag should be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             const t = new And([ | ||||||
|  |                 new Tag("foo", "bar"), | ||||||
|  |                 new Or([ | ||||||
|  |                     new RegexTag("x", "y"), | ||||||
|  |                     new RegexTag("a", "b") | ||||||
|  |                 ]), | ||||||
|  |                 new Or([ | ||||||
|  |                     new RegexTag("x", "y"), | ||||||
|  |                     new RegexTag("c", "d") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& ( (a=b&c=d) |x=y)") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("with nested ors and inverted regextags should _not_ be extracted", () => { | ||||||
|  | 
 | ||||||
|  |             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 | ||||||
|  |             const t = new And([ | ||||||
|  |                 new Tag("foo", "bar"), | ||||||
|  |                 new Or([ | ||||||
|  |                     new RegexTag("x", "y"), | ||||||
|  |                     new RegexTag("a", "b") | ||||||
|  |                 ]), | ||||||
|  |                 new Or([ | ||||||
|  |                     new RegexTag("x", "y", true), | ||||||
|  |                     new RegexTag("c", "d") | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar& (a=b|x=y) & (c=d|x!=y)") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should move regextag to the end", () => { | ||||||
|  |             const t = new And([ | ||||||
|  |                 new RegexTag("x", "y"), | ||||||
|  |                 new Tag("a", "b") | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("a=b&x=y") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should sort tags by their popularity (least popular first)", () => { | ||||||
|  |             const t = new And([ | ||||||
|  |                 new Tag("bicycle", "yes"), | ||||||
|  |                 new Tag("amenity", "binoculars") | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should optimize nested ORs", () => { | ||||||
|  |             const filter = TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     "X=Y", "FOO=BAR", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["X=Y", "FOO=BAR"] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle=yes" | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }) | ||||||
|  |             // (X=Y | FOO=BAR | (bicycle=yes & (X=Y | FOO=BAR)) )
 | ||||||
|  |             // This is equivalent to (X=Y | FOO=BAR)
 | ||||||
|  |             const opt = filter.optimize() | ||||||
|  |             console.log(opt) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should optimize an advanced, real world case", () => { | ||||||
|  |             const filter = TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle=yes" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": ["amenity=charging_station", "disused:amenity=charging_station", "planned:amenity=charging_station", "construction:amenity=charging_station"] | ||||||
|  |                             }, | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     "amenity=toilets", | ||||||
|  |                     "amenity=bench", | ||||||
|  |                     "leisure=picnic_table", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "tower:type=observation" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "amenity=bicycle_repair_station" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             { | ||||||
|  |                                 "or": [ | ||||||
|  |                                     "amenity=bicycle_rental", | ||||||
|  |                                     "bicycle_rental~*", | ||||||
|  |                                     "service:bicycle:rental=yes", | ||||||
|  |                                     "rental~.*bicycle.*" | ||||||
|  |                                 ] | ||||||
|  |                             }, | ||||||
|  |                             "bicycle_rental!=docking_station" | ||||||
|  |                         ] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "leisure=playground", | ||||||
|  |                             "playground!=forest" | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }); | ||||||
|  |             const opt = <TagsFilter>filter.optimize() | ||||||
|  |             const expected = ["amenity=charging_station", | ||||||
|  |                 "amenity=toilets", | ||||||
|  |                 "amenity=bench", | ||||||
|  |                 "amenity=bicycle_repair_station", | ||||||
|  |                 "construction:amenity=charging_station", | ||||||
|  |                 "disused:amenity=charging_station", | ||||||
|  |                 "leisure=picnic_table", | ||||||
|  |                 "planned:amenity=charging_station", | ||||||
|  |                 "tower:type=observation", | ||||||
|  |                 "(amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!=docking_station", | ||||||
|  |                 "leisure=playground&playground!=forest"] | ||||||
|  | 
 | ||||||
|  |             expect((<Or>opt).or.map(f => TagUtils.toString(f))).deep.eq( | ||||||
|  |                 expected | ||||||
|  |             ) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should detect conflicting tags", () => { | ||||||
|  |             const q = new And([new Tag("key", "value"), new RegexTag("key", "value", true)]) | ||||||
|  |             expect(q.optimize()).eq(false) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should detect conflicting tags with a regex", () => { | ||||||
|  |             const q = new And([new Tag("key", "value"), new RegexTag("key", /value/, true)]) | ||||||
|  |             expect(q.optimize()).eq(false) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     describe("Or", () => { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         it("with nested And which has a common property should be dropped", () => { | ||||||
|  | 
 | ||||||
|  |             const t = new Or([ | ||||||
|  |                 new Tag("foo", "bar"), | ||||||
|  |                 new And([ | ||||||
|  |                     new Tag("foo", "bar"), | ||||||
|  |                     new Tag("x", "y"), | ||||||
|  |                 ]) | ||||||
|  |             ]) | ||||||
|  |             const opt = <TagsFilter>t.optimize() | ||||||
|  |             expect(TagUtils.toString(opt)).eq("foo=bar") | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should flatten nested ors", () => { | ||||||
|  |             const t = new Or([ | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y") | ||||||
|  |                 ]) | ||||||
|  |             ]).optimize() | ||||||
|  |             expect(t).deep.eq(new Tag("x", "y")) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         it("should flatten nested ors", () => { | ||||||
|  |             const t = new Or([ | ||||||
|  |                 new Tag("a", "b"), | ||||||
|  |                 new Or([ | ||||||
|  |                     new Tag("x", "y") | ||||||
|  |                 ]) | ||||||
|  |             ]).optimize() | ||||||
|  |             expect(t).deep.eq(new Or([new Tag("a", "b"), new Tag("x", "y")])) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     it("should not generate a conflict for climbing tags", () => { | ||||||
|  |         const club_tags = TagUtils.Tag( | ||||||
|  |             { | ||||||
|  |                 "or": [ | ||||||
|  |                     "club=climbing", | ||||||
|  |                     { | ||||||
|  |                         "and": [ | ||||||
|  |                             "sport=climbing", | ||||||
|  |                             { | ||||||
|  |                                 "or": [ | ||||||
|  |                                     "office~*", | ||||||
|  |                                     "club~*" | ||||||
|  |                                 ] | ||||||
|  |                             } | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }) | ||||||
|  |         const gym_tags = TagUtils.Tag({ | ||||||
|  |             "and": [ | ||||||
|  |                 "sport=climbing", | ||||||
|  |                 "leisure=sports_centre" | ||||||
|  |             ] | ||||||
|  |         }) | ||||||
|  |         const other_climbing = TagUtils.Tag({ | ||||||
|  |             "and": [ | ||||||
|  |                 "sport=climbing", | ||||||
|  |                 "climbing!~route", | ||||||
|  |                 "leisure!~sports_centre", | ||||||
|  |                 "climbing!=route_top", | ||||||
|  |                 "climbing!=route_bottom" | ||||||
|  |             ] | ||||||
|  |         }) | ||||||
|  |         const together = new Or([club_tags, gym_tags, other_climbing]) | ||||||
|  |         const opt = together.optimize() | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          club=climbing | (sport=climbing&(office~* | club~*)) | ||||||
|  |          OR | ||||||
|  |          sport=climbing & leisure=sports_centre | ||||||
|  |          OR | ||||||
|  |          sport=climbing & climbing!~route & leisure!~sports_centre | ||||||
|  |         */ | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          > When the first OR is written out, this becomes | ||||||
|  |          club=climbing  | ||||||
|  |          OR  | ||||||
|  |          (sport=climbing&(office~* | club~*)) | ||||||
|  |          OR | ||||||
|  |          (sport=climbing & leisure=sports_centre) | ||||||
|  |          OR | ||||||
|  |          (sport=climbing & climbing!~route & leisure!~sports_centre & ...) | ||||||
|  |          */ | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |          > We can join the 'sport=climbing' in the last 3 phrases | ||||||
|  |          club=climbing  | ||||||
|  |          OR  | ||||||
|  |          (sport=climbing AND  | ||||||
|  |              (office~* | club~*)) | ||||||
|  |              OR | ||||||
|  |              (leisure=sports_centre) | ||||||
|  |              OR | ||||||
|  |              (climbing!~route & leisure!~sports_centre & ...) | ||||||
|  |          ) | ||||||
|  |          */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         expect(opt).deep.eq( | ||||||
|  |             TagUtils.Tag({ | ||||||
|  |                 or: [ | ||||||
|  |                     "club=climbing", | ||||||
|  |                     { | ||||||
|  |                         and: ["sport=climbing", | ||||||
|  |                             {or: ["club~*", "office~*"]}] | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         and: ["sport=climbing", | ||||||
|  |                             { | ||||||
|  |                                 or: [ | ||||||
|  |                                     "leisure=sports_centre", | ||||||
|  |                                     { | ||||||
|  |                                         and: [ | ||||||
|  |                                             "climbing!~route", | ||||||
|  |                                             "climbing!=route_top", | ||||||
|  |                                             "climbing!=route_bottom", | ||||||
|  |                                             "leisure!~sports_centre" | ||||||
|  |                                         ] | ||||||
|  |                                     } | ||||||
|  |                                 ] | ||||||
|  |                             }] | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 ], | ||||||
|  | 
 | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -1,150 +0,0 @@ | ||||||
| import {describe} from 'mocha' |  | ||||||
| import {expect} from 'chai' |  | ||||||
| import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; |  | ||||||
| import {And} from "../../../Logic/Tags/And"; |  | ||||||
| import {Tag} from "../../../Logic/Tags/Tag"; |  | ||||||
| import {TagUtils} from "../../../Logic/Tags/TagUtils"; |  | ||||||
| import {Or} from "../../../Logic/Tags/Or"; |  | ||||||
| import {RegexTag} from "../../../Logic/Tags/RegexTag"; |  | ||||||
| 
 |  | ||||||
| describe("Tag optimalization", () => { |  | ||||||
|      |  | ||||||
|     describe("And", () => { |  | ||||||
|         it("with condition and nested and should be flattened", () => { |  | ||||||
|             const t = new And( |  | ||||||
|                 [ |  | ||||||
|                     new And([ |  | ||||||
|                         new Tag("x", "y") |  | ||||||
|                     ]), |  | ||||||
|                     new Tag("a", "b") |  | ||||||
|                 ] |  | ||||||
|             ) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq(`a=b&x=y`) |  | ||||||
|         }) |  | ||||||
|          |  | ||||||
|         it("with nested ors and commons property should be extracted", () => { |  | ||||||
| 
 |  | ||||||
|             // foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
 |  | ||||||
|             const t = new And([ |  | ||||||
|                 new Tag("foo","bar"), |  | ||||||
|                 new Or([ |  | ||||||
|                     new Tag("x", "y"), |  | ||||||
|                     new Tag("a", "b") |  | ||||||
|                 ]), |  | ||||||
|                 new Or([ |  | ||||||
|                     new Tag("x", "y"), |  | ||||||
|                     new Tag("c", "d") |  | ||||||
|                 ]) |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )") |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|         it("should move regextag to the end", () => { |  | ||||||
|             const t = new And([ |  | ||||||
|                 new RegexTag("x","y"), |  | ||||||
|                 new Tag("a","b") |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("a=b&x~^y$") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|          |  | ||||||
|         it("should sort tags by their popularity (least popular first)", () => { |  | ||||||
|             const t = new And([ |  | ||||||
|                 new Tag("bicycle","yes"), |  | ||||||
|                 new Tag("amenity","binoculars") |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|         it("should optimize an advanced, real world case", () => { |  | ||||||
|             const filter = TagUtils.Tag(  {or:   [ |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"] |  | ||||||
|                             }, |  | ||||||
|                             "bicycle=yes" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"] |  | ||||||
|                             }, |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     "amenity=toilets", |  | ||||||
|                     "amenity=bench", |  | ||||||
|                     "leisure=picnic_table", |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "tower:type=observation" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "amenity=bicycle_repair_station" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             { |  | ||||||
|                                 "or": [ |  | ||||||
|                                     "amenity=bicycle_rental", |  | ||||||
|                                     "bicycle_rental~*", |  | ||||||
|                                     "service:bicycle:rental=yes", |  | ||||||
|                                     "rental~.*bicycle.*" |  | ||||||
|                                 ] |  | ||||||
|                             }, |  | ||||||
|                             "bicycle_rental!=docking_station" |  | ||||||
|                         ] |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "and": [ |  | ||||||
|                             "leisure=playground", |  | ||||||
|                             "playground!=forest" |  | ||||||
|                         ] |  | ||||||
|                     } |  | ||||||
|                 ]}); |  | ||||||
|             const opt = <TagsFilter> filter.optimize() |  | ||||||
|             const expected = "amenity=charging_station|" + |  | ||||||
|                 "amenity=toilets|" + |  | ||||||
|                 "amenity=bench|" + |  | ||||||
|                 "amenity=bicycle_repair_station" + |  | ||||||
|                 "|construction:amenity=charging_station|" + |  | ||||||
|                 "disused:amenity=charging_station|" + |  | ||||||
|                 "leisure=picnic_table|" + |  | ||||||
|                 "planned:amenity=charging_station|" + |  | ||||||
|                 "tower:type=observation| " + |  | ||||||
|                 "( (amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!~^docking_station$) |" + |  | ||||||
|                 " (leisure=playground&playground!~^forest$)" |  | ||||||
|              |  | ||||||
|             expect(TagUtils.toString(opt).replace(/ /g, "")) |  | ||||||
|                 .eq(expected.replace(/ /g, "")) |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
|      |  | ||||||
|     }) |  | ||||||
|      |  | ||||||
|     describe("Or", () => { |  | ||||||
|         it("with nested And which has a common property should be dropped", () => { |  | ||||||
| 
 |  | ||||||
|             const t = new Or([ |  | ||||||
|                 new Tag("foo","bar"), |  | ||||||
|                 new And([ |  | ||||||
|                     new Tag("foo", "bar"), |  | ||||||
|                     new Tag("x", "y"), |  | ||||||
|                 ]) |  | ||||||
|             ]) |  | ||||||
|             const opt =<TagsFilter> t.optimize() |  | ||||||
|             expect(TagUtils.toString(opt)).eq("foo=bar") |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         }) |  | ||||||
| }) |  | ||||||
							
								
								
									
										19
									
								
								test/Models/ThemeConfig/SourceConfig.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								test/Models/ThemeConfig/SourceConfig.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import {describe} from 'mocha' | ||||||
|  | import {expect} from 'chai' | ||||||
|  | import SourceConfig from "../../../Models/ThemeConfig/SourceConfig"; | ||||||
|  | import {TagUtils} from "../../../Logic/Tags/TagUtils"; | ||||||
|  | 
 | ||||||
|  | describe("SourceConfig", () => { | ||||||
|  | 
 | ||||||
|  |     it("should throw an error on conflicting tags", () => { | ||||||
|  |         expect(() => { | ||||||
|  |             new SourceConfig( | ||||||
|  |                 { | ||||||
|  |                     osmTags: TagUtils.Tag({ | ||||||
|  |                         and: ["x=y", "a=b", "x!=y"] | ||||||
|  |                     }) | ||||||
|  |                 }, false | ||||||
|  |             ) | ||||||
|  |         }).to.throw(/tags are conflicting/) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -34,7 +34,7 @@ describe("GenerateCache", () => { | ||||||
|             } |             } | ||||||
|             mkdirSync("/tmp/np-cache") |             mkdirSync("/tmp/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!~%22%5E98%24%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!~%22%5Epermissive%24%22%5D%5B%22access%22!~%22%5Eprivate%24%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", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue