Merge develop

This commit is contained in:
pietervdvn 2021-10-13 17:23:51 +02:00
commit 448468c928
97 changed files with 5039 additions and 1139 deletions

View file

@ -13,6 +13,7 @@ export default class AllKnownLayers {
const sharedLayers = new Map<string, LayerConfig>();
for (const layer of known_layers.layers) {
try {
// @ts-ignore
const parsed = new LayerConfig(layer, "shared_layers")
sharedLayers.set(layer.id, parsed);
sharedLayers[layer.id] = parsed;

View file

@ -63,6 +63,7 @@ export class AllKnownLayouts {
private static AllLayouts(): Map<string, LayoutConfig> {
const dict: Map<string, LayoutConfig> = new Map();
for (const layoutConfigJson of known_themes.themes) {
// @ts-ignore
const layout = new LayoutConfig(layoutConfigJson, true)
if (layout.id === "cyclofix") {

View file

@ -28,6 +28,15 @@ The latitude and longitude of the point (or centerpoint in the case of a way/are
### _layer
The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.
### _surface, _surface:ha
@ -173,7 +182,7 @@ For example to get all objects which overlap or embed from a layer, use `_contai
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)
0. list of features
0. list of features or a layer name or '*' to get all features
### closestn
@ -181,7 +190,7 @@ For example to get all objects which overlap or embed from a layer, use `_contai
If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)
0. list of features or layer name
0. list of features or layer name or '*' to get all features
1. amount of features
2. unique tag key (optional)
3. maxDistanceInMeters (optional)

View file

@ -24,7 +24,7 @@ A geographical length in meters (rounded at two points). Will give an extra mini
## wikidata
A wikidata identifier, e.g. Q42
A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]
## int

View file

@ -14,7 +14,7 @@
name | default | description
------ | --------- | -------------
image key/prefix | image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc...
image key/prefix (multiple values allowed if comma-seperated) | image | The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc...
#### Example usage
@ -26,10 +26,11 @@ image key/prefix | image | The keys given to the images, e.g. if <span class='li
name | default | description
------ | --------- | -------------
image-key | image | Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)
label | Add image | The text to show on the button
#### Example usage
`{image_upload(image)}`
`{image_upload(image,Add image)}`
### wikipedia
A box showing the corresponding wikipedia article - based on the wikidata tag
@ -154,4 +155,19 @@ minzoom | 18 | How far the contributor must zoom in before being able to import
#### Example usage
`{import_button(,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18)}` Generated from UI/SpecialVisualisations.ts
`{import_button(,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,18)}`
### multi_apply
A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags
name | default | description
------ | --------- | -------------
feature_ids | undefined | A JSOn-serialized list of IDs of features to apply the tagging on
keys | undefined | One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.
text | undefined | The text to show on the button
autoapply | undefined | A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown
overwrite | undefined | If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change
#### Example usage
{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)} Generated from UI/SpecialVisualisations.ts

View file

@ -140,25 +140,6 @@
"description": "Layer 'Benches' shows survey:date= with a fixed text, namely 'Surveyed today!' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Benches') Picking this answer will delete the key survey:date.",
"value": ""
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Benches' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Benches')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Benches' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Benches')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Benches' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Benches')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Benches' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Benches')",
"value": "yes"
},
{
"key": "bench",
"description": "The MapComplete theme Benches has a layer Benches at public transport stops showing features with this tag",

View file

@ -48,25 +48,6 @@
{
"key": "direction",
"description": "Layer 'Binoculars' shows and asks freeform values for key 'direction' (in the MapComplete.osm.be theme 'Binoculars')"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Binoculars' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Binoculars')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Binoculars' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Binoculars')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Binoculars' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Binoculars')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Binoculars' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Binoculars')",
"value": "yes"
}
]
}

View file

@ -120,6 +120,26 @@
"key": "wheelchair",
"description": "Layer 'Cafés and pubs' shows wheelchair=no with a fixed text, namely 'This place is not reachable with a wheelchair' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "no"
},
{
"key": "dog",
"description": "Layer 'Cafés and pubs' shows dog=yes with a fixed text, namely 'Dogs are allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "yes"
},
{
"key": "dog",
"description": "Layer 'Cafés and pubs' shows dog=no with a fixed text, namely 'Dogs are <b>not</b> allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "no"
},
{
"key": "dog",
"description": "Layer 'Cafés and pubs' shows dog=leashed with a fixed text, namely 'Dogs are allowed, but they have to be leashed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "leashed"
},
{
"key": "dog",
"description": "Layer 'Cafés and pubs' shows dog=unleashed with a fixed text, namely 'Dogs are allowed and can run around freely' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Cafés and pubs')",
"value": "unleashed"
}
]
}

View file

@ -106,84 +106,147 @@
},
{
"key": "socket:schuko",
"description": "Layer 'Charging stations' shows socket:schuko=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/CEE7_4F.svg'/> <b>Schuko wall plug</b> without ground pin (CEE7/4 type F)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:schuko=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/CEE7_4F.svg'/> <span><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:schuko",
"description": "Layer 'Charging stations' shows socket:schuko~^..*$&socket:schuko!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/CEE7_4F.svg'/> <b>Schuko wall plug</b> without ground pin (CEE7/4 type F)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:schuko~^..*$&socket:schuko!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/CEE7_4F.svg'/> <span><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:typee",
"description": "Layer 'Charging stations' shows socket:typee=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/TypeE.svg'/> <b>European wall plug</b> with ground pin (CEE7/4 type E)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:typee=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/TypeE.svg'/> <span><b>European wall plug</b> with ground pin (CEE7/4 type E)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:typee",
"description": "Layer 'Charging stations' shows socket:typee~^..*$&socket:typee!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/TypeE.svg'/> <b>European wall plug</b> with ground pin (CEE7/4 type E)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:typee~^..*$&socket:typee!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/TypeE.svg'/> <span><b>European wall plug</b> with ground pin (CEE7/4 type E)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:chademo",
"description": "Layer 'Charging stations' shows socket:chademo=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Chademo_type4.svg'/> <b>Chademo</b>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:chademo=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Chademo_type4.svg'/> <span><b>Chademo</b></span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:chademo",
"description": "Layer 'Charging stations' shows socket:chademo~^..*$&socket:chademo!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Chademo_type4.svg'/> <b>Chademo</b>' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:chademo~^..*$&socket:chademo!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Chademo_type4.svg'/> <span><b>Chademo</b></span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1_cable",
"description": "Layer 'Charging stations' shows socket:type1_cable=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <b>Type 1 with cable</b> (J1772)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:type1_cable=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <span><b>Type 1 with cable</b> (J1772)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type1_cable",
"description": "Layer 'Charging stations' shows socket:type1_cable~^..*$&socket:type1_cable!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <b>Type 1 with cable</b> (J1772)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:type1_cable~^..*$&socket:type1_cable!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <span><b>Type 1 with cable</b> (J1772)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1",
"description": "Layer 'Charging stations' shows socket:type1=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <b>Type 1 <i>without</i> cable</b> (J1772)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:type1=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <span><b>Type 1 <i>without</i> cable</b> (J1772)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type1",
"description": "Layer 'Charging stations' shows socket:type1~^..*$&socket:type1!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <b>Type 1 <i>without</i> cable</b> (J1772)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:type1~^..*$&socket:type1!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1_J1772.svg'/> <span><b>Type 1 <i>without</i> cable</b> (J1772)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type1_combo",
"description": "Layer 'Charging stations' shows socket:type1_combo=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1-ccs.svg'/> <b>Type 1 CCS</b> (aka Type 1 Combo)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:type1_combo=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1-ccs.svg'/> <span><b>Type 1 CCS</b> (aka Type 1 Combo)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type1_combo",
"description": "Layer 'Charging stations' shows socket:type1_combo~^..*$&socket:type1_combo!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type1-ccs.svg'/> <b>Type 1 CCS</b> (aka Type 1 Combo)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:type1_combo~^..*$&socket:type1_combo!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type1-ccs.svg'/> <span><b>Type 1 CCS</b> (aka Type 1 Combo)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_supercharger",
"description": "Layer 'Charging stations' shows socket:tesla_supercharger=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <b>Tesla Supercharger</b>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:tesla_supercharger=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <span><b>Tesla Supercharger</b></span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:tesla_supercharger",
"description": "Layer 'Charging stations' shows socket:tesla_supercharger~^..*$&socket:tesla_supercharger!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <b>Tesla Supercharger</b>' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:tesla_supercharger~^..*$&socket:tesla_supercharger!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <span><b>Tesla Supercharger</b></span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2",
"description": "Layer 'Charging stations' shows socket:type2=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_socket.svg'/> <b>Type 2</b> (mennekes)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:type2=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_socket.svg'/> <span><b>Type 2</b> (mennekes)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type2",
"description": "Layer 'Charging stations' shows socket:type2~^..*$&socket:type2!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_socket.svg'/> <b>Type 2</b> (mennekes)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:type2~^..*$&socket:type2!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_socket.svg'/> <span><b>Type 2</b> (mennekes)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2_combo",
"description": "Layer 'Charging stations' shows socket:type2_combo=1 with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <b>Type 2 CCS</b> (mennekes)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:type2_combo=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <span><b>Type 2 CCS</b> (mennekes)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type2_combo",
"description": "Layer 'Charging stations' shows socket:type2_combo~^..*$&socket:type2_combo!~^1$ with a fixed text, namely '<img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <b>Type 2 CCS</b> (mennekes)' (in the MapComplete.osm.be theme 'Charging stations')"
"description": "Layer 'Charging stations' shows socket:type2_combo~^..*$&socket:type2_combo!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <span><b>Type 2 CCS</b> (mennekes)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2_cable",
"description": "Layer 'Charging stations' shows socket:type2_cable=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_tethered.svg'/> <span><b>Type 2 with cable</b> (mennekes)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:type2_cable",
"description": "Layer 'Charging stations' shows socket:type2_cable~^..*$&socket:type2_cable!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_tethered.svg'/> <span><b>Type 2 with cable</b> (mennekes)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_supercharger_ccs",
"description": "Layer 'Charging stations' shows socket:tesla_supercharger_ccs=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <span><b>Tesla Supercharger CCS</b> (a branded type2_css)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:tesla_supercharger_ccs",
"description": "Layer 'Charging stations' shows socket:tesla_supercharger_ccs~^..*$&socket:tesla_supercharger_ccs!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_CCS.svg'/> <span><b>Tesla Supercharger CCS</b> (a branded type2_css)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows socket:tesla_destination=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <span><b>Tesla Supercharger (destination)</b></span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows socket:tesla_destination~^..*$&socket:tesla_destination!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> <span><b>Tesla Supercharger (destination)</b></span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows socket:tesla_destination=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_tethered.svg'/> <span><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows socket:tesla_destination~^..*$&socket:tesla_destination!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/Type2_tethered.svg'/> <span><b>Tesla supercharger (destination</b> (A Type 2 with cable branded as tesla)</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:USB-A",
"description": "Layer 'Charging stations' shows socket:USB-A=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/usb_port.svg'/> <span><b>USB</b> to charge phones and small electronics</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:USB-A",
"description": "Layer 'Charging stations' shows socket:USB-A~^..*$&socket:USB-A!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/usb_port.svg'/> <span><b>USB</b> to charge phones and small electronics</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin",
"description": "Layer 'Charging stations' shows socket:bosch_3pin=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/bosch-3pin.svg'/> <span><b>Bosch Active Connect with 3 pins</b> and cable</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:bosch_3pin",
"description": "Layer 'Charging stations' shows socket:bosch_3pin~^..*$&socket:bosch_3pin!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/bosch-3pin.svg'/> <span><b>Bosch Active Connect with 3 pins</b> and cable</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin",
"description": "Layer 'Charging stations' shows socket:bosch_5pin=1 with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/bosch-5pin.svg'/> <span><b>Bosch Active Connect with 5 pins</b> and cable</span></div>' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1"
},
{
"key": "socket:bosch_5pin",
"description": "Layer 'Charging stations' shows socket:bosch_5pin~^..*$&socket:bosch_5pin!~^1$ with a fixed text, namely '<div class='flex'><img class='w-12 mx-4' src='./assets/layers/charging_station/bosch-5pin.svg'/> <span><b>Bosch Active Connect with 5 pins</b> and cable</span></div>' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:schuko",
@ -195,7 +258,7 @@
},
{
"key": "socket:socket:schuko:voltage",
"description": "Layer 'Charging stations' shows socket:socket:schuko:voltage=230 V with a fixed text, namely '<b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem;' src='./assets/layers/charging_station/CEE7_4F.svg'/> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:schuko:voltage=230 V with a fixed text, namely '<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> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
@ -204,7 +267,7 @@
},
{
"key": "socket:socket:schuko:current",
"description": "Layer 'Charging stations' shows socket:socket:schuko:current=16 A with a fixed text, namely '<b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem;' src='./assets/layers/charging_station/CEE7_4F.svg'/> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:schuko:current=16 A with a fixed text, namely '<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> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
@ -213,7 +276,7 @@
},
{
"key": "socket:socket:schuko:output",
"description": "Layer 'Charging stations' shows socket:socket:schuko:output=3.6 kw with a fixed text, namely '<b><b>Schuko wall plug</b> without ground pin (CEE7/4 type F)</b> <img style='width:1rem;' src='./assets/layers/charging_station/CEE7_4F.svg'/> outputs at most 3.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:schuko:output=3.6 kw with a fixed text, namely '<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> outputs at most 3.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.6 kw"
},
{
@ -226,7 +289,7 @@
},
{
"key": "socket:socket:typee:voltage",
"description": "Layer 'Charging stations' shows socket:socket:typee:voltage=230 V with a fixed text, namely '<b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem;' src='./assets/layers/charging_station/TypeE.svg'/> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:typee:voltage=230 V with a fixed text, namely '<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> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
@ -235,7 +298,7 @@
},
{
"key": "socket:socket:typee:current",
"description": "Layer 'Charging stations' shows socket:socket:typee:current=16 A with a fixed text, namely '<b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem;' src='./assets/layers/charging_station/TypeE.svg'/> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:typee:current=16 A with a fixed text, namely '<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> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
@ -244,12 +307,12 @@
},
{
"key": "socket:socket:typee:output",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=3 kw with a fixed text, namely '<b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem;' src='./assets/layers/charging_station/TypeE.svg'/> outputs at most 3 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=3 kw with a fixed text, namely '<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> outputs at most 3 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3 kw"
},
{
"key": "socket:socket:typee:output",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=22 kw with a fixed text, namely '<b><b>European wall plug</b> with ground pin (CEE7/4 type E)</b> <img style='width:1rem;' src='./assets/layers/charging_station/TypeE.svg'/> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:typee:output=22 kw with a fixed text, namely '<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> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
@ -262,7 +325,7 @@
},
{
"key": "socket:socket:chademo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:chademo:voltage=500 V with a fixed text, namely '<b><b>Chademo</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Chademo_type4.svg'/> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:chademo:voltage=500 V with a fixed text, namely '<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> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
@ -271,7 +334,7 @@
},
{
"key": "socket:socket:chademo:current",
"description": "Layer 'Charging stations' shows socket:socket:chademo:current=120 A with a fixed text, namely '<b><b>Chademo</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Chademo_type4.svg'/> outputs at most 120 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:chademo:current=120 A with a fixed text, namely '<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> outputs at most 120 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 A"
},
{
@ -280,7 +343,7 @@
},
{
"key": "socket:socket:chademo:output",
"description": "Layer 'Charging stations' shows socket:socket:chademo:output=50 kw with a fixed text, namely '<b><b>Chademo</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Chademo_type4.svg'/> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:chademo:output=50 kw with a fixed text, namely '<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> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
@ -293,12 +356,12 @@
},
{
"key": "socket:socket:type1_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=200 V with a fixed text, namely '<b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=200 V with a fixed text, namely '<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> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "200 V"
},
{
"key": "socket:socket:type1_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=240 V with a fixed text, namely '<b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:voltage=240 V with a fixed text, namely '<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> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "240 V"
},
{
@ -307,7 +370,7 @@
},
{
"key": "socket:socket:type1_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:current=32 A with a fixed text, namely '<b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:current=32 A with a fixed text, namely '<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> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
@ -316,12 +379,12 @@
},
{
"key": "socket:socket:type1_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=3.7 kw with a fixed text, namely '<b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=3.7 kw with a fixed text, namely '<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> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.7 kw"
},
{
"key": "socket:socket:type1_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=7 kw with a fixed text, namely '<b><b>Type 1 with cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_cable:output=7 kw with a fixed text, namely '<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> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7 kw"
},
{
@ -334,12 +397,12 @@
},
{
"key": "socket:socket:type1:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=200 V with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=200 V with a fixed text, namely '<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> outputs 200 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "200 V"
},
{
"key": "socket:socket:type1:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=240 V with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:voltage=240 V with a fixed text, namely '<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> outputs 240 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "240 V"
},
{
@ -348,7 +411,7 @@
},
{
"key": "socket:socket:type1:current",
"description": "Layer 'Charging stations' shows socket:socket:type1:current=32 A with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:current=32 A with a fixed text, namely '<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> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
@ -357,22 +420,22 @@
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=3.7 kw with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=3.7 kw with a fixed text, namely '<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> outputs at most 3.7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "3.7 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=6.6 kw with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 6.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=6.6 kw with a fixed text, namely '<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> outputs at most 6.6 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "6.6 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7 kw with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7 kw with a fixed text, namely '<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> outputs at most 7 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7 kw"
},
{
"key": "socket:socket:type1:output",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7.2 kw with a fixed text, namely '<b><b>Type 1 <i>without</i> cable</b> (J1772)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1_J1772.svg'/> outputs at most 7.2 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1:output=7.2 kw with a fixed text, namely '<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> outputs at most 7.2 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "7.2 kw"
},
{
@ -385,12 +448,12 @@
},
{
"key": "socket:socket:type1_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=400 V with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=400 V with a fixed text, namely '<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> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:socket:type1_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=1000 V with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs 1000 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:voltage=1000 V with a fixed text, namely '<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> outputs 1000 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1000 V"
},
{
@ -399,12 +462,12 @@
},
{
"key": "socket:socket:type1_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=50 A with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 50 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=50 A with a fixed text, namely '<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> outputs at most 50 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 A"
},
{
"key": "socket:socket:type1_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=125 A with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:current=125 A with a fixed text, namely '<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> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
@ -413,22 +476,22 @@
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=50 kw with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=50 kw with a fixed text, namely '<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> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=62.5 kw with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 62.5 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=62.5 kw with a fixed text, namely '<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> outputs at most 62.5 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "62.5 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=150 kw with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=150 kw with a fixed text, namely '<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> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:type1_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=350 kw with a fixed text, namely '<b><b>Type 1 CCS</b> (aka Type 1 Combo)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type1-ccs.svg'/> outputs at most 350 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type1_combo:output=350 kw with a fixed text, namely '<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> outputs at most 350 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 kw"
},
{
@ -441,7 +504,7 @@
},
{
"key": "socket:socket:tesla_supercharger:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:voltage=480 V with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs 480 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:voltage=480 V with a fixed text, namely '<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> outputs 480 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "480 V"
},
{
@ -450,12 +513,12 @@
},
{
"key": "socket:socket:tesla_supercharger:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=125 A with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=125 A with a fixed text, namely '<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> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_supercharger:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=350 A with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:current=350 A with a fixed text, namely '<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> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
@ -464,17 +527,17 @@
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=120 kw with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most 120 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=120 kw with a fixed text, namely '<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> outputs at most 120 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 kw"
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=150 kw with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=150 kw with a fixed text, namely '<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> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:tesla_supercharger:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=250 kw with a fixed text, namely '<b><b>Tesla Supercharger</b></b> <img style='width:1rem;' src='./assets/layers/charging_station/Tesla-hpwc-model-s.svg'/> outputs at most 250 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger:output=250 kw with a fixed text, namely '<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> outputs at most 250 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "250 kw"
},
{
@ -487,12 +550,12 @@
},
{
"key": "socket:socket:type2:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=230 V with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=230 V with a fixed text, namely '<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> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:type2:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=400 V with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:voltage=400 V with a fixed text, namely '<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> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
@ -501,12 +564,12 @@
},
{
"key": "socket:socket:type2:current",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=16 A with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=16 A with a fixed text, namely '<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> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:type2:current",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=32 A with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:current=32 A with a fixed text, namely '<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> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
@ -515,12 +578,12 @@
},
{
"key": "socket:socket:type2:output",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=11 kw with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=11 kw with a fixed text, namely '<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> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:type2:output",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=22 kw with a fixed text, namely '<b><b>Type 2</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_socket.svg'/> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2:output=22 kw with a fixed text, namely '<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> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
@ -533,12 +596,12 @@
},
{
"key": "socket:socket:type2_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=500 V with a fixed text, namely '<b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=500 V with a fixed text, namely '<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> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
"key": "socket:socket:type2_combo:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=920 V with a fixed text, namely '<b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs 920 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:voltage=920 V with a fixed text, namely '<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> outputs 920 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "920 V"
},
{
@ -547,12 +610,12 @@
},
{
"key": "socket:socket:type2_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=125 A with a fixed text, namely '<b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=125 A with a fixed text, namely '<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> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:type2_combo:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=350 A with a fixed text, namely '<b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:current=350 A with a fixed text, namely '<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> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
@ -561,9 +624,261 @@
},
{
"key": "socket:socket:type2_combo:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:output=50 kw with a fixed text, namely '<b><b>Type 2 CCS</b> (mennekes)</b> <img style='width:1rem;' src='./assets/layers/charging_station/Type2_CCS.svg'/> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows socket:socket:type2_combo:output=50 kw with a fixed text, namely '<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> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:type2_cable",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:voltage=230 V with a fixed text, namely '<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> outputs 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:type2_cable:voltage",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:voltage=400 V with a fixed text, namely '<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> outputs 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:type2_cable:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:current=16 A with a fixed text, namely '<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> outputs at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:type2_cable:current",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:current=32 A with a fixed text, namely '<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> outputs at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:type2_cable:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:type2_cable:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:type2_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:output=11 kw with a fixed text, namely '<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> outputs at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:type2_cable:output",
"description": "Layer 'Charging stations' shows socket:socket:type2_cable:output=22 kw with a fixed text, namely '<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> outputs at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:tesla_supercharger_ccs",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:voltage=500 V with a fixed text, namely '<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> outputs 500 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "500 V"
},
{
"key": "socket:socket:tesla_supercharger_ccs:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:voltage=920 V with a fixed text, namely '<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> outputs 920 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "920 V"
},
{
"key": "socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:current=125 A with a fixed text, namely '<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> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_supercharger_ccs:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:current=350 A with a fixed text, namely '<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> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:tesla_supercharger_ccs:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_supercharger_ccs:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_supercharger_ccs:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_supercharger_ccs:output=50 kw with a fixed text, namely '<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> outputs at most 50 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "50 kw"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=480 V with a fixed text, namely '<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> outputs 480 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "480 V"
},
{
"key": "socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=125 A with a fixed text, namely '<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> outputs at most 125 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "125 A"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=350 A with a fixed text, namely '<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> outputs at most 350 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "350 A"
},
{
"key": "socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=120 kw with a fixed text, namely '<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> outputs at most 120 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "120 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=150 kw with a fixed text, namely '<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> outputs at most 150 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "150 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=250 kw with a fixed text, namely '<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> outputs at most 250 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "250 kw"
},
{
"key": "socket:tesla_destination",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=230 V with a fixed text, namely '<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 230 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "230 V"
},
{
"key": "socket:socket:tesla_destination:voltage",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:voltage=400 V with a fixed text, namely '<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 400 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "400 V"
},
{
"key": "socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=16 A with a fixed text, namely '<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 at most 16 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "16 A"
},
{
"key": "socket:socket:tesla_destination:current",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:current=32 A with a fixed text, namely '<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 at most 32 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "32 A"
},
{
"key": "socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:tesla_destination:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=11 kw with a fixed text, namely '<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 at most 11 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "11 kw"
},
{
"key": "socket:socket:tesla_destination:output",
"description": "Layer 'Charging stations' shows socket:socket:tesla_destination:output=22 kw with a fixed text, namely '<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 at most 22 kw' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "22 kw"
},
{
"key": "socket:USB-A",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:USB-A:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:voltage",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:voltage=5 V with a fixed text, namely '<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 5 volt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "5 V"
},
{
"key": "socket:USB-A:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:current",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:current=1 A with a fixed text, namely '<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 at most 1 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "1 A"
},
{
"key": "socket:socket:USB-A:current",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:current=2 A with a fixed text, namely '<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 at most 2 A' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "2 A"
},
{
"key": "socket:USB-A:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:USB-A:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:socket:USB-A:output",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:output=5w with a fixed text, namely '<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 at most 5w' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "5w"
},
{
"key": "socket:socket:USB-A:output",
"description": "Layer 'Charging stations' shows socket:socket:USB-A:output=10w with a fixed text, namely '<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 at most 10w' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "10w"
},
{
"key": "socket:bosch_3pin",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_3pin:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_3pin:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:voltage",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:voltage' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:current",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:current' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "socket:bosch_5pin:output",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'socket:bosch_5pin:output' (in the MapComplete.osm.be theme 'Charging stations')"
},
{
"key": "authentication:membership_card",
"description": "Layer 'Charging stations' shows authentication:membership_card=yes with a fixed text, namely 'Authentication by a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
@ -646,6 +961,11 @@
"description": "Layer 'Charging stations' shows payment:app=yes with a fixed text, namely 'Payment is done using a dedicated app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "payment:membership_card",
"description": "Layer 'Charging stations' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
},
{
"key": "maxstay",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'maxstay' (in the MapComplete.osm.be theme 'Charging stations')"
@ -770,27 +1090,23 @@
},
{
"key": "amenity",
"description": "Layer 'Charging stations' shows amenity=charging_station with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"description": "Layer 'Charging stations' shows amenity=charging_station&operational_status= with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "charging_station"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Charging stations')"
"key": "operational_status",
"description": "Layer 'Charging stations' shows amenity=charging_station&operational_status= with a fixed text, namely 'This charging station works' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations') Picking this answer will delete the key operational_status.",
"value": ""
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Charging stations' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no&service:bicycle:cleaning:charge="
"key": "parking:fee",
"description": "Layer 'Charging stations' shows parking:fee=no with a fixed text, namely 'No additional parking cost while charging' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Charging stations' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Charging stations')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Charging stations' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"key": "parking:fee",
"description": "Layer 'Charging stations' shows parking:fee=yes with a fixed text, namely 'An additional parking fee should be paid while charging' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Charging stations')",
"value": "yes"
}
]
}
}

