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
|
||||
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
|
||||
------------
|
||||
|
||||
|
@ -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
|
||||
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)
|
||||
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
|
||||
------------
|
||||
|
||||
|
@ -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
|
||||
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.
|
||||
|
||||
### Using layers as filters
|
||||
|
||||
Using layers as filters - this doesn't work!
|
||||
|
||||
Use the `filter`-functionality instead.
|
||||
|
||||
### 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
|
||||
|
|
|
@ -22,9 +22,8 @@
|
|||
"#": "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": [
|
||||
"bench",
|
||||
{
|
||||
"id": "a singular nound describing the feature, in english",
|
||||
"id": "a singular noun describing the feature, in english",
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"#": "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 FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {BBox} from "../BBox";
|
||||
import Constants from "../../Models/Constants";
|
||||
|
||||
export interface GeoLocationPointProperties {
|
||||
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
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
* @private
|
||||
*/
|
||||
private _lastUserRequest: Date;
|
||||
private _lastUserRequest: UIEventSource<Date>;
|
||||
|
||||
/**
|
||||
* 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 leafletMap = state.leafletMap
|
||||
const initedAt = new Date()
|
||||
let autozoomDone = false;
|
||||
const hasLocation = currentGPSLocation.map(
|
||||
(location) => location !== undefined
|
||||
);
|
||||
|
@ -97,13 +97,28 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
|
||||
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(_ => {
|
||||
window.setTimeout(() => {
|
||||
if (lastClickWithinThreeSecs.data) {
|
||||
if (lastClickWithinThreeSecs.data || willFocus.data) {
|
||||
lastClick.ping()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
super(
|
||||
hasLocation.map(
|
||||
(hasLocationData) => {
|
||||
|
@ -116,7 +131,8 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
if (!hasLocationData) {
|
||||
// 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;")
|
||||
return icon;
|
||||
}
|
||||
|
@ -130,7 +146,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
// We have a location, so we show a dot in the center
|
||||
return Svg.location_svg();
|
||||
},
|
||||
[isActive, isLocked, permission, lastClickWithinThreeSecs]
|
||||
[isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus]
|
||||
)
|
||||
);
|
||||
this.SetClass("mapcontrol")
|
||||
|
@ -142,6 +158,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
this._leafletMap = leafletMap;
|
||||
this._layoutToUse = state.layoutToUse;
|
||||
this._hasLocation = hasLocation;
|
||||
this._lastUserRequest = lastClick
|
||||
const self = this;
|
||||
|
||||
const currentPointer = this._isActive.map(
|
||||
|
@ -183,7 +200,6 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
self.init(true, true);
|
||||
});
|
||||
|
||||
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
|
||||
|
||||
const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined
|
||||
this.init(false, doAutoZoomToLocation);
|
||||
|
@ -221,8 +237,12 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
self.currentLocation?.features?.setData([{feature, freshness: new Date()}])
|
||||
|
||||
const timeSinceRequest =
|
||||
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||
if (timeSinceRequest < 30) {
|
||||
(new Date().getTime() - (self._lastUserRequest.data?.getTime() ?? 0)) / 1000;
|
||||
|
||||
if (willFocus.data) {
|
||||
console.log("Zooming to user location: willFocus is set")
|
||||
willFocus.setData(false)
|
||||
autozoomDone = true;
|
||||
self.MoveToCurrentLocation(16);
|
||||
} else if (self._isLocked.data) {
|
||||
self.MoveToCurrentLocation();
|
||||
|
@ -240,7 +260,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
return;
|
||||
}
|
||||
|
||||
if(typeof navigator === "undefined"){
|
||||
if (typeof navigator === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -321,7 +341,7 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
*/
|
||||
private MoveToCurrentLocation(targetZoom?: number) {
|
||||
const location = this._currentGPSLocation.data;
|
||||
this._lastUserRequest = undefined;
|
||||
this._lastUserRequest.setData(undefined);
|
||||
|
||||
if (
|
||||
this._currentGPSLocation.data.latitude === 0 &&
|
||||
|
@ -341,14 +361,9 @@ export default class GeoLocationHandler extends VariableUiElement {
|
|||
}
|
||||
}
|
||||
if (!inRange) {
|
||||
console.log(
|
||||
"Not zooming to GPS location: out of bounds",
|
||||
b,
|
||||
location
|
||||
);
|
||||
console.log("Not zooming to GPS location: out of bounds", b, location);
|
||||
} else {
|
||||
const currentZoom = this._leafletMap.data.getZoom()
|
||||
|
||||
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) {
|
||||
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") {
|
||||
self._previousLocationGrant.setData("");
|
||||
self._isActive.setData(false)
|
||||
|
|
|
@ -76,9 +76,9 @@ export class OsmPreferences {
|
|||
|
||||
|
||||
function updateData(l: number) {
|
||||
if (l === undefined) {
|
||||
source.setData(undefined);
|
||||
return;
|
||||
if(Object.keys(self.preferences.data).length === 0){
|
||||
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
||||
return
|
||||
}
|
||||
const prefsCount = Number(l);
|
||||
if (prefsCount > 100) {
|
||||
|
@ -86,7 +86,11 @@ export class OsmPreferences {
|
|||
}
|
||||
let str = "";
|
||||
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);
|
||||
|
@ -95,7 +99,9 @@ export class OsmPreferences {
|
|||
length.addCallback(l => {
|
||||
updateData(Number(l));
|
||||
});
|
||||
this.preferences.addCallbackAndRun(_ => {
|
||||
updateData(Number(length.data));
|
||||
})
|
||||
|
||||
return source;
|
||||
}
|
||||
|
@ -127,7 +133,8 @@ export class OsmPreferences {
|
|||
public ClearPreferences() {
|
||||
let isRunning = false;
|
||||
const self = this;
|
||||
this.preferences.addCallbackAndRun(prefs => {
|
||||
this.preferences.addCallback(prefs => {
|
||||
console.log("Cleaning preferences...")
|
||||
if (Object.keys(prefs).length == 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -135,19 +142,17 @@ export class OsmPreferences {
|
|||
return
|
||||
}
|
||||
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 prefix of prefixes) {
|
||||
if (key.startsWith(prefix)) {
|
||||
const matches = prefixes.some(prefix => key.startsWith(prefix))
|
||||
if (matches) {
|
||||
console.log("Clearing ", key)
|
||||
self.GetPreference(key, "").setData("")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
isRunning = false;
|
||||
return true;
|
||||
|
||||
return;
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -173,7 +178,6 @@ export class OsmPreferences {
|
|||
// For differing values, the server overrides local changes
|
||||
self.preferenceSources.forEach((preference, 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){
|
||||
// OSM doesn't know this value yet
|
||||
self.UploadPreference(key, preference.data)
|
||||
|
|
|
@ -44,28 +44,33 @@ export default class ElementsState extends FeatureSwitchState {
|
|||
constructor(layoutToUse: LayoutConfig) {
|
||||
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
|
||||
const zoom = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(
|
||||
"z",
|
||||
"" + (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"))
|
||||
);
|
||||
const zoom = localStorageSynced("z",(layoutToUse?.startZoom ?? 1),"The initial/current zoom level")
|
||||
const lat = localStorageSynced("lat",(layoutToUse?.startLat ?? 0),"The initial/current latitude")
|
||||
const lon = localStorageSynced("lon",(layoutToUse?.startLon ?? 0),"The initial/current longitude of the app")
|
||||
|
||||
|
||||
this.locationControl.setData({
|
||||
zoom: Utils.asFloat(zoom.data),
|
||||
|
@ -73,7 +78,7 @@ export default class ElementsState extends FeatureSwitchState {
|
|||
lon: Utils.asFloat(lon.data),
|
||||
})
|
||||
this.locationControl.addCallback((latlonz) => {
|
||||
// Sync th location controls
|
||||
// Sync the location controls
|
||||
zoom.setData(latlonz.zoom);
|
||||
lat.setData(latlonz.lat);
|
||||
lon.setData(latlonz.lon);
|
||||
|
|
|
@ -9,6 +9,13 @@ export class And extends TagsFilter {
|
|||
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[] {
|
||||
const values = [];
|
||||
for (const or of choices) {
|
||||
|
@ -45,7 +52,7 @@ export class And extends TagsFilter {
|
|||
* import {RegexTag} from "./RegexTag";
|
||||
*
|
||||
* 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[] {
|
||||
let allChoices: string[] = null;
|
||||
|
@ -87,17 +94,17 @@ export class And extends TagsFilter {
|
|||
* ])
|
||||
* const t1 = new And([new Tag("valves", "A")])
|
||||
* const t2 = new And([new Tag("valves", "B")])
|
||||
* t0.isEquivalent(t0) // => true
|
||||
* t1.isEquivalent(t1) // => true
|
||||
* t2.isEquivalent(t2) // => true
|
||||
* t0.isEquivalent(t1) // => false
|
||||
* t0.isEquivalent(t2) // => false
|
||||
* t1.isEquivalent(t0) // => false
|
||||
* t1.isEquivalent(t2) // => false
|
||||
* t2.isEquivalent(t0) // => false
|
||||
* t2.isEquivalent(t1) // => false
|
||||
* t0.shadows(t0) // => true
|
||||
* t1.shadows(t1) // => true
|
||||
* t2.shadows(t2) // => true
|
||||
* t0.shadows(t1) // => false
|
||||
* t0.shadows(t2) // => false
|
||||
* t1.shadows(t0) // => false
|
||||
* t1.shadows(t2) // => false
|
||||
* t2.shadows(t0) // => false
|
||||
* t2.shadows(t1) // => false
|
||||
*/
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (!(other instanceof And)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -105,7 +112,7 @@ export class And extends TagsFilter {
|
|||
for (const selfTag of this.and) {
|
||||
let matchFound = false;
|
||||
for (const otherTag of other.and) {
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
if (matchFound) {
|
||||
break;
|
||||
}
|
||||
|
@ -118,7 +125,7 @@ export class And extends TagsFilter {
|
|||
for (const otherTag of other.and) {
|
||||
let matchFound = false;
|
||||
for (const selfTag of this.and) {
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
if (matchFound) {
|
||||
break;
|
||||
}
|
||||
|
@ -148,23 +155,90 @@ export class And extends TagsFilter {
|
|||
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 {
|
||||
if(this.and.length === 0){
|
||||
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[] = []
|
||||
|
||||
let containedOrs : Or[] = []
|
||||
for (const tf of optimized) {
|
||||
if(tf === false){
|
||||
return false
|
||||
}
|
||||
if(tf === true){
|
||||
continue
|
||||
}
|
||||
|
||||
if(tf instanceof And){
|
||||
newAnds.push(...tf.and)
|
||||
}else if(tf instanceof Or){
|
||||
|
@ -174,26 +248,55 @@ export class And extends TagsFilter {
|
|||
}
|
||||
}
|
||||
|
||||
containedOrs = containedOrs.filter(ca => {
|
||||
for (const element of ca.or) {
|
||||
if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){
|
||||
// At least one part of the 'OR' is matched by the outer or, so this means that this OR isn't needed at all
|
||||
// XY & (XY | AB) === XY
|
||||
{
|
||||
let dirty = false;
|
||||
do {
|
||||
const cleanedContainedOrs : Or[] = []
|
||||
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
|
||||
}
|
||||
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
|
||||
if(containedOrs.length === 1){
|
||||
newAnds.push(containedOrs[0])
|
||||
}
|
||||
if(containedOrs.length > 1){
|
||||
}else if(containedOrs.length > 1){
|
||||
let commonValues : TagsFilter [] = containedOrs[0].or
|
||||
for (let i = 1; i < containedOrs.length && commonValues.length > 0; 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){
|
||||
newAnds.push(...containedOrs)
|
||||
|
@ -201,19 +304,11 @@ export class And extends TagsFilter {
|
|||
const newOrs: TagsFilter[] = []
|
||||
for (const containedOr of containedOrs) {
|
||||
const elements = containedOr.or
|
||||
.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate)))
|
||||
const or = new Or(elements).optimize()
|
||||
if(or === true){
|
||||
// neutral element
|
||||
continue
|
||||
}
|
||||
if(or === false){
|
||||
return false
|
||||
}
|
||||
newOrs.push(or)
|
||||
.filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
|
||||
newOrs.push(Or.construct(elements))
|
||||
}
|
||||
|
||||
commonValues.push(new And(newOrs))
|
||||
commonValues.push(And.construct(newOrs))
|
||||
const result = new Or(commonValues).optimize()
|
||||
if(result === false){
|
||||
return false
|
||||
|
@ -224,16 +319,22 @@ export class And extends TagsFilter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newAnds.length === 1){
|
||||
return newAnds[0]
|
||||
if(newAnds.length === 0){
|
||||
return true
|
||||
}
|
||||
|
||||
if(TagUtils.ContainsOppositeTags(newAnds)){
|
||||
return false
|
||||
}
|
||||
|
||||
TagUtils.sortFilters(newAnds, true)
|
||||
|
||||
return new And(newAnds)
|
||||
return And.construct(newAnds)
|
||||
}
|
||||
|
||||
isNegative(): boolean {
|
||||
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"
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
public static construct(or: TagsFilter[]): TagsFilter{
|
||||
if(or.length === 1){
|
||||
return or[0]
|
||||
}
|
||||
return new Or(or)
|
||||
}
|
||||
|
||||
|
||||
matchesProperties(properties: any): boolean {
|
||||
for (const tagsFilter of this.or) {
|
||||
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 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
|
||||
* 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;
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (other instanceof Or) {
|
||||
|
||||
for (const selfTag of this.or) {
|
||||
let matchFound = false;
|
||||
for (let i = 0; i < other.or.length && !matchFound; i++) {
|
||||
let otherTag = other.or[i];
|
||||
matchFound = selfTag.isEquivalent(otherTag);
|
||||
matchFound = selfTag.shadows(otherTag);
|
||||
}
|
||||
if (!matchFound) {
|
||||
return false;
|
||||
|
@ -85,45 +93,127 @@ export class Or extends TagsFilter {
|
|||
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 {
|
||||
|
||||
if(this.or.length === 0){
|
||||
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[] = []
|
||||
|
||||
let containedAnds : And[] = []
|
||||
for (const tf of optimized) {
|
||||
if(tf === true){
|
||||
return true
|
||||
}
|
||||
if(tf === false){
|
||||
continue
|
||||
}
|
||||
|
||||
if(tf instanceof Or){
|
||||
// expand all the nested ors...
|
||||
newOrs.push(...tf.or)
|
||||
}else if(tf instanceof And){
|
||||
// partition of all the ands
|
||||
containedAnds.push(tf)
|
||||
} else {
|
||||
newOrs.push(tf)
|
||||
}
|
||||
}
|
||||
|
||||
containedAnds = containedAnds.filter(ca => {
|
||||
for (const element of ca.and) {
|
||||
if(optimized.some(opt => typeof opt !== "boolean" && element.isEquivalent(opt) )){
|
||||
// At least one part of the 'AND' is matched by the outer or, so this means that this OR isn't needed at all
|
||||
// XY | (XY & AB) === XY
|
||||
return false
|
||||
{
|
||||
let dirty = false;
|
||||
do {
|
||||
const cleanedContainedANds : And[] = []
|
||||
outer: for (let containedAnd of containedAnds) {
|
||||
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
|
||||
if(containedAnds.length === 1){
|
||||
newOrs.push(containedAnds[0])
|
||||
|
@ -131,40 +221,46 @@ export class Or extends TagsFilter {
|
|||
let commonValues : TagsFilter [] = containedAnds[0].and
|
||||
for (let i = 1; i < containedAnds.length && commonValues.length > 0; 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){
|
||||
newOrs.push(...containedAnds)
|
||||
}else{
|
||||
const newAnds: TagsFilter[] = []
|
||||
for (const containedAnd of containedAnds) {
|
||||
const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.isEquivalent(candidate)))
|
||||
newAnds.push(new And(elements))
|
||||
const elements = containedAnd.and.filter(candidate => !commonValues.some(cv => cv.shadows(candidate)))
|
||||
newAnds.push(And.construct(elements))
|
||||
}
|
||||
|
||||
commonValues.push(new Or(newAnds))
|
||||
commonValues.push(Or.construct(newAnds))
|
||||
const result = new And(commonValues).optimize()
|
||||
if(result === true){
|
||||
return true
|
||||
}else if(result === false){
|
||||
// neutral element: skip
|
||||
}else{
|
||||
newOrs.push(new And(commonValues))
|
||||
newOrs.push(And.construct(commonValues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newOrs.length === 1){
|
||||
return newOrs[0]
|
||||
if(newOrs.length === 0){
|
||||
return false
|
||||
}
|
||||
|
||||
if(TagUtils.ContainsOppositeTags(newOrs)){
|
||||
return true
|
||||
}
|
||||
|
||||
TagUtils.sortFilters(newOrs, false)
|
||||
|
||||
return new Or(newOrs)
|
||||
return Or.construct(newOrs)
|
||||
}
|
||||
|
||||
isNegative(): boolean {
|
||||
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) {
|
||||
super();
|
||||
this.key = key;
|
||||
if (typeof value === "string") {
|
||||
if (value.indexOf("^") < 0 && value.indexOf("$") < 0) {
|
||||
value = "^" + value + "$"
|
||||
}
|
||||
value = new RegExp(value)
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.invert = invert;
|
||||
this.matchesEmpty = RegexTag.doesMatch("", this.value);
|
||||
|
@ -79,14 +72,14 @@ export class RegexTag extends TagsFilter {
|
|||
/**
|
||||
* 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": "other_value"}) // => true
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => false
|
||||
* isNotEmpty.matchesProperties({"other_key": ""}) // => 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": "other_value"}) // => false
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => true
|
||||
|
@ -121,6 +114,9 @@ export class RegexTag extends TagsFilter {
|
|||
* importMatch.matchesProperties({"tags": "amenity=public_bookcase"}) // =>true
|
||||
* importMatch.matchesProperties({"tags": "name=test;amenity=public_bookcase"}) // =>true
|
||||
* 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 {
|
||||
if (typeof this.key === "string") {
|
||||
|
@ -147,17 +143,87 @@ export class RegexTag extends TagsFilter {
|
|||
|
||||
asHumanString() {
|
||||
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)}`
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
throw "A variable with substitution can not be used to query overpass"
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (!(other instanceof SubstitutingTag)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -88,14 +88,23 @@ export class Tag extends TagsFilter {
|
|||
return true;
|
||||
}
|
||||
|
||||
isEquivalent(other: TagsFilter): boolean {
|
||||
if (other instanceof Tag) {
|
||||
return this.key === other.key && this.value === other.value;
|
||||
/**
|
||||
* // should handle advanced regexes
|
||||
* 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[] {
|
||||
|
|
|
@ -200,15 +200,16 @@ export class TagUtils {
|
|||
*
|
||||
* TagUtils.Tag("key=value") // => new Tag("key", "value")
|
||||
* TagUtils.Tag("key=") // => new Tag("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~*") // => new RegexTag("key", /^..*$/)
|
||||
* TagUtils.Tag("key!=value") // => new RegexTag("key", "value", true)
|
||||
* TagUtils.Tag("vending~.*bicycle_tube.*") // => new RegexTag("vending", /^.*bicycle_tube.*$/)
|
||||
* 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("name~[sS]peelbos.*") // => new RegexTag("name", /^[sS]peelbos.*$/)
|
||||
* TagUtils.Tag("survey:date:={_date:now}") // => new SubstitutingTag("survey:date", "{_date:now}")
|
||||
* 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("xyz<5").matchesProperties({xyz: 4}) // => true
|
||||
|
@ -306,7 +307,7 @@ export class TagUtils {
|
|||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
split[1],
|
||||
new RegExp("^"+ split[1]+"$"),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
@ -338,17 +339,6 @@ export class TagUtils {
|
|||
split[1] = "..*"
|
||||
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(
|
||||
split[0],
|
||||
split[1],
|
||||
|
@ -357,15 +347,18 @@ export class TagUtils {
|
|||
}
|
||||
if (tag.indexOf("~") >= 0) {
|
||||
const split = Utils.SplitFirst(tag, "~");
|
||||
let value : string | RegExp = split[1]
|
||||
if (split[1] === "") {
|
||||
throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")"
|
||||
}
|
||||
if (split[1] === "*") {
|
||||
split[1] = "..*"
|
||||
if (value === "*") {
|
||||
value = /^..*$/
|
||||
}else {
|
||||
value = new RegExp("^"+value+"$")
|
||||
}
|
||||
return new RegexTag(
|
||||
split[0],
|
||||
split[1]
|
||||
value
|
||||
);
|
||||
}
|
||||
if (tag.indexOf("=") >= 0) {
|
||||
|
@ -431,4 +424,94 @@ export class TagUtils {
|
|||
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 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;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.18.1";
|
||||
public static vNumber = "0.18.2";
|
||||
|
||||
public static ImgurApiKey = '7070e7167f0a25a'
|
||||
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 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 {
|
||||
if (Utils.runningFromConsole) {
|
||||
|
|
|
@ -127,6 +127,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
idKey: json.source["idKey"]
|
||||
|
||||
},
|
||||
Constants.priviliged_layers.indexOf(this.id) > 0,
|
||||
json.id
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||
import {RegexTag} from "../../Logic/Tags/RegexTag";
|
||||
import {param} from "jquery";
|
||||
|
||||
export default class SourceConfig {
|
||||
|
||||
|
@ -19,7 +20,7 @@ export default class SourceConfig {
|
|||
isOsmCache?: boolean,
|
||||
geojsonSourceLevel?: number,
|
||||
idKey?: string
|
||||
}, context?: string) {
|
||||
}, isSpecialLayer: boolean, context?: string) {
|
||||
|
||||
let defined = 0;
|
||||
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})`
|
||||
}
|
||||
}
|
||||
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.overpassScript = params.overpassScript;
|
||||
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 Translations from "../../UI/i18n/Translations";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
|
@ -22,8 +22,8 @@ export default class TagRenderingConfig {
|
|||
|
||||
public readonly id: string;
|
||||
public readonly group: string;
|
||||
public readonly render?: Translation;
|
||||
public readonly question?: Translation;
|
||||
public readonly render?: TypedTranslation<object>;
|
||||
public readonly question?: TypedTranslation<object>;
|
||||
public readonly condition?: TagsFilter;
|
||||
|
||||
public readonly configuration_warnings: string[] = []
|
||||
|
@ -43,7 +43,7 @@ export default class TagRenderingConfig {
|
|||
public readonly mappings?: {
|
||||
readonly if: TagsFilter,
|
||||
readonly ifnot?: TagsFilter,
|
||||
readonly then: Translation,
|
||||
readonly then: TypedTranslation<object>,
|
||||
readonly icon: string,
|
||||
readonly iconClass: string
|
||||
readonly hideInAnswer: boolean | TagsFilter
|
||||
|
@ -110,12 +110,13 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
const type = json.freeform.type ?? "string"
|
||||
|
||||
let placeholder = Translations.T(json.freeform.placeholder)
|
||||
let placeholder: Translation = Translations.T(json.freeform.placeholder)
|
||||
if (placeholder === undefined) {
|
||||
const typeDescription = Translations.t.validation[type]?.description
|
||||
placeholder = Translations.T(json.freeform.key+" ("+type+")")
|
||||
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 usedFreeformValues = new Set<string>()
|
||||
// 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) {
|
||||
return mapping;
|
||||
}
|
||||
|
@ -404,7 +405,7 @@ export default class TagRenderingConfig {
|
|||
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
|
||||
for (const leftover of leftovers) {
|
||||
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
|
||||
}
|
||||
|
||||
public GetRenderValue(tags: any, defltValue: any = undefined): Translation {
|
||||
public GetRenderValue(tags: any, defltValue: any = undefined): TypedTranslation<any> {
|
||||
return this.GetRenderValueWithImage(tags, defltValue).then
|
||||
}
|
||||
|
||||
|
@ -421,7 +422,7 @@ export default class TagRenderingConfig {
|
|||
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
|
||||
* @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) {
|
||||
for (const mapping of this.mappings) {
|
||||
if (mapping.if === undefined) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Link from "./Link";
|
|||
import Svg from "../../Svg";
|
||||
|
||||
export default class LinkToWeblate extends VariableUiElement {
|
||||
private static URI: any;
|
||||
constructor(context: string, availableTranslations: object) {
|
||||
super( Locale.language.map(ln => {
|
||||
if (Locale.showLinkToWeblate.data === false) {
|
||||
|
@ -36,4 +37,10 @@ export default class LinkToWeblate extends VariableUiElement {
|
|||
const baseUrl = "https://hosted.weblate.org/translate/mapcomplete/"
|
||||
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) {
|
||||
if (clss == undefined) {
|
||||
return
|
||||
return this
|
||||
}
|
||||
const all = clss.split(" ").map(clsName => clsName.trim());
|
||||
let recordedChange = false;
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class AddNewMarker extends Combine {
|
|||
let last = undefined;
|
||||
for (const filteredLayer of filteredLayers) {
|
||||
const layer = filteredLayer.layerDef;
|
||||
if(layer.name === undefined){
|
||||
if(layer.name === undefined && !filteredLayer.isDisplayed.data){
|
||||
continue
|
||||
}
|
||||
for (const preset of filteredLayer.layerDef.presets) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
|||
import Constants from "../../Models/Constants";
|
||||
import ContributorCount from "../../Logic/ContributorCount";
|
||||
import Img from "../Base/Img";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {TypedTranslation} from "../i18n/Translation";
|
||||
import TranslatorsPanel from "./TranslatorsPanel";
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
private static CodeContributors(contributors, translation: Translation): BaseUIElement {
|
||||
private static CodeContributors(contributors, translation: TypedTranslation<{contributors, hiddenCount}>): BaseUIElement {
|
||||
|
||||
const total = contributors.contributors.length;
|
||||
let filtered = [...contributors.contributors]
|
||||
|
|
|
@ -90,8 +90,8 @@ export default class MoreScreen extends Combine {
|
|||
}
|
||||
|
||||
let hash = ""
|
||||
if(layout.definition !== undefined){
|
||||
hash = "#"+btoa(JSON.stringify(layout.definition))
|
||||
if (layout.definition !== undefined) {
|
||||
hash = "#" + btoa(JSON.stringify(layout.definition))
|
||||
}
|
||||
|
||||
const linkText = currentLocation?.map(currentLocation => {
|
||||
|
@ -106,11 +106,10 @@ export default class MoreScreen extends Combine {
|
|||
}) ?? new UIEventSource<string>(`${linkPrefix}`)
|
||||
|
||||
|
||||
|
||||
return new SubtleButton(layout.icon,
|
||||
new Combine([
|
||||
`<dt class='text-lg leading-6 font-medium text-gray-900 group-hover:text-blue-800'>`,
|
||||
new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:"+layout.id+".title" : undefined),
|
||||
new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined),
|
||||
`</dt>`,
|
||||
`<dd class='mt-1 text-base text-gray-500 group-hover:text-blue-900 overflow-ellipsis'>`,
|
||||
new Translation(layout.shortDescription)?.SetClass("subtle") ?? "",
|
||||
|
@ -128,15 +127,13 @@ export default class MoreScreen extends Combine {
|
|||
}
|
||||
|
||||
private static createUnofficialButtonFor(state: UserRelatedState, id: string): BaseUIElement {
|
||||
const allPreferences = state.osmConnection.preferencesHandler.preferences.data;
|
||||
const length = Number(allPreferences[id + "-length"])
|
||||
let str = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
str += allPreferences[id + "-" + i]
|
||||
}
|
||||
if(str === undefined || str === "undefined"){
|
||||
const pref = state.osmConnection.GetLongPreference(id)
|
||||
const str = pref.data
|
||||
if (str === undefined || str === "undefined" || str === "") {
|
||||
pref.setData(null)
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const value: {
|
||||
id: string
|
||||
|
@ -149,7 +146,8 @@ export default class MoreScreen extends Combine {
|
|||
value.isOfficial = false
|
||||
return MoreScreen.createLinkButton(state, value, true)
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
@ -163,16 +161,14 @@ export default class MoreScreen extends Combine {
|
|||
|
||||
for (const key in allPreferences) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
});
|
||||
|
||||
var stableIds = UIEventSource.ListStabilized<string>(currentIds)
|
||||
|
||||
return new VariableUiElement(
|
||||
stableIds.map(ids => {
|
||||
const allThemes: BaseUIElement[] = []
|
||||
|
@ -182,12 +178,11 @@ export default class MoreScreen extends Combine {
|
|||
allThemes.push(link.SetClass(buttonClass))
|
||||
}
|
||||
}
|
||||
|
||||
if (allThemes.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return new Combine([
|
||||
Translations.t.general.customThemeIntro.Clone(),
|
||||
Translations.t.general.customThemeIntro,
|
||||
new Combine(allThemes).SetClass(themeListClasses)
|
||||
]);
|
||||
}));
|
||||
|
|
|
@ -208,15 +208,20 @@ export default class SimpleAddUI extends Toggle {
|
|||
const allButtons = [];
|
||||
for (const layer of state.filteredLayers.data) {
|
||||
|
||||
if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) {
|
||||
// The layer is not displayed and we cannot enable the layer control -> we skip
|
||||
if (layer.isDisplayed.data === false) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
|
|
|
@ -14,7 +14,9 @@ import Title from "../Base/Title";
|
|||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {SubtleButton} from "../Base/SubtleButton";
|
||||
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 {
|
||||
constructor(layout: LayoutConfig, isTranslator: UIEventSource<boolean>) {
|
||||
|
@ -24,7 +26,7 @@ class TranslatorsPanelContent extends Combine {
|
|||
|
||||
const seed = t.completeness
|
||||
for (const ln of Array.from(completeness.keys())) {
|
||||
if(ln === "*"){
|
||||
if (ln === "*") {
|
||||
continue
|
||||
}
|
||||
if (seed.translations[ln] === undefined) {
|
||||
|
@ -35,20 +37,37 @@ class TranslatorsPanelContent extends Combine {
|
|||
const completenessTr = {}
|
||||
const completenessPercentage = {}
|
||||
seed.SupportedLanguages().forEach(ln => {
|
||||
completenessTr[ln] = ""+(completeness.get(ln) ?? 0)
|
||||
completenessPercentage[ln] = ""+Math.round(100 * (completeness.get(ln) ?? 0) / total)
|
||||
completenessTr[ln] = "" + (completeness.get(ln) ?? 0)
|
||||
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)
|
||||
.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}",
|
||||
const translated = seed.Subs({total, theme: layout.title,
|
||||
const translated = seed.Subs({
|
||||
total, theme: layout.title,
|
||||
percentage: new Translation(completenessPercentage),
|
||||
translated: new Translation(completenessTr)
|
||||
translated: new Translation(completenessTr),
|
||||
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng)
|
||||
})
|
||||
|
||||
super([
|
||||
|
@ -65,13 +84,16 @@ class TranslatorsPanelContent extends Combine {
|
|||
}),
|
||||
|
||||
new VariableUiElement(Locale.language.map(ln => {
|
||||
|
||||
const missing = missingTranslationsFor(ln)
|
||||
if (missing.length === 0) {
|
||||
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(
|
||||
new Title(Translations.t.translations.missing.Subs({count: missing.length})),
|
||||
new Title(title),
|
||||
new Combine(missing).SetClass("flex flex-col")
|
||||
)
|
||||
}))
|
||||
|
@ -97,24 +119,23 @@ export default class TranslatorsPanel extends Toggle {
|
|||
}
|
||||
|
||||
|
||||
public static MissingTranslationsFor(layout: LayoutConfig) : {completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number} {
|
||||
public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } {
|
||||
let total = 0
|
||||
const completeness = new Map<string, number>()
|
||||
const untranslated = new Map<string, string[]>()
|
||||
|
||||
Utils.WalkObject(layout, (o, path) => {
|
||||
const translation = <Translation><any>o;
|
||||
if(translation.translations["*"] !== undefined){
|
||||
if (translation.translations["*"] !== undefined) {
|
||||
return
|
||||
}
|
||||
if(translation.context === undefined || translation.context.indexOf(":") < 0){
|
||||
if (translation.context === undefined || translation.context.indexOf(":") < 0) {
|
||||
// no source given - lets ignore
|
||||
return
|
||||
}
|
||||
|
||||
for (const lang of translation.SupportedLanguages()) {
|
||||
completeness.set(lang, 1 + (completeness.get(lang) ?? 0))
|
||||
}
|
||||
layout.title.SupportedLanguages().forEach(ln => {
|
||||
total ++
|
||||
used_languages.languages.forEach(ln => {
|
||||
const trans = translation.translations
|
||||
if (trans["*"] !== undefined) {
|
||||
return;
|
||||
|
@ -124,11 +145,11 @@ export default class TranslatorsPanel extends Toggle {
|
|||
untranslated.set(ln, [])
|
||||
}
|
||||
untranslated.get(ln).push(translation.context)
|
||||
}else{
|
||||
completeness.set(ln, 1 + (completeness.get(ln) ?? 0))
|
||||
}
|
||||
})
|
||||
if(translation.translations["*"] === undefined){
|
||||
total++
|
||||
}
|
||||
|
||||
}, o => {
|
||||
if (o === undefined || o === null) {
|
||||
return false;
|
||||
|
|
|
@ -248,7 +248,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
const inputEl = new InputElementMap<number[], TagsFilter>(
|
||||
checkBoxes,
|
||||
(t0, t1) => {
|
||||
return t0?.isEquivalent(t1) ?? false
|
||||
return t0?.shadows(t1) ?? false
|
||||
},
|
||||
(indices) => {
|
||||
if (indices.length === 0) {
|
||||
|
@ -370,7 +370,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
return new FixedInputElement(
|
||||
TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state),
|
||||
tagging,
|
||||
(t0, t1) => t1.isEquivalent(t0));
|
||||
(t0, t1) => t1.shadows(t0));
|
||||
}
|
||||
|
||||
private static GenerateMappingContent(mapping: {
|
||||
|
@ -450,7 +450,7 @@ export default class TagRenderingQuestion extends Combine {
|
|||
})
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class ReviewElement extends VariableUiElement {
|
|||
SingleReview.GenStars(avg),
|
||||
new Link(
|
||||
revs.length === 1 ? Translations.t.reviews.title_singular.Clone() :
|
||||
Translations.t.reviews.title.Clone()
|
||||
Translations.t.reviews.title
|
||||
.Subs({count: "" + revs.length}),
|
||||
`https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`,
|
||||
true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {Translation, TypedTranslation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
@ -22,7 +22,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
private static extraProperties: {
|
||||
requires?: { p: number, q?: number }[],
|
||||
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,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import Locale from "./Locale";
|
||||
import {Utils} from "../../Utils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Link from "../Base/Link";
|
||||
import Svg from "../../Svg";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LinkToWeblate from "../Base/LinkToWeblate";
|
||||
|
||||
export class Translation extends BaseUIElement {
|
||||
|
@ -165,24 +162,6 @@ export class Translation extends BaseUIElement {
|
|||
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 {
|
||||
const newTranslations = {};
|
||||
for (const lang in this.translations) {
|
||||
|
@ -278,5 +257,28 @@ export class Translation extends BaseUIElement {
|
|||
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 {Translation} from "./Translation";
|
||||
import {Translation, TypedTranslation} from "./Translation";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import * as known_languages from "../../assets/generated/used_languages.json"
|
||||
import CompiledTranslations from "../../assets/generated/CompiledTranslations";
|
||||
|
@ -22,7 +22,7 @@ export default class Translations {
|
|||
return s;
|
||||
}
|
||||
|
||||
static T(t: string | any, context = undefined): Translation {
|
||||
static T(t: string | any, context = undefined): TypedTranslation<object> {
|
||||
if (t === undefined || t === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -30,17 +30,17 @@ export default class Translations {
|
|||
t = "" + t
|
||||
}
|
||||
if (typeof t === "string") {
|
||||
return new Translation({"*": t}, context);
|
||||
return new TypedTranslation({"*": t}, context);
|
||||
}
|
||||
if (t.render !== undefined) {
|
||||
const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly"
|
||||
console.error(msg, t);
|
||||
throw msg
|
||||
}
|
||||
if (t instanceof Translation) {
|
||||
if (t instanceof TypedTranslation) {
|
||||
return t;
|
||||
}
|
||||
return new Translation(t, context);
|
||||
return new TypedTranslation(t, context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {readFileSync, writeFileSync} from "fs";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {TagRenderingConfigJson} from "../../../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||
import ScriptUtils from "../../../scripts/ScriptUtils";
|
||||
import {LayerConfigJson} from "../../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import FilterConfigJson from "../../../Models/ThemeConfig/Json/FilterConfigJson";
|
||||
|
@ -8,7 +7,7 @@ import {QuestionableTagRenderingConfigJson} from "../../../Models/ThemeConfig/Js
|
|||
|
||||
|
||||
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): {
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
],
|
||||
"overrideAll": {
|
||||
"allowMove": {
|
||||
"improveAccuracy": true
|
||||
"enableRelocation": false,
|
||||
"enableImproveAccuracy": true
|
||||
},
|
||||
"+titleIcons": [
|
||||
{
|
||||
|
|
|
@ -276,17 +276,17 @@
|
|||
"willBePublished": "La teva foto serà publicada: "
|
||||
},
|
||||
"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.",
|
||||
"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",
|
||||
"inspectLooksCorrect": "Aquests valors semblen correctes",
|
||||
"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>"
|
||||
},
|
||||
"login": {
|
||||
"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",
|
||||
"loginIsCorrect": "<b>{name}</b> és el compte correcte per crear les notes d'importació.",
|
||||
"loginRequired": "Has d'entrar per continuar",
|
||||
"userAccountTitle": "Seleccionar compte d'usuari"
|
||||
},
|
||||
"mapPreview": {
|
||||
"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",
|
||||
|
@ -294,6 +294,12 @@
|
|||
"selectLayer": "Amb quina capa coincideix aquesta importació?",
|
||||
"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": {
|
||||
"description": "Seleccionar un fitxer .csv o .geojson per començar",
|
||||
"errDuplicate": "Algunes columnes tenen el mateix nom",
|
||||
|
@ -308,11 +314,7 @@
|
|||
"noFilesLoaded": "No s'ha carregat cap arxiu",
|
||||
"title": "Seleccionar arxiu"
|
||||
},
|
||||
"selectLayer": "Seleccionar capa...",
|
||||
"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"
|
||||
"title": "Ajuda de l'importador"
|
||||
},
|
||||
"importInspector": {
|
||||
"title": "Inspeccionar i controlar notes d'importació"
|
||||
|
|
|
@ -408,11 +408,10 @@
|
|||
"title": "Thema auswählen",
|
||||
"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",
|
||||
"title": "Import-Helfer",
|
||||
"userAccountTitle": "Wähle einen Benutzeraccount",
|
||||
"validateDataTitle": "Bestätige Daten"
|
||||
"validateDataTitle": "Bestätige Daten",
|
||||
"title": "Import-Helfer"
|
||||
},
|
||||
"importInspector": {
|
||||
"title": "Importhinweise überprüfen und verwalten"
|
||||
|
|
|
@ -619,6 +619,7 @@
|
|||
},
|
||||
"translations": {
|
||||
"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",
|
||||
"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.",
|
||||
|
|
|
@ -275,8 +275,7 @@
|
|||
"selectFile": {
|
||||
"title": "Seleccionar archivo"
|
||||
},
|
||||
"title": "Ayudante de importación",
|
||||
"validateDataTitle": "Validar datos"
|
||||
"title": "Ayudante de importación"
|
||||
},
|
||||
"importLayer": {
|
||||
"layerName": "Posible {title}",
|
||||
|
|
|
@ -42,11 +42,6 @@
|
|||
"getStartedLogin": "Entra no OpenStreetMap para comezar",
|
||||
"getStartedNewAccount": " ou <a href='https://www.openstreetmap.org/user/new' target='_blank'>crea unha nova conta</a>",
|
||||
"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": {
|
||||
"title": "Seleccionar capas",
|
||||
"zoomInToSeeThisLayer": "Achégate para ver esta capa"
|
||||
|
|
|
@ -267,9 +267,13 @@
|
|||
"willBePublished": "A képed így lesz közzétéve: "
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"previewAttributes": {
|
||||
"allAttributesSame": "Ez a címke minden importálandó objektumon szerepel"
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"#": "Ezek a szövegek akkor jelennek meg a témagombok felett, ha nincs betöltve téma",
|
||||
"featuredThemeTitle": "Kiemelt ezen a héten",
|
||||
|
|
|
@ -97,12 +97,6 @@
|
|||
"backToMapcomplete": "Terug naar het themaoverzicht",
|
||||
"backgroundMap": "Achtergrondkaart",
|
||||
"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",
|
||||
"customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.",
|
||||
"download": {
|
||||
|
@ -134,12 +128,6 @@
|
|||
"histogram": {
|
||||
"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": {
|
||||
"title": "Selecteer lagen",
|
||||
"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.",
|
||||
"removeLocationHistory": "Verwijder de geschiedenis aan locaties",
|
||||
"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",
|
||||
"screenToSmall": "Open {theme} in een nieuw venster",
|
||||
"search": {
|
||||
|
@ -304,17 +276,17 @@
|
|||
"willBePublished": "Jouw foto wordt gepubliceerd "
|
||||
},
|
||||
"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.",
|
||||
"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",
|
||||
"inspectLooksCorrect": "Deze waardes zien er correct uit",
|
||||
"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>"
|
||||
},
|
||||
"login": {
|
||||
"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",
|
||||
"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",
|
||||
"userAccountTitle": "Selecteer een account"
|
||||
},
|
||||
"mapPreview": {
|
||||
"autodetected": "Deze laag was automatisch gekozen gebaseerd op de aanwezige eigenschappen",
|
||||
"confirm": "De objecten bevinden zich op de juiste locatie",
|
||||
|
@ -322,6 +294,12 @@
|
|||
"selectLayer": "Met welke laag komt deze te importeren dataset overeen?",
|
||||
"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": {
|
||||
"description": "Selecteer een .csv of .geojson-bestand",
|
||||
"errDuplicate": "Sommige kolommen hebben dezelfde naam",
|
||||
|
@ -336,11 +314,7 @@
|
|||
"noFilesLoaded": "Geen bestand ingeladen op dit moment",
|
||||
"title": "Selecteer bestand"
|
||||
},
|
||||
"selectLayer": "Selecteer een laag...",
|
||||
"someHaveSame": "{count} te importeren objecten hebben dit attribuut, dit is {percentage}% van het totaal",
|
||||
"title": "Importeer-helper",
|
||||
"userAccountTitle": "Selecteer een account",
|
||||
"validateDataTitle": "Valideer data"
|
||||
"title": "Importeer-helper"
|
||||
},
|
||||
"importInspector": {
|
||||
"title": "Inspecteer en beheer importeer-notas"
|
||||
|
|
112
langs/pl.json
112
langs/pl.json
|
@ -35,25 +35,6 @@
|
|||
"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.",
|
||||
"general": {
|
||||
"about": "Łatwo edytuj i dodaj OpenStreetMap dla określonego motywu",
|
||||
"aboutMapcomplete": "<h3>O MapComplete</h3><p>Dzięki MapComplete możesz wzbogacić OpenStreetMap o informacje na <b>pojedynczy temat.</b> Odpowiedz na kilka pytań, a w ciągu kilku minut Twój wkład będzie dostępny na całym świecie! Opiekun <b>tematu</b> definiuje elementy, pytania i języki dla tematu.</p><h3>Dowiedz się więcej</h3><p>MapComplete zawsze <b>oferuje następny krok</b>, by dowiedzieć się więcej o OpenStreetMap.</p><ul><li>Po osadzeniu na stronie internetowej, element iframe łączy się z pełnoekranowym MapComplete</li><li>Wersja pełnoekranowa oferuje informacje o OpenStreetMap</li><li>Przeglądanie działa bez logowania, ale edycja wymaga loginu OSM.</li><li>Jeżeli nie jesteś zalogowany, zostaniesz poproszony o zalogowanie się</li><li>Po udzieleniu odpowiedzi na jedno pytanie, możesz dodać nowe punkty do mapy</li><li>Po chwili wyświetlane są rzeczywiste tagi OSM, które później linkują do wiki</li></ul><p></p><br><p>Zauważyłeś <b>problem</b>? Czy masz <b>prośbę o dodanie jakiejś funkcji</b>? Chcesz <b>pomóc w tłumaczeniu</b>? Udaj się do <a href=\"https://github.com/pietervdvn/MapComplete\" target=\"_blank\">kodu źródłowego</a> lub <a href=\"https://github.com/pietervdvn/MapComplete/issues\" target=\"_blank\">issue trackera.</a> </p><p> Chcesz zobaczyć <b>swoje postępy</b>? Śledź liczbę edycji na <a href=\"{osmcha_link}\" target=\"_blank\">OsmCha</a>.</p>",
|
||||
"add": {
|
||||
"addNew": "Dodaj nową {category} tutaj",
|
||||
"confirmButton": "Dodaj tutaj {category}.<br><div class=\"alert\">Twój dodatek jest widoczny dla wszystkich</div>",
|
||||
"confirmIntro": "<h3>Czy dodać tutaj {title}?</h3> Punkt, który tutaj utworzysz, będzie <b>widoczny dla wszystkich</b>. Proszę, dodawaj rzeczy do mapy tylko wtedy, gdy naprawdę istnieją. Wiele aplikacji korzysta z tych danych.",
|
||||
"intro": "Kliknąłeś gdzieś, gdzie nie są jeszcze znane żadne dane.<br>",
|
||||
"layerNotEnabled": "Warstwa {layer} nie jest włączona. Włącz tę warstwę, aby dodać punkt",
|
||||
"openLayerControl": "Otwórz okno sterowania warstwą",
|
||||
"pleaseLogin": "<a class=\"activate-osm-authentication\">Zaloguj się, aby dodać nowy punkt</a>",
|
||||
"stillLoading": "Dane wciąż się ładują. Poczekaj chwilę, zanim dodasz nowy punkt.",
|
||||
"title": "Czy dodać nowy punkt?",
|
||||
"zoomInFurther": "Powiększ jeszcze bardziej, aby dodać punkt."
|
||||
},
|
||||
"backgroundMap": "Tło mapy",
|
||||
"cancel": "Anuluj",
|
||||
"customThemeIntro": "<h3>Motywy własne</h3>Są to wcześniej odwiedzone motywy stworzone przez użytkowników.",
|
||||
"fewChangesBefore": "Proszę odpowiedzieć na kilka pytań dotyczących istniejących punktów przed dodaniem nowego punktu.",
|
||||
"getStartedLogin": "Zaloguj się za pomocą OpenStreetMap, aby rozpocząć",
|
||||
"getStartedNewAccount": " lub <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">utwórz nowe konto</a>",
|
||||
"goToInbox": "Otwórz skrzynkę odbiorczą",
|
||||
|
@ -141,99 +122,6 @@
|
|||
},
|
||||
"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": {
|
||||
"addPicture": "Dodaj zdjęcie",
|
||||
"ccb": "na licencji CC-BY",
|
||||
|
|
|
@ -90,130 +90,6 @@
|
|||
"title": "下載可視的資料"
|
||||
},
|
||||
"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": "登入開放街圖帳號來開始",
|
||||
"getStartedNewAccount": " 或是 <a href=\"https://www.openstreetmap.org/user/new\" target=\"_blank\">註冊新帳號</a>",
|
||||
"goToInbox": "開啟訊息框",
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -43,7 +43,7 @@
|
|||
"libphonenumber-js": "^1.7.55",
|
||||
"lz-string": "^1.4.4",
|
||||
"mangrove-reviews": "^0.1.3",
|
||||
"moment": "^2.29.0",
|
||||
"moment": "^2.29.2",
|
||||
"opening_hours": "^3.6.0",
|
||||
"osm-auth": "^1.0.2",
|
||||
"osmtogeojson": "^3.0.0-beta.4",
|
||||
|
@ -9738,9 +9738,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
|
@ -24294,9 +24294,9 @@
|
|||
"integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
|
||||
},
|
||||
"monotone-convex-hull-2d": {
|
||||
"version": "1.0.1",
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
"libphonenumber-js": "^1.7.55",
|
||||
"lz-string": "^1.4.4",
|
||||
"mangrove-reviews": "^0.1.3",
|
||||
"moment": "^2.29.0",
|
||||
"moment": "^2.29.2",
|
||||
"opening_hours": "^3.6.0",
|
||||
"osm-auth": "^1.0.2",
|
||||
"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;
|
||||
}
|
||||
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 {
|
||||
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 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 += "\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")
|
||||
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([
|
||||
"natuurpunt",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue