Merge pull request #1 from pietervdvn/themes/maxspeed

Themes/maxspeed
This commit is contained in:
yopaseopor 2022-04-22 23:07:18 +02:00 committed by GitHub
commit bcd51b4ee2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
255 changed files with 13427 additions and 6551 deletions

View file

@ -65,6 +65,9 @@
- birdhide
- cafe_pub
- charging_station
- climbing
- climbing_gym
- climbing_route
- defibrillator
- drinking_water
- entrance
@ -94,6 +97,7 @@
- viewpoint
- village_green
- watermill
- windturbine
@ -108,7 +112,10 @@
- bicycle_rental
- bike_themed_object
- cafe_pub
- climbing_club
- climbing_gym
- food
- hackerspace
- nature_reserve
- observation_tower
- playground
@ -127,7 +134,10 @@
- bicycle_rental
- bike_themed_object
- cafe_pub
- climbing_club
- climbing_gym
- food
- hackerspace
- recycling
@ -143,7 +153,10 @@
- bicycle_rental
- bike_themed_object
- cafe_pub
- climbing_club
- climbing_gym
- food
- hackerspace
- recycling
@ -160,6 +173,8 @@
- bike_shop
- bike_themed_object
- cafe_pub
- climbing_club
- climbing_gym
- food
@ -251,6 +266,7 @@
- cafe_pub
- defibrillator
- food
- hackerspace
- observation_tower

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

BIN
Docs/FilteredByDepth.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

View file

@ -94,12 +94,11 @@ The question is **What kind of bicycle rental is this?**
- **This is a shop whose main focus is bicycle rental** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:shop' target='_blank'>shop</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:shop%3Dbicycle_rental' target='_blank'>bicycle_rental</a>&<a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Dshop' target='_blank'>shop</a>
- **This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:shop' target='_blank'>shop</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:shop%3Drental' target='_blank'>rental</a>_This option cannot be chosen as answer_
- **This is a shop which sells or repairs bicycles, but also rents out bicycles** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:service:bicycle:rental' target='_blank'>service:bicycle:rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:service:bicycle:rental%3Dyes' target='_blank'>yes</a>&<a href='https://wiki.openstreetmap.org/wiki/Key:shop' target='_blank'>shop</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:shop%3Dbicycle' target='_blank'>bicycle</a>
- **This is an automated docking station, where a bicycle is mechanically locked into a structure** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Ddocking_station' target='_blank'>docking_station</a>
- **A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Dkey_dispensing_machine' target='_blank'>key_dispensing_machine</a>
- **This is a dropoff point: a designated bicycle parking for this cycle rental** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Ddropoff_point' target='_blank'>dropoff_point</a>
- **This is a shop whose main focus is bicycle rental** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:shop' target='_blank'>shop</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:shop%3Drental' target='_blank'>rental</a>&<a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Dshop' target='_blank'>shop</a>
- **This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:service:bicycle:rental' target='_blank'>service:bicycle:rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:service:bicycle:rental%3Dyes' target='_blank'>yes</a>&<a href='https://wiki.openstreetmap.org/wiki/Key:shop' target='_blank'>shop</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:shop%3Dbicycle' target='_blank'>bicycle</a>
- **This is a shop which sells or repairs bicycles, but also rents out bicycles** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Ddocking_station' target='_blank'>docking_station</a>
- **This is an automated docking station, where a bicycle is mechanically locked into a structure** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Dkey_dispensing_machine' target='_blank'>key_dispensing_machine</a>
- **A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby** corresponds with <a href='https://wiki.openstreetmap.org/wiki/Key:bicycle_rental' target='_blank'>bicycle_rental</a>=<a href='https://wiki.openstreetmap.org/wiki/Tag:bicycle_rental%3Ddropoff_point' target='_blank'>dropoff_point</a>
Only visible if `amenity=bicycle_rental` is shown

View file

@ -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,14 +20,217 @@ 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.
![](./FilterFunctionality.gif)
```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:
![](./FilteredByDepth.gif)
```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
------------
[A basic template is availalbe here](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/theme-template.json)
[A basic template is available here](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/theme-template.json)
The custom theme generator
--------------------------
@ -229,18 +437,21 @@ 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
example, reusing background images and substituting the colours or HTML rendering. If you need advanced stuff, read it
through!
### Forgetting adjacent concepts
Some new contributors might add a POI to indicate something that resembles it, but quite isn't.
For example, if they are only offered a layer with public bookcases, they might map their local library with a public bookcase.
The perfect solution for this is to provide both the library-layer and public bookcases layer - but this requires having both layers.
A good solution is to clearly explain what a certain feature is and what it is not.

View file

@ -46,42 +46,37 @@
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows shop=bicycle_rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "bicycle_rental"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows shop=bicycle_rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "shop"
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows shop=rental with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows shop=rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "rental"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows shop=rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "shop"
},
{
"key": "service:bicycle:rental",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "yes"
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "bicycle"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=docking_station with a fixed text, namely 'This is an automated docking station, where a bicycle is mechanically locked into a structure' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=docking_station with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "docking_station"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=key_dispensing_machine with a fixed text, namely 'A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=key_dispensing_machine with a fixed text, namely 'This is an automated docking station, where a bicycle is mechanically locked into a structure' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "key_dispensing_machine"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=dropoff_point with a fixed text, namely 'This is a dropoff point: a designated bicycle parking for this cycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=dropoff_point with a fixed text, namely 'A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Bicycle rental')",
"value": "dropoff_point"
},
{

View file

@ -607,42 +607,37 @@
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows shop=bicycle_rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "bicycle_rental"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows shop=bicycle_rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "shop"
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows shop=rental with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows shop=rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "rental"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows shop=rental&bicycle_rental=shop with a fixed text, namely 'This is a shop whose main focus is bicycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "shop"
},
{
"key": "service:bicycle:rental",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "yes"
},
{
"key": "shop",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows service:bicycle:rental=yes&shop=bicycle with a fixed text, namely 'This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "bicycle"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=docking_station with a fixed text, namely 'This is an automated docking station, where a bicycle is mechanically locked into a structure' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=docking_station with a fixed text, namely 'This is a shop which sells or repairs bicycles, but also rents out bicycles' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "docking_station"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=key_dispensing_machine with a fixed text, namely 'A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=key_dispensing_machine with a fixed text, namely 'This is an automated docking station, where a bicycle is mechanically locked into a structure' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "key_dispensing_machine"
},
{
"key": "bicycle_rental",
"description": "Layer 'Bicycle rental' shows bicycle_rental=dropoff_point with a fixed text, namely 'This is a dropoff point: a designated bicycle parking for this cycle rental' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"description": "Layer 'Bicycle rental' shows bicycle_rental=dropoff_point with a fixed text, namely 'A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Personal theme')",
"value": "dropoff_point"
},
{

View file

@ -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",

View file

@ -101,7 +101,6 @@ export default class GeoLocationHandler extends VariableUiElement {
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
const willFocus = lastClick.map(lastUserRequest => {
const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000
console.log("TimeDiff with initedAtt is ", timeDiffInited)
if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) {
return true
}
@ -109,7 +108,6 @@ export default class GeoLocationHandler extends VariableUiElement {
return false;
}
const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000
console.log("TimeDiff with lastClick is ", timeDiff)
return timeDiff <= Constants.zoomToLocationTimeout
})
@ -202,7 +200,7 @@ export default class GeoLocationHandler extends VariableUiElement {
self.init(true, true);
});
const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined
this.init(false, doAutoZoomToLocation);
@ -363,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));
}
}

View file

@ -16,7 +16,7 @@ export default class TitleHandler {
const currentTitle: UIEventSource<string> = state.selectedElement.map(
selected => {
const layout = state.layoutToUse
const defaultTitle = Translations.WT(layout?.title)?.txt ?? "MapComplete"
const defaultTitle = layout?.title?.txt ?? "MapComplete"
if (selected === undefined) {
return defaultTitle

View file

@ -14,7 +14,6 @@ import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
import SharedTagRenderings from "../Customizations/SharedTagRenderings";
import * as known_layers from "../assets/generated/known_layers.json"
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme";
import * as licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
@ -43,10 +42,6 @@ export default class DetermineLayout {
}
let layoutId: string = undefined
if (location.href.indexOf("buurtnatuur.be") >= 0) {
layoutId = "buurtnatuur"
}
const path = window.location.pathname.split("/").slice(-1)[0];
if (path !== "theme.html" && path !== "") {
@ -72,7 +67,7 @@ export default class DetermineLayout {
public static LoadLayoutFromHash(
userLayoutParam: UIEventSource<string>
): (LayoutConfig & {definition: LayoutConfigJson}) | null {
): LayoutConfig | null {
let hash = location.hash.substr(1);
let json: any;
@ -113,9 +108,7 @@ export default class DetermineLayout {
const layoutToUse = DetermineLayout.prepCustomTheme(json)
userLayoutParam.setData(layoutToUse.id);
const config = new LayoutConfig(layoutToUse, false);
config["definition"] = json
return <any> config
return layoutToUse
} catch (e) {
console.error(e)
if (hash === undefined || hash.length < 10) {
@ -144,7 +137,7 @@ export default class DetermineLayout {
.AttachTo("centermessage");
}
private static prepCustomTheme(json: any): LayoutConfigJson {
private static prepCustomTheme(json: any, sourceUrl?: string): LayoutConfig {
if(json.layers === undefined && json.tagRenderings !== undefined){
const iconTr = json.mapRendering.map(mr => mr.icon).find(icon => icon !== undefined)
@ -161,7 +154,6 @@ export default class DetermineLayout {
}
}
const knownLayersDict = new Map<string, LayerConfigJson>()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
@ -172,10 +164,17 @@ export default class DetermineLayout {
sharedLayers: knownLayersDict
}
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme")
const raw = json;
json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images")
json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme")
console.log("The layoutconfig is ", json)
return json
return new LayoutConfig(json, false, {
definitionRaw: JSON.stringify(raw, null, " "),
definedAtUrl: sourceUrl
})
}
private static async LoadRemoteTheme(link: string): Promise<LayoutConfig | null> {
@ -190,8 +189,7 @@ export default class DetermineLayout {
try {
parsed.id = link;
console.log("Loaded remote link:", link)
const layoutToUse = DetermineLayout.prepCustomTheme(parsed)
return new LayoutConfig(layoutToUse, false)
return DetermineLayout.prepCustomTheme(parsed, link)
} catch (e) {
console.error(e)
DetermineLayout.ShowErrorOnCustomTheme(

View file

@ -1,5 +1,5 @@
/**
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indiciates with what renderConfig it should be rendered.
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
*/
import {UIEventSource} from "../../UIEventSource";
import {GeoOperations} from "../../GeoOperations";
@ -11,22 +11,25 @@ export default class RenderingMultiPlexerFeatureSource {
public readonly features: UIEventSource<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
constructor(upstream: FeatureSource, layer: LayerConfig) {
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
rendering: r,
index: i
}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
const hasCentroid = centroidRenderings.length > 0 || projectedCentroidRenderings.length > 0
const lineRenderObjects = layer.lineRendering
this.features = upstream.features.map(
features => {
if (features === undefined) {
return;
}
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
rendering: r,
index: i
}))
const pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
const centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
const startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
const endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
const lineRenderObjects = layer.lineRendering
const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined, multiLineStringIndex: number | undefined })[] = [];
@ -55,12 +58,25 @@ export default class RenderingMultiPlexerFeatureSource {
}
} else {
// This is a a line: add the centroids
for (const rendering of centroidRenderings) {
addAsPoint(feat, rendering, GeoOperations.centerpointCoordinates(feat))
let centerpoint: [number, number] = undefined;
let projectedCenterPoint : [number, number] = undefined
if(hasCentroid){
centerpoint = GeoOperations.centerpointCoordinates(feat)
if(projectedCentroidRenderings.length > 0){
projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
}
}
for (const rendering of centroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
if (feat.geometry.type === "LineString") {
for (const rendering of projectedCentroidRenderings) {
addAsPoint(feat, rendering, projectedCenterPoint)
}
// Add start- and endpoints
const coordinates = feat.geometry.coordinates
for (const rendering of startRenderings) {
@ -71,6 +87,10 @@ export default class RenderingMultiPlexerFeatureSource {
addAsPoint(feat, rendering, coordinate)
}
}else{
for (const rendering of projectedCentroidRenderings) {
addAsPoint(feat, rendering, centerpoint)
}
}
// AT last, add it 'as is' to what we should render

View file

@ -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));
});
updateData(Number(length.data));
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)) {
console.log("Clearing ", key)
self.GetPreference(key, "").setData("")
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)

View file

@ -8,6 +8,13 @@ export class And extends TagsFilter {
super();
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 = [];
@ -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){
@ -173,27 +247,56 @@ export class And extends TagsFilter {
newAnds.push(tf)
}
}
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
return false
{
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
}
// 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)
}
}
return true;
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());
}
}

View file

@ -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;
}

View file

@ -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)
}
}
return true;
})
containedAnds = cleanedContainedANds
} while(dirty)
}
// 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());
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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[] {

View file

@ -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)))
}
}

View file