View file

@ -170,10 +170,6 @@
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike repair/shop showing features with this tag",
"value": ""
},
{
"key": "shop",
"description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike repair/shop showing features with this tag"
},
{
"key": "image",
"description": "The layer 'Bike repair/shop allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"

View file

@ -304,6 +304,26 @@
"key": "reusable_packaging:accept",
"description": "Layer 'Restaurants and fast food' shows reusable_packaging:accept=only with a fixed text, namely 'You <b>must</b> bring your own container to order here.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "only"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=yes with a fixed text, namely 'Dogs are allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "yes"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=no with a fixed text, namely 'Dogs are <b>not</b> allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "no"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=leashed with a fixed text, namely 'Dogs are allowed, but they have to be leashed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "leashed"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=unleashed with a fixed text, namely 'Dogs are allowed and can run around freely' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Restaurants and fast food')",
"value": "unleashed"
}
]
}

View file

@ -310,6 +310,26 @@
"description": "Layer 'Fries shop' shows reusable_packaging:accept=only with a fixed text, namely 'You <b>must</b> bring your own container to order here.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "only"
},
{
"key": "dog",
"description": "Layer 'Fries shop' shows dog=yes with a fixed text, namely 'Dogs are allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "dog",
"description": "Layer 'Fries shop' shows dog=no with a fixed text, namely 'Dogs are <b>not</b> allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "no"
},
{
"key": "dog",
"description": "Layer 'Fries shop' shows dog=leashed with a fixed text, namely 'Dogs are allowed, but they have to be leashed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "leashed"
},
{
"key": "dog",
"description": "Layer 'Fries shop' shows dog=unleashed with a fixed text, namely 'Dogs are allowed and can run around freely' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "unleashed"
},
{
"key": "amenity",
"description": "The MapComplete theme Friturenkaart has a layer Restaurants and fast food showing features with this tag",
@ -604,6 +624,26 @@
"key": "reusable_packaging:accept",
"description": "Layer 'Restaurants and fast food' shows reusable_packaging:accept=only with a fixed text, namely 'You <b>must</b> bring your own container to order here.' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "only"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=yes with a fixed text, namely 'Dogs are allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "yes"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=no with a fixed text, namely 'Dogs are <b>not</b> allowed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "no"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=leashed with a fixed text, namely 'Dogs are allowed, but they have to be leashed' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "leashed"
},
{
"key": "dog",
"description": "Layer 'Restaurants and fast food' shows dog=unleashed with a fixed text, namely 'Dogs are allowed and can run around freely' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Friturenkaart')",
"value": "unleashed"
}
]
}

View file

@ -77,7 +77,7 @@
},
{
"key": "drink:club-mate",
"description": "Layer 'Hackerspace' shows drink:club-mate=no with a fixed text, namely 'This hackerspace is not worthy of the name hackerspace as it does not serve club mate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Hackerspaces')",
"description": "Layer 'Hackerspace' shows drink:club-mate=no with a fixed text, namely 'This hackerspace does not serve club mate' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Hackerspaces')",
"value": "no"
},
{

View file

@ -428,16 +428,16 @@
},
{
"key": "operator",
"description": "Layer 'Natuurgebied' shows operator=Natuurpunt with a fixed text, namely '<img src=\"./assets/themes/buurtnatuur/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door Natuurpunt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"description": "Layer 'Natuurgebied' shows operator=Natuurpunt with a fixed text, namely '<img src=\"./assets/layers/nature_reserve/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door Natuurpunt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "Natuurpunt"
},
{
"key": "operator",
"description": "Layer 'Natuurgebied' shows operator~^(n|N)atuurpunt.*$ with a fixed text, namely '<img src=\"./assets/themes/buurtnatuur/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door {operator}' (in the MapComplete.osm.be theme 'De Natuur in')"
"description": "Layer 'Natuurgebied' shows operator~^(n|N)atuurpunt.*$ with a fixed text, namely '<img src=\"./assets/layers/nature_reserve/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door {operator}' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "operator",
"description": "Layer 'Natuurgebied' shows operator=Agentschap Natuur en Bos with a fixed text, namely '<img src=\"./assets/themes/buurtnatuur/ANB.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door het Agentschap Natuur en Bos' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"description": "Layer 'Natuurgebied' shows operator=Agentschap Natuur en Bos with a fixed text, namely '<img src=\"./assets/layers/nature_reserve/ANB.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door het Agentschap Natuur en Bos' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'De Natuur in')",
"value": "Agentschap Natuur en Bos"
},
{
@ -497,6 +497,15 @@
"key": "description:0",
"description": "Layer 'Natuurgebied' shows and asks freeform values for key 'description:0' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "wikidata",
"description": "Layer 'Natuurgebied' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'De Natuur in')"
},
{
"key": "wikidata",
"description": "Layer 'Natuurgebied' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'De Natuur in') Picking this answer will delete the key wikidata.",
"value": ""
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Natuurgebied' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'De Natuur in')"

View file

@ -81,6 +81,11 @@
"description": "Layer 'Observation towers' shows payment:app=yes with a fixed text, namely 'Payment is done using a dedicated app' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"value": "yes"
},
{
"key": "payment:membership_card",
"description": "Layer 'Observation towers' shows payment:membership_card=yes with a fixed text, namely 'Payment is done using a membership card' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"value": "yes"
},
{
"key": "wheelchair",
"description": "Layer 'Observation towers' shows wheelchair=designated with a fixed text, namely 'This place is specially adapated for wheelchair users' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
@ -102,23 +107,13 @@
"value": "no"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Observation towers' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Observation towers')"
"key": "wikidata",
"description": "Layer 'Observation towers' shows and asks freeform values for key 'wikidata' (in the MapComplete.osm.be theme 'Observation towers')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Observation towers' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Observation towers' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Observation towers')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Observation towers' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Observation towers')",
"value": "yes"
"key": "wikidata",
"description": "Layer 'Observation towers' shows wikidata= with a fixed text, namely 'No Wikipedia page has been linked yet' (in the MapComplete.osm.be theme 'Observation towers') Picking this answer will delete the key wikidata.",
"value": ""
}
]
}

View file

@ -12,111 +12,34 @@
"tags": [
{
"key": "amenity",
"description": "The MapComplete theme Parking has a layer parking showing features with this tag",
"description": "The MapComplete theme Parking has a layer Parking showing features with this tag",
"value": "parking"
},
{
"key": "amenity",
"description": "The MapComplete theme Parking has a layer parking showing features with this tag",
"description": "The MapComplete theme Parking has a layer Parking showing features with this tag",
"value": "motorcycle_parking"
},
{
"key": "amenity",
"description": "The MapComplete theme Parking has a layer parking showing features with this tag",
"description": "The MapComplete theme Parking has a layer Parking showing features with this tag",
"value": "bicycle_parking"
},
{
"key": "image",
"description": "The layer 'parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
"description": "The layer 'Parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
"description": "The layer 'Parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
"description": "The layer 'Parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "access:description",
"description": "Layer 'parking' shows and asks freeform values for key 'access:description' (in the MapComplete.osm.be theme 'Parking')"
},
{
"key": "access",
"description": "Layer 'parking' shows access=yes&fee= with a fixed text, namely 'Vrij toegankelijk' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "yes"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=yes&fee= with a fixed text, namely 'Vrij toegankelijk' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking') Picking this answer will delete the key fee.",
"value": ""
},
{
"key": "access",
"description": "Layer 'parking' shows access=no&fee= with a fixed text, namely 'Niet toegankelijk' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "no"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=no&fee= with a fixed text, namely 'Niet toegankelijk' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking') Picking this answer will delete the key fee.",
"value": ""
},
{
"key": "access",
"description": "Layer 'parking' shows access=private&fee= with a fixed text, namely 'Niet toegankelijk, want privégebied' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "private"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=private&fee= with a fixed text, namely 'Niet toegankelijk, want privégebied' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking') Picking this answer will delete the key fee.",
"value": ""
},
{
"key": "access",
"description": "Layer 'parking' shows access=permissive&fee= with a fixed text, namely 'Toegankelijk, ondanks dat het privegebied is' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "permissive"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=permissive&fee= with a fixed text, namely 'Toegankelijk, ondanks dat het privegebied is' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking') Picking this answer will delete the key fee.",
"value": ""
},
{
"key": "access",
"description": "Layer 'parking' shows access=guided&fee= with a fixed text, namely 'Enkel toegankelijk met een gids of tijdens een activiteit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "guided"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=guided&fee= with a fixed text, namely 'Enkel toegankelijk met een gids of tijdens een activiteit' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking') Picking this answer will delete the key fee.",
"value": ""
},
{
"key": "access",
"description": "Layer 'parking' shows access=yes&fee=yes with a fixed text, namely 'Toegankelijk mits betaling' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "yes"
},
{
"key": "fee",
"description": "Layer 'parking' shows access=yes&fee=yes with a fixed text, namely 'Toegankelijk mits betaling' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "yes"
},
{
"key": "operator",
"description": "Layer 'parking' shows and asks freeform values for key 'operator' (in the MapComplete.osm.be theme 'Parking')"
},
{
"key": "operator",
"description": "Layer 'parking' shows operator=Natuurpunt with a fixed text, namely '<img src=\"./assets/themes/buurtnatuur/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door Natuurpunt' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Parking')",
"value": "Natuurpunt"
},
{
"key": "operator",
"description": "Layer 'parking' shows operator~^(n|N)atuurpunt.*$ with a fixed text, namely '<img src=\"./assets/themes/buurtnatuur/Natuurpunt.jpg\" style=\"width:1.5em\">Dit gebied wordt beheerd door {operator}' (in the MapComplete.osm.be theme 'Parking')"
"description": "The layer 'Parking allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
}
]
}

View file

@ -0,0 +1,65 @@
{
"data_format": 1,
"project": {
"name": "MapComplete Postbox and Post Office Map",
"description": "A map showing postboxes and post offices",
"project_url": "https://mapcomplete.osm.be/postboxes",
"doc_url": "https://github.com/pietervdvn/MapComplete/tree/master/assets/themes/",
"icon_url": "https://mapcomplete.osm.be/assets/themes/postboxes/postbox.svg",
"contact_name": "Pieter Vander Vennet, ",
"contact_email": "pietervdvn@posteo.net"
},
"tags": [
{
"key": "amenity",
"description": "The MapComplete theme Postbox and Post Office Map has a layer Postboxes showing features with this tag",
"value": "post_box"
},
{
"key": "image",
"description": "The layer 'Postboxes allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Postboxes allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Postboxes allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Postboxes allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "amenity",
"description": "The MapComplete theme Postbox and Post Office Map has a layer Post offices showing features with this tag",
"value": "post_office"
},
{
"key": "image",
"description": "The layer 'Post offices allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "mapillary",
"description": "The layer 'Post offices allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikidata",
"description": "The layer 'Post offices allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "wikipedia",
"description": "The layer 'Post offices allows to upload images and adds them under the 'image'-tag (and image:0, image:1, ... for multiple images). Furhtermore, this layer shows images based on the keys image, wikidata, wikipedia, wikimedia_commons and mapillary"
},
{
"key": "opening_hours",
"description": "Layer 'Post offices' shows and asks freeform values for key 'opening_hours' (in the MapComplete.osm.be theme 'Postbox and Post Office Map')"
},
{
"key": "opening_hours",
"description": "Layer 'Post offices' shows opening_hours=24/7 with a fixed text, namely '24/7 opened (including holidays)' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Postbox and Post Office Map')",
"value": "24/7"
}
]
}

View file

@ -139,23 +139,24 @@
"value": "dedicated_room"
},
{
"key": "service:bicycle:cleaning:charge",
"description": "Layer 'Toilets' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Open Toilet Map')"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Toilets' shows service:bicycle:cleaning:fee=no&service:bicycle:cleaning:charge= with a fixed text, namely 'The cleaning service is free to use' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "no&service:bicycle:cleaning:charge="
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Toilets' shows service:bicycle:cleaning:fee=no& with a fixed text, namely 'Free to use' (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "no&"
},
{
"key": "service:bicycle:cleaning:fee",
"description": "Layer 'Toilets' shows service:bicycle:cleaning:fee=yes with a fixed text, namely 'The cleaning service has a fee' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"key": "toilets:handwashing",
"description": "Layer 'Toilets' shows toilets:handwashing=yes with a fixed text, namely 'This toilets have a sink to wash your hands' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "yes"
},
{
"key": "toilets:handwashing",
"description": "Layer 'Toilets' shows toilets:handwashing=no with a fixed text, namely 'This toilets <b>don't</b> have a sink to wash your hands' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "no"
},
{
"key": "toilets:paper_supplied",
"description": "Layer 'Toilets' shows toilets:paper_supplied=yes with a fixed text, namely 'Toilet paper is equipped with toilet paper' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "yes"
},
{
"key": "toilets:paper_supplied",
"description": "Layer 'Toilets' shows toilets:paper_supplied=no with a fixed text, namely 'You have to bring your own toilet paper to this toilet' and allows to pick this as a default answer (in the MapComplete.osm.be theme 'Open Toilet Map')",
"value": "no"
}
]
}

View file

@ -119,7 +119,7 @@ export class ExtraFunction {
{
name: "closest",
doc: "Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. Returns a single geojson feature or undefined if nothing is found (or not yet laoded)",
args: ["list of features"]
args: ["list of features or a layer name or '*' to get all features"]
},
(params, feature) => {
return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat
@ -132,7 +132,7 @@ export class ExtraFunction {
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). In the case of ways/polygons, only the centerpoint is considered. " +
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet loaded)\n\n" +
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
args: ["list of features or layer name", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
args: ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
},
(params, feature) => {

View file

@ -402,6 +402,9 @@ export default class FeaturePipeline {
}
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][] {
if(layerId === "*"){
return this.GetAllFeaturesWithin(bbox)
}
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
if (requestedHierarchy === undefined) {
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))

View file

@ -48,8 +48,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
if(whitelist !== undefined){
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
if(!isWhiteListed){
console.log("Not whitelisted:",zxy, isWhiteListed, whitelist)
// return undefined;
return undefined;
}
}

View file

@ -16,35 +16,35 @@ export default class AllImageProviders {
Mapillary.singleton,
WikidataImageProvider.singleton,
WikimediaImageProvider.singleton,
new GenericImageProvider([].concat(...Imgur.defaultValuePrefix, WikimediaImageProvider.commonsPrefix, ...Mapillary.valuePrefixes))]
new GenericImageProvider(
[].concat(...Imgur.defaultValuePrefix, ...WikimediaImageProvider.commonsPrefixes, ...Mapillary.valuePrefixes)
)
]
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
public static LoadImagesFor(tags: UIEventSource<any>, imagePrefix?: string): UIEventSource<ProvidedImage[]> {
const id = tags.data.id
if (id === undefined) {
public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string): UIEventSource<ProvidedImage[]> {
if (tags.data.id === undefined) {
return undefined;
}
const cached = this._cache.get(tags.data.id)
const cacheKey = tags.data.id+tagKey
const cached = this._cache.get(cacheKey)
if (cached !== undefined) {
return cached
}
const source = new UIEventSource([])
this._cache.set(id, source)
this._cache.set(cacheKey, source)
const allSources = []
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
let prefixes = imageProvider.defaultKeyPrefixes
if(imagePrefix !== undefined){
prefixes = [...prefixes]
if(prefixes.indexOf("image") >= 0){
prefixes.splice(prefixes.indexOf("image"), 1)
}
prefixes.push(imagePrefix)
if(tagKey !== undefined){
prefixes = [tagKey]
}
const singleSource = imageProvider.GetRelevantUrls(tags, {

View file

@ -20,7 +20,14 @@ export default class GenericImageProvider extends ImageProvider {
if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
return []
}
try{
new URL(value)
}catch (_){
// Not a valid URL
return []
}
return [Promise.resolve({
key: key,
url: value,

View file

@ -1,23 +1,26 @@
import {UIEventSource} from "../UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement";
import {LicenseInfo} from "./LicenseInfo";
import {Utils} from "../../Utils";
export interface ProvidedImage {
url: string, key: string, provider: ImageProvider
url: string,
key: string,
provider: ImageProvider
}
export default abstract class ImageProvider {
public abstract readonly defaultKeyPrefixes : string[] = ["mapillary", "image"]
public abstract readonly defaultKeyPrefixes: string[] = ["mapillary", "image"]
private _cache = new Map<string, UIEventSource<LicenseInfo>>()
GetAttributionFor(url: string): UIEventSource<LicenseInfo> {
const cached = this._cache.get(url);
if (cached !== undefined) {
return cached;
}
const src =UIEventSource.FromPromise(this.DownloadAttribution(url))
const src = UIEventSource.FromPromise(this.DownloadAttribution(url))
this._cache.set(url, src)
return src;
}
@ -31,42 +34,48 @@ export default abstract class ImageProvider {
*/
public GetRelevantUrls(allTags: UIEventSource<any>, options?: {
prefixes?: string[]
}):UIEventSource<ProvidedImage[]> {
}): UIEventSource<ProvidedImage[]> {
const prefixes = options?.prefixes ?? this.defaultKeyPrefixes
if(prefixes === undefined){
throw "The image provider"+this.constructor.name+" doesn't define `defaultKeyPrefixes`"
if (prefixes === undefined) {
throw "The image provider" + this.constructor.name + " doesn't define `defaultKeyPrefixes`"
}
const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([])
const seenValues = new Set<string>()
const self =this
allTags.addCallbackAndRunD(tags => {
for (const key in tags) {
if(!prefixes.some(prefix => key.startsWith(prefix))){
if (!prefixes.some(prefix => key.startsWith(prefix))) {
continue
}
const value = tags[key]
if(seenValues.has(value)){
continue
}
seenValues.add(value)
this.ExtractUrls(key, value).then(promises => {
for (const promise of promises ?? []) {
if(promise === undefined){
continue
}
promise.then(providedImage => {
if(providedImage === undefined){
return
}
relevantUrls.data.push(providedImage)
relevantUrls.ping()
})
const values = Utils.NoEmpty(tags[key]?.split(";")?.map(v => v.trim()) ?? [])
for (const value of values) {
if (seenValues.has(value)) {
continue
}
})
seenValues.add(value)
this.ExtractUrls(key, value).then(promises => {
for (const promise of promises ?? []) {
if (promise === undefined) {
continue
}
promise.then(providedImage => {
if (providedImage === undefined) {
return
}
relevantUrls.data.push(providedImage)
relevantUrls.ping()
})
}
})
}
}
})
return relevantUrls
}
public abstract ExtractUrls(key: string, value: string) : Promise<Promise<ProvidedImage>[]>;
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
}

View file

@ -1,4 +1,5 @@
export class LicenseInfo {
title: string = ""
artist: string = "";
license: string = "";
licenseShortName: string = "";

View file

@ -34,9 +34,17 @@ export class WikidataImageProvider extends ImageProvider {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
allImages.push(...promises)
}
// P373 is 'commons category'
for (let cat of Array.from(entity.claims.get("P373") ?? [])) {
if(!cat.startsWith("Category:")){
cat = "Category:"+cat
}
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
allImages.push(...promises)
}
const commons = entity.commons
if (commons !== undefined) {
if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined , commons)
allImages.push(...promises)
}

View file

@ -4,6 +4,7 @@ import Svg from "../../Svg";
import Link from "../../UI/Base/Link";
import {Utils} from "../../Utils";
import {LicenseInfo} from "./LicenseInfo";
import Wikimedia from "../Web/Wikimedia";
/**
* This module provides endpoints for wikimedia and others
@ -14,56 +15,12 @@ export class WikimediaImageProvider extends ImageProvider {
private readonly commons_key = "wikimedia_commons"
public readonly defaultKeyPrefixes = [this.commons_key,"image"]
public static readonly singleton = new WikimediaImageProvider();
public static readonly commonsPrefix = "https://commons.wikimedia.org/wiki/"
public static readonly commonsPrefixes = ["https://commons.wikimedia.org/wiki/", "https://upload.wikimedia.org", "File:"]
private constructor() {
super();
}
/**
* Recursively walks a wikimedia commons category in order to search for (image) files
* Returns (a promise of) a list of URLS
* @param categoryName The name of the wikimedia category
* @param maxLoad: the maximum amount of images to return
* @param continueParameter: if the page indicates that more pages should be loaded, this uses a token to continue. Provided by wikimedia
*/
private static async GetImagesInCategory(categoryName: string,
maxLoad = 10,
continueParameter: string = undefined): Promise<string[]> {
if (categoryName === undefined || categoryName === null || categoryName === "") {
return [];
}
if (!categoryName.startsWith("Category:")) {
categoryName = "Category:" + categoryName;
}
let url = "https://commons.wikimedia.org/w/api.php?" +
"action=query&list=categorymembers&format=json&" +
"&origin=*" +
"&cmtitle=" + encodeURIComponent(categoryName);
if (continueParameter !== undefined) {
url = `${url}&cmcontinue=${continueParameter}`;
}
const response = await Utils.downloadJson(url)
const members = response.query?.categorymembers ?? [];
const imageOverview: string[] = members.map(member => member.title);
if (response.continue === undefined) {
// We are done crawling through the category - no continuation in sight
return imageOverview;
}
if (maxLoad - imageOverview.length <= 0) {
console.debug(`Recursive wikimedia category load stopped for ${categoryName}`)
return imageOverview;
}
// We do have a continue token - let's load the next page
const recursive = await this.GetImagesInCategory(categoryName, maxLoad - imageOverview.length, response.continue.cmcontinue)
imageOverview.push(...recursive)
return imageOverview
}
private static ExtractFileName(url: string) {
if (!url.startsWith("http")) {
return url;
@ -87,7 +44,7 @@ export class WikimediaImageProvider extends ImageProvider {
}
private PrepareUrl(value: string): string {
private static PrepareUrl(value: string): string {
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
return value;
@ -108,12 +65,26 @@ export class WikimediaImageProvider extends ImageProvider {
"&format=json&origin=*";
const data = await Utils.downloadJson(url)
const licenseInfo = new LicenseInfo();
const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata;
const pageInfo = data.query.pages[-1]
if(pageInfo === undefined){
return undefined;
}
const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata;
if (license === undefined) {
console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!")
console.warn("The file", filename ,"has no usable metedata or license attached... Please fix the license info file yourself!")
return undefined;
}
let title = pageInfo.title
if(title.startsWith("File:")){
title= title.substr("File:".length)
}
if(title.endsWith(".jpg") || title.endsWith(".png")){
title = title.substring(0, title.length - 4)
}
licenseInfo.title = title
licenseInfo.artist = license.Artist?.value;
licenseInfo.license = license.License?.value;
licenseInfo.copyrighted = license.Copyrighted?.value;
@ -126,41 +97,71 @@ export class WikimediaImageProvider extends ImageProvider {
}
private async UrlForImage(image: string): Promise<ProvidedImage> {
private UrlForImage(image: string): ProvidedImage {
if (!image.startsWith("File:")) {
image = "File:" + image
}
return {url: this.PrepareUrl(image), key: undefined, provider: this}
return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this}
}
private static startsWithCommonsPrefix(value: string): boolean{
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
}
private static removeCommonsPrefix(value: string): string{
if(value.startsWith("https://upload.wikimedia.org/")){
value = value.substring(value.lastIndexOf("/") + 1)
value = decodeURIComponent(value)
if(!value.startsWith("File:")){
value = "File:"+value
}
return value;
}
for (const prefix of WikimediaImageProvider.commonsPrefixes) {
if(value.startsWith(prefix)){
let part = value.substr(prefix.length)
if(prefix.startsWith("http")){
part = decodeURIComponent(part)
}
return part
}
}
return value;
}
public PrepUrl(value: string): ProvidedImage {
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
value = WikimediaImageProvider.removeCommonsPrefix(value)
if (value.startsWith("File:")) {
return this.UrlForImage(value)
}
// We do a last effort and assume this is a file
return this.UrlForImage("File:" + value)
}
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
if(key !== undefined && key !== this.commons_key && !value.startsWith(WikimediaImageProvider.commonsPrefix)){
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
if(key !== undefined && key !== this.commons_key && !hasCommonsPrefix){
return []
}
if (value.startsWith(WikimediaImageProvider.commonsPrefix)) {
value = value.substring(WikimediaImageProvider.commonsPrefix.length)
} else if (value.startsWith("https://upload.wikimedia.org")) {
const result: ProvidedImage = {
key: undefined,
url: value,
provider: this
}
return [Promise.resolve(result)]
}
value = WikimediaImageProvider.removeCommonsPrefix(value)
if (value.startsWith("Category:")) {
const urls = await WikimediaImageProvider.GetImagesInCategory(value)
return urls.map(image => this.UrlForImage(image))
const urls = await Wikimedia.GetCategoryContents(value)
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
}
if (value.startsWith("File:")) {
return [this.UrlForImage(value)]
return [Promise.resolve(this.UrlForImage(value))]
}
if (value.startsWith("http")) {
// PRobably an error
return []
}
// We do a last effort and assume this is a file
return [this.UrlForImage("File:" + value)]
return [Promise.resolve(this.UrlForImage("File:" + value))]
}

View file

@ -64,12 +64,12 @@ export default class MetaTagging {
if(metatag.isLazy){
somethingChanged = true;
metatag.applyMetaTagsOnFeature(feature, freshness)
metatag.applyMetaTagsOnFeature(feature, freshness, layer)
}else{
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness)
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer)
/* Note that the expression:
* `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)`
* Is WRONG

View file

@ -5,6 +5,24 @@ import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
*/
export interface ChangeDescription {
/**
* Metadata to be included in the changeset
*/
meta: {
/*
* The theme with which this changeset was made
*/
theme: string,
/**
* The type of the change
*/
changeType: "answer" | "create" | "split" | "delete" | string
/**
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
*/
specialMotivation?: string
},
/**
* Identifier of the object
*/

View file

@ -7,12 +7,17 @@ export default class ChangeTagAction extends OsmChangeAction {
private readonly _elementId: string;
private readonly _tagsFilter: TagsFilter;
private readonly _currentTags: any;
private readonly _meta: {theme: string, changeType: string};
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any) {
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
theme: string,
changeType: "answer" | "soft-delete" | "add-image"
}) {
super();
this._elementId = elementId;
this._tagsFilter = tagsFilter;
this._currentTags = currentTags;
this._meta = meta;
}
/**
@ -43,10 +48,10 @@ export default class ChangeTagAction extends OsmChangeAction {
const type = typeId[0]
const id = Number(typeId [1])
return [{
// @ts-ignore
type: type,
type: <"node"|"way"|"relation"> type,
id: id,
tags: changedTags
tags: changedTags,
meta: this._meta
}]
}
}

View file

@ -14,8 +14,14 @@ export default class CreateNewNodeAction extends OsmChangeAction {
private readonly _lon: number;
private readonly _snapOnto: OsmWay;
private readonly _reusePointDistance: number;
private meta: { changeType: "create" | "import"; theme: string };
constructor(basicTags: Tag[], lat: number, lon: number, options?: { snapOnto: OsmWay, reusePointWithinMeters?: number }) {
constructor(basicTags: Tag[],
lat: number, lon: number,
options: {
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" }) {
super()
this._basicTags = basicTags;
this._lat = lat;
@ -25,6 +31,10 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
this._snapOnto = options?.snapOnto;
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
this.meta = {
theme: options.theme,
changeType: options.changeType
}
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
@ -47,7 +57,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: {
lat: this._lat,
lon: this._lon
}
},
meta: this.meta
}
if (this._snapOnto === undefined) {
return [newPointChange]
@ -78,7 +89,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
return [{
tags: new And(this._basicTags).asChange(properties),
type: "node",
id: reusedPointId
id: reusedPointId,
meta: this.meta
}]
}
@ -99,7 +111,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: {
coordinates: locations,
nodes: ids
}
},
meta:this.meta
}
]
}

View file

@ -1,225 +1,62 @@
import {UIEventSource} from "../../UIEventSource";
import {Translation} from "../../../UI/i18n/Translation";
import State from "../../../State";
import {OsmObject} from "../OsmObject";
import Translations from "../../../UI/i18n/Translations";
import Constants from "../../../Models/Constants";
import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription";
import ChangeTagAction from "./ChangeTagAction";
import {TagsFilter} from "../../Tags/TagsFilter";
import {And} from "../../Tags/And";
import {Tag} from "../../Tags/Tag";
export default class DeleteAction {
export default class DeleteAction extends OsmChangeAction {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
public readonly isDeleted = new UIEventSource<boolean>(false);
private readonly _softDeletionTags: TagsFilter;
private readonly meta: {
theme: string,
specialMotivation: string,
changeType: "deletion"
};
private readonly _id: string;
private readonly _allowDeletionAtChangesetCount: number;
private _hardDelete: boolean;
constructor(id: string, allowDeletionAtChangesetCount?: number) {
constructor(id: string,
softDeletionTags: TagsFilter,
meta: {
theme: string,
specialMotivation: string
},
hardDelete: boolean) {
super()
this._id = id;
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE;
this._hardDelete = hardDelete;
this.meta = {...meta, changeType: "deletion"};
this._softDeletionTags = new And([softDeletionTags,
new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`)
]);
this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
canBeDeleted: undefined,
reason: Translations.t.delete.loading
})
this.CheckDeleteability(false)
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
/**
* Does actually delete the feature; returns the event source 'this.isDeleted'
* If deletion is not allowed, triggers the callback instead
*/
public DoDelete(reason: string, onNotAllowed: () => void): void {
const isDeleted = this.isDeleted
const self = this;
let deletionStarted = false;
this.canBeDeleted.addCallbackAndRun(
canBeDeleted => {
if (isDeleted.data || deletionStarted) {
// Already deleted...
return;
const osmObject = await OsmObject.DownloadObjectAsync(this._id)
if (this._hardDelete) {
return [{
meta: this.meta,
doDelete: true,
type: osmObject.type,
id: osmObject.id,
}]
} else {
return await new ChangeTagAction(
this._id, this._softDeletionTags, osmObject.tags,
{
theme: State.state?.layoutToUse?.id ?? "unkown",
changeType: "soft-delete"
}
if (canBeDeleted.canBeDeleted === false) {
// We aren't allowed to delete
deletionStarted = true;
onNotAllowed();
isDeleted.setData(true);
return;
}
if (!canBeDeleted) {
// We are not allowed to delete (yet), this might change in the future though
return;
}
deletionStarted = true;
OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => {
if (obj === undefined) {
return;
}
State.state.osmConnection.changesetHandler.DeleteElement(
obj,
State.state.layoutToUse,
reason,
State.state.allElements,
() => {
isDeleted.setData(true)
}
)
})
}
)
}
/**
* Checks if the currently logged in user can delete the current point.
* State is written into this._canBeDeleted
* @constructor
* @private
*/
public CheckDeleteability(useTheInternet: boolean): void {
const t = Translations.t.delete;
const id = this._id;
const state = this.canBeDeleted
if (!id.startsWith("node")) {
this.canBeDeleted.setData({
canBeDeleted: false,
reason: t.isntAPoint
})
return;
).CreateChangeDescriptions(changes)
}
// Does the currently logged in user have enough experience to delete this point?
const deletingPointsOfOtherAllowed = State.state.osmConnection.userDetails.map(ud => {
if (ud === undefined) {
return undefined;
}
if (!ud.loggedIn) {
return false;
}
return ud.csCount >= Math.min(Constants.userJourney.deletePointsOfOthersUnlock, this._allowDeletionAtChangesetCount);
})
const previousEditors = new UIEventSource<number[]>(undefined)
const allByMyself = previousEditors.map(previous => {
if (previous === null || previous === undefined) {
// Not yet downloaded
return null;
}
const userId = State.state.osmConnection.userDetails.data.uid;
return !previous.some(editor => editor !== userId)
}, [State.state.osmConnection.userDetails])
// User allowed OR only edited by self?
const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => {
if (isAllowed === undefined) {
// No logged in user => definitively not allowed to delete!
return false;
}
if (isAllowed === true) {
return true;
}
// At this point, the logged in user is not allowed to delete points created/edited by _others_
// however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
if (allByMyself.data === null && useTheInternet) {
// We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors)
}
if (allByMyself.data === true) {
// Yay! We can download!
return true;
}
if (allByMyself.data === false) {
// Nope, downloading not allowed...
return false;
}
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
return undefined;
}, [allByMyself])
const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null)
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
deletetionAllowed.addCallbackAndRunD(deletetionAllowed => {
if (deletetionAllowed === false) {
// Nope, we are not allowed to delete
state.setData({
canBeDeleted: false,
reason: t.notEnoughExperience
})
return true; // unregister this caller!
}
if (!useTheInternet) {
return;
}
// All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
OsmObject.DownloadReferencingRelations(id).then(rels => {
hasRelations.setData(rels.length > 0)
})
OsmObject.DownloadReferencingWays(id).then(ways => {
hasWays.setData(ways.length > 0)
})
return true; // unregister to only run once
})
const hasWaysOrRelations = hasRelations.map(hasRelationsData => {
if (hasRelationsData === true) {
return true;
}
if (hasWays.data === true) {
return true;
}
if (hasWays.data === null || hasRelationsData === null) {
return null;
}
if (hasWays.data === false && hasRelationsData === false) {
return false;
}
return null;
}, [hasWays])
hasWaysOrRelations.addCallbackAndRun(
waysOrRelations => {
if (waysOrRelations == null) {
// Not yet loaded - we still wait a little bit
return;
}
if (waysOrRelations) {
// not deleteble by mapcomplete
state.setData({
canBeDeleted: false,
reason: t.partOfOthers
})
} else {
// alright, this point can be safely deleted!
state.setData({
canBeDeleted: true,
reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete
})
}
}
)
}
}

View file

@ -16,14 +16,16 @@ export interface RelationSplitInput {
*/
export default class RelationSplitHandler extends OsmChangeAction {
private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) {
constructor(input: RelationSplitInput, theme: string) {
super()
this._input = input;
this._theme = theme;
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
return new InPlaceReplacedmentRTSH(this._input).CreateChangeDescriptions(changes)
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes)
}
@ -39,10 +41,12 @@ export default class RelationSplitHandler extends OsmChangeAction {
*/
export class InPlaceReplacedmentRTSH extends OsmChangeAction {
private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) {
constructor(input: RelationSplitInput, theme: string) {
super();
this._input = input;
this._theme = theme;
}
/**
@ -137,7 +141,11 @@ export class InPlaceReplacedmentRTSH extends OsmChangeAction {
return [{
id: relation.id,
type: "relation",
changes: {members: newMembers}
changes: {members: newMembers},
meta:{
changeType: "relation-fix",
theme: this._theme
}
}];
}

View file

@ -14,16 +14,19 @@ interface SplitInfo {
export default class SplitAction extends OsmChangeAction {
private readonly wayId: string;
private readonly _splitPointsCoordinates: [number, number] []// lon, lat
private _meta: { theme: string, changeType: "split" };
/**
*
* @param wayId
* @param splitPointCoordinates: lon, lat
* @param meta
*/
constructor(wayId: string, splitPointCoordinates: [number, number][]) {
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: {theme: string}) {
super()
this.wayId = wayId;
this._splitPointsCoordinates = splitPointCoordinates
this._meta = {...meta, changeType: "split"};
}
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
@ -89,7 +92,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
lon: element.lngLat[0],
lat: element.lngLat[1]
}
},
meta: this._meta
})
}
@ -110,7 +114,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds
}
},
meta: this._meta
})
allWayIdsInOrder.push(originalElement.id)
allWaysNodesInOrder.push(nodeIds)
@ -135,7 +140,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds
}
},
meta: this._meta
})
allWayIdsInOrder.push(id)
@ -152,8 +158,8 @@ export default class SplitAction extends OsmChangeAction {
allWayIdsInOrder: allWayIdsInOrder,
originalNodes: originalNodes,
allWaysNodesInOrder: allWaysNodesInOrder,
originalWayId: originalElement.id
}).CreateChangeDescriptions(changes)
originalWayId: originalElement.id,
}, this._meta.theme).CreateChangeDescriptions(changes)
changeDescription.push(...changDescrs)
}
@ -240,7 +246,6 @@ export default class SplitAction extends OsmChangeAction {
closest = prevPoint
}
// Ok, we have a closest point!
if(closest.originalIndex === 0 || closest.originalIndex === originalPoints.length){
// We can not split on the first or last points...
continue

View file

@ -64,9 +64,9 @@ export class Changes {
if (deletedElements.length > 0) {
changes +=
"\n<deleted>\n" +
"\n<delete>\n" +
deletedElements.map(e => e.ChangesetXML(csId)).join("\n") +
"\n</deleted>"
"\n</delete>"
}
changes += "</osmChange>";
@ -99,7 +99,7 @@ export class Changes {
}
this.isUploading.setData(true)
this.flushChangesAsync(flushreason)
this.flushChangesAsync()
.then(_ => {
this.isUploading.setData(false)
console.log("Changes flushed!");
@ -110,39 +110,94 @@ export class Changes {
})
}
private async flushChangesAsync(flushreason: string = undefined): Promise<void> {
/**
* UPload the selected changes to OSM.
* Returns 'true' if successfull and if they can be removed
* @param pending
* @private
*/
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{
const self = this;
const neededIds = Changes.GetNeededIds(pending)
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
const changes: {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = self.CreateChangesetObjects(pending, osmObjects)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
console.log("No changes to be made")
return true
}
const meta = pending[0].meta
const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({
key: key,
value: count,
aggregate: true
}))
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
.map(descr => ({
key: descr.meta.changeType+":"+descr.type+"/"+descr.id,
value: descr.meta.specialMotivation
}))
const metatags = [{
key: "comment",
value: "Adding data with #MapComplete for theme #"+meta.theme
},
{
key:"theme",
value:meta.theme
},
...perType,
...motivations
]
await State.state.osmConnection.changesetHandler.UploadChangeset(
(csId) => Changes.createChangesetFor(""+csId, changes),
metatags
)
console.log("Upload successfull!")
return true;
}
private async flushChangesAsync(): Promise<void> {
const self = this;
try {
console.log("Beginning upload... " + flushreason ?? "");
// At last, we build the changeset and upload
const pending = self.pendingChanges.data;
const neededIds = Changes.GetNeededIds(pending)
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
const changes: {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = self.CreateChangesetObjects(pending, osmObjects)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
console.log("No changes to be made")
self.pendingChanges.setData([])
self.isUploading.setData(false)
const pendingPerTheme = new Map<string, ChangeDescription[]>()
for (const changeDescription of pending) {
const theme = changeDescription.meta.theme
if(!pendingPerTheme.has(theme)){
pendingPerTheme.set(theme, [])
}
pendingPerTheme.get(theme).push(changeDescription)
}
const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value)
.map(async pendingChanges => {
try{
return await self.flushSelectChanges(pendingChanges);
}catch(e){
console.error("Could not upload some changes:",e)
return false
}
}))
if(!successes.some(s => s == false)){
// All changes successfull, we clear the data!
this.pendingChanges.setData([]);
}
await State.state.osmConnection.UploadChangeset(
State.state.layoutToUse,
State.state.allElements,
(csId) => Changes.createChangesetFor(csId, changes),
)
console.log("Upload successfull!")
this.pendingChanges.setData([]);
this.isUploading.setData(false)
} catch (e) {
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
self.pendingChanges.setData([])
}finally {
self.isUploading.setData(false)
}

View file

@ -6,29 +6,47 @@ import {ElementStorage} from "../ElementStorage";
import State from "../../State";
import Locale from "../../UI/i18n/Locale";
import Constants from "../../Models/Constants";
import {OsmObject} from "./OsmObject";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "./Changes";
import {Utils} from "../../Utils";
export interface ChangesetTag {
key: string,
value: string | number,
aggregate?: boolean
}
export class ChangesetHandler {
public readonly currentChangeset: UIEventSource<string>;
public readonly currentChangeset: UIEventSource<number>;
private readonly allElements: ElementStorage;
private osmConnection: OsmConnection;
private readonly changes: Changes;
private readonly _dryRun: boolean;
private readonly userDetails: UIEventSource<UserDetails>;
private readonly auth: any;
private readonly backend: string;
constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection,
constructor(layoutName: string, dryRun: boolean,
osmConnection: OsmConnection,
allElements: ElementStorage,
changes: Changes,
auth) {
this.osmConnection = osmConnection;
this.allElements = allElements;
this.changes = changes;
this._dryRun = dryRun;
this.userDetails = osmConnection.userDetails;
this.backend = osmConnection._oauth_config.url
this.auth = auth;
this.currentChangeset = osmConnection.GetPreference("current-open-changeset-" + layoutName);
this.currentChangeset = osmConnection.GetPreference("current-open-changeset-" + layoutName).map(
str => {
const n = Number(str);
if (isNaN(n)) {
return undefined
}
return n
}, [], n => "" + n
);
if (dryRun) {
console.log("DRYRUN ENABLED");
@ -39,7 +57,7 @@ export class ChangesetHandler {
const oldId = parseInt(node.attributes.old_id.value);
if (node.attributes.new_id === undefined) {
// We just removed this point!
const element =this. allElements.getEventSourceById("node/" + oldId);
const element = this.allElements.getEventSourceById("node/" + oldId);
element.data._deleted = "yes"
element.ping();
return;
@ -56,6 +74,10 @@ export class ChangesetHandler {
}
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
const element = this.allElements.getEventSourceById("node/" + oldId);
if(element === undefined){
// Element to rewrite not found, probably a node or relation that is not rendered
return undefined
}
element.data.id = type + "/" + newId;
this.allElements.addElementById(type + "/" + newId, element);
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
@ -83,7 +105,7 @@ export class ChangesetHandler {
}
}
this.changes.registerIdRewrites(mappings)
}
/**
@ -97,102 +119,96 @@ export class ChangesetHandler {
*
*/
public async UploadChangeset(
layout: LayoutConfig,
generateChangeXML: (csid: string) => string): Promise<void> {
generateChangeXML: (csid: number) => string,
extraMetaTags: ChangesetTag[]): Promise<void> {
if (!extraMetaTags.some(tag => tag.key === "comment") || !extraMetaTags.some(tag => tag.key === "theme")) {
throw "The meta tags should at least contain a `comment` and a `theme`"
}
if (this.userDetails.data.csCount == 0) {
// The user became a contributor!
this.userDetails.data.csCount = 1;
this.userDetails.ping();
}
if (this._dryRun) {
const changesetXML = generateChangeXML("123456");
const changesetXML = generateChangeXML(123456);
console.log(changesetXML);
return;
}
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") {
if (this.currentChangeset.data === undefined) {
// We have to open a new changeset
try {
const csId = await this.OpenChangeset(layout)
const csId = await this.OpenChangeset(extraMetaTags)
this.currentChangeset.setData(csId);
const changeset = generateChangeXML(csId);
console.log("Current changeset is:", changeset);
await this.AddChange(csId, changeset)
} catch (e) {
console.error("Could not open/upload changeset due to ", e)
this.currentChangeset.setData("")
this.currentChangeset.setData(undefined)
}
} else {
// There still exists an open changeset (or at least we hope so)
// Let's check!
const csId = this.currentChangeset.data;
try {
const oldChangesetMeta = await this.GetChangesetMeta(csId)
if (!oldChangesetMeta.open) {
// Mark the CS as closed...
this.currentChangeset.setData(undefined);
// ... and try again. As the cs is closed, no recursive loop can exist
await this.UploadChangeset(generateChangeXML, extraMetaTags)
return;
}
const extraTagsById = new Map<string, ChangesetTag>()
for (const extraMetaTag of extraMetaTags) {
extraTagsById.set(extraMetaTag.key, extraMetaTag)
}
const oldCsTags = oldChangesetMeta.tags
for (const key in oldCsTags) {
const newMetaTag = extraTagsById.get(key)
if (newMetaTag === undefined) {
extraMetaTags.push({
key: key,
value: oldCsTags[key]
})
} else if (newMetaTag.aggregate) {
let n = Number(newMetaTag.value)
if (isNaN(n)) {
n = 0
}
let o = Number(oldCsTags[key])
if (isNaN(o)) {
o = 0
}
// We _update_ the tag itself, as it'll be updated in 'extraMetaTags' straight away
newMetaTag.value = "" + (n + o)
} else {
// The old value is overwritten, thus we drop
}
}
await this.UpdateTags(csId, extraMetaTags.map(csTag => <[string, string]>[csTag.key, csTag.value]))
await this.AddChange(
csId,
generateChangeXML(csId))
} catch (e) {
console.warn("Could not upload, changeset is probably closed: ", e);
// Mark the CS as closed...
this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist
await this.UploadChangeset(layout, generateChangeXML)
this.currentChangeset.setData(undefined);
}
}
}
/**
* Deletes the element with the given ID from the OSM database.
* DOES NOT PERFORM ANY SAFETY CHECKS!
*
* For the deletion of an element, a new, separate changeset is created with a slightly changed comment and some extra flags set.
* The CS will be closed afterwards.
*
* If dryrun is specified, will not actually delete the point but print the CS-XML to console instead
*
*/
public DeleteElement(object: OsmObject,
layout: LayoutConfig,
reason: string,
allElements: ElementStorage,
continuation: () => void) {
return this.DeleteElementAsync(object, layout, reason, allElements).then(continuation)
}
public async DeleteElementAsync(object: OsmObject,
layout: LayoutConfig,
reason: string,
allElements: ElementStorage): Promise<void> {
function generateChangeXML(csId: string) {
let [lat, lon] = object.centerpoint();
let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`;
changes +=
`<delete><${object.type} id="${object.id}" version="${object.version}" changeset="${csId}" lat="${lat}" lon="${lon}" /></delete>`;
changes += "</osmChange>";
return changes;
}
if (this._dryRun) {
const changesetXML = generateChangeXML("123456");
console.log(changesetXML);
return;
}
const csId = await this.OpenChangeset(layout, {
isDeletionCS: true,
deletionReason: reason
})
// The cs is open - let us actually upload!
const changes = generateChangeXML(csId)
await this.AddChange(csId, changes)
await this.CloseChangeset(csId)
}
private async CloseChangeset(changesetId: string = undefined): Promise<void> {
private async CloseChangeset(changesetId: number = undefined): Promise<void> {
const self = this
return new Promise<void>(function (resolve, reject) {
if (changesetId === undefined) {
@ -202,7 +218,7 @@ export class ChangesetHandler {
return;
}
console.log("closing changeset", changesetId);
self.currentChangeset.setData("");
self.currentChangeset.setData(undefined);
self.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/' + changesetId + '/close',
@ -217,39 +233,63 @@ export class ChangesetHandler {
})
}
private OpenChangeset(
layout: LayoutConfig,
options?: {
isDeletionCS?: boolean,
deletionReason?: string,
}
): Promise<string> {
private async GetChangesetMeta(csId: number): Promise<{
id: number,
open: boolean,
uid: number,
changes_count: number,
tags: any
}> {
const url = `${this.backend}/api/0.6/changeset/${csId}`
const csData = await Utils.downloadJson(url)
return csData.elements[0]
}
private async UpdateTags(
csId: number,
tags: [string, string][]) {
const self = this;
return new Promise<string>(function (resolve, reject) {
options = options ?? {}
options.isDeletionCS = options.isDeletionCS ?? false
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
if (options.isDeletionCS) {
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
if (options.deletionReason) {
comment += ": " + options.deletionReason;
tags = Utils.NoNull(tags).filter(([k, v]) => k !== undefined && v !== undefined && k !== "" && v !== "")
const metadata = tags.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
self.auth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/' + csId,
options: {header: {'Content-Type': 'text/xml'}},
content: [`<osm><changeset>`,
metadata,
`</changeset></osm>`].join("")
}, function (err, response) {
if (response === undefined) {
console.log("err", err);
reject(err)
} else {
resolve(response);
}
}
});
})
}
private OpenChangeset(
changesetTags: ChangesetTag[]
): Promise<number> {
const self = this;
return new Promise<number>(function (resolve, reject) {
let path = window.location.pathname;
path = path.substr(1, path.lastIndexOf("/"));
const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`],
["comment", comment],
["deletion", options.isDeletionCS ? "yes" : undefined],
["theme", layout.id],
["language", Locale.language.data],
["host", window.location.host],
["path", path],
["source", State.state.currentGPSLocation.data !== undefined ? "survey" : undefined],
["imagery", State.state.backgroundLayer.data.id],
["theme-creator", layout.maintainer]
...changesetTags.map(cstag => [cstag.key, cstag.value])
]
.filter(kv => (kv[1] ?? "") !== "")
.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
@ -268,7 +308,7 @@ export class ChangesetHandler {
console.log("err", err);
reject(err)
} else {
resolve(response);
resolve(Number(response));
}
});
})
@ -278,8 +318,8 @@ export class ChangesetHandler {
/**
* Upload a changesetXML
*/
private AddChange(changesetId: string,
changesetXML: string): Promise<string> {
private AddChange(changesetId: number,
changesetXML: string): Promise<number> {
const self = this;
return new Promise(function (resolve, reject) {
self.auth.xhr({

View file

@ -124,13 +124,6 @@ export class OsmConnection {
}
}
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string): Promise<void> {
return this.changesetHandler.UploadChangeset(layout, generateChangeXML);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
return this.preferencesHandler.GetPreference(key, prefix);
}

View file

@ -11,7 +11,7 @@ export abstract class OsmObject {
private static polygonFeatures = OsmObject.constructPolygonFeatures()
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>();
type: string;
type: "node" | "way" | "relation";
id: number;
/**
* The OSM tags as simple object
@ -23,6 +23,7 @@ export abstract class OsmObject {
protected constructor(type: string, id: number) {
this.id = id;
// @ts-ignore
this.type = type;
this.tags = {
id: `${this.type}/${id}`

View file

@ -6,6 +6,7 @@ import Combine from "../UI/Base/Combine";
import BaseUIElement from "../UI/BaseUIElement";
import Title from "../UI/Base/Title";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
const cardinalDirections = {
@ -62,6 +63,20 @@ export default class SimpleMetaTagger {
return true;
})
);
private static layerInfo = new SimpleMetaTagger(
{
doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.",
keys:["_layer"],
includesDates: false,
},
(feature, freshness, layer) => {
if(feature.properties._layer === layer.id){
return false;
}
feature.properties._layer = layer.id
return true;
}
)
private static surfaceArea = new SimpleMetaTagger(
{
keys: ["_surface", "_surface:ha"],
@ -329,6 +344,7 @@ export default class SimpleMetaTagger {
)
public static metatags = [
SimpleMetaTagger.latlon,
SimpleMetaTagger.layerInfo,
SimpleMetaTagger.surfaceArea,
SimpleMetaTagger.lngth,
SimpleMetaTagger.canonicalize,
@ -346,7 +362,7 @@ export default class SimpleMetaTagger {
public readonly doc: string;
public readonly isLazy: boolean;
public readonly includesDates: boolean
public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date) => boolean;
public readonly applyMetaTagsOnFeature: (feature: any, freshness: Date, layer: LayerConfig) => boolean;
/***
* A function that adds some extra data to a feature
@ -354,7 +370,7 @@ export default class SimpleMetaTagger {
* @param f: apply the changes. Returns true if something changed
*/
constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean },
f: ((feature: any, freshness: Date) => boolean)) {
f: ((feature: any, freshness: Date, layer: LayerConfig) => boolean)) {
this.keys = docs.keys;
this.doc = docs.doc;
this.isLazy = docs.isLazy

View file

@ -75,6 +75,27 @@ export class UIEventSource<T> {
promise?.catch(err => console.warn("Promise failed:", err))
return src
}
public AsPromise(): Promise<T>{
const self = this;
return new Promise((resolve, reject) => {
if(self.data !== undefined){
resolve(self.data)
}else{
self.addCallbackD(data => {
resolve(data)
return true; // return true to unregister as we only need to be called once
})
}
})
}
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
const self = this;
promise?.then(d => self.setData(d))
promise?.catch(err =>onFail(err))
return this
}
/**
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
@ -195,16 +216,20 @@ export class UIEventSource<T> {
const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<UIEventSource<X>>();
mapped.addCallbackAndRun(newEventSource => {
if (newEventSource === undefined) {
if (newEventSource === null) {
sink.setData(null)
} else if (newEventSource === undefined) {
sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) {
}else if (!seenEventSources.has(newEventSource)) {
seenEventSources.add(newEventSource)
newEventSource.addCallbackAndRun(resultData => {
if (mapped.data === newEventSource) {
sink.setData(resultData);
}
})
}else{
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
}
})

View file

@ -2,22 +2,33 @@ import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource";
export interface WikidataResponse {
export class WikidataResponse {
public readonly id: string
public readonly labels: Map<string, string>
public readonly descriptions: Map<string, string>
public readonly claims: Map<string, Set<string>>
public readonly wikisites: Map<string, string>
public readonly commons: string
id: string,
labels: Map<string, string>,
descriptions: Map<string, string>,
claims: Map<string, Set<string>>,
wikisites: Map<string, string>
commons: string
}
constructor(
id: string,
labels: Map<string, string>,
descriptions: Map<string, string>,
claims: Map<string, Set<string>>,
wikisites: Map<string, string>,
commons: string
) {
/**
* Utility functions around wikidata
*/
export default class Wikidata {
this.id = id
this.labels = labels
this.descriptions = descriptions
this.claims = claims
this.wikisites = wikisites
this.commons = commons
private static ParseResponse(entity: any): WikidataResponse {
}
public static fromJson(entity: any): WikidataResponse {
const labels = new Map<string, string>()
for (const labelName in entity.labels) {
// The labelname is the language code
@ -37,64 +48,239 @@ export default class Wikidata {
const title = entity.sitelinks[labelName].title
sitelinks.set(language, title)
}
const commons = sitelinks.get("commons")
sitelinks.delete("commons")
const claims = WikidataResponse.extractClaims(entity.claims);
return new WikidataResponse(
entity.id,
labels,
descr,
claims,
sitelinks,
commons
)
}
static extractClaims(claimsJson: any): Map<string, Set<string>> {
const claims = new Map<string, Set<string>>();
for (const claimId of entity.claims) {
for (const claimId in claimsJson) {
const claimsList: any[] = entity.claims[claimId]
const claimsList: any[] = claimsJson[claimId]
const values = new Set<string>()
for (const claim of claimsList) {
const value = claim.mainsnak.datavalue.value;
let value = claim.mainsnak?.datavalue?.value;
if (value === undefined) {
continue;
}
if (value.id !== undefined) {
value = value.id
}
values.add(value)
}
claims.set(claimId, values);
}
return claims
}
}
return {
claims: claims,
descriptions: descr,
id: entity.id,
labels: labels,
wikisites: sitelinks,
commons: commons
export class WikidataLexeme {
id: string
lemma: Map<string, string>
senses: Map<string, string>
claims: Map<string, Set<string>>
constructor(json) {
this.id = json.id
this.claims = WikidataResponse.extractClaims(json.claims)
this.lemma = new Map<string, string>()
for (const language in json.lemmas) {
this.lemma.set(language, json.lemmas[language].value)
}
this.senses = new Map<string, string>()
for (const sense of json.senses) {
const glosses = sense.glosses
for (const language in glosses) {
let previousSenses = this.senses.get(language)
if(previousSenses === undefined){
previousSenses = ""
}else{
previousSenses = previousSenses+"; "
}
this.senses.set(language, previousSenses + glosses[language].value ?? "")
}
}
}
private static readonly _cache = new Map<number, UIEventSource<{success: WikidataResponse} | {error: any}>>()
public static LoadWikidataEntry(value: string | number): UIEventSource<{success: WikidataResponse} | {error: any}> {
asWikidataResponse() {
return new WikidataResponse(
this.id,
this.lemma,
this.senses,
this.claims,
new Map(),
undefined
);
}
}
export interface WikidataSearchoptions {
lang?: "en" | string,
maxCount?: 20 | 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 _cache = new Map<string, UIEventSource<{ success: WikidataResponse } | { error: any }>>()
public static LoadWikidataEntry(value: string | number): UIEventSource<{ success: WikidataResponse } | { error: any }> {
const key = this.ExtractKey(value)
const cached = Wikidata._cache.get(key)
if(cached !== undefined){
if (cached !== undefined) {
return cached
}
const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
Wikidata._cache.set(key, src)
return src;
}
private static ExtractKey(value: string | number) : number{
public static async search(
search: string,
options?: WikidataSearchoptions,
page = 1
): Promise<{
id: string,
label: string,
description: string
}[]> {
const maxCount = options?.maxCount ?? 20
let pageCount = Math.min(maxCount, 50)
const start = page * pageCount - pageCount;
const lang = (options?.lang ?? "en")
const url =
"https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" +
search +
"&language=" +
lang +
"&limit=" + pageCount + "&continue=" +
start +
"&format=json&uselang=" +
lang +
"&type=item&origin=*" +
"&props=";// props= removes some unused values in the result
const response = await Utils.downloadJson(url)
const result: any[] = response.search
if (result.length < pageCount) {
// No next page
return result;
}
if (result.length < maxCount) {
const newOptions = {...options}
newOptions.maxCount = maxCount - result.length
result.push(...await Wikidata.search(search,
newOptions,
page + 1
))
}
return result;
}
public static async searchAndFetch(
search: string,
options?: WikidataSearchoptions
): 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;
}
if (wd.claims.get("P31" /*Instance of*/)?.has("Q4167410"/* Wikimedia Disambiguation page*/)) {
return false;
}
return true;
})
responses.splice(maxCount, responses.length - maxCount)
return responses
}
public static ExtractKey(value: string | number): string {
if (typeof value === "number") {
return value
return "Q" + value
}
const wikidataUrl = "https://www.wikidata.org/wiki/"
if (value.startsWith(wikidataUrl)) {
value = value.substring(wikidataUrl.length)
if (value === undefined) {
console.error("ExtractKey: value is undefined")
return undefined;
}
if (value.startsWith("http")) {
value = value.trim().toLowerCase()
for (const prefix of Wikidata._prefixesToRemove) {
if (value.startsWith(prefix)) {
value = value.substring(prefix.length)
}
}
if (value.startsWith("http") && value === "") {
// Probably some random link in the image field - we skip it
return undefined
}
if (value.startsWith("Q")) {
value = value.substring(1)
for (const identifierPrefix of Wikidata._identifierPrefixes) {
if (value.startsWith(identifierPrefix)) {
const trimmed = value.substring(identifierPrefix.length);
if(trimmed === ""){
return undefined
}
const n = Number(trimmed)
if (isNaN(n)) {
return undefined
}
return value.toUpperCase();
}
}
const n = Number(value)
if(isNaN(n)){
return undefined
if (value !== "" && !isNaN(Number(value))) {
return "Q" + value
}
return n;
return undefined;
}
public static IdToArticle(id: string){
if(id.startsWith("Q")){
return "https://wikidata.org/wiki/"+id
}
if(id.startsWith("L")){
return "https://wikidata.org/wiki/Lexeme:"+id
}
throw "Unknown id type: "+id
}
/**
@ -103,14 +289,22 @@ export default class Wikidata {
*/
public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
const id = Wikidata.ExtractKey(value)
if(id === undefined){
if (id === undefined) {
console.warn("Could not extract a wikidata entry from", value)
return undefined;
throw "Could not extract a wikidata entry from " + value
}
console.log("Requesting wikidata with id", id)
const url = "https://www.wikidata.org/wiki/Special:EntityData/Q" + id + ".json";
const response = await Utils.downloadJson(url)
return Wikidata.ParseResponse(response.entities["Q" + id])
const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json";
const entities = (await Utils.downloadJson(url)).entities
const firstKey = <string> Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
const response = entities[firstKey]
if (id.startsWith("L")) {
// This is a lexeme:
return new WikidataLexeme(response).asWikidataResponse()
}
return WikidataResponse.fromJson(response)
}
}

47
Logic/Web/Wikimedia.ts Normal file
View file

@ -0,0 +1,47 @@
import {Utils} from "../../Utils";
export default class Wikimedia {
/**
* Recursively walks a wikimedia commons category in order to search for entries, which can be File: or Category: entries
* Returns (a promise of) a list of URLS
* @param categoryName The name of the wikimedia category
* @param maxLoad: the maximum amount of images to return
* @param continueParameter: if the page indicates that more pages should be loaded, this uses a token to continue. Provided by wikimedia
*/
public static async GetCategoryContents(categoryName: string,
maxLoad = 10,
continueParameter: string = undefined): Promise<string[]> {
if (categoryName === undefined || categoryName === null || categoryName === "") {
return [];
}
if (!categoryName.startsWith("Category:")) {
categoryName = "Category:" + categoryName;
}
let url = "https://commons.wikimedia.org/w/api.php?" +
"action=query&list=categorymembers&format=json&" +
"&origin=*" +
"&cmtitle=" + encodeURIComponent(categoryName);
if (continueParameter !== undefined) {
url = `${url}&cmcontinue=${continueParameter}`;
}
const response = await Utils.downloadJson(url)
const members = response.query?.categorymembers ?? [];
const imageOverview: string[] = members.map(member => member.title);
if (response.continue === undefined) {
// We are done crawling through the category - no continuation in sight
return imageOverview;
}
if (maxLoad - imageOverview.length <= 0) {
console.debug(`Recursive wikimedia category load stopped for ${categoryName}`)
return imageOverview;
}
// We do have a continue token - let's load the next page
const recursive = await Wikimedia.GetCategoryContents(categoryName, maxLoad - imageOverview.length, response.continue.cmcontinue)
imageOverview.push(...recursive)
return imageOverview
}
}

View file

@ -22,6 +22,10 @@ export default class Wikipedia {
"mw-selflink",
"hatnote" // Often redirects
]
private static readonly idsToRemove = [
"sjabloon_zie"
]
private static readonly _cache = new Map<string, UIEventSource<{ success: string } | { error: any }>>()
@ -59,6 +63,13 @@ export default class Wikipedia {
}
}
for (const forbiddenId of Wikipedia.idsToRemove) {
const toRemove = content.querySelector("#"+forbiddenId)
toRemove?.parentElement?.removeChild(toRemove)
}
const links = Array.from(content.getElementsByTagName("a"))
// Rewrite relative links to absolute links + open them in a new tab

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.10.5";
public static vNumber = "0.11.0-alpha-2";
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
@ -14,7 +14,7 @@ export default class Constants {
"https://overpass.kumi.systems/api/interpreter",
// Offline: "https://overpass.nchc.org.tw/api/interpreter",
"https://overpass.openstreetmap.ru/cgi/interpreter",
// Doesn't support nwr "https://overpass.openstreetmap.fr/api/interpreter"
// Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter"
]

View file

@ -35,10 +35,7 @@ export interface LayoutConfigJson {
* Who does maintian this preset?
*/
maintainer: string;
/**
* Extra piece of text that can be added to the changeset
*/
changesetmessage?: string;
/**
* A version number, either semantically or by date.
* Should be sortable, where the higher value is the later version

View file

@ -52,7 +52,7 @@ export interface TagRenderingConfigJson {
* Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md'
*/
helperArgs?: (string | number | boolean)[];
helperArgs?: (string | number | boolean | any)[];
/**
* If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked'

View file

@ -12,7 +12,6 @@ export default class LayoutConfig {
public readonly id: string;
public readonly maintainer: string;
public readonly credits?: string;
public readonly changesetmessage?: string;
public readonly version: string;
public readonly language: string[];
public readonly title: Translation;
@ -61,7 +60,6 @@ export default class LayoutConfig {
context = (context ?? "") + "." + this.id;
this.maintainer = json.maintainer;
this.credits = json.credits;
this.changesetmessage = json.changesetmessage;
this.version = json.version;
this.language = [];
if (typeof json.language === "string") {

View file

@ -30,10 +30,15 @@ export default class Combine extends BaseUIElement {
if (subEl === undefined || subEl === null) {
continue;
}
try{
const subHtml = subEl.ConstructElement()
if (subHtml !== undefined) {
el.appendChild(subHtml)
}
}catch(e){
console.error("Could not generate subelement in combine due to ", e)
}
}
} catch (e) {
const domExc = e as DOMException

View file

@ -6,11 +6,16 @@ import {VariableUiElement} from "./VariableUIElement";
export class TabbedComponent extends Combine {
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) {
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
openedTab: (UIEventSource<number> | number) = 0,
options?: {
leftOfHeader?: BaseUIElement
styleHeader?: (header: BaseUIElement) => void
}) {
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
const tabs: BaseUIElement[] = []
const tabs: BaseUIElement[] = [options?.leftOfHeader ]
const contentElements: BaseUIElement[] = [];
for (let i = 0; i < elements.length; i++) {
let element = elements[i];
@ -25,16 +30,19 @@ export class TabbedComponent extends Combine {
}
})
const content = Translations.W(element.content)
content.SetClass("relative p-4 w-full inline-block")
content.SetClass("relative w-full inline-block")
contentElements.push(content);
const tab = header.SetClass("block tab-single-header")
tabs.push(tab)
}
const header = new Combine(tabs).SetClass("tabs-header-bar")
if(options?.styleHeader){
options.styleHeader(header)
}
const actualContent = new VariableUiElement(
openedTabSrc.map(i => contentElements[i])
)
).SetStyle("max-height: inherit; height: inherit")
super([header, actualContent])
}

View file

@ -73,6 +73,9 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
}
);
tabs.forEach(c => c.content.SetClass("p-4"))
tabsWithAboutMc.forEach(c => c.content.SetClass("p-4"))
return new Toggle(
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),

View file

@ -44,7 +44,10 @@ export default class ImportButton extends Toggle {
}
originalTags.data["_imported"] = "yes"
originalTags.ping() // will set isImported as per its definition
const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon)
const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, {
theme: State.state.layoutToUse.id,
changeType: "import"
})
await State.state.changes.applyAction(newElementAction)
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId

View file

@ -56,7 +56,10 @@ export default class SimpleAddUI extends Toggle {
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {snapOnto: snapOntoWay})
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: State.state?.layoutToUse?.id ?? "unkown",
changeType: "create",
snapOnto: snapOntoWay})
await State.state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
isShown.setData(false)

View file

@ -21,10 +21,11 @@ export default class Attribution extends VariableUiElement {
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
new Combine([
Translations.W(license?.artist ?? ".").SetClass("block font-bold"),
Translations.W(license?.title).SetClass("block"),
Translations.W(license?.artist ?? "").SetClass("block font-bold"),
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
]).SetClass("flex flex-col")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images")
}));
}

View file

@ -16,7 +16,10 @@ export default class DeleteImage extends Toggle {
.SetClass("rounded-full p-1")
.SetStyle("color:white;background:#ff8c8c")
.onClick(async() => {
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data))
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
changeType: "answer",
theme: "test"
}))
});
const deleteButton = Translations.t.image.doDelete.Clone()
@ -24,7 +27,10 @@ export default class DeleteImage extends Toggle {
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
.onClick( async() => {
await State.state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data)
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data,{
changeType: "answer",
theme: "test"
})
)
});

View file

@ -12,10 +12,11 @@ import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader";
import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {FixedUiElement} from "../Base/FixedUiElement";
export class ImageUploadFlow extends Toggle {
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image") {
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
const uploader = new ImgurUploader(url => {
// A file was uploaded - we add it to the tags of the object
@ -31,7 +32,11 @@ export class ImageUploadFlow extends Toggle {
console.log("Adding image:" + key, url);
Promise.resolve(State.state.changes
.applyAction(new ChangeTagAction(
tags.id, new Tag(key, url), tagsSource.data
tags.id, new Tag(key, url), tagsSource.data,
{
changeType: "add-image",
theme: State.state.layoutToUse.id
}
)))
})
@ -42,10 +47,17 @@ export class ImageUploadFlow extends Toggle {
const licensePicker = new LicensePicker()
const t = Translations.t.image;
let labelContent : BaseUIElement
if(text === undefined) {
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
}else{
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
}
const label = new Combine([
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"),
Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3")
]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center")
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
labelContent
]).SetClass("p-2 border-4 border-black rounded-full font-bold h-full align-middle w-full flex justify-center")
const fileSelector = new FileSelectorButton(label)
fileSelector.GetValue().addCallback(filelist => {

View file

@ -16,6 +16,8 @@ import LengthInput from "./LengthInput";
import {GeoOperations} from "../../Logic/GeoOperations";
import {Unit} from "../../Models/Unit";
import {FixedInputElement} from "./FixedInputElement";
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
import Wikidata from "../../Logic/Web/Wikidata";
interface TextFieldDef {
name: string,
@ -147,23 +149,58 @@ export default class ValidatedTextField {
),
ValidatedTextField.tp(
"wikidata",
"A wikidata identifier, e.g. Q42",
"A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]",
(str) => {
if (str === undefined) {
return false;
}
return (str.length > 1 && (str.startsWith("q") || str.startsWith("Q")) || str.startsWith("https://www.wikidata.org/wiki/Q"))
if(str.length <= 2){
return false;
}
return !str.split(";").some(str => Wikidata.ExtractKey(str) === undefined)
},
(str) => {
if (str === undefined) {
return undefined;
}
const wd = "https://www.wikidata.org/wiki/";
if (str.startsWith(wd)) {
str = str.substr(wd.length)
let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
if(str.endsWith(";")){
out = out + ";"
}
return str.toUpperCase();
}),
return out;
},
(currentValue, inputHelperOptions) => {
const args = inputHelperOptions.args ?? []
const searchKey = args[0] ?? "name"
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
const options = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[]>options["removePrefixes"]
const postfixes = <string[]>options["removePostfixes"]
for (const postfix of postfixes ?? []) {
if (searchFor.endsWith(postfix)) {
searchFor = searchFor.substring(0, searchFor.length - postfix.length)
break;
}
}
for (const prefix of prefixes ?? []) {
if (searchFor.startsWith(prefix)) {
searchFor = searchFor.substring(prefix.length)
break;
}
}
}
return new WikidataSearchBox({
value: currentValue,
searchText: new UIEventSource<string>(searchFor)
})
}
),
ValidatedTextField.tp(
"int",
@ -361,13 +398,13 @@ export default class ValidatedTextField {
// This implies:
// We have to create a dropdown with applicable denominations, and fuse those values
const unit = options.unit
const isSingular = input.GetValue().map(str => str?.trim() === "1")
const unitDropDown =
unit.denominations.length === 1 ?
new FixedInputElement( unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
: new DropDown("",
unit.denominations.map(denom => {
return {
@ -378,17 +415,17 @@ export default class ValidatedTextField {
)
unitDropDown.GetValue().setData(unit.defaultDenom)
unitDropDown.SetClass("w-min")
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
input = new CombinedInputElement(
input,
unitDropDown,
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
(text, denom) => {
if(denom === undefined){
if (denom === undefined) {
return text
}
return denom?.canonicalValue(text, true)
return denom?.canonicalValue(text, true)
},
(valueWithDenom: string) => {
// Take the value from OSM and feed it into the textfield and the dropdown

View file

@ -4,7 +4,6 @@ import Toggle from "../Input/Toggle";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction";
import {Tag} from "../../Logic/Tags/Tag";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import TagRenderingQuestion from "./TagRenderingQuestion";
@ -13,13 +12,11 @@ import {SubtleButton} from "../Base/SubtleButton";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation";
import BaseUIElement from "../BaseUIElement";
import {Changes} from "../../Logic/Osm/Changes";
import {And} from "../../Logic/Tags/And";
import Constants from "../../Models/Constants";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig";
import {OsmObject} from "../../Logic/Osm/OsmObject";
export default class DeleteWizard extends Toggle {
/**
@ -43,44 +40,32 @@ export default class DeleteWizard extends Toggle {
constructor(id: string,
options: DeleteConfig) {
const deleteAction = new DeleteAction(id, options.neededChangesets);
const deleteAbility = new DeleteabilityChecker(id, options.neededChangesets)
const tagsSource = State.state.allElements.getEventSourceById(id)
const isDeleted = new UIEventSource(false)
const allowSoftDeletion = !!options.softDeletionTags
const confirm = new UIEventSource<boolean>(false)
async function softDelete(reason: string, tagsToApply: { k: string, v: string }[]) {
if (reason !== undefined) {
tagsToApply.splice(0, 0, {
k: "fixme",
v: `A mapcomplete user marked this feature to be deleted (${reason})`
})
}
await (State.state?.changes ?? new Changes())
.applyAction(new ChangeTagAction(
id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data
))
}
function doDelete(selected: TagsFilter) {
// Selected == the reasons, not the tags of the object
const tgs = selected.asChange(tagsSource.data)
const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason")
if (deleteReasonMatch.length > 0) {
// We should actually delete!
const deleteReason = deleteReasonMatch[0].v
deleteAction.DoDelete(deleteReason, () => {
// The user doesn't have sufficient permissions to _actually_ delete the feature
// We 'soft delete' instead (and add a fixme)
softDelete(deleteReason, tgs.filter(kv => kv.k !== "_delete_reason"))
});
return
} else {
// This is a 'non-delete'-option that was selected
softDelete(undefined, tgs)
if (deleteReasonMatch.length === 0) {
return;
}
const deleteAction = new DeleteAction(id,
options.softDeletionTags,
{
theme: State.state?.layoutToUse?.id ?? "unkown",
specialMotivation: deleteReasonMatch[0]?.v
},
deleteAbility.canBeDeleted.data.canBeDeleted
)
State.state.changes.applyAction(deleteAction)
isDeleted.setData(true)
}
@ -98,7 +83,7 @@ export default class DeleteWizard extends Toggle {
saveButtonConstr: (v) => DeleteWizard.constructConfirmButton(v).onClick(() => {
doDelete(v.data)
}),
bottomText: (v) => DeleteWizard.constructExplanation(v, deleteAction)
bottomText: (v) => DeleteWizard.constructExplanation(v, deleteAbility)
}
)
}))
@ -110,7 +95,7 @@ export default class DeleteWizard extends Toggle {
const deleteButton = new SubtleButton(
Svg.delete_icon_ui().SetStyle("width: 2rem; height: 2rem;"), t.delete.Clone()).onClick(
() => {
deleteAction.CheckDeleteability(true)
deleteAbility.CheckDeleteability(true)
confirm.setData(true);
}
).SetClass("w-1/2 float-right");
@ -132,13 +117,13 @@ export default class DeleteWizard extends Toggle {
deleteButton,
confirm),
new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))),
deleteAction.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
new VariableUiElement(deleteAbility.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))),
deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin),
State.state.osmConnection.isLoggedIn
),
deleteAction.isDeleted),
isDeleted),
undefined,
isShown)
@ -167,7 +152,7 @@ export default class DeleteWizard extends Toggle {
}
private static constructExplanation(tags: UIEventSource<TagsFilter>, deleteAction: DeleteAction) {
private static constructExplanation(tags: UIEventSource<TagsFilter>, deleteAction: DeleteabilityChecker) {
const t = Translations.t.delete;
return new VariableUiElement(tags.map(
currentTags => {
@ -263,4 +248,172 @@ export default class DeleteWizard extends Toggle {
)
}
}
class DeleteabilityChecker {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
private readonly _id: string;
private readonly _allowDeletionAtChangesetCount: number;
constructor(id: string,
allowDeletionAtChangesetCount?: number) {
this._id = id;
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE;
this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
canBeDeleted: undefined,
reason: Translations.t.delete.loading
})
this.CheckDeleteability(false)
}
/**
* Checks if the currently logged in user can delete the current point.
* State is written into this._canBeDeleted
* @constructor
* @private
*/
public CheckDeleteability(useTheInternet: boolean): void {
const t = Translations.t.delete;
const id = this._id;
const state = this.canBeDeleted
if (!id.startsWith("node")) {
this.canBeDeleted.setData({
canBeDeleted: false,
reason: t.isntAPoint
})
return;
}
// Does the currently logged in user have enough experience to delete this point?
const deletingPointsOfOtherAllowed = State.state.osmConnection.userDetails.map(ud => {
if (ud === undefined) {
return undefined;
}
if (!ud.loggedIn) {
return false;
}
return ud.csCount >= Math.min(Constants.userJourney.deletePointsOfOthersUnlock, this._allowDeletionAtChangesetCount);
})
const previousEditors = new UIEventSource<number[]>(undefined)
const allByMyself = previousEditors.map(previous => {
if (previous === null || previous === undefined) {
// Not yet downloaded
return null;
}
const userId = State.state.osmConnection.userDetails.data.uid;
return !previous.some(editor => editor !== userId)
}, [State.state.osmConnection.userDetails])
// User allowed OR only edited by self?
const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => {
if (isAllowed === undefined) {
// No logged in user => definitively not allowed to delete!
return false;
}
if (isAllowed === true) {
return true;
}
// At this point, the logged in user is not allowed to delete points created/edited by _others_
// however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
if (allByMyself.data === null && useTheInternet) {
// We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors)
}
if (allByMyself.data === true) {
// Yay! We can download!
return true;
}
if (allByMyself.data === false) {
// Nope, downloading not allowed...
return false;
}
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
return undefined;
}, [allByMyself])
const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null)
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
deletetionAllowed.addCallbackAndRunD(deletetionAllowed => {
if (deletetionAllowed === false) {
// Nope, we are not allowed to delete
state.setData({
canBeDeleted: false,
reason: t.notEnoughExperience
})
return true; // unregister this caller!
}
if (!useTheInternet) {
return;
}
// All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
OsmObject.DownloadReferencingRelations(id).then(rels => {
hasRelations.setData(rels.length > 0)
})
OsmObject.DownloadReferencingWays(id).then(ways => {
hasWays.setData(ways.length > 0)
})
return true; // unregister to only run once
})
const hasWaysOrRelations = hasRelations.map(hasRelationsData => {
if (hasRelationsData === true) {
return true;
}
if (hasWays.data === true) {
return true;
}
if (hasWays.data === null || hasRelationsData === null) {
return null;
}
if (hasWays.data === false && hasRelationsData === false) {
return false;
}
return null;
}, [hasWays])
hasWaysOrRelations.addCallbackAndRun(
waysOrRelations => {
if (waysOrRelations == null) {
// Not yet loaded - we still wait a little bit
return;
}
if (waysOrRelations) {
// not deleteble by mapcomplete
state.setData({
canBeDeleted: false,
reason: t.partOfOthers
})
} else {
// alright, this point can be safely deleted!
state.setData({
canBeDeleted: true,
reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete
})
}
}
)
}
}

157
UI/Popup/MultiApply.ts Normal file
View file

@ -0,0 +1,157 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import {Changes} from "../../Logic/Osm/Changes";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {Tag} from "../../Logic/Tags/Tag";
import {ElementStorage} from "../../Logic/ElementStorage";
import {And} from "../../Logic/Tags/And";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import Toggle from "../Input/Toggle";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
export interface MultiApplyParams {
featureIds: UIEventSource<string[]>,
keysToApply: string[],
text: string,
autoapply: boolean,
overwrite: boolean,
tagsSource: UIEventSource<any>,
state: {
changes: Changes,
allElements: ElementStorage,
layoutToUse: LayoutConfig,
osmConnection: OsmConnection
}
}
class MultiApplyExecutor {
private readonly originalValues = new Map<string, string>()
private readonly params: MultiApplyParams;
private constructor(params: MultiApplyParams) {
this.params = params;
const p = params
for (const key of p.keysToApply) {
this.originalValues.set(key, p.tagsSource.data[key])
}
if (p.autoapply) {
const self = this;
const relevantValues = p.tagsSource.map(tags => {
const currentValues = p.keysToApply.map(key => tags[key])
// By stringifying, we have a very clear ping when they changec
return JSON.stringify(currentValues);
})
relevantValues.addCallbackD(_ => {
self.applyTaggingOnOtherFeatures()
})
}
}
public applyTaggingOnOtherFeatures() {
console.log("Multi-applying changes...")
const featuresToChange = this.params.featureIds.data
const changes = this.params.state.changes
const allElements = this.params.state.allElements
const keysToChange = this.params.keysToApply
const overwrite = this.params.overwrite
const selfTags = this.params.tagsSource.data;
const theme = this.params.state.layoutToUse.id
for (const id of featuresToChange) {
const tagsToApply: Tag[] = []
const otherFeatureTags = allElements.getEventSourceById(id).data
for (const key of keysToChange) {
const newValue = selfTags[key]
if (newValue === undefined) {
continue
}
const otherValue = otherFeatureTags[key]
if (newValue === otherValue) {
continue;// No changes to be made
}
if (overwrite) {
tagsToApply.push(new Tag(key, newValue))
continue;
}
if (otherValue === undefined || otherValue === "" || otherValue === this.originalValues.get(key)) {
tagsToApply.push(new Tag(key, newValue))
}
}
if (tagsToApply.length == 0) {
continue;
}
changes.applyAction(
new ChangeTagAction(id, new And(tagsToApply), otherFeatureTags, {
theme,
changeType: "answer"
}))
}
}
private static executorCache = new Map<string, MultiApplyExecutor>()
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
if (MultiApplyExecutor.executorCache.has(id)) {
return MultiApplyExecutor.executorCache.get(id)
}
const applicator = new MultiApplyExecutor(params)
MultiApplyExecutor.executorCache.set(id, applicator)
return applicator
}
}
export default class MultiApply extends Toggle {
constructor(params: MultiApplyParams) {
const p = params
const t = Translations.t.multi_apply
const featureId = p.tagsSource.data.id
if (featureId === undefined) {
throw "MultiApply needs a feature id"
}
const applicator = MultiApplyExecutor.GetApplicator(featureId, params)
const elems: (string | BaseUIElement)[] = []
if (p.autoapply) {
elems.push(new FixedUiElement(p.text).SetClass("block"))
elems.push(new VariableUiElement(p.featureIds.map(featureIds =>
t.autoApply.Subs({
attr_names: p.keysToApply.join(", "),
count: "" + featureIds.length
}))).SetClass("block subtle text-sm"))
} else {
elems.push(
new SubtleButton(undefined, p.text).onClick(() => applicator.applyTaggingOnOtherFeatures())
)
}
const isShown: UIEventSource<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => {
return loggedIn && p.featureIds.data.length > 0
}, [p.featureIds])
super(new Combine(elems), undefined, isShown);
}
}

View file

@ -136,7 +136,9 @@ export default class SplitRoadWizard extends Toggle {
// Save button
const saveButton = new Button(t.split.Clone(), () => {
hasBeenSplit.setData(true)
State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates)))
State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), {
theme: State.state?.layoutToUse?.id
}))
})
saveButton.SetClass("btn btn-primary mr-3");

View file

@ -86,7 +86,10 @@ export default class TagRenderingQuestion extends Combine {
if (selection) {
(State.state?.changes ?? new Changes())
.applyAction(new ChangeTagAction(
tags.data.id, selection, tags.data
tags.data.id, selection, tags.data, {
theme: State.state?.layoutToUse?.id ?? "unkown",
changeType: "answer",
}
)).then(_ => {
console.log("Tagchanges applied")
})
@ -133,7 +136,7 @@ export default class TagRenderingQuestion extends Combine {
options.cancelButton,
saveButton,
bottomTags])
this.SetClass("question")
this.SetClass("question disable-links")
}

View file

@ -6,6 +6,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import State from "../../State";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class ShowDataLayer {
@ -28,9 +29,13 @@ export default class ShowDataLayer {
*/
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
private readonly showDataLayerid : number;
private static dataLayerIds = 0
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
this._leafletMap = options.leafletMap;
this.showDataLayerid = ShowDataLayer.dataLayerIds;
ShowDataLayer.dataLayerIds++
this._enablePopups = options.enablePopups ?? true;
if (options.features === undefined) {
throw "Invalid ShowDataLayer invocation"
@ -221,9 +226,8 @@ export default class ShowDataLayer {
let infobox: FeatureInfoBox = undefined;
const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this._cleanCount}`
popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type}</div>`)
const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}`
popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`)
leafletLayer.on("popupopen", () => {
if (infobox === undefined) {
const tags = State.state.allElements.getEventSourceById(feature.properties.id);

View file

@ -159,10 +159,8 @@ export class TileHierarchyAggregator implements FeatureSource {
const self = this
const empty = []
return new StaticFeatureSource(
locationControl.map(loc => {
const targetZoom = loc.zoom
if(targetZoom > clusteringConfig.maxZoom){
locationControl.map(loc => loc.zoom).map(targetZoom => {
if(targetZoom-1 > clusteringConfig.maxZoom){
return empty
}

View file

@ -26,8 +26,9 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
import Minimap from "./Base/Minimap";
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
import WikipediaBox from "./WikipediaBox";
import WikipediaBox from "./Wikipedia/WikipediaBox";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
import MultiApply from "./Popup/MultiApply";
export interface SpecialVisualization {
funcName: string,
@ -81,13 +82,16 @@ export default class SpecialVisualizations {
funcName: "image_carousel",
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
args: [{
name: "image key/prefix",
name: "image key/prefix (multiple values allowed if comma-seperated)",
defaultValue: "image",
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... "
}],
constr: (state: State, tags, args) => {
const imagePrefix = args[0];
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefix), tags);
let imagePrefixes = undefined;
if(args.length > 0){
imagePrefixes = args;
}
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags);
}
},
{
@ -97,9 +101,13 @@ export default class SpecialVisualizations {
name: "image-key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "image"
},{
name:"label",
doc:"The text to show on the button",
defaultValue: "Add image"
}],
constr: (state: State, tags, args) => {
return new ImageUploadFlow(tags, args[0])
return new ImageUploadFlow(tags, args[0], args[1])
}
},
{
@ -114,7 +122,16 @@ export default class SpecialVisualizations {
],
example: "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) =>
new WikipediaBox( tagsSource.map(tags => tags[args[0]]))
new VariableUiElement(
tagsSource.map(tags => tags[args[0]])
.map(wikidata => {
const wikidatas : string[] =
Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? [])
return new WikipediaBox(wikidatas)
})
)
},
{
funcName: "minimap",
@ -468,8 +485,49 @@ There are also some technicalities in your theme to keep in mind:
args[2], args[1], tagSource, rewrittenTags, lat, lon, Number(args[3]), state
)
}
},
{funcName: "multi_apply",
docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags",
args:[
{name: "feature_ids", doc: "A JSOn-serialized list of IDs of features to apply the tagging on"},
{name: "keys", doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." },
{name: "text", doc: "The text to show on the button"},
{name:"autoapply",doc:"A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown"},
{name:"overwrite",doc:"If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change"}
],
example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}",
constr: (state, tagsSource, args) => {
const featureIdsKey = args[0]
const keysToApply = args[1].split(";")
const text = args[2]
const autoapply = args[3]?.toLowerCase() === "true"
const overwrite = args[4]?.toLowerCase() === "true"
const featureIds : UIEventSource<string[]> = tagsSource.map(tags => {
const ids = tags[featureIdsKey]
try{
if(ids === undefined){
return []
}
return JSON.parse(ids);
}catch(e){
console.warn("Could not parse ", ids, "as JSON to extract IDS which should be shown on the map.")
return []
}
})
return new MultiApply(
{
featureIds,
keysToApply,
text,
autoapply,
overwrite,
tagsSource,
state
}
);
}
}
]
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();

View file

@ -0,0 +1,80 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import {Translation} from "../i18n/Translation";
import {FixedUiElement} from "../Base/FixedUiElement";
import Loading from "../Base/Loading";
import {Transform} from "stream";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import Img from "../Base/Img";
import {WikimediaImageProvider} from "../../Logic/ImageProviders/WikimediaImageProvider";
import Link from "../Base/Link";
import Svg from "../../Svg";
import BaseUIElement from "../BaseUIElement";
export default class WikidataPreviewBox extends VariableUiElement {
constructor(wikidataId : UIEventSource<string>) {
let inited = false;
const wikidata = wikidataId
.stabilized(250)
.bind(id => {
if (id === undefined || id === "" || id === "Q") {
return null;
}
inited = true;
return Wikidata.LoadWikidataEntry(id)
})
super(wikidata.map(maybeWikidata => {
if(maybeWikidata === null || !inited){
return undefined;
}
if(maybeWikidata === undefined){
return new Loading(Translations.t.general.loading)
}
if (maybeWikidata["error"] !== undefined) {
return new FixedUiElement(maybeWikidata["error"]).SetClass("alert")
}
const wikidata = <WikidataResponse> maybeWikidata["success"]
return WikidataPreviewBox.WikidataResponsePreview(wikidata)
}))
}
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement{
let link = new Link(
new Combine([
wikidata.id,
Svg.wikidata_ui().SetStyle("width: 2.5rem").SetClass("block")
]).SetClass("flex"),
Wikidata.IdToArticle(wikidata.id) ,true).SetClass("must-link")
let info = new Combine( [
new Combine([Translation.fromMap(wikidata.labels).SetClass("font-bold"),
link]).SetClass("flex justify-between"),
Translation.fromMap(wikidata.descriptions)
]).SetClass("flex flex-col link-underline")
let imageUrl = undefined
if(wikidata.claims.get("P18")?.size > 0){
imageUrl = Array.from(wikidata.claims.get("P18"))[0]
}
if(imageUrl){
imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url
info = new Combine([ new Img(imageUrl).SetStyle("max-width: 5rem; width: unset; height: 4rem").SetClass("rounded-xl mr-2"),
info.SetClass("w-full")]).SetClass("flex")
}
info.SetClass("p-2 w-full")
return info
}
}

View file

@ -0,0 +1,117 @@
import Combine from "../Base/Combine";
import {InputElement} from "../Input/InputElement";
import {TextField} from "../Input/TextField";
import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import Locale from "../i18n/Locale";
import {VariableUiElement} from "../Base/VariableUIElement";
import WikidataPreviewBox from "./WikidataPreviewBox";
import Title from "../Base/Title";
import WikipediaBox from "./WikipediaBox";
import Svg from "../../Svg";
export default class WikidataSearchBox extends InputElement<string> {
private readonly wikidataId: UIEventSource<string>
private readonly searchText: UIEventSource<string>
constructor(options?: {
searchText?: UIEventSource<string>,
value?: UIEventSource<string>
}) {
super();
this.searchText = options?.searchText
this.wikidataId = options?.value ?? new UIEventSource<string>(undefined);
}
GetValue(): UIEventSource<string> {
return this.wikidataId;
}
protected InnerConstructElement(): HTMLElement {
const searchField = new TextField({
placeholder: Translations.t.general.wikipedia.searchWikidata,
value: this.searchText,
inputStyle: "width: calc(100% - 0.5rem); border: 1px solid black"
})
const selectedWikidataId = this.wikidataId
const lastSearchResults = new UIEventSource<WikidataResponse[]>([])
const searchFailMessage = new UIEventSource(undefined)
searchField.GetValue().addCallbackAndRunD(searchText => {
if (searchText.length < 3) {
return;
}
searchFailMessage.setData(undefined)
lastSearchResults.WaitForPromise(
Wikidata.searchAndFetch(searchText, {
lang: Locale.language.data,
maxCount: 5
}
), err => searchFailMessage.setData(err))
})
const previews = new VariableUiElement(lastSearchResults.map(searchResults => {
if (searchFailMessage.data !== undefined) {
return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchFailMessage.data])
}
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")
el.onClick(() => {
selectedWikidataId.setData(wikidataresponse.id)
})
selectedWikidataId.addCallbackAndRunD(selected => {
if (selected === wikidataresponse.id) {
el.SetClass("subtle-background border-attention")
} else {
el.RemoveClass("subtle-background")
el.RemoveClass("border-attention")
}
})
return el;
})).SetClass("flex flex-col")
}, [searchFailMessage]))
//
const full = new Combine([
new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"),
new Combine([
Svg.search_ui().SetStyle("width: 1.5rem"),
searchField.SetClass("m-2 w-full")]).SetClass("flex"),
previews
]).SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
return new Combine([
new VariableUiElement(selectedWikidataId.map(wid => {
if (wid === undefined) {
return undefined
}
return new WikipediaBox(wid.split(";"));
})).SetStyle("max-height:12.5rem"),
full
]).ConstructElement();
}
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
IsValid(t: string): boolean {
return t.startsWith("Q") && !isNaN(Number(t.substring(1)));
}
}

