Merge branch 'alpha' into develop

This commit is contained in:
pietervdvn 2021-10-13 17:23:02 +02:00
commit e0e1bfbe00
91 changed files with 4982 additions and 1093 deletions

View file

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

View file

@ -63,6 +63,7 @@ export class AllKnownLayouts {
private static AllLayouts(): Map<string, LayoutConfig> { private static AllLayouts(): Map<string, LayoutConfig> {
const dict: Map<string, LayoutConfig> = new Map(); const dict: Map<string, LayoutConfig> = new Map();
for (const layoutConfigJson of known_themes.themes) { for (const layoutConfigJson of known_themes.themes) {
// @ts-ignore
const layout = new LayoutConfig(layoutConfigJson, true) const layout = new LayoutConfig(layoutConfigJson, true)
if (layout.id === "cyclofix") { 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 ### _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) 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 ### 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) 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 1. amount of features
2. unique tag key (optional) 2. unique tag key (optional)
3. maxDistanceInMeters (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 ## 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 ## int

View file

@ -14,7 +14,7 @@
name | default | description 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 #### 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 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) 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 #### Example usage
`{image_upload(image)}` `{image_upload(image,Add image)}`
### wikipedia ### wikipedia
A box showing the corresponding wikipedia article - based on the wikidata tag 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 #### 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.", "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": "" "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", "key": "bench",
"description": "The MapComplete theme Benches has a layer Benches at public transport stops showing features with this tag", "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", "key": "direction",
"description": "Layer 'Binoculars' shows and asks freeform values for key 'direction' (in the MapComplete.osm.be theme 'Binoculars')" "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", "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')", "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" "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", "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" "value": "1"
}, },
{ {
"key": "socket:schuko", "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", "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" "value": "1"
}, },
{ {
"key": "socket:typee", "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", "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" "value": "1"
}, },
{ {
"key": "socket:chademo", "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", "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" "value": "1"
}, },
{ {
"key": "socket:type1_cable", "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", "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" "value": "1"
}, },
{ {
"key": "socket:type1", "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", "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" "value": "1"
}, },
{ {
"key": "socket:type1_combo", "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", "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" "value": "1"
}, },
{ {
"key": "socket:tesla_supercharger", "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", "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" "value": "1"
}, },
{ {
"key": "socket:type2", "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", "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" "value": "1"
}, },
{ {
"key": "socket:type2_combo", "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", "key": "socket:schuko",
@ -195,7 +258,7 @@
}, },
{ {
"key": "socket:socket:schuko:voltage", "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" "value": "230 V"
}, },
{ {
@ -204,7 +267,7 @@
}, },
{ {
"key": "socket:socket:schuko:current", "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" "value": "16 A"
}, },
{ {
@ -213,7 +276,7 @@
}, },
{ {
"key": "socket:socket:schuko:output", "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" "value": "3.6 kw"
}, },
{ {
@ -226,7 +289,7 @@
}, },
{ {
"key": "socket:socket:typee:voltage", "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" "value": "230 V"
}, },
{ {
@ -235,7 +298,7 @@
}, },
{ {
"key": "socket:socket:typee:current", "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" "value": "16 A"
}, },
{ {
@ -244,12 +307,12 @@
}, },
{ {
"key": "socket:socket:typee:output", "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" "value": "3 kw"
}, },
{ {
"key": "socket:socket:typee:output", "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" "value": "22 kw"
}, },
{ {
@ -262,7 +325,7 @@
}, },
{ {
"key": "socket:socket:chademo:voltage", "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" "value": "500 V"
}, },
{ {
@ -271,7 +334,7 @@
}, },
{ {
"key": "socket:socket:chademo:current", "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" "value": "120 A"
}, },
{ {
@ -280,7 +343,7 @@
}, },
{ {
"key": "socket:socket:chademo:output", "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" "value": "50 kw"
}, },
{ {
@ -293,12 +356,12 @@
}, },
{ {
"key": "socket:socket:type1_cable:voltage", "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" "value": "200 V"
}, },
{ {
"key": "socket:socket:type1_cable:voltage", "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" "value": "240 V"
}, },
{ {
@ -307,7 +370,7 @@
}, },
{ {
"key": "socket:socket:type1_cable:current", "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" "value": "32 A"
}, },
{ {
@ -316,12 +379,12 @@
}, },
{ {
"key": "socket:socket:type1_cable:output", "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" "value": "3.7 kw"
}, },
{ {
"key": "socket:socket:type1_cable:output", "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" "value": "7 kw"
}, },
{ {
@ -334,12 +397,12 @@
}, },
{ {
"key": "socket:socket:type1:voltage", "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" "value": "200 V"
}, },
{ {
"key": "socket:socket:type1:voltage", "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" "value": "240 V"
}, },
{ {
@ -348,7 +411,7 @@
}, },
{ {
"key": "socket:socket:type1:current", "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" "value": "32 A"
}, },
{ {
@ -357,22 +420,22 @@
}, },
{ {
"key": "socket:socket:type1:output", "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" "value": "3.7 kw"
}, },
{ {
"key": "socket:socket:type1:output", "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" "value": "6.6 kw"
}, },
{ {
"key": "socket:socket:type1:output", "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" "value": "7 kw"
}, },
{ {
"key": "socket:socket:type1:output", "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" "value": "7.2 kw"
}, },
{ {
@ -385,12 +448,12 @@
}, },
{ {
"key": "socket:socket:type1_combo:voltage", "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" "value": "400 V"
}, },
{ {
"key": "socket:socket:type1_combo:voltage", "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" "value": "1000 V"
}, },
{ {
@ -399,12 +462,12 @@
}, },
{ {
"key": "socket:socket:type1_combo:current", "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" "value": "50 A"
}, },
{ {
"key": "socket:socket:type1_combo:current", "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" "value": "125 A"
}, },
{ {
@ -413,22 +476,22 @@
}, },
{ {
"key": "socket:socket:type1_combo:output", "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" "value": "50 kw"
}, },
{ {
"key": "socket:socket:type1_combo:output", "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" "value": "62.5 kw"
}, },
{ {
"key": "socket:socket:type1_combo:output", "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" "value": "150 kw"
}, },
{ {
"key": "socket:socket:type1_combo:output", "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" "value": "350 kw"
}, },
{ {
@ -441,7 +504,7 @@
}, },
{ {
"key": "socket:socket:tesla_supercharger:voltage", "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" "value": "480 V"
}, },
{ {
@ -450,12 +513,12 @@
}, },
{ {
"key": "socket:socket:tesla_supercharger:current", "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" "value": "125 A"
}, },
{ {
"key": "socket:socket:tesla_supercharger:current", "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" "value": "350 A"
}, },
{ {
@ -464,17 +527,17 @@
}, },
{ {
"key": "socket:socket:tesla_supercharger:output", "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" "value": "120 kw"
}, },
{ {
"key": "socket:socket:tesla_supercharger:output", "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" "value": "150 kw"
}, },
{ {
"key": "socket:socket:tesla_supercharger:output", "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" "value": "250 kw"
}, },
{ {
@ -487,12 +550,12 @@
}, },
{ {
"key": "socket:socket:type2:voltage", "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" "value": "230 V"
}, },
{ {
"key": "socket:socket:type2:voltage", "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" "value": "400 V"
}, },
{ {
@ -501,12 +564,12 @@
}, },
{ {
"key": "socket:socket:type2:current", "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" "value": "16 A"
}, },
{ {
"key": "socket:socket:type2:current", "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" "value": "32 A"
}, },
{ {
@ -515,12 +578,12 @@
}, },
{ {
"key": "socket:socket:type2:output", "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" "value": "11 kw"
}, },
{ {
"key": "socket:socket:type2:output", "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" "value": "22 kw"
}, },
{ {
@ -533,12 +596,12 @@
}, },
{ {
"key": "socket:socket:type2_combo:voltage", "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" "value": "500 V"
}, },
{ {
"key": "socket:socket:type2_combo:voltage", "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" "value": "920 V"
}, },
{ {
@ -547,12 +610,12 @@
}, },
{ {
"key": "socket:socket:type2_combo:current", "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" "value": "125 A"
}, },
{ {
"key": "socket:socket:type2_combo:current", "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" "value": "350 A"
}, },
{ {
@ -561,9 +624,261 @@
}, },
{ {
"key": "socket:socket:type2_combo:output", "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" "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", "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')", "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')", "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" "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", "key": "maxstay",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'maxstay' (in the MapComplete.osm.be theme 'Charging stations')" "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", "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" "value": "charging_station"
}, },
{ {
"key": "service:bicycle:cleaning:charge", "key": "operational_status",
"description": "Layer 'Charging stations' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (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') Picking this answer will delete the key operational_status.",
"value": ""
}, },
{ {
"key": "service:bicycle:cleaning:fee", "key": "parking: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')", "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&service:bicycle:cleaning:charge=" "value": "no"
}, },
{ {
"key": "service:bicycle:cleaning:fee", "key": "parking: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')", "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": "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')",
"value": "yes" "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", "description": "The MapComplete theme Cyclofix - an open map for cyclists has a layer Bike repair/shop showing features with this tag",
"value": "" "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", "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" "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", "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')", "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" "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')", "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" "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", "key": "amenity",
"description": "The MapComplete theme Friturenkaart has a layer Restaurants and fast food showing features with this tag", "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", "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')", "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" "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", "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" "value": "no"
}, },
{ {

View file

@ -428,16 +428,16 @@
}, },
{ {
"key": "operator", "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" "value": "Natuurpunt"
}, },
{ {
"key": "operator", "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", "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" "value": "Agentschap Natuur en Bos"
}, },
{ {
@ -497,6 +497,15 @@
"key": "description:0", "key": "description:0",
"description": "Layer 'Natuurgebied' shows and asks freeform values for key 'description:0' (in the MapComplete.osm.be theme 'De Natuur in')" "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", "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')" "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')", "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" "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", "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')", "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" "value": "no"
}, },
{ {
"key": "service:bicycle:cleaning:charge", "key": "wikidata",
"description": "Layer 'Observation towers' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Observation towers')" "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", "key": "wikidata",
"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')", "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": "no&service:bicycle:cleaning:charge=" "value": ""
},
{
"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"
} }
] ]
} }

View file

@ -12,111 +12,34 @@
"tags": [ "tags": [
{ {
"key": "amenity", "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" "value": "parking"
}, },
{ {
"key": "amenity", "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" "value": "motorcycle_parking"
}, },
{ {
"key": "amenity", "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" "value": "bicycle_parking"
}, },
{ {
"key": "image", "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", "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", "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", "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" "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')"
} }
] ]
} }

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" "value": "dedicated_room"
}, },
{ {
"key": "service:bicycle:cleaning:charge", "key": "toilets:handwashing",
"description": "Layer 'Toilets' shows and asks freeform values for key 'service:bicycle:cleaning:charge' (in the MapComplete.osm.be theme 'Open Toilet Map')" "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')",
},
{
"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')",
"value": "yes" "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", 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)", 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) => { (params, feature) => {
return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat 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. " + 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" + "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)", "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) => { (params, feature) => {

View file

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

View file

@ -16,35 +16,35 @@ export default class AllImageProviders {
Mapillary.singleton, Mapillary.singleton,
WikidataImageProvider.singleton, WikidataImageProvider.singleton,
WikimediaImageProvider.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[]>>() private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
public static LoadImagesFor(tags: UIEventSource<any>, imagePrefix?: string): UIEventSource<ProvidedImage[]> { public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string): UIEventSource<ProvidedImage[]> {
const id = tags.data.id if (tags.data.id === undefined) {
if (id === undefined) {
return 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) { if (cached !== undefined) {
return cached return cached
} }
const source = new UIEventSource([]) const source = new UIEventSource([])
this._cache.set(id, source) this._cache.set(cacheKey, source)
const allSources = [] const allSources = []
for (const imageProvider of AllImageProviders.ImageAttributionSource) { for (const imageProvider of AllImageProviders.ImageAttributionSource) {
let prefixes = imageProvider.defaultKeyPrefixes let prefixes = imageProvider.defaultKeyPrefixes
if(imagePrefix !== undefined){ if(tagKey !== undefined){
prefixes = [...prefixes] prefixes = [tagKey]
if(prefixes.indexOf("image") >= 0){
prefixes.splice(prefixes.indexOf("image"), 1)
}
prefixes.push(imagePrefix)
} }
const singleSource = imageProvider.GetRelevantUrls(tags, { 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))) { if (this._valuePrefixBlacklist.some(prefix => value.startsWith(prefix))) {
return [] return []
} }
try{
new URL(value)
}catch (_){
// Not a valid URL
return []
}
return [Promise.resolve({ return [Promise.resolve({
key: key, key: key,
url: value, url: value,

View file

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

View file

@ -34,9 +34,17 @@ export class WikidataImageProvider extends ImageProvider {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img) const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
allImages.push(...promises) 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 const commons = entity.commons
if (commons !== undefined) { if (commons !== undefined && (commons.startsWith("Category:") || commons.startsWith("File:"))) {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined , commons) const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined , commons)
allImages.push(...promises) allImages.push(...promises)
} }

View file

@ -4,6 +4,7 @@ import Svg from "../../Svg";
import Link from "../../UI/Base/Link"; import Link from "../../UI/Base/Link";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {LicenseInfo} from "./LicenseInfo"; import {LicenseInfo} from "./LicenseInfo";
import Wikimedia from "../Web/Wikimedia";
/** /**
* This module provides endpoints for wikimedia and others * This module provides endpoints for wikimedia and others
@ -14,56 +15,12 @@ export class WikimediaImageProvider extends ImageProvider {
private readonly commons_key = "wikimedia_commons" private readonly commons_key = "wikimedia_commons"
public readonly defaultKeyPrefixes = [this.commons_key,"image"] public readonly defaultKeyPrefixes = [this.commons_key,"image"]
public static readonly singleton = new WikimediaImageProvider(); 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() { private constructor() {
super(); 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) { private static ExtractFileName(url: string) {
if (!url.startsWith("http")) { if (!url.startsWith("http")) {
return url; 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/")) { if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
return value; return value;
@ -108,12 +65,26 @@ export class WikimediaImageProvider extends ImageProvider {
"&format=json&origin=*"; "&format=json&origin=*";
const data = await Utils.downloadJson(url) const data = await Utils.downloadJson(url)
const licenseInfo = new LicenseInfo(); 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) { 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; 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.artist = license.Artist?.value;
licenseInfo.license = license.License?.value; licenseInfo.license = license.License?.value;
licenseInfo.copyrighted = license.Copyrighted?.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:")) { if (!image.startsWith("File:")) {
image = "File:" + image 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>[]> { 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 [] return []
} }
if (value.startsWith(WikimediaImageProvider.commonsPrefix)) { value = WikimediaImageProvider.removeCommonsPrefix(value)
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)]
}
if (value.startsWith("Category:")) { if (value.startsWith("Category:")) {
const urls = await WikimediaImageProvider.GetImagesInCategory(value) const urls = await Wikimedia.GetCategoryContents(value)
return urls.map(image => this.UrlForImage(image)) return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
} }
if (value.startsWith("File:")) { if (value.startsWith("File:")) {
return [this.UrlForImage(value)] return [Promise.resolve(this.UrlForImage(value))]
} }
if (value.startsWith("http")) { if (value.startsWith("http")) {
// PRobably an error // PRobably an error
return [] return []
} }
// We do a last effort and assume this is a file // 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){ if(metatag.isLazy){
somethingChanged = true; somethingChanged = true;
metatag.applyMetaTagsOnFeature(feature, freshness) metatag.applyMetaTagsOnFeature(feature, freshness, layer)
}else{ }else{
const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness) const newValueAdded = metatag.applyMetaTagsOnFeature(feature, freshness, layer)
/* Note that the expression: /* Note that the expression:
* `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)` * `somethingChanged = newValueAdded || metatag.applyMetaTagsOnFeature(feature, freshness)`
* Is WRONG * Is WRONG

View file

@ -5,6 +5,24 @@ import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
*/ */
export interface ChangeDescription { 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 * Identifier of the object
*/ */

View file

@ -7,12 +7,17 @@ export default class ChangeTagAction extends OsmChangeAction {
private readonly _elementId: string; private readonly _elementId: string;
private readonly _tagsFilter: TagsFilter; private readonly _tagsFilter: TagsFilter;
private readonly _currentTags: any; 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(); super();
this._elementId = elementId; this._elementId = elementId;
this._tagsFilter = tagsFilter; this._tagsFilter = tagsFilter;
this._currentTags = currentTags; this._currentTags = currentTags;
this._meta = meta;
} }
/** /**
@ -43,10 +48,10 @@ export default class ChangeTagAction extends OsmChangeAction {
const type = typeId[0] const type = typeId[0]
const id = Number(typeId [1]) const id = Number(typeId [1])
return [{ return [{
// @ts-ignore type: <"node"|"way"|"relation"> type,
type: type,
id: id, 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 _lon: number;
private readonly _snapOnto: OsmWay; private readonly _snapOnto: OsmWay;
private readonly _reusePointDistance: number; 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() super()
this._basicTags = basicTags; this._basicTags = basicTags;
this._lat = lat; this._lat = lat;
@ -25,6 +31,10 @@ export default class CreateNewNodeAction extends OsmChangeAction {
} }
this._snapOnto = options?.snapOnto; this._snapOnto = options?.snapOnto;
this._reusePointDistance = options?.reusePointWithinMeters ?? 1 this._reusePointDistance = options?.reusePointWithinMeters ?? 1
this.meta = {
theme: options.theme,
changeType: options.changeType
}
} }
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
@ -47,7 +57,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: { changes: {
lat: this._lat, lat: this._lat,
lon: this._lon lon: this._lon
} },
meta: this.meta
} }
if (this._snapOnto === undefined) { if (this._snapOnto === undefined) {
return [newPointChange] return [newPointChange]
@ -78,7 +89,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
return [{ return [{
tags: new And(this._basicTags).asChange(properties), tags: new And(this._basicTags).asChange(properties),
type: "node", type: "node",
id: reusedPointId id: reusedPointId,
meta: this.meta
}] }]
} }
@ -99,7 +111,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: { changes: {
coordinates: locations, coordinates: locations,
nodes: ids 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 State from "../../../State";
import {OsmObject} from "../OsmObject"; import {OsmObject} from "../OsmObject";
import Translations from "../../../UI/i18n/Translations"; import OsmChangeAction from "./OsmChangeAction";
import Constants from "../../../Models/Constants"; 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 }>; private readonly _softDeletionTags: TagsFilter;
public readonly isDeleted = new UIEventSource<boolean>(false); private readonly meta: {
theme: string,
specialMotivation: string,
changeType: "deletion"
};
private readonly _id: string; 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._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[]> {
/** const osmObject = await OsmObject.DownloadObjectAsync(this._id)
* Does actually delete the feature; returns the event source 'this.isDeleted'
* If deletion is not allowed, triggers the callback instead if (this._hardDelete) {
*/ return [{
public DoDelete(reason: string, onNotAllowed: () => void): void { meta: this.meta,
const isDeleted = this.isDeleted doDelete: true,
const self = this; type: osmObject.type,
let deletionStarted = false; id: osmObject.id,
this.canBeDeleted.addCallbackAndRun( }]
canBeDeleted => { } else {
if (isDeleted.data || deletionStarted) { return await new ChangeTagAction(
// Already deleted... this._id, this._softDeletionTags, osmObject.tags,
return; {
theme: State.state?.layoutToUse?.id ?? "unkown",
changeType: "soft-delete"
} }
).CreateChangeDescriptions(changes)
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;
} }
// 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 { export default class RelationSplitHandler extends OsmChangeAction {
private readonly _input: RelationSplitInput; private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) { constructor(input: RelationSplitInput, theme: string) {
super() super()
this._input = input; this._input = input;
this._theme = theme;
} }
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { 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 { export class InPlaceReplacedmentRTSH extends OsmChangeAction {
private readonly _input: RelationSplitInput; private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) { constructor(input: RelationSplitInput, theme: string) {
super(); super();
this._input = input; this._input = input;
this._theme = theme;
} }
/** /**
@ -137,7 +141,11 @@ export class InPlaceReplacedmentRTSH extends OsmChangeAction {
return [{ return [{
id: relation.id, id: relation.id,
type: "relation", 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 { export default class SplitAction extends OsmChangeAction {
private readonly wayId: string; private readonly wayId: string;
private readonly _splitPointsCoordinates: [number, number] []// lon, lat private readonly _splitPointsCoordinates: [number, number] []// lon, lat
private _meta: { theme: string, changeType: "split" };
/** /**
* *
* @param wayId * @param wayId
* @param splitPointCoordinates: lon, lat * @param splitPointCoordinates: lon, lat
* @param meta
*/ */
constructor(wayId: string, splitPointCoordinates: [number, number][]) { constructor(wayId: string, splitPointCoordinates: [number, number][], meta: {theme: string}) {
super() super()
this.wayId = wayId; this.wayId = wayId;
this._splitPointsCoordinates = splitPointCoordinates this._splitPointsCoordinates = splitPointCoordinates
this._meta = {...meta, changeType: "split"};
} }
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
@ -89,7 +92,8 @@ export default class SplitAction extends OsmChangeAction {
changes: { changes: {
lon: element.lngLat[0], lon: element.lngLat[0],
lat: element.lngLat[1] lat: element.lngLat[1]
} },
meta: this._meta
}) })
} }
@ -110,7 +114,8 @@ export default class SplitAction extends OsmChangeAction {
changes: { changes: {
coordinates: wayPart.map(p => p.lngLat), coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds nodes: nodeIds
} },
meta: this._meta
}) })
allWayIdsInOrder.push(originalElement.id) allWayIdsInOrder.push(originalElement.id)
allWaysNodesInOrder.push(nodeIds) allWaysNodesInOrder.push(nodeIds)
@ -135,7 +140,8 @@ export default class SplitAction extends OsmChangeAction {
changes: { changes: {
coordinates: wayPart.map(p => p.lngLat), coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds nodes: nodeIds
} },
meta: this._meta
}) })
allWayIdsInOrder.push(id) allWayIdsInOrder.push(id)
@ -152,8 +158,8 @@ export default class SplitAction extends OsmChangeAction {
allWayIdsInOrder: allWayIdsInOrder, allWayIdsInOrder: allWayIdsInOrder,
originalNodes: originalNodes, originalNodes: originalNodes,
allWaysNodesInOrder: allWaysNodesInOrder, allWaysNodesInOrder: allWaysNodesInOrder,
originalWayId: originalElement.id originalWayId: originalElement.id,
}).CreateChangeDescriptions(changes) }, this._meta.theme).CreateChangeDescriptions(changes)
changeDescription.push(...changDescrs) changeDescription.push(...changDescrs)
} }
@ -240,7 +246,6 @@ export default class SplitAction extends OsmChangeAction {
closest = prevPoint closest = prevPoint
} }
// Ok, we have a closest point! // Ok, we have a closest point!
if(closest.originalIndex === 0 || closest.originalIndex === originalPoints.length){ if(closest.originalIndex === 0 || closest.originalIndex === originalPoints.length){
// We can not split on the first or last points... // We can not split on the first or last points...
continue continue

View file

@ -64,9 +64,9 @@ export class Changes {
if (deletedElements.length > 0) { if (deletedElements.length > 0) {
changes += changes +=
"\n<deleted>\n" + "\n<delete>\n" +
deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + deletedElements.map(e => e.ChangesetXML(csId)).join("\n") +
"\n</deleted>" "\n</delete>"
} }
changes += "</osmChange>"; changes += "</osmChange>";
@ -99,7 +99,7 @@ export class Changes {
} }
this.isUploading.setData(true) this.isUploading.setData(true)
this.flushChangesAsync(flushreason) this.flushChangesAsync()
.then(_ => { .then(_ => {
this.isUploading.setData(false) this.isUploading.setData(false)
console.log("Changes flushed!"); 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; const self = this;
try { try {
console.log("Beginning upload... " + flushreason ?? "");
// At last, we build the changeset and upload // At last, we build the changeset and upload
const pending = self.pendingChanges.data; const pending = self.pendingChanges.data;
const neededIds = Changes.GetNeededIds(pending)
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id))); const pendingPerTheme = new Map<string, ChangeDescription[]>()
console.log("Got the fresh objects!", osmObjects, "pending: ", pending) for (const changeDescription of pending) {
const changes: { const theme = changeDescription.meta.theme
newObjects: OsmObject[], if(!pendingPerTheme.has(theme)){
modifiedObjects: OsmObject[] pendingPerTheme.set(theme, [])
deletedObjects: OsmObject[] }
} = self.CreateChangesetObjects(pending, osmObjects) pendingPerTheme.get(theme).push(changeDescription)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { }
console.log("No changes to be made")
self.pendingChanges.setData([]) const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value)
self.isUploading.setData(false) .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) { } catch (e) {
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e) console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
self.pendingChanges.setData([]) self.pendingChanges.setData([])
}finally {
self.isUploading.setData(false) self.isUploading.setData(false)
} }

View file

@ -6,29 +6,47 @@ import {ElementStorage} from "../ElementStorage";
import State from "../../State"; import State from "../../State";
import Locale from "../../UI/i18n/Locale"; import Locale from "../../UI/i18n/Locale";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import {OsmObject} from "./OsmObject";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "./Changes"; import {Changes} from "./Changes";
import {Utils} from "../../Utils";
export interface ChangesetTag {
key: string,
value: string | number,
aggregate?: boolean
}
export class ChangesetHandler { export class ChangesetHandler {
public readonly currentChangeset: UIEventSource<string>; public readonly currentChangeset: UIEventSource<number>;
private readonly allElements: ElementStorage; private readonly allElements: ElementStorage;
private osmConnection: OsmConnection;
private readonly changes: Changes; private readonly changes: Changes;
private readonly _dryRun: boolean; private readonly _dryRun: boolean;
private readonly userDetails: UIEventSource<UserDetails>; private readonly userDetails: UIEventSource<UserDetails>;
private readonly auth: any; private readonly auth: any;
private readonly backend: string;
constructor(layoutName: string, dryRun: boolean, osmConnection: OsmConnection, constructor(layoutName: string, dryRun: boolean,
osmConnection: OsmConnection,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
auth) { auth) {
this.osmConnection = osmConnection;
this.allElements = allElements; this.allElements = allElements;
this.changes = changes; this.changes = changes;
this._dryRun = dryRun; this._dryRun = dryRun;
this.userDetails = osmConnection.userDetails; this.userDetails = osmConnection.userDetails;
this.backend = osmConnection._oauth_config.url
this.auth = auth; 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) { if (dryRun) {
console.log("DRYRUN ENABLED"); console.log("DRYRUN ENABLED");
@ -39,7 +57,7 @@ export class ChangesetHandler {
const oldId = parseInt(node.attributes.old_id.value); const oldId = parseInt(node.attributes.old_id.value);
if (node.attributes.new_id === undefined) { if (node.attributes.new_id === undefined) {
// We just removed this point! // We just removed this point!
const element =this. allElements.getEventSourceById("node/" + oldId); const element = this.allElements.getEventSourceById("node/" + oldId);
element.data._deleted = "yes" element.data._deleted = "yes"
element.ping(); element.ping();
return; return;
@ -56,6 +74,10 @@ export class ChangesetHandler {
} }
console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId); console.log("Rewriting id: ", type + "/" + oldId, "-->", type + "/" + newId);
const element = this.allElements.getEventSourceById("node/" + oldId); 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; element.data.id = type + "/" + newId;
this.allElements.addElementById(type + "/" + newId, element); this.allElements.addElementById(type + "/" + newId, element);
this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId)) this.allElements.ContainingFeatures.set(type + "/" + newId, this.allElements.ContainingFeatures.get(type + "/" + oldId))
@ -83,7 +105,7 @@ export class ChangesetHandler {
} }
} }
this.changes.registerIdRewrites(mappings) this.changes.registerIdRewrites(mappings)
} }
/** /**
@ -97,102 +119,96 @@ export class ChangesetHandler {
* *
*/ */
public async UploadChangeset( public async UploadChangeset(
layout: LayoutConfig, generateChangeXML: (csid: number) => string,
generateChangeXML: (csid: string) => string): Promise<void> { 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) { if (this.userDetails.data.csCount == 0) {
// The user became a contributor! // The user became a contributor!
this.userDetails.data.csCount = 1; this.userDetails.data.csCount = 1;
this.userDetails.ping(); this.userDetails.ping();
} }
if (this._dryRun) { if (this._dryRun) {
const changesetXML = generateChangeXML("123456"); const changesetXML = generateChangeXML(123456);
console.log(changesetXML); console.log(changesetXML);
return; return;
} }
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") { if (this.currentChangeset.data === undefined) {
// We have to open a new changeset // We have to open a new changeset
try { try {
const csId = await this.OpenChangeset(layout) const csId = await this.OpenChangeset(extraMetaTags)
this.currentChangeset.setData(csId); this.currentChangeset.setData(csId);
const changeset = generateChangeXML(csId); const changeset = generateChangeXML(csId);
console.log("Current changeset is:", changeset); console.log("Current changeset is:", changeset);
await this.AddChange(csId, changeset) await this.AddChange(csId, changeset)
} catch (e) { } catch (e) {
console.error("Could not open/upload changeset due to ", e) console.error("Could not open/upload changeset due to ", e)
this.currentChangeset.setData("") this.currentChangeset.setData(undefined)
} }
} else { } else {
// There still exists an open changeset (or at least we hope so) // There still exists an open changeset (or at least we hope so)
// Let's check!
const csId = this.currentChangeset.data; const csId = this.currentChangeset.data;
try { 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( await this.AddChange(
csId, csId,
generateChangeXML(csId)) generateChangeXML(csId))
} catch (e) { } catch (e) {
console.warn("Could not upload, changeset is probably closed: ", e); console.warn("Could not upload, changeset is probably closed: ", e);
// Mark the CS as closed... this.currentChangeset.setData(undefined);
this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist
await this.UploadChangeset(layout, generateChangeXML)
} }
} }
} }
/** private async CloseChangeset(changesetId: number = undefined): Promise<void> {
* 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> {
const self = this const self = this
return new Promise<void>(function (resolve, reject) { return new Promise<void>(function (resolve, reject) {
if (changesetId === undefined) { if (changesetId === undefined) {
@ -202,7 +218,7 @@ export class ChangesetHandler {
return; return;
} }
console.log("closing changeset", changesetId); console.log("closing changeset", changesetId);
self.currentChangeset.setData(""); self.currentChangeset.setData(undefined);
self.auth.xhr({ self.auth.xhr({
method: 'PUT', method: 'PUT',
path: '/api/0.6/changeset/' + changesetId + '/close', path: '/api/0.6/changeset/' + changesetId + '/close',
@ -217,39 +233,63 @@ export class ChangesetHandler {
}) })
} }
private OpenChangeset( private async GetChangesetMeta(csId: number): Promise<{
layout: LayoutConfig, id: number,
options?: { open: boolean,
isDeletionCS?: boolean, uid: number,
deletionReason?: string, changes_count: number,
} tags: any
): Promise<string> { }> {
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; const self = this;
return new Promise<string>(function (resolve, reject) { return new Promise<string>(function (resolve, reject) {
options = options ?? {}
options.isDeletionCS = options.isDeletionCS ?? false tags = Utils.NoNull(tags).filter(([k, v]) => k !== undefined && v !== undefined && k !== "" && v !== "")
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; const metadata = tags.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
if (options.isDeletionCS) { self.auth.xhr({
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` method: 'PUT',
if (options.deletionReason) { path: '/api/0.6/changeset/' + csId,
comment += ": " + options.deletionReason; 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; let path = window.location.pathname;
path = path.substr(1, path.lastIndexOf("/")); path = path.substr(1, path.lastIndexOf("/"));
const metadata = [ const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`], ["created_by", `MapComplete ${Constants.vNumber}`],
["comment", comment],
["deletion", options.isDeletionCS ? "yes" : undefined],
["theme", layout.id],
["language", Locale.language.data], ["language", Locale.language.data],
["host", window.location.host], ["host", window.location.host],
["path", path], ["path", path],
["source", State.state.currentGPSLocation.data !== undefined ? "survey" : undefined], ["source", State.state.currentGPSLocation.data !== undefined ? "survey" : undefined],
["imagery", State.state.backgroundLayer.data.id], ["imagery", State.state.backgroundLayer.data.id],
["theme-creator", layout.maintainer] ...changesetTags.map(cstag => [cstag.key, cstag.value])
] ]
.filter(kv => (kv[1] ?? "") !== "") .filter(kv => (kv[1] ?? "") !== "")
.map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`) .map(kv => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
@ -268,7 +308,7 @@ export class ChangesetHandler {
console.log("err", err); console.log("err", err);
reject(err) reject(err)
} else { } else {
resolve(response); resolve(Number(response));
} }
}); });
}) })
@ -278,8 +318,8 @@ export class ChangesetHandler {
/** /**
* Upload a changesetXML * Upload a changesetXML
*/ */
private AddChange(changesetId: string, private AddChange(changesetId: number,
changesetXML: string): Promise<string> { changesetXML: string): Promise<number> {
const self = this; const self = this;
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
self.auth.xhr({ 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> { public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
return this.preferencesHandler.GetPreference(key, prefix); return this.preferencesHandler.GetPreference(key, prefix);
} }

View file

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

View file

@ -6,6 +6,7 @@ import Combine from "../UI/Base/Combine";
import BaseUIElement from "../UI/BaseUIElement"; import BaseUIElement from "../UI/BaseUIElement";
import Title from "../UI/Base/Title"; import Title from "../UI/Base/Title";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {FixedUiElement} from "../UI/Base/FixedUiElement";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
const cardinalDirections = { const cardinalDirections = {
@ -62,6 +63,20 @@ export default class SimpleMetaTagger {
return true; 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( private static surfaceArea = new SimpleMetaTagger(
{ {
keys: ["_surface", "_surface:ha"], keys: ["_surface", "_surface:ha"],
@ -329,6 +344,7 @@ export default class SimpleMetaTagger {
) )
public static metatags = [ public static metatags = [
SimpleMetaTagger.latlon, SimpleMetaTagger.latlon,
SimpleMetaTagger.layerInfo,
SimpleMetaTagger.surfaceArea, SimpleMetaTagger.surfaceArea,
SimpleMetaTagger.lngth, SimpleMetaTagger.lngth,
SimpleMetaTagger.canonicalize, SimpleMetaTagger.canonicalize,
@ -346,7 +362,7 @@ export default class SimpleMetaTagger {
public readonly doc: string; public readonly doc: string;
public readonly isLazy: boolean; public readonly isLazy: boolean;
public readonly includesDates: 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 * 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 * @param f: apply the changes. Returns true if something changed
*/ */
constructor(docs: { keys: string[], doc: string, includesDates?: boolean, isLazy?: boolean }, 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.keys = docs.keys;
this.doc = docs.doc; this.doc = docs.doc;
this.isLazy = docs.isLazy this.isLazy = docs.isLazy

View file

@ -75,6 +75,27 @@ export class UIEventSource<T> {
promise?.catch(err => console.warn("Promise failed:", err)) promise?.catch(err => console.warn("Promise failed:", err))
return src 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. * 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 sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<UIEventSource<X>>(); const seenEventSources = new Set<UIEventSource<X>>();
mapped.addCallbackAndRun(newEventSource => { mapped.addCallbackAndRun(newEventSource => {
if (newEventSource === null) {
if (newEventSource === undefined) { sink.setData(null)
} else if (newEventSource === undefined) {
sink.setData(undefined) sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) { }else if (!seenEventSources.has(newEventSource)) {
seenEventSources.add(newEventSource) seenEventSources.add(newEventSource)
newEventSource.addCallbackAndRun(resultData => { newEventSource.addCallbackAndRun(resultData => {
if (mapped.data === newEventSource) { if (mapped.data === newEventSource) {
sink.setData(resultData); 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"; 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, constructor(
labels: Map<string, string>, id: string,
descriptions: Map<string, string>, labels: Map<string, string>,
claims: Map<string, Set<string>>, descriptions: Map<string, string>,
wikisites: Map<string, string> claims: Map<string, Set<string>>,
commons: string wikisites: Map<string, string>,
} commons: string
) {
/** this.id = id
* Utility functions around wikidata this.labels = labels
*/ this.descriptions = descriptions
export default class Wikidata { 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>() const labels = new Map<string, string>()
for (const labelName in entity.labels) { for (const labelName in entity.labels) {
// The labelname is the language code // The labelname is the language code
@ -37,64 +48,239 @@ export default class Wikidata {
const title = entity.sitelinks[labelName].title const title = entity.sitelinks[labelName].title
sitelinks.set(language, title) sitelinks.set(language, title)
} }
const commons = sitelinks.get("commons") const commons = sitelinks.get("commons")
sitelinks.delete("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>>(); 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>() const values = new Set<string>()
for (const claim of claimsList) { 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) values.add(value)
} }
claims.set(claimId, values); claims.set(claimId, values);
} }
return claims
}
}
return { export class WikidataLexeme {
claims: claims, id: string
descriptions: descr, lemma: Map<string, string>
id: entity.id, senses: Map<string, string>
labels: labels, claims: Map<string, Set<string>>
wikisites: sitelinks,
commons: commons
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}>>() asWikidataResponse() {
public static LoadWikidataEntry(value: string | number): UIEventSource<{success: WikidataResponse} | {error: any}> { 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 key = this.ExtractKey(value)
const cached = Wikidata._cache.get(key) const cached = Wikidata._cache.get(key)
if(cached !== undefined){ if (cached !== undefined) {
return cached return cached
} }
const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
Wikidata._cache.set(key, src) Wikidata._cache.set(key, src)
return 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") { if (typeof value === "number") {
return value return "Q" + value
} }
const wikidataUrl = "https://www.wikidata.org/wiki/" if (value === undefined) {
if (value.startsWith(wikidataUrl)) { console.error("ExtractKey: value is undefined")
value = value.substring(wikidataUrl.length) 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 // Probably some random link in the image field - we skip it
return undefined 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)){ if (value !== "" && !isNaN(Number(value))) {
return undefined 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> { public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
const id = Wikidata.ExtractKey(value) const id = Wikidata.ExtractKey(value)
if(id === undefined){ if (id === undefined) {
console.warn("Could not extract a wikidata entry from", value) 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 url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json";
const response = await Utils.downloadJson(url) const entities = (await Utils.downloadJson(url)).entities
return Wikidata.ParseResponse(response.entities["Q" + id]) 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", "mw-selflink",
"hatnote" // Often redirects "hatnote" // Often redirects
] ]
private static readonly idsToRemove = [
"sjabloon_zie"
]
private static readonly _cache = new Map<string, UIEventSource<{ success: string } | { error: any }>>() 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")) const links = Array.from(content.getElementsByTagName("a"))
// Rewrite relative links to absolute links + open them in a new tab // 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 { export default class Constants {
public static vNumber = "0.10.5"; public static vNumber = "0.11.0-alpha-2";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
@ -14,7 +14,7 @@ export default class Constants {
"https://overpass.kumi.systems/api/interpreter", "https://overpass.kumi.systems/api/interpreter",
// Offline: "https://overpass.nchc.org.tw/api/interpreter", // Offline: "https://overpass.nchc.org.tw/api/interpreter",
"https://overpass.openstreetmap.ru/cgi/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? * Who does maintian this preset?
*/ */
maintainer: string; maintainer: string;
/**
* Extra piece of text that can be added to the changeset
*/
changesetmessage?: string;
/** /**
* A version number, either semantically or by date. * A version number, either semantically or by date.
* Should be sortable, where the higher value is the later version * 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. * Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md' * 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. * If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked' * 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 id: string;
public readonly maintainer: string; public readonly maintainer: string;
public readonly credits?: string; public readonly credits?: string;
public readonly changesetmessage?: string;
public readonly version: string; public readonly version: string;
public readonly language: string[]; public readonly language: string[];
public readonly title: Translation; public readonly title: Translation;
@ -61,7 +60,6 @@ export default class LayoutConfig {
context = (context ?? "") + "." + this.id; context = (context ?? "") + "." + this.id;
this.maintainer = json.maintainer; this.maintainer = json.maintainer;
this.credits = json.credits; this.credits = json.credits;
this.changesetmessage = json.changesetmessage;
this.version = json.version; this.version = json.version;
this.language = []; this.language = [];
if (typeof json.language === "string") { if (typeof json.language === "string") {

View file

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

View file

@ -6,11 +6,16 @@ import {VariableUiElement} from "./VariableUIElement";
export class TabbedComponent extends Combine { 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 openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
const tabs: BaseUIElement[] = [] const tabs: BaseUIElement[] = [options?.leftOfHeader ]
const contentElements: BaseUIElement[] = []; const contentElements: BaseUIElement[] = [];
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
let element = elements[i]; let element = elements[i];
@ -25,16 +30,19 @@ export class TabbedComponent extends Combine {
} }
}) })
const content = Translations.W(element.content) 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); contentElements.push(content);
const tab = header.SetClass("block tab-single-header") const tab = header.SetClass("block tab-single-header")
tabs.push(tab) tabs.push(tab)
} }
const header = new Combine(tabs).SetClass("tabs-header-bar") const header = new Combine(tabs).SetClass("tabs-header-bar")
if(options?.styleHeader){
options.styleHeader(header)
}
const actualContent = new VariableUiElement( const actualContent = new VariableUiElement(
openedTabSrc.map(i => contentElements[i]) openedTabSrc.map(i => contentElements[i])
) ).SetStyle("max-height: inherit; height: inherit")
super([header, actualContent]) 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( return new Toggle(
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabs, 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.data["_imported"] = "yes"
originalTags.ping() // will set isImported as per its definition 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) await State.state.changes.applyAction(newElementAction)
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId 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) { 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) await State.state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined) selectedPreset.setData(undefined)
isShown.setData(false) 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;"), icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
new Combine([ 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 ?? "")) Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
]).SetClass("flex flex-col") ]).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") .SetClass("rounded-full p-1")
.SetStyle("color:white;background:#ff8c8c") .SetStyle("color:white;background:#ff8c8c")
.onClick(async() => { .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() 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;") .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
.onClick( async() => { .onClick( async() => {
await State.state?.changes?.applyAction( 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 UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {FixedUiElement} from "../Base/FixedUiElement";
export class ImageUploadFlow extends Toggle { 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 => { const uploader = new ImgurUploader(url => {
// A file was uploaded - we add it to the tags of the object // 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); console.log("Adding image:" + key, url);
Promise.resolve(State.state.changes Promise.resolve(State.state.changes
.applyAction(new ChangeTagAction( .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 licensePicker = new LicensePicker()
const t = Translations.t.image; 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([ const label = new Combine([
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"), Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3") labelContent
]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center") ]).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) const fileSelector = new FileSelectorButton(label)
fileSelector.GetValue().addCallback(filelist => { fileSelector.GetValue().addCallback(filelist => {

View file

@ -16,6 +16,8 @@ import LengthInput from "./LengthInput";
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import {Unit} from "../../Models/Unit"; import {Unit} from "../../Models/Unit";
import {FixedInputElement} from "./FixedInputElement"; import {FixedInputElement} from "./FixedInputElement";
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
import Wikidata from "../../Logic/Web/Wikidata";
interface TextFieldDef { interface TextFieldDef {
name: string, name: string,
@ -147,23 +149,58 @@ export default class ValidatedTextField {
), ),
ValidatedTextField.tp( ValidatedTextField.tp(
"wikidata", "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) => { (str) => {
if (str === undefined) { if (str === undefined) {
return false; 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) => { (str) => {
if (str === undefined) { if (str === undefined) {
return undefined; return undefined;
} }
const wd = "https://www.wikidata.org/wiki/"; let out = str.split(";").map(str => Wikidata.ExtractKey(str)).join("; ")
if (str.startsWith(wd)) { if(str.endsWith(";")){
str = str.substr(wd.length) 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( ValidatedTextField.tp(
"int", "int",
@ -361,13 +398,13 @@ export default class ValidatedTextField {
// This implies: // This implies:
// We have to create a dropdown with applicable denominations, and fuse those values // We have to create a dropdown with applicable denominations, and fuse those values
const unit = options.unit const unit = options.unit
const isSingular = input.GetValue().map(str => str?.trim() === "1") const isSingular = input.GetValue().map(str => str?.trim() === "1")
const unitDropDown = const unitDropDown =
unit.denominations.length === 1 ? 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("", : new DropDown("",
unit.denominations.map(denom => { unit.denominations.map(denom => {
return { return {
@ -378,17 +415,17 @@ export default class ValidatedTextField {
) )
unitDropDown.GetValue().setData(unit.defaultDenom) unitDropDown.GetValue().setData(unit.defaultDenom)
unitDropDown.SetClass("w-min") 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 = new CombinedInputElement(
input, input,
unitDropDown, unitDropDown,
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM // combine the value from the textfield and the dropdown into the resulting value that should go into OSM
(text, denom) => { (text, denom) => {
if(denom === undefined){ if (denom === undefined) {
return text return text
} }
return denom?.canonicalValue(text, true) return denom?.canonicalValue(text, true)
}, },
(valueWithDenom: string) => { (valueWithDenom: string) => {
// Take the value from OSM and feed it into the textfield and the dropdown // 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 Translations from "../i18n/Translations";
import Svg from "../../Svg"; import Svg from "../../Svg";
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; import DeleteAction from "../../Logic/Osm/Actions/DeleteAction";
import {Tag} from "../../Logic/Tags/Tag";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import TagRenderingQuestion from "./TagRenderingQuestion"; import TagRenderingQuestion from "./TagRenderingQuestion";
@ -13,13 +12,11 @@ import {SubtleButton} from "../Base/SubtleButton";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {Changes} from "../../Logic/Osm/Changes";
import {And} from "../../Logic/Tags/And";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"; import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig";
import {OsmObject} from "../../Logic/Osm/OsmObject";
export default class DeleteWizard extends Toggle { export default class DeleteWizard extends Toggle {
/** /**
@ -43,44 +40,32 @@ export default class DeleteWizard extends Toggle {
constructor(id: string, constructor(id: string,
options: DeleteConfig) { options: DeleteConfig) {
const deleteAction = new DeleteAction(id, options.neededChangesets); const deleteAbility = new DeleteabilityChecker(id, options.neededChangesets)
const tagsSource = State.state.allElements.getEventSourceById(id) const tagsSource = State.state.allElements.getEventSourceById(id)
const isDeleted = new UIEventSource(false)
const allowSoftDeletion = !!options.softDeletionTags const allowSoftDeletion = !!options.softDeletionTags
const confirm = new UIEventSource<boolean>(false) 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) { function doDelete(selected: TagsFilter) {
// Selected == the reasons, not the tags of the object
const tgs = selected.asChange(tagsSource.data) const tgs = selected.asChange(tagsSource.data)
const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason") const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason")
if (deleteReasonMatch.length > 0) { if (deleteReasonMatch.length === 0) {
// We should actually delete! return;
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)
} }
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(() => { saveButtonConstr: (v) => DeleteWizard.constructConfirmButton(v).onClick(() => {
doDelete(v.data) 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( const deleteButton = new SubtleButton(
Svg.delete_icon_ui().SetStyle("width: 2rem; height: 2rem;"), t.delete.Clone()).onClick( Svg.delete_icon_ui().SetStyle("width: 2rem; height: 2rem;"), t.delete.Clone()).onClick(
() => { () => {
deleteAction.CheckDeleteability(true) deleteAbility.CheckDeleteability(true)
confirm.setData(true); confirm.setData(true);
} }
).SetClass("w-1/2 float-right"); ).SetClass("w-1/2 float-right");
@ -132,13 +117,13 @@ export default class DeleteWizard extends Toggle {
deleteButton, deleteButton,
confirm), confirm),
new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))), new VariableUiElement(deleteAbility.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))),
deleteAction.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin),
State.state.osmConnection.isLoggedIn State.state.osmConnection.isLoggedIn
), ),
deleteAction.isDeleted), isDeleted),
undefined, undefined,
isShown) 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; const t = Translations.t.delete;
return new VariableUiElement(tags.map( return new VariableUiElement(tags.map(
currentTags => { 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 // Save button
const saveButton = new Button(t.split.Clone(), () => { const saveButton = new Button(t.split.Clone(), () => {
hasBeenSplit.setData(true) 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"); saveButton.SetClass("btn btn-primary mr-3");

View file

@ -86,7 +86,10 @@ export default class TagRenderingQuestion extends Combine {
if (selection) { if (selection) {
(State.state?.changes ?? new Changes()) (State.state?.changes ?? new Changes())
.applyAction(new ChangeTagAction( .applyAction(new ChangeTagAction(
tags.data.id, selection, tags.data tags.data.id, selection, tags.data, {
theme: State.state?.layoutToUse?.id ?? "unkown",
changeType: "answer",
}
)).then(_ => { )).then(_ => {
console.log("Tagchanges applied") console.log("Tagchanges applied")
}) })
@ -133,7 +136,7 @@ export default class TagRenderingQuestion extends Combine {
options.cancelButton, options.cancelButton,
saveButton, saveButton,
bottomTags]) 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 FeatureInfoBox from "../Popup/FeatureInfoBox";
import State from "../../State"; import State from "../../State";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
import {FixedUiElement} from "../Base/FixedUiElement";
export default class ShowDataLayer { export default class ShowDataLayer {
@ -28,9 +29,13 @@ export default class ShowDataLayer {
*/ */
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>() private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
private readonly showDataLayerid : number;
private static dataLayerIds = 0
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
this._leafletMap = options.leafletMap; this._leafletMap = options.leafletMap;
this.showDataLayerid = ShowDataLayer.dataLayerIds;
ShowDataLayer.dataLayerIds++
this._enablePopups = options.enablePopups ?? true; this._enablePopups = options.enablePopups ?? true;
if (options.features === undefined) { if (options.features === undefined) {
throw "Invalid ShowDataLayer invocation" throw "Invalid ShowDataLayer invocation"
@ -221,9 +226,8 @@ export default class ShowDataLayer {
let infobox: FeatureInfoBox = undefined; let infobox: FeatureInfoBox = undefined;
const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this._cleanCount}` 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}</div>`) popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`)
leafletLayer.on("popupopen", () => { leafletLayer.on("popupopen", () => {
if (infobox === undefined) { if (infobox === undefined) {
const tags = State.state.allElements.getEventSourceById(feature.properties.id); const tags = State.state.allElements.getEventSourceById(feature.properties.id);

View file

@ -26,8 +26,9 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
import Minimap from "./Base/Minimap"; import Minimap from "./Base/Minimap";
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
import WikipediaBox from "./WikipediaBox"; import WikipediaBox from "./Wikipedia/WikipediaBox";
import SimpleMetaTagger from "../Logic/SimpleMetaTagger"; import SimpleMetaTagger from "../Logic/SimpleMetaTagger";
import MultiApply from "./Popup/MultiApply";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -81,13 +82,16 @@ export default class SpecialVisualizations {
funcName: "image_carousel", 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)", 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: [{ args: [{
name: "image key/prefix", name: "image key/prefix (multiple values allowed if comma-seperated)",
defaultValue: "image", 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... " 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) => { constr: (state: State, tags, args) => {
const imagePrefix = args[0]; let imagePrefixes = undefined;
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefix), tags); 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", name: "image-key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "image" defaultValue: "image"
},{
name:"label",
doc:"The text to show on the button",
defaultValue: "Add image"
}], }],
constr: (state: State, tags, args) => { 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", 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) => 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", 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 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(); 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 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; 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[] { public static NoEmpty(array: string[]): string[] {
const ls: string[] = []; const ls: string[] = [];
for (const t of array) { 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()}" "render": "{image_carousel()}{image_upload()}"
}, },
"wikipedia": { "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": { "reviews": {
"render": "{reviews()}" "render": "{reviews()}"
@ -44,7 +62,23 @@
}, },
"wikipedialink": { "wikipedialink": {
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/wikipedia.svg' alt='WP'/></a>", "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": { "email": {
"render": "<a href='mailto:{email}' target='_blank'>{email}</a>", "render": "<a href='mailto:{email}' target='_blank'>{email}</a>",

View file

@ -16,9 +16,8 @@
"en", "en",
"nl" "nl"
], ],
"hideFromOverview": true,
"maintainer": "", "maintainer": "",
"icon": "./assets/svg/bug.svg", "icon": "./assets/layers/etymology/logo.svg",
"version": "0", "version": "0",
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
@ -26,62 +25,51 @@
"widenFactor": 2, "widenFactor": 2,
"socialImage": "", "socialImage": "",
"layers": [ "layers": [
"etymology",
{ {
"id": "has_a_name", "builtin": "etymology",
"name": { "override": {
"en": "Has etymolgy", "id": "streets_without_etymology",
"nl": "Heeft etymology info" "name": {
}, "en": "Streets without etymology information",
"minzoom": 12, "nl": "Straten zonder etymologische informatie"
"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"
}
}, },
{ "minzoom": 18,
"id": "wikipedia-etymology", "source": {
"render": { "osmTags": {
"*": "{wikipedia(name:etymology:wikidata):max-height:20rem}" "and": [
"name~*",
"highway~*",
"highway!=bus_stop"
]
} }
} }
], }
"icon": { },
"render": "./assets/svg/bug.svg" {
}, "builtin": "etymology",
"width": { "override": {
"render": "8" "id": "parks_and_forests_without_etymology",
}, "name": {
"iconSize": { "en": "Parks and forests without etymology information",
"render": "40,40,center" "nl": "Parken en bossen zonder etymologische informatie"
}, },
"color": { "minzoom": 18,
"render": "#00f" "source": {
}, "osmTags": {
"presets": [] "and": [
"name~*",
{
"or": [
"leisure=park",
"landuse=forest"
]
}
]
}
}
}
} }
], ],
"roamingRenderings": [] "hideFromOverview": true
} }

View file

@ -824,6 +824,10 @@ video {
margin: 1rem; margin: 1rem;
} }
.m-px {
margin: 1px;
}
.my-2 { .my-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -848,10 +852,6 @@ video {
margin-left: 0.75rem; margin-left: 0.75rem;
} }
.mr-3 {
margin-right: 0.75rem;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -912,6 +912,10 @@ video {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.mr-3 {
margin-right: 0.75rem;
}
.mb-4 { .mb-4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -1386,14 +1390,6 @@ video {
padding: 0px; padding: 0px;
} }
.pl-6 {
padding-left: 1.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.pl-2 { .pl-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
@ -1462,6 +1458,14 @@ video {
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.pl-6 {
padding-left: 1.5rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1586,6 +1590,10 @@ video {
opacity: 0; opacity: 0;
} }
.opacity-50 {
opacity: 0.5;
}
.opacity-40 { .opacity-40 {
opacity: 0.4; opacity: 0.4;
} }
@ -1628,6 +1636,18 @@ video {
transition-duration: 150ms; 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 {
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, -webkit-backdrop-filter !important;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important; transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important;
@ -1788,6 +1808,10 @@ svg, img {
height: 100%; height: 100%;
} }
.no-images img {
display: none;
}
.mapcontrol svg path { .mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important; fill: var(--subtle-detail-color-contrast) !important;
} }
@ -1888,6 +1912,10 @@ li::marker {
border: 5px solid var(--catch-detail-color); border: 5px solid var(--catch-detail-color);
} }
.border-attention {
border-color: var(--catch-detail-color);
}
.direction-svg svg path { .direction-svg svg path {
fill: var(--catch-detail-color) !important; fill: var(--catch-detail-color) !important;
} }
@ -1930,6 +1958,27 @@ li::marker {
max-width: 2em !important; 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 ****************/ /**************** GENERIC ****************/
.alert { .alert {
@ -2189,6 +2238,10 @@ li::marker {
color: rgba(30, 64, 175, var(--tw-text-opacity)); color: rgba(30, 64, 175, var(--tw-text-opacity));
} }
.hover\:opacity-100:hover {
opacity: 1;
}
.hover\:shadow-xl:hover { .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); --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); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
@ -2250,6 +2303,10 @@ li::marker {
justify-content: space-between; justify-content: space-between;
} }
.sm\:border-4 {
border-width: 4px;
}
.sm\:p-0\.5 { .sm\:p-0\.5 {
padding: 0.125rem; padding: 0.125rem;
} }
@ -2266,6 +2323,10 @@ li::marker {
padding: 0.25rem; padding: 0.25rem;
} }
.sm\:p-2 {
padding: 0.5rem;
}
.sm\:pl-2 { .sm\:pl-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
@ -2365,6 +2426,10 @@ li::marker {
padding: 0.5rem; padding: 0.5rem;
} }
.md\:p-3 {
padding: 0.75rem;
}
.md\:pt-4 { .md\:pt-4 {
padding-top: 1rem; padding-top: 1rem;
} }

View file

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

View file

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

View file

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

View file

@ -93,6 +93,10 @@ svg, img {
height: 100%; height: 100%;
} }
.no-images img {
display: none;
}
.mapcontrol svg path { .mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important; fill: var(--subtle-detail-color-contrast) !important;
} }
@ -191,6 +195,10 @@ li::marker {
border: 5px solid var(--catch-detail-color); border: 5px solid var(--catch-detail-color);
} }
.border-attention {
border-color: var(--catch-detail-color);
}
.direction-svg svg path { .direction-svg svg path {
fill: var(--catch-detail-color) !important; 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 ****************/ /**************** GENERIC ****************/

View file

@ -221,9 +221,13 @@
}, },
"wikipedia": { "wikipedia": {
"wikipediaboxTitle": "Wikipedia", "wikipediaboxTitle": "Wikipedia",
"failed":"Loading the wikipedia entry failed", "failed": "Loading the wikipedia entry failed",
"loading": "Loading Wikipedia...", "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": { "favourite": {
@ -246,5 +250,8 @@
"tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>", "tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>",
"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>.", "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" "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"
} }
} }

View file

@ -2500,6 +2500,34 @@
"render": "Drinking water" "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": { "food": {
"filter": { "filter": {
"0": { "0": {

View file

@ -2446,6 +2446,31 @@
"render": "Drinkbaar water" "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": { "food": {
"filter": { "filter": {
"0": { "0": {

View file

@ -78,6 +78,22 @@
} }
}, },
"question": "Is this place accessible with a wheelchair?" "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?" "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": { "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.", "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": { "layers": {
"0": { "1": {
"description": "All objects which have an etymology known", "override": {
"name": "Has etymolgy", "name": "Streets without etymology information"
"tagRenderings": {
"simple etymology": {
"render": "Named after {name:etymology}"
}
} }
} }
}, },

View file

@ -627,13 +627,9 @@
"etymology": { "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.", "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": { "layers": {
"0": { "1": {
"description": "Alle lagen met een gelinkt etymology", "override": {
"name": "Heeft etymology info", "name": "Straten zonder etymologische informatie"
"tagRenderings": {
"simple etymology": {
"render": "Vernoemd naar {name:etymology}"
}
} }
} }
}, },

27
test.ts
View file

@ -1,26 +1,3 @@
import FeatureInfoBox from "./UI/Popup/FeatureInfoBox"; import WikipediaBox from "./UI/Wikipedia/WikipediaBox";
import {UIEventSource} from "./Logic/UIEventSource";
import AllKnownLayers from "./Customizations/AllKnownLayers";
import State from "./State";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
State.state = new State(AllKnownLayouts.allKnownLayouts.get("charging_stations")) new WikipediaBox(["L614072"]).AttachTo("maindiv")
State.state.changes.pendingChanges.setData([])
const geojson = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [51.0, 4]
},
properties:
{
id: "node/42",
amenity: "charging_station",
}
}
State.state.allElements.addOrGetElement(geojson)
const tags = State.state.allElements.getEventSourceById("node/42")
new FeatureInfoBox(
tags,
AllKnownLayers.sharedLayers.get("charging_station")
).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 {InPlaceReplacedmentRTSH} from "../Logic/Osm/Actions/RelationSplitHandler";
import {OsmObject, OsmRelation} from "../Logic/Osm/OsmObject"; import {OsmObject, OsmRelation} from "../Logic/Osm/OsmObject";
import {Changes} from "../Logic/Osm/Changes"; import {Changes} from "../Logic/Osm/Changes";
import {equal} from "assert";
import {Utils} from "../Utils"; import {Utils} from "../Utils";
export default class RelationSplitHandlerSpec extends T { export default class RelationSplitHandlerSpec extends T {
@ -49,7 +48,7 @@ export default class RelationSplitHandlerSpec extends T {
allWayIdsInOrder: [295132739, -1], allWayIdsInOrder: [295132739, -1],
originalNodes: originalNodeIds, originalNodes: originalNodeIds,
allWaysNodesInOrder: withSplit allWaysNodesInOrder: withSplit
}) },"no-theme")
const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
const allIds = changeDescription[0].changes["members"].map(m => m.ref).join(",") const allIds = changeDescription[0].changes["members"].map(m => m.ref).join(",")
const expected = "687866206,295132739,-1,690497698" 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 // Lets split road https://www.openstreetmap.org/way/295132739
const id = "way/295132739" const id = "way/295132739"
const splitPoint: [number, number] = [3.246733546257019, 51.181710380278176] 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()) const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
equal(changeDescription[0].type, "node") equal(changeDescription[0].type, "node")
@ -235,7 +237,9 @@ export default class SplitActionSpec extends T {
const id = "way/61435323" const id = "way/61435323"
const splitPoint: [number, number] = [ 3.2021324336528774, 51.2170001600597] 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()) const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
// Should be a new node // 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 // Lets split road near an already existing point https://www.openstreetmap.org/way/295132739
const id = "way/295132739" const id = "way/295132739"
const splitPoint: [number, number] = [3.2451081275939937, 51.18116898253599] 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()) const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
equal(2, changeDescription.length) equal(2, changeDescription.length)

View file

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

View file

@ -40,6 +40,14 @@ export default class T {
throw "Expected true, but got false: " + msg 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) { static isFalse(b: boolean, msg: string) {
if (b) { if (b) {

File diff suppressed because one or more lines are too long