@ -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;
@ -30,7 +34,7 @@ export abstract class TagsFilter {
* Returns an optimized version (or self) of this tagsFilter
*/
abstract optimize(): TagsFilter | boolean;
/**
* Returns 'true' if the tagsfilter might select all features (i.e. the filter will return everything from OSM, except a few entries).
*

View file

@ -1,6 +1,6 @@
import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
import * as wds from "wikibase-sdk"
import * as wds from "wikidata-sdk"
export class WikidataResponse {
public readonly id: string
@ -126,13 +126,22 @@ export interface WikidataSearchoptions {
maxCount?: 20 | number
}
export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions {
instanceOf?: number[];
notInstanceOf?: number[]
}
/**
* Utility functions around wikidata
*/
export default class Wikidata {
private static readonly _identifierPrefixes = ["Q", "L"].map(str => str.toLowerCase())
private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:", "https://www.wikidata.org/wiki/", "Lexeme:"].map(str => str.toLowerCase())
private static readonly _prefixesToRemove = ["https://www.wikidata.org/wiki/Lexeme:",
"https://www.wikidata.org/wiki/",
"http://www.wikidata.org/entity/",
"Lexeme:"].map(str => str.toLowerCase())
private static readonly _cache = new Map<string, UIEventSource<{ success: WikidataResponse } | { error: any }>>()
@ -148,6 +157,52 @@ export default class Wikidata {
return src;
}
/**
* Given a search text, searches for the relevant wikidata entries, excluding pages "outside of the main tree", e.g. disambiguation pages.
* Optionally, an 'instance of' can be given to limit the scope, e.g. instanceOf:5 (humans) will only search for humans
*/
public static async searchAdvanced(text: string, options: WikidataAdvancedSearchoptions): Promise<{
id: string,
relevance?: number,
label: string,
description?: string
}[]> {
let instanceOf = ""
if (options?.instanceOf !== undefined && options.instanceOf.length > 0) {
const phrases = options.instanceOf.map(q => `{ ?item wdt:P31/wdt:P279* wd:Q${q}. }`)
instanceOf = "{"+ phrases.join(" UNION ") + "}"
}
const forbidden = (options?.notInstanceOf ?? [])
.concat([17379835]) // blacklist 'wikimedia pages outside of the main knowledge tree', e.g. disambiguation pages
const minusPhrases = forbidden.map(q => `MINUS {?item wdt:P31/wdt:P279* wd:Q${q} .}`)
const sparql = `SELECT * WHERE {
SERVICE wikibase:mwapi {
bd:serviceParam wikibase:api "EntitySearch" .
bd:serviceParam wikibase:endpoint "www.wikidata.org" .
bd:serviceParam mwapi:search "${text}" .
bd:serviceParam mwapi:language "${options.lang}" .
?item wikibase:apiOutputItem mwapi:item .
?num wikibase:apiOrdinal true .
bd:serviceParam wikibase:limit ${Math.round((options.maxCount ?? 20) * 1.5) /*Some padding for disambiguation pages */} .
?label wikibase:apiOutput mwapi:label .
?description wikibase:apiOutput "@description" .
}
${instanceOf}
${minusPhrases.join("\n ")}
} ORDER BY ASC(?num) LIMIT ${options.maxCount ?? 20}`
const url = wds.sparqlQuery(sparql)
const result = await Utils.downloadJson(url)
/*The full uri of the wikidata-item*/
return result.results.bindings.map(({item, label, description, num}) => ({
relevance: num?.value,
id: item?.value,
label: label?.value,
description: description?.value
}))
}
public static async search(
search: string,
options?: WikidataSearchoptions,
@ -195,39 +250,28 @@ export default class Wikidata {
public static async searchAndFetch(
search: string,
options?: WikidataSearchoptions
options?: WikidataAdvancedSearchoptions
): Promise<WikidataResponse[]> {
const maxCount = options.maxCount
// We provide some padding to filter away invalid values
options.maxCount = Math.ceil((options.maxCount ?? 20) * 1.5)
const searchResults = await Wikidata.search(search, options)
const maybeResponses = await Promise.all(searchResults.map(async r => {
try {
return await Wikidata.LoadWikidataEntry(r.id).AsPromise()
} catch (e) {
console.error(e)
return undefined;
}
}))
const responses = maybeResponses
.map(r => <WikidataResponse>r["success"])
.filter(wd => {
if (wd === undefined) {
return false;
const searchResults = await Wikidata.searchAdvanced(search, options)
const maybeResponses = await Promise.all(
searchResults.map(async r => {
try {
console.log("Loading ", r.id)
return await Wikidata.LoadWikidataEntry(r.id).AsPromise()
} catch (e) {
console.error(e)
return undefined;
}
if (wd.claims.get("P31" /*Instance of*/)?.has("Q4167410"/* Wikimedia Disambiguation page*/)) {
return false;
}
return true;
})
responses.splice(maxCount, responses.length - maxCount)
return responses
}))
return Utils.NoNull(maybeResponses.map(r => <WikidataResponse>r["success"]))
}
/**
* Gets the 'key' segment from a URL
*
*
* Wikidata.ExtractKey("https://www.wikidata.org/wiki/Lexeme:L614072") // => "L614072"
* Wikidata.ExtractKey("http://www.wikidata.org/entity/Q55008046") // => "Q55008046"
*/
public static ExtractKey(value: string | number): string {
if (typeof value === "number") {
@ -271,6 +315,35 @@ export default class Wikidata {
return undefined;
}
/**
* Converts 'Q123' into 123, returns undefined if invalid
*
* Wikidata.QIdToNumber("Q123") // => 123
* Wikidata.QIdToNumber(" Q123 ") // => 123
* Wikidata.QIdToNumber(" X123 ") // => undefined
* Wikidata.QIdToNumber(" Q123X ") // => undefined
* Wikidata.QIdToNumber(undefined) // => undefined
* Wikidata.QIdToNumber(123) // => 123
*/
public static QIdToNumber(q: string | number): number | undefined {
if(q === undefined || q === null){
return
}
if(typeof q === "number"){
return q
}
q = q.trim()
if (!q.startsWith("Q")) {
return
}
q = q.substr(1)
const n = Number(q)
if (isNaN(n)) {
return
}
return n
}
public static IdToArticle(id: string) {
if (id.startsWith("Q")) {
return "https://wikidata.org/wiki/" + id
@ -305,4 +378,4 @@ export default class Wikidata {
return WikidataResponse.fromJson(response)
}
}
}

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.19.0-alpha";
public static vNumber = "0.18.2";
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -38,16 +38,87 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* ]
* }
* rewritten // => expected
*
* // should use the ID if one is present instead of the index
* const theme = {
* layers: [
* {
* tagRenderings:[
*
* {id: "some-tr",
* question:{
* en:"Question?"
* }
* }
* ]
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const expected = {
* layers: [
* {
* tagRenderings:[
*
* {id: "some-tr",
* question:{
* _context: "prefix:context.layers.0.tagRenderings.some-tr.question"
* en:"Question?"
* }
* }
* ]
* }
* ]
* }
* rewritten // => expected
*
* // should preserve nulls
* const theme = {
* layers: [
* {
* builtin: ["abc"],
* override: {
* name:null
* }
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const expected = {
* layers: [
* {
* builtin: ["abc"],
* override: {
* name: null
* }
* }
* ]
* }
* rewritten // => expected
*/
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
const result = Utils.WalkJson(json, (leaf, path) => {
if(leaf === undefined || leaf === null){
return leaf
}
if (typeof leaf === "object") {
// follow the path. If we encounter a number, check that there is no ID we can use instead
let breadcrumb = json;
for (let i = 0; i < path.length; i++) {
const pointer = path[i]
breadcrumb = breadcrumb[pointer]
if(pointer.match("[0-9]+") && breadcrumb["id"] !== undefined){
path[i] = breadcrumb["id"]
}
}
return {...leaf, _context: this._prefix + context + "." + path.join(".")}
} else {
return leaf
}
}, obj => obj !== undefined && obj !== null && Translations.isProbablyATranslation(obj))
}, obj => obj === undefined || obj === null || Translations.isProbablyATranslation(obj))
return {
result

View file

@ -158,6 +158,20 @@ export class On<P, T> extends DesugaringStep<T> {
}
}
export class Pass<T> extends Conversion<T, T> {
constructor(message?: string) {
super(message??"Does nothing, often to swap out steps in testing", [], "Pass");
}
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
return {
result: json
};
}
}
export class Concat<X, T> extends Conversion<X[], T[]> {
private readonly _step: Conversion<X, T[]>;
@ -202,13 +216,18 @@ export class Fuse<T> extends DesugaringStep<T> {
const information = []
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
let r = step.convert(json, "While running step " + step.name + ": " + context)
errors.push(...r.errors ?? [])
warnings.push(...r.warnings ?? [])
information.push(...r.information ?? [])
json = r.result
if (errors.length > 0) {
break;
try{
let r = step.convert(json, "While running step " + step.name + ": " + context)
errors.push(...r.errors ?? [])
warnings.push(...r.warnings ?? [])
information.push(...r.information ?? [])
json = r.result
if (errors.length > 0) {
break;
}
}catch(e){
console.error("Step "+step.name+" failed due to "+e);
throw e
}
}
return {

View file

@ -1,4 +1,4 @@
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, SetDefault} from "./Conversion";
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault} from "./Conversion";
import {LayoutConfigJson} from "../Json/LayoutConfigJson";
import {PrepareLayer} from "./PrepareLayer";
import {LayerConfigJson} from "../Json/LayerConfigJson";
@ -13,28 +13,30 @@ import {AddContextToTranslations} from "./AddContextToTranslations";
class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfigJson[]> {
private readonly _state: DesugaringContext;
constructor(
state: DesugaringContext,
) {
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", [],"SubstituteLayer");
super("Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form", [], "SubstituteLayer");
this._state = state;
}
convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], information?: string[] } {
const errors = []
const information = []
const state= this._state
function reportNotFound(name: string){
const state = this._state
function reportNotFound(name: string) {
const knownLayers = Array.from(state.sharedLayers.keys())
const withDistance = knownLayers.map(lname => [lname, Utils.levenshteinDistance(name, lname)])
const withDistance = knownLayers.map(lname => [lname, Utils.levenshteinDistance(name, lname)])
withDistance.sort((a, b) => a[1] - b[1])
const ids = withDistance.map(n => n[0])
// Known builtin layers are "+.join(",")+"\n For more information, see "
// Known builtin layers are "+.join(",")+"\n For more information, see "
errors.push(`${context}: The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}?
For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`)
}
if (typeof json === "string") {
const found = state.sharedLayers.get(json)
if (found === undefined) {
@ -72,40 +74,40 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig
} catch (e) {
errors.push(`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(json["override"],)}`)
}
if(json["hideTagRenderingsWithLabels"]){
if (json["hideTagRenderingsWithLabels"]) {
const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
// These labels caused at least one deletion
const usedLabels : Set<string> = new Set<string>();
const usedLabels: Set<string> = new Set<string>();
const filtered = []
for (const tr of found.tagRenderings) {
const labels = tr["labels"]
if(labels !== undefined){
if (labels !== undefined) {
const forbiddenLabel = labels.findIndex(l => hideLabels.has(l))
if(forbiddenLabel >= 0){
if (forbiddenLabel >= 0) {
usedLabels.add(labels[forbiddenLabel])
information.push(context+": Dropping tagRendering "+tr["id"]+" as it has a forbidden label: "+labels[forbiddenLabel])
information.push(context + ": Dropping tagRendering " + tr["id"] + " as it has a forbidden label: " + labels[forbiddenLabel])
continue
}
}
if(hideLabels.has(tr["id"])){
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
information.push(context+": Dropping tagRendering "+tr["id"]+" as its id is a forbidden label")
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its id is a forbidden label")
continue
}
if(hideLabels.has(tr["group"])){
if (hideLabels.has(tr["group"])) {
usedLabels.add(tr["group"])
information.push(context+": Dropping tagRendering "+tr["id"]+" as its group `"+tr["group"]+"` is a forbidden label")
information.push(context + ": Dropping tagRendering " + tr["id"] + " as its group `" + tr["group"] + "` is a forbidden label")
continue
}
filtered.push(tr)
}
const unused = Array.from(hideLabels).filter(l => !usedLabels.has(l))
if(unused.length > 0){
errors.push("This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: "+unused.join(", ")+"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore")
if (unused.length > 0) {
errors.push("This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + unused.join(", ") + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore")
}
found.tagRenderings = filtered
}
@ -130,7 +132,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
private _state: DesugaringContext;
constructor(state: DesugaringContext) {
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"],"AddDefaultLayers");
super("Adds the default layers, namely: " + Constants.added_by_default.join(", "), ["layers"], "AddDefaultLayers");
this._state = state;
}
@ -147,8 +149,8 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
errors.push("Default layer " + layerName + " not found")
continue
}
if(alreadyLoaded.has(v.id)){
warnings.push("Layout "+context+" already has a layer with name "+v.id+"; skipping inclusion of this builtin layer")
if (alreadyLoaded.has(v.id)) {
warnings.push("Layout " + context + " already has a layer with name " + v.id + "; skipping inclusion of this builtin layer")
continue
}
json.layers.push(v)
@ -165,7 +167,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"],"AddImportLayers");
super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"], "AddImportLayers");
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[] } {
@ -176,7 +178,7 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
json.layers = [...json.layers]
if(json.enableNoteImports ?? true) {
if (json.enableNoteImports ?? true) {
const creator = new CreateNoteImportLayer()
for (let i1 = 0; i1 < allLayers.length; i1++) {
const layer = allLayers[i1];
@ -222,15 +224,16 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
private readonly _state: DesugaringContext;
constructor(state: DesugaringContext, ) {
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"],"AddMiniMap");
constructor(state: DesugaringContext,) {
super("Adds a default 'minimap'-element to the tagrenderings if none of the elements define such a minimap", ["tagRenderings"], "AddMiniMap");
this._state = state;
}
/**
* Returns true if this tag rendering has a minimap in some language.
* Note: this minimap can be hidden by conditions
*
*
* AddMiniMap.hasMinimap({render: "{minimap()}"}) // => true
* AddMiniMap.hasMinimap({render: {en: "{minimap()}"}}) // => true
* AddMiniMap.hasMinimap({render: {en: "{minimap()}", nl: "{minimap()}"}}) // => true
@ -280,23 +283,23 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
}
}
class AddContextToTransltionsInLayout extends DesugaringStep <LayoutConfigJson>{
class AddContextToTransltionsInLayout extends DesugaringStep <LayoutConfigJson> {
constructor() {
super("Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",["_context"], "AddContextToTranlationsInLayout");
super("Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too", ["_context"], "AddContextToTranlationsInLayout");
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:")
return conversion.convert(json, json.id);
}
}
class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", ["overrideAll", "layers"],"ApplyOverrideAll");
super("Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards", ["overrideAll", "layers"], "ApplyOverrideAll");
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
@ -325,8 +328,9 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext;
constructor(state: DesugaringContext, ) {
super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"],"AddDependencyLayersToTheme");
constructor(state: DesugaringContext,) {
super("If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)", ["layers"], "AddDependencyLayersToTheme");
this._state = state;
}
@ -340,17 +344,17 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
const dependencies: { neededLayer: string, reason: string, context?: string, neededBy: string }[] = []
for (const layerConfig of alreadyLoaded) {
try{
try {
const layerDeps = DependencyCalculator.getLayerDependencies(new LayerConfig(layerConfig))
dependencies.push(...layerDeps)
}catch(e){
} catch (e) {
console.error(e)
throw "Detecting layer dependencies for "+layerConfig.id+" failed due to "+e
throw "Detecting layer dependencies for " + layerConfig.id + " failed due to " + e
}
}
for (const dependency of dependencies) {
if(loadedLayerIds.has(dependency.neededLayer)){
if (loadedLayerIds.has(dependency.neededLayer)) {
// We mark the needed layer as 'mustLoad'
alreadyLoaded.find(l => l.id === dependency.neededLayer).forceLoad = true
}
@ -380,7 +384,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd.map(dep => {
dep = Utils.Clone(dep);
dep.forceLoad = true
@ -418,46 +422,47 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext;
constructor(state: DesugaringContext) {
super("Adds every public layer to the personal theme",["layers"],"PreparePersonalTheme");
super("Adds every public layer to the personal theme", ["layers"], "PreparePersonalTheme");
this._state = state;
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if(json.id !== "personal"){
if (json.id !== "personal") {
return {result: json}
}
json.layers = Array.from(this._state.sharedLayers.keys()).filter(l => Constants.priviliged_layers.indexOf(l) < 0)
return {result: json};
}
}
class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>{
class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson> {
constructor() {
super("Generates a warning if a theme uses an unsubstituted layer", ["layers"],"WarnForUnsubstitutedLayersInTheme");
super("Generates a warning if a theme uses an unsubstituted layer", ["layers"], "WarnForUnsubstitutedLayersInTheme");
}
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if(json.hideFromOverview === true){
if (json.hideFromOverview === true) {
return {result: json}
}
const warnings = []
for (const layer of json.layers) {
if(typeof layer === "string"){
if (typeof layer === "string") {
continue
}
if(layer["builtin"] !== undefined){
if (layer["builtin"] !== undefined) {
continue
}
if(layer["source"]["geojson"] !== undefined){
if (layer["source"]["geojson"] !== undefined) {
// We turn a blind eye for import layers
continue
}
const wrn = "The theme "+json.id+" has an inline layer: "+layer["id"]+". This is discouraged."
const wrn = "The theme " + json.id + " has an inline layer: " + layer["id"] + ". This is discouraged."
warnings.push(wrn)
}
return {
@ -465,13 +470,16 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
warnings
};
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
constructor(state: DesugaringContext) {
constructor(state: DesugaringContext, options?: {
skipDefaultLayers: false | boolean
}) {
super(
"Fully prepares and expands a theme",
new AddContextToTransltionsInLayout(),
new PreparePersonalTheme(state),
new WarnForUnsubstitutedLayersInTheme(),
@ -483,7 +491,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
new ApplyOverrideAll(),
// And then we prepare all the layers _again_ in case that an override all contained unexpanded tagrenderings!
new On("layers", new Each(new PrepareLayer(state))),
new AddDefaultLayers(state),
options?.skipDefaultLayers ? new Pass("AddDefaultLayers is disabled due to the set flag") : new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new On("layers", new Each(new AddMiniMap(state)))

View file

@ -68,7 +68,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const warnings = []
const information = []
const theme = new LayoutConfig(json, true, "test")
const theme = new LayoutConfig(json, true)
{
// Legacy format checks
@ -217,12 +217,17 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson>{
convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const warnings = []
const errors = []
if(json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)){
errors.push("The theme "+json.id+" has no 'layers' defined ("+context+")")
}
if(json.socialImage === ""){
warnings.push("Social image for theme "+json.id+" is the emtpy string")
}
return {
result :json,
warnings
warnings,
errors
};
}
}
@ -231,8 +236,8 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
constructor() {
super("Various consistency checks on the raw JSON",
new OverrideShadowingCheck(),
new MiscThemeChecks()
new MiscThemeChecks(),
new OverrideShadowingCheck()
);
}

View file

@ -13,9 +13,10 @@ export default interface PointRenderingConfigJson {
/**
* All the locations that this point should be rendered at.
* Using `location: ["point", "centroid"] will always render centerpoint
* Using `location: ["point", "centroid"] will always render centerpoint.
* 'projected_centerpoint' will show an item on the line itself, near the middle of the line. (LineStrings only)
*/
location: ("point" | "centroid" | "start" | "end" | string)[]
location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | string)[]
/**
* The icon for an element.

View file

@ -18,9 +18,12 @@ export default interface UnitConfigJson {
export interface ApplicableUnitJson {
/**
* The canonical value which will be added to the text.
* The canonical value which will be added to the value in OSM.
* e.g. "m" for meters
* If the user inputs '42', the canonical value will be added and it'll become '42m'
* If the user inputs '42', the canonical value will be added and it'll become '42m'.
*
* Important: often, _no_ canonical values are expected, e.g. in the case of 'maxspeed' where 'km/h' is the default.
* In this case, an empty string should be used
*/
canonicalDenomination: string,
/**

View file

@ -127,12 +127,16 @@ export default class LayerConfig extends WithContextLoader {
idKey: json.source["idKey"]
},
Constants.priviliged_layers.indexOf(this.id) > 0,
json.id
);
this.allowSplit = json.allowSplit ?? false;
this.name = Translations.T(json.name, translationContext + ".name");
if(json.units!==undefined && !Array.isArray(json.units)){
throw "At "+context+".units: the 'units'-section should be a list; you probably have an object there"
}
this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`)))
if (json.description !== undefined) {

View file

@ -56,9 +56,17 @@ export default class LayoutConfig {
public readonly usedImages: string[]
public readonly extraLink?: ExtraLinkConfig
constructor(json: LayoutConfigJson, official = true, context?: string) {
public readonly definedAtUrl? : string;
public readonly definitionRaw?: string;
constructor(json: LayoutConfigJson, official = true,options?: {
definedAtUrl?: string,
definitionRaw?: string
}) {
this.official = official;
this.id = json.id;
this.definedAtUrl = options?.definedAtUrl
this.definitionRaw = options?.definitionRaw
if (official) {
if (json.id.toLowerCase() !== json.id) {
throw "The id of a theme should be lowercase: " + json.id
@ -67,11 +75,7 @@ export default class LayoutConfig {
throw "The id of a theme should match [a-z0-9-_]*: " + json.id
}
}
if(context === undefined){
context = this.id
}else{
context = context + "." + this.id;
}
const context = this.id
this.maintainer = json.maintainer;
this.credits = json.credits;
this.version = json.version;

View file

@ -15,8 +15,8 @@ import {VariableUiElement} from "../../UI/Base/VariableUIElement";
export default class PointRenderingConfig extends WithContextLoader {
private static readonly allowed_location_codes = new Set<string>(["point", "centroid", "start", "end"])
public readonly location: Set<"point" | "centroid" | "start" | "end" | string>
private static readonly allowed_location_codes = new Set<string>(["point", "centroid", "start", "end","projected_centerpoint"])
public readonly location: Set<"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string>
public readonly icon: TagRenderingConfig;
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[];

View file

@ -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;

View file

@ -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) {

View file

@ -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="
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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]

View file

@ -17,7 +17,10 @@ export default class LicensePicker extends DropDown<string> {
{value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone()},
{value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone()}
],
state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource<string>("CC0")
state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource<string>("CC0"),
{
select_class:"w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200"
}
)
this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left");
}

View file

@ -90,10 +90,10 @@ 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 => {
const params = [
["z", currentLocation?.zoom],
@ -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)
]);
}));

View file

@ -14,6 +14,8 @@ import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
import {InputElement} from "../Input/InputElement";
import CheckBoxes, {CheckBox} from "../Input/Checkboxes";
import {SubtleButton} from "../Base/SubtleButton";
import LZString from "lz-string";
export default class ShareScreen extends Combine {
@ -24,14 +26,6 @@ export default class ShareScreen extends Combine {
const optionCheckboxes: InputElement<boolean>[] = []
const optionParts: (UIEventSource<string>)[] = [];
function check() {
return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;");
}
function nocheck() {
return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;");
}
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
optionCheckboxes.push(includeLocation);
@ -49,6 +43,7 @@ export default class ShareScreen extends Combine {
} else {
return null;
}
}, [currentLocation]));
@ -119,6 +114,9 @@ export default class ShareScreen extends Combine {
}
if(layout.definitionRaw !== undefined){
optionParts.push(new UIEventSource("userlayout="+(layout.definedAtUrl ?? layout.id)))
}
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
@ -126,13 +124,21 @@ export default class ShareScreen extends Combine {
const host = window.location.host;
let path = window.location.pathname;
path = path.substr(0, path.lastIndexOf("/"));
let literalText = `https://${host}${path}/${layout.id.toLowerCase()}`
let id = layout.id.toLowerCase()
if(layout.definitionRaw !== undefined){
id="theme.html"
}
let literalText = `https://${host}${path}/${id}`
let hash = ""
if(layout.definedAtUrl === undefined && layout.definitionRaw !== undefined){
hash = "#"+ LZString.compressToBase64( Utils.MinifyJSON(layout.definitionRaw))
}
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
if (parts.length === 0) {
return literalText;
return literalText + hash;
}
return literalText + "?" + parts.join("&");
return literalText + "?" + parts.join("&") + hash;
}, optionParts);
@ -184,13 +190,27 @@ export default class ShareScreen extends Combine {
});
let downloadThemeConfig: BaseUIElement = undefined;
if(layout.definitionRaw !== undefined){
downloadThemeConfig = new SubtleButton(Svg.download_svg(), new Combine([
tr.downloadCustomTheme,
tr.downloadCustomThemeHelp.SetClass("subtle")
]).onClick(() => {
Utils.offerContentsAsDownloadableFile(layout.definitionRaw, layout.id+".mapcomplete-theme-definition.json", {
mimetype:"application/json"
})
})
.SetClass("flex flex-col"))
}
super([
tr.intro.Clone(),
tr.intro,
link,
new VariableUiElement(linkStatus),
tr.addToHomeScreen.Clone(),
tr.embedIntro.Clone(),
downloadThemeConfig,
tr.addToHomeScreen,
tr.embedIntro,
options,
iframeCode,
])

View file

@ -191,7 +191,7 @@ export default class SimpleAddUI extends Toggle {
preset.icon(),
new Combine([
title.SetClass("font-bold"),
Translations.WT(preset.description)?.FirstSentence()
preset.description?.FirstSentence()
]).SetClass("flex flex-col")
)
}
@ -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
continue;
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 layer can never be toggled on in any case, so we skip the presets
continue;
}
}
if (layer.layerDef.name === undefined) {
// this is a parlty hidden layer
continue;
}
const presets = layer.layerDef.presets;
for (const preset of presets) {

View file

@ -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,36 +26,53 @@ 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) {
seed.translations[ln] = seed.translations["en"]
}
}
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) ?? [])
.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))
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"))
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([
new Title(
Translations.t.translations.activateButton,
Translations.t.translations.activateButton,
),
new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator),
t.help,
@ -63,15 +82,18 @@ class TranslatorsPanelContent extends Combine {
.onClick(() => {
Locale.showLinkToWeblate.setData(false)
}),
new VariableUiElement(Locale.language.map(ln => {
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")
)
}))
@ -83,38 +105,37 @@ class TranslatorsPanelContent extends Combine {
export default class TranslatorsPanel extends Toggle {
constructor(state: { layoutToUse: LayoutConfig, isTranslator: UIEventSource<boolean> }, iconStyle?: string) {
const t = Translations.t.translations
super(
new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
).SetClass("flex flex-col"),
new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() => Locale.showLinkToWeblate.setData(true)),
Locale.showLinkToWeblate
Locale.showLinkToWeblate
)
this.SetClass("hidden-on-mobile")
}
public static MissingTranslationsFor(layout: LayoutConfig) : {completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number} {
public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } {
let total = 0
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;

View file

@ -15,6 +15,7 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes";
import Loading from "../Base/Loading";
export class ImageUploadFlow extends Toggle {
@ -138,16 +139,16 @@ export class ImageUploadFlow extends Toggle {
if (l == 0) {
return undefined
}
return t.uploadFailed.Clone().SetClass("alert");
return new Loading(t.uploadFailed).SetClass("alert");
})),
new VariableUiElement(uploadedCount.map(l => {
if (l == 0) {
return undefined;
}
if (l == 1) {
return t.uploadDone.Clone().SetClass("thanks");
return t.uploadDone.Clone().SetClass("thanks block");
}
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks")
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks block")
})),
fileSelector,

View file

@ -4,8 +4,12 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import ValidatedTextField from "../Input/ValidatedTextField";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import Title from "../Base/Title";
import {FixedUiElement} from "../Base/FixedUiElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
export class AskMetadata extends Combine implements FlowStep<{
features: any[],
@ -25,7 +29,7 @@ export class AskMetadata extends Combine implements FlowStep<{
public readonly IsValid: UIEventSource<boolean>;
constructor(params: ({ features: any[], theme: string })) {
const t = Translations.t.importHelper.askMetadata
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
value: LocalStorageSource.Get("import-helper-introduction-text"),
inputStyle: "width: 100%"
@ -42,28 +46,39 @@ export class AskMetadata extends Combine implements FlowStep<{
})
super([
new Title("Set metadata"),
"Before adding " + params.features.length + " notes, please provide some extra information.",
"Please, write an introduction for someone who sees the note",
new Title(t.title),
t.intro.Subs({count: params.features.length}),
t.giveDescription,
introduction.SetClass("w-full border border-black"),
"What is the source of this data? If 'source' is set in the feature, this value will be ignored",
source.SetClass("w-full border border-black"),
"On what wikipage can one find more information about this import?",
t.giveSource,
source.SetClass("w-full border border-black"),
t.giveWikilink ,
wikilink.SetClass("w-full border border-black"),
new VariableUiElement(wikilink.GetValue().map(wikilink => {
try{
const url = new URL(wikilink)
if(url.hostname.toLowerCase() !== "wiki.openstreetmap.org"){
return new FixedUiElement("Expected a link to wiki.openstreetmap.org").SetClass("alert");
return t.shouldBeOsmWikilink.SetClass("alert");
}
if(url.pathname.toLowerCase() === "/wiki/main_page"){
return new FixedUiElement("Nope, the home page isn't allowed either. Enter the URL of a proper wikipage documenting your import").SetClass("alert");
return t.shouldNotBeHomepage.SetClass("alert");
}
}catch(e){
return new FixedUiElement("Not a valid URL").SetClass("alert")
return t.shouldBeUrl.SetClass("alert")
}
}))
})),
t.orDownload,
new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading("Preparing your download",
async ( ) => {
const geojson = {
type:"FeatureCollection",
features: params.features
}
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+params.theme+".geojson",{
mimetype: "application/vnd.geo+json"
})
})
]);
this.SetClass("flex flex-col")

View file

@ -21,6 +21,7 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import * as known_layers from "../../assets/generated/known_layers.json"
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
import Translations from "../i18n/Translations";
/**
* Filters out points for which the import-note already exists, to prevent duplicates
@ -28,11 +29,11 @@ import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson";
export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }> {
public IsValid: UIEventSource<boolean>
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[] , theme: string}>
public Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, features: any[], theme: string }>
constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) {
constructor(state, params: { bbox: BBox, layer: LayerConfig, features: any[], theme: string }) {
const t = Translations.t.importHelper.compareToAlreadyExistingNotes
const layerConfig = known_layers.layers.filter(l => l.id === params.layer.id)[0]
if (layerConfig === undefined) {
console.error("WEIRD: layer not found in the builtin layer overview")
@ -45,7 +46,7 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
layerDef: importLayer
}
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
allNotesWithinBbox.features.map(f => MetaTagging.addMetatags(
f,
{
@ -63,7 +64,6 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
)
)
const alreadyOpenImportNotes = new FilteringFeatureSource(state, undefined, allNotesWithinBbox)
alreadyOpenImportNotes.features.addCallbackD(features => console.log("Loaded and filtered features are", features))
const map = Minimap.createMiniMap()
map.SetClass("w-full").SetStyle("height: 500px")
@ -99,43 +99,46 @@ export class CompareToAlreadyExistingNotes extends Combine implements FlowStep<{
})
super([
new Title("Compare with already existing 'to-import'-notes"),
new Title(t.titleLong),
new VariableUiElement(
alreadyOpenImportNotes.features.map(notesWithImport => {
if(allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined){
return new FixedUiElement("Loading notes failed: "+allNotesWithinBbox.state.data["error"] )
if (allNotesWithinBbox.state.data !== undefined && allNotesWithinBbox.state.data["error"] !== undefined) {
t.loadingFailed.Subs(allNotesWithinBbox.state.data)
}
if (allNotesWithinBbox.features.data === undefined || allNotesWithinBbox.features.data.length === 0) {
return new Loading("Fetching notes from OSM")
return new Loading(t.loading)
}
if (notesWithImport.length === 0) {
return new FixedUiElement("No previous import notes found").SetClass("thanks")
return t.noPreviousNotesFound.SetClass("thanks")
}
return new Combine([
"The red elements on the next map are all the data points from your dataset. There are <b>"+params.features.length+"</b> elements in your dataset.",
t.mapExplanation.Subs(params.features),
map,
new VariableUiElement( partitionedImportPoints.map(({noNearby, hasNearby}) => {
if(noNearby.length === 0){
// Nothing can be imported
return new FixedUiElement("All of the proposed points have (or had) an import note already").SetClass("alert w-full block").SetStyle("padding: 0.5rem")
}
if(hasNearby.length === 0){
// All points can be imported
return new FixedUiElement("All of the proposed points have don't have a previous import note nearby").SetClass("thanks w-full block").SetStyle("padding: 0.5rem")
}
return new Combine([
new FixedUiElement(hasNearby.length+" points do have an already existing import note within "+maxDistance.data+" meter.").SetClass("alert"),
"These data points will <i>not</i> be imported and are shown as red dots on the map below",
comparisonMap.SetClass("w-full")
]).SetClass("w-full")
new VariableUiElement(partitionedImportPoints.map(({noNearby, hasNearby}) => {
if (noNearby.length === 0) {
// Nothing can be imported
return t.completelyImported.SetClass("alert w-full block").SetStyle("padding: 0.5rem")
}
if (hasNearby.length === 0) {
// All points can be imported
return t.nothingNearby.SetClass("thanks w-full block").SetStyle("padding: 0.5rem")
}
return new Combine([
t.someNearby.Subs({
hasNearby: hasNearby.length,
distance: maxDistance.data
}).SetClass("alert"),
t.wontBeImported,
comparisonMap.SetClass("w-full")
]).SetClass("w-full")
}))
]).SetClass("flex flex-col")
}, [allNotesWithinBbox.features, allNotesWithinBbox.state])

View file

@ -2,45 +2,31 @@ import Combine from "../Base/Combine";
import {FlowStep} from "./FlowStep";
import {UIEventSource} from "../../Logic/UIEventSource";
import Link from "../Base/Link";
import {FixedUiElement} from "../Base/FixedUiElement";
import CheckBoxes from "../Input/Checkboxes";
import Title from "../Base/Title";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import Translations from "../i18n/Translations";
export class ConfirmProcess extends Combine implements FlowStep<{ features: any[], theme: string }> {
public IsValid: UIEventSource<boolean>
public Value: UIEventSource<{ features: any[],theme: string }>
public Value: UIEventSource<{ features: any[], theme: string }>
constructor(v: { features: any[], theme: string }) {
constructor(v: { features: any[], theme: string }) {
const t = Translations.t.importHelper.confirmProcess;
const elements = [
new Link(t.readImportGuidelines, "https://wiki.openstreetmap.org/wiki/Import_guidelines", true),
t.contactedCommunity,
t.licenseIsCompatible,
t.wikipageIsMade
]
const toConfirm = new CheckBoxes(elements);
const toConfirm = [
new Combine(["I have read the ", new Link("import guidelines on the OSM wiki", "https://wiki.openstreetmap.org/wiki/Import_guidelines", true)]),
new FixedUiElement("I did contact the (local) community about this import"),
new FixedUiElement("The license of the data to import allows it to be imported into OSM. They are allowed to be redistributed commercially, with only minimal attribution"),
new FixedUiElement("The process is documented on the OSM-wiki (you'll need this link later)")
];
const licenseClear = new CheckBoxes(toConfirm)
super([
new Title("Did you go through the import process?"),
licenseClear,
new FixedUiElement("Alternatively, you can download the dataset to import directly"),
new SubtleButton(Svg.download_svg(), "Download geojson").OnClickWithLoading("Preparing your download",
async ( ) => {
const geojson = {
type:"FeatureCollection",
features: v.features
}
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), "prepared_import_"+v.theme+".geojson",{
mimetype: "application/vnd.geo+json"
})
})
new Title(t.titleLong),
toConfirm,
]);
this.SetClass("link-underline")
this.IsValid = licenseClear.GetValue().map(selected => toConfirm.length == selected.length)
this.IsValid = toConfirm.GetValue().map(selected => elements.length == selected.length)
this.Value = new UIEventSource<{ features: any[], theme: string }>(v)
}
}

View file

@ -7,7 +7,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import Constants from "../../Models/Constants";
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {FlowStep} from "./FlowStep";
import Loading from "../Base/Loading";
import {SubtleButton} from "../Base/SubtleButton";
@ -28,6 +27,7 @@ import * as import_candidate from "../../assets/layers/import_candidate/import_c
import {GeoOperations} from "../../Logic/GeoOperations";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import {ImportUtils} from "./ImportUtils";
import Translations from "../i18n/Translations";
/**
* Given the data to import, the bbox and the layer, will query overpass for similar items
@ -47,6 +47,27 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
const toImport: {features: any[]} = params;
let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle")
const cacheAge = new UIEventSource<number>(undefined);
function loadDataFromOverpass(){
// Load the data!
const url = Constants.defaultOverpassUrls[1]
const relationTracker = new RelationsTracker()
const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource<number>(180), relationTracker, true)
console.log("Loading from overpass!")
overpassStatus.setData("running")
overpass.queryGeoJson(bbox).then(
([data, date]) => {
console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date);
overpassStatus.setData("success")
fromLocalStorage.setData([data, date])
},
(error) => {
overpassStatus.setData({error})
})
}
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, {
whenLoaded: (v) => {
@ -63,22 +84,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
}
cacheAge.setData(-1)
}
// Load the data!
const url = Constants.defaultOverpassUrls[1]
const relationTracker = new RelationsTracker()
const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource<number>(180), relationTracker, true)
console.log("Loading from overpass!")
overpassStatus.setData("running")
overpass.queryGeoJson(bbox).then(
([data, date]) => {
console.log("Received overpass-data: ", data.features.length, "features are loaded at ", date);
overpassStatus.setData("success")
fromLocalStorage.setData([data, date])
},
(error) => {
overpassStatus.setData({error})
})
loadDataFromOverpass()
}
});
@ -166,7 +172,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
return osmData.features.filter(f =>
toImport.features.some(imp =>
maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))))
}, [nearbyCutoff.GetValue()]), false);
}, [nearbyCutoff.GetValue().stabilized(500)]), false);
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number));
// Featuresource showing OSM-features which are nearby a toImport-feature
@ -190,6 +196,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
features: toImportWithNearby
})
const t = Translations.t.importHelper.conflationChecker
const conflationMaps = new Combine([
new VariableUiElement(
@ -197,7 +204,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
if (geojson === undefined) {
return undefined;
}
return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => {
return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(() => {
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", {
mimetype: "application/json+geo"
})
@ -208,43 +215,57 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea
return undefined;
}
if (age < 0) {
return new FixedUiElement("Cache was expired")
return t.cacheExpired
}
return new FixedUiElement("Loaded data is from the cache and is " + Utils.toHumanTime(age) + " old")
return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}),
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
.onClick(loadDataFromOverpass)
.SetClass("h-12")
])
})),
new Title("Live data on OSM"),
"The "+toImport.features.length+" red elements on the following map are all your import candidates.",
new VariableUiElement(geojson.map(geojson => new FixedUiElement((geojson?.features?.length ?? "No") + " elements are loaded from OpenStreetMap which match the layer "+layer.id+". Zooming in might be needed to show them"))),
new Title(t.titleLive),
t.importCandidatesCount.Subs({count:toImport.features.length }),
new VariableUiElement(geojson.map(geojson => {
if(geojson?.features?.length === undefined || geojson?.features?.length === 0){
return t.nothingLoaded.Subs(layer).SetClass("alert")
}
return new Combine([
t.osmLoaded.Subs({count: geojson.features.length, name: layer.name}),
])
})),
osmLiveData,
new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => "" + l.zoom))]).SetClass("flex"),
new Title("Nearby features"),
new Combine(["The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"),
new VariableUiElement(osmLiveData.location.map(location => {
return t.zoomIn.Subs({needed:zoomLevel, current: location.zoom })
} )),
new Title(t.titleNearby),
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
new VariableUiElement(toImportWithNearby.features.map(feats =>
new FixedUiElement("The "+ feats.length +" red elements on the following map will <b>not</b> be imported!").SetClass("alert"))),
"Set the range to 0 or 1 if you want to import them all",
t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))),
t.setRangeToZero,
matchedFeaturesMap]).SetClass("flex flex-col")
super([
new Title("Comparison with existing data"),
new Title(t.title),
new VariableUiElement(overpassStatus.map(d => {
if (d === "idle") {
return new Loading("Checking local storage...")
}
if (d["error"] !== undefined) {
return new FixedUiElement("Could not load latest data from overpass: " + d["error"]).SetClass("alert")
return new Loading(t.states.idle)
}
if (d === "running") {
return new Loading("Querying overpass...")
return new Loading(t.states.running)
}
if (d["error"] !== undefined) {
return t.states.error.Subs({error: d["error"]}).SetClass("alert")
}
if (d === "cached") {
return conflationMaps
}
if (d === "success") {
return conflationMaps
}
return new FixedUiElement("Unexpected state " + d).SetClass("alert")
return t.states.unexpected.Subs({state: d}).SetClass("alert")
}))
])

View file

@ -8,47 +8,72 @@ import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import {Translation} from "../i18n/Translation";
export class CreateNotes extends Combine {
public static createNoteContentsUi(feature: {properties: any, geometry: {coordinates: [number,number]}},
options: {wikilink: string; intro: string; source: string, theme: string }
): (Translation | string)[]{
const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source
delete feature.properties["source"]
delete feature.properties["src"]
let extraNote = ""
if (feature.properties["note"]) {
extraNote = feature.properties["note"] + "\n"
delete feature.properties["note"]
}
const tags: string [] = []
for (const key in feature.properties) {
if (feature.properties[key] === null || feature.properties[key] === undefined) {
console.warn("Null or undefined key for ", feature.properties)
continue
}
if (feature.properties[key] === "") {
continue
}
tags.push(key + "=" + (feature.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
}
const lat = feature.geometry.coordinates[1]
const lon = feature.geometry.coordinates[0]
const note = Translations.t.importHelper.noteParts
return [
options.intro,
extraNote,
note.datasource.Subs({source: src}),
note.wikilink.Subs(options),
'',
note.importEasily,
`https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
...tags]
}
public static createNoteContents(feature: {properties: any, geometry: {coordinates: [number,number]}},
options: {wikilink: string; intro: string; source: string, theme: string }
): string[]{
return CreateNotes.createNoteContentsUi(feature, options).map(trOrStr => {
if(typeof trOrStr === "string"){
return trOrStr
}
return trOrStr.txt
})
}
constructor(state: { osmConnection: OsmConnection }, v: { features: any[]; wikilink: string; intro: string; source: string, theme: string }) {
const t = Translations.t.importHelper.createNotes;
const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([])
const failed = new UIEventSource<string[]>([])
const currentNote = createdNotes.map(n => n.length)
for (const f of v.features) {
const src = f.properties["source"] ?? f.properties["src"] ?? v.source
delete f.properties["source"]
delete f.properties["src"]
let extraNote = ""
if (f.properties["note"]) {
extraNote = f.properties["note"] + "\n"
delete f.properties["note"]
}
const tags: string [] = []
for (const key in f.properties) {
if (f.properties[key] === null || f.properties[key] === undefined) {
console.warn("Null or undefined key for ", f.properties)
continue
}
if (f.properties[key] === "") {
continue
}
tags.push(key + "=" + (f.properties[key]+"").replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n"))
}
const lat = f.geometry.coordinates[1]
const lon = f.geometry.coordinates[0]
const text = [v.intro,
extraNote,
"Source: " + src,
'More information at ' + v.wikilink,
'',
'Import this point easily with',
`https://mapcomplete.osm.be/${v.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
...tags].join("\n")
const text = CreateNotes.createNoteContents(f, v).join("\n")
state.osmConnection.openNote(
lat, lon, text)
@ -62,13 +87,19 @@ export class CreateNotes extends Combine {
}
super([
new Title("Creating notes"),
"Hang on while we are importing...",
new Title(t.title),
t.loading ,
new Toggle(
new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported <b>" + count + "</b> out of " + v.features.length + " notes")))),
new Loading(new VariableUiElement(currentNote.map(count => t.creating.Subs({
count, total: v.features.length
}
)))),
new Combine([
new FixedUiElement("All done!").SetClass("thanks"),
new SubtleButton(Svg.note_svg(), "Inspect the progress of your notes in the 'import_viewer'", {
Svg.party_svg().SetClass("w-24"),
t.done.Subs({count: v.features.length}).SetClass("thanks"),
new SubtleButton(Svg.note_svg(),
t.openImportViewer , {
url: "import_viewer.html"
})
]

View file

@ -25,15 +25,15 @@ export class FlowPanelFactory<T> {
this._stepNames = stepNames;
}
public static start<TOut>(name: string | BaseUIElement, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
return new FlowPanelFactory(step, [], [name])
public static start<TOut>(name:{title: BaseUIElement}, step: FlowStep<TOut>): FlowPanelFactory<TOut> {
return new FlowPanelFactory(step, [], [name.title])
}
public then<TOut>(name: string | BaseUIElement, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
public then<TOut>(name: string | {title: BaseUIElement}, construct: ((t: T) => FlowStep<TOut>)): FlowPanelFactory<TOut> {
return new FlowPanelFactory<TOut>(
this._initial,
this._steps.concat([construct]),
this._stepNames.concat([name])
this._stepNames.concat([name["title"] ?? name])
)
}
@ -120,11 +120,11 @@ export class FlowPanel<T> extends Toggle {
isError.setData(true)
}
}),
"Select a valid value to continue",
new SubtleButton(Svg.invalid_svg(), t.notValid),
initial.IsValid
),
new Toggle(
new FixedUiElement("Something went wrong...").SetClass("alert"),
t.error.SetClass("alert"),
undefined,
isError),
]).SetClass("flex w-full justify-end space-x-2"),

View file

@ -7,7 +7,7 @@ import MinimapImplementation from "../Base/MinimapImplementation";
import Translations from "../i18n/Translations";
import {FlowPanelFactory} from "./FlowStep";
import {RequestFile} from "./RequestFile";
import {PreviewPanel} from "./PreviewPanel";
import {PreviewAttributesPanel} from "./PreviewPanel";
import ConflationChecker from "./ConflationChecker";
import {AskMetadata} from "./AskMetadata";
import {ConfirmProcess} from "./ConfirmProcess";
@ -26,20 +26,20 @@ import SelectTheme from "./SelectTheme";
export default class ImportHelperGui extends LeftIndex {
constructor() {
const state = new UserRelatedState(undefined)
const t = Translations.t.importHelper;
const {flow, furthestStep, titles} =
FlowPanelFactory
.start("Introduction", new Introdution())
.then("Login", _ => new LoginToImport(state))
.then("Select file", _ => new RequestFile())
.then("Inspect attributes", geojson => new PreviewPanel(state, geojson))
.then("Inspect data", geojson => new MapPreview(state, geojson))
.then("Select theme", v => new SelectTheme(v))
.then("Compare with open notes", v => new CompareToAlreadyExistingNotes(state, v))
.then("Compare with existing data", v => new ConflationChecker(state, v))
.then("License and community check", v => new ConfirmProcess(v))
.then("Metadata", (v) => new AskMetadata(v))
.finish("Note creation", v => new CreateNotes(state, v));
.start(t.introduction, new Introdution())
.then(t.login, _ => new LoginToImport(state))
.then(t.selectFile, _ => new RequestFile())
.then(t.previewAttributes, geojson => new PreviewAttributesPanel(state, geojson))
.then(t.mapPreview, geojson => new MapPreview(state, geojson))
.then(t.selectTheme, v => new SelectTheme(v))
.then(t.compareToAlreadyExistingNotes, v => new CompareToAlreadyExistingNotes(state, v))
.then(t.conflationChecker, v => new ConflationChecker(state, v))
.then(t.confirmProcess, v => new ConfirmProcess(v))
.then(t.askMetadata, (v) => new AskMetadata(v))
.finish(t.createNotes.title, v => new CreateNotes(state, v));
const toc = new List(
titles.map((title, i) => new VariableUiElement(furthestStep.map(currentStep => {
@ -58,11 +58,11 @@ export default class ImportHelperGui extends LeftIndex {
, true)
const leftContents: BaseUIElement[] = [
new SubtleButton(undefined, "Inspect your preview imports", {
new SubtleButton(undefined, t.gotoImportViewer, {
url: "import_viewer.html"
}),
toc,
new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting),
new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting),
LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"),
].map(el => el?.SetClass("pl-4"))

View file

@ -3,18 +3,43 @@ import {FlowStep} from "./FlowStep";
import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import {CreateNotes} from "./CreateNotes";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class Introdution extends Combine implements FlowStep<void> {
readonly IsValid: UIEventSource<boolean> = new UIEventSource<boolean>(true);
readonly Value: UIEventSource<void> = new UIEventSource<void>(undefined);
readonly IsValid: UIEventSource<boolean>;
readonly Value: UIEventSource<void>;
constructor() {
const example = CreateNotes.createNoteContentsUi({
properties:{
"some_key":"some_value",
"note":"a note in the original dataset"
},
geometry:{
coordinates: [3.4,51.2]
}
}, {
wikilink: "https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>",
intro: "There might be an XYZ here",
theme: "theme",
source: "source of the data"
}).map(el => el === "" ? new FixedUiElement("").SetClass("block") : el)
super([
new Title(Translations.t.importHelper.title),
Translations.t.importHelper.description,
Translations.t.importHelper.importFormat,
new Title(Translations.t.importHelper.introduction.title),
Translations.t.importHelper.introduction.description,
Translations.t.importHelper.introduction.importFormat,
new Combine(
[new Combine(
example
).SetClass("flex flex-col")
] ).SetClass("literal-code")
]);
this.SetClass("flex flex-col")
this. IsValid= new UIEventSource<boolean>(true);
this. Value = new UIEventSource<void>(undefined);
}
}

View file

@ -21,7 +21,7 @@ export default class LoginToImport extends Combine implements FlowStep<UserRelat
private static readonly whitelist = [15015689];
constructor(state: UserRelatedState) {
const t = Translations.t.importHelper
const t = Translations.t.importHelper.login
const check = new CheckBoxes([new VariableUiElement(state.osmConnection.userDetails.map(ud => t.loginIsCorrect.Subs(ud)))])
const isValid = state.osmConnection.userDetails.map(ud =>
LoginToImport.whitelist.indexOf(ud.uid) >= 0 || ud.csCount >= Constants.userJourney.importHelperUnlock)

View file

@ -12,16 +12,16 @@ import List from "../Base/List";
import CheckBoxes from "../Input/Checkboxes";
/**
* Shows the data to import on a map, asks for the correct layer to be selected
* Shows the attributes by value, requests to check them of
*/
export class PreviewPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> {
export class PreviewAttributesPanel extends Combine implements FlowStep<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }> {
public readonly IsValid: UIEventSource<boolean>;
public readonly Value: UIEventSource<{ features: { properties: any, geometry: { coordinates: [number, number] } }[] }>
constructor(
state: UserRelatedState,
geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) {
const t = Translations.t.importHelper;
const t = Translations.t.importHelper.previewAttributes;
const propertyKeys = new Set<string>()
for (const f of geojson.features) {

View file

@ -10,12 +10,12 @@ import Title from "../Base/Title";
import {RadioButton} from "../Input/RadioButton";
import {And} from "../../Logic/Tags/And";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import Toggleable from "../Base/Toggleable";
import {BBox} from "../../Logic/BBox";
import BaseUIElement from "../BaseUIElement";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import List from "../Base/List";
import Translations from "../i18n/Translations";
export default class SelectTheme extends Combine implements FlowStep<{
features: any[],
@ -33,7 +33,7 @@ export default class SelectTheme extends Combine implements FlowStep<{
public readonly IsValid: UIEventSource<boolean>;
constructor(params: ({ features: any[], layer: LayerConfig, bbox: BBox, })) {
const t = Translations.t.importHelper.selectTheme
let options: InputElement<string>[] = AllKnownLayouts.layoutsList
.filter(th => th.layers.some(l => l.id === params.layer.id))
.filter(th => th.id !== "personal")
@ -69,15 +69,15 @@ export default class SelectTheme extends Combine implements FlowStep<{
})
super([
new Title("Select a theme"),
"All of the following themes will show the import notes. However, the note on OpenStreetMap can link to only one single theme. Choose which theme that the created notes will link to",
new Title(t.title),
t.intro,
themeRadios,
new VariableUiElement(applicablePresets.map(applicablePresets => {
if (themeRadios.GetValue().data === undefined) {
return undefined
}
if (applicablePresets === undefined || applicablePresets.length === 0) {
return new FixedUiElement("This theme has no presets loaded. As a result, imports won't work here").SetClass("alert")
return t.noMatchingPresets.SetClass("alert")
}
}, [themeRadios.GetValue()])),
@ -115,11 +115,14 @@ export default class SelectTheme extends Combine implements FlowStep<{
if (unmatched === undefined || unmatched.length === 0) {
return
}
const applicablePresetsOverview = applicablePresets.map(preset => new Combine([
preset.title.txt, "needs tags",
new FixedUiElement(preset.tags.map(t => t.asHumanString()).join(" & ")).SetClass("thanks")
]))
const t = Translations.t.importHelper.selectTheme
const applicablePresetsOverview = applicablePresets.map(preset =>
t.needsTags.Subs(
{title: preset.title,
tags:preset.tags.map(t => t.asHumanString()).join(" & ") })
.SetClass("thanks")
);
const unmatchedPanels: BaseUIElement[] = []
for (const feat of unmatched) {
@ -133,20 +136,16 @@ export default class SelectTheme extends Combine implements FlowStep<{
const missing = []
for (const {k, v} of tags) {
if (preset[k] === undefined) {
missing.push(
`Expected ${k}=${v}, but it is completely missing`
)
missing.push(t.missing.Subs({k,v}))
} else if (feat.properties[k] !== v) {
missing.push(
`Property with key ${k} does not have expected value ${v}; instead it is ${feat.properties}`
)
missing.push(t.misMatch.Subs({k, v, properties: feat.properties}))
}
}
if (missing.length > 0) {
parts.push(
new Combine([
new FixedUiElement(`Preset ${preset.title.txt} is not applicable:`),
t.notApplicable.Subs(preset),
new List(missing)
]).SetClass("flex flex-col alert")
)
@ -158,9 +157,9 @@ export default class SelectTheme extends Combine implements FlowStep<{
}
return new Combine([
new FixedUiElement(unmatched.length + " objects dont match any presets").SetClass("alert"),
t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"),
...applicablePresetsOverview,
new Toggleable(new Title("The following elements don't match any of the presets"),
new Toggleable(new Title(t.unmatchedTitle),
new Combine(unmatchedPanels))
]).SetClass("flex flex-col")

View file

@ -47,7 +47,7 @@ export class DropDown<T> extends InputElement<T> {
}
options = options ?? {}
options.select_class = options.select_class ?? 'bg-indigo-100 p-1 rounded hover:bg-indigo-200'
options.select_class = options.select_class ?? 'w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200'
{

View file

@ -9,7 +9,8 @@ export default class InputElementWrapper<T> extends InputElement<T> {
private readonly _inputElement: InputElement<T>;
private readonly _renderElement: BaseUIElement
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>, state: FeaturePipelineState) {
constructor(inputElement: InputElement<T>, translation: Translation, key: string,
tags: UIEventSource<any>, state: FeaturePipelineState) {
super()
this._inputElement = inputElement;
const mapping = new Map<string, BaseUIElement>()

View file

@ -250,13 +250,15 @@ class WikidataTextField extends TextFieldDef {
["subarg", "doc"],
[["removePrefixes", "remove these snippets of text from the start of the passed string to search"],
["removePostfixes", "remove these snippets of text from the end of the passed string to search"],
["instanceOf","A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"],
["notInstanceof","A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results"]
]
)])
]]),
new Title("Example usage"),
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`
\`\`\`json
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
@ -269,11 +271,29 @@ class WikidataTextField extends TextFieldDef {
"path",
"square",
"plaza",
]
],
"#": "Remove streets and parks from the search results:"
"notInstanceOf": ["Q79007","Q22698"]
}
]
}
\`\`\``
\`\`\`
Another example is to search for species and trees:
\`\`\`json
"freeform": {
"key": "species:wikidata",
"type": "wikidata",
"helperArgs": [
"species",
{
"instanceOf": [10884, 16521]
}]
}
\`\`\`
`
]));
}
@ -304,9 +324,9 @@ class WikidataTextField extends TextFieldDef {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
let searchFor = <string>(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
const options = args[1]
const options: any = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[]>options["removePrefixes"]
const postfixes = <string[]>options["removePostfixes"]
@ -325,10 +345,18 @@ class WikidataTextField extends TextFieldDef {
}
}
let instanceOf : number[] = Utils.NoNull((options?.instanceOf ?? []).map(i => Wikidata.QIdToNumber(i)))
let notInstanceOf : number[] = Utils.NoNull((options?.notInstanceOf ?? []).map(i => Wikidata.QIdToNumber(i)))
console.log("Instance of", instanceOf)
return new WikidataSearchBox({
value: currentValue,
searchText: new UIEventSource<string>(searchFor)
searchText: new UIEventSource<string>(searchFor),
instanceOf,
notInstanceOf
})
}
}
@ -424,7 +452,7 @@ class UrlTextfieldDef extends TextFieldDef {
reformat(str: string): string {
try {
let url: URL
str = str.toLowerCase()
// str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
if (!str.startsWith("http://") && !str.startsWith("https://") && !str.startsWith("http:")) {
url = new URL("https://" + str)
} else {

View file

@ -202,7 +202,7 @@ export default class DeleteWizard extends Toggle {
private static generateDeleteTagRenderingConfig(softDeletionTags: TagsFilter,
nonDeleteOptions: { if: TagsFilter; then: Translation }[],
extraDeleteReasons: { explanation: Translation; changesetMessage: string }[],
currentTags: any) {
currentTags: any): TagRenderingConfig {
const t = Translations.t.delete
nonDeleteOptions = nonDeleteOptions ?? []
let softDeletionTagsStr = []

View file

@ -194,7 +194,7 @@ ${Utils.special_visualizations_importRequirementDocs}
importFlow,
isImported
),
t.zoomInMore.SetClass("alert"),
t.zoomInMore.SetClass("alert block"),
state.locationControl.map(l => l.zoom >= 18)
),
pleaseLoginButton,
@ -613,7 +613,7 @@ export class ImportPointButton extends AbstractImportButton {
icon: () => new Img(args.icon),
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
name: args.text,
title: Translations.WT(args.text),
title: Translations.T(args.text),
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
boundsFactor: 3
}

View file

@ -86,7 +86,7 @@ export default class MoveWizard extends Toggle {
moveReason.setData(reason)
moveButton = new SubtleButton(
reason.icon.SetStyle("height: 1.5rem; width: 1.5rem;"),
Translations.WT(reason.invitingText)
Translations.T(reason.invitingText)
).onClick(() => {
currentStep.setData("pick_location")
})

View file

@ -11,7 +11,7 @@ import {SaveButton} from "./SaveButton";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation";
import {Translation, TypedTranslation} from "../i18n/Translation";
import Constants from "../../Models/Constants";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
@ -51,7 +51,7 @@ export default class TagRenderingQuestion extends Combine {
const applicableMappingsSrc =
UIEventSource.ListStabilized(tags.map(tags => {
const applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = []
const applicableMappings: { if: TagsFilter, icon?: string, then: TypedTranslation<object>, ifnot?: TagsFilter, addExtraTags: Tag[] }[] = []
for (const mapping of configuration.mappings ?? []) {
if (mapping.hideInAnswer === true) {
continue
@ -158,7 +158,7 @@ export default class TagRenderingQuestion extends Combine {
private static GenerateInputElement(
state,
configuration: TagRenderingConfig,
applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[],
applicableMappings: { if: TagsFilter, then: TypedTranslation<object>, icon?: string, ifnot?: TagsFilter, addExtraTags: Tag[] }[],
applicableUnit: Unit,
tagsSource: UIEventSource<any>,
feedback: UIEventSource<Translation>
@ -168,7 +168,7 @@ export default class TagRenderingQuestion extends Combine {
const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback);
const hasImages = applicableMappings.findIndex(mapping => mapping.then.icon !== undefined) >= 0
const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0
let inputEls: InputElement<TagsFilter>[];
const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined)
@ -207,7 +207,7 @@ export default class TagRenderingQuestion extends Combine {
applicableMappings.map((mapping, i) => {
return {
value: new And([mapping.if, ...allIfNotsExcept(i)]),
shown: Translations.WT(mapping.then)
shown: mapping.then.Subs(tagsSource.data)
}
})
)
@ -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
);

View file

@ -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

View file

@ -46,6 +46,8 @@ import {LoginToggle} from "./Popup/LoginButton";
import {start} from "repl";
import {SubstitutedTranslation} from "./SubstitutedTranslation";
import {TextField} from "./Input/TextField";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
import {Translation} from "./i18n/Translation";
export interface SpecialVisualization {
funcName: string,
@ -159,19 +161,19 @@ class CloseNoteButton implements SpecialVisualization {
tags.ping()
})
})
if((params.minZoom??"") !== "" && !isNaN(Number(params.minZoom))){
closeButton = new Toggle(
if ((params.minZoom ?? "") !== "" && !isNaN(Number(params.minZoom))) {
closeButton = new Toggle(
closeButton,
params.zoomButton ?? "",
state. locationControl.map(l => l.zoom >= Number(params.minZoom))
state.locationControl.map(l => l.zoom >= Number(params.minZoom))
)
}
return new LoginToggle(new Toggle(
t.isClosed.SetClass("thanks"),
closeButton,
isClosed
), t.loginToClose, state)
}
@ -180,7 +182,7 @@ class CloseNoteButton implements SpecialVisualization {
export default class SpecialVisualizations {
public static specialVisualizations : SpecialVisualization[] = SpecialVisualizations.init()
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.init()
public static HelpMessage() {
@ -207,28 +209,28 @@ export default class SpecialVisualizations {
));
return new Combine([
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax",4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}, one can also write`,
new FixedUiElement(JSON.stringify({
render: {
special:{
type: "some_special_visualisation",
"argname": "some_arg",
"message":{
en:"some other really long message",
nl: "een boodschap in een andere taal"
},
"other_arg_name":"more args"
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax", 4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}, one can also write`,
new FixedUiElement(JSON.stringify({
render: {
special: {
type: "some_special_visualisation",
"argname": "some_arg",
"message": {
en: "some other really long message",
nl: "een boodschap in een andere taal"
},
"other_arg_name": "more args"
}
}
}
})).SetClass("code")
]).SetClass("flex flex-col"),
})).SetClass("code")
]).SetClass("flex flex-col"),
...helpTexts
]
).SetClass("flex flex-col");
@ -297,6 +299,32 @@ export default class SpecialVisualizations {
)
},
{
funcName: "wikidata_label",
docs: "Shows the label of the corresponding wikidata-item",
args: [
{
name: "keyToShowWikidataFor",
doc: "Use the wikidata entry from this key to show the label",
defaultValue: "wikidata"
}
],
example: "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself",
constr: (_, tagsSource, args) =>
new VariableUiElement(
tagsSource.map(tags => tags[args[0]])
.map(wikidata => {
wikidata = Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? [])[0]
const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement(entry.map(e => {
if (e === undefined || e["success"] === undefined) {
return wikidata
}
const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels)
}))
}))
},
{
funcName: "minimap",
docs: "A small map showing the selected feature.",
@ -482,7 +510,7 @@ export default class SpecialVisualizations {
docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}",
example: "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}",
args: [{
name: "Url",
name: "Url",
doc: "The URL to load",
required: true
}, {
@ -783,7 +811,7 @@ export default class SpecialVisualizations {
const textField = new TextField(
{
placeholder: t.addCommentPlaceholder,
inputStyle: "width: 100%; height: 6rem;",
inputStyle: "width: 100%; height: 6rem;",
textAreaRows: 3,
htmlType: "area"
}
@ -846,7 +874,7 @@ export default class SpecialVisualizations {
textField,
new Combine([
stateButtons.SetClass("sm:mr-2"),
new Toggle(addCommentButton,
new Toggle(addCommentButton,
new Combine([t.typeText]).SetClass("flex items-center h-full subtle"),
textField.GetValue().map(t => t !== undefined && t.length >= 1)).SetClass("sm:mr-2")
]).SetClass("sm:flex sm:justify-between sm:items-stretch")
@ -947,7 +975,7 @@ export default class SpecialVisualizations {
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
return specialVisualizations;
}

View file

@ -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,

View file

@ -17,14 +17,20 @@ export default class WikidataSearchBox extends InputElement<string> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly wikidataId: UIEventSource<string>
private readonly searchText: UIEventSource<string>
private readonly instanceOf?: number[];
private readonly notInstanceOf?: number[];
constructor(options?: {
searchText?: UIEventSource<string>,
value?: UIEventSource<string>
value?: UIEventSource<string>,
notInstanceOf?: number[],
instanceOf?: number[]
}) {
super();
this.searchText = options?.searchText
this.wikidataId = options?.value ?? new UIEventSource<string>(undefined);
this.instanceOf = options?.instanceOf
this.notInstanceOf = options?.notInstanceOf
}
GetValue(): UIEventSource<string> {
@ -59,7 +65,9 @@ export default class WikidataSearchBox extends InputElement<string> {
if (promise === undefined) {
promise = Wikidata.searchAndFetch(searchText, {
lang,
maxCount: 5
maxCount: 5,
notInstanceOf: this.notInstanceOf,
instanceOf: this.instanceOf
}
)
WikidataSearchBox._searchCache.set(key, promise)
@ -75,13 +83,15 @@ export default class WikidataSearchBox extends InputElement<string> {
return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchFailMessage.data])
}
if (searchField.GetValue().data.length === 0) {
return Translations.t.general.wikipedia.doSearch
}
if (searchResults.length === 0) {
return Translations.t.general.wikipedia.noResults.Subs({search: searchField.GetValue().data ?? ""})
}
if (searchResults.length === 0) {
return Translations.t.general.wikipedia.doSearch
}
return new Combine(searchResults.map(wikidataresponse => {
const el = WikidataPreviewBox.WikidataResponsePreview(wikidataresponse).SetClass("rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors")

View file

@ -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 {
@ -164,25 +161,7 @@ export class Translation extends BaseUIElement {
public AllValues(): string[] {
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)
}
}

View file

@ -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,26 @@ export default class Translations {
return s;
}
static T(t: string | any, context = undefined): Translation {
/**
* Converts a string or an object into a typed translation.
* Translation objects ('Translation' and 'TypedTranslation') are converted/returned
*
* Translations.T("some text") // => new TypedTranslation({"*": "some text"})
* Translations.T("some text").txt // => "some text"
*
* const t = new Translation({"nl": "vertaling", "en": "translation"})
* Translations.T(t) // => new TypedTranslation<object>({"nl": "vertaling", "en": "translation"})
*
* const t = new TypedTranslation({"nl": "vertaling", "en": "translation"})
* Translations.T(t) // => t
*
* const json: any = {"en": "English", "nl": "Nederlands"};
* const translation = Translations.T(new Translation(json));
* translation.textFor("en") // => "English"
* translation.textFor("nl") // => "Nederlands"
*
*/
static T(t: string | any, context = undefined): TypedTranslation<object> {
if (t === undefined || t === null) {
return undefined;
}
@ -30,40 +49,22 @@ export default class Translations {
t = "" + t
}
if (typeof t === "string") {
return new Translation({"*": t}, context);
return new TypedTranslation<object>({"*": 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);
if(t instanceof Translation){
return new TypedTranslation<object>(t.translations)
}
return new TypedTranslation<object>(t, context);
}
/**
* 'Wrap Translation': given an object containing translations OR a string, returns a translation object
*
* const json: any = {"en": "English", "nl": "Nederlands"};
* const translation = Translations.WT(new Translation(json));
* translation.textFor("en") // => "English"
* translation.textFor("nl") // => "Nederlands"
*/
public static WT(s: string | Translation): Translation {
if (s === undefined || s === null) {
return undefined;
}
if (typeof (s) === "string") {
return new Translation({'*': s});
}
if (s instanceof Translation) {
return s.Clone() /* MUST CLONE HERE! */;
}
console.error("Trying to Translation.WT, but got ", s)
throw "??? Not a valid translation"
}
public static CountTranslations() {
const queue: any = [Translations.t];

View file

@ -518,11 +518,35 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Apply a function on every leaf of the JSON; used to rewrite parts of the JSON.
* Returns a modified copy of the original object.
*
* 'null' and 'undefined' are _always_ considered a leaf, even if 'isLeaf' says it isn't
*
* Hangs if the object contains a loop
*
* // should walk a json
* const walked = Utils.WalkJson({
* key: "value"
* }, (x: string) => x + "!")
* walked // => {key: "value!"}
*
* // should preserve undefined and null:
* const walked = Utils.WalkJson({
* u: undefined,
* n: null,
* v: "value"
* }, (x) => {if(x !== undefined && x !== null){return x+"!}; return x})
* walked // => {v: "value!", u: undefined, n: null}
*
* // should preserve undefined and null, also with a negative isLeaf:
* const walked = Utils.WalkJson({
* u: undefined,
* n: null,
* v: "value"
* }, (x) => return x}, _ => false)
* walked // => {v: "value", u: undefined, n: null}
*/
static WalkJson(json: any, f: (v: object | number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, path: string[] = []) {
if (json === undefined) {
return f(undefined, path)
if (json === undefined || json === null) {
return f(json, path)
}
const jtp = typeof json
if (isLeaf !== undefined) {
@ -732,7 +756,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Triggers a 'download file' popup which will download the contents
*/
public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt",
options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" }) {
options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" | "application/json" }) {
const element = document.createElement("a");
let file;
if (typeof (contents) === "string") {

View file

@ -1,7 +1,7 @@
{
"contributors": [
{
"commits": 3508,
"commits": 3540,
"contributor": "Pieter Vander Vennet"
},
{
@ -17,7 +17,7 @@
"contributor": "Christian Neumann"
},
{
"commits": 31,
"commits": 32,
"contributor": "Win Olario"
},
{

View file

@ -1,22 +1,23 @@
{
"ca": "català",
"de": "Deutsch",
"eo": "Esperanto",
"fr": "français",
"es": "español",
"gl": "lingua galega",
"fi": "suomi",
"en": "English",
"ru": "русский язык",
"it": "italiano",
"pl": "język polski",
"ja": "日本語",
"sv": "svenska",
"pt": "português",
"nl": "Nederlands",
"id": "Indonesia",
"eo": "Esperanto",
"es": "español",
"fi": "suomi",
"fr": "français",
"gl": "lingua galega",
"hu": "magyar",
"id": "Indonesia",
"it": "italiano",
"ja": "日本語",
"nb_NO": "bokmål",
"zh_Hant": "簡體中文",
"pt_BR": "português brasileiro"
"nl": "Nederlands",
"pl": "język polski",
"pt": "português",
"pt_BR": "português brasileiro",
"ru": "русский язык",
"sl": "slovenščina",
"sv": "svenska",
"zh_Hant": "簡體中文"
}

View file

@ -1,27 +1,4 @@
{
"ca": {
"en": "Catalan",
"de": "Katalanisch",
"nl": "Catalaans",
"ca": "català",
"es": "catalán",
"fr": "catalan",
"eo": "kataluna lingvo",
"fi": "katalaani",
"gl": "lingua catalá",
"hu": "katalán",
"id": "Bahasa Katala",
"it": "catalano",
"ja": "カタルーニャ語",
"nb_NO": "katalansk",
"pl": "język kataloński",
"pt": "língua catalã",
"pt_BR": "língua catalã",
"ru": "каталанский язык",
"sv": "katalanska",
"zh_Hans": "加泰罗尼亚语",
"zh_Hant": "加泰隆尼亞語"
},
"de": {
"ca": "alemany",
"en": "German",
@ -41,6 +18,7 @@
"pl": "język niemiecki",
"pt": "alemão",
"ru": "немецкий язык",
"sl": "nemščina",
"sv": "tyska",
"zh_Hans": "德语",
"zh_Hant": "德語"
@ -65,10 +43,36 @@
"pt": "esperanto",
"pt_BR": "esperanto",
"ru": "эсперанто",
"sl": "esperanto",
"sv": "esperanto",
"zh_Hans": "世界语"
},
"ca": {
"en": "Catalan",
"de": "Katalanisch",
"nl": "Catalaans",
"ca": "català",
"es": "catalán",
"fr": "catalan",
"eo": "kataluna lingvo",
"fi": "katalaani",
"gl": "lingua catalá",
"hu": "katalán",
"id": "Bahasa Katala",
"it": "catalano",
"ja": "カタルーニャ語",
"nb_NO": "katalansk",
"pl": "język kataloński",
"pt": "língua catalã",
"pt_BR": "língua catalã",
"ru": "каталанский язык",
"sl": "Katalonščina",
"sv": "katalanska",
"zh_Hans": "加泰罗尼亚语",
"zh_Hant": "加泰隆尼亞語"
},
"fr": {
"zh_Hans": "法语",
"es": "francés",
"nl": "Frans",
"en": "French",
@ -84,11 +88,11 @@
"fi": "ranska",
"gl": "lingua francesa",
"ja": "フランス語",
"zh_Hans": "法语",
"nb_NO": "fransk",
"pl": "język francuski",
"pt_BR": "francês",
"ru": "французский язык",
"sl": "francoščina",
"sv": "franska"
},
"es": {
@ -110,6 +114,7 @@
"pt": "espanhol",
"pt_BR": "espanhol",
"ru": "испанский язык",
"sl": "španščina",
"sv": "spanska",
"zh_Hans": "西班牙语",
"zh_Hant": "西班牙語"
@ -133,17 +138,11 @@
"pt": "galego",
"pt_BR": "língua galega",
"ru": "галисийский язык",
"sl": "Galicijščina",
"sv": "galiciska",
"zh_Hant": "加利西亞語"
},
"fi": {
"nb_NO": "finsk",
"pl": "język fiński",
"pt": "finlandês",
"pt_BR": "língua finlandesa",
"sv": "finska",
"zh_Hans": "芬兰语",
"zh_Hant": "芬蘭語",
"nl": "Fins",
"de": "Finnisch",
"en": "Finnish",
@ -157,16 +156,17 @@
"ca": "finès",
"eo": "finna lingvo",
"fr": "finnois",
"gl": "finés"
"gl": "finés",
"nb_NO": "finsk",
"pl": "język fiński",
"pt": "finlandês",
"pt_BR": "língua finlandesa",
"sl": "finščina",
"sv": "finska",
"zh_Hans": "芬兰语",
"zh_Hant": "芬蘭語"
},
"en": {
"pl": "język angielski",
"pt": "inglês",
"pt_BR": "inglês",
"ru": "английский язык",
"sv": "engelska",
"zh_Hans": "英语",
"zh_Hant": "英語",
"id": "Inggris",
"it": "inglese",
"ja": "英語",
@ -180,9 +180,25 @@
"fi": "englanti",
"fr": "anglais",
"gl": "lingua inglesa",
"hu": "angol"
"hu": "angol",
"pl": "język angielski",
"pt": "inglês",
"pt_BR": "inglês",
"ru": "английский язык",
"sl": "angleščina",
"sv": "engelska",
"zh_Hans": "英语",
"zh_Hant": "英語"
},
"ru": {
"zh_Hans": "俄语",
"zh_Hant": "俄語",
"hu": "orosz",
"en": "Russian",
"ru": "русский язык",
"es": "ruso",
"fr": "russe",
"nl": "Russisch",
"ca": "rus",
"de": "Russisch",
"eo": "rusa lingvo",
@ -191,88 +207,35 @@
"id": "Rusia",
"it": "russo",
"ja": "ロシア語",
"hu": "orosz",
"en": "Russian",
"ru": "русский язык",
"es": "ruso",
"fr": "russe",
"nl": "Russisch",
"sv": "ryska",
"zh_Hans": "俄语",
"zh_Hant": "俄語",
"nb_NO": "russisk",
"pl": "język rosyjski",
"pt": "russo",
"pt_BR": "russo"
"pt_BR": "russo",
"sl": "ruščina",
"sv": "ryska"
},
"it": {
"zh_Hans": "意大利语",
"zh_Hant": "義大利語",
"nb_NO": "italiensk",
"pl": "język włoski",
"pt_BR": "língua italiana",
"ru": "итальянский язык",
"sv": "italienska",
"ca": "italià",
"eo": "itala lingvo",
"fi": "italia",
"gl": "lingua italiana",
"en": "Italian",
"ja": "イタリア語",
"es": "italiano",
"hu": "olasz",
"id": "Italia",
"it": "italiano",
"fr": "italien",
"de": "Italienisch",
"nl": "Italiaans",
"pt": "italiano"
},
"pl": {
"de": "Polnisch",
"en": "Polish",
"pl": "język polski",
"es": "polaco",
"fr": "polonais",
"ca": "polonès",
"eo": "pola lingvo",
"fi": "puola",
"gl": "lingua polaca",
"hu": "lengyel",
"id": "Polandia",
"zh_Hans": "波兰语",
"zh_Hant": "波蘭語",
"it": "polacco",
"ja": "ポーランド語",
"nb_NO": "polsk",
"nl": "Pools",
"pt": "polaco",
"pt_BR": "língua polonesa",
"ru": "польский язык",
"sv": "polska"
},
"ja": {
"nb_NO": "japansk",
"nl": "Japans",
"pl": "język japoński",
"pt": "japonês",
"pt_BR": "língua japonesa",
"ru": "японский язык",
"sv": "japanska",
"zh_Hans": "日语",
"zh_Hant": "日語",
"en": "Japanese",
"ca": "japonès",
"de": "Japanisch",
"eo": "japana lingvo",
"es": "japonés",
"fi": "japani",
"fr": "japonais",
"gl": "lingua xaponesa",
"hu": "japán",
"id": "Bahasa Jepang",
"it": "giapponese",
"ja": "日本語"
"sl": {
"en": "Slovene",
"ca": "eslovè",
"de": "Slowenisch",
"eo": "slovena lingvo",
"es": "esloveno",
"fi": "sloveeni",
"fr": "slovène",
"gl": "lingua eslovena",
"hu": "szlovén",
"id": "Bahasa Slovenia",
"it": "sloveno",
"ja": "スロベニア語",
"nb_NO": "slovensk",
"nl": "Sloveens",
"pl": "język słoweński",
"pt": "língua eslovena",
"pt_BR": "língua eslovena",
"ru": "словенский язык",
"sl": "slovenščina",
"sv": "slovenska",
"zh_Hant": "斯洛維尼亞語"
},
"sv": {
"en": "Swedish",
@ -294,8 +257,33 @@
"pt": "língua sueca",
"pt_BR": "língua sueca",
"ru": "шведский язык",
"sl": "švedščina",
"zh_Hant": "瑞典語"
},
"it": {
"en": "Italian",
"ja": "イタリア語",
"es": "italiano",
"hu": "olasz",
"id": "Italia",
"it": "italiano",
"fr": "italien",
"de": "Italienisch",
"nl": "Italiaans",
"pt": "italiano",
"ca": "italià",
"eo": "itala lingvo",
"fi": "italia",
"gl": "lingua italiana",
"nb_NO": "italiensk",
"pl": "język włoski",
"pt_BR": "língua italiana",
"ru": "итальянский язык",
"sl": "italijanščina",
"sv": "italienska",
"zh_Hans": "意大利语",
"zh_Hant": "義大利語"
},
"pt": {
"en": "Portuguese",
"hu": "portugál",
@ -315,32 +303,105 @@
"pl": "język portugalski",
"pt_BR": "português",
"ru": "португальский язык",
"sl": "portugalščina",
"sv": "portugisiska",
"zh_Hans": "葡萄牙语",
"zh_Hant": "葡萄牙語"
},
"pl": {
"de": "Polnisch",
"en": "Polish",
"pl": "język polski",
"es": "polaco",
"fr": "polonais",
"nb_NO": "polsk",
"nl": "Pools",
"pt": "polaco",
"pt_BR": "língua polonesa",
"ru": "польский язык",
"sl": "poljščina",
"sv": "polska",
"zh_Hans": "波兰语",
"zh_Hant": "波蘭語",
"ca": "polonès",
"eo": "pola lingvo",
"fi": "puola",
"gl": "lingua polaca",
"hu": "lengyel",
"id": "Polandia",
"it": "polacco",
"ja": "ポーランド語"
},
"ja": {
"nb_NO": "japansk",
"nl": "Japans",
"pl": "język japoński",
"pt": "japonês",
"pt_BR": "língua japonesa",
"ru": "японский язык",
"sl": "Japonščina",
"sv": "japanska",
"zh_Hans": "日语",
"zh_Hant": "日語",
"id": "bahasa Jepang",
"en": "Japanese",
"ca": "japonès",
"de": "Japanisch",
"eo": "japana lingvo",
"es": "japonés",
"fi": "japani",
"fr": "japonais",
"gl": "lingua xaponesa",
"hu": "japán",
"it": "giapponese",
"ja": "日本語"
},
"nl": {
"en": "Dutch",
"de": "Niederländisch",
"nl": "Nederlands",
"ca": "neerlandès",
"es": "neerlandés",
"fr": "néerlandais",
"hu": "holland",
"id": "Belanda",
"it": "olandese",
"es": "Neerlandés",
"eo": "nederlanda lingvo",
"fi": "hollanti",
"gl": "lingua neerlandesa",
"zh_Hans": "荷兰语",
"zh_Hant": "荷蘭語",
"ja": "オランダ語",
"nb_NO": "nederlandsk",
"pl": "język niderlandzki",
"pt": "neerlandês",
"pt_BR": "neerlandês",
"ru": "нидерландский язык",
"sv": "nederländska"
"sl": "Nizozemščina",
"sv": "nederländska",
"zh_Hans": "荷兰语",
"zh_Hant": "荷蘭語"
},
"hu": {
"en": "Hungarian",
"fr": "hongrois",
"pt": "húngaro",
"pt_BR": "língua húngara",
"ru": "венгерский язык",
"sl": "madžarščina",
"sv": "ungerska",
"zh_Hant": "匈牙利語",
"ca": "hongarès",
"de": "Ungarisch",
"eo": "hungara lingvo",
"es": "húngaro",
"fi": "unkari",
"gl": "lingua húngara",
"hu": "magyar",
"id": "Hongaria",
"it": "ungherese",
"ja": "ハンガリー語",
"nb_NO": "ungarsk",
"nl": "Hongaars",
"pl": "język węgierski"
},
"id": {
"en": "Indonesian",
@ -361,33 +422,16 @@
"pt": "língua indonésia",
"pt_BR": "língua indonésia",
"ru": "индонезийский язык",
"sl": "indonezijščina",
"sv": "indonesiska",
"zh_Hans": "印度尼西亚语",
"zh_Hant": "印尼語"
},
"hu": {
"nb_NO": "ungarsk",
"nl": "Hongaars",
"pl": "język węgierski",
"pt": "húngaro",
"pt_BR": "língua húngara",
"ru": "венгерский язык",
"sv": "ungerska",
"zh_Hant": "匈牙利語",
"ca": "hongarès",
"de": "Ungarisch",
"eo": "hungara lingvo",
"es": "húngaro",
"fi": "unkari",
"gl": "lingua húngara",
"hu": "magyar",
"id": "Hongaria",
"it": "ungherese",
"ja": "ハンガリー語",
"en": "Hungarian",
"fr": "hongrois"
},
"nb_NO": {
"ca": "bokmål",
"de": "Bokmål",
"en": "Bokmål",
"eo": "Bokmål",
"es": "bokmål",
"fi": "kirjanorja",
"fr": "bokmål",
@ -402,11 +446,8 @@
"pt": "bokmål",
"pt_BR": "Bokmål",
"ru": "букмол",
"sv": "bokmål",
"ca": "bokmål",
"de": "Bokmål",
"en": "Bokmål",
"eo": "Bokmål"
"sl": "Bokmål",
"sv": "bokmål"
},
"zh_Hant": {
"ca": "xinès simplificat",
@ -420,7 +461,7 @@
"it": "cinese semplificato",
"ja": "簡体字中国語",
"nb_NO": "tradisjonell kinesisk",
"pl": "Chiński uproszczony",
"pl": "język chiński uproszczony",
"pt": "chinês simplificado",
"zh_Hans": "简体中文",
"zh_Hant": "簡體中文",

View file

@ -10,7 +10,8 @@
"ru": "Известные адреса в OSM",
"id": "Alamat yang dikenal di OSM",
"es": "Direcciones conocidas en OSM",
"zh_Hans": "OSM中已知的地址"
"zh_Hans": "OSM中已知的地址",
"nb_NO": "Kjente adresser i OSM"
},
"minzoom": 18,
"source": {
@ -38,7 +39,8 @@
"ru": "Известный адрес",
"es": "Domicilio conocido",
"zh_Hans": "已知的地址",
"id": "Alamat yang diketahui"
"id": "Alamat yang diketahui",
"nb_NO": "Kjent adresse"
}
},
"description": {
@ -52,7 +54,9 @@
"pl": "Adresy",
"id": "Alamat",
"es": "Direcciones",
"zh_Hans": "地址"
"zh_Hans": "地址",
"ca": "Adreces",
"nb_NO": "Adresser"
},
"tagRenderings": [
{
@ -66,7 +70,8 @@
"pl": "Numer tego domu to <b>{addr:housenumber}</b>",
"ru": "Номер дома <b>{addr:housenumber}</b>",
"zh_Hans": "门牌号是<b>{addr:housenumber}</b>",
"id": "Nomor rumah ini <b>{addr:housenumber}</b>"
"id": "Nomor rumah ini <b>{addr:housenumber}</b>",
"es": "La numeración de la casa es <b>{addr:housenumber}</b>"
},
"question": {
"en": "What is the number of this house?",
@ -118,7 +123,8 @@
"fr": "Le nom de la voie est <b>{addr:street}</b>",
"pl": "Ten adres znajduje się na ulicy <b>{addr:street}</b>",
"zh_Hans": "这个地址位于<b>{addr:street}</b>街",
"id": "Alamat ini ada di jalan <b>{addr:street}</b>"
"id": "Alamat ini ada di jalan <b>{addr:street}</b>",
"es": "La dirección está en la calle <b>{addr:street}</b>"
},
"question": {
"en": "What street is this address located in?",

View file

@ -10,7 +10,8 @@
"hu": "Mentőállomás-térkép",
"nl": "Kaart van ambulancestations",
"zh_Hans": "救护车站地图",
"id": "Peta stasiun ambulans"
"id": "Peta stasiun ambulans",
"es": "Mapa de estaciones de ambulancias"
},
"minzoom": 12,
"source": {
@ -29,7 +30,8 @@
"de": "Rettungswache",
"it": "Stazione delle ambulanze",
"hu": "Mentőállomás",
"nl": "Ambulancestation"
"nl": "Ambulancestation",
"es": "Estación de Ambulancias"
}
},
"description": {
@ -40,7 +42,8 @@
"it": "La stazione delle ambulanze è unarea per lo stoccaggio delle ambulanze, dellequipaggiamento medico, dei dispositivi di protezione individuale e di altre forniture medicali.",
"hu": "A mentőállomás olyan terület, ahol mentőautókat, orvosi felszereléseket, egyéni védőfelszereléseket és egyéb orvosi felszereléseket tárolnak.",
"ru": "Станция скорой помощи это полигон для хранения транспорта, медицинского оборудования, средств индивидуальной защиты и других медицинских принадлежностей.",
"nl": "Een ambulancestation is een plaats waar ambulances, medisch materiaal, persoonlijk beschermingsmateriaal en aanverwanten worden bewaard."
"nl": "Een ambulancestation is een plaats waar ambulances, medisch materiaal, persoonlijk beschermingsmateriaal en aanverwanten worden bewaard.",
"es": "Una estación de ambulancias es una zona para almacenar vehículos de ambulancia, equipamiento médico, equipos de protección personal y otros suministros médicos."
},
"tagRenderings": [
{
@ -56,7 +59,9 @@
"it": "Qual è il nome di questa stazione delle ambulanze?",
"de": "Wie heißt diese Rettungswache?",
"hu": "Mi a neve ennek a menőtállomásnak?",
"nl": "Hoe heet dit ambulancestation?"
"nl": "Hoe heet dit ambulancestation?",
"es": "¿Cual es el nombre de esta estación de ambulancias?",
"nb_NO": "Hva er navnet på denne ambulansestasjonen?"
},
"render": {
"en": "This station is called {name}.",
@ -66,7 +71,9 @@
"it": "Questa stazione è chiamata {name}.",
"de": "Diese Rettungswache heißt {name}.",
"hu": "A mentőállomás neve: {name}.",
"nl": "Dit station heet {name}."
"nl": "Dit station heet {name}.",
"es": "Esta estación se llama {name}.",
"nb_NO": "Denne stasjonen heter {name}."
}
},
{
@ -82,7 +89,8 @@
"it": " Come si chiama la strada in cui si trova questa stazione?",
"de": " Wie lautet der Name der Straße, in der sich die Rettungswache befindet?",
"hu": " Mi a neve annak az utcának, amelyben az állomás található?",
"nl": " In welke straat ligt dit station?"
"nl": " In welke straat ligt dit station?",
"es": " ¿Cual es el nombre de la calle en la que se encuentra la estación?"
},
"render": {
"en": "This station is along a highway called {addr:street}.",
@ -92,7 +100,8 @@
"it": "Questa stazione si trova in {addr:street}.",
"de": "Dieser Bahnhof liegt an der Straße {addr:street}.",
"hu": "Ez az állomás a következő utcában van: {addr:street}.",
"nl": "Straat waar dit station ligt: {addr:street}"
"nl": "Straat waar dit station ligt: {addr:street}",
"es": "Esta estación se encuentra al lado de una autovía llamada {addr:street}."
}
},
{
@ -105,7 +114,8 @@
"it": "Dove si trova la stazione? (ad es. quartiere, paese o città)",
"de": "Wo befindet sich die Rettungswache? (z. B. Name von Stadtviertel, Dorf oder Stadt)",
"hu": "Hol található az állomás? (Pl. a falu, kisváros vagy városrész neve.)",
"nl": "Waar ligt het station? (v.b. naam van de buurt, dorp of stad)"
"nl": "Waar ligt het station? (v.b. naam van de buurt, dorp of stad)",
"es": "¿Dónde se encuentra la estación? (ej. nombre del barrio, pueblo o ciudad)"
},
"freeform": {
"key": "addr:place"
@ -117,7 +127,8 @@
"it": "La stazione si trova a {addr:place}.",
"de": "Diese Rettungswache befindet sich in {addr:place}.",
"hu": "Ez az állomás itt található: {addr:place}.",
"nl": "Dit station ligt in {addr:place}."
"nl": "Dit station ligt in {addr:place}.",
"es": "Esta estación se encuentra en {addr:place}."
}
},
{
@ -130,7 +141,8 @@
"de": "Welches Unternehmen betreibt diese Rettungswache?",
"hu": "Milyen szervezet működteti ezt az állomást?",
"ru": "Какая организация управляет этой станцией?",
"nl": "Welke organisatie beheert dit station?"
"nl": "Welke organisatie beheert dit station?",
"es": "¿Qué agencia opera esta estación?"
},
"render": {
"en": "This station is operated by {operator}.",
@ -140,7 +152,8 @@
"de": "Diese Rettungswache wird betrieben von {operator}.",
"hu": "Az állomás üzemeltetője: {operator}.",
"ru": "Эта станция управляется {operator}.",
"nl": "Dit station wordt beheerd door {operator}."
"nl": "Dit station wordt beheerd door {operator}.",
"es": "Esta estación la opera {operator}."
},
"freeform": {
"key": "operator"
@ -156,7 +169,8 @@
"it": "Comè classificato il gestore della stazione?",
"de": "Wie kann der Betreiber der Rettungswache eingestuft werden?",
"hu": "Hogyan sorolható be az állomás üzemeltetője?",
"nl": "Wat voor een organisatie is de beheerder van dit station?"
"nl": "Wat voor een organisatie is de beheerder van dit station?",
"es": "¿Como está clasificada la operadora de la estación?"
},
"render": {
"en": "The operator is a(n) {operator:type} entity.",
@ -165,7 +179,8 @@
"it": "Loperatore è un ente {operator:type}.",
"de": "Der Betreiber ist eine {operator:type}.",
"hu": "Az üzemeltető egy {operator:type} jellegű szervezet.",
"nl": "De beheerder is van het type {operator:type}."
"nl": "De beheerder is van het type {operator:type}.",
"es": "La operador a no es una entidad de tipo {operator:type}."
},
"freeform": {
"key": "operator:type"
@ -185,7 +200,8 @@
"de": "Die Rettungswache wird von einer Behörde betrieben.",
"hu": "A mentőállomást a kormány üzemelteti.",
"ru": "Станция управляется правительством.",
"nl": "Dit station wordt beheerd door de overheid."
"nl": "Dit station wordt beheerd door de overheid.",
"es": "La estación la opera el govierno."
}
},
{
@ -202,7 +218,8 @@
"de": "Die Rettungswache wird von einer gemeindenahen oder informellen Organisation betrieben.",
"hu": "Mentőállomást egy közösségi vagy nem hivatalos szervezet működteti.",
"ru": "Станция управляется волонтёрами или частной организацией.",
"nl": "Dit station wordt beheerd door een informele of community organisatie."
"nl": "Dit station wordt beheerd door een informele of community organisatie.",
"es": "La estación la opera una organización basada en la comunidad o informal."
}
},
{
@ -218,7 +235,8 @@
"it": "La stazione è gestita da un gruppo ufficiale di volontari.",
"de": "Die Rettungswache wird von einer Freiwilligenorganisation betrieben.",
"hu": "A mentőállomást egy önkéntesekből álló hivatalos csoport működteti.",
"nl": "Dit station wordt beheerd door een formele groep vrijwilligers."
"nl": "Dit station wordt beheerd door een formele groep vrijwilligers.",
"es": "La estación la opera un grupo formal de voluntarios."
}
},
{
@ -234,7 +252,8 @@
"it": "La stazione è gestita da un privato.",
"de": "Die Rettungswache wird von einer privaten Organisation betrieben.",
"hu": "Az állomást egy magánkézben lévő szervezet működteti.",
"nl": "Dit station wordt beheerd door een privé-organisatie."
"nl": "Dit station wordt beheerd door een privé-organisatie.",
"es": "La estación es de gestión privada."
}
}
]
@ -256,7 +275,9 @@
"hu": "Mentőállomás",
"nl": "een ambulancestation",
"zh_Hans": "救护车站",
"id": "Stasiun ambulans"
"id": "Stasiun ambulans",
"es": "una estación de ambulancias",
"nb_NO": "en ambulansestasjon"
},
"description": {
"en": "Add an ambulance station to the map",
@ -268,7 +289,9 @@
"hu": "Mentőállomás hozzáadása a térképhez",
"nl": "Voeg een ambulancestation toe aan de kaart",
"zh_Hans": "向地图中添加一个救护车站",
"id": "Tambahkan stasiun ambulans ke peta"
"id": "Tambahkan stasiun ambulans ke peta",
"es": "Añadir una estación de ambulancias al mapa",
"nb_NO": "Legg til en ambulansestasjon på kartet"
}
}
],

View file

@ -14,7 +14,8 @@
"nb_NO": "Kunstverk",
"pt": "Obras de arte",
"hu": "Műalkotások",
"pl": "Dzieła sztuki"
"pl": "Dzieła sztuki",
"ca": "Obres d'art"
},
"source": {
"osmTags": "tourism=artwork"
@ -38,7 +39,8 @@
"pl": "Dzieło sztuki",
"pt": "Obra de arte",
"pt_BR": "Obra de arte",
"sv": "Konstverk"
"sv": "Konstverk",
"ca": "Obra d'art"
},
"mappings": [
{
@ -166,7 +168,8 @@
"pt": "Arquitetura",
"hu": "Építészet",
"pl": "Architektura",
"es": "Arquitectura"
"es": "Arquitectura",
"ca": "Arquitectura"
}
},
{
@ -185,7 +188,8 @@
"pt": "Mural",
"hu": "Falfestmény",
"pl": "Mural",
"es": "Mural"
"es": "Mural",
"ca": "Mural"
}
},
{
@ -204,7 +208,8 @@
"pt": "Pintura",
"hu": "Festmény",
"pl": "Obraz",
"es": "Pintura"
"es": "Pintura",
"ca": "Pintura"
}
},
{
@ -223,7 +228,8 @@
"pt": "Escultura",
"hu": "Absztrakt szobor",
"pl": "Rzeźba",
"es": "Escultura"
"es": "Escultura",
"ca": "Escultura"
}
},
{
@ -241,7 +247,8 @@
"pt": "Estátua",
"hu": "Szobor",
"pl": "Posąg",
"es": "Estatua"
"es": "Estatua",
"ca": "Estàtua"
}
},
{
@ -259,7 +266,8 @@
"pt": "Busto",
"hu": "Mellszobor",
"pl": "Popiersie",
"es": "Busto"
"es": "Busto",
"ca": "Bust"
}
},
{
@ -277,7 +285,9 @@
"id": "Batu",
"pt": "Pedra",
"hu": "Kő",
"pl": "Skała"
"pl": "Skała",
"ca": "Pedra",
"es": "Piedra"
}
},
{
@ -296,7 +306,8 @@
"pt": "Instalação",
"hu": "Installáció",
"pl": "Instalacja artystyczna",
"es": "Instalación"
"es": "Instalación",
"ca": "Instal·lació"
}
},
{
@ -315,7 +326,8 @@
"pt": "Graffiti",
"hu": "Graffiti",
"pl": "Graffiti",
"es": "Grafiti"
"es": "Grafiti",
"ca": "Grafiti"
}
},
{
@ -333,7 +345,9 @@
"id": "Relief",
"pt": "Relevo",
"hu": "Dombormű",
"pl": "Płaskorzeźba"
"pl": "Płaskorzeźba",
"ca": "Relleu",
"es": "Relieve"
}
},
{
@ -351,7 +365,8 @@
"id": "Azulejo (ubin dekoratif Spanyol)",
"pt": "Azulejo (azulejo decorativo espanhol e português)",
"hu": "Azulejo (portugál vagy spanyol dekoratív csempe)",
"pl": "Azulejo (hiszpańskie płytka dekoracyjna)"
"pl": "Azulejo (hiszpańskie płytka dekoracyjna)",
"es": "Azulejo (azulejos decorativos españoles)"
}
},
{
@ -368,7 +383,9 @@
"nb_NO": "Flisarbeid",
"pt": "Ladrilhos",
"hu": "Csempe",
"pl": "Płyta ceramiczna (fliza)"
"pl": "Płyta ceramiczna (fliza)",
"ca": "Enrajolat",
"es": "Cerámica"
}
}
],
@ -388,7 +405,8 @@
"id": "Seniman mana yang menciptakan ini?",
"pt": "Que artista criou isto?",
"hu": "Melyik művész alkotása ezt?",
"pl": "Który artysta to stworzył?"
"pl": "Który artysta to stworzył?",
"es": "¿Que artista creó esto?"
},
"render": {
"en": "Created by {artist_name}",
@ -403,7 +421,8 @@
"id": "Dibuat oleh {artist_name}",
"pt": "Criado por {artist_name}",
"hu": "Alkotó: {artist_name}",
"pl": "Stworzone przez {artist_name}"
"pl": "Stworzone przez {artist_name}",
"es": "Creado por {artist_name}"
},
"freeform": {
"key": "artist_name"
@ -424,7 +443,8 @@
"id": "Adakah situs web mengenai informasi lebih lanjut tentang karya seni ini?",
"pt": "Existe um site com mais informações sobre esta obra de arte?",
"hu": "Van-e olyan honlap, amely további információkat tartalmaz erről a műalkotásról?",
"pl": "Gdzie znajdę więcej informacji na temat tego dzieła sztuki?"
"pl": "Gdzie znajdę więcej informacji na temat tego dzieła sztuki?",
"es": "¿Hay un sitio web con más información sobre esta obra de arte?"
},
"render": {
"en": "More information on <a href='{website}' target='_blank'>this website</a>",
@ -439,7 +459,8 @@
"nb_NO": "Mer info er å finne på <a href='{website}' target='_blank'>denne nettsiden</a>",
"pt": "Mais informações <a href='{website}' target='_blank'>neste site</a>",
"hu": "További információ <a href='{website}' target='_blank'>ezen a weboldalon</a>",
"pl": "Więcej informacji na <a href='{website}' target='_blank'>tej stronie</a>"
"pl": "Więcej informacji na <a href='{website}' target='_blank'>tej stronie</a>",
"es": "Más información en <a href='{website}' target='_blank'>este sitio web</a>"
},
"freeform": {
"key": "website",
@ -460,7 +481,8 @@
"nb_NO": "Hvilken Wikipedia-oppføring samsvarer med <b>dette kunstverket</b>?",
"id": "Entri Wikidata mana yang sesuai dengan <b>karya seni ini</b>?",
"pt": "Que entrada no Wikidata corresponde a <b>esta obra de arte</b>?",
"hu": "Melyik Wikidata-bejegyzés felel meg <b>ennek a műalkotásnak</b>?"
"hu": "Melyik Wikidata-bejegyzés felel meg <b>ennek a műalkotásnak</b>?",
"es": "¿Qué entrada de Wikidata se corresponde con <b>esta obra de arte</b>?"
},
"render": {
"en": "Corresponds with <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>",
@ -474,7 +496,8 @@
"nb_NO": "Samsvarer med <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>",
"id": "Sesuai dengan <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>",
"pt": "Corresponde a <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>",
"hu": "Ez a megfelelő: <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>"
"hu": "Ez a megfelelő: <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>",
"es": "Se corresponde con <a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'>{wikidata}</a>"
},
"freeform": {
"key": "wikidata",

View file

@ -7,7 +7,8 @@
"ru": "Препятствия",
"hu": "Akadályok",
"fr": "Barrières",
"es": "Barreras"
"es": "Barreras",
"ca": "Barreres"
},
"description": {
"en": "Obstacles while cycling, such as bollards and cycle barriers",
@ -32,7 +33,8 @@
"de": "Hindernis",
"ru": "Препятствие",
"fr": "Barrière",
"es": "Barrera"
"es": "Barrera",
"ca": "Barrera"
},
"mappings": [
{
@ -43,7 +45,8 @@
"de": "Poller",
"ru": "Прикол",
"fr": "Bollard",
"es": "Bolardo"
"es": "Bolardo",
"ca": "Pilona"
}
},
{
@ -52,7 +55,8 @@
"en": "Cycling Barrier",
"nl": "Fietshekjes",
"de": "Barriere für Radfahrer",
"fr": "Barrière cyclable"
"fr": "Barrière cyclable",
"ca": "Barrera ciclista"
}
}
]
@ -66,7 +70,8 @@
"ru": "Прикол",
"fr": "une bollard",
"hu": "Terelőoszlop",
"es": "una bolardo"
"es": "una bolardo",
"ca": "una pilona"
},
"tags": [
"barrier=bollard"
@ -76,7 +81,8 @@
"nl": "Een paaltje in de weg",
"de": "Ein Poller auf der Straße",
"hu": "Terelőoszlop az úton",
"fr": "Un potelet sur le chemin"
"fr": "Un potelet sur le chemin",
"es": "Un bolardo en la carretera"
},
"preciseInput": {
"preferredBackground": [
@ -155,7 +161,9 @@
"en": "This is a single bollard in the road",
"hu": "Ez egyetlen oszlop az úton",
"nl": "Dit is een enkel paaltje in de weg",
"fr": "C'est un plot unique sur la route"
"fr": "C'est un plot unique sur la route",
"de": "Dies ist ein einzelner Poller auf der Straße",
"es": "Este es un único bolardo en la carretera"
}
},
{
@ -164,7 +172,9 @@
"en": "This is a cycle barrier slowing down cyclists",
"nl": "Dit zijn fietshekjes die fietsers afremmen",
"hu": "Ez egy kerékpárakadály, amely lelassítja a kerékpárosokat",
"fr": "C'est une barrière visant à ralentir les vélos"
"fr": "C'est une barrière visant à ralentir les vélos",
"de": "Dies ist eine Fahrradbarriere, die Radfahrer abbremst",
"es": "Esta es una barrera ciclista que ralentiza a los ciclistas"
}
}
]
@ -187,7 +197,8 @@
"nl": "Verwijderbare paal",
"de": "Entfernbarer Poller",
"fr": "Bollard amovible",
"hu": "Eltávolítható terelőoszlop"
"hu": "Eltávolítható terelőoszlop",
"ca": "Pilona desmuntable"
}
},
{
@ -198,7 +209,8 @@
"de": "Feststehender Poller",
"fr": "Bollard fixe",
"hu": "Rögzített terelőoszlop",
"es": "Bolardo fijo"
"es": "Bolardo fijo",
"ca": "Pilona fixa"
}
},
{
@ -230,7 +242,8 @@
"de": "Ausfahrender Poller",
"fr": "Bollard rétractable",
"hu": "Felemelkedő terelőoszlop",
"es": "Bolardo levadizo"
"es": "Bolardo levadizo",
"ca": "Pilona retràctil"
}
}
],
@ -242,7 +255,8 @@
"nl": "Wat voor fietshekjes zijn dit?",
"de": "Um welche Art Fahrradhindernis handelt es sich?",
"hu": "Milyen fajta kerékpárakadály ez?",
"fr": "Quel est ce type de barrière cyclable?"
"fr": "Quel est ce type de barrière cyclable?",
"es": "¿Qué tipo de barrera ciclista es esta?"
},
"condition": "barrier=cycle_barrier",
"mappings": [
@ -311,14 +325,16 @@
"nl": "Maximumbreedte: {maxwidth:physical} m",
"de": "Maximale Durchfahrtsbreite: {maxwidth:physical} m",
"fr": "Largeur maximale: {maxwidth:physical} m",
"hu": "Legnagyobb szélesség: {maxwidth:physical} m"
"hu": "Legnagyobb szélesség: {maxwidth:physical} m",
"es": "Anchura máxima: {maxwidth:physical} m"
},
"question": {
"en": "How wide is the gap left over besides the barrier?",
"nl": "Hoe breed is de ruimte naast de barrière?",
"de": "Welche Durchfahrtsbreite hat das Hindernis?",
"hu": "Milyen széles az akadályon az a rés, amelyen át lehet haladni?",
"fr": "Quelle est la largeur du passage?"
"fr": "Quelle est la largeur du passage?",
"es": "¿Cómo de ancho es el hueco dejado fuera de la barrera?"
},
"condition": {
"and": [
@ -342,14 +358,16 @@
"nl": "Ruimte tussen barrières (langs de lengte van de weg): {width:separation} m",
"de": "Abstand zwischen den Barrieren (entlang der Straße): {width:separation} m",
"hu": "A korlátok közötti távolság (az út irányában): {width:separation} m",
"fr": "Espace entre deux barrières successives : {width:separation}m"
"fr": "Espace entre deux barrières successives : {width:separation}m",
"es": "Espacio entre barreras (a lo largo de la longitud de la carretera): {width:separation} m"
},
"question": {
"en": "How much space is there between the barriers (along the length of the road)?",
"nl": "Hoeveel ruimte is er tussen de barrières (langs de lengte van de weg)?",
"de": "Wie groß ist der Abstand zwischen den Barrieren (entlang der Straße)?",
"hu": "Mekkora távolság van a korlátok között (az út irányában)?",
"fr": "Combien despace sépare deux barrières successives?"
"fr": "Combien despace sépare deux barrières successives?",
"es": "¿Cuánto espacio hay entre las barreras (a lo largo de la longitud de la carretera)?"
},
"condition": {
"or": [
@ -373,14 +391,16 @@
"nl": "Breedte van de opening: {width:opening} m",
"de": "Breite der Öffnung: {width:opening} m",
"fr": "Largeur de l'ouverture : {width:opening} m",
"hu": "Nyílás szélessége: {width:opening} m"
"hu": "Nyílás szélessége: {width:opening} m",
"es": "Anchura de la apertura: {width:opening} m"
},
"question": {
"en": "How wide is the smallest opening next to the barriers?",
"nl": "Hoe breed is de smalste opening naast de barrières?",
"de": "Wie breit ist die kleinste Öffnung neben den Barrieren?",
"hu": "Milyen széles a korlátok melletti legkisebb nyílás?",
"fr": "Quelle est la largeur d'ouverture après la plus petite près de la barrière ?"
"fr": "Quelle est la largeur d'ouverture après la plus petite près de la barrière ?",
"es": "¿Cómo de año es la apertura más pequeña al lado de las barreras?"
},
"condition": {
"or": [
@ -404,14 +424,16 @@
"de": "Überschneidung: {overlap} m",
"hu": "Átfedés: {overlap} m",
"fr": "Chevauchement : {overlap}m",
"nl": "Overlap: {overlap} m"
"nl": "Overlap: {overlap} m",
"es": "Solapado: {overlap} m"
},
"question": {
"en": "How much overlap do the barriers have?",
"nl": "Hoeveel overlappen de barrières?",
"de": "Wie stark überschneiden sich die Barrieren?",
"hu": "Mekkora a korlátok átfedése?",
"fr": "Quel est le chevauchement des barrières?"
"fr": "Quel est le chevauchement des barrières?",
"es": "¿Cuánto se solapan las barreras?"
},
"condition": {
"or": [

View file

@ -16,7 +16,8 @@
"fi": "Penkit",
"pl": "Ławki",
"pt_BR": "Bancos",
"pt": "Bancos"
"pt": "Bancos",
"ca": "Bancs"
},
"minzoom": 17,
"source": {
@ -39,7 +40,8 @@
"fi": "Penkki",
"pl": "Ławka",
"pt_BR": "Banco",
"pt": "Banco"
"pt": "Banco",
"ca": "Banc"
}
},
"tagRenderings": [
@ -64,7 +66,8 @@
"fi": "Selkänoja: kyllä",
"pl": "Oparcie: Tak",
"pt_BR": "Encosto: Sim",
"pt": "Encosto: Sim"
"pt": "Encosto: Sim",
"ca": "Respatller: sí"
}
},
{
@ -85,7 +88,8 @@
"fi": "Selkänoja: ei",
"pl": "Oparcie: Nie",
"pt_BR": "Encosto: Não",
"pt": "Encosto: Não"
"pt": "Encosto: Não",
"ca": "Respatller: No"
}
}
],
@ -123,7 +127,8 @@
"nb_NO": "{seats} seter",
"pl": "{seats} siedzeń",
"pt_BR": "{seats} assentos",
"pt": "{seats} assentos"
"pt": "{seats} assentos",
"ca": "{seats} seients"
},
"freeform": {
"key": "seats",
@ -164,7 +169,8 @@
"pl": "Materiał: {material}",
"pt_BR": "Material: {material}",
"pt": "Material: {material}",
"eo": "Materialo: {material}"
"eo": "Materialo: {material}",
"ca": "Material: {material}"
},
"freeform": {
"key": "material",
@ -189,7 +195,8 @@
"fi": "Materiaali: puu",
"pl": "Materiał: drewno",
"pt": "Material: madeira",
"eo": "Materialo: ligna"
"eo": "Materialo: ligna",
"ca": "Material: fusta"
}
},
{
@ -209,7 +216,8 @@
"pl": "Materiał: metal",
"pt_BR": "Material: metal",
"pt": "Material: metal",
"eo": "Materialo: metala"
"eo": "Materialo: metala",
"ca": "Material: metall"
}
},
{
@ -230,7 +238,8 @@
"fi": "Materiaali: kivi",
"pl": "Materiał: kamień",
"pt": "Material: pedra",
"eo": "Materialo: ŝtona"
"eo": "Materialo: ŝtona",
"ca": "Material: pedra"
}
},
{
@ -251,7 +260,8 @@
"fi": "Materiaali: betoni",
"pl": "Materiał: beton",
"pt": "Material: concreto",
"eo": "Materialo: betona"
"eo": "Materialo: betona",
"ca": "Material: ciment"
}
},
{
@ -272,7 +282,8 @@
"fi": "Materiaali: muovi",
"pl": "Materiał: plastik",
"pt": "Material: plástico",
"eo": "Materialo: plasta"
"eo": "Materialo: plasta",
"ca": "Material: plàstic"
}
},
{
@ -293,7 +304,8 @@
"fi": "Materiaali: teräs",
"pl": "Materiał: stal",
"pt": "Material: aço",
"eo": "Materialo: ŝtala"
"eo": "Materialo: ŝtala",
"ca": "Material: acer"
}
}
],
@ -309,7 +321,8 @@
"zh_Hant": "這個長椅 (座位) 是什麼做的?",
"pt_BR": "De que é feito o banco (assento)?",
"pl": "Z czego wykonana jest ławka (siedzisko)?",
"pt": "De que é feito o banco (assento)?"
"pt": "De que é feito o banco (assento)?",
"es": "¿De que está hecho el banco (asiento)?"
},
"id": "bench-material"
},
@ -341,7 +354,8 @@
"zh_Hant": "當坐在長椅時,那個人朝向 {direction}°。",
"pl": "Siedząc na ławce, patrzy się w kierunku {direction}°.",
"pt_BR": "Ao sentar-se no banco, olha-se para {direction} °.",
"pt": "Ao sentar-se no banco, olha-se para {direction} °."
"pt": "Ao sentar-se no banco, olha-se para {direction} °.",
"es": "¿Cuando está sentado en el banco, uno mira hacia {direction}º."
},
"freeform": {
"key": "direction",
@ -367,7 +381,8 @@
"pl": "Kolor: {colour}",
"pt": "Cor: {colour}",
"eo": "Koloro: {colour}",
"es": "Color: {colour}"
"es": "Color: {colour}",
"ca": "Color: {colour}"
},
"question": {
"en": "Which colour does this bench have?",
@ -406,7 +421,9 @@
"fi": "Väri: ruskea",
"pl": "Kolor: brązowy",
"pt": "Cor: castanho",
"eo": "Koloro: bruna"
"eo": "Koloro: bruna",
"ca": "Color: marró",
"es": "Color: marrón"
}
},
{
@ -427,7 +444,8 @@
"pl": "Kolor: zielony",
"pt": "Cor: verde",
"eo": "Koloro: verda",
"es": "Color: verde"
"es": "Color: verde",
"ca": "Color: verd"
}
},
{
@ -448,7 +466,8 @@
"pl": "Kolor: szary",
"pt": "Cor: cinzento",
"eo": "Koloro: griza",
"es": "Color: gris"
"es": "Color: gris",
"ca": "Color: gris"
}
},
{
@ -469,7 +488,8 @@
"pl": "Kolor: biały",
"pt": "Cor: branco",
"eo": "Koloro: blanka",
"es": "Color: blanco"
"es": "Color: blanco",
"ca": "Color: blanc"
}
},
{
@ -490,7 +510,8 @@
"pl": "Kolor: czerwony",
"pt": "Cor: vermelho",
"eo": "Koloro: ruĝa",
"es": "Color: rojo"
"es": "Color: rojo",
"ca": "Color: vermell"
}
},
{
@ -511,7 +532,8 @@
"pl": "Kolor: czarny",
"pt": "Cor: preto",
"eo": "Koloro: nigra",
"es": "Color: negro"
"es": "Color: negro",
"ca": "Color: negre"
}
},
{
@ -532,7 +554,8 @@
"pl": "Kolor: niebieski",
"pt": "Cor: azul",
"eo": "Koloro: blua",
"es": "Color: azul"
"es": "Color: azul",
"ca": "Color: blau"
}
},
{
@ -553,7 +576,8 @@
"pl": "Kolor: żółty",
"pt": "Cor: amarelo",
"eo": "Koloro: flava",
"es": "Color: amarillo"
"es": "Color: amarillo",
"ca": "Color: groc"
}
}
],
@ -571,7 +595,8 @@
"zh_Hant": "上一次探察長椅是什麼時候?",
"pt_BR": "Quando esta bancada foi pesquisada pela última vez?",
"pl": "Kiedy ostatnio badano tę ławkę?",
"pt": "Quando esta bancada foi pesquisada pela última vez?"
"pt": "Quando esta bancada foi pesquisada pela última vez?",
"es": "¿Cuándo fue la última vez que se inspeccionó este banco?"
},
"render": {
"en": "This bench was last surveyed on {survey:date}",
@ -584,7 +609,8 @@
"zh_Hant": "這個長椅最後是在 {survey:date} 探查的",
"pt_BR": "Esta bancada foi pesquisada pela última vez em {survey:date}",
"pl": "Ławka ta była ostatnio badana w dniu {survey:date}",
"pt": "Esta bancada foi pesquisada pela última vez em {survey:date}"
"pt": "Esta bancada foi pesquisada pela última vez em {survey:date}",
"es": "Este banco se inspeccionó por última vez el {survey:date}"
},
"freeform": {
"key": "survey:date",
@ -619,7 +645,8 @@
"pt_BR": "uma banco",
"fi": "penkki",
"pl": "Ławka",
"pt": "uma banco"
"pt": "uma banco",
"ca": "un banc"
},
"presiceInput": {
"preferredBackground": "photo"
@ -656,6 +683,8 @@
"description": {
"nl": "Deze laag toont zitbanken en enkele vragen over deze zitbanken",
"en": "A bench is a wooden, metal, stone, ... surface where a human can sit. This layers visualises them and asks a few questions about them.",
"fr": "Un banc est une surface en bois, métal, pierre... sur laquelle un humain peut s'asseoir. Cette couche permet de les visualiser et pose des questions à leur sujet."
"fr": "Un banc est une surface en bois, métal, pierre... sur laquelle un humain peut s'asseoir. Cette couche permet de les visualiser et pose des questions à leur sujet.",
"de": "Diese Karte stellt Sitzbänke aus Holz, Metall, Stein, ... dar und stellt ein paar Fragen, um weitere Informationen zu ergänzen.",
"es": "Un banco es una superficie de madera, metal, piedra, ... donde un humano se puede sentar. Estas capas los visualizan y preguntan algunas preguntas sobre ellos."
}
}

View file

@ -42,7 +42,8 @@
"pt_BR": "Banco",
"fi": "Penkki",
"pl": "Ławka",
"pt": "Banco"
"pt": "Banco",
"ca": "Banc"
},
"mappings": [
{
@ -65,7 +66,8 @@
"zh_Hant": "大眾運輸站點的長椅",
"pt_BR": "Banco em ponto de transporte público",
"pl": "Ławka na przystanku komunikacji miejskiej",
"pt": "Banco em ponto de transporte público"
"pt": "Banco em ponto de transporte público",
"es": "Banco en una parada de transporte público"
}
},
{
@ -109,7 +111,8 @@
"pl": "{name}",
"pt": "{name}",
"eo": "{name}",
"es": "{name}"
"es": "{name}",
"ca": "{name}"
},
"freeform": {
"key": "name"
@ -154,7 +157,8 @@
"en": "There is no bench here",
"fr": "Il n'y a pas de banc ici",
"de": "Hier gibt es keine Bank",
"nl": "Er is hier geen bank"
"nl": "Er is hier geen bank",
"es": "No hay ningún banco aquí"
}
}
]
@ -185,6 +189,7 @@
"en": "A layer showing all public-transport-stops which do have a bench",
"es": "Una capa que muestra todas las paradas de transporte público que tienen bancos",
"nl": "Een laag die stopplaatsen van openbaar vervoer toont waar er een zitbank is",
"fr": "Une couche montrant tous les arrêts de transports publics qui ont un banc"
"fr": "Une couche montrant tous les arrêts de transports publics qui ont un banc",
"de": "Eine Ebene mit allen Haltestellen des öffentlichen Nahverkehrs, die über eine Sitzbank verfügen"
}
}

View file

@ -9,7 +9,8 @@
"zh_Hant": "單車圖書館",
"pt_BR": "Biblioteca de bicicleta",
"de": "Fahrradbibliotheken",
"pt": "Biblioteca de bicicleta"
"pt": "Biblioteca de bicicleta",
"ca": "Biblioteca per a bicicletes"
},
"minzoom": 8,
"source": {
@ -25,7 +26,8 @@
"zh_Hant": "單車圖書館",
"pt_BR": "Biblioteca de bicicleta",
"de": "Fahrradbibliothek",
"pt": "Biblioteca de bicicleta"
"pt": "Biblioteca de bicicleta",
"ca": "Biblioteca per a bicicletes"
},
"mappings": [
{
@ -120,7 +122,8 @@
"nb_NO": "Sykkelleie koster {charge}",
"zh_Hant": "租借單車需要 {charge}",
"pt_BR": "Custos de empréstimo de bicicleta {charge}",
"pt": "Custos de empréstimo de bicicleta {charge}"
"pt": "Custos de empréstimo de bicicleta {charge}",
"es": "Alquilar una bicicleta cuesta {charge}"
},
"freeform": {
"key": "charge",
@ -201,7 +204,8 @@
"ru": "Доступны детские велосипеды",
"zh_Hant": "提供兒童單車",
"pt_BR": "Bicicletas para crianças disponíveis",
"pt": "Bicicletas para crianças disponíveis"
"pt": "Bicicletas para crianças disponíveis",
"es": "Bicicletas para niños disponibles"
}
},
{
@ -215,7 +219,8 @@
"ru": "Доступны велосипеды для взрослых",
"zh_Hant": "有提供成人單車",
"pt_BR": "Bicicletas para adulto disponíveis",
"pt": "Bicicletas para adulto disponíveis"
"pt": "Bicicletas para adulto disponíveis",
"es": "Bicicletas para adultos disponibles"
}
},
{
@ -229,7 +234,8 @@
"ru": "Доступны велосипеды для людей с ограниченными возможностями",
"zh_Hant": "有提供行動不便人士的單車",
"pt_BR": "Bicicletas para deficientes físicos disponíveis",
"pt": "Bicicletas para deficientes físicos disponíveis"
"pt": "Bicicletas para deficientes físicos disponíveis",
"es": "Bicicletas para discapacitados disponibles"
}
}
]

View file

@ -25,7 +25,8 @@
"render": {
"en": "Bicycle rental",
"nl": "Fietsverhuur",
"es": "Alquiler de bicicletas"
"es": "Alquiler de bicicletas",
"de": "Fahrradverleih"
},
"mappings": [
{
@ -36,7 +37,10 @@
},
"then": {
"en": "{name}",
"nl": "{name}"
"nl": "{name}",
"ca": "{name}",
"de": "{name}",
"es": "{name}"
}
}
]
@ -44,7 +48,9 @@
"description": {
"en": "Bicycle rental stations",
"nl": "Fietsverhuustations",
"fr": "Station de location de vélo"
"fr": "Station de location de vélo",
"de": "Fahrradverleihstationen",
"es": "Estaciones de alquiler de bicicletas"
},
"tagRenderings": [
"images",
@ -52,7 +58,9 @@
"id": "bicycle_rental_type",
"question": {
"en": "What kind of bicycle rental is this?",
"nl": "Wat voor fietsverhuur is dit?"
"nl": "Wat voor fietsverhuur is dit?",
"de": "Was ist das für ein Fahrradverleih?",
"es": "¿Qué tipo de alquiler de bicicletas es este?"
},
"mappings": [
{
@ -64,7 +72,9 @@
},
"then": {
"en": "This is a shop whose main focus is bicycle rental",
"nl": "Dit is een zaak die focust op fietsverhuur"
"nl": "Dit is een zaak die focust op fietsverhuur",
"de": "Dies ist ein Geschäft, dessen Schwerpunkt auf dem Fahrradverleih liegt",
"es": "Esta es una tienda que se centra en el alquiler de bicicletas"
}
},
{
@ -76,21 +86,27 @@
},
"then": {
"en": "This is a rental buisiness which rents out various objects and/or vehicles. It rents out bicycles too, but this is not the main focus",
"nl": "Dit is een zaak die verschillende voorwerpen en/of voertuigen verhuurt, waaronder ook fietsen; al zijn fietsen niet de hoofdfocus"
"nl": "Dit is een zaak die verschillende voorwerpen en/of voertuigen verhuurt, waaronder ook fietsen; al zijn fietsen niet de hoofdfocus",
"de": "Dies ist ein Vermietungsunternehmen, das verschiedene Gegenstände und/oder Fahrzeuge vermietet. Es vermietet auch Fahrräder, aber das ist nicht der Hauptschwerpunkt",
"es": "Este es un negocio de alquileres que alquila varios objetos y/o vehículos. También alquila bicicletas, pero este no es el enfoque principal"
}
},
{
"if": "bicycle_rental=docking_station",
"then": {
"en": "This is a shop which sells or repairs bicycles, but also rents out bicycles",
"nl": "Dit is een fietsenmaker of fietswinkel die ook fietsen verhuurt"
"nl": "Dit is een fietsenmaker of fietswinkel die ook fietsen verhuurt",
"de": "Dies ist ein Geschäft, das Fahrräder verkauft oder repariert, aber auch Fahrräder vermietet",
"es": "Esta es una tienda que vende o alquila bicicletas, pero también las alquila"
}
},
{
"if": "bicycle_rental=key_dispensing_machine",
"then": {
"en": "This is an automated docking station, where a bicycle is mechanically locked into a structure",
"nl": "Dit is een docking station waar de fietsen mechanisch in een grotere structuur worden vastgemaakt"
"nl": "Dit is een docking station waar de fietsen mechanisch in een grotere structuur worden vastgemaakt",
"de": "Dies ist eine automatisierte Dockingstation, bei der ein Fahrrad mechanisch in einer Struktur verriegelt wird",
"es": "Esta es una estación automática, en la que una bici se asegura mecánicamente en una estructura"
}
},
{
@ -98,7 +114,8 @@
"then": {
"en": "A machine is present which dispenses and accepts keys, eventually after authentication and/or payment. The bicycles are parked nearby",
"nl": "Hier is een machine die fietssleutels verdeelt en terugneemt, eventueel na aanmelden of betaling. De fietsen staan in de buurt geparkeerd",
"hu": "Ez egy leadási pont: ennek a kerékpárkölcsönzőnek a kijelölt kerékpártárolója"
"hu": "Ez egy leadási pont: ennek a kerékpárkölcsönzőnek a kijelölt kerékpártárolója",
"de": "Es gibt einen Automaten, der Schlüssel ausgibt und annimmt, eventuell nach Authentifizierung und/oder Bezahlung. Die Fahrräder sind in der Nähe geparkt"
}
}
],
@ -137,11 +154,15 @@
],
"question": {
"en": "What kind of bicycles and accessories are rented here?",
"nl": "Wat voor soort fietsen en fietstoebehoren worden hier verhuurd?"
"nl": "Wat voor soort fietsen en fietstoebehoren worden hier verhuurd?",
"de": "Welche Art von Fahrrädern und Zubehör wird hier vermietet?",
"es": "¿Qué tipo de bicicletas y accesorios se alquilan aquí?"
},
"render": {
"en": "{rental} is rented here",
"nl": "{rental} kunnen hier uitgeleend worden"
"nl": "{rental} kunnen hier uitgeleend worden",
"de": "{rental} wird hier vermietet",
"es": "{rental} se alquilan aquí"
},
"freeform": {
"key": "rental",
@ -153,49 +174,62 @@
"if": "rental=city_bike",
"then": {
"en": "Normal city bikes can be rented here",
"nl": "Gewone stadsfietsen kunnen hier gehuurd worden"
"nl": "Gewone stadsfietsen kunnen hier gehuurd worden",
"de": "Normale Stadtfahrräder können hier gemietet werden",
"es": "Aquí se pueden alquilar bicis normales"
}
},
{
"if": "rental=ebike",
"then": {
"en": "Electrical bikes can be rented here",
"nl": "Elektrische fietsen kunnen hier gehuurd worden"
"nl": "Elektrische fietsen kunnen hier gehuurd worden",
"de": "Elektrofahrräder können hier gemietet werden",
"es": "Aquí se pueden alquilar bicis eléctricas"
}
},
{
"if": "rental=bmx",
"then": {
"en": "BMX bikes can be rented here",
"nl": "BMX-fietsen kunnen hier gehuurd worden"
"nl": "BMX-fietsen kunnen hier gehuurd worden",
"de": "BMX-Räder können hier gemietet werden",
"es": "Aquí se pueden alquilar bicis BMX"
}
},
{
"if": "rental=mtb",
"then": {
"en": "Mountainbikes can be rented here",
"nl": "Mountainbikes kunnen hier gehuurd worden"
"nl": "Mountainbikes kunnen hier gehuurd worden",
"de": "Mountainbikes können hier gemietet werden",
"es": "Aquí se pueden alquilar bicis de montaña"
}
},
{
"if": "rental=kid_bike",
"then": {
"en": "Bikes for childs can be rented here",
"nl": "Kinderfietsen kunnen hier gehuurd worden"
"nl": "Kinderfietsen kunnen hier gehuurd worden",
"de": "Kinderfahrräder können hier gemietet werden",
"es": "Aquí se pueden alquilar bicis infantiles"
}
},
{
"if": "rental=tandem",
"then": {
"en": "Tandem bicycles can be rented here",
"nl": "Tandems kunnen hier gehuurd worden"
"nl": "Tandems kunnen hier gehuurd worden",
"de": "Tandems können hier gemietet werden"
}
},
{
"if": "rental=racebike",
"then": {
"en": "Race bicycles can be rented here",
"nl": "Wielerfietsen (sportfietsen) kunnen hier gehuurd worden"
"nl": "Wielerfietsen (sportfietsen) kunnen hier gehuurd worden",
"de": "Rennräder können hier gemietet werden",
"es": "Aquí se pueden alquilar bicicletas de carreras"
}
}
]
@ -211,49 +245,63 @@
"city_bike",
{
"en": "city bikes",
"nl": "stadsfietsen"
"nl": "stadsfietsen",
"de": "Stadträder",
"es": "bicis de ciudad"
}
],
[
"ebike",
{
"en": "electrical bikes",
"nl": "elektrische fietsen"
"nl": "elektrische fietsen",
"de": "Elektrofahrräder",
"es": "bicis eléctricas"
}
],
[
"kid_bike",
{
"en": "bikes for children",
"nl": "kinderfietsen"
"nl": "kinderfietsen",
"de": "Kinderfahrräder",
"es": "bicis infantiles"
}
],
[
"bmx",
{
"en": "BMX bikes",
"nl": "BMX-fietsen"
"nl": "BMX-fietsen",
"de": "BMX-Räder",
"es": "bicis BMX"
}
],
[
"mtb",
{
"en": "mountainbike",
"nl": "mountainbike"
"nl": "mountainbike",
"ca": "bicicleta de muntanya",
"de": "Mountainbikes",
"es": "bicis de montaña"
}
],
[
"bicycle_pannier",
{
"en": "bicycle panniers",
"nl": "fietstassen"
"nl": "fietstassen",
"de": "Fahrradtaschen"
}
],
[
"tandem_bicycle",
{
"en": "tandem",
"nl": "tandem"
"nl": "tandem",
"ca": "tàndem",
"de": "Tandems"
}
]
]
@ -286,7 +334,9 @@
"title": {
"en": "a bicycle rental shop",
"nl": "een fietsverhuurzaak",
"fr": "une magasin de location de vélos"
"fr": "une magasin de location de vélos",
"de": "Ein Fahrradverleih",
"es": "una tienda de alquiler de bicicletas"
},
"tags": [
"amenity=bicycle_rental",
@ -295,13 +345,15 @@
"description": {
"en": "A manned shop which focuses on bicycle rental",
"nl": "Een bemande winkel die focust op fietsverhuur",
"fr": "Un magasin qui priorise la location de vélos"
"fr": "Un magasin qui priorise la location de vélos",
"de": "Ein Geschäft, das sich auf den Fahrradverleih konzentriert"
}
},
{
"title": {
"en": "a bicycle rental",
"nl": "een fietsverhuur"
"nl": "een fietsverhuur",
"de": "Ein Fahrradverleih"
},
"tags": [
"amenity=bicycle_rental"
@ -346,7 +398,9 @@
"explanation": {
"nl": "{title()} is permanent gestopt",
"en": "{title()} has closed down permanently",
"fr": "{title()} a était fermé de façon permanente"
"fr": "{title()} a était fermé de façon permanente",
"de": "{title()} ist dauerhaft geschlossen",
"es": "{title()} ha cerrado permanentemente"
},
"changesetMessage": "shop_closed"
}
@ -361,7 +415,9 @@
"then": {
"en": "This bicycle shop used to rent out bikes but doesn't rent out bikes anymore",
"nl": "Deze fietszaak verhuurde vroeger fietsen, maar nu niet meer",
"fr": "Ce magasin de vélo louait des vélos, mais n'en loue plus maintenant"
"fr": "Ce magasin de vélo louait des vélos, mais n'en loue plus maintenant",
"de": "Dieser Fahrradladen vermietete früher Fahrräder, aber jetzt nicht mehr",
"es": "Esta tienda de bicicletas alquilaba bicis, pero ya no lo hace"
}
}
]

View file

@ -78,7 +78,8 @@
"de": "Ist dieser Automat noch in Betrieb?",
"zh_Hant": "這個自動販賣機仍有運作嗎?",
"pt_BR": "Esta máquina de venda automática ainda está operacional?",
"pt": "Esta máquina de venda automática ainda está operacional?"
"pt": "Esta máquina de venda automática ainda está operacional?",
"es": "¿Todavía es operacional esta máquina exprendedora?"
},
"render": {
"en": "The operational status is <i>{operational_status}</i>",
@ -89,7 +90,8 @@
"ru": "Рабочий статус: <i> {operational_status}</i>",
"zh_Hant": "運作狀態是 <i>{operational_status}</i>",
"pt_BR": "O estado operacional é: <i>{operational_status}</i>",
"pt": "O estado operacional é: <i>{operational_status}</i>"
"pt": "O estado operacional é: <i>{operational_status}</i>",
"es": "El estado operacional es <i>{operational_status}</i></i>"
},
"freeform": {
"key": "operational_status"
@ -124,7 +126,8 @@
"de": "Dieser Automat ist kaputt",
"zh_Hant": "這個自動販賣機沒有運作了",
"pt_BR": "Esta máquina de venda automática está quebrada",
"pt": "Esta máquina de venda automática está quebrada"
"pt": "Esta máquina de venda automática está quebrada",
"es": "Esta máquina exprendedora está rota"
}
},
{
@ -140,7 +143,8 @@
"de": "Dieser Automat ist geschlossen",
"zh_Hant": "這個自動販賣機已經關閉了",
"pt_BR": "Esta máquina de venda automática está fechada",
"pt": "Esta máquina de venda automática está fechada"
"pt": "Esta máquina de venda automática está fechada",
"es": "Esta máquina exprendedora está cerrada"
}
}
],
@ -287,6 +291,7 @@
],
"description": {
"en": "A layer showing vending machines for bicycle tubes (either purpose-built bicycle tube vending machines or classical vending machines with bicycle tubes and optionally additional bicycle related objects such as lights, gloves, locks, ...)",
"nl": "Een laag met verkoopsautomaten met binnenbanden voor fietsen (dit kan een automaat zijn met énkel fietsbanden, of een gewone automaat met fietsbanden en andere fietsaccessoires zoals lichten, handschoenen, sloten,...)"
"nl": "Een laag met verkoopsautomaten met binnenbanden voor fietsen (dit kan een automaat zijn met énkel fietsbanden, of een gewone automaat met fietsbanden en andere fietsaccessoires zoals lichten, handschoenen, sloten,...)",
"de": "Eine Ebene mit Automaten für Fahrradschläuche (entweder spezielle Fahrradschlauch-Automaten oder klassische Automaten mit Fahrradschläuchen und optional zusätzlichen fahrradbezogenen Gegenständen wie Lampen, Handschuhe, Schlösser, ...)"
}
}

View file

@ -11,7 +11,8 @@
"ru": "Велосипедное кафе",
"zh_Hant": "單車咖啡廳",
"pt_BR": "Café de bicicletas",
"pt": "Café de bicicletas"
"pt": "Café de bicicletas",
"ca": "Cafeteria per a bicicletes"
},
"minzoom": 13,
"source": {
@ -50,7 +51,8 @@
"ru": "Велосипедное кафе",
"zh_Hant": "單車咖啡廳",
"pt_BR": "Café de bicicleta",
"pt": "Café de bicicleta"
"pt": "Café de bicicleta",
"ca": "Cafeteria per a bicicletes"
},
"mappings": [
{
@ -282,7 +284,8 @@
"zh_Hans": "{name}的电话号码是什么?",
"zh_Hant": "{name} 的電話號碼是?",
"pt_BR": "Qual o número de telefone de {name}?",
"pt": "Qual é o número de telefone de {name}?"
"pt": "Qual é o número de telefone de {name}?",
"es": "¿Cual es el número de teléfono de {name}?"
},
"render": "<a href='tel:{phone}'>{phone}</a>",
"freeform": {
@ -303,7 +306,8 @@
"zh_Hans": "{name}的电子邮箱是什么?",
"zh_Hant": "{name} 的電子郵件地址是?",
"pt_BR": "Qual o endereço de email de {name}?",
"pt": "Qual o endereço de email de {name}?"
"pt": "Qual o endereço de email de {name}?",
"es": "¿Cual es la dirección de correo electrónico de {name}?"
},
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>",
"freeform": {
@ -378,6 +382,7 @@
],
"description": {
"en": "A bike café is a café geared towards cyclists, for example with services such as a pump, with lots of bicycle-related decoration, ...",
"nl": "Een fietscafé is een café dat gericht is op fietsers, bijvoorbeeld omdat het een fietspomp heeft, fietsgerelateerde decoratie heeft enzovoorts."
"nl": "Een fietscafé is een café dat gericht is op fietsers, bijvoorbeeld omdat het een fietspomp heeft, fietsgerelateerde decoratie heeft enzovoorts.",
"de": "Ein Fahrradcafé ist ein Café, das auf Radfahrer ausgerichtet ist, zum Beispiel mit Dienstleistungen wie einer Pumpe, mit viel fahrradbezogener Dekoration, ..."
}
}

View file

@ -9,7 +9,8 @@
"zh_Hant": "單車清理服務",
"pt_BR": "Serviço de limpeza de bicicletas",
"pt": "Serviço de limpeza de bicicletas",
"ru": "Услуги по чистке велосипедов"
"ru": "Услуги по чистке велосипедов",
"es": "Servicio de limpieza de bicis"
},
"title": {
"render": {
@ -21,7 +22,8 @@
"zh_Hant": "單車清理服務",
"pt_BR": "Serviço de limpeza de bicicletas",
"pt": "Serviço de limpeza de bicicletas",
"ru": "Услуги по чистке велосипедов"
"ru": "Услуги по чистке велосипедов",
"es": "Servicio de limpieza de bicis"
},
"mappings": [
{
@ -34,7 +36,8 @@
"de": "Fahrrad-Reinigungsdienst<i>{name}</i>",
"zh_Hant": "單車清理服務 <i>{name}</i>",
"pt_BR": "Serviço de limpeza de bicicletas <i>{name}</i>",
"pt": "Serviço de limpeza de bicicletas <i>{name}</i>"
"pt": "Serviço de limpeza de bicicletas <i>{name}</i>",
"es": "Servicio de limpieza de bicis <i>{name}</i>"
}
}
]
@ -61,7 +64,8 @@
"zh_Hant": "單車清理服務",
"pt_BR": "uma serviço de limpeza de bicicletas",
"pt": "uma serviço de limpeza de bicicletas",
"ru": "Услуги по чистке велосипедов"
"ru": "Услуги по чистке велосипедов",
"es": "un servicio de limpieza de bicis"
},
"tags": [
"amenity=bicycle_wash"
@ -74,12 +78,14 @@
"question": {
"en": "How much does it cost to use the cleaning service?",
"de": "Wie viel kostet die Nutzung des Reinigungsdienstes?",
"nl": "Hoeveel kost het gebruik van het fietsschoonmaakpunt?"
"nl": "Hoeveel kost het gebruik van het fietsschoonmaakpunt?",
"es": "¿Cuánto cuesta utilizar el servicio de limpieza?"
},
"render": {
"en": "Using the cleaning service costs {service:bicycle:cleaning:charge}",
"de": "Nutzung des Reinigungsservice kostet {service:bicycle:cleaning:charge}",
"nl": "Het gebruik van het fietsschoonmaakpunt kost {service:bicycle:cleaning:charge}"
"nl": "Het gebruik van het fietsschoonmaakpunt kost {service:bicycle:cleaning:charge}",
"es": "Utilizar el servicio de limpieza cuesta {service:bicycle:cleaning:charge}"
},
"condition": {
"and": [
@ -100,7 +106,8 @@
"then": {
"en": "The cleaning service is free to use",
"de": "Der Reinigungsservice ist kostenlos",
"nl": "Het fietsschoonmaakpunt is gratis"
"nl": "Het fietsschoonmaakpunt is gratis",
"es": "El servicio de limpieza es gratis"
}
},
{
@ -108,7 +115,8 @@
"then": {
"en": "Free to use",
"de": "Kostenlose Nutzung",
"nl": "Gratis te gebruiken"
"nl": "Gratis te gebruiken",
"es": "Gratis"
},
"hideInAnswer": true
},
@ -117,7 +125,8 @@
"then": {
"en": "The cleaning service has a fee, but the amount is not known",
"de": "Der Reinigungsdienst ist kostenpflichtig, aber der Betrag ist nicht bekannt",
"nl": "Het fietsschoonmaakpunt is betalend, maar de prijs is onbekend"
"nl": "Het fietsschoonmaakpunt is betalend, maar de prijs is onbekend",
"es": "El servicio de limpieza tiene una tasa, pero la cantidad se desconoce"
},
"hideInAnswer": true
}
@ -128,12 +137,14 @@
"question": {
"en": "How much does it cost to use the cleaning service?",
"de": "Wie viel kostet die Nutzung des Reinigungsdienstes?",
"nl": "Hoeveel kost het gebruik van het fietsschoonmaakpunt?"
"nl": "Hoeveel kost het gebruik van het fietsschoonmaakpunt?",
"es": "¿Cuánto cuesta utilizar el servicio de limpieza?"
},
"render": {
"en": "Using the cleaning service costs {charge}",
"de": "Die Nutzung des Reinigungsdienstes kostet {charge}",
"nl": "Het gebruik van het fietsschoonmaakpunt kost {charge}"
"nl": "Het gebruik van het fietsschoonmaakpunt kost {charge}",
"es": "Utilizar el servicio de limpieza cuesta {charge}"
},
"condition": {
"or": [
@ -153,7 +164,8 @@
"then": {
"en": "Free to use cleaning service",
"de": "Kostenloser Reinigungsservice",
"nl": "Gratis fietsschoonmaakpunt"
"nl": "Gratis fietsschoonmaakpunt",
"es": "Un servicio de limpieza gratis"
}
},
{
@ -161,7 +173,8 @@
"then": {
"en": "Free to use",
"de": "Kostenlose Nutzung",
"nl": "Gratis te gebruiken"
"nl": "Gratis te gebruiken",
"es": "Gratis"
},
"hideInAnswer": true
},
@ -170,7 +183,8 @@
"then": {
"en": "The cleaning service has a fee",
"de": "Der Reinigungsservice ist kostenpflichtig",
"nl": "Je moet betalen voor het fietsschoonmaakpunt"
"nl": "Je moet betalen voor het fietsschoonmaakpunt",
"es": "El servicio de limpieza tiene una tarifa"
}
}
],
@ -219,6 +233,8 @@
],
"description": {
"en": "A layer showing facilities where one can clean their bike",
"nl": "Een laag die plaatsen toont waar je je fiets kunt wassen"
"nl": "Een laag die plaatsen toont waar je je fiets kunt wassen",
"de": "Eine Ebene mit Einrichtungen, in denen man sein Fahrrad reinigen kann",
"es": "Una capa que muestra facilidades en las que uno puede limpiar su bici"
}
}

View file

@ -12,7 +12,9 @@
"ru": "Велосипедная парковка",
"pl": "Parking dla rowerów",
"pt_BR": "Estacionamento de bicicletas",
"pt": "Estacionamento de bicicletas"
"pt": "Estacionamento de bicicletas",
"ca": "Aparcament per a bicicletes",
"es": "Aparcamiento de bicis"
},
"minzoom": 17,
"source": {
@ -36,7 +38,8 @@
"ru": "Велосипедная парковка",
"pl": "Parking dla rowerów",
"pt_BR": "uma estacionamento de bicicletas",
"pt": "uma estacionamento de bicicletas"
"pt": "uma estacionamento de bicicletas",
"es": "un aparcamiento de bicis"
},
"tags": [
"amenity=bicycle_parking"
@ -56,7 +59,9 @@
"ru": "Велосипедная парковка",
"pl": "Parking dla rowerów",
"pt_BR": "Estacionamento de bicicletas",
"pt": "Estacionamento de bicicletas"
"pt": "Estacionamento de bicicletas",
"ca": "Aparcament per a bicicletes",
"es": "Aparcamiento de bicis"
}
},
"tagRenderings": [
@ -74,7 +79,8 @@
"zh_Hant": "這是那種類型的單車停車場?",
"pl": "Jaki jest typ tego parkingu dla rowerów?",
"pt_BR": "Qual o tipo deste estacionamento de bicicletas?",
"pt": "Qual o tipo deste estacionamento de bicicletas?"
"pt": "Qual o tipo deste estacionamento de bicicletas?",
"es": "¿Cual es el tipo de este aparcamiento de bicicletas?"
},
"render": {
"en": "This is a bicycle parking of the type: {bicycle_parking}",
@ -88,7 +94,8 @@
"ru": "Это велопарковка типа {bicycle_parking}",
"pl": "Jest to parking rowerowy typu: {bicycle_parking}",
"pt_BR": "Este é um estacionamento de bicicletas do tipo: {bicycle_parking}",
"pt": "Este é um estacionamento de bicicletas do tipo: {bicycle_parking}"
"pt": "Este é um estacionamento de bicicletas do tipo: {bicycle_parking}",
"es": "Este es un aparcamiento de bicicletas del tipo: {bicycle_parking}"
},
"freeform": {
"key": "bicycle_parking",
@ -107,7 +114,8 @@
"de": "Fahrradbügel",
"hu": "Korlát",
"it": "Archetti",
"zh_Hant": "單車架"
"zh_Hant": "單車架",
"ca": "Bastidors de grapes"
},
"icon": {
"path": "./assets/layers/bike_parking/staple.svg",
@ -124,7 +132,8 @@
"de": "Metallgestänge",
"hu": "Kerékbefogó hurok",
"it": "Scolapiatti",
"zh_Hant": "車輪架/圓圈"
"zh_Hant": "車輪架/圓圈",
"ca": "Portarodes/bucles"
},
"icon": {
"path": "./assets/layers/bike_parking/wall_loops.svg",
@ -140,7 +149,8 @@
"gl": "Cadeado para guiador",
"de": "Halter für Fahrradlenker",
"it": "Blocca manubrio",
"zh_Hant": "車把架"
"zh_Hant": "車把架",
"ca": "Suport de manillar"
},
"icon": {
"path": "./assets/layers/bike_parking/handlebar_holder.svg",
@ -157,7 +167,8 @@
"de": "Gestell",
"zh_Hant": "車架",
"it": "Rastrelliera",
"ru": "Стойка"
"ru": "Стойка",
"ca": "Enganxament"
},
"icon": {
"path": "./assets/layers/bike_parking/rack.svg",
@ -175,7 +186,8 @@
"hu": "Kétszintű",
"zh_Hant": "兩層",
"it": "A due piani",
"ru": "Двухуровневая"
"ru": "Двухуровневая",
"ca": "De dos nivells"
},
"icon": {
"path": "./assets/layers/bike_parking/two_tier.svg",
@ -193,7 +205,8 @@
"hu": "Fészer",
"zh_Hant": "車棚",
"it": "Rimessa",
"ru": "Навес"
"ru": "Навес",
"es": "Caseta"
},
"icon": {
"path": "./assets/layers/bike_parking/shed.svg",
@ -208,7 +221,9 @@
"fr": "Potelet",
"it": "Colonnina",
"de": "Poller",
"zh_Hant": "柱子"
"zh_Hant": "柱子",
"ca": "Pilona",
"es": "Bolardo"
},
"icon": {
"path": "./assets/layers/bike_parking/bollard.svg",
@ -223,7 +238,8 @@
"fr": "Zone au sol qui est marquée pour le stationnement des vélos",
"it": "Una zona del pavimento che è marcata per il parcheggio delle bici",
"de": "Ein Bereich auf dem Boden, der für das Abstellen von Fahrrädern gekennzeichnet ist",
"zh_Hant": "樓層當中標示為單車停車場的區域"
"zh_Hant": "樓層當中標示為單車停車場的區域",
"es": "Una área en el suelo que está marcada para el aparcamiento de bicicletas"
}
}
],
@ -239,7 +255,8 @@
"pl": "Jaka jest względna lokalizacja tego parkingu rowerowego?",
"pt_BR": "Qual a localização relativa deste estacionamento de bicicletas?",
"de": "Wo befinden sich diese Fahrradabstellplätze?",
"pt": "Qual a localização relativa deste estacionamento de bicicletas?"
"pt": "Qual a localização relativa deste estacionamento de bicicletas?",
"es": "¿Cual es la localización relativa de este aparcamiento de bicicletas?"
},
"mappings": [
{
@ -253,7 +270,9 @@
"de": "Tiefgarage",
"zh_Hant": "地下停車場",
"pt_BR": "Estacionamento subterrâneo",
"pt": "Estacionamento subterrâneo"
"pt": "Estacionamento subterrâneo",
"ca": "Aparcament subterrani",
"es": "Aparcamiento subterráneo"
}
},
{
@ -268,7 +287,8 @@
"zh_Hant": "地面停車場",
"pt_BR": "Estacionamento de superfície",
"pt": "Estacionamento de superfície",
"hu": "Felszíni parkoló"
"hu": "Felszíni parkoló",
"es": "Aparcamiento a nivel de calle"
}
},
{
@ -283,7 +303,9 @@
"zh_Hant": "屋頂停車場",
"pt_BR": "Estacionamento no telhado",
"pt": "Estacionamento no telhado",
"ru": "Парковка на крыше"
"ru": "Парковка на крыше",
"ca": "Aparcament al terrat",
"es": "Aparcamiento de azotea"
}
},
{
@ -315,7 +337,8 @@
"it": "È un parcheggio coperto? Indicare “coperto” per parcheggi allinterno.",
"zh_Hant": "這個停車場是否有車棚?如果是室內停車場也請選擇\"遮蔽\"。",
"pt_BR": "Este estacionamento é coberto? Também selecione \"coberto\" para estacionamentos internos.",
"pt": "Este estacionamento é coberto? Também selecione \"coberto\" para estacionamentos internos."
"pt": "Este estacionamento é coberto? Também selecione \"coberto\" para estacionamentos internos.",
"es": "¿Está cubierto este aparcamiento? Selecciona \"cubierto\" también para aparcamientos interiores."
},
"condition": {
"and": [
@ -337,7 +360,8 @@
"zh_Hant": "這個停車場有遮蔽 (有屋頂)",
"ru": "Это крытая парковка (есть крыша/навес)",
"pt_BR": "Este estacionamento é coberto (tem um telhado)",
"pt": "Este estacionamento é coberto (tem um telhado)"
"pt": "Este estacionamento é coberto (tem um telhado)",
"es": "Este aparcamiento está cubierto (tiene un tejado)"
}
},
{
@ -353,7 +377,8 @@
"zh_Hant": "這個停車場沒有遮蔽",
"ru": "Это открытая парковка",
"pt_BR": "Este estacionamento não é coberto",
"pt": "Este estacionamento não é coberto"
"pt": "Este estacionamento não é coberto",
"es": "Este aparcamiento no está cubierto"
}
}
],
@ -367,7 +392,8 @@
"gl": "Cantas bicicletas caben neste aparcadoiro de bicicletas (incluídas as posíbeis bicicletas de carga)?",
"de": "Wie viele Fahrräder passen auf diesen Fahrrad-Parkplatz (einschließlich möglicher Lastenfahrräder)?",
"it": "Quante biciclette entrano in questo parcheggio per bici (incluse le eventuali bici da trasporto)?",
"zh_Hant": "這個單車停車場能放幾台單車 (包括裝箱單車)"
"zh_Hant": "這個單車停車場能放幾台單車 (包括裝箱單車)",
"es": "¿Cuántas bicicletas caben en este aparcamiento de bicicletas (incluyendo posibles bicicletas de carga)?"
},
"render": {
"en": "Place for {capacity} bikes",
@ -379,7 +405,8 @@
"zh_Hant": "{capacity} 單車的地方",
"ru": "Место для {capacity} велосипеда(ов)",
"pt_BR": "Lugar para {capacity} bicicletas",
"pt": "Lugar para {capacity} bicicletas"
"pt": "Lugar para {capacity} bicicletas",
"es": "Espacio para {capacity} bicis"
},
"freeform": {
"key": "capacity",
@ -397,7 +424,8 @@
"zh_Hant": "誰可以使用這個單車停車場?",
"ru": "Кто может пользоваться этой велопарковкой?",
"pt_BR": "Quem pode usar este estacionamento de bicicletas?",
"pt": "Quem pode usar este estacionamento de bicicletas?"
"pt": "Quem pode usar este estacionamento de bicicletas?",
"es": "¿Quién puede utilizar este aparcamiento de bicicletas?"
},
"render": {
"en": "{access}",
@ -411,7 +439,9 @@
"fi": "{access}",
"pt_BR": "{access}",
"pt": "{access}",
"eo": "{access}"
"eo": "{access}",
"ca": "{access}",
"es": "{access}"
},
"freeform": {
"key": "access",
@ -430,7 +460,9 @@
"de": "Öffentlich zugänglich",
"zh_Hant": "公開可用",
"pt_BR": "Acessível ao público",
"pt": "Acessível ao público"
"pt": "Acessível ao público",
"ca": "Accessible al públic",
"es": "Accesible públicamente"
}
},
{
@ -443,7 +475,8 @@
"zh_Hant": "通行性主要是為了企業的顧客",
"pt_BR": "Acesso é principalmente para visitantes de uma empresa",
"de": "Der Zugang ist in erster Linie für Besucher eines Unternehmens bestimmt",
"pt": "Acesso é principalmente para visitantes de uma empresa"
"pt": "Acesso é principalmente para visitantes de uma empresa",
"es": "El acceso es primariamente para visitantes a un negocio"
}
},
{
@ -456,7 +489,8 @@
"zh_Hant": "通行性僅限學校、公司或組織的成員",
"pt_BR": "Acesso é limitado aos membros de uma escola, companhia ou organização",
"de": "Der Zugang ist beschränkt auf Mitglieder einer Schule, eines Unternehmens oder einer Organisation",
"pt": "Acesso é limitado aos membros de uma escola, companhia ou organização"
"pt": "Acesso é limitado aos membros de uma escola, companhia ou organização",
"es": "El acceso se limita a miembros de una escuela, compañía u organización"
}
}
],
@ -472,7 +506,8 @@
"it": "Questo parcheggio dispone di posti specifici per le bici da trasporto?",
"zh_Hant": "這個單車停車場有地方放裝箱的單車嗎?",
"pt_BR": "O estacionamento de bicicletas tem vagas para bicicletas de carga?",
"pt": "O estacionamento de bicicletas tem vagas para bicicletas de carga?"
"pt": "O estacionamento de bicicletas tem vagas para bicicletas de carga?",
"es": "¿Este aparcamiento de bicicletas tiene huevos para bicicletas de carga?"
},
"mappings": [
{
@ -480,13 +515,14 @@
"then": {
"en": "This parking has room for cargo bikes",
"nl": "Deze parking heeft plaats voor bakfietsen",
"gl": "Este aparcadoiro ten espazo para bicicletas de carga.",
"gl": "Este aparcadoiro ten espazo para bicicletas de carga",
"de": "Dieser Parkplatz bietet Platz für Lastenfahrräder",
"fr": "Ce parking a de la place pour les vélos cargo",
"it": "Questo parcheggio ha posto per bici da trasporto",
"zh_Hant": "這個停車場有地方可以放裝箱單車",
"pt_BR": "Este estacionamento tem vagas para bicicletas de carga",
"pt": "Este estacionamento tem vagas para bicicletas de carga"
"pt": "Este estacionamento tem vagas para bicicletas de carga",
"es": "Este aparcamiento tiene espacio para bicicletas de carga"
}
},
{
@ -500,7 +536,8 @@
"it": "Questo parcheggio ha posti destinati (ufficialmente) alle bici da trasporto.",
"zh_Hant": "這停車場有設計 (官方) 空間給裝箱的單車。",
"pt_BR": "Este estacionamento tem vagas (oficiais) projetadas para bicicletas de carga.",
"pt": "Este estacionamento tem vagas (oficiais) projetadas para bicicletas de carga."
"pt": "Este estacionamento tem vagas (oficiais) projetadas para bicicletas de carga.",
"es": "Este aparcamiento tiene huecos (oficialmente) designados para bicicletas de carga."
}
},
{
@ -513,7 +550,8 @@
"fr": "Il est interdit de garer des vélos cargo",
"it": "Il parcheggio delle bici da trasporto è proibito",
"pt_BR": "Você não tem permissão para estacionar bicicletas de carga",
"pt": "Não tem permissão para estacionar bicicletas de carga"
"pt": "Não tem permissão para estacionar bicicletas de carga",
"es": "No se permite aparcar bicicletas de carga"
}
}
],
@ -528,7 +566,8 @@
"de": "Wie viele Lastenfahrräder passen auf diesen Fahrrad-Parkplatz?",
"it": "Quante bici da trasporto entrano in questo parcheggio per bici?",
"pt_BR": "Quantas bicicletas de carga cabem neste estacionamento de bicicletas?",
"pt": "Quantas bicicletas de carga cabem neste estacionamento de bicicletas?"
"pt": "Quantas bicicletas de carga cabem neste estacionamento de bicicletas?",
"es": "¿Cuántas bicicletas de carga caben en este aparcamiento de bicicletas?"
},
"render": {
"en": "This parking fits {capacity:cargo_bike} cargo bikes",
@ -538,7 +577,8 @@
"de": "Auf diesen Parkplatz passen {capacity:cargo_bike} Lastenfahrräder",
"it": "Questo parcheggio può contenere {capacity:cargo_bike} bici da trasporto",
"pt_BR": "Neste estacionamento cabem {capacity:cargo_bike} bicicletas de carga",
"pt": "Neste estacionamento cabem {capacity:cargo_bike} bicicletas de carga"
"pt": "Neste estacionamento cabem {capacity:cargo_bike} bicicletas de carga",
"es": "En este aparcamiento caben {capacity:cargo_bike} bicis de carga"
},
"condition": "cargo_bike~designated|yes",
"freeform": {
@ -579,6 +619,8 @@
],
"description": {
"en": "A layer showing where you can park your bike",
"nl": "Een laag die toont waar je je fiets kunt parkeren"
"nl": "Een laag die toont waar je je fiets kunt parkeren",
"de": "Eine Ebene, die anzeigt, wo Sie Ihr Fahrrad abstellen können",
"es": "Una capa que muestra donde puedes aparcar tu bici"
}
}

View file

@ -8,7 +8,8 @@
"de": "Fahrradstationen (Reparatur, Pumpe oder beides)",
"it": "Stazioni bici (riparazione, gonfiaggio o entrambi)",
"pt_BR": "Estações de bicicletas (reparo, bomba ou ambos)",
"ru": "Велостанции (ремонт, накачка шин или сразу всё)"
"ru": "Велостанции (ремонт, накачка шин или сразу всё)",
"es": "Bomba y reparación de bicicletas"
},
"minzoom": 13,
"source": {
@ -27,7 +28,8 @@
"de": "Fahrradstation (Pumpe & Reparatur)",
"it": "Stazione bici (gonfiaggio & riparazione)",
"pt_BR": "Estação de bicicletas (bomba e reparo)",
"ru": "Велостанция (накачка шин и ремонт)"
"ru": "Велостанция (накачка шин и ремонт)",
"es": "Estación de bicis (bomba y reparación)"
},
"mappings": [
{
@ -46,7 +48,8 @@
"it": "Stazione riparazione bici",
"pt_BR": "Estação de reparo de bicicletas",
"pt": "Estação de reparo de bicicletas",
"ru": "Станция обслуживания велосипедов"
"ru": "Станция обслуживания велосипедов",
"es": "Estación de reparación de bicis"
}
},
{
@ -65,7 +68,8 @@
"it": "Stazione riparazione bici",
"pt_BR": "Estação de reparo de bicicletas",
"pt": "Estação de reparo de bicicletas",
"ru": "Станция обслуживания велосипедов"
"ru": "Станция обслуживания велосипедов",
"es": "Estación de reparación de bicis"
}
},
{
@ -88,7 +92,9 @@
"de": "Kaputte Pumpe",
"it": "Pompa rotta",
"ru": "Насос сломан",
"pt_BR": "Bomba quebrada"
"pt_BR": "Bomba quebrada",
"ca": "Bomba trencada",
"es": "Bomba rota"
}
},
{
@ -107,7 +113,8 @@
"de": "Fahrradpumpe <i>{name}</i>",
"it": "Pompa per bici <i>{name}</i>",
"ru": "Велосипедный насос <i>{name}</i>",
"pt_BR": "Bomba de bicicleta <i>{name}</i>"
"pt_BR": "Bomba de bicicleta <i>{name}</i>",
"es": "Bomba de bicicletas <i>{name}</i>"
}
},
{
@ -125,7 +132,9 @@
"de": "Fahrradpumpe",
"it": "Pompa per bici",
"ru": "Велосипедный насос",
"pt_BR": "Bomba de bicicleta"
"pt_BR": "Bomba de bicicleta",
"ca": "Bomba de bicicleta",
"es": "Bomba para bicicletas"
}
}
]
@ -149,7 +158,8 @@
"de": "Welche Einrichtungen stehen an dieser Fahrradstation zur Verfügung?",
"it": "Quali servizi sono disponibili in questa stazione per bici?",
"pt_BR": "Quais serviços estão disponíveis nesta estação de bicicletas?",
"pt": "Quais serviços estão disponíveis nesta estação de bicicletas?"
"pt": "Quais serviços estão disponíveis nesta estação de bicicletas?",
"es": "¿Qué servicios están disponibles en esta localización?"
},
"mappings": [
{
@ -167,7 +177,8 @@
"de": "Es ist nur eine Pumpe vorhanden",
"it": "Cè solamente una pompa presente",
"pt_BR": "Há somente uma bomba presente",
"pt": "Há somente uma bomba presente"
"pt": "Há somente uma bomba presente",
"es": "Solo hay una bomba presente"
}
},
{
@ -185,7 +196,8 @@
"de": "Es sind nur Werkzeuge (Schraubenzieher, Zangen...) vorhanden",
"it": "Ci sono solo degli attrezzi (cacciaviti, pinze…) presenti",
"pt_BR": "Há somente ferramentas (chaves de fenda, alicates...) presentes",
"pt": "Há somente ferramentas (chaves de fenda, alicates...) presentes"
"pt": "Há somente ferramentas (chaves de fenda, alicates...) presentes",
"es": "Solo hay herramientas (destornilladores, pinzas...) presentes"
}
},
{
@ -203,7 +215,8 @@
"de": "Es sind sowohl Werkzeuge als auch eine Pumpe vorhanden",
"it": "Ci sono sia attrezzi che pompa presenti",
"pt_BR": "Há tanto ferramentas e uma bomba presente",
"pt": "Há tanto ferramentas e uma bomba presente"
"pt": "Há tanto ferramentas e uma bomba presente",
"es": "Hay tanto herramientas como bombas"
}
}
]
@ -217,7 +230,8 @@
"de": "Ist die Fahrradpumpe noch funktionstüchtig?",
"it": "La pompa per bici è sempre funzionante?",
"ru": "Велосипедный насос все еще работает?",
"pl": "Czy pompka rowerowa jest nadal sprawna?"
"pl": "Czy pompka rowerowa jest nadal sprawna?",
"es": "¿Todavía está operativa la bomba de bicicletas?"
},
"condition": "service:bicycle:pump=yes",
"mappings": [
@ -231,7 +245,8 @@
"de": "Die Fahrradpumpe ist kaputt",
"it": "La pompa per bici è guasta",
"ru": "Велосипедный насос сломан",
"pl": "Pompka rowerowa jest zepsuta"
"pl": "Pompka rowerowa jest zepsuta",
"es": "La bomba de bicicletas está rota"
}
},
{
@ -244,7 +259,8 @@
"de": "Die Fahrradpumpe ist betriebsbereit",
"it": "La pompa per bici funziona",
"ru": "Велосипедный насос работает",
"pl": "Pompka rowerowa jest sprawna"
"pl": "Pompka rowerowa jest sprawna",
"es": "La bomba de bicicletas está operativa"
}
}
],
@ -257,7 +273,8 @@
"fr": "Quand ce point de réparation de vélo est-il ouvert ?",
"it": "Quando è aperto questo punto riparazione bici?",
"de": "Wann ist diese Fahrradreparaturstelle geöffnet?",
"ru": "Когда работает эта точка обслуживания велосипедов?"
"ru": "Когда работает эта точка обслуживания велосипедов?",
"es": "¿Cuándo está abierto este punto de reparación de bicicletas?"
},
"render": "{opening_hours_table()}",
"freeform": {
@ -275,7 +292,9 @@
"de": "Immer geöffnet",
"ru": "Всегда открыто",
"pt_BR": "Sempre aberto",
"pt": "Sempre aberto"
"pt": "Sempre aberto",
"ca": "Sempre obert",
"es": "Siempre abierto"
}
}
],
@ -285,21 +304,27 @@
"id": "access",
"question": {
"en": "Who is allowed to use this repair station?",
"nl": "Wie kan dit herstelpunt gebruiken?"
"nl": "Wie kan dit herstelpunt gebruiken?",
"de": "Wer darf diese Reparaturstation benutzen?",
"es": "¿A quién se le permite utilizar esta estación de reparación?"
},
"mappings": [
{
"if": "access=yes",
"then": {
"en": "Publicly accessible",
"nl": "Publiek toegankelijk"
"nl": "Publiek toegankelijk",
"de": "Öffentlich zugänglich",
"es": "Accesible públicamente"
}
},
{
"if": "access=public",
"then": {
"en": "Publicly accessible",
"nl": "Publiek toegankelijk"
"nl": "Publiek toegankelijk",
"de": "Öffentlich zugänglich",
"es": "Accesible públicamente"
},
"hideInAnswer": true
},
@ -307,14 +332,18 @@
"if": "access=customers",
"then": {
"en": "Only for customers",
"nl": "Enkel voor klanten van de bijhorende zaak"
"nl": "Enkel voor klanten van de bijhorende zaak",
"de": "Nur für Kunden",
"es": "Solo para clientes"
}
},
{
"if": "access=private",
"then": {
"en": "Not accessible to the general public",
"nl": "Niet publiek toegankelijk"
"nl": "Niet publiek toegankelijk",
"de": "Nicht für die Allgemeinheit zugänglich",
"es": "No accesible para el público general"
},
"icon": "./assets/svg/invalid.svg"
},
@ -322,7 +351,9 @@
"if": "access=no",
"then": {
"en": "Not accessible to the general public",
"nl": "Niet publiek toegankelijk"
"nl": "Niet publiek toegankelijk",
"de": "Nicht für die Allgemeinheit zugänglich",
"es": "No accesible para el público general"
},
"icon": "./assets/svg/invalid.svg",
"hideInAnswer": true
@ -340,7 +371,8 @@
"it": "Chi gestisce questa pompa per bici?",
"de": "Wer wartet diese Fahrradpumpe?",
"pt_BR": "Quem faz a manutenção desta bomba de ciclo?",
"pt": "Quem faz a manutenção desta bomba de ciclo?"
"pt": "Quem faz a manutenção desta bomba de ciclo?",
"es": "¿Quién mantiene esta bomba para bicicletas?"
},
"render": {
"nl": "Beheer door {operator}",
@ -349,7 +381,8 @@
"it": "Manutenuta da {operator}",
"de": "Gewartet von {operator}",
"pt_BR": "Mantida por {operator}",
"pt": "Mantida por {operator}"
"pt": "Mantida por {operator}",
"es": "Mantenido por {operator}"
},
"freeform": {
"key": "operator"
@ -364,7 +397,8 @@
"en": "What is the email address of the maintainer?",
"nl": "Wat is het email-adres van de beheerder?",
"de": "Wie lautet die E-Mail-Adresse des Betreuers?",
"fr": "Quelle est l'adresse email du service de maintenance ?"
"fr": "Quelle est l'adresse email du service de maintenance ?",
"es": "¿Es esta la dirección de correo electrónico del mantenedor?"
},
"freeform": {
"key": "email",
@ -381,7 +415,8 @@
"en": "What is the phone number of the maintainer?",
"nl": "Wat is het telefoonnummer van de beheerder?",
"de": "Wie lautet die Telefonnummer des Betreibers?",
"fr": "Quel est le numéro de téléphone du service de maintenance ?"
"fr": "Quel est le numéro de téléphone du service de maintenance ?",
"es": "¿Cual es el número de teléfono del mantenedor?"
},
"freeform": {
"key": "phone",
@ -398,7 +433,8 @@
"fr": "Est-ce que cette station vélo a un outil specifique pour réparer la chaîne du vélo ?",
"gl": "Esta estación de arranxo de bicicletas ten unha ferramenta especial para arranxar a cadea da túa bicicleta?",
"de": "Verfügt diese Fahrrad-Reparaturstation über Spezialwerkzeug zur Reparatur von Fahrradketten?",
"it": "Questa stazione di riparazione bici ha un attrezzo speciale per riparare la catena della bici?"
"it": "Questa stazione di riparazione bici ha un attrezzo speciale per riparare la catena della bici?",
"es": "¿Esta estación de reparación tiene una herramienta especial para reparar la cadena de tu bici?"
},
"condition": "service:bicycle:tools=yes",
"mappings": [
@ -412,7 +448,8 @@
"de": "Es gibt ein Kettenwerkzeug",
"it": "È presente un utensile per riparare la catena",
"pt_BR": "Há uma ferramenta de corrente",
"pt": "Há uma ferramenta de corrente"
"pt": "Há uma ferramenta de corrente",
"es": "Hay una herramienta de cadenas"
}
},
{
@ -425,7 +462,8 @@
"de": "Es gibt kein Kettenwerkzeug",
"it": "Non è presente un utensile per riparare la catena",
"pt_BR": "Não há uma ferramenta de corrente",
"pt": "Não há uma ferramenta de corrente"
"pt": "Não há uma ferramenta de corrente",
"es": "No hay herramienta de cadenas"
}
}
]
@ -438,7 +476,8 @@
"fr": "Est-ce que cette station vélo à un crochet pour suspendre son vélo ou une accroche pour l'élevé ?",
"gl": "Esta estación de bicicletas ten un guindastre para pendurar a túa bicicleta ou un soporte para elevala?",
"de": "Hat diese Fahrradstation einen Haken, an dem Sie Ihr Fahrrad aufhängen können, oder einen Ständer, um es anzuheben?",
"it": "Questa stazione bici ha un gancio per tenere sospesa la bici o un supporto per alzarla?"
"it": "Questa stazione bici ha un gancio per tenere sospesa la bici o un supporto per alzarla?",
"es": "¿Esta estación tiene un gancho para colgar tu bici o un soporte para elevarla?"
},
"condition": "service:bicycle:tools=yes",
"mappings": [
@ -452,7 +491,8 @@
"de": "Es gibt einen Haken oder Ständer",
"it": "Cè un gancio o un supporto",
"pt_BR": "Há um gancho ou um suporte",
"pt": "Há um gancho ou um suporte"
"pt": "Há um gancho ou um suporte",
"es": "Hay un gancho o soporte"
}
},
{
@ -465,7 +505,8 @@
"de": "Es gibt keinen Haken oder Ständer",
"it": "Non cè né un gancio né un supporto",
"pt_BR": "Não há um gancho ou um suporte",
"pt": "Não há um gancho ou um suporte"
"pt": "Não há um gancho ou um suporte",
"es": "No hay ningún gancho o soporte"
}
}
]
@ -492,7 +533,8 @@
"gl": "Que válvulas son compatíbeis?",
"de": "Welche Ventile werden unterstützt?",
"it": "Quali valvole sono supportate?",
"pl": "Jakie zawory są obsługiwane?"
"pl": "Jakie zawory są obsługiwane?",
"es": "¿Que válvulas se soportan?"
},
"render": {
"en": "This pump supports the following valves: {valves}",
@ -502,7 +544,8 @@
"de": "Diese Pumpe unterstützt die folgenden Ventile: {valves}",
"it": "Questa pompa è compatibile con le seguenti valvole: {valves}",
"ru": "Этот насос поддерживает следующие клапаны: {valves}",
"pl": "Ta pompka obsługuje następujące zawory: {valves}"
"pl": "Ta pompka obsługuje następujące zawory: {valves}",
"es": "Esta bomba soporta las siguiente válvulas: {valves}"
},
"freeform": {
"#addExtraTags": [
@ -560,7 +603,8 @@
"de": "Ist dies eine elektrische Fahrradpumpe?",
"it": "Questa pompa per bici è elettrica?",
"ru": "Это электрический велосипедный насос?",
"pl": "Czy jest to elektryczna pompka do roweru?"
"pl": "Czy jest to elektryczna pompka do roweru?",
"es": "¿Hay una bomba eléctrica para bicis?"
},
"condition": "service:bicycle:pump=yes",
"mappings": [
@ -576,7 +620,9 @@
"ru": "Ручной насос",
"pl": "Pompa ręczna",
"pt_BR": "Bomba manual",
"pt": "Bomba manual"
"pt": "Bomba manual",
"ca": "Bomba manual",
"es": "Bomba manual"
}
},
{
@ -591,7 +637,9 @@
"ru": "Электрический насос",
"pl": "Pompka elektryczna",
"pt_BR": "Bomba elétrica",
"pt": "Bomba elétrica"
"pt": "Bomba elétrica",
"ca": "Bomba elèctrica",
"es": "Bomba eléctrica"
}
}
]
@ -605,7 +653,8 @@
"gl": "Ten a bomba de ar un indicador de presión ou un manómetro?",
"de": "Verfügt die Pumpe über einen Druckanzeiger oder ein Manometer?",
"it": "Questa pompa ha lindicatore della pressione o il manometro?",
"pl": "Czy pompka posiada wskaźnik ciśnienia lub manometr?"
"pl": "Czy pompka posiada wskaźnik ciśnienia lub manometr?",
"es": "¿La bomba tiene un indicador de presión o manómetro?"
},
"condition": "service:bicycle:pump=yes",
"mappings": [
@ -621,7 +670,8 @@
"ru": "Есть манометр",
"pl": "Jest manometr",
"pt_BR": "Há um manômetro",
"pt": "Há um manômetro"
"pt": "Há um manômetro",
"es": "Hay un manómetro"
}
},
{
@ -636,7 +686,8 @@
"ru": "Нет манометра",
"pl": "Nie ma manometru",
"pt_BR": "Não há um manômetro",
"pt": "Não há um manômetro"
"pt": "Não há um manômetro",
"es": "No hay ningún manometro"
}
},
{
@ -651,7 +702,8 @@
"ru": "Есть манометр, но он сломан",
"pl": "Jest manometr, ale jest uszkodzony",
"pt_BR": "Há um manômetro mas está quebrado",
"pt": "Há um manômetro mas está quebrado"
"pt": "Há um manômetro mas está quebrado",
"es": "Hay un manómetro pero está roto"
}
}
]
@ -670,7 +722,8 @@
"ru": "bелосипедный насос",
"fi": "pyöräpumppu",
"pl": "pompka do roweru",
"pt_BR": "uma bomba de bicicleta"
"pt_BR": "uma bomba de bicicleta",
"es": "una bomba de bicicletas"
},
"tags": [
"amenity=bicycle_repair_station",
@ -685,7 +738,8 @@
"de": "Ein Gerät zum Aufpumpen von Reifen an einem festen Standort im öffentlichen Raum.",
"pl": "Urządzenie do pompowania opon w stałym miejscu w przestrzeni publicznej.",
"pt_BR": "Um dispositivo para encher seus pneus em um local fixa no espaço público",
"pt": "Um aparelho para encher os seus pneus num local fixa no espaço público"
"pt": "Um aparelho para encher os seus pneus num local fixa no espaço público",
"es": "Un dispositivo para inflar tus ruedas en una posición fija en el espacio público."
},
"exampleImages": [
"./assets/layers/bike_repair_station/pump_example_round.jpg",
@ -701,7 +755,8 @@
"gl": "estación de arranxo de bicicletas con bomba de ar",
"de": "eine fahrrad-reparaturstation und pumpe",
"it": "una stazione di riparazione bici e pompa",
"pl": "stacja naprawy rowerów i pompka"
"pl": "stacja naprawy rowerów i pompka",
"es": "En estación de reparación de bicicletas y bomba"
},
"tags": [
"amenity=bicycle_repair_station",
@ -713,7 +768,8 @@
"nl": "Een fietspomp en gereedschap om je fiets te herstellen in de publieke ruimte. Deze zijn op een vastgemaakt, bijvoorbeeld aan een paal.",
"fr": "Un dispositif avec des outils pour réparer votre vélo combiné à une pompe a un emplacement fixe. Les outils sont souvent attachés par une chaîne pour empêcher le vol.",
"it": "Un dispositivo con attrezzi per riparare la tua bici e una pompa in un luogo fisso. Gli attrezzi sono spesso attaccati ad una catena per prevenire il furto.",
"de": "Ein Gerät mit Werkzeugen zur Reparatur von Fahrrädern kombiniert mit einer Pumpe an einem festen Standort. Die Werkzeuge sind oft mit Ketten gegen Diebstahl gesichert."
"de": "Ein Gerät mit Werkzeugen zur Reparatur von Fahrrädern kombiniert mit einer Pumpe an einem festen Standort. Die Werkzeuge sind oft mit Ketten gegen Diebstahl gesichert.",
"es": "Una bomba de bicicletas y herramientas para reparar tu bicicleta en el espacio público. Las herramientas habitualmente están aseguradas con cadenas contra el robo."
},
"exampleImages": [
"./assets/layers/bike_repair_station/repair_station_example_2.jpg",
@ -728,7 +784,8 @@
"gl": "estación de arranxo de bicicletas sin bomba de ar",
"de": "eine fahrrad-reparaturstation ohne pumpe",
"it": "una stazione di riparazione bici senza pompa",
"ru": "Станция обслуживания велосипедов без накачки (насоса)"
"ru": "Станция обслуживания велосипедов без накачки (насоса)",
"es": "una estación de reparación de bicicletas sin bomba"
},
"tags": [
"amenity=bicycle_repair_station",
@ -737,7 +794,9 @@
],
"description": {
"en": "Tools to repair your bike in the public space (without pump). The tools are secured against theft.",
"nl": "Gereedschap om je fiets te herstellen in de publieke ruimte (zonder pomp). Deze zijn op een vastgemaakt, bijvoorbeeld aan een paal."
"nl": "Gereedschap om je fiets te herstellen in de publieke ruimte (zonder pomp). Deze zijn op een vastgemaakt, bijvoorbeeld aan een paal.",
"de": "Werkzeug, um Ihr Fahrrad im öffentlichen Raum zu reparieren (ohne Pumpe). Die Werkzeuge sind gegen Diebstahl gesichert.",
"es": "Herramientas para reparar tu bici en el espacio público (sin bomba).Las herramientas están aseguradas contra el robo."
}
}
],
@ -829,6 +888,8 @@
],
"description": {
"en": "A layer showing bicycle pumps and bicycle repair tool stands",
"nl": "Deze laag toont fietspompen en herstelpunten voor fietsen"
"nl": "Deze laag toont fietspompen en herstelpunten voor fietsen",
"de": "Eine Ebene mit Fahrradpumpen und Werkzeugständern für die Fahrradreparatur",
"es": "Una capa que muestra bombas de bicicletas y puestos de herramientas de reparación de bicicletas"
}
}