View file

@ -0,0 +1,205 @@
import BaseUIElement from "../BaseUIElement";
import Locale from "../i18n/Locale";
import {VariableUiElement} from "../Base/VariableUIElement";
import {Translation} from "../i18n/Translation";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import Wikipedia from "../../Logic/Web/Wikipedia";
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import {TabbedComponent} from "../Base/TabbedComponent";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loading from "../Base/Loading";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import Link from "../Base/Link";
import WikidataPreviewBox from "./WikidataPreviewBox";
export default class WikipediaBox extends Combine {
constructor(wikidataIds: string[]) {
const mainContents = []
const pages = wikidataIds.map(wdId => WikipediaBox.createLinkedContent(wdId.trim()))
if (wikidataIds.length == 1) {
const page = pages[0]
mainContents.push(
new Combine([
new Combine([Svg.wikipedia_ui()
.SetStyle("width: 1.5rem").SetClass("inline-block mr-3"), page.titleElement])
.SetClass("flex"),
page.linkElement
]).SetClass("flex justify-between align-middle"),
)
mainContents.push(page.contents)
} else if (wikidataIds.length > 1) {
const tabbed = new TabbedComponent(
pages.map(page => {
const contents = page.contents.SetClass("block").SetStyle("max-height: inherit; height: inherit; padding-bottom: 3.3rem")
return {
header: page.titleElement.SetClass("pl-2 pr-2"),
content: new Combine([
page.linkElement
.SetStyle("top: 2rem; right: 2.5rem;")
.SetClass("absolute subtle-background rounded-full p-3 opacity-50 hover:opacity-100 transition-opacity"),
contents
]).SetStyle("max-height: inherit; height: inherit").SetClass("relative")
}
}),
0,
{
leftOfHeader: Svg.wikipedia_ui().SetStyle("width: 1.5rem; align-self: center;").SetClass("mr-4"),
styleHeader: header => header.SetClass("subtle-background").SetStyle("height: 3.3rem")
}
)
tabbed.SetStyle("height: inherit; max-height: inherit; overflow: hidden")
mainContents.push(tabbed)
}
super(mainContents)
this.SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col")
.SetStyle("max-height: inherit")
}
private static createLinkedContent(wikidataId: string): {
titleElement: BaseUIElement,
contents: BaseUIElement,
linkElement: BaseUIElement
} {
const wp = Translations.t.general.wikipedia;
const wikiLink: UIEventSource<[string, string, WikidataResponse] | "loading" | "failed" | ["no page", WikidataResponse]> =
Wikidata.LoadWikidataEntry(wikidataId)
.map(maybewikidata => {
if (maybewikidata === undefined) {
return "loading"
}
if (maybewikidata["error"] !== undefined) {
return "failed"
}
const wikidata = <WikidataResponse>maybewikidata["success"]
if(wikidata === undefined){
return "failed"
}
if (wikidata.wikisites.size === 0) {
return ["no page", wikidata]
}
const preferredLanguage = [Locale.language.data, "en", Array.from(wikidata.wikisites.keys())[0]]
let language
let pagetitle;
let i = 0
do {
language = preferredLanguage[i]
pagetitle = wikidata.wikisites.get(language)
i++;
} while (pagetitle === undefined)
return [pagetitle, language, wikidata]
}, [Locale.language])
const contents = new VariableUiElement(
wikiLink.map(status => {
if (status === "loading") {
return new Loading(wp.loading.Clone()).SetClass("pl-6 pt-2")
}
if (status === "failed") {
return wp.failed.Clone().SetClass("alert p-4")
}
if (status[0] == "no page") {
const [_, wd] = <[string, WikidataResponse]> status
return new Combine([
WikidataPreviewBox.WikidataResponsePreview(wd),
wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4")
}
const [pagetitle, language, wd] = <[string, string, WikidataResponse]> status
return WikipediaBox.createContents(pagetitle, language, wd)
})
).SetClass("overflow-auto normal-background rounded-lg")
const titleElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") {
const [pagetitle, _] = state
if(pagetitle === "no page"){
const wd = <WikidataResponse> state[1]
return new Title( Translation.fromMap(wd.labels),3)
}
return new Title(pagetitle, 3)
}
//return new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)
return new Title(wikidataId,3)
}))
const linkElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") {
const [pagetitle, language] = state
if(pagetitle === "no page"){
const wd = <WikidataResponse> state[1]
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "),
"https://www.wikidata.org/wiki/"+wd.id
, true)
}
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "), url, true)
}
return undefined
}))
.SetClass("flex items-center enable-links")
return {
contents: contents,
linkElement: linkElement,
titleElement: titleElement
}
}
/**
* Returns the actual content in a scrollable way
* @param pagename
* @param language
* @private
*/
private static createContents(pagename: string, language: string, wikidata: WikidataResponse): BaseUIElement {
const htmlContent = Wikipedia.GetArticle({
pageName: pagename,
language: language
})
const wp = Translations.t.general.wikipedia
const contents: UIEventSource<string | BaseUIElement> = htmlContent.map(htmlContent => {
if (htmlContent === undefined) {
// Still loading
return new Loading(wp.loading.Clone())
}
if (htmlContent["success"] !== undefined) {
return new FixedUiElement(htmlContent["success"]).SetClass("wikipedia-article")
}
if (htmlContent["error"]) {
console.warn("Loading wikipage failed due to", htmlContent["error"])
return wp.failed.Clone().SetClass("alert p-4")
}
return undefined
})
return new Combine([new VariableUiElement(contents)
.SetClass("block pl-6 pt-2")])
}
}

