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();
 | 
				
			||||||
| 
						 | 
					@ -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));
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        this.preferences.addCallbackAndRun(_ => {
 | 
				
			||||||
            updateData(Number(length.data));
 | 
					            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)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,28 +44,33 @@ 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);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,13 @@ export class And extends TagsFilter {
 | 
				
			||||||
        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 = [];
 | 
				
			||||||
        for (const or of choices) {
 | 
					        for (const or of choices) {
 | 
				
			||||||
| 
						 | 
					@ -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){
 | 
				
			||||||
| 
						 | 
					@ -174,26 +248,55 @@ export class And extends TagsFilter {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        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) {
 | 
				
			||||||
 | 
					                    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
 | 
					                            return false
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (cleaned instanceof Or) {
 | 
				
			||||||
 | 
					                            containedOr = cleaned
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
            return true;
 | 
					                        // 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
 | 
				
			||||||
 | 
					            } 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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										154
									
								
								Logic/Tags/Or.ts
									
										
									
									
									
								
							
							
						
						
									
										154
									
								
								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
 | 
				
			||||||
 | 
					            } while(dirty)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 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) {
 | 
					 | 
				
			||||||
            other.isEquivalent(this);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return other.matchesProperties({[this.key]: this.value});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,6 @@ 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'>`,
 | 
				
			||||||
| 
						 | 
					@ -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...
 | 
				
			||||||
 | 
					                if(!state.featureSwitchFilter.data){
 | 
				
			||||||
 | 
					                    // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (layer.layerDef.name === undefined) {
 | 
					                if (layer.layerDef.name === undefined) {
 | 
				
			||||||
                // this is a parlty hidden layer
 | 
					                    // this layer can never be toggled on in any case, so we skip the presets
 | 
				
			||||||
                    continue;
 | 
					                    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>) {
 | 
				
			||||||
| 
						 | 
					@ -39,16 +41,33 @@ class TranslatorsPanelContent extends Combine {
 | 
				
			||||||
            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[] {
 | 
				
			||||||
 | 
					            // e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description
 | 
				
			||||||
 | 
					            const missingKeys = Utils.NoNull(untranslated.get(language) ?? [])
 | 
				
			||||||
                .filter(ctx => ctx.indexOf(":") >= 0)
 | 
					                .filter(ctx => ctx.indexOf(":") >= 0)
 | 
				
			||||||
                .map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
 | 
					                .map(ctx => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
 | 
				
			||||||
            .map(context => new Link(context, LinkToWeblate.hrefToWeblate(ln, context), true))
 | 
					
 | 
				
			||||||
 | 
					            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([
 | 
				
			||||||
| 
						 | 
					@ -65,13 +84,16 @@ class TranslatorsPanelContent extends Combine {
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            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")
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            }))
 | 
					            }))
 | 
				
			||||||
| 
						 | 
					@ -101,6 +123,7 @@ export default class TranslatorsPanel extends Toggle {
 | 
				
			||||||
        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) {
 | 
				
			||||||
| 
						 | 
					@ -111,10 +134,8 @@ export default class TranslatorsPanel extends Toggle {
 | 
				
			||||||
                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 {
 | 
				
			||||||
| 
						 | 
					@ -165,24 +162,6 @@ export class Translation extends BaseUIElement {
 | 
				
			||||||
        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í.",
 | 
				
			||||||
        "locked": "Necessites almenys {importHelperUnlock} per utilitzar l'ajudant d'importació",
 | 
					 | 
				
			||||||
            "loggedInWith": "Actualment has entrat com a <b>{name}</b> i has fet {csCount} conjunts de canvis",
 | 
					            "loggedInWith": "Actualment has entrat com a <b>{name}</b> i has fet {csCount} conjunts de canvis",
 | 
				
			||||||
            "loginIsCorrect": "<b>{name}</b> és el compte correcte per crear les notes d'importació.",
 | 
					            "loginIsCorrect": "<b>{name}</b> és el compte correcte per crear les notes d'importació.",
 | 
				
			||||||
            "loginRequired": "Has d'entrar per continuar",
 | 
					            "loginRequired": "Has d'entrar per continuar",
 | 
				
			||||||
 | 
					            "userAccountTitle": "Seleccionar compte d'usuari"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "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,9 +267,13 @@
 | 
				
			||||||
        "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",
 | 
				
			||||||
        "featuredThemeTitle": "Kiemelt ezen a héten",
 | 
					        "featuredThemeTitle": "Kiemelt ezen a héten",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.",
 | 
				
			||||||
        "locked": "Je hebt minstens {importHelperUnlock} changesets nodig om de import helper te gebruiken",
 | 
					 | 
				
			||||||
            "loggedInWith": "Je bent momenteel aangemeld als <b>{name}</b> and maakte {csCount} eerdere wijzigingen",
 | 
					            "loggedInWith": "Je bent momenteel aangemeld als <b>{name}</b> and maakte {csCount} eerdere wijzigingen",
 | 
				
			||||||
            "loginIsCorrect": "<b>{name}</b> is de correcte account om de import-nota's mee te maken.",
 | 
					            "loginIsCorrect": "<b>{name}</b> is de correcte account om de import-nota's mee te maken.",
 | 
				
			||||||
            "loginRequired": "Je moet ingelogd zijn om verder te gaan",
 | 
					            "loginRequired": "Je moet ingelogd zijn om verder te gaan",
 | 
				
			||||||
 | 
					            "userAccountTitle": "Selecteer een account"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "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,25 +35,6 @@
 | 
				
			||||||
        "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ąć",
 | 
					        "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ą",
 | 
				
			||||||
| 
						 | 
					@ -141,99 +122,6 @@
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "welcomeBack": "Jesteś zalogowany, witaj z powrotem!"
 | 
					        "welcomeBack": "Jesteś zalogowany, witaj z powrotem!"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
        "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ą",
 | 
					 | 
				
			||||||
        "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": {
 | 
					 | 
				
			||||||
            "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!"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "image": {
 | 
					    "image": {
 | 
				
			||||||
        "addPicture": "Dodaj zdjęcie",
 | 
					        "addPicture": "Dodaj zdjęcie",
 | 
				
			||||||
        "ccb": "na licencji CC-BY",
 | 
					        "ccb": "na licencji CC-BY",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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