View file

@ -9,7 +9,8 @@
"it": "Venditore/riparatore bici",
"ru": "Обслуживание велосипедов/магазин",
"pt_BR": "Reparo/loja de bicicletas",
"pt": "Reparo/loja de bicicletas"
"pt": "Reparo/loja de bicicletas",
"ca": "Botiga/reparació de bicicletes"
},
"minzoom": 13,
"allowMove": true,
@ -54,7 +55,8 @@
"it": "Venditore/riparatore bici",
"ru": "Обслуживание велосипедов/магазин",
"pt_BR": "Reparo/loja de bicicletas",
"pt": "Reparo/loja de bicicletas"
"pt": "Reparo/loja de bicicletas",
"ca": "Botiga/reparació de bicicletes"
},
"mappings": [
{
@ -102,7 +104,8 @@
"ru": "Прокат велосипедов <i>{name}</i>",
"de": "Fahrradverleih<i>{name}</i>",
"pt_BR": "Aluguel de bicicletas <i>{name}</i>",
"pt": "Aluguel de bicicletas <i>{name}</i>"
"pt": "Aluguel de bicicletas <i>{name}</i>",
"es": "Alquiler de bicicletas <i>{name}</i>"
}
},
{
@ -121,7 +124,8 @@
"it": "Riparazione biciclette <i>{name}</i>",
"ru": "Ремонт велосипедов <i>{name}</i>",
"pt_BR": "Reparo de bicicletas <i>{name}</i>",
"pt": "Reparo de bicicletas <i>{name}</i>"
"pt": "Reparo de bicicletas <i>{name}</i>",
"es": "Reparación de bicis <i>{name}</i>"
}
},
{
@ -139,7 +143,8 @@
"it": "Negozio di biciclette <i>{name}</i>",
"ru": "Магазин велосипедов <i>{name}</i>",
"pt_BR": "Loja de bicicletas <i>{name}</i>",
"pt": "Loja de bicicletas <i>{name}</i>"
"pt": "Loja de bicicletas <i>{name}</i>",
"es": "Tienda de bicis <i>{name}</i>"
}
},
{
@ -189,7 +194,8 @@
"ru": "Магазин, специализирующийся на продаже велосипедов или сопутствующих товаров",
"pt_BR": "Uma loja que vende especificamente bicicletas ou itens relacionados",
"de": "Ein Geschäft, das speziell Fahrräder oder verwandte Artikel verkauft",
"pt": "Uma loja que vende especificamente bicicletas ou itens relacionados"
"pt": "Uma loja que vende especificamente bicicletas ou itens relacionados",
"es": "Una tiene que vende específicamente bicis u objetos relacionados"
},
"tagRenderings": [
"images",
@ -209,14 +215,16 @@
"it": "Questo negozio è specializzato nella vendita di {shop} ed effettua attività relative alle biciclette",
"pt_BR": "Esta loja é especializada em vender {shop} e faz atividades relacionadas à bicicletas",
"de": "Dieses Geschäft ist auf den Verkauf von {shop} spezialisiert und im Bereich Fahrrad tätig",
"pt": "Esta loja é especializada em vender {shop} e faz atividades relacionadas à bicicletas"
"pt": "Esta loja é especializada em vender {shop} e faz atividades relacionadas à bicicletas",
"es": "Esta tienda está especializada en vender {shop} y hace actividades relacionadas con bicicletas"
},
"mappings": [
{
"if": "shop=rental",
"then": {
"nl": "Deze zaak focust op verhuur",
"en": "Deze business focuses on rental"
"en": "Deze business focuses on rental",
"de": "Dieses Geschäft konzentriert sich auf die Vermietung"
}
}
]
@ -231,7 +239,8 @@
"it": "Qual è il nome di questo negozio di biciclette?",
"ru": "Как называется магазин велосипедов?",
"pt_BR": "Qual o nome desta loja de bicicletas?",
"pt": "Qual o nome desta loja de bicicletas?"
"pt": "Qual o nome desta loja de bicicletas?",
"es": "¿Cual es el nombre de esta tienda de bicicletas?"
},
"render": {
"en": "This bicycle shop is called {name}",
@ -242,7 +251,8 @@
"it": "Questo negozio di biciclette è chiamato {name}",
"ru": "Этот магазин велосипедов называется {name}",
"pt_BR": "Esta loja de bicicletas se chama {name}",
"pt": "Esta loja de bicicletas se chama {name}"
"pt": "Esta loja de bicicletas se chama {name}",
"es": "Esta tienda de bicicletas se llama {name}"
},
"freeform": {
"key": "name"
@ -260,7 +270,8 @@
"id": "URL {name} apa?",
"de": "Was ist die Webseite von {name}?",
"pt_BR": "Qual o website de {name}?",
"pt": "Qual o website de {name}?"
"pt": "Qual o website de {name}?",
"es": "¿Cual es el sitio web de {name}?"
},
"render": "<a href='{website}' target='_blank'>{website}</a>",
"freeform": {
@ -279,7 +290,8 @@
"ru": "Какой номер телефона у {name}?",
"de": "Wie lautet die Telefonnummer von {name}?",
"pt_BR": "Qual o número de telefone de {name}?",
"pt": "Qual é o número de telefone de {name}?"
"pt": "Qual é o número de telefone de {name}?",
"es": "¿Cual es el número de teléfono de {name}?"
},
"render": "<a href='tel:{phone}'>{phone}</a>",
"freeform": {
@ -298,7 +310,8 @@
"ru": "Какой адрес электронной почты у {name}?",
"de": "Wie lautet die E-Mail-Adresse von {name}?",
"pt_BR": "Qual o endereço de email de {name}?",
"pt": "Qual o endereço de email de {name}?"
"pt": "Qual o endereço de email de {name}?",
"es": "¿Cual es la dirección de correo electrónico de {name}?"
},
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>",
"freeform": {
@ -311,7 +324,9 @@
{
"render": {
"en": "Only accessible to {access}",
"nl": "Enkel voor {access}"
"nl": "Enkel voor {access}",
"de": "Nur zugänglich für {access}",
"es": "Solo accesible a {access}"
},
"freeform": {
"key": "access"
@ -329,7 +344,8 @@
"it": "Questo negozio vende bici?",
"ru": "Продаются ли велосипеды в этом магазине?",
"pt_BR": "Esta loja vende bicicletas?",
"pt": "Esta loja vende bicicletas?"
"pt": "Esta loja vende bicicletas?",
"es": "¿Vende bicis esta tienda?"
},
"mappings": [
{
@ -343,7 +359,8 @@
"it": "Questo negozio vende bici",
"ru": "В этом магазине продаются велосипеды",
"pt_BR": "Esta loja vende bicicletas",
"pt": "Esta loja vende bicicletas"
"pt": "Esta loja vende bicicletas",
"es": "Esta tienda vende bicis"
}
},
{
@ -357,7 +374,8 @@
"it": "Questo negozio non vende bici",
"ru": "В этом магазине не продают велосипеды",
"pt_BR": "Esta loja não vende bicicletas",
"pt": "Esta loja não vende bicicletas"
"pt": "Esta loja não vende bicicletas",
"es": "Esta tienda no vende bicis"
}
}
]
@ -373,7 +391,8 @@
"it": "Questo negozio ripara bici?",
"ru": "В этом магазине ремонтируют велосипеды?",
"pt_BR": "Esta loja conserta bicicletas?",
"pt": "Esta loja conserta bicicletas?"
"pt": "Esta loja conserta bicicletas?",
"es": "¿Repara bicis esta tienda?"
},
"mappings": [
{
@ -387,7 +406,8 @@
"it": "Questo negozio ripara bici",
"ru": "Этот магазин ремонтирует велосипеды",
"pt_BR": "Esta loja conserta bicicletas",
"pt": "Esta loja conserta bicicletas"
"pt": "Esta loja conserta bicicletas",
"es": "Esta tienda repara bicis"
}
},
{
@ -401,7 +421,8 @@
"it": "Questo negozio non ripara bici",
"ru": "Этот магазин не ремонтирует велосипеды",
"pt_BR": "Esta loja não conserta bicicletas",
"pt": "Esta loja não conserta bicicletas"
"pt": "Esta loja não conserta bicicletas",
"es": "Esta tienda no repara bicis"
}
},
{
@ -415,7 +436,8 @@
"it": "Questo negozio ripara solo le bici che sono state acquistate qua",
"ru": "Этот магазин ремонтирует только велосипеды, купленные здесь",
"pt_BR": "Esta loja conserta bicicletas compradas aqui",
"pt": "Esta loja conserta bicicletas compradas aqui"
"pt": "Esta loja conserta bicicletas compradas aqui",
"es": "Esta tienda solo repara bicis compradas aquí"
}
},
{
@ -429,7 +451,8 @@
"it": "Questo negozio ripara solo le biciclette di una certa marca",
"ru": "В этом магазине обслуживают велосипеды определённого бренда",
"pt_BR": "Esta loja conserta bicicletas de uma certa marca",
"pt": "Esta loja conserta bicicletas de uma certa marca"
"pt": "Esta loja conserta bicicletas de uma certa marca",
"es": "Esta tienda solo repara bicis de una cierta marca"
}
}
]
@ -446,7 +469,8 @@
"it": "Questo negozio noleggia le bici?",
"ru": "Этот магазин сдает велосипеды в аренду?",
"pt_BR": "Esta loja aluga bicicletas?",
"pt": "Esta loja aluga bicicletas?"
"pt": "Esta loja aluga bicicletas?",
"es": "¿Alquila bicicis esta tienda?"
},
"mappings": [
{
@ -460,7 +484,8 @@
"it": "Questo negozio noleggia le bici",
"ru": "Этот магазин сдает велосипеды в аренду",
"pt_BR": "Esta loja aluga bicicletas",
"pt": "Esta loja aluga bicicletas"
"pt": "Esta loja aluga bicicletas",
"es": "Esta tienda alquila bicis"
}
},
{
@ -474,7 +499,8 @@
"it": "Questo negozio non noleggia le bici",
"ru": "Этот магазин не сдает велосипеды напрокат",
"pt_BR": "Esta loja não aluga bicicletas",
"pt": "Esta loja não aluga bicicletas"
"pt": "Esta loja não aluga bicicletas",
"es": "Esta tienda no alquila bicis"
}
}
]
@ -489,7 +515,8 @@
"gl": "Esta tenda vende bicicletas de segunda man?",
"de": "Verkauft dieses Geschäft gebrauchte Fahrräder?",
"it": "Questo negozio vende bici usate?",
"ru": "В этом магазине продаются подержанные велосипеды?"
"ru": "В этом магазине продаются подержанные велосипеды?",
"es": "¿Vende bicis de segunda mano esta tienda?"
},
"mappings": [
{
@ -501,7 +528,8 @@
"gl": "Esta tenda vende bicicletas de segunda man",
"de": "Dieses Geschäft verkauft gebrauchte Fahrräder",
"it": "Questo negozio vende bici usate",
"ru": "В этом магазине продаются подержанные велосипеды"
"ru": "В этом магазине продаются подержанные велосипеды",
"es": "Esta tienda vende bicis de segunda mano"
}
},
{
@ -513,7 +541,8 @@
"gl": "Esta tenda non vende bicicletas de segunda man",
"de": "Dieses Geschäft verkauft keine gebrauchten Fahrräder",
"it": "Questo negozio non vende bici usate",
"ru": "В этом магазине не продаются подержанные велосипеды"
"ru": "В этом магазине не продаются подержанные велосипеды",
"es": "Esta tienda no vende bicis de segunda mano"
}
},
{
@ -525,7 +554,8 @@
"gl": "Esta tenda só vende bicicletas de segunda man",
"de": "Dieses Geschäft verkauft nur gebrauchte Fahrräder",
"it": "Questo negozio vende solamente bici usate",
"ru": "В этом магазине продаются только подержанные велосипеды"
"ru": "В этом магазине продаются только подержанные велосипеды",
"es": "Esta tienda solo vende bicis de segunda mano"
}
}
]
@ -539,7 +569,8 @@
"gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa?",
"de": "Bietet dieses Geschäft eine Fahrradpumpe zur Benutzung für alle an?",
"it": "Questo negozio offre luso a chiunque di una pompa per bici?",
"ru": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?"
"ru": "Предлагается ли в этом магазине велосипедный насос для всеобщего пользования?",
"es": "¿Esta tienda ofrece una bomba para que la utilice cualquiera?"
},
"mappings": [
{
@ -551,7 +582,8 @@
"gl": "Esta tenda ofrece unha bomba de ar para uso de calquera persoa",
"de": "Dieses Geschäft bietet eine Fahrradpumpe für alle an",
"it": "Questo negozio offre luso pubblico di una pompa per bici",
"ru": "В этом магазине есть велосипедный насос для всеобщего пользования"
"ru": "В этом магазине есть велосипедный насос для всеобщего пользования",
"es": "Esta tienda ofrece una bomba para cualquiera"
}
},
{
@ -563,7 +595,8 @@
"gl": "Esta tenda non ofrece unha bomba de ar para uso de calquera persoa",
"de": "Dieses Geschäft bietet für niemanden eine Fahrradpumpe an",
"it": "Questo negozio non offre luso pubblico di una pompa per bici",
"ru": "В этом магазине нет велосипедного насоса для всеобщего пользования"
"ru": "В этом магазине нет велосипедного насоса для всеобщего пользования",
"es": "Esta tienda no ofrece una bomba para cualquiera"
}
},
{
@ -573,7 +606,8 @@
"nl": "Er is een fietspomp, deze is apart aangeduid",
"fr": "Il y a une pompe à vélo, c'est indiqué comme un point séparé ",
"it": "Cè una pompa per bici, è mostrata come punto separato ",
"de": "Es gibt eine Fahrradpumpe, sie wird als separater Punkt angezeigt "
"de": "Es gibt eine Fahrradpumpe, sie wird als separater Punkt angezeigt ",
"es": "Hay una bomba para bicicletas, se muestra como un punto separado "
}
}
]
@ -587,7 +621,8 @@
"gl": "Hai ferramentas aquí para arranxar a túa propia bicicleta?",
"de": "Gibt es hier Werkzeuge, um das eigene Fahrrad zu reparieren?",
"it": "Sono presenti degli attrezzi per riparare la propria bici?",
"ru": "Есть ли здесь инструменты для починки собственного велосипеда?"
"ru": "Есть ли здесь инструменты для починки собственного велосипеда?",
"es": "¿Hay herramientas para reparar tu propia bici?"
},
"mappings": [
{
@ -633,7 +668,8 @@
"fr": "Lave-t-on les vélos ici ?",
"it": "Vengono lavate le bici qua?",
"ru": "Здесь моют велосипеды?",
"de": "Werden hier Fahrräder gewaschen?"
"de": "Werden hier Fahrräder gewaschen?",
"es": "¿Aquí se lavan bicicletas?"
},
"mappings": [
{
@ -644,7 +680,8 @@
"fr": "Ce magasin lave les vélos",
"it": "Questo negozio lava le biciclette",
"de": "Dieses Geschäft reinigt Fahrräder",
"ru": "В этом магазине оказываются услуги мойки/чистки велосипедов"
"ru": "В этом магазине оказываются услуги мойки/чистки велосипедов",
"es": "Esta tienda limpia bicicletas"
}
},
{
@ -654,7 +691,8 @@
"nl": "Deze winkel biedt een installatie aan om zelf je fiets schoon te maken",
"fr": "Ce magasin a une installation pour laver soi même des vélos",
"it": "Questo negozio ha una struttura dove è possibile pulire la propria bici",
"de": "Dieser Laden hat eine Anlage, in der man Fahrräder selbst reinigen kann"
"de": "Dieser Laden hat eine Anlage, in der man Fahrräder selbst reinigen kann",
"es": "Esta tienda tiene una instalación donde uno puede limpiar bicicletas por si mismo"
}
},
{
@ -665,7 +703,8 @@
"fr": "Ce magasin ne fait pas le nettoyage de vélo",
"it": "Questo negozio non offre la pulizia della bicicletta",
"de": "Dieser Laden bietet keine Fahrradreinigung an",
"ru": "В этом магазине нет услуг мойки/чистки велосипедов"
"ru": "В этом магазине нет услуг мойки/чистки велосипедов",
"es": "Esta tienda no ofrece limpieza de bicicletas"
}
}
]

View file

@ -5,7 +5,8 @@
"nl": "Fietsgerelateerd object",
"fr": "Objet cycliste",
"de": "Weitere fahrradbezogene Objekte",
"it": "Oggetto relativo alle bici"
"it": "Oggetto relativo alle bici",
"es": "Objeto relacionada con bicis"
},
"minzoom": 13,
"source": {
@ -29,7 +30,8 @@
"nl": "Fietsgerelateerd object",
"fr": "Objet cycliste",
"de": "Mit Fahrrad zusammenhängendes Objekt",
"it": "Oggetto relativo alle bici"
"it": "Oggetto relativo alle bici",
"es": "Objeto relacionado con bicis"
},
"mappings": [
{
@ -44,7 +46,9 @@
"fr": "Piste cyclable",
"it": "Pista ciclabile",
"de": "Radweg",
"ru": "Велотрек"
"ru": "Велотрек",
"ca": "Pista ciclable",
"es": "Carril bici"
}
}
]
@ -82,6 +86,8 @@
],
"description": {
"en": "A layer with bike-themed objects but who don't match any other layer",
"nl": "Een laag met fietsgerelateerde diensten, die in geen enkele andere laag konden ondergebracht worden"
"nl": "Een laag met fietsgerelateerde diensten, die in geen enkele andere laag konden ondergebracht worden",
"de": "Eine Ebene mit Objekten zum Thema Fahrrad, die zu keiner anderen Ebene passen",
"es": "Una capa con los objetos relacionados con bicis pero que no coinciden con ninguna otra capa"
}
}

View file

@ -4,7 +4,8 @@
"en": "Binoculars",
"nl": "Verrekijkers",
"de": "Ferngläser",
"ru": "Бинокль"
"ru": "Бинокль",
"ca": "Prismàtics"
},
"minzoom": 0,
"title": {
@ -12,14 +13,17 @@
"en": "Binoculars",
"nl": "Verrekijker",
"de": "Ferngläser",
"ru": "Бинокль"
"ru": "Бинокль",
"ca": "Prismàtics",
"es": "Binoculares"
}
},
"description": {
"en": "Binoculas",
"nl": "Verrekijkers",
"de": "Fernglas",
"ru": "Бинокли"
"ru": "Бинокли",
"ca": "Prismàtics"
},
"tagRenderings": [
"images",
@ -48,12 +52,14 @@
"render": {
"en": "Using these binoculars costs {charge}",
"nl": "Deze verrekijker gebruiken kost {charge}",
"de": "Die Benutzung dieses Fernglases kostet {charge}"
"de": "Die Benutzung dieses Fernglases kostet {charge}",
"es": "Utilizar estos binoculares cuesta {charge}"
},
"question": {
"en": "How much does one have to pay to use these binoculars?",
"nl": "Hoeveel moet men betalen om deze verrekijker te gebruiken?",
"de": "Wie viel muss man für die Nutzung dieser Ferngläser bezahlen?"
"de": "Wie viel muss man für die Nutzung dieser Ferngläser bezahlen?",
"es": "¿Cuánto hay que pagar para utilizar estos binoculares?"
},
"id": "binocular-charge"
},
@ -61,12 +67,14 @@
"question": {
"en": "When looking through this binocular, in what direction does one look?",
"nl": "Welke richting kijkt men uit als men door deze verrekijker kijkt?",
"de": "In welche Richtung blickt man, wenn man durch dieses Fernglas schaut?"
"de": "In welche Richtung blickt man, wenn man durch dieses Fernglas schaut?",
"es": "¿Cuándo uno mira a través de este binocular, en qué dirección lo hace?"
},
"render": {
"en": "Looks towards {direction}°",
"nl": "Kijkt richting {direction}°",
"de": "Blick in Richtung {direction}°"
"de": "Blick in Richtung {direction}°",
"es": "Mira hacia {direction}º"
},
"freeform": {
"key": "direction",
@ -84,7 +92,8 @@
"en": "a binoculars",
"nl": "een verrekijker",
"de": "eine ferngläser",
"ru": "бинокль"
"ru": "бинокль",
"ca": "uns prismàtics"
},
"description": {
"en": "A telescope or pair of binoculars mounted on a pole, available to the public to look around. <img src='./assets/layers/binocular/binoculars_example.jpg' style='height: 300px; width: auto; display: block;' />",

View file

@ -3,7 +3,8 @@
"name": {
"en": "Bird watching places",
"nl": "Vogelkijkhutten",
"de": "Orte zur Vogelbeobachtung"
"de": "Orte zur Vogelbeobachtung",
"es": "Lugares para ver pájaros"
},
"minzoom": 14,
"source": {
@ -142,7 +143,8 @@
},
"then": {
"en": "There are special provisions for wheelchair users",
"nl": "Er zijn speciale voorzieningen voor rolstoelen"
"nl": "Er zijn speciale voorzieningen voor rolstoelen",
"es": "Hay provisiones especiales para usuarios de sillas de ruedas"
}
},
{
@ -175,7 +177,9 @@
},
"then": {
"en": "Not accessible to wheelchair users",
"nl": "Niet rolstoeltoegankelijk"
"nl": "Niet rolstoeltoegankelijk",
"de": "Nicht zugänglich für Rollstuhlfahrer",
"es": "No accesible a usuarios con sillas de ruedas"
}
}
]
@ -183,7 +187,9 @@
{
"render": {
"en": "Operated by {operator}",
"nl": "Beheer door {operator}"
"nl": "Beheer door {operator}",
"de": "Betrieben von {operator}",
"es": "Operado por {operator}"
},
"freeform": {
"key": "operator"
@ -197,14 +203,17 @@
"if": "operator=Natuurpunt",
"then": {
"en": "Operated by Natuurpunt",
"nl": "Beheer door Natuurpunt"
"nl": "Beheer door Natuurpunt",
"de": "Betrieben von Natuurpunt",
"es": "Operado por Natuurpunt"
}
},
{
"if": "operator=Agentschap Natuur en Bos",
"then": {
"en": "Operated by the Agency for Nature and Forests",
"nl": "Beheer door het Agentschap Natuur en Bos"
"nl": "Beheer door het Agentschap Natuur en Bos",
"de": "Betrieben von einer Forst- bzw. Naturschutzbehörde"
}
}
],
@ -239,7 +248,9 @@
},
"description": {
"en": "A covered shelter where one can watch birds comfortably",
"nl": "Een overdekte hut waarbinnen er warm en droog naar vogels gekeken kan worden"
"nl": "Een overdekte hut waarbinnen er warm en droog naar vogels gekeken kan worden",
"de": "Ein überdachter Unterstand, in dem man bequem Vögel beobachten kann",
"es": "Un refugio cubierto donde se pueden ver pájaros confortablemente"
}
},
{
@ -254,7 +265,8 @@
},
"description": {
"en": "A screen or wall with openings to watch birds",
"nl": "Een vogelkijkwand waarachter men kan staan om vogels te kijken"
"nl": "Een vogelkijkwand waarachter men kan staan om vogels te kijken",
"es": "Una pantalla o pared con aperturas para ver pájaros"
}
}
],
@ -267,7 +279,9 @@
"nl": "Rolstoeltoegankelijk",
"en": "Wheelchair accessible",
"de": "Zugänglich für Rollstuhlfahrer",
"fr": "Accessible aux fauteuils roulants"
"fr": "Accessible aux fauteuils roulants",
"ca": "Accessible per a cadires de rodes",
"es": "Accesible con sillas de ruedas"
},
"osmTags": {
"or": [

View file

@ -6,7 +6,8 @@
"de": "Cafés und Kneipen",
"fr": "Cafés et pubs",
"zh_Hant": "咖啡廳與酒吧",
"hu": "Kávézók és kocsmák"
"hu": "Kávézók és kocsmák",
"es": "Cafeterías y bares"
},
"source": {
"osmTags": {
@ -28,11 +29,14 @@
"nl": "een bruin café of kroeg",
"de": "eine kneipe",
"ru": "паб",
"hu": "kocsma"
"hu": "kocsma",
"ca": "un bar"
},
"description": {
"en": "A pub, mostly for drinking beers in a warm, relaxed interior",
"nl": "Dit is <b>een bruin café of een kroeg</b> waar voornamelijk bier wordt gedronken. De inrichting is typisch gezellig met veel houtwerk"
"nl": "Dit is <b>een bruin café of een kroeg</b> waar voornamelijk bier wordt gedronken. De inrichting is typisch gezellig met veel houtwerk",
"de": "Eine Kneipe, in der vor allem Bier in ruhiger, entspannter Atmosphäre getrunken wird",
"es": "Un bar, principalmente para beber cervezas en un interior templado y relajado"
},
"preciseInput": {
"preferredBackground": "map"
@ -47,11 +51,13 @@
"nl": "een bar",
"de": "eine bar",
"ru": "бар",
"hu": "bár"
"hu": "bár",
"ca": "un pub"
},
"description": {
"en": "A more modern and commercial <b>bar</b>, possibly with a music and light installation",
"nl": "Dit is een <b>bar</b> waar men ter plaatse alcoholische drank nuttigt. De inrichting is typisch modern en commercieel, soms met lichtinstallatie en feestmuziek"
"nl": "Dit is een <b>bar</b> waar men ter plaatse alcoholische drank nuttigt. De inrichting is typisch modern en commercieel, soms met lichtinstallatie en feestmuziek",
"de": "Eine modernere und kommerzielle <b>Bar</b>, möglicherweise mit einer Musik- und Lichtinstallation"
},
"preciseInput": {
"preferredBackground": "map"
@ -66,11 +72,13 @@
"nl": "een café",
"de": "eine café",
"ru": "кафе",
"hu": "kávézó"
"hu": "kávézó",
"ca": "un cafè"
},
"description": {
"en": "A <b>cafe</b> to drink tea, coffee or an alcoholical bevarage in a quiet environment",
"nl": "Dit is een <b>cafe</b> - een plaats waar men rustig kan zitten om een thee, koffie of alcoholische drank te nuttigen."
"nl": "Dit is een <b>cafe</b> - een plaats waar men rustig kan zitten om een thee, koffie of alcoholische drank te nuttigen.",
"de": "Ein <b>Café</b>, um in ruhiger Umgebung Tee, Kaffee oder ein alkoholisches Getränk zu trinken"
},
"preciseInput": {
"preferredBackground": "map"
@ -80,7 +88,9 @@
"title": {
"render": {
"en": "Pub",
"nl": "Café"
"nl": "Café",
"ca": "Bar",
"de": "Kneipe"
},
"mappings": [
{
@ -93,7 +103,8 @@
"nl": "<i>{name}</i>",
"en": "<i>{name}</i>",
"de": "<i>{name}</i>",
"ru": "<i>{name}</i>"
"ru": "<i>{name}</i>",
"ca": "<i>{name}</i>"
}
}
]
@ -132,35 +143,40 @@
"if": "amenity=pub",
"then": {
"en": "A pub, mostly for drinking beers in a warm, relaxed interior",
"nl": "Dit is <b>een bruin café of een kroeg</b> waar voornamelijk bier wordt gedronken. De inrichting is typisch gezellig met veel houtwerk"
"nl": "Dit is <b>een bruin café of een kroeg</b> waar voornamelijk bier wordt gedronken. De inrichting is typisch gezellig met veel houtwerk",
"de": "Eine Kneipe, in der vor allem Bier in ruhiger, entspannter Atmosphäre getrunken wird"
}
},
{
"if": "amenity=bar",
"then": {
"en": "A more modern and commercial <b>bar</b>, possibly with a music and light installation",
"nl": "Dit is een <b>bar</b> waar men ter plaatse alcoholische drank nuttigt. De inrichting is typisch modern en commercieel, soms met lichtinstallatie en feestmuziek"
"nl": "Dit is een <b>bar</b> waar men ter plaatse alcoholische drank nuttigt. De inrichting is typisch modern en commercieel, soms met lichtinstallatie en feestmuziek",
"de": "Eine modernere und kommerzielle <b>Bar</b>, möglicherweise mit einer Musik- und Lichtinstallation"
}
},
{
"if": "amenity=cafe",
"then": {
"en": "A <b>cafe</b> to drink tea, coffee or an alcoholical bevarage in a quiet environment",
"nl": "Dit is een <b>cafe</b> - een plaats waar men rustig kan zitten om een thee, koffie of alcoholische drank te nuttigen."
"nl": "Dit is een <b>cafe</b> - een plaats waar men rustig kan zitten om een thee, koffie of alcoholische drank te nuttigen.",
"de": "Ein <b>Café</b>, um in ruhiger Umgebung Tee, Kaffee oder ein alkoholisches Getränk zu trinken"
}
},
{
"if": "amenity=restaurant",
"then": {
"en": "A <b>restuarant</b> where one can get a proper meal",
"nl": "Dit is een <b>restaurant</b> waar men een maaltijd geserveerd krijgt"
"nl": "Dit is een <b>restaurant</b> waar men een maaltijd geserveerd krijgt",
"de": "Ein <b>Restaurant</b>, in dem man ordentlich essen kann"
}
},
{
"if": "amenity=biergarten",
"then": {
"en": "An open space where beer is served, typically seen in Germany",
"nl": "Een open ruimte waar bier geserveerd wordt. Typisch in Duitsland"
"nl": "Een open ruimte waar bier geserveerd wordt. Typisch in Duitsland",
"de": "Ein Außenbereich mit Bierausschank, typischerweise in Deutschland"
},
"hideInAnswer": "_country!=de"
}
@ -186,7 +202,9 @@
"nl": "Nu geopened",
"de": "Derzeit geöffnet",
"fr": "Ouvert maintenant",
"hu": "Most nyitva van"
"hu": "Most nyitva van",
"ca": "Obert ara",
"es": "Abiert oahora"
},
"osmTags": "_isOpen=yes"
}
@ -204,7 +222,9 @@
{
"explanation": {
"nl": "{title()} is permanent gestopt",
"en": "{title()} has closed down permanently"
"en": "{title()} has closed down permanently",
"de": "{title()} wurde dauerhaft geschlossen",
"es": "{title()} ha cerrado permanentemente"
},
"changesetMessage": "shop_closed"
}
@ -245,6 +265,8 @@
"description": {
"en": "A layer showing cafés and pubs where one can gather around a drink. The layer asks for some relevant questions",
"hu": "Egy olyan réteg, amely kávézókat és kocsmákat jelenít meg, ahol össze lehet gyűlni egy ital köré. A réteg néhány lényeges kérdést tesz fel",
"nl": "Een laag die kroegen en koffiehuizen toont waar je iets kunt drinken. De laag zal je enkele vragen stellen"
"nl": "Een laag die kroegen en koffiehuizen toont waar je iets kunt drinken. De laag zal je enkele vragen stellen",
"de": "Eine Ebene mit Cafés und Kneipen, in denen man sich auf ein Getränk treffen kann. Die Ebene fragt nach einigen relevanten Eigenschaften",
"es": "Una capa que muestra cafeterías y bares donde uno se puede reunir con una bebida. La capa pregunta algunas preguntas relevantes"
}
}