View file

@ -1,109 +0,0 @@
import {UIEventSource} from "../Logic/UIEventSource";
import {VariableUiElement} from "./Base/VariableUIElement";
import Wikipedia from "../Logic/Web/Wikipedia";
import Loading from "./Base/Loading";
import {FixedUiElement} from "./Base/FixedUiElement";
import Combine from "./Base/Combine";
import BaseUIElement from "./BaseUIElement";
import Title from "./Base/Title";
import Translations from "./i18n/Translations";
import Svg from "../Svg";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
import Locale from "./i18n/Locale";
import Toggle from "./Input/Toggle";
export default class WikipediaBox extends Toggle {
constructor(wikidataId: string | UIEventSource<string>) {
const wp = Translations.t.general.wikipedia;
if (typeof wikidataId === "string") {
wikidataId = new UIEventSource(wikidataId)
}
const wikibox = wikidataId
.bind(id => {
console.log("Wikidata is", id)
if(id === undefined){
return undefined
}
console.log("Initing load WIkidataentry with id", id)
return Wikidata.LoadWikidataEntry(id);
})
.map(maybewikidata => {
if (maybewikidata === undefined) {
return new Loading(wp.loading.Clone())
}
if (maybewikidata["error"] !== undefined) {
return wp.failed.Clone().SetClass("alert p-4")
}
const wikidata = <WikidataResponse>maybewikidata["success"]
console.log("Got wikidata response", wikidata)
if (wikidata.wikisites.size === 0) {
return wp.noWikipediaPage.Clone()
}
const preferredLanguage = [Locale.language.data, "en", Array.from(wikidata.wikisites.keys())[0]]
let language
let pagetitle;
let i = 0
do {
language = preferredLanguage[i]
pagetitle = wikidata.wikisites.get(language)
i++;
} while (pagetitle === undefined)
return WikipediaBox.createContents(pagetitle, language)
}, [Locale.language])
const contents = new VariableUiElement(
wikibox
).SetClass("overflow-auto normal-background rounded-lg")
const mainContent = new Combine([
new Combine([Svg.wikipedia_ui().SetStyle("width: 1.5rem").SetClass("mr-3"),
new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)]).SetClass("flex"),
contents]).SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col")
.SetStyle("max-height: inherit")
super(
mainContent,
undefined,
wikidataId.map(id => id !== undefined)
)
}
/**
* Returns the actual content in a scrollable way
* @param pagename
* @param language
* @private
*/
private static createContents(pagename: string, language: string): BaseUIElement {
const htmlContent = Wikipedia.GetArticle({
pageName: pagename,
language: language
})
const wp = Translations.t.general.wikipedia
const contents: UIEventSource<string | BaseUIElement> = htmlContent.map(htmlContent => {
if (htmlContent === undefined) {
// Still loading
return new Loading(wp.loading.Clone())
}
if (htmlContent["success"] !== undefined) {
return new FixedUiElement(htmlContent["success"]).SetClass("wikipedia-article")
}
if (htmlContent["error"]) {
console.warn("Loading wikipage failed due to", htmlContent["error"])
return wp.failed.Clone().SetClass("alert p-4")
}
return undefined
})
return new Combine([new VariableUiElement(contents).SetClass("block pl-6 pt-2")])
.SetClass("block")
}
}

View file

@ -214,4 +214,12 @@ export class Translation extends BaseUIElement {
}
return allTranslations
}
static fromMap(transl: Map<string, string>) {
const translations = {}
transl?.forEach((value, key) => {
translations[key] = value
})
return new Translation(translations);
}
}

View file

@ -89,6 +89,14 @@ export class Utils {
return ls;
}
public static Hist(array: string[]): Map<string, number>{
const hist = new Map<string, number>();
for (const s of array) {
hist.set(s, 1 + (hist.get(s) ?? 0))
}
return hist;
}
public static NoEmpty(array: string[]): string[] {
const ls: string[] = [];
for (const t of array) {

View file

@ -0,0 +1,169 @@
{
"id": "etymology",
"#": "A layer showing all objects having etymology info (either via `name:etymology:wikidata` or `name:etymology`. The intention is that this layer is reused for a certain category to also _ask_ for information",
"name": {
"en": "Has etymolgy",
"nl": "Heeft etymology info"
},
"minzoom": 12,
"source": {
"osmTags": {
"or": [
"name:etymology:wikidata~*",
"name:etymology~*"
]
}
},
"title": {
"render": {
"*": "{name}"
}
},
"description": {
"en": "All objects which have an etymology known",
"nl": "Alle lagen met een gelinkt etymology"
},
"calculatedTags": [
"_same_name_ids=feat.closestn('*', 250, undefined, 2500)?.filter(f => f.feat.properties.name === feat.properties.name)?.map(f => f.feat.properties.id)??[]"
],
"tagRenderings": [
{
"id": "etymology-images-from-wikipedia",
"render": {
"*": "{image_carousel(name:etymology:wikidata)}"
}
},
{
"id": "wikipedia-etymology",
"question": {
"en": "What is the Wikidata-item that this object is named after?",
"nl": "Wat is het Wikidata-item van hetgeen dit object is naar vernoemd?"
},
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": [
"steenweg",
"heirbaan",
"baan",
"straat",
"street",
"weg",
"dreef",
"laan",
"boulevard",
"pad",
"path",
"plein",
"square",
"plaza",
"wegel",
"kerk",
"church",
"kaai"
]
}
]
},
"render": {
"en": "<h3>Wikipedia article of the name giver</h3>{wikipedia(name:etymology:wikidata):max-height:20rem}",
"nl": "<h3>Wikipedia artikel van de naamgever</h3>{wikipedia(name:etymology:wikidata):max-height:20rem}"
},
"condition": "name:etymology!=unknown"
},
{
"id": "zoeken op inventaris onroerend erfgoed",
"render": {
"nl": "<a href='https://inventaris.onroerenderfgoed.be/erfgoedobjecten?tekst={name}' target='_blank'>Zoeken op inventaris onroerend erfgoed</a>",
"en": "<a href='https://inventaris.onroerenderfgoed.be/erfgoedobjecten?tekst={name}' target='_blank'>Search on inventaris onroerend erfgoed</a>"
},
"conditions": "_country=be"
},
{
"id": "simple etymology",
"question": {
"en": "What is this object named after?<br/><span class='subtle'>This might be written on the street name sign</span>",
"nl": "Naar wat is dit object vernoemd?<br/><span class='subtle'>Dit staat mogelijks vermeld op het straatnaambordje</subtle>"
},
"render": {
"en": "Named after {name:etymology}",
"nl": "Vernoemd naar {name:etymology}"
},
"freeform": {
"key": "name:etymology"
},
"mappings": [
{
"if": "name:etymology=unknown",
"then": {
"en": "The origin of this name is unknown in all literature",
"nl": "De oorsprong van deze naam is onbekend in de literatuur"
}
}
],
"condition": {
"or": [
"name:etymology~*",
"name:etymology:wikidata="
]
}
},
{
"id": "street-name-sign-image",
"render": {
"en": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}",
"nl": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
}
},
{
"id": "minimap",
"render": {
"*": "{minimap(18, id, _same_name_ids):height:10rem}"
}
},
{
"id": "etymology_multi_apply",
"render": {
"en": "{multi_apply(_same_name_ids, name:etymology:wikidata;name:etymology, Auto-applying data on all segments with the same name, true)}"
}
},
"wikipedia"
],
"icon": {
"render": "pin:#05d7fcaa;./assets/layers/etymology/logo.svg",
"mappings": [
{
"if": {
"and": [
"name:etymology=",
"name:etymology:wikidata="
]
},
"then": "pin:#fcca05aa;./assets/layers/etymology/logo.svg"
}
]
},
"width": {
"render": "8"
},
"iconSize": {
"render": "40,40,center"
},
"color": {
"render": "#05d7fcaa",
"mappings": [
{
"if": {
"and": [
"name:etymology=",
"name:etymology:wikidata="
]
},
"then": "#fcca05aa"
}
]
}
}