View file

@ -3,7 +3,8 @@
"name": {
"en": "Charging stations",
"nl": "Oplaadpunten",
"de": "Ladestationen"
"de": "Ladestationen",
"ca": "Estacions de càrrega"
},
"minzoom": 10,
"source": {
@ -23,7 +24,8 @@
"title": {
"render": {
"en": "Charging station",
"nl": "Oplaadpunt"
"nl": "Oplaadpunt",
"ca": "Estació de càrrega"
},
"mappings": [
{
@ -131,11 +133,13 @@
"id": "access",
"question": {
"en": "Who is allowed to use this charging station?",
"nl": "Wie mag er dit oplaadpunt gebruiken?"
"nl": "Wie mag er dit oplaadpunt gebruiken?",
"de": "Wer darf diese Ladestation benutzen?"
},
"render": {
"en": "Access is {access}",
"nl": "Toegang voor {access}"
"nl": "Toegang voor {access}",
"de": "Zugang ist {access}"
},
"freeform": {
"key": "access",
@ -148,7 +152,8 @@
"if": "access=yes",
"then": {
"en": "Anyone can use this charging station (payment might be needed)",
"nl": "Toegankelijk voor iedereen (mogelijks met aanmelden en/of te betalen)"
"nl": "Toegankelijk voor iedereen (mogelijks met aanmelden en/of te betalen)",
"de": "Jeder kann diese Ladestation nutzen (eventuell gegen Bezahlung)"
}
},
{
@ -160,7 +165,8 @@
},
"then": {
"en": "Anyone can use this charging station (payment might be needed)",
"nl": "Toegankelijk voor iedereen (mogelijks met aanmelden en/of te betalen)"
"nl": "Toegankelijk voor iedereen (mogelijks met aanmelden en/of te betalen)",
"de": "Jeder kann diese Ladestation nutzen (eventuell gegen Bezahlung)"
},
"hideInAnswer": true
},
@ -168,21 +174,24 @@
"if": "access=customers",
"then": {
"en": "Only customers of the place this station belongs to can use this charging station<br/><span class='subtle'>E.g. a charging station operated by hotel which is only usable by their guests</span>",
"nl": "Enkel <b>klanten van de bijhorende plaats</b> mogen dit oplaadpunt gebruiken<br/><span class='subtle'>Bv. op de parking van een hotel en enkel toegankelijk voor klanten van dit hotel</span>"
"nl": "Enkel <b>klanten van de bijhorende plaats</b> mogen dit oplaadpunt gebruiken<br/><span class='subtle'>Bv. op de parking van een hotel en enkel toegankelijk voor klanten van dit hotel</span>",
"de": "Nur Kunden des Ortes, zu dem diese Station gehört, können diese Ladestation nutzen<br/><span class='subtle'>Z.B. eine von einem Hotel betriebene Ladestation, die nur von dessen Gästen genutzt werden kann</span>"
}
},
{
"if": "access=key",
"then": {
"en": "A <b>key</b> must be requested to access this charging station<br/><span class='subtle'>E.g. a charging station operated by hotel which is only usable by their guests, which receive a key from the reception to unlock the charging station</span>",
"nl": "Een <b>sleutel</b> is nodig om dit oplaadpunt te gebruiken<br/><span class='subtle'>Bv. voor klanten van een hotel of een bar, die de sleutel aan de receptie kunnen krijgen</span>"
"nl": "Een <b>sleutel</b> is nodig om dit oplaadpunt te gebruiken<br/><span class='subtle'>Bv. voor klanten van een hotel of een bar, die de sleutel aan de receptie kunnen krijgen</span>",
"de": "Für den Zugang zu dieser Ladestation muss ein <b>Schlüssel</b> angefordert werden<br/><span class='subtle'>Z.B. eine von einem Hotel betriebene Ladestation, die nur von dessen Gästen genutzt werden kann, die an der Rezeption einen Schlüssel erhalten, um die Ladestation aufzuschließen</span>"
}
},
{
"if": "access=private",
"then": {
"en": "Not accessible to the general public (e.g. only accessible to the owners, employees, ...)",
"nl": "Niet toegankelijk voor het publiek <br/><span class='subtle'>Bv. enkel toegankelijk voor de eigenaar, medewerkers ,...</span>"
"nl": "Niet toegankelijk voor het publiek <br/><span class='subtle'>Bv. enkel toegankelijk voor de eigenaar, medewerkers ,...</span>",
"de": "Nicht für die Allgemeinheit zugänglich (z. B. nur für die Eigentümer, Mitarbeiter, ...)"
}
}
]
@ -196,7 +205,8 @@
},
"question": {
"en": "How much vehicles can be charged here at the same time?",
"nl": "Hoeveel voertuigen kunnen hier opgeladen worden?"
"nl": "Hoeveel voertuigen kunnen hier opgeladen worden?",
"de": "Wie viele Fahrzeuge können hier gleichzeitig geladen werden?"
},
"freeform": {
"key": "capacity",
@ -217,7 +227,8 @@
"ifnot": "socket:schuko=",
"then": {
"en": "<b>Schuko wall plug</b> without ground pin (CEE7/4 type F)",
"nl": "<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)"
"nl": "<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)",
"de": "<b>Schuko-Stecker</b> ohne Erdungsstift (CEE7/4 Typ F)"
},
"icon": {
"path": "./assets/layers/charging_station/CEE7_4F.svg",
@ -245,7 +256,8 @@
},
"then": {
"en": "<b>Schuko wall plug</b> without ground pin (CEE7/4 type F)",
"nl": "<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)"
"nl": "<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)",
"de": "<b>Schuko-Stecker</b> ohne Erdungsstift (CEE7/4 Typ F)"
},
"hideInAnswer": true,
"icon": {
@ -258,7 +270,8 @@
"ifnot": "socket:typee=",
"then": {
"en": "<b>European wall plug</b> with ground pin (CEE7/4 type E)",
"nl": "<b>Europese stekker</b> met aardingspin (CEE7/4 type E)"
"nl": "<b>Europese stekker</b> met aardingspin (CEE7/4 type E)",
"de": "<b>Europäischer Netzstecker</b> mit Erdungsstift (CEE7/4 Typ E)"
},
"icon": {
"path": "./assets/layers/charging_station/TypeE.svg",
@ -274,7 +287,8 @@
},
"then": {
"en": "<b>European wall plug</b> with ground pin (CEE7/4 type E)",
"nl": "<b>Europese stekker</b> met aardingspin (CEE7/4 type E)"
"nl": "<b>Europese stekker</b> met aardingspin (CEE7/4 type E)",
"de": "<b>Europäischer Netzstecker</b> mit Erdungsstift (CEE7/4 Typ E)"
},
"hideInAnswer": true,
"icon": {
@ -287,7 +301,9 @@
"ifnot": "socket:chademo=",
"then": {
"en": "<b>Chademo</b>",
"nl": "<b>Chademo</b>"
"nl": "<b>Chademo</b>",
"ca": "<b>Chademo</b>",
"de": "<b>Chademo</b>"
},
"icon": {
"path": "./assets/layers/charging_station/Chademo_type4.svg",
@ -329,7 +345,9 @@
},
"then": {
"en": "<b>Chademo</b>",
"nl": "<b>Chademo</b>"
"nl": "<b>Chademo</b>",
"ca": "<b>Chademo</b>",
"de": "<b>Chademo</b>"
},
"hideInAnswer": true,
"icon": {
@ -342,7 +360,8 @@
"ifnot": "socket:type1_cable=",
"then": {
"en": "<b>Type 1 with cable</b> (J1772)",
"nl": "<b>Type 1 met kabel</b> (J1772)"
"nl": "<b>Type 1 met kabel</b> (J1772)",
"de": "<b>Typ 1 mit Kabel</b> (J1772)"
},
"icon": {
"path": "./assets/layers/charging_station/Type1_J1772.svg",
@ -384,7 +403,8 @@
},
"then": {
"en": "<b>Type 1 with cable</b> (J1772)",
"nl": "<b>Type 1 met kabel</b> (J1772)"
"nl": "<b>Type 1 met kabel</b> (J1772)",
"de": "<b>Typ 1 mit Kabel</b> (J1772)"
},
"hideInAnswer": true,
"icon": {
@ -397,7 +417,8 @@
"ifnot": "socket:type1=",
"then": {
"en": "<b>Type 1 <i>without</i> cable</b> (J1772)",
"nl": "<b>Type 1 <i>zonder</i> kabel</b> (J1772)"
"nl": "<b>Type 1 <i>zonder</i> kabel</b> (J1772)",
"de": "<b>Typ 1 <i>ohne</i> Kabel</b> (J1772)"
},
"icon": {
"path": "./assets/layers/charging_station/Type1_J1772.svg",
@ -439,7 +460,8 @@
},
"then": {
"en": "<b>Type 1 <i>without</i> cable</b> (J1772)",
"nl": "<b>Type 1 <i>zonder</i> kabel</b> (J1772)"
"nl": "<b>Type 1 <i>zonder</i> kabel</b> (J1772)",
"de": "<b> Typ 1 <i>ohne </i> Kabel</b> (J1772)"
},
"hideInAnswer": true,
"icon": {
@ -452,7 +474,8 @@
"ifnot": "socket:type1_combo=",
"then": {
"en": "<b>Type 1 CCS</b> (aka Type 1 Combo)",
"nl": "<b>Type 1 CCS</b> (ook gekend als Type 1 Combo)"
"nl": "<b>Type 1 CCS</b> (ook gekend als Type 1 Combo)",
"de": "<b>Typ 1 CCS</b> (auch bekannt als Typ 1 Combo)"
},
"icon": {
"path": "./assets/layers/charging_station/Type1-ccs.svg",
@ -494,7 +517,8 @@
},
"then": {
"en": "<b>Type 1 CCS</b> (aka Type 1 Combo)",
"nl": "<b>Type 1 CCS</b> (ook gekend als Type 1 Combo)"
"nl": "<b>Type 1 CCS</b> (ook gekend als Type 1 Combo)",
"de": "<b> Typ 1 CCS </b> (auch bekannt als Typ 1 Combo)"
},
"hideInAnswer": true,
"icon": {
@ -507,7 +531,8 @@
"ifnot": "socket:tesla_supercharger=",
"then": {
"en": "<b>Tesla Supercharger</b>",
"nl": "<b>Tesla Supercharger</b>"
"nl": "<b>Tesla Supercharger</b>",
"de": "<b>Tesla Supercharger</b>"
},
"icon": {
"path": "./assets/layers/charging_station/Tesla-hpwc-model-s.svg",
@ -549,7 +574,8 @@
},
"then": {
"en": "<b>Tesla Supercharger</b>",
"nl": "<b>Tesla Supercharger</b>"
"nl": "<b>Tesla Supercharger</b>",
"de": "<b>Tesla Supercharger</b>"
},
"hideInAnswer": true,
"icon": {
@ -562,7 +588,8 @@
"ifnot": "socket:type2=",
"then": {
"en": "<b>Type 2</b> (mennekes)",
"nl": "<b>Type 2</b> (mennekes)"
"nl": "<b>Type 2</b> (mennekes)",
"de": "<b>Typ 2</b> (mennekes)"
},
"icon": {
"path": "./assets/layers/charging_station/Type2_socket.svg",
@ -604,7 +631,8 @@
},
"then": {
"en": "<b>Type 2</b> (mennekes)",
"nl": "<b>Type 2</b> (mennekes)"
"nl": "<b>Type 2</b> (mennekes)",
"de": "<b>Typ 2</b> (mennekes)"
},
"hideInAnswer": true,
"icon": {
@ -617,7 +645,8 @@
"ifnot": "socket:type2_combo=",
"then": {
"en": "<b>Type 2 CCS</b> (mennekes)",
"nl": "<b>Type 2 CCS</b> (mennekes)"
"nl": "<b>Type 2 CCS</b> (mennekes)",
"de": "<b>Typ 2 CCS</b> (mennekes)"
},
"icon": {
"path": "./assets/layers/charging_station/Type2_CCS.svg",
@ -659,7 +688,8 @@
},
"then": {
"en": "<b>Type 2 CCS</b> (mennekes)",
"nl": "<b>Type 2 CCS</b> (mennekes)"
"nl": "<b>Type 2 CCS</b> (mennekes)",
"de": "<b>Typ 2 CCS</b> (mennekes)"
},
"hideInAnswer": true,
"icon": {
@ -672,7 +702,8 @@
"ifnot": "socket:type2_cable=",
"then": {
"en": "<b>Type 2 with cable</b> (mennekes)",
"nl": "<b>Type 2 met kabel</b> (J1772)"
"nl": "<b>Type 2 met kabel</b> (J1772)",
"de": "<b>Typ 2 mit Kabel</b> (mennekes)"
},
"icon": {
"path": "./assets/layers/charging_station/Type2_tethered.svg",
@ -714,7 +745,8 @@
},
"then": {
"en": "<b>Type 2 with cable</b> (mennekes)",
"nl": "<b>Type 2 met kabel</b> (J1772)"
"nl": "<b>Type 2 met kabel</b> (J1772)",
"de": "<b>Typ 2 mit Kabel</b> (mennekes)"
},
"hideInAnswer": true,
"icon": {
@ -727,7 +759,8 @@
"ifnot": "socket:tesla_supercharger_ccs=",
"then": {
"en": "<b>Tesla Supercharger CCS</b> (a branded type2_css)",
"nl": "<b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)"
"nl": "<b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)",
"de": "<b>Tesla Supercharger CCS</b> (ein Markenzeichen von type2_css)"
},
"icon": {
"path": "./assets/layers/charging_station/Type2_CCS.svg",
@ -769,7 +802,8 @@
},
"then": {
"en": "<b>Tesla Supercharger CCS</b> (a branded type2_css)",
"nl": "<b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)"
"nl": "<b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)",
"de": "<b>Tesla Supercharger CCS</b> (ein Markenzeichen von type2_css)"
},
"hideInAnswer": true,
"icon": {
@ -782,7 +816,8 @@
"ifnot": "socket:tesla_destination=",
"then": {
"en": "<b>Tesla Supercharger (destination)</b>",
"nl": "<b>Tesla Supercharger (destination)</b>"
"nl": "<b>Tesla Supercharger (destination)</b>",
"de": "<b>Tesla Supercharger (Destination)</b>"
},
"icon": {
"path": "./assets/layers/charging_station/Tesla-hpwc-model-s.svg",
@ -830,7 +865,8 @@
},
"then": {
"en": "<b>Tesla Supercharger (destination)</b>",
"nl": "<b>Tesla Supercharger (destination)</b>"
"nl": "<b>Tesla Supercharger (destination)</b>",
"de": "<b>Tesla Supercharger (Destination)</b>"
},
"hideInAnswer": true,
"icon": {
@ -843,7 +879,8 @@
"ifnot": "socket:tesla_destination=",
"then": {
"en": "<b>Tesla supercharger (destination)</b> (A Type 2 with cable branded as tesla)",
"nl": "<b>Tesla supercharger (destination</b> (Een Type 2 met kabel en Tesla-logo)"
"nl": "<b>Tesla supercharger (destination</b> (Een Type 2 met kabel en Tesla-logo)",
"de": "<b>Tesla supercharger (Destination)</b> (Typ 2 mit Kabel von Tesla)"
},
"icon": {
"path": "./assets/layers/charging_station/Type2_tethered.svg",
@ -891,7 +928,8 @@
},
"then": {
"en": "<b>Tesla supercharger (destination)</b> (A Type 2 with cable branded as tesla)",
"nl": "<b>Tesla supercharger (destination</b> (Een Type 2 met kabel en Tesla-logo)"
"nl": "<b>Tesla supercharger (destination</b> (Een Type 2 met kabel en Tesla-logo)",
"de": "<b>Tesla supercharger (Destination)</b> (Typ 2 mit Kabel von Tesla)"
},
"hideInAnswer": true,
"icon": {
@ -904,7 +942,8 @@
"ifnot": "socket:USB-A=",
"then": {
"en": "<b>USB</b> to charge phones and small electronics",
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden"
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden",
"de": "<b>USB</b> zum Aufladen von Handys und kleinen Elektrogeräten"
},
"icon": {
"path": "./assets/layers/charging_station/usb_port.svg",
@ -920,7 +959,8 @@
},
"then": {
"en": "<b>USB</b> to charge phones and small electronics",
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden"
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden",
"de": "<b>USB</b> zum Aufladen von Handys und kleinen Elektrogeräten"
},
"hideInAnswer": true,
"icon": {
@ -933,7 +973,8 @@
"ifnot": "socket:bosch_3pin=",
"then": {
"en": "<b>Bosch Active Connect with 3 pins</b> and cable",
"nl": "<b>Bosch Active Connect met 3 pinnen</b> aan een kabel"
"nl": "<b>Bosch Active Connect met 3 pinnen</b> aan een kabel",
"de": "<b>Bosch Active Connect mit 3 Pins</b> und Kabel"
},
"icon": {
"path": "./assets/layers/charging_station/bosch-3pin.svg",
@ -971,7 +1012,8 @@
},
"then": {
"en": "<b>Bosch Active Connect with 3 pins</b> and cable",
"nl": "<b>Bosch Active Connect met 3 pinnen</b> aan een kabel"
"nl": "<b>Bosch Active Connect met 3 pinnen</b> aan een kabel",
"de": "<b> Bosch Active Connect mit 3 Pins </b> und Kabel"
},
"hideInAnswer": true,
"icon": {
@ -984,7 +1026,8 @@
"ifnot": "socket:bosch_5pin=",
"then": {
"en": "<b>Bosch Active Connect with 5 pins</b> and cable",
"nl": "<b>Bosch Active Connect met 5 pinnen</b> aan een kabel"
"nl": "<b>Bosch Active Connect met 5 pinnen</b> aan een kabel",
"de": "<b>Bosch Active Connect mit 5 Pins</b> und Kabel"
},
"icon": {
"path": "./assets/layers/charging_station/bosch-5pin.svg",
@ -1022,7 +1065,8 @@
},
"then": {
"en": "<b>Bosch Active Connect with 5 pins</b> and cable",
"nl": "<b>Bosch Active Connect met 5 pinnen</b> aan een kabel"
"nl": "<b>Bosch Active Connect met 5 pinnen</b> aan een kabel",
"de": "<b> Bosch Active Connect mit 5 Pins </b> und Kabel"
},
"hideInAnswer": true,
"icon": {
@ -1036,11 +1080,13 @@
"id": "plugs-0",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> heeft dit oplaadpunt?",
"de": "Wie viele Stecker vom Typ <div style='display: inline-block'><b><b>Schuko-Stecker</b> ohne Erdungsstift (CEE7/4 Typ F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> sind hier vorhanden?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:schuko}</b> plugs of type <div style='display: inline-block'><b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> available here",
"nl": "Hier zijn <b class='text-xl'>{socket:schuko}</b> stekkers van het type <div style='display: inline-block'><b><b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div>"
"nl": "Hier zijn <b class='text-xl'>{socket:schuko}</b> stekkers van het type <div style='display: inline-block'><b><b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div>",
"de": "Hier sind <b class='text-xl'>{socket:schuko}</b> Stecker des Typs <div style='display: inline-block'><b><b>Schuko-Stecker</b> ohne Erdungsstift (CEE7/4 Typ F)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/CEE7_4F.svg'/></div> vorhanden"
},
"freeform": {
"key": "socket:schuko",
@ -1057,11 +1103,13 @@
"id": "plugs-1",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Europese stekker</b> met aardingspin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Europese stekker</b> met aardingspin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> heeft dit oplaadpunt?",
"de": "Wie viele Stecker des Typs <div style='display: inline-block'><b><b>Europäischer Wandstecker</b> mit Erdungsstift (CEE7/4 Typ E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> sind hier vorhanden?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:typee}</b> plugs of type <div style='display: inline-block'><b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> available here",
"nl": "Hier zijn <b class='text-xl'>{socket:typee}</b> stekkers van het type <div style='display: inline-block'><b><b>Europese stekker</b> met aardingspin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div>"
"nl": "Hier zijn <b class='text-xl'>{socket:typee}</b> stekkers van het type <div style='display: inline-block'><b><b>Europese stekker</b> met aardingspin (CEE7/4 type E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div>",
"de": "Hier sind <b class='text-xl'>{socket:typee}</b> Stecker des Typs <div style='display: inline-block'><b><b>Europäischer Wandstecker</b> mit Erdungsstift (CEE7/4 Typ E)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/TypeE.svg'/></div> vorhanden"
},
"freeform": {
"key": "socket:typee",
@ -1078,7 +1126,7 @@
"id": "plugs-2",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:chademo}</b> plugs of type <div style='display: inline-block'><b><b>Chademo</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Chademo_type4.svg'/></div> available here",
@ -1099,7 +1147,7 @@
"id": "plugs-3",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 met kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 met kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type1_cable}</b> plugs of type <div style='display: inline-block'><b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> available here",
@ -1120,7 +1168,7 @@
"id": "plugs-4",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 <i>zonder</i> kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 <i>zonder</i> kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type1}</b> plugs of type <div style='display: inline-block'><b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1_J1772.svg'/></div> available here",
@ -1141,7 +1189,7 @@
"id": "plugs-5",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 CCS</b> (ook gekend als Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 1 CCS</b> (ook gekend als Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type1_combo}</b> plugs of type <div style='display: inline-block'><b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type1-ccs.svg'/></div> available here",
@ -1162,7 +1210,7 @@
"id": "plugs-6",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:tesla_supercharger}</b> plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> available here",
@ -1183,7 +1231,7 @@
"id": "plugs-7",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type2}</b> plugs of type <div style='display: inline-block'><b><b>Type 2</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_socket.svg'/></div> available here",
@ -1204,7 +1252,7 @@
"id": "plugs-8",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type2_combo}</b> plugs of type <div style='display: inline-block'><b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> available here",
@ -1225,7 +1273,7 @@
"id": "plugs-9",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Type 2 met kabel</b> (J1772)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:type2_cable}</b> plugs of type <div style='display: inline-block'><b><b>Type 2 with cable</b> (mennekes)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> available here",
@ -1246,7 +1294,7 @@
"id": "plugs-10",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (een type2 CCS met Tesla-logo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:tesla_supercharger_ccs}</b> plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger CCS</b> (a branded type2_css)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_CCS.svg'/></div> available here",
@ -1267,7 +1315,7 @@
"id": "plugs-11",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:tesla_destination}</b> plugs of type <div style='display: inline-block'><b><b>Tesla Supercharger (destination)</b></b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/></div> available here",
@ -1309,7 +1357,7 @@
"id": "plugs-13",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:USB-A}</b> plugs of type <div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> available here",
@ -1330,7 +1378,7 @@
"id": "plugs-14",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Bosch Active Connect with 3 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Bosch Active Connect met 3 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Bosch Active Connect met 3 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:bosch_3pin}</b> plugs of type <div style='display: inline-block'><b><b>Bosch Active Connect with 3 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> available here",
@ -1351,7 +1399,7 @@
"id": "plugs-15",
"question": {
"en": "How much plugs of type <div style='display: inline-block'><b><b>Bosch Active Connect with 5 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-5pin.svg'/></div> are available here?",
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Bosch Active Connect met 5 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-5pin.svg'/></div> heeft dit oplaadpunt?"
"nl": "Hoeveel stekkers van type <div style='display: inline-block'><b><b>Bosch Active Connect met 5 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-5pin.svg'/></div> heeft dit oplaadpunt?"
},
"render": {
"en": "There are <b class='text-xl'>{socket:bosch_5pin}</b> plugs of type <div style='display: inline-block'><b><b>Bosch Active Connect with 5 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-5pin.svg'/></div> available here",
@ -2945,7 +2993,8 @@
},
"render": {
"en": "<div style='display: inline-block'><b><b>Tesla supercharger (destination)</b> (A Type 2 with cable branded as tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> outputs {socket:tesla_destination:voltage} volt",
"nl": "<div style='display: inline-block'><b><b>Tesla supercharger (destination).</b> (Een Type 2 met kabel en Tesla-logo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> heeft een spanning van {socket:tesla_destination:voltage} volt"
"nl": "<div style='display: inline-block'><b><b>Tesla supercharger (destination).</b> (Een Type 2 met kabel en Tesla-logo)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> heeft een spanning van {socket:tesla_destination:voltage} volt",
"de": "<div style='display: inline-block'><b><b>Tesla Supercharger (Destination)</b> (Typ 2 mit Kabel von Tesla)</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/Type2_tethered.svg'/></div> liefert {socket:tesla_destination:voltage} Volt"
},
"freeform": {
"key": "socket:tesla_destination:voltage",
@ -3079,11 +3128,13 @@
"group": "technical",
"question": {
"en": "What voltage do the plugs with <div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> offer?",
"nl": "Welke spanning levert de stekker van type <div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div>"
"nl": "Welke spanning levert de stekker van type <div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div>",
"de": "Welche Spannung liefern die Stecker mit <div style='display: inline-block'><b><b>USB</b> zum Laden von Handys und kleinen Elektrogeräten</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div>?"
},
"render": {
"en": "<div style='display: inline-block'><b><b>USB</b> to charge phones and small electronics</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> outputs {socket:USB-A:voltage} volt",
"nl": "<div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> heeft een spanning van {socket:USB-A:voltage} volt"
"nl": "<div style='display: inline-block'><b><b>USB</b> om GSMs en kleine electronica op te laden</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> heeft een spanning van {socket:USB-A:voltage} volt",
"de": "<div style='display: inline-block'><b><b>USB</b> zum Aufladen von Telefonen und kleinen Elektrogeräten</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/usb_port.svg'/></div> liefert {socket:USB-A:voltage} Volt"
},
"freeform": {
"key": "socket:USB-A:voltage",
@ -3094,7 +3145,8 @@
"if": "socket:USB-A:voltage=5 V",
"then": {
"en": "<b>USB</b> to charge phones and small electronics outputs 5 volt",
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden heeft een spanning van 5 volt"
"nl": "<b>USB</b> om GSMs en kleine electronica op te laden heeft een spanning van 5 volt",
"de": "<b>USB</b> zum Aufladen von Handys und kleinen Elektrogeräten liefert 5 Volt"
},
"icon": {
"path": "./assets/layers/charging_station/usb_port.svg",
@ -3206,7 +3258,8 @@
"group": "technical",
"question": {
"en": "What voltage do the plugs with <div style='display: inline-block'><b><b>Bosch Active Connect with 3 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> offer?",
"nl": "Welke spanning levert de stekker van type <div style='display: inline-block'><b><b>Bosch Active Connect met 3 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div>"
"nl": "Welke spanning levert de stekker van type <div style='display: inline-block'><b><b>Bosch Active Connect met 3 pinnen</b> aan een kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div>",
"de": "Welche Spannung bieten die Stecker mit <div style='display: inline-block'><b><b>Bosch Active Connect mit 3 Pins</b> und Kabel</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div>?"
},
"render": {
"en": "<div style='display: inline-block'><b><b>Bosch Active Connect with 3 pins</b> and cable</b> <img style='width:1rem; display: inline-block' src='./assets/layers/charging_station/bosch-3pin.svg'/></div> outputs {socket:bosch_3pin:voltage} volt",
@ -3358,7 +3411,7 @@
"then": {
"en": "24/7 opened (including holidays)",
"nl": "24/7 open - ook tijdens vakanties",
"de": "durchgehend geöffnet (einschließlich Feiertage)",
"de": "Die Station ist durchgehend geöffnet (einschließlich Feiertage)",
"es": "Abre 24/7 (incluidos días festivos)"
}
}
@ -3368,7 +3421,8 @@
"id": "fee",
"question": {
"en": "Does one have to pay to use this charging station?",
"nl": "Moet men betalen om dit oplaadpunt te gebruiken?"
"nl": "Moet men betalen om dit oplaadpunt te gebruiken?",
"de": "Muss man für die Nutzung dieser Ladestation bezahlen?"
},
"mappings": [
{
@ -3382,7 +3436,8 @@
},
"then": {
"nl": "Gratis te gebruiken (zonder aan te melden)",
"en": "Free to use (without authenticating)"
"en": "Free to use (without authenticating)",
"de": "Kostenlos nutzbar (ohne Authentifizierung)"
}
},
{
@ -3396,7 +3451,8 @@
},
"then": {
"nl": "Gratis te gebruiken, maar aanmelden met een applicatie is verplicht",
"en": "Free to use, but one has to authenticate"
"en": "Free to use, but one has to authenticate",
"de": "Kostenlose Nutzung, aber man muss sich authentifizieren"
}
},
{
@ -3407,7 +3463,8 @@
},
"then": {
"nl": "Gratis te gebruiken",
"en": "Free to use"
"en": "Free to use",
"de": "Kostenlose Nutzung"
},
"hideInAnswer": true
},
@ -3420,7 +3477,8 @@
},
"then": {
"nl": "Betalend te gebruiken, maar gratis voor klanten van het bijhorende hotel/café/ziekenhuis/...",
"en": "Paid use, but free for customers of the hotel/pub/hospital/... who operates the charging station"
"en": "Paid use, but free for customers of the hotel/pub/hospital/... who operates the charging station",
"de": "Kostenpflichtige Nutzung, aber kostenlos für Kunden des Hotels / Pub / Krankenhauses / ... wer die Ladestation betreibt"
}
},
{
@ -3432,7 +3490,8 @@
},
"then": {
"nl": "Betalend",
"en": "Paid use"
"en": "Paid use",
"de": "Nutzung gebührenpflichtig"
}
}
]
@ -3441,11 +3500,13 @@
"id": "charge",
"question": {
"en": "How much does one have to pay to use this charging station?",
"nl": "Hoeveel moet men betalen om dit oplaadpunt te gebruiken?"
"nl": "Hoeveel moet men betalen om dit oplaadpunt te gebruiken?",
"de": "Wie viel muss man für die Nutzung dieser Ladestation bezahlen?"
},
"render": {
"en": "Using this charging station costs <b>{charge}</b>",
"nl": "Dit oplaadpunt gebruiken kost <b>{charge}</b>"
"nl": "Dit oplaadpunt gebruiken kost <b>{charge}</b>",
"de": "Die Nutzung dieser Ladestation kostet <b>{charge}</b>"
},
"freeform": {
"key": "charge"
@ -3573,11 +3634,13 @@
"id": "Auth phone",
"render": {
"en": "Authenticate by calling or SMS'ing to <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>",
"nl": "Aanmelden door te bellen of te SMS'en naar <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>"
"nl": "Aanmelden door te bellen of te SMS'en naar <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>",
"de": "Authentifizierung durch Anruf oder SMS an <a href='tel:{authentication:phone_call:number}'>{authentication:phone_call:number}</a>"
},
"question": {
"en": "What's the phone number for authentication call or SMS?",
"nl": "Wat is het telefoonnummer dat men moet bellen of SMS'en om zich aan te melden?"
"nl": "Wat is het telefoonnummer dat men moet bellen of SMS'en om zich aan te melden?",
"de": "Wie lautet die Telefonnummer für den Authentifizierungsanruf oder die SMS?"
},
"freeform": {
"key": "authentication:phone_call:number",
@ -3594,21 +3657,24 @@
"id": "maxstay",
"question": {
"en": "What is the maximum amount of time one is allowed to stay here?",
"nl": "Hoelang mag een voertuig hier blijven staan?"
"nl": "Hoelang mag een voertuig hier blijven staan?",
"de": "Wie lange darf man hier maximal parken?"
},
"freeform": {
"key": "maxstay"
},
"render": {
"en": "One can stay at most <b>{canonical(maxstay)}</b>",
"nl": "De maximale parkeertijd hier is <b>{canonical(maxstay)}</b>"
"nl": "De maximale parkeertijd hier is <b>{canonical(maxstay)}</b>",
"de": "Die maximale Parkdauer beträgt <b>{canonical(maxstay)}</b>"
},
"mappings": [
{
"if": "maxstay=unlimited",
"then": {
"en": "No timelimit on leaving your vehicle here",
"nl": "Geen maximum parkeertijd"
"nl": "Geen maximum parkeertijd",
"de": "Keine Höchstparkdauer"
}
}
],
@ -3734,11 +3800,13 @@
"id": "phone",
"question": {
"en": "What number can one call if there is a problem with this charging station?",
"nl": "Wat is het telefoonnummer van de beheerder van dit oplaadpunt?"
"nl": "Wat is het telefoonnummer van de beheerder van dit oplaadpunt?",
"de": "Welche Nummer kann man anrufen, wenn es ein Problem mit dieser Ladestation gibt?"
},
"render": {
"en": "In case of problems, call <a href='tel:{phone}'>{phone}</a>",
"nl": "Bij problemen, bel naar <a href='tel:{phone}'>{phone}</a>"
"nl": "Bij problemen, bel naar <a href='tel:{phone}'>{phone}</a>",
"de": "Bei Problemen, anrufen unter <a href='tel:{phone}'>{phone}</a>"
},
"freeform": {
"key": "phone",
@ -3749,11 +3817,13 @@
"id": "email",
"question": {
"en": "What is the email address of the operator?",
"nl": "Wat is het email-adres van de operator?"
"nl": "Wat is het email-adres van de operator?",
"de": "Wie lautet die E-Mail-Adresse des Betreibers?"
},
"render": {
"en": "In case of problems, send an email to <a href='mailto:{email}'>{email}</a>",
"nl": "Bij problemen, email naar <a href='mailto:{email}'>{email}</a>"
"nl": "Bij problemen, email naar <a href='mailto:{email}'>{email}</a>",
"de": "Bei Problemen senden Sie bitte eine E-Mail an <a href='mailto:{email}'>{email}</a>"
},
"freeform": {
"key": "email",
@ -4002,7 +4072,8 @@
],
"title": {
"en": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)",
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)"
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)",
"de": "eine Ladestation für Elektrofahrräder mit einer normalen europäischen Steckdose <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (zum Laden von Elektrofahrrädern)"
},
"preciseInput": {
"preferredBackground": "map"
@ -4086,7 +4157,8 @@
"question": {
"en": "All connectors",
"nl": "Alle types",
"de": "Alle Anschlüsse"
"de": "Alle Anschlüsse",
"ca": "Tots els connectors"
}
},
{
@ -4229,12 +4301,14 @@
"human": {
"en": " minutes",
"nl": " minuten",
"ru": " минут"
"ru": " минут",
"ca": " minuts"
},
"humanSingular": {
"en": " minute",
"nl": " minuut",
"ru": " минута"
"ru": " минута",
"ca": " minut"
}
},
{
@ -4251,12 +4325,14 @@
"human": {
"en": " hours",
"nl": " uren",
"ru": " часов"
"ru": " часов",
"ca": " hores"
},
"humanSingular": {
"en": " hour",
"nl": " uur",
"ru": " час"
"ru": " час",
"ca": " hora"
}
},
{
@ -4270,12 +4346,14 @@
"human": {
"en": " days",
"nl": " day",
"ru": " дней"
"ru": " дней",
"ca": " dies"
},
"humanSingular": {
"en": " day",
"nl": " dag",
"ru": " день"
"ru": " день",
"ca": " dia"
}
}
]
@ -4312,7 +4390,8 @@
"human": {
"en": "Volts",
"nl": "volt",
"ru": "Вольт"
"ru": "Вольт",
"ca": "Volts"
}
}
],
@ -4348,7 +4427,8 @@
],
"human": {
"en": "A",
"nl": "A"
"nl": "A",
"ca": "A"
}
}
],
@ -4382,7 +4462,8 @@
"human": {
"en": "kilowatt",
"nl": "kilowatt",
"ru": "киловатт"
"ru": "киловатт",
"ca": "quilovats"
}
},
{
@ -4393,7 +4474,8 @@
"human": {
"en": "megawatt",
"nl": "megawatt",
"ru": "мегаватт"
"ru": "мегаватт",
"ca": "megavats"
}
}
],

View file

@ -0,0 +1,300 @@
{
"id": "climbing",
"name": {
"nl": "Klimgelegenheden",
"de": "Klettermöglichkeiten",
"en": "Climbing opportunities",
"ja": "登坂教室",
"fr": "Opportunité descalade",
"it": "Opportunità di arrampicata"
},
"minzoom": 10,
"source": {
"osmTags": {
"and": [
"sport=climbing",
"climbing!~route",
"leisure!~sports_centre",
"climbing!=route_top",
"climbing!=route_bottom"
]
}
},
"title": {
"render": {
"en": "Climbing opportunity",
"nl": "Klimgelegenheid",
"de": "Klettermöglichkeit",
"ja": "登坂教室",
"nb_NO": "Klatremulighet",
"fr": "Opportunité descalade",
"it": "Opportunità di arrampicata"
},
"mappings": [
{
"if": "climbing=crag",
"then": {
"en": "Climbing crag <b>{name}</b>",
"fr": "Mur descalade <b>{name}</b>",
"it": "Muro da arrampicata <b>{name}</b>",
"de": "Klettergarten <b>{name}</b>"
}
},
{
"if": {
"and": [
{
"or": [
"climbing=area",
"climbing=site"
]
},
"name~*"
]
},
"then": {
"en": "Climbing area <b>{name}</b>",
"nl": "Klimsite <b>{name}</b>",
"fr": "Zone descalade <b>{name}</b>",
"de": "Klettergebiet <b>{name}</b>",
"it": "Area di arrampicata <b>{name}</b>"
}
},
{
"if": {
"or": [
"climbing=site",
"climbing=area"
]
},
"then": {
"en": "Climbing site",
"nl": "Klimsite",
"fr": "Site descalade",
"de": "Klettergebiet",
"it": "Sito di arrampicata",
"ca": "Llocs d'escalada"
}
},
{
"if": "name~*",
"then": {
"nl": "Klimgelegenheid <b>{name}</b>",
"en": "Climbing opportunity <b>{name}</b>",
"fr": "Opportunité descalade <b>{name}</b>",
"de": "Klettermöglichkeit <b>{name}</b>",
"it": "Opportunità di arrampicata <b>{name}</b>"
}
}
]
},
"description": {
"nl": "Een klimgelegenheid",
"de": "Eine Klettergelegenheit",
"en": "A climbing opportunity",
"ja": "登坂教室",
"nb_NO": "En klatremulighet",
"fr": "Opportunité descalade",
"it": "Unopportunità di arrampicata"
},
"tagRenderings": [
"images",
{
"id": "minimap",
"render": "{minimap(18, id, _contained_climbing_route_ids): height: 9rem; overflow: hidden; border-radius:3rem; }"
},
{
"render": {
"en": "<h3>Length overview</h3>{histogram(_length_hist)}",
"fr": "<h3>Résumé de longueur</h3>{histogram(_length_hist)}",
"de": "<h3>Längenübersicht</h3>{histogram(_length_hist)}",
"it": "<h3>Riassunto della lunghezza</h3>{histogram(_length_hist)}"
},
"condition": "_length_hist!~\\[\\]",
"id": "Contained routes length hist"
},
{
"render": {
"en": "<h3>Grades overview</h3>{histogram(_difficulty_hist)}",
"fr": "<h3>Résumé des difficultés</h3>{histogram(_difficulty_hist)}",
"de": "<h3>Schwierigkeitsübersicht</h3>{histogram(_difficulty_hist)}",
"it": "<h3>Riassunto delle difficoltà</h3>{histogram(_difficulty_hist)}"
},
"condition": "_difficulty_hist!~\\[\\]",
"id": "Contained routes hist"
},
{
"render": {
"en": "<h3>Contains {_contained_climbing_routes_count} routes</h3> <ul>{_contained_climbing_routes}</ul>",
"fr": "<h3>Contient {_contained_climbing_routes_count} voies</h3> <ul>{_contained_climbing_routes}</ul>",
"it": "<h3>Contiene {_contained_climbing_routes_count} vie</h3> <ul>{_contained_climbing_routes}</ul>",
"de": "<h3> Enthält {_contained_climbing_routes_count} Routen</h3> <ul>{_contained_climbing_routes}</ul>"
},
"condition": "_contained_climbing_routes~*",
"id": "Contained_climbing_routes"
},
{
"render": {
"en": "<strong>{name}</strong>",
"nl": "<strong>{name}</strong>",
"de": "<strong>{name}</strong>",
"ca": "<strong>{name}</strong>",
"fr": "<strong>{name}</strong>",
"id": "<strong>{name}</strong>",
"ru": "<strong>{name}</strong>",
"ja": "<strong>{name}</strong>",
"it": "<strong>{name}</strong>"
},
"question": {
"en": "What is the name of this climbing opportunity?",
"nl": "Wat is de naam van dit Klimgelegenheid?",
"de": "Wie heißt diese Klettergelegenheit?",
"ja": "この登坂教室の名前は何ですか?",
"fr": "Quel est le nom de ce site ?",
"it": "Qual è il nome di questa opportunità di arrampicata?"
},
"freeform": {
"key": "name"
},
"mappings": [
{
"if": {
"and": [
"noname=yes",
"name="
]
},
"then": {
"en": "This climbing opportunity doesn't have a name",
"nl": "Dit Klimgelegenheid heeft geen naam",
"de": "Diese Klettergelegenheit hat keinen Namen",
"ja": "この登坂教室には名前がついていない",
"fr": "Ce site na pas de nom",
"it": "Questa opportunità di arrampicata non ha un nome"
}
}
],
"id": "name"
},
{
"question": "What kind of climbing opportunity is this?",
"mappings": [
{
"if": "climbing=boulder",
"then": {
"en": "A climbing boulder - a single rock or cliff with one or a few climbing routes which can be climbed safely without rope",
"fr": "Rocher descalade, rocher avec une ou peu de voie permettant descalader sans corde",
"de": "Ein Kletterfelsen - ein einzelner Felsen oder eine Klippe mit einer oder wenigen Kletterrouten, die ohne Seil sicher bestiegen werden können",
"it": "Un masso per arrampicata (una singola roccia o falesia con una o poche vie di arrampicata che possono essere scalate in sicurezza senza una corda)"
}
},
{
"if": "climbing=crag",
"then": {
"en": "A climbing crag - a single rock or cliff with at least a few climbing routes",
"fr": "Mur descalade, rocher avec plusieurs voies descalades",
"it": "Un muro da arrampicata (un singolo masso o falesia con almeno qualche via per arrampicata)",
"de": "Ein Kletterfelsen - ein einzelner Fels oder eine Klippe mit mindestens einigen Kletterrouten"
}
},
{
"if": "climbing=area",
"then": "A climbing area with one or more climbing crags and/or boulders"
}
],
"id": "Type"
},
{
"question": {
"en": "What is the rock type here?",
"fr": "Quel est le type de roche ?",
"de": "Welchen Gesteinstyp gibt es hier?",
"it": "Qual è il tipo di roccia qua?"
},
"render": {
"en": "The rock type is {rock}",
"fr": "La roche est du {rock}",
"de": "Der Gesteinstyp ist {rock}",
"it": "Il tipo di roccia è {rock}"
},
"freeform": {
"key": "rock"
},
"mappings": [
{
"if": "rock=limestone",
"then": {
"en": "Limestone",
"nl": "Kalksteen",
"fr": "Calcaire",
"de": "Kalkstein",
"it": "Calcare"
}
}
],
"condition": {
"or": [
"climbing=crag",
"natural=cliff",
"natural=bare_rock"
]
},
"id": "Rock type (crag/rock/cliff only)"
}
],
"presets": [
{
"tags": [
"sport=climbing"
],
"title": {
"en": "a climbing opportunity",
"nl": "een klimgelegenheid",
"de": "eine klettermöglichkeit",
"ja": "登坂教室",
"nb_NO": "en klatremulighet",
"fr": "une opportunité descalade",
"it": "una opportunità di arrampicata"
},
"description": {
"nl": "Een klimgelegenheid",
"de": "Eine Klettergelegenheit",
"en": "A climbing opportunity",
"ja": "登坂教室",
"nb_NO": "En klatremulighet",
"fr": "Opportunité descalade",
"it": "Unopportunità di arrampicata"
}
}
],
"calculatedTags": [
"_contained_climbing_routes_properties=feat.overlapWith('climbing_route').map(f => f.feat.properties).map(p => {return {id: p.id, name: p.name, 'climbing:grade:french': p['climbing:grade:french'], 'climbing:length': p['climbing:length']} })",
"_contained_climbing_routes=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => `<li><a href='#${p.id}'>${p.name ?? 'climbing route'}</a> (<b>${p['climbing:grade:french'] ?? 'unknown difficulty'}</b>, ${p['climbing:length'] ?? 'unkown length'} meter)</li>`).join('')",
"_contained_climbing_route_ids=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p.id)",
"_difficulty_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:grade:french'])",
"_length_hist=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').map(p => p['climbing:length'])",
"_contained_climbing_routes_count=JSON.parse(feat.properties._contained_climbing_routes_properties ?? '[]').length"
],
"mapRendering": [
{
"icon": {
"render": "./assets/themes/climbing/climbing_no_rope.svg"
},
"iconSize": {
"render": "40,40,center"
},
"location": [
"point",
"centroid"
]
},
{
"color": {
"render": "#d38d5fAA"
},
"width": {
"render": "8"
}
}
]
}

View file

@ -0,0 +1,189 @@
{
"id": "climbing_club",
"name": {
"de": "Klettervereine",
"nl": "Klimclub",
"en": "Climbing club",
"ru": "Клуб скалолазания",
"ja": "クライミングクラブ",
"zh_Hant": "攀岩社團",
"nb_NO": "Klatreklubb",
"fr": "Club descalade",
"it": "Club di arrampicata",
"hu": "Mászóegyesület"
},
"minzoom": 10,
"source": {
"osmTags": {
"or": [
"club=climbing",
{
"and": [
"sport=climbing",
{
"or": [
"office~*",
"club~*"
]
}
]
}
]
}
},
"title": {
"render": {
"en": "Climbing club",
"nl": "Klimclub",
"de": "Kletterverein",
"ru": "Клуб скалолазания",
"ja": "クライミングクラブ",
"zh_Hant": "攀岩社團",
"nb_NO": "Klatreklubb",
"fr": "Club descalade",
"it": "Club di arrampicata"
},
"mappings": [
{
"if": "office~*",
"then": {
"nl": "Klimorganisatie",
"en": "Climbing NGO",
"de": "Kletter-Organisation",
"ja": "クライミングNGO",
"zh_Hant": "攀岩 NGO",
"fr": "Association descalade",
"it": "Associazione di arrampicata",
"hu": "Mászószervezet"
}
}
]
},
"description": {
"de": "Ein Kletterverein oder -organisation",
"nl": "Een klimclub of organisatie",
"en": "A climbing club or organisation",
"ja": "クライミングクラブや団体",
"zh_Hant": "攀岩社團或組織",
"nb_NO": "En klatreklubb eller organisasjoner",
"fr": "Club ou association descalade",
"it": "Un club o associazione di arrampacata",
"hu": "Mászóegyesület vagy -szervezet"
},
"tagRenderings": [
{
"render": {
"en": "<strong>{name}</strong>",
"nl": "<strong>{name}</strong>",
"de": "<strong>{name}</strong>",
"ca": "<strong>{name}</strong>",
"fr": "<strong>{name}</strong>",
"id": "<strong>{name}</strong>",
"ru": "<strong>{name}</strong>",
"ja": "<strong>{name}</strong>",
"zh_Hant": "<strong>{name}</strong>",
"it": "<strong>{name}</strong>",
"hu": "<strong>{name}</strong>"
},
"question": {
"en": "What is the name of this climbing club or NGO?",
"de": "Wie lautet der Name dieses Vereins oder Organisation?",
"nl": "Wat is de naam van deze klimclub?",
"ja": "この登山クラブやNGOの名前は何ですか?",
"fr": "Quel est le nom du club ou de lassociation ?",
"it": "Qual è il nome di questo club o associazione di arrampicata?",
"hu": "Mi a neve ennek a mászóegyesületnek vagy szervezetnek?"
},
"freeform": {
"key": "name"
},
"id": "climbing_club-name"
},
"website",
"email",
"phone",
"opening_hours"
],
"presets": [
{
"tags": [
"club=sport",
"sport=climbing"
],
"title": {
"de": "eine kletterverein",
"en": "a climbing club",
"nl": "een klimclub",
"ja": "クライミングクラブ",
"nb_NO": "en klatreklubb",
"ru": "Клуб скалолазания",
"fr": "une club descalade",
"it": "una club di arrampicata",
"hu": "Mászóegyesület"
},
"description": {
"de": "Ein Kletterverein",
"nl": "Een klimclub",
"en": "A climbing club",
"ja": "クライミングクラブ",
"nb_NO": "En klatreklubb",
"ru": "Клуб скалолазания",
"fr": "Un club descalade",
"it": "Un club di arrampicata",
"hu": "Egy mászóegyesület"
}
},
{
"tags": [
"office=ngo",
"sport=climbing"
],
"title": {
"de": "Eine Kletterorganisation",
"en": "a climbing ngo",
"nl": "een een klimorganisatie",
"ja": "クライミングNGO",
"fr": "une association descalade",
"it": "una associazione di arrampicata",
"hu": "Mászószervezet"
},
"description": {
"de": "Eine Organisation, die sich mit dem Klettern beschäftigt",
"nl": "Een VZW die werkt rond klimmen",
"en": "An NGO working around climbing",
"ja": "登山に関わるNGO",
"fr": "Une association descalade",
"it": "Unassociazione che ha a che fare con larrampicata",
"hu": "Mászással foglalkozó civil szervezet"
}
}
],
"mapRendering": [
{
"icon": {
"render": "./assets/themes/climbing/club.svg"
},
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
}
],
"iconSize": {
"render": "40,40,center"
},
"location": [
"point",
"centroid"
],
"label": {
"mappings": [
{
"if": "name~*",
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
}
]
}
}
]
}

View file

@ -0,0 +1,114 @@
{
"id": "climbing_gym",
"name": {
"de": "Kletterhallen",
"en": "Climbing gyms",
"nl": "Klimzalen",
"ja": "クライミングジム",
"fr": "Salle descalade",
"it": "Palestre di arrampicata",
"ru": "Комплексы скалолазания"
},
"minzoom": 10,
"source": {
"osmTags": {
"and": [
"sport=climbing",
"leisure=sports_centre"
]
}
},
"title": {
"render": {
"nl": "Klimzaal",
"de": "Kletterhalle",
"en": "Climbing gym",
"ja": "クライミングジム",
"fr": "Salle descalade",
"it": "Palestra di arrampicata",
"ru": "Комплекс скалолазания"
},
"mappings": [
{
"if": "name~*",
"then": {
"nl": "Klimzaal <strong>{name}</strong>",
"de": "Kletterhalle <strong>{name}</strong>",
"en": "Climbing gym <strong>{name}</strong>",
"ja": "クライミングジム<strong>{name}</strong>",
"fr": "Salle descalade <strong>{name}</strong>",
"it": "Palestra di arrampicata <strong>{name}</strong>"
}
}
]
},
"description": {
"de": "Eine Kletterhalle",
"en": "A climbing gym",
"ja": "クライミングジム",
"nl": "Een klimzaal",
"fr": "Une salle descalade",
"it": "Una palestra di arrampicata",
"ru": "Комплекс скалолазания"
},
"tagRenderings": [
"images",
{
"render": {
"en": "<strong>{name}</strong>",
"nl": "<strong>{name}</strong>",
"de": "<strong>{name}</strong>",
"ca": "<strong>{name}</strong>",
"fr": "<strong>{name}</strong>",
"id": "<strong>{name}</strong>",
"ru": "<strong>{name}</strong>",
"ja": "<strong>{name}</strong>",
"it": "<strong>{name}</strong>"
},
"question": {
"en": "What is the name of this climbing gym?",
"nl": "Wat is de naam van dit Klimzaal?",
"de": "Wie heißt diese Kletterhalle?",
"ja": "このクライミングジムは何という名前ですか?",
"fr": "Quel est le nom de la salle descalade ?",
"it": "Qual è il nome di questa palestra di arrampicata?"
},
"freeform": {
"key": "name"
},
"id": "name"
},
"website",
"phone",
"email",
"opening_hours"
],
"mapRendering": [
{
"icon": {
"render": "./assets/themes/climbing/climbing_gym.svg"
},
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
}
],
"iconSize": {
"render": "40,40,center"
},
"location": [
"point",
"centroid"
],
"label": {
"mappings": [
{
"if": "name~*",
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
}
]
}
}
]
}

View file

@ -0,0 +1,142 @@
{
"id": "climbing_opportunity",
"name": {
"nl": "Klimgelegenheiden?",
"de": "Klettermöglichkeiten?",
"en": "Climbing opportunities?",
"ja": "登坂教室?",
"nb_NO": "Klatremuligheter?",
"fr": "Opportunités descalade ?",
"it": "Opportunità di arrampicata?"
},
"minzoom": 19,
"source": {
"osmTags": {
"and": [
{
"or": [
"leisure=sports_centre",
"barrier=wall",
"barrier=retaining_wall",
"natural=cliff",
"natural=rock",
"natural=stone"
]
},
"climbing="
]
}
},
"title": {
"render": {
"en": "Climbing opportunity?",
"nl": "Klimgelegenheid?",
"de": "Klettermöglichkeit?",
"ja": "登坂教室?",
"nb_NO": "Klatremulighet?",
"fr": "Opportunité descalade ?",
"it": "Opportunità di arrampicata?"
}
},
"description": {
"nl": "Een klimgelegenheid?",
"de": "Eine Klettergelegenheit?",
"en": "A climbing opportunity?",
"ja": "登坂教室?",
"nb_NO": "En klatremulighet?",
"fr": "Opportunité descalade ?",
"it": "Unopportunità di arrampicata?"
},
"tagRenderings": [
{
"id": "climbing-opportunity-name",
"render": {
"en": "<strong>{name}</strong>",
"de": "<strong>{name}</strong>",
"ca": "<strong>{name}</strong>",
"fr": "<strong>{name}</strong>",
"id": "<strong>{name}</strong>",
"ru": "<strong>{name}</strong>",
"ja": "<strong>{name}</strong>",
"nl": "<strong>{name}</strong>",
"it": "<strong>{name}</strong>"
},
"condition": "name~*"
},
{
"id": "climbing-possible",
"question": {
"en": "Is climbing possible here?",
"de": "Kann hier geklettert werden?",
"ja": "ここで登坂はできますか?",
"nb_NO": "Er klatring mulig her?",
"fr": "Est-il possible descalader ici ?",
"it": "È possibile arrampicarsi qua?"
},
"mappings": [
{
"if": {
"and": [
"sport=climbing"
]
},
"then": {
"en": "Climbing is possible here",
"de": "Hier kann geklettert werden",
"ja": "ここでは登ることができる",
"nb_NO": "Klatring er mulig her",
"nl": "Klimmen is hier niet toegelaten",
"fr": "Escalader est possible",
"it": "È possibile arrampicarsi qua"
}
},
{
"if": "climbing=no",
"then": {
"en": "Climbing is not possible here",
"de": "Hier kann nicht geklettert werden",
"ja": "ここでは登ることができない",
"nb_NO": "Klatring er ikke mulig her",
"nl": "Klimmen is hier niet toegelaten",
"fr": "Escalader nest pas possible",
"it": "Non è possibile arrampicarsi qua"
}
},
{
"if": {
"and": [
"sport!~climbing"
]
},
"then": {
"en": "Climbing is not possible here",
"de": "Hier kann nicht geklettert werden",
"ja": "ここでは登ることができない",
"nb_NO": "Klatring er ikke mulig her",
"nl": "Klimmen is hier niet mogelijk",
"fr": "Escalader nest pas possible",
"it": "Non è possibile arrampicarsi qua"
},
"hideInAnswer": true
}
]
}
],
"mapRendering": [
{
"icon": "./assets/themes/climbing/climbing_unknown.svg",
"location": [
"point",
"centroid"
]
},
{
"color": {
"render": "#ddff55AA"
},
"width": {
"render": "2"
}
}
]
}

View file

@ -0,0 +1,246 @@
{
"id": "climbing_route",
"name": {
"en": "Climbing routes",
"de": "Kletterrouten",
"nl": "Klimroute",
"ja": "登坂ルート",
"nb_NO": "Klatreruter",
"fr": "Voies descalade",
"it": "Vie di arrampicata"
},
"minzoom": 18,
"source": {
"osmTags": {
"and": [
"climbing=route"
]
}
},
"title": {
"render": {
"de": "Kleterroute",
"en": "Climbing route",
"nl": "Klimroute",
"ja": "登坂ルート",
"nb_NO": "Klatrerute",
"it": "Via di arrampicata",
"fr": "Voie descalade"
},
"mappings": [
{
"if": "name~*",
"then": {
"de": "Kleterroute <strong>{name}</strong>",
"en": "Climbing route <strong>{name}</strong>",
"nl": "Klimroute <strong>{name}</strong>",
"ja": "登坂ルート<strong>{name}</strong>",
"it": "Via di arrampicata <strong>{name}</strong>",
"fr": "Voie descalade <strong>{name}</strong>"
}
}
]
},
"tagRenderings": [
"images",
{
"render": {
"en": "<strong>{name}</strong>",
"nl": "<strong>{name}</strong>",
"de": "<strong>{name}</strong>",
"ca": "<strong>{name}</strong>",
"fr": "<strong>{name}</strong>",
"id": "<strong>{name}</strong>",
"ru": "<strong>{name}</strong>",
"ja": "<strong>{name}</strong>",
"it": "<strong>{name}</strong>",
"nb_NO": "<strong>{name}</strong>"
},
"question": {
"en": "What is the name of this climbing route?",
"de": "Wie heißt diese Kletterroute?",
"nl": "Hoe heet deze klimroute?",
"ja": "この登坂ルートの名前は何ですか?",
"it": "Come si chiama questa via di arrampicata?",
"fr": "Quel est le nom de cette voie descalade ?",
"nb_NO": "Hva er navnet på denne klatreruten?"
},
"freeform": {
"key": "name"
},
"mappings": [
{
"if": {
"and": [
"noname=yes",
"name="
]
},
"then": {
"en": "This climbing route doesn't have a name",
"de": "Diese Kletterroute hat keinen Namen",
"nl": "Deze klimroute heeft geen naam",
"ja": "この登坂ルートには名前がありません",
"it": "Questa via di arrampicata non ha un nome",
"fr": "Cette voie na pas de nom",
"nb_NO": "Denne klatreruten har ikke noe navn"
}
}
],
"id": "Name"
},
{
"question": {
"en": "How long is this climbing route (in meters)?",
"nl": "Hoe lang is deze klimroute (in meters)?",
"it": "Quanto è lunga questa via di arrampicata (in metri)?",
"fr": "Quelle est la longueur de cette voie (en mètres) ?",
"de": "Wie lang ist diese Kletterroute (in Metern)?",
"nb_NO": "Hvor mange meter er klatreruten?"
},
"render": {
"de": "Diese Route ist {canonical(climbing:length)} lang",
"en": "This route is {canonical(climbing:length)} long",
"nl": "Deze klimroute is {canonical(climbing:length)} lang",
"ja": "このルート長は、 {canonical(climbing:length)} メーターです",
"nb_NO": "Denne ruten er {canonical(climbing:length)} lang",
"it": "Questo percorso è lungo {canonical(climbing:length)}",
"fr": "Cette voie fait {canonical(climbing:length)} de long"
},
"freeform": {
"key": "climbing:length",
"type": "pnat"
},
"id": "Length"
},
{
"question": {
"en": "What is the grade of this climbing route according to the french/belgian system?",
"nl": "Hoe moeilijk is deze klimroute volgens het Franse/Belgische systeem?",
"it": "Qual è la difficoltà di questa via di arrampicata nel sistema francese/belga?",
"fr": "Quelle est la difficulté de cette voie selon le système franco-belge ?",
"de": "Wie hoch ist der Schwierigkeitsgrad dieser Kletterroute nach dem französisch/belgischen System?"
},
"render": {
"de": "Die Schwierigkeit ist {climbing:grade:french} entsprechend des französisch/belgischen Systems",
"en": "The grade is {climbing:grade:french} according to the french/belgian system",
"nl": "De klimmoeilijkheid is {climbing:grade:french} volgens het Franse/Belgische systeem",
"ja": "フランス/ベルギーのランク評価システムによると、{climbing:grade:french}の困難度です",
"it": "Il grado di difficoltà è {climbing:grade:french} nel sistema francese/belga",
"fr": "Selon le système franco-belge, la difficulté de cette voie est de {climbing:grade:french}"
},
"freeform": {
"key": "climbing:grade:french"
},
"id": "Difficulty"
},
{
"question": {
"en": "How many bolts does this route have before reaching the anchor?",
"fr": "Combien de prises cette voie possède avant datteindre la moulinette ?",
"de": "Wie viele Haken gibt es auf dieser Kletterroute bevor der Umlenker bzw. Standhaken erreicht ist?",
"it": "Quanti bulloni sono presenti in questo percorso prima di arrivare alla moulinette?"
},
"render": {
"en": "This route has {climbing:bolts} bolts",
"fr": "Cette voie a {climbing:bolts} prises",
"de": "Diese Kletterroute hat {climbing:bolts} Haken",
"it": "Questo percorso ha {climbing:bolts} bulloni"
},
"freeform": {
"key": "climbing:bolts",
"type": "pnat",
"addExtraTag": [
"climbing:bolted=yes"
]
},
"mappings": [
{
"if": "climbing:bolted=no",
"then": {
"en": "This route is not bolted",
"fr": "Cette voie na pas de prises",
"de": "Auf dieser Kletterroute sind keine Haken vorhanden",
"it": "In questo percorso non sono presenti bulloni"
},
"hideInAnswer": true
},
{
"if": "climbing:bolted=no&climbing:bolts=",
"then": {
"en": "This route is not bolted",
"fr": "Cette voie na pas de prises",
"de": "Auf dieser Kletterroute sind keine Haken vorhanden",
"it": "In questo percorso non sono presenti bulloni"
}
}
],
"id": "Bolts"
},
{
"question": "Is there other relevant info?",
"render": "<h3>Description</h3><br/>{description}",
"freeform": {
"key": "description"
},
"id": "Description"
},
{
"render": {
"en": "The rock type is {_embedding_features_with_rock:rock} as stated <a href='#{_embedding_features_with_rock:id}'>on the surrounding crag</a>",
"fr": "Le type de roche est {_embedding_features_with_rock:rock} selon <a href='#{_embedding_features_with_rock:id}'>le mur</a>",
"it": "Il tipo di roccia è {_embedding_features_with_rock:rock} come dichiarato sul <a href='#{_embedding_features_with_rock:id}'>muro circostante</a>",
"de": "Der Gesteinstyp ist {_embedding_features_with_rock:rock}, wie <a href='#{_embedding_features_with_rock:id}'>auf dem umgebenden Felsen angegeben</a>"
},
"freeform": {
"key": "_embedding_features_with_rock:rock"
},
"id": "Rock type"
}
],
"presets": [
{
"title": {
"en": "a climbing route",
"nl": "een klimroute",
"fr": "une voie descalade",
"de": "eine kletterroute",
"it": "una via di arrampicata"
},
"tags": [
"sport=climbing",
"climbing=route"
]
}
],
"mapRendering": [
{
"icon": {
"render": "circle:white;./assets/themes/climbing/climbing_route.svg"
},
"iconSize": {
"render": "28,28,center"
},
"location": [
"point",
"centroid"
],
"label": {
"mappings": [
{
"if": "name~*",
"then": "<div style='background: white; padding: 0.25em; border-radius:0.5em'>{name}</div>"
}
]
}
},
{
"color": {
"render": "#0f0"
},
"width": {
"render": "4"
}
}
]
}

View file

@ -4,7 +4,8 @@
"en": "Crossings",
"nl": "Oversteekplaatsen",
"de": "Kreuzungen",
"fr": "Traversée"
"fr": "Traversée",
"ca": "Encreuaments"
},
"description": {
"en": "Crossings for pedestrians and cyclists",
@ -26,7 +27,8 @@
"en": "Crossing",
"nl": "Oversteekplaats",
"de": "Kreuzung",
"fr": "Traversée"
"fr": "Traversée",
"ca": "Encreuament"
},
"mappings": [
{
@ -36,7 +38,8 @@
"nl": "Verkeerslicht",
"ru": "Светофор",
"de": "Ampel",
"fr": "Feu de signalisation"
"fr": "Feu de signalisation",
"ca": "Semàfor"
}
},
{
@ -56,7 +59,8 @@
"en": "a crossing",
"nl": "een oversteekplaats",
"de": "eine kreuzung",
"fr": "une traversée"
"fr": "une traversée",
"ca": "un pas de vianants"
},
"tags": [
"highway=crossing"
@ -132,7 +136,8 @@
"then": {
"en": "Zebra crossing",
"nl": "Zebrapad",
"de": "Zebrastreifen"
"de": "Zebrastreifen",
"ca": "Pas de zebra"
},
"hideInAnswer": true
},

View file

@ -35,7 +35,8 @@
"nl": "Fietspaden",
"de": "Radwege",
"ru": "Велосипедные дорожки",
"fr": "Pistes cyclables"
"fr": "Pistes cyclables",
"ca": "Vies ciclistes"
},
"mappings": [
{
@ -50,7 +51,8 @@
"en": "Cycleway",
"de": "Radweg",
"ru": "Велосипедная дорожка",
"fr": "Piste cyclable"
"fr": "Piste cyclable",
"ca": "Via ciclista"
}
},
{
@ -59,7 +61,8 @@
"nl": "Fietssuggestiestrook",
"en": "Shared lane",
"de": "Gemeinsame Fahrspur",
"fr": "Voie partagée"
"fr": "Voie partagée",
"ca": "Carril compartit"
}
},
{
@ -68,7 +71,8 @@
"nl": "Fietsstrook",
"en": "Bike lane",
"de": "Fahrradspur",
"fr": "Bande cyclable"
"fr": "Bande cyclable",
"ca": "Carril bici"
}
},
{
@ -86,7 +90,8 @@
"nl": "Fietsstraat",
"en": "Cyclestreet",
"de": "Fahrradstraße",
"fr": "Vélorue"
"fr": "Vélorue",
"ca": "Carrer ciclista"
}
}
]
@ -817,7 +822,8 @@
"en": "Compulsory cycleway",
"nl": "Verplicht fietspad",
"de": "Vorgeschriebener Radweg",
"id": "Jalur sepeda wajib"
"id": "Jalur sepeda wajib",
"ca": "Via ciclista obligatòria"
},
"hideInAnswer": "_country!=be",
"icon": {
@ -845,7 +851,8 @@
"en": "Segregated foot/cycleway",
"nl": "Afgescheiden voet-/fietspad",
"de": "Getrennter Fuß-/Radweg",
"id": "Jalur pejalan kaki/sepeda terpisah"
"id": "Jalur pejalan kaki/sepeda terpisah",
"ca": "Via segregada a peu/ciclista"
},
"hideInAnswer": "_country!=be",
"addExtraTags": [
@ -863,7 +870,8 @@
"en": "Unsegregated foot/cycleway",
"nl": "Gedeeld voet-/fietspad",
"de": "Gemeinsamer Fuß-/Radweg",
"id": "Jalur pejalan kaki/sepeda tidak terpisah"
"id": "Jalur pejalan kaki/sepeda tidak terpisah",
"ca": "Via no segregada a peu/ciclista"
},
"hideInAnswer": "_country!=be",
"addExtraTags": [
@ -916,7 +924,8 @@
"en": "Compulsory cycleway",
"nl": "Verplicht fietspad",
"de": "Vorgeschriebener Radweg",
"id": "Jalur sepeda wajib"
"id": "Jalur sepeda wajib",
"ca": "Via ciclista obligatòria"
},
"hideInAnswer": "_country!=be",
"addExtraTags": [
@ -948,7 +957,8 @@
"then": {
"en": "Segregated foot/cycleway",
"nl": "Afgescheiden voet-/fietspad",
"de": "Getrennter Fuß-/Radweg"
"de": "Getrennter Fuß-/Radweg",
"ca": "Via segregada a peu/ciclista"
},
"hideInAnswer": "_country!=be",
"addExtraTags": [
@ -969,7 +979,8 @@
"then": {
"en": "Unsegregated foot/cycleway",
"nl": "Gedeeld voet-/fietspad",
"de": "Gemeinsamer Fuß-/Radweg"
"de": "Gemeinsamer Fuß-/Radweg",
"ca": "Via no segregada a peu/ciclista"
},
"hideInAnswer": "_country!=be",
"addExtraTags": [
@ -989,7 +1000,8 @@
"if": "traffic_sign=NL:G11",
"then": {
"en": "Compulsory cycleway",
"nl": "Verplicht fietspad"
"nl": "Verplicht fietspad",
"ca": "Via ciclista obligatòria"
},
"hideInAnswer": "_country!=nl",
"addExtraTags": [

View file

@ -477,7 +477,8 @@
"it": "{opening_hours_table(opening_hours)}",
"ru": "{opening_hours_table(opening_hours)}",
"de": "{opening_hours_table(opening_hours)}",
"sl": "{opening_hours_table(opening_hours)}"
"sl": "{opening_hours_table(opening_hours)}",
"ca": "{opening_hours_table(opening_hours)}"
},
"question": {
"en": "At what times is this defibrillator available?",
@ -563,7 +564,8 @@
"it": "Verificato oggi!",
"ru": "Проверено сегодня!",
"de": "Heute überprüft!",
"sl": "Preverjeno danes!"
"sl": "Preverjeno danes!",
"ca": "Comprovat avui!"
}
}
],

View file

@ -6,7 +6,8 @@
"fr": "Visualisation de la direction",
"it": "Visualizzazione della direzione",
"ru": "Визуализация направления",
"de": "Aufnahmewinkel der Kamera anzeigen"
"de": "Aufnahmewinkel der Kamera anzeigen",
"ca": "Direcció de la visualització"
},
"minzoom": 16,
"source": {

Some files were not shown because too many files have changed in this diff Show more