View file

@ -0,0 +1,10 @@
[
{
"path": "logo.svg",
"license": "CC0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
}
]

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="31.41128mm"
height="21.6535mm"
viewBox="0 0 31.41128 21.6535"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="logo.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="100.68267"
inkscape:cy="-27.941339"
inkscape:document-units="mm"
inkscape:current-layer="flowRoot10"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1003"
inkscape:window-x="862"
inkscape:window-y="1080"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-18.484101,-182.07744)">
<g
aria-label="ετυμο
λογία "
transform="matrix(0.21233122,0,0,0.21233122,6.7520733,38.096318)"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
id="flowRoot10">
<path
d="m 86.101172,696.96673 v 4.12 h -3.28 q -3.08,0 -4.4,0.68 -1.32,0.68 -1.32,2 0,1.08 1.2,1.76 1.2,0.68 4.04,0.68 2.44,0 4.56,-0.52 2.12,-0.52 3.48,-1.16 v 4.76 q -1.48,0.68 -3.64,1.12 -2.16,0.4 -4.84,0.4 -5.64,0 -8.16,-1.76 -2.52,-1.76 -2.52,-4.68 0,-2.4 1.44,-3.6 1.48,-1.2 3.88,-1.64 v -0.2 q -2.08,-0.48 -3.12,-1.76 -1.04,-1.32 -1.04,-3.28 0,-2.08 1.28,-3.32 1.28,-1.28 3.44,-1.84 2.16,-0.56 4.8,-0.56 2.28,0 4.56,0.44 2.28,0.44 3.96,1.2 l -1.84,4.32 q -1.44,-0.6 -3,-1.08 -1.52,-0.48 -3.52,-0.48 -4.36,0 -4.36,2.04 0,1.28 1.28,1.84 1.32,0.52 4.12,0.52 z"
style="font-weight:bold"
id="path28" />
<path
d="m 111.90111,688.56673 v 4.48 h -7.76 v 10.52 q 0,1.24 0.72,1.88 0.72,0.6 1.88,0.6 1,0 1.92,-0.2 0.92,-0.2 1.84,-0.48 v 4.44 q -0.88,0.4 -2.24,0.68 -1.32,0.32 -2.88,0.32 -2.04,0 -3.68,-0.64 -1.6,-0.64 -2.560002,-2.2 -0.96,-1.6 -0.96,-4.4 v -10.52 h -5.48 v -2.48 l 3.44,-2 z"
style="font-weight:bold"
id="path30" />
<path
d="m 126.26104,710.76673 q -3.92,0 -6.12,-1.36 -2.16,-1.4 -3.04,-3.72 -0.88,-2.36 -0.88,-5.24 v -11.88 h 5.96 v 12.08 q 0,2.84 1.04,4.12 1.04,1.24 3.24,1.24 2.36,0 3.52,-1.72 1.16,-1.72 1.16,-5.88 0,-2.6 -0.4,-4.88 -0.36,-2.32 -1,-4.96 h 6 q 0.68,2.6 1,4.92 0.36,2.28 0.36,5.08 0,6.28 -2.76,9.24 -2.72,2.96 -8.08,2.96 z"
style="font-weight:bold"
id="path32" />
<path
d="m 162.50098,688.56673 v 21.84 h -4.52 l -0.84,-2.92 h -0.28 q -0.8,1.64 -2,2.48 -1.16,0.84 -2.88,0.84 -2.44,0 -3.76,-1.76 h -0.12 q 0.08,0.4 0.12,1.28 0.04,0.84 0.04,1.76 0.04,0.96 0.04,1.68 v 6.24 h -5.96 v -31.44 h 5.96 v 12.76 q 0,4.72 3.56,4.72 2.68,0 3.68,-1.84 1,-1.88 1,-5.36 v -10.28 z"
style="font-weight:bold"
id="path34" />
<path
d="m 188.58097,699.44673 q 0,5.44 -2.88,8.4 -2.84,2.96 -7.76,2.96 -3.04,0 -5.44,-1.32 -2.36,-1.32 -3.72,-3.84 -1.36,-2.56 -1.36,-6.2 0,-5.44 2.84,-8.36 2.84,-2.92 7.8,-2.92 3.08,0 5.44,1.32 2.36,1.32 3.72,3.84 1.36,2.48 1.36,6.12 z m -15.08,0 q 0,3.24 1.04,4.92 1.08,1.64 3.48,1.64 2.36,0 3.4,-1.64 1.08,-1.68 1.08,-4.92 0,-3.24 -1.08,-4.84 -1.04,-1.64 -3.44,-1.64 -2.36,0 -3.44,1.64 -1.04,1.6 -1.04,4.84 z"
style="font-weight:bold"
id="path36" />
<path
d="m 69.861172,760.40673 9.24,-20.64 -0.68,-1.8 q -0.84,-2.08 -1.8,-2.64 -0.96,-0.56 -2.56,-0.56 -0.52,0 -1.08,0.08 -0.52,0.08 -0.92,0.16 v -4.92 q 0.56,-0.12 1.52,-0.2 1,-0.12 1.72,-0.12 2.56,0 4.16,0.84 1.64,0.8 2.68,2.36 1.04,1.56 1.84,3.8 l 5.48,15.08 q 0.92,2.48 1.68,3.24 0.76,0.72 1.6,0.72 0.56,0 1.36,-0.2 v 4.6 q -0.48,0.24 -1.6,0.4 -1.08,0.2 -1.84,0.2 -2.44,0 -3.72,-1.2 -1.24,-1.24 -1.96,-3.24 l -1.88,-5.32 q -0.44,-1.28 -0.84,-2.48 -0.4,-1.24 -0.6,-2.12 h -0.12 q -0.28,1.04 -0.68,2.28 -0.4,1.24 -0.8,2.2 l -4.04,9.48 z"
style="font-weight:bold"
id="path38" />
<path
d="m 116.86115,749.44673 q 0,5.44 -2.88,8.4 -2.84,2.96 -7.76,2.96 -3.04,0 -5.44,-1.32 -2.359996,-1.32 -3.719996,-3.84 -1.36,-2.56 -1.36,-6.2 0,-5.44 2.84,-8.36 2.839996,-2.92 7.799996,-2.92 3.08,0 5.44,1.32 2.36,1.32 3.72,3.84 1.36,2.48 1.36,6.12 z m -15.08,0 q 0,3.24 1.04,4.92 1.08,1.64 3.48,1.64 2.36,0 3.4,-1.64 1.08,-1.68 1.08,-4.92 0,-3.24 -1.08,-4.84 -1.04,-1.64 -3.44,-1.64 -2.36,0 -3.44,1.64 -1.04,1.6 -1.04,4.84 z"
style="font-weight:bold"
id="path40" />
<path
d="m 140.58111,738.56673 -8.08,21.48 q -0.6,1.6 -1.04,3.4 -0.44,1.8 -0.68,3.48 -0.2,1.72 -0.2,3.08 h -6.32 q 0,-1.12 0.24,-2.8 0.28,-1.68 0.72,-3.56 0.44,-1.84 1,-3.48 l -8.32,-21.6 h 6.2 l 3.2,9.64 q 0.36,1.04 0.76,2.52 0.44,1.44 0.76,2.8 0.32,1.32 0.44,2.04 h 0.12 q 0.08,-0.6 0.32,-1.76 0.28,-1.2 0.64,-2.6 0.4,-1.44 0.84,-2.72 l 3.2,-9.92 z"
style="font-weight:bold"
id="path42" />
<path
d="m 149.66107,738.56673 v 15 q 0,1.24 0.72,1.88 0.72,0.6 1.88,0.6 1,0 1.92,-0.2 0.92,-0.2 1.84,-0.48 v 4.44 q -0.88,0.4 -2.24,0.68 -1.32,0.32 -2.88,0.32 -2.04,0 -3.68,-0.64 -1.6,-0.64 -2.56,-2.2 -0.96,-1.6 -0.96,-4.4 v -15 z m -4.84,-2.4 v -0.6 q 0.32,-0.96 0.6,-2.2 0.32,-1.24 0.6,-2.52 0.28,-1.28 0.4,-2.28 h 5.52 v 0.48 q -0.64,1.56 -1.56,3.4 -0.92,1.84 -2.08,3.72 z"
style="font-weight:bold"
id="path44" />
<path
d="m 167.22105,760.80673 q -3.84,0 -6.2,-2.84 -2.36,-2.88 -2.36,-8.4 0,-5.6 2.44,-8.48 2.44,-2.88 6.56,-2.88 2.32,0 3.8,0.84 1.48,0.8 2.48,2.44 h 0.28 q 0.2,-0.68 0.52,-1.48 0.32,-0.84 0.8,-1.44 h 4.92 q -0.44,1.28 -0.92,3.72 -0.48,2.44 -0.48,5.12 v 6.08 q 0,1.36 0.48,1.84 0.52,0.48 1.16,0.48 0.28,0 0.64,-0.08 0.36,-0.08 0.52,-0.12 v 4.68 q -0.28,0.16 -1.16,0.32 -0.84,0.2 -1.52,0.2 -2,0 -3.24,-0.72 -1.24,-0.72 -1.92,-2.56 h -0.4 q -0.88,1.36 -2.4,2.32 -1.52,0.96 -4,0.96 z m 1.8,-4.76 q 2.48,0 3.44,-1.48 1,-1.52 1.04,-4.8 v -0.24 q 0,-3.2 -1,-4.88 -0.96,-1.68 -3.56,-1.68 -2.12,0 -3.16,1.72 -1.04,1.68 -1.04,4.92 0,6.44 4.28,6.44 z"
style="font-weight:bold"
id="path46" />
</g>
<g
style="font-style:normal;font-weight:normal;font-size:8.49324894px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.21233122"
id="text20" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -3,7 +3,25 @@
"render": "{image_carousel()}{image_upload()}"
},
"wikipedia": {
"render": "{wikipedia():max-height:25rem}"
"render": "{wikipedia():max-height:25rem}",
"question": {
"en": "What is the corresponding Wikidata entity?",
"nl": "Welk Wikidata-item komt overeen met dit object?"
},
"mappings": [
{
"if": "wikidata=",
"then": {
"en": "No Wikipedia page has been linked yet",
"nl": "Er werd nog geen Wikipedia-pagina gekoppeld"
},
"hideInAnswer": true
}
],
"freeform": {
"key": "wikidata",
"type": "wikidata"
}
},
"reviews": {
"render": "{reviews()}"
@ -44,7 +62,23 @@
},
"wikipedialink": {
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/wikipedia.svg' alt='WP'/></a>",
"condition": "wikipedia~*"
"question": {
"en": "What is the corresponding item on Wikipedia?",
"nl": "Welk Wikipedia-artikel beschrijft dit object?"
},
"mappings": [
{
"if": "wikidata=",
"then": {
"en": "Not linked with Wikipedia",
"nl": "Nog geen Wikipedia-artikel bekend"
}
}
],
"freeform": {
"key": "wikidata",
"type": "wikidata"
}
},
"email": {
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>",

View file

@ -16,9 +16,8 @@
"en",
"nl"
],
"hideFromOverview": true,
"maintainer": "",
"icon": "./assets/svg/bug.svg",
"icon": "./assets/layers/etymology/logo.svg",
"version": "0",
"startLat": 0,
"startLon": 0,
@ -26,62 +25,51 @@
"widenFactor": 2,
"socialImage": "",
"layers": [
"etymology",
{
"id": "has_a_name",
"name": {
"en": "Has etymolgy",
"nl": "Heeft etymology info"
},
"minzoom": 12,
"source": {
"osmTags": {
"or": [
"name:etymology:wikidata~*",
"name:etymology~*"
]
}
},
"title": {
"render": {
"*": "{name}"
}
},
"description": {
"en": "All objects which have an etymology known",
"nl": "Alle lagen met een gelinkt etymology"
},
"tagRenderings": [
{
"id": "simple etymology",
"render": {
"en": "Named after {name:etymology}",
"nl": "Vernoemd naar {name:etymology}"
},
"freeform": {
"key": "name:etymology"
}
"builtin": "etymology",
"override": {
"id": "streets_without_etymology",
"name": {
"en": "Streets without etymology information",
"nl": "Straten zonder etymologische informatie"
},
{
"id": "wikipedia-etymology",
"render": {
"*": "{wikipedia(name:etymology:wikidata):max-height:20rem}"
"minzoom": 18,
"source": {
"osmTags": {
"and": [
"name~*",
"highway~*",
"highway!=bus_stop"
]
}
}
],
"icon": {
"render": "./assets/svg/bug.svg"
},
"width": {
"render": "8"
},
"iconSize": {
"render": "40,40,center"
},
"color": {
"render": "#00f"
},
"presets": []
}
},
{
"builtin": "etymology",
"override": {
"id": "parks_and_forests_without_etymology",
"name": {
"en": "Parks and forests without etymology information",
"nl": "Parken en bossen zonder etymologische informatie"
},
"minzoom": 18,
"source": {
"osmTags": {
"and": [
"name~*",
{
"or": [
"leisure=park",
"landuse=forest"
]
}
]
}
}
}
}
],
"roamingRenderings": []
"hideFromOverview": true
}

View file

@ -32,7 +32,7 @@
"enablePdfDownload": true,
"enableDownload": true,
"hideFromOverview": true,
"#": "Disable clustering for this theme",
"#": "Disable clustering for this theme",
"clustering": {
"maxZoom": 0
},
@ -71,7 +71,6 @@
]
},
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson",
"geoJsonZoomLevel": 0,
"isOsmCache": "duplicate"
},
"minzoom": 1,

View file

@ -14,7 +14,7 @@
"nl": "Een kaart om toeristisch relevante info op aan te duiden"
},
"description": {
"nl": "Op deze kaart kan je info zien die relevant is voor toerisme, zoals:<br/><ul><li>Eetgelegenheden</li><li>Cafés en bars</li><li>(Fiets)oplaadpunten</li><li>Fietspompen, fietserverhuur en fietswinkels</li><li>Uitkijktorens</li><li>...</li></ul> Zie je fouten op de kaart? Dan kan je zelf makkelijk aanpasingen maken, die zichtbaar zijn voor iedereen. Hiervoor dien je een gratis OpenStreetMap account voor te maken."
"nl": "Op deze kaart kan je info zien die relevant is voor toerisme, zoals:<br/><ul><li>Eetgelegenheden</li><li>Cafés en bars</li><li>(Fiets)oplaadpunten</li><li>Fietspompen, fietserverhuur en fietswinkels</li><li>Uitkijktorens</li><li>...</li></ul> Zie je fouten op de kaart? Dan kan je zelf makkelijk aanpasingen maken, die zichtbaar zijn voor iedereen. Hiervoor dien je een gratis OpenStreetMap account voor te maken.<br/><br/>Met de steun van Toerisme Vlaanderen<img src='./assets/themes/toerisme_vlaanderen/logo.png' />"
},
"descriptionTail": {
"nl": "Met de steun van Toerisme Vlaanderen<img style='height:5rem; width: auto;' src='./assets/themes/toerisme_vlaanderen/logo.png' />"

View file

@ -21,19 +21,9 @@
"widenFactor": 1.01,
"socialImage": "",
"hideFromOverview": true,
"lockLocation": [
[
51.51818357322121,
-0.09293317794799805
],
[
51.52898437160955,
-0.08147478103637695
]
],
"clustering": {
"minNeededFeatures": 25,
"maxZoom": 17
"maxZoom": 16
},
"layers": [
{

View file

@ -828,6 +828,10 @@ video {
margin: 1rem;
}
.m-px {
margin: 1px;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
@ -852,10 +856,6 @@ video {
margin-left: 0.75rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
@ -920,6 +920,10 @@ video {
margin-bottom: 0.25rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.box-border {
box-sizing: border-box;
}
@ -1390,14 +1394,6 @@ video {
padding: 0px;
}
.pl-6 {
padding-left: 1.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.pl-2 {
padding-left: 0.5rem;
}
@ -1466,6 +1462,14 @@ video {
padding-right: 0.5rem;
}
.pl-6 {
padding-left: 1.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.text-center {
text-align: center;
}
@ -1590,6 +1594,10 @@ video {
opacity: 0;
}
.opacity-50 {
opacity: 0.5;
}
.opacity-40 {
opacity: 0.4;
}
@ -1632,6 +1640,18 @@ video {
transition-duration: 150ms;
}
.transition-colors {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.\!transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter !important;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important;
@ -1792,6 +1812,10 @@ svg, img {
height: 100%;
}
.no-images img {
display: none;
}
.mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important;
}
@ -1892,6 +1916,10 @@ li::marker {
border: 5px solid var(--catch-detail-color);
}
.border-attention {
border-color: var(--catch-detail-color);
}
.direction-svg svg path {
fill: var(--catch-detail-color) !important;
}
@ -1934,6 +1962,27 @@ li::marker {
max-width: 2em !important;
}
.block-ruby {
display: block ruby;
}
.disable-links a {
pointer-events: none;
text-decoration: none !important;
color: var(--subtle-detail-color-contrast) !important;
}
.enable-links a {
pointer-events: unset;
text-decoration: underline !important;
color: unset !important;
}
.disable-links a.must-link, .disable-links .must-link a {
/* Hide links if they are disabled */
display: none;
}
/**************** GENERIC ****************/
.alert {
@ -2193,6 +2242,10 @@ li::marker {
color: rgba(30, 64, 175, var(--tw-text-opacity));
}
.hover\:opacity-100:hover {
opacity: 1;
}
.hover\:shadow-xl:hover {
--tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
@ -2254,6 +2307,10 @@ li::marker {
justify-content: space-between;
}
.sm\:border-4 {
border-width: 4px;
}
.sm\:p-0\.5 {
padding: 0.125rem;
}
@ -2270,6 +2327,10 @@ li::marker {
padding: 0.25rem;
}
.sm\:p-2 {
padding: 0.5rem;
}
.sm\:pl-2 {
padding-left: 0.5rem;
}
@ -2373,6 +2434,10 @@ li::marker {
padding: 1rem;
}
.md\:p-3 {
padding: 0.75rem;
}
.md\:pt-4 {
padding-top: 1rem;
}

View file

@ -8,7 +8,7 @@
justify-content: flex-start;
align-items: start;
background-color: var(--background-color);
max-width: 100vw;
max-width: 100%;
overflow-x: auto;
}

View file

@ -20,12 +20,6 @@
height: 100%;
}
.question a {
pointer-events: none;
text-decoration: none;
color: var(--subtle-detail-color-contrast)
}
.question-text {
font-size: larger;
font-weight: bold;

View file

@ -33,6 +33,12 @@
text-decoration: none;
}
.disable-links .wikipedia-article a {
color: black !important;
background: none !important;
text-decoration: none;
}
.wikipedia-article p {
margin-bottom: 0.5rem;

View file

@ -93,6 +93,10 @@ svg, img {
height: 100%;
}
.no-images img {
display: none;
}
.mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important;
}
@ -191,6 +195,10 @@ li::marker {
border: 5px solid var(--catch-detail-color);
}
.border-attention {
border-color: var(--catch-detail-color);
}
.direction-svg svg path {
fill: var(--catch-detail-color) !important;
}
@ -235,6 +243,27 @@ li::marker {
}
.block-ruby {
display: block ruby;
}
.disable-links a {
pointer-events: none;
text-decoration: none !important;
color: var(--subtle-detail-color-contrast) !important;
}
.enable-links a {
pointer-events: unset;
text-decoration: underline !important;
color: unset !important;
}
.disable-links a.must-link, .disable-links .must-link a {
/* Hide links if they are disabled */
display: none;
}
/**************** GENERIC ****************/

View file

@ -223,7 +223,11 @@
"wikipediaboxTitle": "Wikipedia",
"failed": "Loading the wikipedia entry failed",
"loading": "Loading Wikipedia...",
"noWikipediaPage": "This wikidata item has no corresponding wikipedia page yet."
"noWikipediaPage": "This wikidata item has no corresponding wikipedia page yet.",
"searchWikidata": "Search on wikidata",
"noResults": "Nothing found for <i>{search}</i>",
"doSearch": "Search above to see results",
"createNewWikidata": "Create a new wikidata item"
}
},
"favourite": {
@ -247,6 +251,9 @@
"attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
"plz_login": "Login to leave a review"
},
"multi_apply": {
"autoApply": "When changing the attributes {attr_names}, these attributes will automatically be changed on {count} other objects too"
},
"move": {
"loginToMove": "You must be logged in to move a point",
"inviteToMove": "Move this point",

View file

@ -2500,6 +2500,34 @@
"render": "Drinking water"
}
},
"etymology": {
"description": "All objects which have an etymology known",
"name": "Has etymolgy",
"tagRenderings": {
"etymology_multi_apply": {
"render": "{multi_apply(_same_name_ids, name:etymology:wikidata;name:etymology, Auto-applying data on all segments with the same name, true)}"
},
"simple etymology": {
"mappings": {
"0": {
"then": "The origin of this name is unknown in all literature"
}
},
"question": "What is this object named after?<br/><span class='subtle'>This might be written on the street name sign</span>",
"render": "Named after {name:etymology}"
},
"street-name-sign-image": {
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}"
},
"wikipedia-etymology": {
"question": "What is the Wikidata-item that this object is named after?",
"render": "<h3>Wikipedia article of the name giver</h3>{wikipedia(name:etymology:wikidata):max-height:20rem}"
},
"zoeken op inventaris onroerend erfgoed": {
"render": "<a href='https://inventaris.onroerenderfgoed.be/erfgoedobjecten?tekst={name}' target='_blank'>Search on inventaris onroerend erfgoed</a>"
}
}
},
"food": {
"filter": {
"0": {

View file

@ -2446,6 +2446,31 @@
"render": "Drinkbaar water"
}
},
"etymology": {
"description": "Alle lagen met een gelinkt etymology",
"name": "Heeft etymology info",
"tagRenderings": {
"simple etymology": {
"mappings": {
"0": {
"then": "De oorsprong van deze naam is onbekend in de literatuur"
}
},
"question": "Naar wat is dit object vernoemd?<br/><span class='subtle'>Dit staat mogelijks vermeld op het straatnaambordje</subtle>",
"render": "Vernoemd naar {name:etymology}"
},
"street-name-sign-image": {
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
},
"wikipedia-etymology": {
"question": "Wat is het Wikidata-item van hetgeen dit object is naar vernoemd?",
"render": "<h3>Wikipedia artikel van de naamgever</h3>{wikipedia(name:etymology:wikidata):max-height:20rem}"
},
"zoeken op inventaris onroerend erfgoed": {
"render": "<a href='https://inventaris.onroerenderfgoed.be/erfgoedobjecten?tekst={name}' target='_blank'>Zoeken op inventaris onroerend erfgoed</a>"
}
}
},
"food": {
"filter": {
"0": {

View file

@ -78,6 +78,22 @@
}
},
"question": "Is this place accessible with a wheelchair?"
},
"wikipedia": {
"mappings": {
"0": {
"then": "No Wikipedia page has been linked yet"
}
},
"question": "What is the corresponding Wikidata entity?"
},
"wikipedialink": {
"mappings": {
"0": {
"then": "Not linked with Wikipedia"
}
},
"question": "What is the corresponding item on Wikipedia?"
}
}
}

View file

@ -78,6 +78,22 @@
}
},
"question": "Is deze plaats rolstoeltoegankelijk?"
},
"wikipedia": {
"mappings": {
"0": {
"then": "Er werd nog geen Wikipedia-pagina gekoppeld"
}
},
"question": "Welk Wikidata-item komt overeen met dit object?"
},
"wikipedialink": {
"mappings": {
"0": {
"then": "Nog geen Wikipedia-artikel bekend"
}
},
"question": "Welk Wikipedia-artikel beschrijft dit object?"
}
}
}

View file

@ -746,13 +746,9 @@
"etymology": {
"description": "On this map, you can see what an object is named after. The streets, buildings, ... come from OpenStreetMap which got linked with Wikidata. The information comes from Wpikipedia.",
"layers": {
"0": {
"description": "All objects which have an etymology known",
"name": "Has etymolgy",
"tagRenderings": {
"simple etymology": {
"render": "Named after {name:etymology}"
}
"1": {
"override": {
"name": "Streets without etymology information"
}
}
},
@ -1240,6 +1236,57 @@
"shortDescription": "A map with playgrounds",
"title": "Playgrounds"
},
"postboxes": {
"description": "On this map you can find and add data of post offices and post boxes. You can use this map to find where you can mail your next postcard! :)<br/>Spotted an error or is a post box missing? You can edit this map with a free OpenStreetMap account. ",
"layers": {
"0": {
"description": "The layer showing postboxes.",
"name": "Postboxes",
"presets": {
"0": {
"title": "postbox"
}
},
"title": {
"render": "Postbox"
}
},
"1": {
"description": "A layer showing post offices.",
"filter": {
"0": {
"options": {
"0": {
"question": "Currently open"
}
}
}
},
"name": "Post offices",
"presets": {
"0": {
"title": "Post Office"
}
},
"tagRenderings": {
"OH": {
"mappings": {
"0": {
"then": "24/7 opened (including holidays)"
}
},
"question": "What are the opening hours for this post office?",
"render": "Opening Hours: {opening_hours_table()}"
}
},
"title": {
"render": "Post Office"
}
}
},
"shortDescription": "A map showing postboxes and post offices",
"title": "Postbox and Post Office Map"
},
"shops": {
"description": "On this map, one can mark basic information about shops, add opening hours and phone numbers",
"layers": {
@ -1371,56 +1418,5 @@
"description": "On this map, you'll find waste baskets near you. If a waste basket is missing on this map, you can add it yourself",
"shortDescription": "A map with waste baskets",
"title": "Waste Basket"
},
"postboxes": {
"description": "On this map you can find and add data of post offices and post boxes. You can use this map to find where you can mail your next postcard! :)<br/>Spotted an error or is a post box missing? You can edit this map with a free OpenStreetMap account. ",
"layers": {
"0": {
"description": "The layer showing postboxes.",
"name": "Postboxes",
"presets": {
"0": {
"title": "postbox"
}
},
"title": {
"render": "Postbox"
}
},
"1": {
"description": "A layer showing post offices.",
"filter": {
"0": {
"options": {
"0": {
"question": "Currently open"
}
}
}
},
"name": "Post offices",
"presets": {
"0": {
"title": "Post Office"
}
},
"tagRenderings": {
"OH": {
"mappings": {
"0": {
"then": "24/7 opened (including holidays)"
}
},
"question": "What are the opening hours for this post office?",
"render": "Opening Hours: {opening_hours_table()}"
}
},
"title": {
"render": "Post Office"
}
}
},
"shortDescription": "A map showing postboxes and post offices",
"title": "Postbox and Post Office Map"
}
}

View file

@ -627,13 +627,9 @@
"etymology": {
"description": "Op deze kaart zie je waar een plaats naar is vernoemd. De straten, gebouwen, ... komen uit OpenStreetMap, waar een link naar Wikidata werd gelegd. De informatie komt uit wikipedia.",
"layers": {
"0": {
"description": "Alle lagen met een gelinkt etymology",
"name": "Heeft etymology info",
"tagRenderings": {
"simple etymology": {
"render": "Vernoemd naar {name:etymology}"
}
"1": {
"override": {
"name": "Straten zonder etymologische informatie"
}
}
},

View file

@ -219,7 +219,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations
}
// Lets save this tile!
const [z, x, y] = Tiles.tile_from_index(tile.tileIndex)
console.log("Writing tile ", z, x, y, layerId)
// console.log("Writing tile ", z, x, y, layerId)
const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z)
createdTiles.push(tile.tileIndex)
// This is the geojson file containing all features for this tile
@ -241,6 +241,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations
}
perX[key].push(y)
})
console.log("Written overview: ", path, "with ", createdTiles.length, "tiles")
writeFileSync(path, JSON.stringify(perX))
// And, if needed, to create a points-only layer

View file

@ -21,4 +21,5 @@ const feature = {
MinimapImplementation.initialize()
new MoveWizard(
feature,
State.state).AttachTo("maindiv")
State.state).AttachTo("maindiv")

View file

@ -0,0 +1,72 @@
import T from "./TestHelper";
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
import {UIEventSource} from "../Logic/UIEventSource";
export default class ImageProviderSpec extends T {
constructor() {
super("ImageProvider", [
["Search images", () => {
let i = 0
function expects(url, tags, providerName = undefined) {
tags.id = "test/"+i
i++
AllImageProviders.LoadImagesFor(new UIEventSource(tags)).addCallbackD(images => {
console.log("ImageProvider test", tags.id, "for", tags)
const img = images[0]
if(img === undefined){
throw "No image found"
}
T.equals(url, img.url, tags.id)
if(providerName){
T.equals(img.provider.constructor.name, providerName)
}
console.log("OK")
})
}
const muntpoort_expected = "https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABr%C3%BCgge-Muntpoort_6-29510-58192.jpg?width=500&height=400"
expects(
muntpoort_expected,
{ "wikimedia_commons":"File:Brügge-Muntpoort_6-29510-58192.jpg"
} , "WikimediaImageProvider")
expects(muntpoort_expected,
{ "wikimedia_commons":"https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg"
} , "WikimediaImageProvider")
expects(muntpoort_expected , {
"image":"https://upload.wikimedia.org/wikipedia/commons/c/cd/Br%C3%BCgge-Muntpoort_6-29510-58192.jpg"
} , "WikimediaImageProvider")
expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400" , {
"image":"File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg"
} , "WikimediaImageProvider")
expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABelgium-5955_-_Simon_Stevin_(13746657193).jpg?width=500&height=400" , {
"wikimedia_commons":"File:Belgium-5955_-_Simon_Stevin_(13746657193).jpg"
} , "WikimediaImageProvider")
expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3ABrugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg?width=500&height=400",{
image:"File:Brugge_Leeuwstraat_zonder_nummer_Leeuwbrug_-_119334_-_onroerenderfgoed.jpg"
}, "WikimediaImageProvider")
expects("https://commons.wikimedia.org/wiki/Special:FilePath/File%3APapageno_Jef_Claerhout.jpg?width=500&height=400",{
"wikimedia_commons": "File:Papageno_Jef_Claerhout.jpg"
}, "WikimediaImageProvider")
}]
]);
}
}

View file

@ -2,7 +2,6 @@ import T from "./TestHelper";
import {InPlaceReplacedmentRTSH} from "../Logic/Osm/Actions/RelationSplitHandler";
import {OsmObject, OsmRelation} from "../Logic/Osm/OsmObject";
import {Changes} from "../Logic/Osm/Changes";
import {equal} from "assert";
import {Utils} from "../Utils";
export default class RelationSplitHandlerSpec extends T {
@ -49,7 +48,7 @@ export default class RelationSplitHandlerSpec extends T {
allWayIdsInOrder: [295132739, -1],
originalNodes: originalNodeIds,
allWaysNodesInOrder: withSplit
})
},"no-theme")
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
const allIds = changeDescription[0].changes["members"].map(m => m.ref).join(",")
const expected = "687866206,295132739,-1,690497698"

View file

@ -191,7 +191,9 @@ export default class SplitActionSpec extends T {
// Lets split road https://www.openstreetmap.org/way/295132739
const id = "way/295132739"
const splitPoint: [number, number] = [3.246733546257019, 51.181710380278176]
const splitter = new SplitAction(id, [splitPoint])
const splitter = new SplitAction(id, [splitPoint], {
theme: "test"
})
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
equal(changeDescription[0].type, "node")
@ -235,7 +237,9 @@ export default class SplitActionSpec extends T {
const id = "way/61435323"
const splitPoint: [number, number] = [ 3.2021324336528774, 51.2170001600597]
const splitter = new SplitAction(id, [splitPoint])
const splitter = new SplitAction(id, [splitPoint], {
theme: "test"
})
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
// Should be a new node
@ -247,7 +251,9 @@ export default class SplitActionSpec extends T {
// Lets split road near an already existing point https://www.openstreetmap.org/way/295132739
const id = "way/295132739"
const splitPoint: [number, number] = [3.2451081275939937, 51.18116898253599]
const splitter = new SplitAction(id, [splitPoint])
const splitter = new SplitAction(id, [splitPoint], {
theme: "test"
})
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
equal(2, changeDescription.length)

View file

@ -11,6 +11,7 @@ import SplitActionSpec from "./SplitAction.spec";
import {Utils} from "../Utils";
import TileFreshnessCalculatorSpec from "./TileFreshnessCalculator.spec";
import WikidataSpecTest from "./Wikidata.spec.test";
import ImageProviderSpec from "./ImageProvider.spec";
ScriptUtils.fixUtils()
@ -25,7 +26,8 @@ const allTests = [
new RelationSplitHandlerSpec(),
new SplitActionSpec(),
new TileFreshnessCalculatorSpec(),
new WikidataSpecTest()
new WikidataSpecTest(),
new ImageProviderSpec()
]
Utils.externalDownloadFunction = async (url) => {
@ -44,16 +46,17 @@ args = args.map(a => a.toLowerCase())
const allFailures: { testsuite: string, name: string, msg: string } [] = []
let testsToRun = allTests
if (args.length > 0) {
testsToRun = allTests.filter(t => args.indexOf(t.name) >= 0)
args = args.map(a => a.toLowerCase())
testsToRun = allTests.filter(t => args.indexOf(t.name.toLowerCase()) >= 0)
}
if (testsToRun.length == 0) {
throw "No tests found"
throw "No tests found. Try one of "+allTests.map(t => t.name).join(", ")
}
for (let i = 0; i < testsToRun.length; i++) {
const test = testsToRun[i];
console.log(" Running test", i, "/", allTests.length, test.name)
console.log(" Running test", i, "/", testsToRun.length, test.name)
allFailures.push(...(test.Run() ?? []))
console.log("OK!")
}

View file

@ -40,6 +40,14 @@ export default class T {
throw "Expected true, but got false: " + msg
}
}
static equals(a, b, msg?){
if(a !== b){
throw "Not the same: "+(msg??"")+"\n" +
"Expcected: "+a+"\n" +
"Got : "+b
}
}
static isFalse(b: boolean, msg: string) {
if (b) {

File diff suppressed because one or more lines are too long