Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2023-12-08 19:27:47 +01:00
commit d9b084f7d9
300 changed files with 6865 additions and 26946 deletions

View file

@ -33,18 +33,14 @@ jobs:
run: npm run generate:translations run: npm run generate:translations
shell: bash shell: bash
- name: generate layeroverview - name: Prepare deploy
run: npm run reset:layeroverview run: npm run prepare-deploy
shell: bash shell: bash
- name: run tests - name: run tests
run: npm run test run: npm run test
shell: bash shell: bash
- name: Prepare deploy
run: npm run prepare-deploy
shell: bash
- name: Clone deployment repo - name: Clone deployment repo
env: env:
DEPLOY_KEY_PIETERVDVN: ${{ secrets.DEPLOY_KEY_PIETERVDVN }} DEPLOY_KEY_PIETERVDVN: ${{ secrets.DEPLOY_KEY_PIETERVDVN }}

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ scratch
assets/editor-layer-index.json assets/editor-layer-index.json
assets/generated/* assets/generated/*
src/assets/generated/ src/assets/generated/
assets/layers/favourite/favourite.json
public/*.webmanifest public/*.webmanifest
/*.html /*.html
!/index.html !/index.html

View file

@ -11,3 +11,5 @@ Docs/Tools/stats/
Docs/Layers/ Docs/Layers/
Docs/Schemas/ Docs/Schemas/
Docs/TagInfo/ Docs/TagInfo/
src/assets/generated
src/assets/svg

View file

@ -18,5 +18,8 @@
"[svelte]": { "[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.formatOnSave": true "editor.formatOnSave": true,
"files.associations": {
"*.protojson": "json"
}
} }

View file

@ -24,6 +24,11 @@ Please, do reach out to the MapComplete community channel
on [Telegram](https://t.me/MapComplete) on [Telegram](https://t.me/MapComplete)
or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org). or [Matrix](https://app.element.io/#/room/#MapComplete:matrix.org).
Get started
-----------
You can create your own theme at https://mapcomplete.org/studio
What is a good theme? What is a good theme?
--------------------- ---------------------
@ -227,49 +232,7 @@ The entire tagRendering will thus be:
``` ```
The template ## Make it official
------------
[A basic template is available here](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/theme-template.json).
The custom theme generator
--------------------------
The custom theme generator is a special page of MapComplete, where one can create their own theme. It makes it easier to
get started.
However, the custom theme generator is extremely buggy and built before some updates. This means that some features
are _not_ available through the custom theme generator. The custom theme generator is good to get the basics of the
theme set up, but you will have to edit the raw JSON file anyway afterwards.
[A quick tutorial for the custom theme generator can be found here](https://www.youtube.com/watch?v=nVbFrNVPxPw).
Loading your theme
------------------
If you have your JSON file, there are three ways to distribute your theme:
### a. base64 the JSON file
Take the entire JSON file and [base64](https://www.base64encode.org/) encode it.
Then open up the URL `https://mapcomplete.org?userlayout=true#<base64-encoded-json-here>`.
Yes, this URL will be huge; and updates are difficult to distribute as you have to send a new URL to everyone.
This is however excellent to have a 'quick and dirty' test version up and running as these links can be generated from the customThemeGenerator and can be quickly shared with a few other contributors.
You can use the community maintained [ThemeHelper](https://github.com/tordans/MapComplete-ThemeHelper) to make this process easier. This uses `lzString.compressToBase64` which is the counterpart to how MapComplete decompresses the base64 input.
### b. Host the JSON file
Host the JSON file on a publicly accessible webserver (e.g. GitHub) and open up `https://mapcomplete.org?userlayout=<url-to-the-raw.json>`
_Gotcha:_ Make sure the server that hosts your JSON has liberal caching settings. Otherwise one version of the file might get cached in the users' browser cache for a long time and updates are not destributed for this user.
### c. Make it official
Get your theme included into the official MapComplete:
Did you make an awesome theme that you want to share with the OpenStreetMap community? Have it included in the main Did you make an awesome theme that you want to share with the OpenStreetMap community? Have it included in the main
application. This makes sure that: application. This makes sure that:

View file

@ -0,0 +1,29 @@
## Task
Add a (specified) feature as favourite
Find and use the list of favourites
Determine information from this list
Open the popup from this list
## Background info
User has used mapcomplete before
## Results
The user is asked to mark a specified bicycle shop as favourite. They find the big button to mark as favourite at the bottom.
When asked to select another feature, they choose a bicycle pump. When hinted that 'they can add this in a different way', they immediately select the heart title icon.
When asked to open the list of favourites, they open the 'hamburger'-menu. After a bit of looking, they spot the 'Your favourites'-button.
They are a bit confused. The specified bicycle shop is advertised as `building or wall`.
The bicycle pump is shown correctly, the icons are clear. When asked to open the popup for one of them, they click directly on the link.
## Surfaced issues
Due to the way the title is generated, wrong titles appeared: all titles from all layers are mixed and used as title, if the tags match. As such, the title `building or wall` appeared, as it happened to be on top and the bicycle shop had a `building~*` tag.
This was resolved by sorting those titles by popularity. The least occuring tags/titles are placed first, so that the most specific title is shown. This might, in some cases, still result in differing titles (e.g. if something is e.g. both a shop and a café), but this should be exceptional.

View file

@ -1,187 +0,0 @@
{
"#1": "This JSON file is a small template to get you started developing a theme",
"#2": "All lines starting with '#' are comments and can be removed in the theme if you don't need the explanation anymore",
"#3": "Make sure to join our chat channel at https://app.element.io/#/room/#MapComplete:matrix.org for questions, sharing your theme, ...",
"#4": "To actually load your theme: on linux: run a local webserver (e.g. `webfsd`) and go to https://mapcomplete.org/theme?userlayout=http://127.0.0.1:8080/path-to-your-theme.json",
"#5": "If you don't know how to run a webserver: go to https://www.base64encode.org/ , copy paste this entire document in the 'encode' field and encode it;",
"#6": "Then, go to https://mapcomplete.org/theme?userlayout=true#your-base64-encoded-file",
"id": "template",
"credits": "Write your name here (or remove everything)",
"title": {
"en": "Title of your theme",
"#1": "You can add extra languages here (and in all translation blocks), but make sure 'en' is everywhere"
},
"description": {
"en": "The welcome message goes here"
},
"icon": "/path/to/icon.svg OR path to an online svg, such as https://upload.wikimedia.org/wikipedia/commons/9/9f/Missing_Maps_Icon.svg",
"startZoom": 0,
"startLat": 0,
"startLon": 0,
"#7": "For more options and configuration, see the documentation in LayoutConfig.json",
"#8": "`layers` is where most of the content will be. Either reuse an already existing layer by simply calling it's ID or define a whole new layer. An overview of builtin layers is at https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md#normal-layers",
"layers": [
{
"id": "a singular noun describing the feature, in english",
"source": {
"osmTags": {
"#1": "For a description on which tags are possible, see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md",
"and": [
"key0=value0",
"key1=value1",
{
"or": [
"key2!=value3",
"key3=",
"key4~*",
"key5~some.[regex]*"
]
}
]
}
},
"#4": "Minzoom: only download and show if zoom >= minzoom",
"minzoom": 12,
"name": {
"en": "Name of the layer, as shown in the layer selection"
},
"title": {
"render": {
"en": "Title in a popup when a feature is clicked"
},
"mappings": [
{
"if": "name~*",
"then": {
"#1": "If name is given, use name instead as popup title. Note that the translation here uses '*' instead of 'en', which'll be shown in every language",
"*": "{name}"
}
}
],
"#1": "Note that this is a tagRendering, but doesn't have a question field"
},
"allowMove": true,
"deletion": {
"softDeletionTags": {
"and": [
"razed:tourism=artwork",
"tourism="
]
},
"neededChangesets": 5
},
"#2": "The maprenderings describe how a feature is shown on the map",
"mapRendering": [
{
"#1": "Rendering block of a mapping which is shown for points AND at the center point of a line/area",
"location": [
"point",
"centroid"
],
"icon": "circle:white;URL or path to icon.svg",
"iconSize": "30,30,center",
"#2": "Note: all these values can be tagrenderings too, e.g.:",
"label": {
"render": {
"en": "Item"
},
"mappings": [
{
"if": "name~*",
"then": {
"*": "{name}"
}
}
]
}
},
{
"#1": "Rendering of a line",
"color": "#ff0",
"width": 5
}
],
"#3": "Presets describe which new items can be added on click. Delete this block if adding a new point is not relevant",
"presets": [
{
"title": {
"en": "lowercase item"
},
"tags": [
"somekey=somevalue",
"otherkey=othervalue"
],
"description": "A thorough definition of what the item is, usefull if people add stuff wrongly. This is optional",
"exampleImages": [
"optionally add images here",
"an image.jpg",
"another example image of the feature.jpg"
]
}
],
"#1": "The tagrenderings are everything that must be shown and/or asked. Use a full tag-rendering section OR a single string to call a builtin tagrendering (see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinQuestions.md)",
"tagRenderings": [
{
"render": {
"en": "This is a simple tagrendering without a question. It will always show this text as is"
}
},
"images",
"website",
"phone",
"opening_hours",
"email",
"reviews",
{
"render": {
"en": "This is a simple tagrendering without a question. It will always show this text as is"
}
},
{
"render": {
"en": "The value of some_osm_key is {some_osm_key} in this advanced tagrendering"
},
"question": {
"en": "What is XYZ?"
},
"freeform": {
"key": "some_osm_key",
"#1": "Types can be found at https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialInputElements.md",
"type": "nat"
},
"mappings": [
{
"if": "somekey=some_value",
"then": {
"en": "Text on radio button which also is shown if somekey=some_value is present on the object"
},
"#1": "If this option is picked as answer, these tags will be added additionally. However, if 'somekey=some_value' is present, the above rendering will be shown",
"addExtraTags": [
"extrakey=extravalue"
]
}
]
},
{
"mappings": [
{
"if": "somekey=some_value",
"then": {
"en": "Text on radio button which also is shown if somekey=some_value is present on the object"
},
"icon": {
"path": "/path/to/extra-icon.svg OR url",
"class": "medium",
"#1": "An extra icon supporting this option"
},
"#1": "If this option is picked as answer, these tags will be added additionally. However, if 'somekey=some_value' is present, the above rendering will be shown",
"addExtraTags": [
"extrakey=extravalue"
]
}
]
}
]
}
]
}

View file

@ -1115,7 +1115,8 @@
"hideInAnswer": { "hideInAnswer": {
"and": [ "and": [
"advertising!=poster_box", "advertising!=poster_box",
"advertising!=column" "advertising!=column",
"advertising!=billboard"
] ]
} }
}, },

View file

@ -212,7 +212,7 @@
"es": "El cajero es de {operator}" "es": "El cajero es de {operator}"
} }
}, },
"opening_hours", "opening_hours_24_7",
{ {
"id": "cash_out", "id": "cash_out",
"question": { "question": {

View file

@ -329,15 +329,80 @@
"class": "medium" "class": "medium"
}, },
"hideInAnswer": { "hideInAnswer": {
"or": [ "and": [
"_country!=be", "_country!=af",
"_country!=al",
"_country!=dz",
"_country!=as",
"_country!=ad",
"_country!=ao",
"_country!=am",
"_country!=aw",
"_country!=az",
"_country!=by",
"_country!=bt",
"_country!=ba",
"_country!=bg",
"_country!=cv",
"_country!=td",
"_country!=cl",
"_country!=hr",
"_country!=dk",
"_country!=eg",
"_country!=ee",
"_country!=et",
"_country!=fo",
"_country!=fr", "_country!=fr",
"_country!=ma", "_country!=pf",
"_country!=tn", "_country!=ge",
"_country!=pl", "_country!=gr",
"_country!=cs", "_country!=gl",
"_country!=gn",
"_country!=gw",
"_country!=is",
"_country!=id",
"_country!=ir",
"_country!=jo",
"_country!=kz",
"_country!=kg",
"_country!=la",
"_country!=lv",
"_country!=lr",
"_country!=ly",
"_country!=lt",
"_country!=lu",
"_country!=mo",
"_country!=mr",
"_country!=md",
"_country!=mc",
"_country!=mn",
"_country!=me",
"_country!=mz",
"_country!=nl",
"_country!=nc",
"_country!=ne",
"_country!=kp",
"_country!=mk",
"_country!=pt",
"_country!=qa",
"_country!=ro",
"_country!=ru",
"_country!=rw",
"_country!=sm",
"_country!=sk", "_country!=sk",
"_country!=mo" "_country!=si",
"_country!=kr",
"_country!=es",
"_country!=sr",
"_country!=tj",
"_country!=th",
"_country!=tl",
"_country!=tr",
"_country!=tm",
"_country!=ua",
"_country!=uy",
"_country!=uz",
"_country!=vn"
] ]
} }
}, },
@ -378,6 +443,52 @@
"icon": { "icon": {
"path": "./assets/layers/charging_station/TypeE.svg", "path": "./assets/layers/charging_station/TypeE.svg",
"class": "medium" "class": "medium"
},
"hideInAnswer": {
"and": [
"_country!=be",
"_country!=bj",
"_country!=bf",
"_country!=bi",
"_country!=cm",
"_country!=cf",
"_country!=td",
"_country!=km",
"_country!=cz",
"_country!=dk",
"_country!=dj",
"_country!=gq",
"_country!=et",
"_country!=fo",
"_country!=fr",
"_country!=gf",
"_country!=pf",
"_country!=gl",
"_country!=gp",
"_country!=gw",
"_country!=la",
"_country!=lr",
"_country!=mg",
"_country!=ml",
"_country!=mq",
"_country!=mr",
"_country!=mu",
"_country!=mc",
"_country!=mn",
"_country!=ma",
"_country!=ne",
"_country!=pl",
"_country!=pt",
"_country!=rw",
"_country!=mf",
"_country!=pm",
"_country!=sn",
"_country!=sk",
"_country!=sy",
"_country!=tl",
"_country!=tn",
"_country!=uz"
]
} }
}, },
{ {
@ -1031,7 +1142,7 @@
] ]
}, },
{ {
"or": [ "and": [
"_country!=us" "_country!=us"
] ]
} }
@ -5102,16 +5213,15 @@
"tags": [ "tags": [
"amenity=charging_station", "amenity=charging_station",
"motorcar=no", "motorcar=no",
"bicycle=yes", "bicycle=yes"
"socket:typee=1"
], ],
"title": { "title": {
"en": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)", "en": "a charging station for electrical bikes",
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)", "nl": "een oplaadpunt voor elektrische fietsen",
"ca": "una estació de càrrega per a bicicletes elèctriques amb un endoll de paret europeu normal<img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (destinat a carregar bicicletes elèctriques)", "ca": "una estació de càrrega per a bicicletes elèctriques amb un endoll de paret europeu normal<img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (destinat a carregar bicicletes elèctriques)",
"cs": "nabíjecí stanice pro elektrokola s běžnou evropskou zástrčkou <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (určeno k nabíjení elektrických kol)", "cs": "nabíjecí stanice pro elektrokola s běžnou evropskou zástrčkou <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (určeno k nabíjení elektrických kol)",
"da": "en ladestation til elektriske cykler med et normalt europæisk vægstik <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (beregnet til opladning af elektriske cykler)", "da": "en ladestation til elektriske cykler med et normalt europæisk vægstik <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (beregnet til opladning af elektriske cykler)",
"de": "eine Ladestation für Elektrofahrräder mit einer normalen europäischen Steckdose <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (zum Laden von Elektrofahrrädern)", "de": "eine Ladestation für Elektrofahrräder",
"es": "una estación de carga para bicicletas eléctricas con un enchufe de pared europeo normal <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (pensado para cargar bicicletas eléctricas)" "es": "una estación de carga para bicicletas eléctricas con un enchufe de pared europeo normal <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (pensado para cargar bicicletas eléctricas)"
} }
}, },

View file

@ -735,10 +735,12 @@
"point", "point",
"centroid" "centroid"
], ],
"marker": [{ "marker": [
{
"icon": "pin", "icon": "pin",
"color": "#fff" "color": "#fff"
},{ },
{
"icon": { "icon": {
"render": "./assets/themes/charging_stations/plug.svg", "render": "./assets/themes/charging_stations/plug.svg",
"mappings": [ "mappings": [
@ -756,9 +758,9 @@
"then": "./assets/themes/charging_stations/car.svg" "then": "./assets/themes/charging_stations/car.svg"
} }
] ]
} }
}], }
],
"iconBadges": [ "iconBadges": [
{ {
"if": { "if": {
@ -802,12 +804,11 @@
"tags": [ "tags": [
"amenity=charging_station", "amenity=charging_station",
"motorcar=no", "motorcar=no",
"bicycle=yes", "bicycle=yes"
"socket:typee=1"
], ],
"title": { "title": {
"en": "charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/TypeE.svg' class='w-4 h-4 mx-1 bg-white rounded-full'/>", "en": "charging station for electrical bikes",
"nl": "oplaadpunt voor elektrische fietsen met een gewone, europese stekker <img src='./assets/layers/charging_station/TypeE.svg' class='w-4 h-4 mx-1 bg-white rounded-full'/>" "nl": "oplaadpunt voor elektrische fietsen"
} }
}, },
{ {

View file

@ -91,7 +91,7 @@ function run(file, protojson) {
if (e.countryWhiteList.length > 0) { if (e.countryWhiteList.length > 0) {
// This is a 'hideInAnswer', thus _reverse_ logic! // This is a 'hideInAnswer', thus _reverse_ logic!
const countries = e.countryWhiteList.map(country => "_country!=" + country) //HideInAnswer if it is in the wrong country const countries = e.countryWhiteList.map(country => "_country!=" + country) //HideInAnswer if it is in the wrong country
json["hideInAnswer"] = {or: countries} json["hideInAnswer"] = {and: countries} // Should be and, as we want to hide if it does not match any of the countries
} else if (e.countryBlackList.length > 0) { } else if (e.countryBlackList.length > 0) {
const countries = e.countryBlackList.map(country => "_country=" + country) //HideInAnswer if it is in the wrong country const countries = e.countryBlackList.map(country => "_country=" + country) //HideInAnswer if it is in the wrong country
json["hideInAnswer"] = {or: countries} json["hideInAnswer"] = {or: countries}

View file

@ -1,6 +1,6 @@
key,image,description:en,countryWhiteList,countryBlackList,commonVoltages,commonCurrents,commonOutputs,description:nl,associatedVehicleTypes,neverAssociatedWith,extraVisualisationCondition key,image,description:en,countryWhiteList,countryBlackList,commonVoltages,commonCurrents,commonOutputs,description:nl,associatedVehicleTypes,neverAssociatedWith,extraVisualisationCondition
socket:schuko,CEE7_4F.svg,<b>Schuko wall plug</b> without ground pin (CEE7/4 type F),be;fr;ma;tn;pl;cs;sk;mo,,230,16,3.6 kW,<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F),*,, socket:schuko,CEE7_4F.svg,<b>Schuko wall plug</b> without ground pin (CEE7/4 type F),af;al;dz;as;ad;ao;am;aw;az;by;bt;ba;bg;cv;td;cl;hr;dk;eg;ee;et;fo;fr;pf;ge;gr;gl;gn;gw;is;id;ir;jo;kz;kg;la;lv;lr;ly;lt;lu;mo;mr;md;mc;mn;me;mz;nl;nc;ne;kp;mk;pt;qa;ro;ru;rw;sm;sk;si;kr;es;sr;tj;th;tl;tr;tm;ua;uy;uz;vn,,230,16,3.6 kW,<b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F),*,,
socket:typee,TypeE.svg,<b>European wall plug</b> with ground pin (CEE7/4 type E),,,230,16,3 kW;22 kW;,<b>Europese stekker</b> met aardingspin (CEE7/4 type E),*,, socket:typee,TypeE.svg,<b>European wall plug</b> with ground pin (CEE7/4 type E),be;bj;bf;bi;cm;cf;td;km;cz;dk;dj;gq;et;fo;fr;gf;pf;gl;gp;gw;la;lr;mg;ml;mq;mr;mu;mc;mn;ma;ne;pl;pt;rw;mf;pm;sn;sk;sy;tl;tn;uz,,230,16,3 kW;22 kW;,<b>Europese stekker</b> met aardingspin (CEE7/4 type E),*,,
socket:chademo,Chademo_type4.svg,<b>Chademo</b>,,,500,120,50 kW,<b>Chademo</b>,car;motorcar;hgv;bus,bicycle;scooter, socket:chademo,Chademo_type4.svg,<b>Chademo</b>,,,500,120,50 kW,<b>Chademo</b>,car;motorcar;hgv;bus,bicycle;scooter,
socket:type1_cable,Type1_J1772.svg,<b>Type 1 with cable</b> (J1772),,,200;240,32,3.7 kW;7 kW,<b>Type 1 met kabel</b> (J1772),car;motorcar;hgv;bus,bicycle;scooter, socket:type1_cable,Type1_J1772.svg,<b>Type 1 with cable</b> (J1772),,,200;240,32,3.7 kW;7 kW,<b>Type 1 met kabel</b> (J1772),car;motorcar;hgv;bus,bicycle;scooter,
socket:type1,Type1_J1772.svg,<b>Type 1 <i>without</i> cable</b> (J1772),,,200;240,32,3.7 kW;6.6 kW;7 kW;7.2 kW,<b>Type 1 <i>zonder</i> kabel</b> (J1772),car;motorcar;hgv;bus,bicycle;scooter, socket:type1,Type1_J1772.svg,<b>Type 1 <i>without</i> cable</b> (J1772),,,200;240,32,3.7 kW;6.6 kW;7 kW;7.2 kW,<b>Type 1 <i>zonder</i> kabel</b> (J1772),car;motorcar;hgv;bus,bicycle;scooter,

1 key image description:en countryWhiteList countryBlackList commonVoltages commonCurrents commonOutputs description:nl associatedVehicleTypes neverAssociatedWith extraVisualisationCondition
2 socket:schuko CEE7_4F.svg <b>Schuko wall plug</b> without ground pin (CEE7/4 type F) be;fr;ma;tn;pl;cs;sk;mo af;al;dz;as;ad;ao;am;aw;az;by;bt;ba;bg;cv;td;cl;hr;dk;eg;ee;et;fo;fr;pf;ge;gr;gl;gn;gw;is;id;ir;jo;kz;kg;la;lv;lr;ly;lt;lu;mo;mr;md;mc;mn;me;mz;nl;nc;ne;kp;mk;pt;qa;ro;ru;rw;sm;sk;si;kr;es;sr;tj;th;tl;tr;tm;ua;uy;uz;vn 230 16 3.6 kW <b>Schuko stekker</b> zonder aardingspin (CEE7/4 type F) *
3 socket:typee TypeE.svg <b>European wall plug</b> with ground pin (CEE7/4 type E) be;bj;bf;bi;cm;cf;td;km;cz;dk;dj;gq;et;fo;fr;gf;pf;gl;gp;gw;la;lr;mg;ml;mq;mr;mu;mc;mn;ma;ne;pl;pt;rw;mf;pm;sn;sk;sy;tl;tn;uz 230 16 3 kW;22 kW; <b>Europese stekker</b> met aardingspin (CEE7/4 type E) *
4 socket:chademo Chademo_type4.svg <b>Chademo</b> 500 120 50 kW <b>Chademo</b> car;motorcar;hgv;bus bicycle;scooter
5 socket:type1_cable Type1_J1772.svg <b>Type 1 with cable</b> (J1772) 200;240 32 3.7 kW;7 kW <b>Type 1 met kabel</b> (J1772) car;motorcar;hgv;bus bicycle;scooter
6 socket:type1 Type1_J1772.svg <b>Type 1 <i>without</i> cable</b> (J1772) 200;240 32 3.7 kW;6.6 kW;7 kW;7.2 kW <b>Type 1 <i>zonder</i> kabel</b> (J1772) car;motorcar;hgv;bus bicycle;scooter

View file

@ -343,6 +343,7 @@
}, },
"id": "Rock type (crag/rock/cliff only)" "id": "Rock type (crag/rock/cliff only)"
}, },
"reviews",
{ {
"id": "default_climbing_questions", "id": "default_climbing_questions",
"builtin": [ "builtin": [

View file

@ -129,6 +129,7 @@
}, },
"payment-options", "payment-options",
"opening_hours", "opening_hours",
"reviews",
{ {
"id": "shoe_rental", "id": "shoe_rental",
"question": { "question": {

View file

@ -29,7 +29,8 @@
"natural=stone" "natural=stone"
] ]
}, },
"climbing=" "climbing=",
"sport!=climbing"
] ]
} }
}, },

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 474 474" xml:space="preserve">
<g>
<path d="M318.002,137.333l-39.004-75.667C275.974,55.8,268.1,51,261.5,51h-49c-6.6,0-14.474,4.8-17.498,10.667l-39.004,75.667
c-2.149,4.169-4.015,10.296-4.918,15.667h171.84C322.017,147.63,320.151,141.503,318.002,137.333z"/>
<path d="M150.5,462c0,6.6,5.4,12,12,12h149c6.6,0,12-5.4,12-12V341.5h-173V462z"/>
<path d="M209.5,42h55c6.6,0,12-5.4,12-12V12c0-6.6-5.4-12-12-12h-55c-6.6,0-12,5.4-12,12v18C197.5,36.6,202.9,42,209.5,42z"/>
<path d="M327,216c1.925,0,3.5-5.4,3.5-12v-22c0-3.579-0.466-6.796-1.197-9H144.697c-0.731,2.204-1.197,5.421-1.197,9v22
c0,6.6,1.575,12,3.5,12s3.5,1.913,3.5,4.25s-1.575,4.25-3.5,4.25s-3.5,5.4-3.5,12v22c0,6.6,1.575,12,3.5,12s3.5,1.913,3.5,4.25
S148.925,279,147,279s-3.5,5.4-3.5,12v22c0,3.314,0.398,6.323,1.036,8.5h184.928c0.639-2.177,1.036-5.186,1.036-8.5v-22
c0-6.6-1.575-12-3.5-12s-3.5-1.913-3.5-4.25s1.575-4.25,3.5-4.25s3.5-5.4,3.5-12v-22c0-6.6-1.575-12-3.5-12s-3.5-1.913-3.5-4.25
S325.075,216,327,216z M234.125,294.973c-20.875,0-37.797-16.922-37.797-37.797S234.125,193,234.125,193
s37.797,43.301,37.797,64.176S255,294.973,234.125,294.973z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Unkown
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 82.376 100" enable-background="new 0 0 82.376 100" xml:space="preserve"><path d="M52.074,20.308c5.59,0,10.131-4.541,10.155-10.152C62.205,4.563,57.664,0.021,52.074,0
c-5.615,0.021-10.156,4.563-10.153,10.156C41.918,15.767,46.459,20.308,52.074,20.308L52.074,20.308z"></path><path d="M28.78,12.165c5.746-2.962,13.778,2.471,13.846,8.416v19.333l9.178,10.154c4.218,4.726-2.458,11.231-7.115,6.515
l-10.37-11.458c-0.669-0.637-1.152-1.395-1.141-3.149v-9.233L15.04,42.517v49.145C15.045,102.762,0.011,102.76,0,91.769V33.503
c0.011-3.246,1.297-6.405,4.506-8.253L28.78,12.165L28.78,12.165z"></path><path d="M66.41,62.012h15.966V32.96l-32.147-0.055c-4.107-0.001-5.652,4.657-3.094,7.605L66.41,62.012L66.41,62.012z"></path><path d="M57.178,31.604c0.675-0.007,1.23-0.563,1.249-1.249c-0.019-0.686-0.574-1.241-1.249-1.25
c-0.694,0.01-1.249,0.564-1.249,1.25C55.929,31.041,56.483,31.597,57.178,31.604L57.178,31.604z"></path><path d="M57.178,30.355"></path><path d="M60.491,31.82c0.684-0.008,1.238-0.563,1.249-1.247c-0.011-0.685-0.565-1.241-1.249-1.25c-0.687,0.01-1.242,0.566-1.25,1.25
C59.249,31.257,59.805,31.812,60.491,31.82L60.491,31.82z"></path><path d="M60.491,30.574"></path><path d="M60.708,28.13c0.68,0.016,1.234-0.54,1.249-1.197c-0.015-0.711-0.569-1.264-1.249-1.25c-0.689-0.015-1.244,0.539-1.249,1.25
C59.464,27.59,60.019,28.146,60.708,28.13L60.708,28.13z"></path><path d="M60.708,26.934"></path><path d="M57.396,27.91c0.684,0.008,1.238-0.549,1.25-1.248c-0.012-0.67-0.566-1.225-1.25-1.25c-0.686,0.025-1.241,0.58-1.248,1.25
C56.155,27.361,56.711,27.918,57.396,27.91L57.396,27.91z"></path><path d="M57.396,26.662"></path><path d="M58.482,24.545c0.705-0.026,1.289-0.609,1.304-1.304c-0.015-0.743-0.599-1.327-1.304-1.303
c-0.732-0.024-1.314,0.56-1.305,1.303C57.168,23.936,57.75,24.519,58.482,24.545L58.482,24.545z"></path><path d="M58.482,23.241"></path><path d="M62.501,25.197c0.723-0.028,1.307-0.61,1.303-1.306c0.004-0.743-0.58-1.327-1.303-1.3c-0.715-0.026-1.298,0.557-1.303,1.3
C61.203,24.587,61.786,25.169,62.501,25.197L62.501,25.197z"></path><path d="M62.501,23.892"></path><path d="M61.091,21.828c0.735-0.015,1.348-0.625,1.356-1.355c-0.009-0.781-0.621-1.394-1.356-1.414
c-0.775,0.02-1.386,0.633-1.359,1.414C59.705,21.204,60.315,21.813,61.091,21.828L61.091,21.828z"></path><path d="M61.091,20.473"></path><path d="M65.757,24.001c0.744-0.015,1.354-0.628,1.358-1.358c-0.004-0.781-0.614-1.392-1.358-1.41
c-0.767,0.019-1.38,0.629-1.356,1.41C64.377,23.373,64.99,23.987,65.757,24.001L65.757,24.001z"></path><path d="M65.757,22.644"></path><path d="M64.238,20.635c0.774-0.017,1.387-0.627,1.414-1.359c-0.027-0.778-0.64-1.392-1.414-1.41
c-0.734,0.019-1.35,0.632-1.357,1.41C62.889,20.008,63.504,20.619,64.238,20.635L64.238,20.635z"></path><path d="M64.238,19.276"></path><path d="M67.931,20.635c0.846-0.01,1.521-0.687,1.521-1.521c0-0.828-0.675-1.504-1.521-1.521c-0.819,0.018-1.494,0.693-1.521,1.521
C66.437,19.948,67.111,20.625,67.931,20.635L67.931,20.635z"></path><path d="M67.931,19.115"></path><path d="M69.125,25.306c0.819-0.012,1.494-0.688,1.519-1.52c-0.024-0.831-0.699-1.507-1.519-1.521
c-0.846,0.015-1.519,0.69-1.519,1.521C67.606,24.618,68.279,25.293,69.125,25.306L69.125,25.306z"></path><path d="M69.125,23.786"></path><path d="M71.404,22.428c0.889-0.02,1.597-0.727,1.577-1.577c0.02-0.897-0.688-1.605-1.577-1.627
c-0.859,0.022-1.568,0.73-1.572,1.627C69.836,21.701,70.545,22.408,71.404,22.428L71.404,22.428z"></path><path d="M71.404,20.851"></path><path d="M74.446,25.468c0.915,0.006,1.659-0.738,1.685-1.63c-0.025-0.943-0.77-1.685-1.685-1.681
c-0.921-0.004-1.664,0.738-1.683,1.681C72.782,24.73,73.525,25.474,74.446,25.468L74.446,25.468z"></path><path d="M74.446,23.838"></path><path d="M71.568,28.399c0.925,0.006,1.666-0.736,1.686-1.628c-0.02-0.942-0.761-1.689-1.686-1.683
c-0.912-0.006-1.656,0.741-1.626,1.683C69.912,27.663,70.656,28.405,71.568,28.399L71.568,28.399z"></path><path d="M71.568,26.771"></path></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: AIGA
SPDX-License-Identifier: CC0-1.0

View file

@ -30,7 +30,8 @@
{ {
"or": [ "or": [
"amenity=drinking_water", "amenity=drinking_water",
"drinking_water=yes" "drinking_water=yes",
"disused:amenity=drinking_water"
] ]
}, },
"man_made!=reservoir_covered", "man_made!=reservoir_covered",
@ -61,6 +62,11 @@
"cs": "Pitná voda" "cs": "Pitná voda"
} }
}, },
"titleIcons": [
"icons.defaults",
"auto:type",
"auto:seasonal"
],
"pointRendering": [ "pointRendering": [
{ {
"iconBadges": [ "iconBadges": [
@ -68,10 +74,15 @@
"if": { "if": {
"or": [ "or": [
"operational_status=broken", "operational_status=broken",
"operational_status=closed" "operational_status=closed",
"disused:amenity=drinking_water"
] ]
}, },
"then": "close:#c33" "then": "close:#c33"
},
{
"if": "tourism=artwork",
"then": "circle:white;./assets/layers/artwork/artwork.svg"
} }
], ],
"iconSize": "40,40", "iconSize": "40,40",
@ -147,6 +158,10 @@
"mappings": [ "mappings": [
{ {
"if": "operational_status=", "if": "operational_status=",
"addExtraTags": [
"disused:amenity=",
"amenity=drinking_water"
],
"then": { "then": {
"en": "This drinking water works", "en": "This drinking water works",
"nl": "Deze drinkwaterfontein werkt", "nl": "Deze drinkwaterfontein werkt",
@ -190,6 +205,46 @@
], ],
"id": "Still in use?" "id": "Still in use?"
}, },
{
"id": "type",
"question": {
"en": "What type of drinking water point is this?",
"nl": "Wat voor soort drinkwaterpunt is dit?"
},
"mappings": [
{
"if": "fountain=bubbler",
"icon": "./assets/layers/drinking_water/bubbler.svg",
"then": {
"en": "This is a bubbler fountain. A water jet to drink from is sent upwards, typically controlled by a push button."
},
"addExtraTags": [
"man_made="
]
},
{
"if": "fountain=bottle_refill",
"icon": "./assets/layers/drinking_water/bottle.svg",
"then": {
"en": "This is a bottle refill point where the water is sent downwards, typically controlled by a push button or a motion sensor. Drinking directly from the stream might be very hard or impossible."
},
"addExtraTags": [
"man_made=",
"bottle=yes"
]
},
{
"if": "man_made=water_tap",
"icon": "./assets/layers/drinking_water/tap.svg",
"then": {
"en": "This is a water tap. The water flows downward and the stream is controlled by a valve or push-button."
},
"addExtraTags": [
"fountain="
]
}
]
},
{ {
"question": { "question": {
"en": "How easy is it to fill water bottles?", "en": "How easy is it to fill water bottles?",
@ -235,8 +290,153 @@
} }
} }
], ],
"condition": "fountain!=bottle_refill",
"id": "Bottle refill" "id": "Bottle refill"
}, },
{
"id": "fee",
"question": {
"en": "Is this drinking water point free to use?"
},
"mappings": [
{
"if": "fee=no",
"then": {
"en": "Free to use"
}
},
{
"if": "fee=yes",
"then": {
"en": "One needs to pay to use this drinking water point"
}
}
]
},
{
"id": "seasonal",
"question": {
"en": "Is this drinking water point available all year round?"
},
"mappings": [
{
"if": "seasonal=no",
"then": {
"en": "This drinking water point is available all around the year"
}
},
{
"if": "seasonal=summer",
"then": {
"en": "This drinking water point is only available in summer"
}
},
{
"if": "seasonal=spring;summer;autumn",
"icon": "./assets/layers/drinking_water/no_winter.svg",
"then": {
"en": "This drinking water point is closed during the winter"
}
}
]
},
{
"builtin": "opening_hours_24_7",
"override": {
"questionHint": {
"en": "These are the opening hours if the drinking water fountain is operational."
},
"+mappings": [
{
"if": {
"and": [
"seasonal!=no",
"seasonal~*",
{
"or": [
{
"and": [
"seasonal!~.*winter.*",
"_now:date~....-(12|01|02)-.."
]
},
{
"and": [
"seasonal!~.*spring.*",
"_now:date~....-(03|04|05)-.."
]
},
{
"and": [
"seasonal!~.*summer.*",
"_now:date~....-(06|07|08)-.."
]
},
{
"and": [
"seasonal!~.*autumn.*",
"_now:date~....-(09|10|11)-.."
]
}
]
}
]
},
"then": {
"en": "This drinking water fountain is closed this season. As such, the opening hours are not shown."
},
"hideInAnswer": true
}
]
}
},
{
"id": "bench-artwork",
"question": {
"en": "Does this drinking water fountain have an artistic element?",
"nl": "Heeft dit drinkwaterpunt een geintegreerd kunstwerk?"
},
"mappings": [
{
"if": "tourism=artwork",
"addExtraTags": [
"not:tourism:artwork="
],
"then": {
"en": "This drinking water point has an integrated artwork",
"nl": "Dit drinkwaterpunt heeft een geintegreerd kunstwerk"
}
},
{
"if": "not:tourism:artwork=yes",
"then": {
"en": "This drinking water point does not have an integrated artwork",
"nl": "Dit drinkwaterpunt heeft geen geïntegreerd kunstwerk"
},
"addExtraTags": [
"tourism="
]
},
{
"if": "tourism=",
"then": {
"en": "This drinking water point <span class=\"subtle\">probably</span> doesn't have an integrated artwork",
"nl": "Dit drinkwaterpunt heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
},
"hideInAnswer": true
}
],
"questionHint": {
"en": "E.g. it has an integrated statue or other non-trivial, creative work",
"nl": "Bijvoorbeeld een standbeeld of ander, niet-triviaal kunstwerk"
}
},
{
"builtin": "artwork.*artwork-question",
"override": {
"condition": "tourism=artwork"
}
},
{ {
"id": "render-closest-drinking-water", "id": "render-closest-drinking-water",
"render": { "render": {
@ -293,6 +493,19 @@
"ca": "Es tracta d'una aixeta d'aigua o bomba d'aigua amb aigua no potable. <div class='subtle'> Per exemple les aixetes d'aigua amb aigua de pluja per aprofitar i regar les plantes properes</div>", "ca": "Es tracta d'una aixeta d'aigua o bomba d'aigua amb aigua no potable. <div class='subtle'> Per exemple les aixetes d'aigua amb aigua de pluja per aprofitar i regar les plantes properes</div>",
"cs": "Jedná se o vodovodní kohoutek nebo vodní čerpadlo s nepitnou vodou.<div class='subtle'>Příkladem jsou vodovodní kohoutky s dešťovou vodou pro zalévání rostlin v okolí</div>" "cs": "Jedná se o vodovodní kohoutek nebo vodní čerpadlo s nepitnou vodou.<div class='subtle'>Příkladem jsou vodovodní kohoutky s dešťovou vodou pro zalévání rostlin v okolí</div>"
} }
},
{
"if": {
"and": [
"amenity=",
"man_made=pump",
"historic=yes",
"drinking_water=no"
]
},
"then": {
"en": "This is a historic, manual water pump where no drinking water can be found"
}
} }
] ]
}, },

View file

@ -1,4 +1,24 @@
[ [
{
"path": "bottle.svg",
"license": "CC0-1.0",
"authors": [
"Unkown"
],
"sources": [
"https://www.svgrepo.com/svg/83123/water-bottle"
]
},
{
"path": "bubbler.svg",
"license": "CC0-1.0",
"authors": [
"AIGA"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:Drinking_Fountain_-_The_Noun_Project.svg"
]
},
{ {
"path": "drips.svg", "path": "drips.svg",
"license": "CC-BY-SA-4.0", "license": "CC-BY-SA-4.0",
@ -12,5 +32,31 @@
"sources": [ "sources": [
"https://osoc.be/editions/2020/cyclofix" "https://osoc.be/editions/2020/cyclofix"
] ]
},
{
"path": "no_winter.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "tap.svg",
"license": "CC0-1.0",
"authors": [
"Krzysztof Franek"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:Water_DIN-style.svg"
]
},
{
"path": "winter.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
} }
] ]

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg"
version="1.1"
width="307.91855"
height="343.46448"
viewBox="0 0 307.91855 343.46448"
sodipodi:docname="no_winter.svg"
inkscape:version="1.3.1 (1:1.3.1+202311172155+91b66b0783)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.2257936"
inkscape:cx="190.48884"
inkscape:cy="271.25284"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg" />
<g
id="g1"
transform="rotate(-0.2875813,162.85132,95.170788)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 5.008225,86.10653 302.13815,258.03113"
id="path3"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 153.09023,0.048073 1.65028,343.280557"
id="path3-7"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 302.54004,85.82932 5.2907,257.54738"
id="path4"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 5.786561,200.08323 53.987051,25.55191 -3.26951,61.20155"
id="path6" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 56.719332,59.041006 3.20013,59.642774 -55.391241,26.23225"
id="path7" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 201.87442,31.964241 152.44006,65.486554 101.3842,31.580519"
id="path8" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 203.26283,313.48917 -49.04644,-34.08735 -51.44157,33.31792"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 300.19729,199.07429 -53.31225,26.93149 4.84196,61.09726"
id="path10" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 251.37871,59.718988 -4.44648,59.562822 54.83066,27.38467"
id="path11" />
</g>
<path
style="fill:#000000;stroke:#e40000;stroke-width:25;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 23.115301,297.04602 290.36883,29.792506"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="255.66626"
height="237.48238"
id="svg2"
inkscape:version="1.3.1 (1:1.3.1+202311172155+91b66b0783)"
sodipodi:docname="tap.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata22">
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs20">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : -25.017624 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="600 : -25.017624 : 1"
inkscape:persp3d-origin="300 : -112.51762 : 1"
id="perspective24" />
<inkscape:perspective
id="perspective4417"
inkscape:persp3d-origin="0.5 : -287.18429 : 1"
inkscape:vp_z="1 : -287.01762 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : -287.01762 : 1"
sodipodi:type="inkscape:persp3d" />
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="995"
id="namedview18"
showgrid="false"
inkscape:zoom="2.3757615"
inkscape:cx="118.48832"
inkscape:cy="112.17456"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
inkscape:showpageshadow="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;stroke:none;stroke-width:0.5;marker:none;enable-background:accumulate"
d="m 130.85922,0.00198911 c -7.5726,-0.12704 -13.62649,5.86286999 -16.19543,8.89080999 l -47.50163,-1.41908 -0.45904,23.8757499 48.92071,-1.92008 c 0.87811,1.09025 2.18743,2.36572 3.96541,3.50637 l 0.74163,39.73956 H 93.7514 c 0.16847,-0.33722 -1.69824,9.11586 -3.04733,11.64596 -1.34909,2.52988 -1.368597,16.006961 -28.691187,20.054861 -27.32322,4.04785 -34.366163,5.90643 -48.196573,18.55589 -13.82978,12.64952 -13.6491,36.27328 -13.6491,36.27328 L 0,188.21563 41.32432,187.88171 c 0,0 -1.21054,-4.87794 -1.21054,-9.60054 0,-4.72247 -1.386495,-16.34901 3.167475,-25.62534 4.55398,-9.27634 10.882401,-6.14303 18.978211,-7.82964 l 175.514274,-3.39564 c 0,0 2.94683,0.32094 3.93643,1.28285 0.92877,0.90277 1.30836,3.65878 1.30836,3.65878 v 30.69852 h 12.64773 l -0.16721,-124.973861 -11.97952,0.16699 0.48595,36.10628 c -1.01213,2.36131 -3.71491,2.37924 -3.71491,2.37924 l -63.39018,0.16704 c 0,0 -6.55829,0.66502 -10.26861,-3.21417 -3.71032,-3.87919 -3.04733,-15.02695 -3.04733,-15.02695 h -22.14849 l 1.56994,-40.94625 c 1.00229,-0.79886 1.84131,-1.6162 2.505,-2.37936 l 49.004,1.04365 -0.45904,-23.8344099 -47.62688,2.12886 c -2.72771,-3.16073 -8.39659,-8.56263999 -15.56981,-8.68226999 z M 18.19948,194.30829 c -5.93312,17.25329 -15.89776,20.57384 -13.14873,31.682 1.43868,5.81337 6.39567,11.81612 13.81631,11.47847 7.42128,-0.33692 15.19407,-3.04332 15.19407,-15.86164 0,-12.81822 -15.86165,-27.29883 -15.86165,-27.29883 z"
id="path4390"
sodipodi:nodetypes="ccccccccccccccssccaccccccccsccccccccccscsc" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Krzysztof Franek
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg"
version="1.1"
width="307.54224"
height="343.37671"
viewBox="0 0 307.54224 343.37671"
sodipodi:docname="no_winter.svg"
inkscape:version="1.3.1 (1:1.3.1+202311172155+91b66b0783)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.2257936"
inkscape:cx="190.48884"
inkscape:cy="271.25285"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 5.008225,86.10653 302.13815,258.03113"
id="path3"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 153.09023,0.048073 1.65028,343.280557"
id="path3-7"
sodipodi:nodetypes="cc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 302.54004,85.82932 5.2907,257.54738"
id="path4"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 5.786561,200.08323 53.987051,25.55191 -3.26951,61.20155"
id="path6" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 56.719332,59.041006 3.20013,59.642774 -55.391241,26.23225"
id="path7" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="M 201.87442,31.964241 152.44006,65.486554 101.3842,31.580519"
id="path8" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 203.26283,313.48917 -49.04644,-34.08735 -51.44157,33.31792"
id="path9" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 300.19729,199.07429 -53.31225,26.93149 4.84196,61.09726"
id="path10" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-dasharray:none;stroke-opacity:1"
d="m 251.37871,59.718988 -4.44648,59.562822 54.83066,27.38467"
id="path11" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0-1.0

View file

@ -126,6 +126,16 @@
"nl": [ "nl": [
"(basis|lagere |middelbare |secondaire| secundaire)?school" "(basis|lagere |middelbare |secondaire| secundaire)?school"
], ],
"en": [
"east",
"north",
"northeast",
"northwest",
"south",
"southeast",
"southwest",
"west"
],
"fr": [ "fr": [
"allée (des |de la |de l'|de |du |d')?", "allée (des |de la |de l'|de |du |d')?",
"autoroute (des |de la |de l'|de |du |d')?", "autoroute (des |de la |de l'|de |du |d')?",
@ -207,11 +217,22 @@
"en": [ "en": [
"avenue", "avenue",
"boulevard", "boulevard",
"circle",
"church", "church",
"drive",
"expressway",
"freeway",
"highway",
"lane",
"parkway",
"path", "path",
"plaza", "plaza",
"road",
"square", "square",
"street" "street",
"terrace",
"trail",
"turnpike"
] ]
} }
} }

View file

@ -0,0 +1,47 @@
{
"#":"no-translations",
"#dont-translate": "*",
"pointRendering": [
{
"location": [
"point",
"centroid"
],
"marker": [
{
"icon": {
"render": "heart",
"mappings": [
{
"if": "_favourite=no",
"then": "heart_outline"
}
]
},
"color": "red"
}
]
}
],
"description": {
"en": "A generic map layer which shows locations that a contributor marked as favourite",
"nl": "Een laag met persoonlijke favourieten"
},
"name": {
"en": "Favourites",
"nl": "Favorieten"
},
"id": "favourite",
"source": "special",
"isShown": "_favourite=yes",
"minzoom": 0,
"title": {
"render": {
"en": "Favourite location",
"nl": "Favoriete locatie"
}
},
"tagRenderings": [
]
}

View file

@ -14,7 +14,8 @@
{ {
"id": "wikipedialink", "id": "wikipedialink",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank' rel='noopener'><img src='./assets/svg/wikipedia.svg' textmode='📖' alt='Wikipedia'/></a>", "render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank' rel='noopener'><img src='./assets/svg/wikipedia.svg' textmode='📖' alt='Wikipedia'/></a>",
"condition": { "condition": {
@ -27,7 +28,7 @@
{ {
"#": "ignore-image-in-then", "#": "ignore-image-in-then",
"if": "wikipedia=", "if": "wikipedia=",
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank' rel='noopener'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>" "then": "<a class='h-8' href='https://www.wikidata.org/wiki/{wikidata}' target='_blank' rel='noopener'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
} }
] ]
}, },
@ -66,10 +67,57 @@
], ],
"metacondition": "__showTimeSensitiveIcons!=no" "metacondition": "__showTimeSensitiveIcons!=no"
}, },
{
"id": "open_until",
"labels": [
"defaults",
"in_favourite"
],
"#": "Titleicon showing 'open until 17:00'",
"icon": {
"class": "w-20 mx-1 flex items-center"
},
"render": "{opening_hours_state()}",
"condition": {
"or": [
"seasonal=",
"seasonal=no",
{
"or": [
{
"and": [
"seasonal~.*winter.*",
"_now:date~....-(12|01|02)-.."
]
},
{
"and": [
"seasonal~.*spring.*",
"_now:date~....-(03|04|05)-.."
]
},
{
"and": [
"seasonal~.*summer.*",
"_now:date~....-(06|07|08)-.."
]
},
{
"and": [
"seasonal~.*autumn.*",
"_now:date~....-(09|10|11)-.."
]
}
]
}
]
}
},
{ {
"id": "phonelink", "id": "phonelink",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"render": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>", "render": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>",
"mappings": [ "mappings": [
@ -89,7 +137,8 @@
{ {
"id": "emaillink", "id": "emaillink",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>", "render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>",
"mappings": [ "mappings": [
@ -109,7 +158,8 @@
{ {
"id": "websitelink", "id": "websitelink",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>", "render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>",
"condition": "website~*" "condition": "website~*"
@ -117,7 +167,8 @@
{ {
"id": "smokingicon", "id": "smokingicon",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"mappings": [ "mappings": [
{ {
@ -140,6 +191,16 @@
"render": "{share_link()}", "render": "{share_link()}",
"metacondition": "_supports_sharing=yes" "metacondition": "_supports_sharing=yes"
}, },
{
"id": "favourite_title_icon",
"labels": [
"defaults"
],
"render": {
"*": "{favourite_icon()}"
},
"metacondition": "_loggedIn=true"
},
{ {
"id": "osmlink", "id": "osmlink",
"labels": [ "labels": [
@ -162,7 +223,8 @@
{ {
"id": "dogicon", "id": "dogicon",
"labels": [ "labels": [
"defaults" "defaults",
"in_favourite"
], ],
"mappings": [ "mappings": [
{ {
@ -193,6 +255,13 @@
"class": "w-20 mx-1 flex items-center" "class": "w-20 mx-1 flex items-center"
}, },
"render": "{rating()}" "render": "{rating()}"
},
{
"id": "favourite_icon",
"description": "Only for rendering",
"condition": "_favourite=yes",
"icon": "circle:white;heart:red",
"metacondition": "__showTimeSensitiveIcons!=no"
} }
] ]
} }

View file

@ -132,6 +132,7 @@
], ],
"tagRenderings": [ "tagRenderings": [
"images", "images",
"reviews",
{ {
"question": { "question": {
"nl": "Wat is de ondergrond van deze speeltuin?", "nl": "Wat is de ondergrond van deze speeltuin?",

62
assets/svg/center.svg Normal file
View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="544.02838"
height="544.02838"
viewBox="0 0 544.02838 544.02838"
version="1.1"
id="svg1"
sodipodi:docname="center.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="true"
inkscape:zoom="0.90326851"
inkscape:cx="393.57068"
inkscape:cy="250.756"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1">
<sodipodi:guide
position="171.95879,103.32864"
orientation="0,-1"
id="guide4"
inkscape:locked="false" />
<sodipodi:guide
position="271.68286,132.35281"
orientation="1,0"
id="guide5"
inkscape:locked="false" />
</sodipodi:namedview>
<path
d="m 365.63918,111.75001 h -62.375 V 15.9375 c 0,-8.75 -7,-15.9375 -15.625,-15.9375 h -31.1875 c -8.5625,0 -15.625,7.1875 -15.625,15.9375 v 95.81251 h -62.375 l 93.5625,127.75 z"
id="path1"
sodipodi:nodetypes="ccsssscccc" />
<path
d="m 432.27837,365.63919 v -62.375 h 95.8125 c 8.75,0 15.9375,-7 15.9375,-15.625 v -31.1875 c 0,-8.5625 -7.1875,-15.625 -15.9375,-15.625 h -95.8125 v -62.375 l -127.75,93.5625 z"
id="path1-5"
sodipodi:nodetypes="ccsssscccc" />
<path
d="m 178.38918,432.27838 h 62.375 v 95.8125 c 0,8.75 7,15.9375 15.625,15.9375 h 31.1875 c 8.5625,0 15.625,-7.1875 15.625,-15.9375 v -95.8125 h 62.375 l -93.5625,-127.75 z"
id="path2"
sodipodi:nodetypes="ccsssscccc" />
<path
d="m 111.75,178.38919 v 62.375 H 15.9375 c -8.75,0 -15.9375,7 -15.9375,15.625 v 31.1875 c 0,8.5625 7.1875,15.625 15.9375,15.625 H 111.75 v 62.375 l 127.74999,-93.5625 z"
id="path3"
sodipodi:nodetypes="ccsssscccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: Pieter Vander Vennet
SPDX-License-Identifier: CC0-1.0

View file

@ -153,6 +153,14 @@
"https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg" "https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg"
] ]
}, },
{
"path": "center.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{ {
"path": "checkmark.svg", "path": "checkmark.svg",
"license": "CC0-1.0", "license": "CC0-1.0",

View file

@ -69,10 +69,12 @@
}, },
"+titleIcons": [ "+titleIcons": [
{ {
"id": "climbing_length",
"render": "<div class='flex' style='word-wrap: normal; padding-right: 0.25rem;'><img src='./assets/themes/climbing/height.svg' style='height: 1.75rem;'/>{climbing:length}m</div>", "render": "<div class='flex' style='word-wrap: normal; padding-right: 0.25rem;'><img src='./assets/themes/climbing/height.svg' style='height: 1.75rem;'/>{climbing:length}m</div>",
"condition": "climbing:length~*" "condition": "climbing:length~*"
}, },
{ {
"id": "climbing_bolts",
"mappings": [ "mappings": [
{ {
"if": "__bolts_max~*", "if": "__bolts_max~*",
@ -95,6 +97,7 @@
"render": "<div class='w-8 flex justify-center rounded-right-full climbing-{__difficulty_max:char}'> {__difficulty_max}</div>" "render": "<div class='w-8 flex justify-center rounded-right-full climbing-{__difficulty_max:char}'> {__difficulty_max}</div>"
}, },
{ {
"id": "difficulty",
"render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>", "render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>",
"condition": "__difficulty:char~*" "condition": "__difficulty:char~*"
} }
@ -345,8 +348,7 @@
"key": "access:description" "key": "access:description"
} }
}, },
"questions", "questions"
"reviews"
] ]
} }
}, },

View file

@ -112,7 +112,15 @@
"lineRendering": [ "lineRendering": [
{ {
"width": "4", "width": "4",
"color": "#00a703" "color": {
"render": "#00a703",
"mappings": [
{
"if": "state=proposed",
"then": "#f0a513"
}
]
}
} }
], ],
"pointRendering": null "pointRendering": null
@ -134,8 +142,9 @@
}, },
"source": { "source": {
"osmTags": { "osmTags": {
"and": [ "or": [
"rcn_ref~*" "rcn_ref~*",
"proposed:rcn_ref~*"
] ]
} }
}, },
@ -146,14 +155,15 @@
"centroid" "centroid"
], ],
"label": { "label": {
"render": "<div style='position: absolute; top: -30px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>?</div>",
"mappings": [ "mappings": [
{ {
"if": "rcn_ref~*", "if": "rcn_ref~*",
"then": "<div style='position: absolute; top: -10px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>{rcn_ref}</div>" "then": "<div style='position: absolute; top: -30px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>{rcn_ref}</div>"
}, },
{ {
"if": "rcn_ref=", "if": "proposed:rcn_ref~*",
"then": "<div style='position: absolute; top: -10px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>?</div>" "then": "<div style='position: absolute; top: -32px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%; border-style:dotted; border-color:white; border-width: 2px'>{proposed:rcn_ref}</div>"
} }
] ]
} }
@ -162,6 +172,20 @@
"minzoom": 12, "minzoom": 12,
"title": { "title": {
"render": { "render": {
"en": "Cycle node",
"de": "Fahrradknotenpunkt",
"es": "nodo ciclista",
"nb_NO": "sykkelnode",
"nl": "Fietsknooppunt",
"fr": "nœud cycliste",
"ca": "node ciclista",
"cs": "uzel cyklu",
"pl": "węzeł rowerowy"
},
"mappings": [
{
"if": "rcn_ref~*",
"then": {
"en": "Cycle node <strong>{rcn_ref}</strong>", "en": "Cycle node <strong>{rcn_ref}</strong>",
"de": "Fahrradknotenpunkt <strong>{rcn_ref}</strong>", "de": "Fahrradknotenpunkt <strong>{rcn_ref}</strong>",
"es": "nodo ciclista <strong>{rcn_ref}</strong>", "es": "nodo ciclista <strong>{rcn_ref}</strong>",
@ -173,6 +197,15 @@
"pl": "węzeł rowerowy <strong>{rcn_ref}</strong>" "pl": "węzeł rowerowy <strong>{rcn_ref}</strong>"
} }
}, },
{
"if": "proposed:rcn_ref~*",
"then": {
"en": "Proposed cycle node <strong>{proposed:rcn_ref}</strong>",
"nl": "Voorgesteld fietsknooppunt <strong>{proposed:rcn_ref}</strong>"
}
}
]
},
"tagRenderings": [ "tagRenderings": [
{ {
"id": "node-rxn_ref", "id": "node-rxn_ref",
@ -197,7 +230,8 @@
"nl": "Dit fietsknooppunt heeft referentienummer {rcn_ref}", "nl": "Dit fietsknooppunt heeft referentienummer {rcn_ref}",
"de": "Knotenpunktnummer {rcn_ref} des Fahrradknotenpunktnetzwerks", "de": "Knotenpunktnummer {rcn_ref} des Fahrradknotenpunktnetzwerks",
"cs": "Tento cyklistický uzel má referenční číslo {rcn_ref}" "cs": "Tento cyklistický uzel má referenční číslo {rcn_ref}"
} },
"condition": "rcn_ref~*"
}, },
{ {
"builtin": "survey_date", "builtin": "survey_date",

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

@ -542,6 +542,9 @@
"leisure=garden", "leisure=garden",
"garden:type=facade_garden" "garden:type=facade_garden"
], ],
"snapToLayer": [
"walls_and_buildings"
],
"title": { "title": {
"nl": "een geveltuintje", "nl": "een geveltuintje",
"en": "a facade garden", "en": "a facade garden",

View file

@ -1,33 +1,13 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"title": { "title": {
"en": "Changes made with MapComplete", "en": "Changes made with MapComplete"
"ca": "Canvis fets amb MapComplete",
"cs": "Změny provedené pomocí MapComplete",
"de": "Mit MapComplete erstellte Änderungen",
"es": "Cambios realizados con MapComplete",
"fr": "Changements faits avec MapComplete",
"nl": "Wijzigingen gemaakt met MapComplete",
"pl": "Zmiany wprowadzone za pomocą MapComplete"
}, },
"shortDescription": { "shortDescription": {
"en": "Show changes made with MapComplete", "en": "Shows changes made by MapComplete"
"ca": "Mostra els canvis fets amb MapComplete",
"cs": "Zobrazení změn provedených pomocí nástroje MapComplete",
"de": "Mit MapComplete erstellte Änderungen anzeigen",
"es": "Mostrar cambios realizados con MapComplete",
"nl": "Toon wijzigingen gemaakt met MapComplete",
"pl": "Pokaż zmiany wprowadzone za pomocą MapComplete"
}, },
"description": { "description": {
"en": "This maps shows all the changes made with MapComplete", "en": "This maps shows all the changes made with MapComplete"
"ca": "Aquest mapa mostra tots els canvis fets amb MapComplete",
"cs": "Tato mapa zobrazuje všechny změny provedené pomocí MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
"es": "Este mapa muestra todos los cambios realizados con MapComplete",
"fr": "Cette carte montre tous les changements faits avec MapComplete",
"nl": "Deze kaart toont alle wijzigingen die met MapComplete gemaakt werden",
"pl": "Ta mapa pokazuje wszystkie zmiany wprowadzone za pomocą MapComplete"
}, },
"icon": "./assets/svg/logo.svg", "icon": "./assets/svg/logo.svg",
"hideFromOverview": true, "hideFromOverview": true,
@ -40,13 +20,7 @@
{ {
"id": "mapcomplete-changes", "id": "mapcomplete-changes",
"name": { "name": {
"en": "Changeset centers", "en": "Changeset centers"
"ca": "Centre del conjunt de canvis",
"cs": "Centrum změn",
"de": "Zentrum der Änderungssätze",
"es": "Centro del conjunto de cambios",
"nl": "Centerpunt van changeset",
"pl": "Centra zmian"
}, },
"minzoom": 0, "minzoom": 0,
"source": { "source": {
@ -57,85 +31,41 @@
}, },
"title": { "title": {
"render": { "render": {
"en": "Changeset for {theme}", "en": "Changeset for {theme}"
"ca": "Conjunt de canvis per a {theme}",
"cs": "Změna pro {theme}",
"de": "Änderungssatz für {theme}",
"es": "Conjunto de cambios para {theme}",
"fr": "Groupe de modifications pour {theme}",
"pl": "Zestaw zmian dla {theme}"
} }
}, },
"description": { "description": {
"en": "Show all MapComplete changes", "en": "Shows all MapComplete changes"
"ca": "Mostra tots els canvis de MapComplete",
"cs": "Zobrazit všechny změny MapComplete",
"de": "Alle MapComplete-Änderungen anzeigen",
"es": "Mostrar todos los cambios de MapComplete",
"nl": "Toon alle MapComplete wijzigingen",
"pl": "Wyświetl wszystkie zmiany MapComplete"
}, },
"tagRenderings": [ "tagRenderings": [
{ {
"id": "show_changeset_id", "id": "show_changeset_id",
"render": { "render": {
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>", "en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
"ca": "Conjunt de canvi <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"cs": "Změny <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"es": "Conjunto de cambios <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"fr": "Groupe de modifications <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"pl": "Zestaw zmian <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
} }
}, },
{ {
"id": "contributor", "id": "contributor",
"question": { "question": {
"en": "Which contributor made this change?", "en": "What contributor did make this change?"
"ca": "Quin col·laborador va fer aquest canvi?",
"cs": "Který přispěvatel tuto změnu provedl?",
"de": "Wer hat diese Änderung vorgenommen?",
"es": "¿Qué contribuidor hizo este cambio?",
"fr": "Quel contributeur a fait cette modification ?",
"nl": "Welke bijdrager maakte deze wijziging?",
"pl": "Który współautor dokonał tej zmiany?"
}, },
"freeform": { "freeform": {
"key": "user" "key": "user"
}, },
"render": { "render": {
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>", "en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
"ca": "Canvi fet per <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"cs": "Změna provedená <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"de": "Änderung von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"es": "Cambio realizado por <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"fr": "Modification faite par <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"nl": "Wijziging gemaakt door <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"pl": "Zmiana dokonana przez <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
} }
}, },
{ {
"id": "theme-id", "id": "theme-id",
"question": { "question": {
"en": "What theme was used to make this change?", "en": "What theme was used to make this change?"
"ca": "Quin tema es va utilitzar per fer aquest canvi?",
"cs": "Jaké téma bylo použito k provedení této změny?",
"de": "Welches Thema wurde für diese Änderung verwendet?",
"es": "¿Qué tema se utilizó para realizar este cambio?",
"fr": "Quel thème a été utilisé pour faire cette modification ?",
"pl": "Jakiego tematu użyto do wprowadzenia tej zmiany?"
}, },
"freeform": { "freeform": {
"key": "theme" "key": "theme"
}, },
"render": { "render": {
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>", "en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
"ca": "Canvi amb el tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"cs": "Změna s motivem <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"es": "Cambio con tema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"fr": "Modifié avec le thème <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"pl": "Zmiana za pomocą motywu <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
} }
}, },
{ {
@ -144,45 +74,19 @@
"key": "locale" "key": "locale"
}, },
"question": { "question": {
"en": "What locale (language) was this change made in?", "en": "What locale (language) was this change made in?"
"ca": "Amb quina configuració regional (idioma) s'ha fet aquest canvi?",
"cs": "V jakém národním prostředí (jazyce) byla tato změna provedena?",
"de": "In welcher Benutzersprache wurde diese Änderung vorgenommen?",
"es": "¿En qué configuración regional (idioma) se realizó este cambio?",
"fr": "En quelle langue est-ce que ce changement a été fait ?",
"nl": "In welke locale (taal) werd deze wijziging gemaakt?",
"pl": "W jakim języku wprowadzono tę zmianę?"
}, },
"render": { "render": {
"en": "User locale is {locale}", "en": "User locale is {locale}"
"ca": "La configuració regional de l'usuari és {locale}",
"cs": "Uživatelské prostředí je {locale}",
"de": "Benutzersprache {locale}",
"es": "La configuración regional del usuario es {locale}",
"nl": "De gebruikerstaal is {locale}",
"pl": "Ustawienia regionalne użytkownika to {locale}"
} }
}, },
{ {
"id": "host", "id": "host",
"render": { "render": {
"en": "Change made with <a href='{host}'>{host}</a>", "en": "Change with with <a href='{host}'>{host}</a>"
"ca": "Canviat fet amb <a href='{host}'>{host}</a>",
"cs": "Změna provedená pomocí <a href='{host}'>{host}</a>",
"de": "Geändert über <a href='{host}'>{host}</a>",
"es": "Cambio realizado con <a href='{host}'>{host}</a>",
"fr": "Modification faite avec <a href='{host}'>{host}</a>",
"nl": "Wijziging gemaakt met <a href='{host}'>{host}</a>",
"pl": "Zmiana dokonana w <a href='{host}'>{host}</a>"
}, },
"question": { "question": {
"en": "What host (website) was this change made with?", "en": "What host (website) was this change made with?"
"ca": "Amb quin amfitrió (lloc web) es va fer aquest canvi?",
"cs": "U jakého hostitele (webové stránky) byla tato změna provedena?",
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?",
"es": "¿Con qué host (página web) se realizó este cambio?",
"nl": "Met welke host (website) werd deze wijziging gemaakt?",
"pl": "Na jakim hoście (stronie internetowej) dokonano tej zmiany?"
}, },
"freeform": { "freeform": {
"key": "host" "key": "host"
@ -203,22 +107,10 @@
{ {
"id": "version", "id": "version",
"question": { "question": {
"en": "What version of MapComplete was used to make this change?", "en": "What version of MapComplete was used to make this change?"
"ca": "Quina versió de MapComplete es va utilitzar per fer aquest canvi?",
"cs": "Jaká verze aplikace MapComplete byla použita k provedení této změny?",
"de": "Mit welcher Version von MapComplete wurde diese Änderung gemacht?",
"es": "¿Qué versión de MapComplete se usó para realizar este cambio?",
"fr": "Quelle version de MapComplete a été utilisée pour faire cette modification ?",
"pl": "Która wersja MapComplete została wykorzystana, aby zrobić tę zmianę?"
}, },
"render": { "render": {
"en": "Made with {editor}", "en": "Made with {editor}"
"ca": "Fet amb {editor}",
"cs": "Vyrobeno pomocí {editor}",
"de": "Erstellt mit {editor}",
"es": "Realizado con {editor}",
"fr": "Fait avec {editor}",
"pl": "Zrobione za pomocą {editor}"
}, },
"freeform": { "freeform": {
"key": "editor" "key": "editor"
@ -392,6 +284,10 @@
"if": "theme=kerbs_and_crossings", "if": "theme=kerbs_and_crossings",
"then": "./assets/layers/kerbs/KerbIcon.svg" "then": "./assets/layers/kerbs/KerbIcon.svg"
}, },
{
"if": "theme=mapcomplete-changes",
"then": "./assets/svg/logo.svg"
},
{ {
"if": "theme=maproulette", "if": "theme=maproulette",
"then": "./assets/layers/maproulette/logomark.svg" "then": "./assets/layers/maproulette/logomark.svg"
@ -564,13 +460,23 @@
} }
], ],
"question": { "question": {
"en": "Theme name contains {search}", "en": "Themename contains {search}"
"ca": "El nom del tema conté {search}", }
"cs": "Název motivu obsahuje {search}", }
"de": "Themenname enthält {search}", ]
"es": "El nombre del tema contiene {search}", },
"nl": "Themenaam bevat {search}", {
"pl": "Nazwa tematu zawiera {search}" "id": "theme-not-search",
"options": [
{
"osmTags": "theme!~i~.*{search}.*",
"fields": [
{
"name": "search"
}
],
"question": {
"en": "Themename does <b>not</b> contain {search}"
} }
} }
] ]
@ -586,13 +492,7 @@
} }
], ],
"question": { "question": {
"en": "Made by contributor {search}", "en": "Made by contributor {search}"
"ca": "Fet pel col·laborador {search}",
"cs": "Vytvořil přispěvatel {search}",
"de": "Erstellt von {search}",
"es": "Hecho por el colaborador {search}",
"nl": "Gemaakt door bijdrager {search}",
"pl": "Wykonane przez współautora {search}"
} }
} }
] ]
@ -608,13 +508,7 @@
} }
], ],
"question": { "question": {
"en": "<b>Not</b> made by contributor {search}", "en": "<b>Not</b> made by contributor {search}"
"ca": "<b>No</b> fet pel col·laborador {search}",
"cs": "<b>Není</b> vytvořeno přispěvatelem {search}",
"de": "<b>Nicht</b> erstellt von {search}",
"es": "<b>No</b> hecho por el colaborador {search}",
"nl": "<b>Niet</b> gemaakt door bijdrager {search}",
"pl": "<b>Nie</b> wykonane przez współautora {search}"
} }
} }
] ]
@ -631,13 +525,7 @@
} }
], ],
"question": { "question": {
"en": "Made before {search}", "en": "Made before {search}"
"ca": "Fet abans de {search}",
"cs": "Vytvořeno před {search}",
"de": "Erstellt vor {search}",
"es": "Hecho antes de {search}",
"nl": "Gemaakt voor {search}",
"pl": "Stworzone przed {search}"
} }
} }
] ]
@ -654,13 +542,7 @@
} }
], ],
"question": { "question": {
"en": "Made after {search}", "en": "Made after {search}"
"ca": "Fet després de {search}",
"cs": "Vytvořeno po {search}",
"de": "Erstellt nach {search}",
"es": "Hecho después de {search}",
"nl": "Gemaakt na {search}",
"pl": "Stworzone po {search}"
} }
} }
] ]
@ -676,14 +558,7 @@
} }
], ],
"question": { "question": {
"en": "User language (iso-code) {search}", "en": "User language (iso-code) {search}"
"ca": "Idioma de l'usuari (codi iso) {search}",
"cs": "Jazyk uživatele (iso-kód) {search}",
"de": "Benutzersprache (ISO-Code) {search}",
"es": "Use idioma (ISO-code) {search}",
"fr": "Langage utilisateur (code-ISO) {search}",
"nl": "De taal van de bijdrager is {search}",
"pl": "Język użytkownika (kod iso) {search}"
} }
} }
] ]
@ -699,13 +574,7 @@
} }
], ],
"question": { "question": {
"en": "Made with host {search}", "en": "Made with host {search}"
"ca": "Fet amb l'amfitrió {search}",
"cs": "Vytvořeno pomocí hostitele {search}",
"de": "Erstellt mit Host {search}",
"es": "Hecho con el host {search}",
"nl": "Gemaakt met host {search}",
"pl": "Wykonane z hostem {search}"
} }
} }
] ]
@ -716,14 +585,29 @@
{ {
"osmTags": "add-image>0", "osmTags": "add-image>0",
"question": { "question": {
"en": "Changeset added at least one image", "en": "Changeset added at least one image"
"ca": "El conjunt de canvis ha afegit almenys una imatge", }
"cs": "Sada změn přidala alespoň jeden obrázek", }
"de": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt", ]
"es": "El conjunto de cambios ha añadido al menos una imagen", },
"fr": "Le groupe de modifications a ajouté au moins une image", {
"nl": "Changeset bevat minstens één afbeelding", "id": "exclude_grb",
"pl": "Zestaw zmian dodał co najmniej jedno zdjęcie" "options": [
{
"osmTags": "theme!=grb",
"question": {
"en": "Exclude GRB theme"
}
}
]
},
{
"id": "exclude_etymology",
"options": [
{
"osmTags": "theme!=etymology",
"question": {
"en": "Exclude etymology theme"
} }
} }
] ]
@ -738,13 +622,7 @@
{ {
"id": "link_to_more", "id": "link_to_more",
"render": { "render": {
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>", "en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>"
"ca": "Es pot trobar més estadística <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
"cs": "Další statistiky najdete <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"de": "Mehr Statistiken gibt es <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>",
"es": "Puede encontrar más estadísticas <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
"fr": "D'autres statistiques sont disponibles <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>ici</a>",
"pl": "Więcej statystyk można znaleźć <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>tutaj</a>"
} }
}, },
{ {

View file

@ -151,6 +151,22 @@
} }
] ]
}, },
{
"id": "theme-not-search",
"options": [
{
"osmTags": "theme!~i~.*{search}.*",
"fields": [
{
"name": "search"
}
],
"question": {
"en": "Themename does <b>not</b> contain {search}"
}
}
]
},
{ {
"id": "created_by", "id": "created_by",
"options": [ "options": [
@ -259,6 +275,28 @@
} }
} }
] ]
},
{
"id": "exclude_grb",
"options": [
{
"osmTags": "theme!=grb",
"question": {
"en": "Exclude GRB theme"
}
}
]
},
{
"id": "exclude_etymology",
"options": [
{
"osmTags": "theme!=etymology",
"question": {
"en": "Exclude etymology theme"
}
}
]
} }
] ]
}, },

View file

@ -166,31 +166,31 @@
{ {
"if": "sidewalk:left|right=yes", "if": "sidewalk:left|right=yes",
"then": { "then": {
"en": "Yes, there is a sidewalk on this side of the road", "en": "There is a sidewalk on this side of the road",
"de": "Ja, es gibt einen Bürgersteig auf dieser Straßenseite", "de": "Es gibt einen Bürgersteig auf dieser Straßenseite",
"da": "Ja, der er et fortov på denne side af vejen", "da": "Der er et fortov på denne side af vejen",
"nl": "Ja, er is een stoep aan deze kant van de weg", "nl": "Er is een stoep aan deze kant van de weg",
"fr": "Oui, il y a un trottoir de ce côté de la route", "fr": "Il y a un trottoir de ce côté de la route",
"ca": "Sí, hi ha una vorera a aquest costat del carrer", "ca": "Hi ha una vorera a aquest costat del carrer",
"es": "Sí, hay una acera en este lado de la calle", "es": "Hay una acera en este lado de la calle",
"cs": "Ano, na této straně silnice je chodník", "cs": "Na této straně silnice je chodník",
"it": "Sì, c'è un marciapiede su questo lato della strada", "it": "C'è un marciapiede su questo lato della strada",
"pl": "Tak, jest chodnik z boku drogi" "pl": "Jest chodnik z boku drogi"
} }
}, },
{ {
"if": "sidewalk:left|right=no", "if": "sidewalk:left|right=no",
"then": { "then": {
"en": "No, there is no sidewalk to walk on", "en": "There is no sidewalk to walk on",
"de": "Nein, es gibt keinen Bürgersteig für Fußgänger", "de": "Es gibt keinen Bürgersteig für Fußgänger",
"da": "Nej, der er ikke noget fortov at gå på", "da": "Der er ikke noget fortov at gå på",
"nl": "Nee, er is geen stoep om op te lopen", "nl": "Er is geen stoep om op te lopen",
"fr": "Non, il n'y a pas de trottoir où marcher", "fr": "Il n'y a pas de trottoir où marcher",
"ca": "No, no hi ha vorera per la que caminar", "ca": "No hi ha vorera per la que caminar",
"es": "No, no hay acera por la que caminar", "es": "No hay acera por la que caminar",
"cs": "Ne, není tu žádný chodník", "cs": "Není tu žádný chodník",
"it": "No, non c'è un marciapiede su cui camminare", "it": "Non c'è un marciapiede su cui camminare",
"pl": "Nie, nie ma chodnika, którym można chodzić" "pl": "Nie ma chodnika, którym można chodzić"
} }
}, },
{ {

View file

@ -35,10 +35,15 @@
"source": { "source": {
"osmTags": { "osmTags": {
"and": [ "and": [
"network=rwn", {
"network:type=node_network", "or": [
"route=hiking",
"route=foot" "route=foot"
] ]
},
"network=rwn",
"network:type=node_network"
]
} }
}, },
"minzoom": 12, "minzoom": 12,
@ -72,7 +77,15 @@
"lineRendering": [ "lineRendering": [
{ {
"width": "4", "width": "4",
"color": "#452b29" "color": {
"render": "#452b29",
"mappings": [
{
"if": "state=proposed",
"then": "#f0a513"
}
]
}
} }
], ],
"pointRendering": null, "pointRendering": null,
@ -123,8 +136,9 @@
}, },
"source": { "source": {
"osmTags": { "osmTags": {
"and": [ "or": [
"rwn_ref~*" "rwn_ref~*",
"proposed:rwn_ref~*"
] ]
} }
}, },
@ -135,14 +149,15 @@
"centroid" "centroid"
], ],
"label": { "label": {
"render": "<div style='position: absolute; top: -30px; right: -10px; color: white; background-color: #452b29; width: 20px; height: 20px; border-radius: 100%'>?</div>",
"mappings": [ "mappings": [
{ {
"if": "rwn_ref~*", "if": "rwn_ref~*",
"then": "<div style='position: absolute; top: -10px; right: -10px; color: white; background-color: #452b29; width: 20px; height: 20px; border-radius: 100%'>{rwn_ref}</div>" "then": "<div style='position: absolute; top: -30px; right: -10px; color: white; background-color: #452b29; width: 20px; height: 20px; border-radius: 100%'>{rwn_ref}</div>"
}, },
{ {
"if": "rwn_ref=", "if": "proposed:rwn_ref~*",
"then": "<div style='position: absolute; top: -10px; right: -10px; color: white; background-color: #452b29; width: 20px; height: 20px; border-radius: 100%'>?</div>" "then": "<div style='position: absolute; top: -31px; right: -10px; color: white; background-color: #452b29; width: 22px; height: 22px; border-radius: 100%; border-style:dotted; border-color:white; border-width: 2px'>{proposed:rwn_ref}</div>"
} }
] ]
} }
@ -151,11 +166,27 @@
"minzoom": 12, "minzoom": 12,
"title": { "title": {
"render": { "render": {
"en": "Walking node",
"nl": "Wandelknooppunt"
},
"mappings": [
{
"if": "rwn_ref~*",
"then": {
"en": "Walking node <strong>{rwn_ref}</strong>", "en": "Walking node <strong>{rwn_ref}</strong>",
"nl": "Wandelknooppunt <strong>{rwn_ref}</strong>", "nl": "Wandelknooppunt <strong>{rwn_ref}</strong>",
"de": "Wanderknoten <strong>{rwn_ref}</strong>" "de": "Wanderknoten <strong>{rwn_ref}</strong>"
} }
}, },
{
"if": "proposed:rwn_ref~*",
"then": {
"en": "Proposed walking node <strong>{proposed:rwn_ref}</strong>",
"nl": "Voorgesteld wandelknooppunt <strong>{proposed:rwn_ref}</strong>"
}
}
]
},
"tagRenderings": [ "tagRenderings": [
{ {
"id": "node-rwn_ref", "id": "node-rwn_ref",
@ -179,7 +210,8 @@
"en": "This walking node has reference number {rwn_ref}", "en": "This walking node has reference number {rwn_ref}",
"nl": "Dit wandelknooppunt heeft referentienummer {rwn_ref}", "nl": "Dit wandelknooppunt heeft referentienummer {rwn_ref}",
"de": "Dieser Wanderknoten hat die Referenznummer {rwn_ref}" "de": "Dieser Wanderknoten hat die Referenznummer {rwn_ref}"
} },
"condition": "rwn_ref~*"
}, },
{ {
"builtin": "survey_date", "builtin": "survey_date",

View file

@ -50,6 +50,22 @@
"panelIntro": "<h3>Your personal theme</h3>Activate your favourite layers from all the official themes", "panelIntro": "<h3>Your personal theme</h3>Activate your favourite layers from all the official themes",
"reload": "Reload the data" "reload": "Reload the data"
}, },
"favouritePoi": {
"button": {
"isFavourite": "This location is currently marked as favourite and will show up on all thematic maps of MapComplete you visit.",
"markAsFavouriteTitle": "Mark this location as favourite location",
"markDescription": "Add this location to a personal list of your favourites",
"unmark": "Remove from your personal list of favourites",
"unmarkNotDeleted": "This point will not be deleted and still be visible on the appropriate map for you and others"
},
"downloadGeojson": "Download your favourites as geojson",
"downloadGpx": "Download your favourites as GPX",
"intro": "You marked {length} locations as a favourite location.",
"introPrivacy": "This list is only visible to you",
"loginToSeeList": "Login to see the list of locations you marked as favourite",
"tab": "Your favourites",
"title": "Your favourite locations"
},
"flyer": { "flyer": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen", "aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.org", "callToAction": "Test it on mapcomplete.org",
@ -404,6 +420,7 @@
"key": "Key combination", "key": "Key combination",
"openLayersPanel": "Opens the layers and filters panel", "openLayersPanel": "Opens the layers and filters panel",
"selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers", "selectAerial": "Set the background to aerial or satellite imagery. Toggles between the two best, available layers",
"selectFavourites": "Open the favourites page",
"selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used", "selectItem": "Select the POI which is closest to the map center (crosshair). Only when in keyboard navigation is used",
"selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers", "selectMap": "Set the background to a map from external sources. Toggles between the two best, available layers",
"selectMapnik": "Set the background layer to OpenStreetMap-carto", "selectMapnik": "Set the background layer to OpenStreetMap-carto",
@ -419,6 +436,7 @@
"isDeleted": "Deleted", "isDeleted": "Deleted",
"nearby": { "nearby": {
"link": "This picture shows the object", "link": "This picture shows the object",
"noNearbyImages": "No nearby images were found",
"seeNearby": "Browse and link nearby pictures", "seeNearby": "Browse and link nearby pictures",
"title": "Nearby streetview imagery" "title": "Nearby streetview imagery"
}, },

View file

@ -1993,7 +1993,7 @@
"name": "Ladestationen", "name": "Ladestationen",
"presets": { "presets": {
"0": { "0": {
"title": "eine Ladestation für Elektrofahrräder mit einer normalen europäischen Steckdose <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (zum Laden von Elektrofahrrädern)" "title": "eine Ladestation für Elektrofahrräder"
}, },
"1": { "1": {
"title": "Eine Ladestation für Elektrofahrzeuge" "title": "Eine Ladestation für Elektrofahrzeuge"

View file

@ -1993,7 +1993,7 @@
"name": "Charging stations", "name": "Charging stations",
"presets": { "presets": {
"0": { "0": {
"title": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)" "title": "a charging station for electrical bikes"
}, },
"1": { "1": {
"title": "a charging station for cars" "title": "a charging station for cars"
@ -4218,6 +4218,9 @@
}, },
"1": { "1": {
"then": "This is a water tap or water pump with non-drinkable water.<div class='subtle'>Examples are water taps with rain water to tap water for nearby plants</div>" "then": "This is a water tap or water pump with non-drinkable water.<div class='subtle'>Examples are water taps with rain water to tap water for nearby plants</div>"
},
"2": {
"then": "This is a historic, manual water pump where no drinking water can be found"
} }
} }
}, },
@ -4256,8 +4259,72 @@
"question": "Is this drinking water spot still operational?", "question": "Is this drinking water spot still operational?",
"render": "The operational status is <i>{operational_status}</i>" "render": "The operational status is <i>{operational_status}</i>"
}, },
"bench-artwork": {
"mappings": {
"0": {
"then": "This drinking water point has an integrated artwork"
},
"1": {
"then": "This drinking water point does not have an integrated artwork"
},
"2": {
"then": "This drinking water point <span class=\"subtle\">probably</span> doesn't have an integrated artwork"
}
},
"question": "Does this drinking water fountain have an artistic element?",
"questionHint": "E.g. it has an integrated statue or other non-trivial, creative work"
},
"fee": {
"mappings": {
"0": {
"then": "Free to use"
},
"1": {
"then": "One needs to pay to use this drinking water point"
}
},
"question": "Is this drinking water point free to use?"
},
"opening_hours_24_7": {
"override": {
"+mappings": {
"0": {
"then": "This drinking water fountain is closed this season. As such, the opening hours are not shown."
}
},
"questionHint": "These are the opening hours if the drinking water fountain is operational."
}
},
"render-closest-drinking-water": { "render-closest-drinking-water": {
"render": "<a href='#{_closest_other_drinking_water_id}'>There is another drinking water fountain at {_closest_other_drinking_water_distance} meters</a>" "render": "<a href='#{_closest_other_drinking_water_id}'>There is another drinking water fountain at {_closest_other_drinking_water_distance} meters</a>"
},
"seasonal": {
"mappings": {
"0": {
"then": "This drinking water point is available all around the year"
},
"1": {
"then": "This drinking water point is only available in summer"
},
"2": {
"then": "This drinking water point is closed during the winter"
}
},
"question": "Is this drinking water point available all year round?"
},
"type": {
"mappings": {
"0": {
"then": "This is a bubbler fountain. A water jet to drink from is sent upwards, typically controlled by a push button."
},
"1": {
"then": "This is a bottle refill point where the water is sent downwards, typically controlled by a push button or a motion sensor. Drinking directly from the stream might be very hard or impossible."
},
"2": {
"then": "This is a water tap. The water flows downward and the stream is controlled by a valve or push-button."
}
},
"question": "What type of drinking water point is this?"
} }
}, },
"title": { "title": {

View file

@ -1842,7 +1842,7 @@
"name": "Oplaadpunten", "name": "Oplaadpunten",
"presets": { "presets": {
"0": { "0": {
"title": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)" "title": "een oplaadpunt voor elektrische fietsen"
}, },
"1": { "1": {
"title": "een oplaadstation voor elektrische auto's" "title": "een oplaadstation voor elektrische auto's"
@ -4092,8 +4092,26 @@
"question": "Is deze drinkwaterkraan nog steeds werkende?", "question": "Is deze drinkwaterkraan nog steeds werkende?",
"render": "Deze waterkraan-status is <i>{operational_status}</i>" "render": "Deze waterkraan-status is <i>{operational_status}</i>"
}, },
"bench-artwork": {
"mappings": {
"0": {
"then": "Dit drinkwaterpunt heeft een geintegreerd kunstwerk"
},
"1": {
"then": "Dit drinkwaterpunt heeft geen geïntegreerd kunstwerk"
},
"2": {
"then": "Dit drinkwaterpunt heeft <span class=\"subtle\">waarschijnlijk</span> geen geïntegreerd kunstwerk"
}
},
"question": "Heeft dit drinkwaterpunt een geintegreerd kunstwerk?",
"questionHint": "Bijvoorbeeld een standbeeld of ander, niet-triviaal kunstwerk"
},
"render-closest-drinking-water": { "render-closest-drinking-water": {
"render": "<a href='#{_closest_other_drinking_water_id}'>Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter</a>" "render": "<a href='#{_closest_other_drinking_water_id}'>Er bevindt zich een ander drinkwaterpunt op {_closest_other_drinking_water_distance} meter</a>"
},
"type": {
"question": "Wat voor soort drinkwaterpunt is dit?"
} }
}, },
"title": { "title": {

View file

@ -509,7 +509,12 @@
} }
}, },
"title": { "title": {
"render": "node ciclista <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "node ciclista <strong>{rcn_ref}</strong>"
}
},
"render": "node ciclista"
} }
} }
}, },
@ -860,49 +865,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Fet pel col·laborador {search}" "question": "Fet pel col·laborador {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>No</b> fet pel col·laborador {search}" "question": "<b>No</b> fet pel col·laborador {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Fet abans de {search}" "question": "Fet abans de {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Fet després de {search}" "question": "Fet després de {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Idioma de l'usuari (codi iso) {search}" "question": "Idioma de l'usuari (codi iso) {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Fet amb l'amfitrió {search}" "question": "Fet amb l'amfitrió {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "El conjunt de canvis ha afegit almenys una imatge" "question": "El conjunt de canvis ha afegit almenys una imatge"
@ -1159,10 +1164,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Sí, hi ha una vorera a aquest costat del carrer" "then": "Hi ha una vorera a aquest costat del carrer"
}, },
"1": { "1": {
"then": "No, no hi ha vorera per la que caminar" "then": "No hi ha vorera per la que caminar"
}, },
"2": { "2": {
"then": "Hi ha una vorera mapejada separadament per on caminar" "then": "Hi ha una vorera mapejada separadament per on caminar"

View file

@ -519,7 +519,12 @@
} }
}, },
"title": { "title": {
"render": "uzel cyklu <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "uzel cyklu <strong>{rcn_ref}</strong>"
}
},
"render": "uzel cyklu"
} }
}, },
"2": { "2": {
@ -895,49 +900,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Vytvořil přispěvatel {search}" "question": "Vytvořil přispěvatel {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>Není</b> vytvořeno přispěvatelem {search}" "question": "<b>Není</b> vytvořeno přispěvatelem {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Vytvořeno před {search}" "question": "Vytvořeno před {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Vytvořeno po {search}" "question": "Vytvořeno po {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Jazyk uživatele (iso-kód) {search}" "question": "Jazyk uživatele (iso-kód) {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Vytvořeno pomocí hostitele {search}" "question": "Vytvořeno pomocí hostitele {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "Sada změn přidala alespoň jeden obrázek" "question": "Sada změn přidala alespoň jeden obrázek"
@ -1194,10 +1199,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Ano, na této straně silnice je chodník" "then": "Na této straně silnice je chodník"
}, },
"1": { "1": {
"then": "Ne, není tu žádný chodník" "then": "Není tu žádný chodník"
}, },
"2": { "2": {
"then": "Na mapě je vyznačen samostatný chodník" "then": "Na mapě je vyznačen samostatný chodník"

View file

@ -806,10 +806,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Ja, der er et fortov på denne side af vejen" "then": "Der er et fortov på denne side af vejen"
}, },
"1": { "1": {
"then": "Nej, der er ikke noget fortov at gå på" "then": "Der er ikke noget fortov at gå på"
}, },
"2": { "2": {
"then": "Der er et særskilt kortlagt fortov at gå på" "then": "Der er et særskilt kortlagt fortov at gå på"

View file

@ -524,7 +524,12 @@
} }
}, },
"title": { "title": {
"render": "Fahrradknotenpunkt <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "Fahrradknotenpunkt <strong>{rcn_ref}</strong>"
}
},
"render": "Fahrradknotenpunkt"
} }
}, },
"2": { "2": {
@ -900,49 +905,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Erstellt von {search}" "question": "Erstellt von {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>Nicht</b> erstellt von {search}" "question": "<b>Nicht</b> erstellt von {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Erstellt vor {search}" "question": "Erstellt vor {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Erstellt nach {search}" "question": "Erstellt nach {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Benutzersprache (ISO-Code) {search}" "question": "Benutzersprache (ISO-Code) {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Erstellt mit Host {search}" "question": "Erstellt mit Host {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt" "question": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt"
@ -1199,10 +1204,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Ja, es gibt einen Bürgersteig auf dieser Straßenseite" "then": "Es gibt einen Bürgersteig auf dieser Straßenseite"
}, },
"1": { "1": {
"then": "Nein, es gibt keinen Bürgersteig für Fußgänger" "then": "Es gibt keinen Bürgersteig für Fußgänger"
}, },
"2": { "2": {
"then": "Es gibt einen separat kartierten Bürgersteig für Fußgänger" "then": "Es gibt einen separat kartierten Bürgersteig für Fußgänger"
@ -1422,7 +1427,11 @@
} }
}, },
"title": { "title": {
"render": "Wanderknoten <strong>{rwn_ref}</strong>" "mappings": {
"0": {
"then": "Wanderknoten <strong>{rwn_ref}</strong>"
}
}
} }
}, },
"2": { "2": {

View file

@ -524,7 +524,15 @@
} }
}, },
"title": { "title": {
"render": "Cycle node <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "Cycle node <strong>{rcn_ref}</strong>"
},
"1": {
"then": "Proposed cycle node <strong>{proposed:rcn_ref}</strong>"
}
},
"render": "Cycle node"
} }
}, },
"2": { "2": {
@ -903,46 +911,67 @@
"1": { "1": {
"options": { "options": {
"0": { "0": {
"question": "Made by contributor {search}" "question": "Theme name does <b>not</b> contain {search}"
} }
} }
}, },
"2": { "2": {
"options": { "options": {
"0": { "0": {
"question": "<b>Not</b> made by contributor {search}" "question": "Made by contributor {search}"
} }
} }
}, },
"3": { "3": {
"options": { "options": {
"0": { "0": {
"question": "Made before {search}" "question": "<b>Not</b> made by contributor {search}"
} }
} }
}, },
"4": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Made after {search}" "question": "Made before {search}"
} }
} }
}, },
"5": { "5": {
"options": { "options": {
"0": { "0": {
"question": "User language (iso-code) {search}" "question": "Made after {search}"
} }
} }
}, },
"6": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Made with host {search}" "question": "User language (iso-code) {search}"
} }
} }
}, },
"7": { "7": {
"options": {
"0": {
"question": "Made with host {search}"
}
}
},
"8": {
"options": {
"0": {
"question": "Changeset added at least one image"
}
}
},
"9": {
"options": {
"0": {
"question": "Made with host {search}"
}
}
},
"10": {
"options": { "options": {
"0": { "0": {
"question": "Changeset added at least one image" "question": "Changeset added at least one image"
@ -1199,10 +1228,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Yes, there is a sidewalk on this side of the road" "then": "There is a sidewalk on this side of the road"
}, },
"1": { "1": {
"then": "No, there is no sidewalk to walk on" "then": "There is no sidewalk to walk on"
}, },
"2": { "2": {
"then": "There is a separately mapped sidewalk to walk on" "then": "There is a separately mapped sidewalk to walk on"
@ -1422,7 +1451,15 @@
} }
}, },
"title": { "title": {
"render": "Walking node <strong>{rwn_ref}</strong>" "mappings": {
"0": {
"then": "Walking node <strong>{rwn_ref}</strong>"
},
"1": {
"then": "Proposed walking node <strong>{proposed:rwn_ref}</strong>"
}
},
"render": "Walking node"
} }
}, },
"2": { "2": {

View file

@ -509,7 +509,12 @@
} }
}, },
"title": { "title": {
"render": "nodo ciclista <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "nodo ciclista <strong>{rcn_ref}</strong>"
}
},
"render": "nodo ciclista"
} }
} }
}, },
@ -864,49 +869,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Hecho por el colaborador {search}" "question": "Hecho por el colaborador {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>No</b> hecho por el colaborador {search}" "question": "<b>No</b> hecho por el colaborador {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Hecho antes de {search}" "question": "Hecho antes de {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Hecho después de {search}" "question": "Hecho después de {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Use idioma (ISO-code) {search}" "question": "Use idioma (ISO-code) {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Hecho con el host {search}" "question": "Hecho con el host {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "El conjunto de cambios ha añadido al menos una imagen" "question": "El conjunto de cambios ha añadido al menos una imagen"
@ -1163,10 +1168,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Sí, hay una acera en este lado de la calle" "then": "Hay una acera en este lado de la calle"
}, },
"1": { "1": {
"then": "No, no hay acera por la que caminar" "then": "No hay acera por la que caminar"
}, },
"2": { "2": {
"then": "Hay una acera mapeada por separado por la que caminar" "then": "Hay una acera mapeada por separado por la que caminar"

View file

@ -506,7 +506,12 @@
} }
}, },
"title": { "title": {
"render": "nœud cycliste <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "nœud cycliste <strong>{rcn_ref}</strong>"
}
},
"render": "nœud cycliste"
} }
} }
}, },
@ -842,14 +847,14 @@
"layers": { "layers": {
"0": { "0": {
"filter": { "filter": {
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Langage utilisateur (code-ISO) {search}" "question": "Langage utilisateur (code-ISO) {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "Le groupe de modifications a ajouté au moins une image" "question": "Le groupe de modifications a ajouté au moins une image"
@ -1102,10 +1107,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Oui, il y a un trottoir de ce côté de la route" "then": "Il y a un trottoir de ce côté de la route"
}, },
"1": { "1": {
"then": "Non, il n'y a pas de trottoir où marcher" "then": "Il n'y a pas de trottoir où marcher"
}, },
"2": { "2": {
"then": "Il y a un trottoir où marcher cartographié séparément" "then": "Il y a un trottoir où marcher cartographié séparément"

View file

@ -599,10 +599,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Sì, c'è un marciapiede su questo lato della strada" "then": "C'è un marciapiede su questo lato della strada"
}, },
"1": { "1": {
"then": "No, non c'è un marciapiede su cui camminare" "then": "Non c'è un marciapiede su cui camminare"
} }
} }
} }

View file

@ -283,7 +283,12 @@
"1": { "1": {
"name": "noder", "name": "noder",
"title": { "title": {
"render": "sykkelnode <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "sykkelnode <strong>{rcn_ref}</strong>"
}
},
"render": "sykkelnode"
} }
} }
}, },

View file

@ -475,7 +475,15 @@
} }
}, },
"title": { "title": {
"render": "Fietsknooppunt <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "Fietsknooppunt <strong>{rcn_ref}</strong>"
},
"1": {
"then": "Voorgesteld fietsknooppunt <strong>{proposed:rcn_ref}</strong>"
}
},
"render": "Fietsknooppunt"
} }
} }
}, },
@ -875,49 +883,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Gemaakt door bijdrager {search}" "question": "Gemaakt door bijdrager {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>Niet</b> gemaakt door bijdrager {search}" "question": "<b>Niet</b> gemaakt door bijdrager {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Gemaakt voor {search}" "question": "Gemaakt voor {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Gemaakt na {search}" "question": "Gemaakt na {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "De taal van de bijdrager is {search}" "question": "De taal van de bijdrager is {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Gemaakt met host {search}" "question": "Gemaakt met host {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "Changeset bevat minstens één afbeelding" "question": "Changeset bevat minstens één afbeelding"
@ -1156,10 +1164,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Ja, er is een stoep aan deze kant van de weg" "then": "Er is een stoep aan deze kant van de weg"
}, },
"1": { "1": {
"then": "Nee, er is geen stoep om op te lopen" "then": "Er is geen stoep om op te lopen"
}, },
"2": { "2": {
"then": "Er is een apart ingetekende stoep om op te lopen" "then": "Er is een apart ingetekende stoep om op te lopen"
@ -1424,7 +1432,15 @@
} }
}, },
"title": { "title": {
"render": "Wandelknooppunt <strong>{rwn_ref}</strong>" "mappings": {
"0": {
"then": "Wandelknooppunt <strong>{rwn_ref}</strong>"
},
"1": {
"then": "Voorgesteld wandelknooppunt <strong>{proposed:rwn_ref}</strong>"
}
},
"render": "Wandelknooppunt"
} }
} }
}, },

View file

@ -509,7 +509,12 @@
} }
}, },
"title": { "title": {
"render": "węzeł rowerowy <strong>{rcn_ref}</strong>" "mappings": {
"0": {
"then": "węzeł rowerowy <strong>{rcn_ref}</strong>"
}
},
"render": "węzeł rowerowy"
} }
} }
}, },
@ -864,49 +869,49 @@
} }
} }
}, },
"1": { "2": {
"options": { "options": {
"0": { "0": {
"question": "Wykonane przez współautora {search}" "question": "Wykonane przez współautora {search}"
} }
} }
}, },
"2": { "3": {
"options": { "options": {
"0": { "0": {
"question": "<b>Nie</b> wykonane przez współautora {search}" "question": "<b>Nie</b> wykonane przez współautora {search}"
} }
} }
}, },
"3": { "4": {
"options": { "options": {
"0": { "0": {
"question": "Stworzone przed {search}" "question": "Stworzone przed {search}"
} }
} }
}, },
"4": { "5": {
"options": { "options": {
"0": { "0": {
"question": "Stworzone po {search}" "question": "Stworzone po {search}"
} }
} }
}, },
"5": { "6": {
"options": { "options": {
"0": { "0": {
"question": "Język użytkownika (kod iso) {search}" "question": "Język użytkownika (kod iso) {search}"
} }
} }
}, },
"6": { "7": {
"options": { "options": {
"0": { "0": {
"question": "Wykonane z hostem {search}" "question": "Wykonane z hostem {search}"
} }
} }
}, },
"7": { "8": {
"options": { "options": {
"0": { "0": {
"question": "Zestaw zmian dodał co najmniej jedno zdjęcie" "question": "Zestaw zmian dodał co najmniej jedno zdjęcie"
@ -1163,10 +1168,10 @@
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
"then": "Tak, jest chodnik z boku drogi" "then": "Jest chodnik z boku drogi"
}, },
"1": { "1": {
"then": "Nie, nie ma chodnika, którym można chodzić" "then": "Nie ma chodnika, którym można chodzić"
}, },
"2": { "2": {
"then": "Jest oddzielnie oznaczony chodnik" "then": "Jest oddzielnie oznaczony chodnik"

902
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.35.1", "version": "0.36.3",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -62,11 +62,12 @@
"generate:schemas": "ts2json-schema -p src/Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && echo 'tsjson is done' && vite-node scripts/fixSchemas.ts ", "generate:schemas": "ts2json-schema -p src/Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && echo 'tsjson is done' && vite-node scripts/fixSchemas.ts ",
"fix:schemas": "vite-node scripts/fixSchemas.ts ", "fix:schemas": "vite-node scripts/fixSchemas.ts ",
"watch:schemas": "cd Models/ThemeConfig/Json & ls | entr -s 'npm run generate:schemas' ", "watch:schemas": "cd Models/ThemeConfig/Json & ls | entr -s 'npm run generate:schemas' ",
"generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js", "generate:service-worker": "tsc src/service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i.bak \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js && rm public/service-worker.js.bak",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", "optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"generate:stats": "vite-node scripts/GenerateSeries.ts", "generate:stats": "vite-node scripts/GenerateSeries.ts",
"reset:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force", "reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview",
"generate": "mkdir -p ./assets/generated; npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run reset:layeroverview; npm run generate:service-worker", "prep:layeroverview": "mkdir -p ./src/assets/generated/layers; echo {\\\"themes\\\":[]} > ./src/assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./src/assets/generated/known_layers.json && rm -f ./src/assets/generated/layers/*.json && rm -f ./src/assets/generated/themes/*.json && cp ./assets/layers/usersettings/usersettings.json ./src/assets/generated/layers/usersettings.json && echo '{}' > ./src/assets/generated/layers/favourite.json",
"generate": "npm run generate:licenses && npm run generate:images && npm run generate:charging-stations && npm run generate:translations && npm run refresh:layeroverview && npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -", "generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh",
"lint": "npm run lint:prettier && npm run lint:eslint", "lint": "npm run lint:prettier && npm run lint:eslint",
@ -133,6 +134,7 @@
"opening_hours": "^3.6.0", "opening_hours": "^3.6.0",
"osm-auth": "^2.2.0", "osm-auth": "^2.2.0",
"osmtogeojson": "^3.0.0-beta.5", "osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",
"pic4carto": "^2.1.15", "pic4carto": "^2.1.15",
"prompt-sync": "^4.2.0", "prompt-sync": "^4.2.0",
@ -141,6 +143,7 @@
"svg-path-parser": "^1.1.0", "svg-path-parser": "^1.1.0",
"tailwind-merge": "^1.13.1", "tailwind-merge": "^1.13.1",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.1.8",
"trap-focus-svelte": "^1.0.1",
"vite-node": "^0.28.3", "vite-node": "^0.28.3",
"vitest": "^0.28.3", "vitest": "^0.28.3",
"wikibase-sdk": "^7.14.0", "wikibase-sdk": "^7.14.0",
@ -178,7 +181,7 @@
"prettier-plugin-tailwindcss": "^0.3.0", "prettier-plugin-tailwindcss": "^0.3.0",
"read-file": "^0.2.0", "read-file": "^0.2.0",
"sass": "^1.58.0", "sass": "^1.58.0",
"sharp": "^0.30.5", "sharp": "^0.32.6",
"svelte": "^3.55.1", "svelte": "^3.55.1",
"svelte-check": "^3.0.2", "svelte-check": "^3.0.2",
"svelte-preprocess": "^5.0.1", "svelte-preprocess": "^5.0.1",

View file

@ -729,6 +729,14 @@ video {
bottom: 0px; bottom: 0px;
} }
.right-4 {
right: 1rem;
}
.top-4 {
top: 1rem;
}
.right-1\/3 { .right-1\/3 {
right: 33.333333%; right: 33.333333%;
} }
@ -745,6 +753,10 @@ video {
top: 2.5rem; top: 2.5rem;
} }
.left-1\/4 {
left: 25%;
}
.isolate { .isolate {
isolation: isolate; isolation: isolate;
} }
@ -765,10 +777,6 @@ video {
float: left; float: left;
} }
.m-8 {
margin: 2rem;
}
.m-4 { .m-4 {
margin: 1rem; margin: 1rem;
} }
@ -781,6 +789,10 @@ video {
margin: 0px; margin: 0px;
} }
.m-8 {
margin: 2rem;
}
.m-2 { .m-2 {
margin: 0.5rem; margin: 0.5rem;
} }
@ -793,10 +805,58 @@ video {
margin: 0.125rem; margin: 0.125rem;
} }
.m-11 {
margin: 2.75rem;
}
.m-20 {
margin: 5rem;
}
.m-9 {
margin: 2.25rem;
}
.m-5 {
margin: 1.25rem;
}
.m-14 {
margin: 3.5rem;
}
.m-52 {
margin: 13rem;
}
.m-36 {
margin: 9rem;
}
.m-72 {
margin: 18rem;
}
.m-6 { .m-6 {
margin: 1.5rem; margin: 1.5rem;
} }
.m-32 {
margin: 8rem;
}
.m-44 {
margin: 11rem;
}
.m-28 {
margin: 7rem;
}
.m-7 {
margin: 1.75rem;
}
.m-px { .m-px {
margin: 1px; margin: 1px;
} }
@ -841,6 +901,10 @@ video {
margin-right: 3rem; margin-right: 3rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 { .mt-4 {
margin-top: 1rem; margin-top: 1rem;
} }
@ -877,10 +941,6 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -929,6 +989,10 @@ video {
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.mt-12 {
margin-top: 3rem;
}
.mr-12 { .mr-12 {
margin-right: 3rem; margin-right: 3rem;
} }
@ -1025,14 +1089,14 @@ video {
height: 6rem; height: 6rem;
} }
.h-full {
height: 100%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.h-full {
height: 100%;
}
.h-32 { .h-32 {
height: 8rem; height: 8rem;
} }
@ -1084,6 +1148,10 @@ video {
height: 2.75rem; height: 2.75rem;
} }
.h-5 {
height: 1.25rem;
}
.h-48 { .h-48 {
height: 12rem; height: 12rem;
} }
@ -1104,14 +1172,14 @@ video {
height: 10rem; height: 10rem;
} }
.h-80 {
height: 20rem;
}
.h-64 { .h-64 {
height: 16rem; height: 16rem;
} }
.h-80 {
height: 20rem;
}
.max-h-12 { .max-h-12 {
max-height: 3rem; max-height: 3rem;
} }
@ -1120,6 +1188,10 @@ video {
max-height: 6rem; max-height: 6rem;
} }
.max-h-64 {
max-height: 16rem;
}
.max-h-7 { .max-h-7 {
max-height: 1.75rem; max-height: 1.75rem;
} }
@ -1194,6 +1266,18 @@ video {
width: 50%; width: 50%;
} }
.w-14 {
width: 3.5rem;
}
.w-auto {
width: auto;
}
.w-5 {
width: 1.25rem;
}
.w-10 { .w-10 {
width: 2.5rem; width: 2.5rem;
} }
@ -1207,10 +1291,6 @@ video {
width: 12rem; width: 12rem;
} }
.w-auto {
width: auto;
}
.max-w-full { .max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -1285,6 +1365,10 @@ video {
appearance: none; appearance: none;
} }
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
@ -1345,6 +1429,10 @@ video {
justify-content: space-between; justify-content: space-between;
} }
.justify-around {
justify-content: space-around;
}
.gap-1 { .gap-1 {
gap: 0.25rem; gap: 0.25rem;
} }
@ -1437,6 +1525,14 @@ video {
align-self: center; align-self: center;
} }
.justify-self-start {
justify-self: start;
}
.justify-self-end {
justify-self: end;
}
.overflow-auto { .overflow-auto {
overflow: auto; overflow: auto;
} }
@ -1593,6 +1689,10 @@ video {
border-style: dotted; border-style: dotted;
} }
.border-none {
border-style: none;
}
.border-black { .border-black {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity)); border-color: rgb(0 0 0 / var(--tw-border-opacity));
@ -1647,6 +1747,10 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-white\/50 {
background-color: rgb(255 255 255 / 0.5);
}
.bg-red-400 { .bg-red-400 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity)); background-color: rgb(248 113 113 / var(--tw-bg-opacity));
@ -1692,14 +1796,14 @@ video {
padding: 0.25rem; padding: 0.25rem;
} }
.p-0\.5 {
padding: 0.125rem;
}
.p-0 { .p-0 {
padding: 0px; padding: 0px;
} }
.p-0\.5 {
padding: 0.125rem;
}
.p-12 { .p-12 {
padding: 3rem; padding: 3rem;
} }
@ -1764,6 +1868,10 @@ video {
padding-left: 1rem; padding-left: 1rem;
} }
.pr-1 {
padding-right: 0.25rem;
}
.pl-3 { .pl-3 {
padding-left: 0.75rem; padding-left: 0.75rem;
} }
@ -1772,10 +1880,6 @@ video {
padding-right: 0px; padding-right: 0px;
} }
.pr-1 {
padding-right: 0.25rem;
}
.pb-10 { .pb-10 {
padding-bottom: 2.5rem; padding-bottom: 2.5rem;
} }
@ -1784,6 +1888,10 @@ video {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.pt-1 {
padding-top: 0.25rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1945,6 +2053,10 @@ video {
opacity: 0.5; opacity: 0.5;
} }
.opacity-100 {
opacity: 1;
}
.shadow { .shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@ -2046,6 +2158,12 @@ video {
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
} }
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition { .transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, -webkit-transform, -webkit-filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, -webkit-transform, -webkit-filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@ -2054,10 +2172,8 @@ video {
transition-duration: 150ms; transition-duration: 150ms;
} }
.transition-colors { .duration-200 {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-duration: 200ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
} }
.ease-in-out { .ease-in-out {
@ -2126,6 +2242,10 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif; font-family: "Helvetica Neue", Arial, sans-serif;
} }
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
}
svg, svg,
img { img {
box-sizing: content-box; box-sizing: content-box;
@ -2327,6 +2447,16 @@ button.disabled:hover, .button.disabled:hover {
color: unset; color: unset;
} }
button.link {
border: none;
text-decoration: underline;
background-color: unset;
}
button.link:hover {
color:unset;
}
.interactive button.disabled svg path, .interactive .button.disabled svg path { .interactive button.disabled svg path, .interactive .button.disabled svg path {
fill: var(--interactive-foreground) !important; fill: var(--interactive-foreground) !important;
} }
@ -2696,6 +2826,15 @@ a.link-underline {
min-height: 8rem; min-height: 8rem;
} }
.max-w-full {
max-width: 100%;
}
.hover\:bg-white:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.hover\:bg-indigo-200:hover { .hover\:bg-indigo-200:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(199 210 254 / var(--tw-bg-opacity)); background-color: rgb(199 210 254 / var(--tw-bg-opacity));
@ -2850,10 +2989,6 @@ a.link-underline {
padding: 1.5rem; padding: 1.5rem;
} }
.md\:p-4 {
padding: 1rem;
}
.md\:p-3 { .md\:p-3 {
padding: 0.75rem; padding: 0.75rem;
} }

View file

@ -10,11 +10,15 @@ mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192" export NODE_OPTIONS="--max-old-space-size=16384"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build # This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index && npm run generate:editor-layer-index &&
npm run generate && npm run prep:layeroverview &&
npm run generate && # includes a single "refresh:layeroverview". Resetting the files is unnecessary as they are not in there in the first place
npm run refresh:layeroverview && # run refresh:layeroverview a second time to propagate all calls
npm run refresh:layeroverview && # run refresh:layeroverview a third time to fix some issues with the favourite layer all calls
npm run generate:layouts npm run generate:layouts
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@ -48,9 +52,10 @@ else
exit 1 exit 1
fi fi
export NODE_OPTIONS=--max-old-space-size=7000 export NODE_OPTIONS=--max-old-space-size=16000
which vite which vite
vite build --sourcemap vite --version
vite build # --sourcemap
# Copy the layer files, as these might contain assets (e.g. svgs) # Copy the layer files, as these might contain assets (e.g. svgs)
cp -r assets/layers/ dist/assets/layers/ cp -r assets/layers/ dist/assets/layers/
cp -r assets/themes/ dist/assets/themes/ cp -r assets/themes/ dist/assets/themes/

View file

@ -0,0 +1,335 @@
import Script from "./Script"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { existsSync, readFileSync, writeFileSync } from "fs"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts"
import { Utils } from "../src/Utils"
import { AddEditingElements } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson"
import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
export class GenerateFavouritesLayer extends Script {
private readonly layers: LayerConfigJson[] = []
constructor() {
super("Prepares the 'favourites'-layer")
const allThemes = new AllKnownLayoutsLazy(false).values()
for (const theme of allThemes) {
if (theme.hideFromOverview) {
continue
}
for (const layer of theme.layers) {
if (!layer.source) {
continue
}
if (layer.source.geojsonSource) {
continue
}
const layerConfig = AllSharedLayers.getSharedLayersConfigs().get(layer.id)
if (!layerConfig) {
continue
}
this.layers.push(layerConfig)
}
}
}
private sortMappings(mappings: MappingConfigJson[]): MappingConfigJson[] {
const sortedMappings: MappingConfigJson[] = [...mappings]
sortedMappings.sort((a, b) => {
const aTag = TagUtils.Tag(a.if)
const bTag = TagUtils.Tag(b.if)
const aPop = TagUtils.GetPopularity(aTag)
const bPop = TagUtils.GetPopularity(bTag)
return aPop - bPop
})
return sortedMappings
}
private addTagRenderings(proto: LayerConfigJson) {
const blacklistedIds = new Set([
"images",
"questions",
"mapillary",
"leftover-questions",
"last_edit",
"minimap",
"move-button",
"delete-button",
"all-tags",
"all_tags",
...AddEditingElements.addedElements,
])
const generatedTagRenderings: (string | QuestionableTagRenderingConfigJson)[] = []
const trPerId = new Map<
string,
{ conditions: TagConfigJson[]; tr: QuestionableTagRenderingConfigJson }
>()
for (const layerConfig of this.layers) {
if (!layerConfig.tagRenderings) {
continue
}
for (const tagRendering of layerConfig.tagRenderings) {
if (typeof tagRendering === "string") {
if (blacklistedIds.has(tagRendering)) {
continue
}
generatedTagRenderings.push(tagRendering)
blacklistedIds.add(tagRendering)
continue
}
if (tagRendering["builtin"]) {
continue
}
const id = tagRendering.id
if (blacklistedIds.has(id)) {
continue
}
if (trPerId.has(id)) {
const old = trPerId.get(id).tr
// We need to figure out if this was a 'recycled' tag rendering or just happens to have the same id
function isSame(fieldName: string) {
return old[fieldName]?.["en"] === tagRendering[fieldName]?.["en"]
}
const sameQuestion = isSame("question") && isSame("render")
if (!sameQuestion) {
const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering)
newTr.id = layerConfig.id + "_" + newTr.id
if (blacklistedIds.has(newTr.id)) {
continue
}
newTr.condition = {
and: Utils.NoNull([newTr.condition, layerConfig.source["osmTags"]]),
}
generatedTagRenderings.push(newTr)
blacklistedIds.add(newTr.id)
continue
}
}
if (!trPerId.has(id)) {
const newTr = <QuestionableTagRenderingConfigJson>Utils.Clone(tagRendering)
generatedTagRenderings.push(newTr)
trPerId.set(newTr.id, { tr: newTr, conditions: [] })
}
const conditions = trPerId.get(id).conditions
if (tagRendering["condition"]) {
conditions.push({
and: [tagRendering["condition"], layerConfig.source["osmTags"]],
})
} else {
conditions.push(layerConfig.source["osmTags"])
}
}
}
for (const { tr, conditions } of Array.from(trPerId.values())) {
const optimized = TagUtils.optimzeJson({ or: conditions })
if (optimized === true) {
continue
}
if (optimized === false) {
throw "Optimized into 'false', this is weird..."
}
tr.condition = optimized
}
const allTags: QuestionableTagRenderingConfigJson = {
id: "all-tags",
render: { "*": "{all_tags()}" },
metacondition: {
or: [
"__featureSwitchIsDebugging=true",
"mapcomplete-show_tags=full",
"mapcomplete-show_debug=yes",
],
},
}
proto.tagRenderings = [
"images",
...generatedTagRenderings,
...proto.tagRenderings,
"questions",
allTags,
]
}
/**
* const titleIcons = new GenerateFavouritesLayer().generateTitleIcons()
* JSON.stringify(titleIcons).indexOf("icons.defaults") // => -1
* */
private generateTitleIcons(): TagRenderingConfigJson[] {
let iconsLibrary: Map<string, TagRenderingConfigJson[]> = new Map<
string,
TagRenderingConfigJson[]
>()
const path = "./src/assets/generated/layers/icons.json"
if (existsSync(path)) {
const config = <LayerConfigJson>JSON.parse(readFileSync(path, "utf8"))
for (const tagRendering of config.tagRenderings) {
const qtr = <QuestionableTagRenderingConfigJson>tagRendering
const id = qtr.id
if (id) {
iconsLibrary.set(id, [qtr])
}
for (const label of tagRendering["labels"] ?? []) {
if (!iconsLibrary.has(label)) {
iconsLibrary.set(label, [])
}
iconsLibrary.get(label).push(qtr)
}
}
}
let titleIcons: TagRenderingConfigJson[] = []
const seenTitleIcons = new Set<string>()
for (const layer of this.layers) {
for (const titleIcon of layer.titleIcons) {
if (typeof titleIcon === "string") {
continue
}
if (titleIcon["labels"]?.indexOf("defaults") >= 0) {
continue
}
if (titleIcon.id === "rating") {
if (!seenTitleIcons.has("rating")) {
titleIcons.unshift(...iconsLibrary.get("rating"))
seenTitleIcons.add("rating")
}
continue
}
if (seenTitleIcons.has(titleIcon.id)) {
continue
}
seenTitleIcons.add(titleIcon.id)
console.log("Adding ", titleIcon.id)
titleIcons.push(titleIcon)
}
}
titleIcons.push(...(iconsLibrary.get("defaults") ?? []))
return titleIcons
}
private addTitle(proto: LayerConfigJson) {
let mappings: MappingConfigJson[] = []
for (const layer of this.layers) {
const t = layer.title
const tags: TagConfigJson = layer.source["osmTags"]
if (!t) {
continue
}
if (typeof t === "string") {
mappings.push({ if: tags, then: t })
} else if (t["render"] !== undefined || t["mappings"] !== undefined) {
const tr = <TagRenderingConfigJson>t
for (let i = 0; i < (tr.mappings ?? []).length; i++) {
const mapping = tr.mappings[i]
const optimized = TagUtils.optimzeJson({
and: [mapping.if, tags],
})
if (optimized === false) {
console.warn(
"The following tags yielded 'false':",
JSON.stringify(mapping.if),
JSON.stringify(tags)
)
continue
}
if (optimized === true) {
console.error(
"The following tags yielded 'false':",
JSON.stringify(mapping.if),
JSON.stringify(tags)
)
throw "Tags for title optimized to true"
}
if (!mapping.then) {
throw (
"The title has a missing 'then' for mapping " +
i +
" in layer " +
layer.id
)
}
mappings.push({
if: optimized,
then: mapping.then,
})
}
if (tr.render) {
mappings.push({
if: tags,
then: <Translatable>tr.render,
})
}
} else {
mappings.push({ if: tags, then: <Record<string, string>>t })
}
}
mappings = this.sortMappings(mappings)
if (proto.title["mappings"]) {
mappings.unshift(...proto.title["mappings"])
}
if (proto.title["render"]) {
mappings.push({
if: "id~*",
then: proto.title["render"],
})
}
for (const mapping of mappings) {
const opt = TagUtils.optimzeJson(mapping.if)
if (typeof opt === "boolean") {
continue
}
mapping.if = opt
}
proto.title = {
mappings,
}
}
async main(args: string[]): Promise<void> {
console.log("Generating the favourite layer: stealing _all_ tagRenderings")
const proto = this.readLayer("favourite/favourite.proto.json")
this.addTagRenderings(proto)
this.addTitle(proto)
proto.titleIcons = this.generateTitleIcons()
const targetContent = JSON.stringify(proto, null, " ")
const path = "./assets/layers/favourite/favourite.json"
if (existsSync(path)) {
if (readFileSync(path, "utf8") === targetContent) {
console.log(
"Already existing favourite layer is identical to the generated one, not writing"
)
return
}
}
console.log("Written favourite layer to", path)
writeFileSync(path, targetContent)
}
private readLayer(path: string): LayerConfigJson {
try {
return JSON.parse(readFileSync("./assets/layers/" + path, "utf8"))
} catch (e) {
console.error("Could not read ./assets/layers/" + path)
throw e
}
}
}
new GenerateFavouritesLayer().run()

View file

@ -4,7 +4,7 @@ import { RegexTag } from "../src/Logic/Tags/RegexTag"
import { ImmutableStore } from "../src/Logic/UIEventSource" import { ImmutableStore } from "../src/Logic/UIEventSource"
import { BBox } from "../src/Logic/BBox" import { BBox } from "../src/Logic/BBox"
import * as fs from "fs" import * as fs from "fs"
import { writeFileSync } from "fs" import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { Feature } from "geojson" import { Feature } from "geojson"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import { Imgur } from "../src/Logic/ImageProviders/Imgur" import { Imgur } from "../src/Logic/ImageProviders/Imgur"
@ -181,6 +181,58 @@ export default class GenerateImageAnalysis extends Script {
} }
} }
async downloadViews(datapath: string): Promise<void> {
const { allImages, imageSource } = this.loadImageUrls(datapath)
console.log("Detected", allImages.size, "images")
const results: [string, number][] = []
const today = new Date().toISOString().substring(0, "YYYY-MM-DD".length)
const viewDir = datapath + "/views_" + today
if (!existsSync(viewDir)) {
mkdirSync(viewDir)
}
const targetpath = datapath + "/views.csv"
const total = allImages.size
let dloaded = 0
let skipped = 0
let err = 0
for (const image of Array.from(allImages)) {
const cachedView = viewDir + "/" + image.replace(/\//g, "_")
let attribution: LicenseInfo
if (existsSync(cachedView)) {
attribution = JSON.parse(readFileSync(cachedView, "utf8"))
skipped++
} else {
try {
attribution = await Imgur.singleton.DownloadAttribution(image)
await ScriptUtils.sleep(500)
writeFileSync(cachedView, JSON.stringify(attribution))
dloaded++
} catch (e) {
err++
continue
}
}
results.push([image, attribution.views])
if (dloaded % 50 === 0) {
console.log({
dloaded,
skipped,
total,
err,
progress: Math.round(dloaded + skipped + err),
})
}
if ((dloaded + skipped + err) % 100 === 0) {
console.log("Writing views to", targetpath)
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
}
}
console.log("Writing views to", targetpath)
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
}
async downloadImage(url: string, imagePath: string): Promise<boolean> { async downloadImage(url: string, imagePath: string): Promise<boolean> {
const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg" const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg"
const targetPathLong = imagePath + "/" + filenameLong const targetPathLong = imagePath + "/" + filenameLong
@ -390,6 +442,7 @@ export default class GenerateImageAnalysis extends Script {
const imageBackupPath = args[0] const imageBackupPath = args[0]
await this.downloadData(datapath, cached) await this.downloadData(datapath, cached)
await this.downloadViews(datapath)
await this.downloadMetadata(datapath) await this.downloadMetadata(datapath)
await this.downloadAllImages(datapath, imageBackupPath) await this.downloadAllImages(datapath, imageBackupPath)
this.analyze(datapath) this.analyze(datapath)

View file

@ -27,7 +27,8 @@ function genImages(dryrun = false) {
"star_outline", "star_outline",
"star", "star",
"osm_logo_us", "osm_logo_us",
"triangle",
"teardrop_with_hole_green",
"SocialImageForeground", "SocialImageForeground",
"wikipedia", "wikipedia",
"Upload", "Upload",

View file

@ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
import { GenerateFavouritesLayer } from "./generateFavouritesLayer"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files. // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them
@ -381,23 +382,20 @@ class LayerOverviewUtils extends Script {
forceReload forceReload
) )
writeFileSync(
"./src/assets/generated/known_themes.json",
JSON.stringify({
themes: Array.from(sharedThemes.values()),
})
)
writeFileSync( writeFileSync(
"./src/assets/generated/known_layers.json", "./src/assets/generated/known_layers.json",
JSON.stringify({ layers: Array.from(sharedLayers.values()) }) JSON.stringify({
layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"),
})
) )
const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json" const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json"
if ( if (
(recompiledThemes.length > 0 && (recompiledThemes.length > 0 &&
!(recompiledThemes.length === 1 && recompiledThemes[0] === "mapcomplete-changes") && !(
args.indexOf("--generate-change-map") >= 0) || recompiledThemes.length === 1 && recompiledThemes[0] === "mapcomplete-changes"
)) ||
args.indexOf("--generate-change-map") >= 0 ||
!existsSync(mcChangesPath) !existsSync(mcChangesPath)
) { ) {
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme // mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
@ -428,6 +426,19 @@ class LayerOverviewUtils extends Script {
ConversionContext.construct([], []) ConversionContext.construct([], [])
) )
for (const [_, theme] of sharedThemes) {
theme.layers = theme.layers.filter(
(l) => Constants.added_by_default.indexOf(l["id"]) < 0
)
}
writeFileSync(
"./src/assets/generated/known_themes.json",
JSON.stringify({
themes: Array.from(sharedThemes.values()),
})
)
const end = new Date() const end = new Date()
const millisNeeded = end.getTime() - start.getTime() const millisNeeded = end.getTime() - start.getTime()
if (AllSharedLayers.getSharedLayersConfigs().size == 0) { if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
@ -711,7 +722,9 @@ class LayerOverviewUtils extends Script {
ConversionContext.construct([themePath], ["PrepareLayer"]) ConversionContext.construct([themePath], ["PrepareLayer"])
) )
try { try {
themeFile = new PrepareTheme(convertState).convertStrict( themeFile = new PrepareTheme(convertState, {
skipDefaultLayers: true,
}).convertStrict(
themeFile, themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"]) ConversionContext.construct([themePath], ["PrepareLayer"])
) )
@ -791,4 +804,5 @@ class LayerOverviewUtils extends Script {
} }
} }
new GenerateFavouritesLayer().run()
new LayerOverviewUtils().run() new LayerOverviewUtils().run()

View file

@ -19,7 +19,7 @@ import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtil
const sharp = require("sharp") const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8") const template = readFileSync("theme.html", "utf8")
const codeTemplate = readFileSync("src/index_theme.ts.template", "utf8") let codeTemplate = readFileSync("src/index_theme.ts.template", "utf8")
function enc(str: string): string { function enc(str: string): string {
return encodeURIComponent(str.toLowerCase()) return encodeURIComponent(str.toLowerCase())
@ -487,8 +487,19 @@ async function createIndexFor(theme: LayoutConfig) {
`import layout from "./src/assets/generated/themes/${theme.id}.json"`, `import layout from "./src/assets/generated/themes/${theme.id}.json"`,
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`, `import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
] ]
for (const layerName of Constants.added_by_default) {
imports.push(`import ${layerName} from "./src/assets/generated/layers/${layerName}.json"`)
}
writeFileSync(filename, imports.join("\n") + "\n") writeFileSync(filename, imports.join("\n") + "\n")
const addLayers = []
for (const layerName of Constants.added_by_default) {
addLayers.push(` layout.layers.push(<any> ${layerName})`)
}
codeTemplate = codeTemplate.replace(" // LAYOUT.ADD_LAYERS", addLayers.join("\n"))
appendFileSync(filename, codeTemplate) appendFileSync(filename, codeTemplate)
} }

View file

@ -4,6 +4,8 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import { writeFileSync } from "fs" import { writeFileSync } from "fs"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig"
import { And } from "../src/Logic/Tags/And"
/* Downloads stats on osmSource-tags and keys from tagInfo */ /* Downloads stats on osmSource-tags and keys from tagInfo */
@ -21,7 +23,12 @@ async function main(includeTags = true) {
continue continue
} }
const sources = TagUtils.Tag(layer.source["osmTags"]) const sourcesList = [TagUtils.Tag(layer.source["osmTags"])]
if (layer?.title) {
sourcesList.push(...new TagRenderingConfig(layer.title).usedTags())
}
const sources = new And(sourcesList)
const allKeys = sources.usedKeys() const allKeys = sources.usedKeys()
for (const key of allKeys) { for (const key of allKeys) {
if (!keysAndTags.has(key)) { if (!keysAndTags.has(key)) {
@ -68,6 +75,8 @@ async function main(includeTags = true) {
"./src/assets/key_totals.json", "./src/assets/key_totals.json",
JSON.stringify( JSON.stringify(
{ {
"#": "Generated with generateStats.ts",
date: new Date().toISOString(),
keys: Utils.MapToObj(keyTotal, (t) => t), keys: Utils.MapToObj(keyTotal, (t) => t),
tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)), tags: Utils.MapToObj(tagTotal, (v) => Utils.MapToObj(v, (t) => t)),
}, },

View file

@ -44,6 +44,36 @@ async function prepareFile(url: string): Promise<string> {
} }
return null return null
} }
async function handleDelete(req: http.IncomingMessage, res: ServerResponse) {
let body = ""
req.on("data", (chunk) => {
body = body + chunk
})
const paths = req.url.split("/")
console.log("Got a valid delete to:", paths.join("/"))
for (let i = 1; i < paths.length; i++) {
const p = paths.slice(0, i)
const dir = STATIC_PATH + p.join("/")
if (!fs.existsSync(dir)) {
res.writeHead(304, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>No parent directory, nothing deleted</body></html>", "utf8")
res.end()
return
}
}
const path = STATIC_PATH + paths.join("/")
if(!fs.existsSync(path)){
res.writeHead(304, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>File not found</body></html>", "utf8")
res.end()
return
}
fs.renameSync(path, path+".bak")
res.writeHead(200, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>File moved to backup</body></html>", "utf8")
res.end()
}
async function handlePost(req: http.IncomingMessage, res: ServerResponse) { async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
let body = "" let body = ""
@ -53,6 +83,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
await new Promise((resolve) => req.on("end", resolve)) await new Promise((resolve) => req.on("end", resolve))
console.log(new Date().toISOString())
let parsed: any let parsed: any
try { try {
parsed = JSON.parse(body) parsed = JSON.parse(body)
@ -84,7 +115,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
http.createServer(async (req: http.IncomingMessage, res) => { http.createServer(async (req: http.IncomingMessage, res) => {
try { try {
console.log(req.method + " " + req.url, "from:", req.headers.origin) console.log(req.method + " " + req.url, "from:", req.headers.origin, new Date().toISOString())
res.setHeader( res.setHeader(
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept" "Origin, X-Requested-With, Content-Type, Accept"
@ -101,6 +132,12 @@ http.createServer(async (req: http.IncomingMessage, res) => {
return return
} }
if(req.method === "DELETE"){
console.log("Got a DELETE", new Date())
await handleDelete(req, res)
return
}
const url = new URL(`http://127.0.0.1/` + req.url) const url = new URL(`http://127.0.0.1/` + req.url)
console.log("URL pathname is") console.log("URL pathname is")
if (url.pathname.endsWith("overview")) { if (url.pathname.endsWith("overview")) {

View file

@ -1,45 +1,54 @@
import known_themes from "../assets/generated/known_themes.json" import known_themes from "../assets/generated/known_themes.json"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import favourite from "../assets/generated/layers/favourite.json"
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
import { AllSharedLayers } from "./AllSharedLayers"
import Constants from "../Models/Constants"
/** /**
* Somewhat of a dictionary, which lazily parses needed themes * Somewhat of a dictionary, which lazily parses needed themes
*/ */
export class AllKnownLayoutsLazy { export class AllKnownLayoutsLazy {
private readonly dict: Map<string, { data: LayoutConfig } | { func: () => LayoutConfig }> = private readonly raw: Map<string, LayoutConfigJson> = new Map()
new Map() private readonly dict: Map<string, LayoutConfig> = new Map()
constructor() {
constructor(includeFavouriteLayer = true) {
for (const layoutConfigJson of known_themes["themes"]) { for (const layoutConfigJson of known_themes["themes"]) {
this.dict.set(layoutConfigJson.id, { for (const layerId of Constants.added_by_default) {
func: () => { if (layerId === "favourite" && favourite.id) {
const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true) if (includeFavouriteLayer) {
for (let i = 0; i < layout.layers.length; i++) { layoutConfigJson.layers.push(favourite)
let layer = layout.layers[i]
if (typeof layer === "string") {
throw "Layer " + layer + " was not expanded in " + layout.id
} }
continue
} }
return layout const defaultLayer = AllSharedLayers.getSharedLayersConfigs().get(layerId)
}, if (defaultLayer === undefined) {
}) console.error("Could not find builtin layer", layerId)
continue
}
layoutConfigJson.layers.push(defaultLayer)
}
this.raw.set(layoutConfigJson.id, layoutConfigJson)
} }
} }
public getConfig(key: string): LayoutConfigJson {
return this.raw.get(key)
}
public get(key: string): LayoutConfig { public get(key: string): LayoutConfig {
const thunk = this.dict.get(key) const cached = this.dict.get(key)
if (thunk === undefined) { if (cached !== undefined) {
return undefined return cached
} }
if (thunk["data"]) {
return thunk["data"] const layout = new LayoutConfig(this.getConfig(key))
} this.dict.set(key, layout)
const layout = thunk["func"]()
this.dict.set(key, { data: layout })
return layout return layout
} }
public keys() { public keys() {
return this.dict.keys() return this.raw.keys()
} }
public values() { public values() {

View file

@ -6,13 +6,22 @@ import { Changes } from "../Osm/Changes"
import { OsmConnection } from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SimpleMetaTagger from "../SimpleMetaTagger" import SimpleMetaTagger from "../SimpleMetaTagger"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { Feature } from "geojson" import { Feature } from "geojson"
import { OsmTags } from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
interface TagsUpdaterState {
selectedElement: UIEventSource<Feature>
featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> }
changes: Changes
osmConnection: OsmConnection
layout: LayoutConfig
osmObjectDownloader: OsmObjectDownloader
indexedFeatures: IndexedFeatureSource
}
export default class SelectedElementTagsUpdater { export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set([ private static readonly metatags = new Set([
"timestamp", "timestamp",
@ -23,38 +32,96 @@ export default class SelectedElementTagsUpdater {
"id", "id",
]) ])
private readonly state: { constructor(state: TagsUpdaterState) {
selectedElement: UIEventSource<Feature>
featureProperties: FeaturePropertiesStore
changes: Changes
osmConnection: OsmConnection
layout: LayoutConfig
osmObjectDownloader: OsmObjectDownloader
indexedFeatures: IndexedFeatureSource
}
constructor(state: {
selectedElement: UIEventSource<Feature>
featureProperties: FeaturePropertiesStore
indexedFeatures: IndexedFeatureSource
changes: Changes
osmConnection: OsmConnection
layout: LayoutConfig
osmObjectDownloader: OsmObjectDownloader
}) {
this.state = state
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (!isLoggedIn && !Utils.runningFromConsole) { if (!isLoggedIn && !Utils.runningFromConsole) {
return return
} }
this.installCallback() this.installCallback(state)
// We only have to do this once... // We only have to do this once...
return true return true
}) })
} }
private installCallback() { public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
const state = this.state try {
const leftRightSensitive = state.layout.isLeftRightSensitive()
if (leftRightSensitive) {
SimpleMetaTagger.removeBothTagging(latestTags)
}
const pendingChanges = state.changes.pendingChanges.data
.filter((change) => change.type + "/" + change.id === id)
.filter((change) => change.tags !== undefined)
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
}
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false
const currentTagsSource = state.featureProperties.getStore(id)
if (currentTagsSource === undefined) {
console.warn("No tags store found for", id, "cannot update tags")
return
}
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if (typeof osmValue === "number") {
osmValue = "" + osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
somethingChanged = true
currentTags[key] = osmValue
}
}
for (const currentKey in currentTags) {
if (currentKey.startsWith("_")) {
continue
}
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
continue
}
if (currentKey in latestTags) {
continue
}
console.log("Removing key as deleted upstream", currentKey)
delete currentTags[currentKey]
somethingChanged = true
}
if (somethingChanged) {
console.log(
"Detected upstream changes to the object " +
id +
" when opening it, updating..."
)
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
return currentTags
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}
private installCallback(state: TagsUpdaterState) {
state.selectedElement.addCallbackAndRunD(async (s) => { state.selectedElement.addCallbackAndRunD(async (s) => {
let id = s.properties?.id let id = s.properties?.id
if (!id) { if (!id) {
@ -94,7 +161,7 @@ export default class SelectedElementTagsUpdater {
oldFeature.geometry = newGeometry oldFeature.geometry = newGeometry
state.featureProperties.getStore(id)?.ping() state.featureProperties.getStore(id)?.ping()
} }
this.applyUpdate(latestTags, id) SelectedElementTagsUpdater.applyUpdate(latestTags, id, state)
console.log("Updated", id) console.log("Updated", id)
} catch (e) { } catch (e) {
@ -102,73 +169,4 @@ export default class SelectedElementTagsUpdater {
} }
}) })
} }
private applyUpdate(latestTags: OsmTags, id: string) {
const state = this.state
try {
const leftRightSensitive = state.layout.isLeftRightSensitive()
if (leftRightSensitive) {
SimpleMetaTagger.removeBothTagging(latestTags)
}
const pendingChanges = state.changes.pendingChanges.data
.filter((change) => change.type + "/" + change.id === id)
.filter((change) => change.tags !== undefined)
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
}
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false
const currentTagsSource = state.featureProperties.getStore(id)
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if (typeof osmValue === "number") {
osmValue = "" + osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
somethingChanged = true
currentTags[key] = osmValue
}
}
for (const currentKey in currentTags) {
if (currentKey.startsWith("_")) {
continue
}
if (SelectedElementTagsUpdater.metatags.has(currentKey)) {
continue
}
if (currentKey in latestTags) {
continue
}
console.log("Removing key as deleted upstream", currentKey)
delete currentTags[currentKey]
somethingChanged = true
}
if (somethingChanged) {
console.log("Detected upstream changes to the object when opening it, updating...")
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}
} }

View file

@ -139,9 +139,9 @@ export default class DetermineLayout {
const layerConfig = <LayerConfigJson>json const layerConfig = <LayerConfigJson>json
const iconTr: string | TagRenderingConfigJson = <any>( const iconTr: string | TagRenderingConfigJson = <any>(
layerConfig.pointRendering layerConfig.pointRendering
.map((mr) => mr.marker.find((icon) => icon.icon !== undefined).icon) .map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
.find((i) => i !== undefined) .find((i) => i !== undefined)
) ) ?? "bug"
const icon = new TagRenderingConfig(iconTr).render.txt const icon = new TagRenderingConfig(iconTr).render.txt
json = { json = {
id: json.id, id: json.id,

View file

@ -0,0 +1,220 @@
import StaticFeatureSource from "./StaticFeatureSource"
import { Feature } from "geojson"
import { Store, Stores, UIEventSource } from "../../UIEventSource"
import { OsmConnection } from "../../Osm/OsmConnection"
import { OsmId } from "../../../Models/OsmFeature"
import { GeoOperations } from "../../GeoOperations"
import { IndexedFeatureSource } from "../FeatureSource"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater"
/**
* Generates the favourites from the preferences and marks them as favourite
*/
export default class FavouritesFeatureSource extends StaticFeatureSource {
public static readonly prefix = "mapcomplete-favourite-"
private readonly _osmConnection: OsmConnection
private readonly _detectedIds: Store<string[]>
/**
* All favourites, including the ones which are filtered away because they are already displayed
*/
public readonly allFavourites: Store<Feature[]>
constructor(state: SpecialVisualizationState) {
const features: Store<Feature[]> = Stores.ListStabilized(
state.osmConnection.preferencesHandler.preferences.map((prefs) => {
const feats: Feature[] = []
const allIds = new Set<string>()
for (const key in prefs) {
if (!key.startsWith(FavouritesFeatureSource.prefix)) {
continue
}
try {
const feat = FavouritesFeatureSource.ExtractFavourite(key, prefs)
if (!feat) {
continue
}
feats.push(feat)
allIds.add(feat.properties.id)
} catch (e) {
console.error("Could not create favourite from", key, "due to", e)
}
}
return feats
})
)
const featuresWithoutAlreadyPresent = features.map((features) =>
features.filter(
(feat) => !state.layout.layers.some((l) => l.id === feat.properties._orig_layer)
)
)
super(featuresWithoutAlreadyPresent)
this.allFavourites = features
this._osmConnection = state.osmConnection
this._detectedIds = Stores.ListStabilized(
features.map((feats) => feats.map((f) => f.properties.id))
)
let allFeatures = state.indexedFeatures
this._detectedIds.addCallbackAndRunD((detected) =>
this.markFeatures(detected, state.featureProperties, allFeatures)
)
// We use the indexedFeatureSource as signal to update
allFeatures.features.map((_) =>
this.markFeatures(this._detectedIds.data, state.featureProperties, allFeatures)
)
this.allFavourites.addCallbackD((features) => {
for (const feature of features) {
this.updateFeature(feature, state.osmObjectDownloader, state)
}
return true
})
}
private async updateFeature(
feature: Feature,
osmObjectDownloader: OsmObjectDownloader,
state: SpecialVisualizationState
) {
const id = feature.properties.id
const upstream = await osmObjectDownloader.DownloadObjectAsync(id)
if (upstream === "deleted") {
this.removeFavourite(feature)
return
}
console.log("Updating metadata due to favourite of", id)
const latestTags = SelectedElementTagsUpdater.applyUpdate(upstream.tags, id, state)
this.updatePropertiesOfFavourite(latestTags)
}
private static ExtractFavourite(key: string, prefs: Record<string, string>): Feature {
const id = key.substring(FavouritesFeatureSource.prefix.length)
const osmId = id.replace("-", "/")
if (id.indexOf("-property-") > 0 || id.endsWith("-layer") || id.endsWith("-theme")) {
return undefined
}
const geometry = <[number, number]>JSON.parse(prefs[key])
const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id)
properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"]
properties.id = osmId
properties._favourite = "yes"
return {
type: "Feature",
properties,
geometry: {
type: "Point",
coordinates: geometry,
},
}
}
private static getPropertiesFor(
prefs: Record<string, string>,
id: string
): Record<string, string> {
const properties: Record<string, string> = {}
const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
for (const key in prefs) {
if (key.length < minLength) {
continue
}
if (!key.startsWith(FavouritesFeatureSource.prefix + id)) {
continue
}
const propertyName = key.substring(minLength).replaceAll("__", ":")
properties[propertyName] = prefs[key]
}
return properties
}
/**
* Sets all the (normal) properties as the feature is updated
*/
private updatePropertiesOfFavourite(properties: Record<string, string>) {
const id = properties?.id?.replace("/", "-")
if (!id) {
return
}
console.log("Updating store for", id)
for (const key in properties) {
const pref = this._osmConnection.GetPreference(
"favourite-" + id + "-property-" + key.replaceAll(":", "__")
)
const v = properties[key]
if (v === "" || !v) {
continue
}
pref.setData("" + v)
}
}
public removeFavourite(feature: Feature, tags?: UIEventSource<Record<string, string>>) {
const id = feature.properties.id.replace("/", "-")
const pref = this._osmConnection.GetPreference("favourite-" + id)
this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id)
if (tags) {
delete tags.data._favourite
tags.ping()
}
}
public markAsFavourite(
feature: Feature,
layer: string,
theme: string,
tags: UIEventSource<Record<string, string> & { id: OsmId }>,
isFavourite: boolean = true
) {
{
if (!isFavourite) {
this.removeFavourite(feature, tags)
return
}
const id = tags.data.id.replace("/", "-")
const pref = this._osmConnection.GetPreference("favourite-" + id)
const center = GeoOperations.centerpointCoordinates(feature)
pref.setData(JSON.stringify(center))
this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
this.updatePropertiesOfFavourite(tags.data)
}
tags.data._favourite = "yes"
tags.ping()
}
private markFeatures(
detected: string[],
featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> },
allFeatures: IndexedFeatureSource
) {
const feature = allFeatures.features.data
for (const f of feature) {
const id = f.properties.id
if (!id) {
continue
}
const store = featureProperties.getStore(id)
const origValue = store.data._favourite
if (detected.indexOf(id) >= 0) {
if (origValue !== "yes") {
store.data._favourite = "yes"
store.ping()
}
} else {
if (origValue) {
store.data._favourite = ""
store.ping()
}
}
}
}
}

View file

@ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource"
import LayerState from "../../State/LayerState" import LayerState from "../../State/LayerState"
export default class NearbyFeatureSource implements FeatureSource { export default class NearbyFeatureSource implements FeatureSource {
private readonly _result = new UIEventSource<Feature[]>(undefined)
public readonly features: Store<Feature[]> public readonly features: Store<Feature[]>
private readonly _targetPoint: Store<{ lon: number; lat: number }> private readonly _targetPoint: Store<{ lon: number; lat: number }>
private readonly _numberOfNeededFeatures: number private readonly _numberOfNeededFeatures: number
private readonly _layerState?: LayerState
private readonly _currentZoom: Store<number> private readonly _currentZoom: Store<number>
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
constructor( constructor(
targetPoint: Store<{ lon: number; lat: number }>, targetPoint: Store<{ lon: number; lat: number }>,
@ -18,41 +22,44 @@ export default class NearbyFeatureSource implements FeatureSource {
layerState?: LayerState, layerState?: LayerState,
currentZoom?: Store<number> currentZoom?: Store<number>
) { ) {
this._layerState = layerState
this._targetPoint = targetPoint.stabilized(100) this._targetPoint = targetPoint.stabilized(100)
this._numberOfNeededFeatures = numberOfNeededFeatures this._numberOfNeededFeatures = numberOfNeededFeatures
this._currentZoom = currentZoom.stabilized(500) this._currentZoom = currentZoom.stabilized(500)
const allSources: Store<{ feat: Feature; d: number }[]>[] = [] this.features = Stores.ListStabilized(this._result)
let minzoom = 999
const result = new UIEventSource<Feature[]>(undefined)
this.features = Stores.ListStabilized(result)
function update() {
let features: { feat: Feature; d: number }[] = []
for (const src of allSources) {
features.push(...src.data)
}
features.sort((a, b) => a.d - b.d)
if (numberOfNeededFeatures !== undefined) {
features = features.slice(0, numberOfNeededFeatures)
}
result.setData(features.map((f) => f.feat))
}
sources.forEach((source, layer) => { sources.forEach((source, layer) => {
const flayer = layerState?.filteredLayers.get(layer) this.registerSource(source, layer)
minzoom = Math.min(minzoom, flayer.layerDef.minzoom) })
}
public registerSource(source: FeatureSource, layerId: string) {
const flayer = this._layerState?.filteredLayers.get(layerId)
if (!flayer) {
return
}
const calcSource = this.createSource( const calcSource = this.createSource(
source.features, source.features,
flayer.layerDef.minzoom, flayer.layerDef.minzoom,
flayer.isDisplayed flayer.isDisplayed
) )
calcSource.addCallbackAndRunD((features) => { calcSource.addCallbackAndRunD((features) => {
update() this.update()
})
allSources.push(calcSource)
}) })
this._allSources.push(calcSource)
}
private update() {
let features: { feat: Feature; d: number }[] = []
for (const src of this._allSources) {
features.push(...src.data)
}
features.sort((a, b) => a.d - b.d)
if (this._numberOfNeededFeatures !== undefined) {
features = features.slice(0, this._numberOfNeededFeatures)
}
this._result.setData(features.map((f) => f.feat))
} }
/** /**

View file

@ -501,147 +501,43 @@ export class GeoOperations {
) )
} }
public static IdentifieCommonSegments(coordinatess: [number, number][][]): { /**
originalIndex: number * Given a list of points, convert into a GPX-list, e.g. for favourites
segmentShardWith: number[] * @param locations
coordinates: [] * @param title
}[] { */
// An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1]) public static toGpxPoints(
type edge = { locations: Feature<Point, { date?: string; altitude?: number | string }>[],
start: [number, number] title?: string
end: [number, number] ) {
intermediate: [number, number][] title = title?.trim()
members: { index: number; isReversed: boolean }[] if (title === undefined || title === "") {
title = "Created with MapComplete"
} }
title = Utils.EncodeXmlValue(title)
// The strategy: const trackPoints: string[] = []
// 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them for (const l of locations) {
// 2. Join these edges back together - as long as their membership groups are the same let trkpt = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
// 3. Convert to results for (const key in l.properties) {
const keyCleaned = key.replaceAll(":", "__")
const allEdgesByKey = new Map<string, edge>() trkpt += ` <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n`
if (key === "website") {
for (let index = 0; index < coordinatess.length; index++) { trkpt += ` <link>${l.properties[key]}</link>\n`
const coordinates = coordinatess[index]
for (let i = 0; i < coordinates.length - 1; i++) {
const c0 = coordinates[i]
const c1 = coordinates[i + 1]
const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1])
let key: string
if (isReversed) {
key = "" + c1 + ";" + c0
} else {
key = "" + c0 + ";" + c1
}
const member = { index, isReversed }
if (allEdgesByKey.has(key)) {
allEdgesByKey.get(key).members.push(member)
continue
}
let edge: edge
if (!isReversed) {
edge = {
start: c0,
end: c1,
members: [member],
intermediate: [],
}
} else {
edge = {
start: c1,
end: c0,
members: [member],
intermediate: [],
} }
} }
allEdgesByKey.set(key, edge) trkpt += " </wpt>\n"
trackPoints.push(trkpt)
} }
} const header =
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
// Lets merge them back together! return (
header +
let didMergeSomething = false "\n<name>" +
let allMergedEdges = Array.from(allEdgesByKey.values()) title +
const allEdgesByStartPoint = new Map<string, edge[]>() "</name>\n<trk><trkseg>\n" +
for (const edge of allMergedEdges) { trackPoints.join("\n") +
edge.members.sort((m0, m1) => m0.index - m1.index) "\n</trkseg></trk></gpx>"
)
const kstart = edge.start + ""
if (!allEdgesByStartPoint.has(kstart)) {
allEdgesByStartPoint.set(kstart, [])
}
allEdgesByStartPoint.get(kstart).push(edge)
}
function membersAreCompatible(first: edge, second: edge): boolean {
// There must be an exact match between the members
if (first.members === second.members) {
return true
}
if (first.members.length !== second.members.length) {
return false
}
// Members are sorted and have the same length, so we can check quickly
for (let i = 0; i < first.members.length; i++) {
const m0 = first.members[i]
const m1 = second.members[i]
if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) {
return false
}
}
// Allrigth, they are the same, lets mark this permanently
second.members = first.members
return true
}
do {
didMergeSomething = false
// We use 'allMergedEdges' as our running list
const consumed = new Set<edge>()
for (const edge of allMergedEdges) {
// Can we make this edge longer at the end?
if (consumed.has(edge)) {
continue
}
console.log("Considering edge", edge)
const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "")
console.log("Matchign endpoints:", matchingEndEdges)
if (matchingEndEdges === undefined) {
continue
}
for (let i = 0; i < matchingEndEdges.length; i++) {
const endEdge = matchingEndEdges[i]
if (consumed.has(endEdge)) {
continue
}
if (!membersAreCompatible(edge, endEdge)) {
continue
}
// We can make the segment longer!
didMergeSomething = true
console.log("Merging ", edge, "with ", endEdge)
edge.intermediate.push(edge.end)
edge.end = endEdge.end
consumed.add(endEdge)
matchingEndEdges.splice(i, 1)
break
}
}
allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge))
} while (didMergeSomething)
return []
} }
/** /**

View file

@ -31,11 +31,12 @@ export default class GenericImageProvider extends ImageProvider {
key: key, key: key,
url: value, url: value,
provider: this, provider: this,
id: value
}), }),
] ]
} }
SourceIcon(backlinkSource?: string) { SourceIcon() {
return undefined return undefined
} }

View file

@ -5,14 +5,16 @@ import { Utils } from "../../Utils"
export interface ProvidedImage { export interface ProvidedImage {
url: string url: string
url_hd?: string
key: string key: string
provider: ImageProvider provider: ImageProvider
id: string
} }
export default abstract class ImageProvider { export default abstract class ImageProvider {
public abstract readonly defaultKeyPrefixes: string[] public abstract readonly defaultKeyPrefixes: string[]
public abstract SourceIcon(backlinkSource?: string): BaseUIElement public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement
/** /**
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider * Given a properies object, maps it onto _all_ the available pictures for this imageProvider
@ -28,7 +30,7 @@ export default abstract class ImageProvider {
throw "No `defaultKeyPrefixes` defined by this image provider" throw "No `defaultKeyPrefixes` defined by this image provider"
} }
const relevantUrls = new UIEventSource< const relevantUrls = new UIEventSource<
{ url: string; key: string; provider: ImageProvider }[] { id: string; url: string; key: string; provider: ImageProvider }[]
>([]) >([])
const seenValues = new Set<string>() const seenValues = new Set<string>()
allTags.addCallbackAndRunD((tags) => { allTags.addCallbackAndRunD((tags) => {
@ -67,4 +69,8 @@ export default abstract class ImageProvider {
public abstract DownloadAttribution(url: string): Promise<LicenseInfo> public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
public abstract apiUrls(): string[] public abstract apiUrls(): string[]
public backlink(): string | undefined {
return undefined
}
} }

View file

@ -107,7 +107,8 @@ export class ImageUploadManager {
title, title,
description, description,
file, file,
targetKey targetKey,
tags?.data?.["_orig_theme"]
) )
if (!isNaN(Number(featureId))) { if (!isNaN(Number(featureId))) {
// This is a map note // This is a map note
@ -126,7 +127,8 @@ export class ImageUploadManager {
title: string, title: string,
description: string, description: string,
blob: File, blob: File,
targetKey: string | undefined targetKey: string | undefined,
theme?: string
): Promise<LinkImageAction> { ): Promise<LinkImageAction> {
this.increaseCountFor(this._uploadStarted, featureId) this.increaseCountFor(this._uploadStarted, featureId)
const properties = this._featureProperties.getStore(featureId) const properties = this._featureProperties.getStore(featureId)
@ -148,7 +150,7 @@ export class ImageUploadManager {
console.log("Uploading done, creating action for", featureId) console.log("Uploading done, creating action for", featureId)
key = targetKey ?? key key = targetKey ?? key
const action = new LinkImageAction(featureId, key, value, properties, { const action = new LinkImageAction(featureId, key, value, properties, {
theme: this._layout.id, theme: theme ?? this._layout.id,
changeType: "add-image", changeType: "add-image",
}) })
this.increaseCountFor(this._uploadFinished, featureId) this.increaseCountFor(this._uploadFinished, featureId)

View file

@ -66,6 +66,7 @@ export class Imgur extends ImageProvider implements ImageUploader {
url: value, url: value,
key: key, key: key,
provider: this, provider: this,
id: value
}), }),
] ]
} }
@ -81,6 +82,8 @@ export class Imgur extends ImageProvider implements ImageUploader {
* const expected = new LicenseInfo() * const expected = new LicenseInfo()
* expected.licenseShortName = "CC-BY 4.0" * expected.licenseShortName = "CC-BY 4.0"
* expected.artist = "Pieter Vander Vennet" * expected.artist = "Pieter Vander Vennet"
* expected.date = new Date(1655052078000)
* expected.views = 2
* licenseInfo // => expected * licenseInfo // => expected
*/ */
public async DownloadAttribution(url: string): Promise<LicenseInfo> { public async DownloadAttribution(url: string): Promise<LicenseInfo> {
@ -93,6 +96,8 @@ export class Imgur extends ImageProvider implements ImageUploader {
const descr: string = response.data.description ?? "" const descr: string = response.data.description ?? ""
const data: any = {} const data: any = {}
const imgurData = response.data
for (const tag of descr.split("\n")) { for (const tag of descr.split("\n")) {
const kv = tag.split(":") const kv = tag.split(":")
const k = kv[0] const k = kv[0]
@ -103,6 +108,8 @@ export class Imgur extends ImageProvider implements ImageUploader {
licenseInfo.licenseShortName = data.license licenseInfo.licenseShortName = data.license
licenseInfo.artist = data.author licenseInfo.artist = data.author
licenseInfo.date = new Date(Number(imgurData.datetime) * 1000)
licenseInfo.views = imgurData.views
return licenseInfo return licenseInfo
} }

View file

@ -9,4 +9,6 @@ export class LicenseInfo {
credit: string = "" credit: string = ""
description: string = "" description: string = ""
informationLocation: URL = undefined informationLocation: URL = undefined
date?: Date
views?: number
} }

View file

@ -4,6 +4,7 @@ import Svg from "../../Svg"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo" import { LicenseInfo } from "./LicenseInfo"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import Link from "../../UI/Base/Link"
export class Mapillary extends ImageProvider { export class Mapillary extends ImageProvider {
public static readonly singleton = new Mapillary() public static readonly singleton = new Mapillary()
@ -17,10 +18,6 @@ export class Mapillary extends ImageProvider {
] ]
defaultKeyPrefixes = ["mapillary", "image"] defaultKeyPrefixes = ["mapillary", "image"]
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
/** /**
* Indicates that this is the same URL * Indicates that this is the same URL
* Ignores 'stp' parameter * Ignores 'stp' parameter
@ -57,6 +54,30 @@ export class Mapillary extends ImageProvider {
return false return false
} }
static createLink(
location: {
lon: number
lat: number
} = undefined,
zoom: number = 17,
pKey?: string
) {
const params = {
focus: pKey === undefined ? "map" : "photo",
lat: location?.lat,
lng: location?.lon,
z: location === undefined ? undefined : Math.max((zoom ?? 2) - 1, 1),
pKey,
}
const baselink = `https://www.mapillary.com/app/?`
const paramsStr = Utils.NoNull(
Object.keys(params).map((k) =>
params[k] === undefined ? undefined : k + "=" + params[k]
)
)
return baselink + paramsStr.join("&")
}
/** /**
* Returns the correct key for API v4.0 * Returns the correct key for API v4.0
*/ */
@ -80,8 +101,22 @@ export class Mapillary extends ImageProvider {
return undefined return undefined
} }
SourceIcon(backlinkSource?: string): BaseUIElement { apiUrls(): string[] {
return Svg.mapillary_svg() return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
SourceIcon(
id: string,
location?: {
lon: number
lat: number
}
): BaseUIElement {
const icon = Svg.mapillary_svg()
if (!id) {
return icon
}
return new Link(icon, Mapillary.createLink(location, 16, "" + id), true)
} }
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
@ -106,14 +141,18 @@ export class Mapillary extends ImageProvider {
const metadataUrl = const metadataUrl =
"https://graph.mapillary.com/" + "https://graph.mapillary.com/" +
mapillaryId + mapillaryId +
"?fields=thumb_1024_url&&access_token=" + "?fields=thumb_1024_url,thumb_original_url&access_token=" +
Constants.mapillary_client_token_v4 Constants.mapillary_client_token_v4
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60) const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
const url = <string>response["thumb_1024_url"] const url = <string>response["thumb_1024_url"]
console.log(response)
const url_hd = <string>response["thumb_original_url"]
return { return {
url: url, id: "" + mapillaryId,
url,
url_hd,
provider: this, provider: this,
key: key, key,
} }
} }
} }

View file

@ -15,7 +15,7 @@ export class WikidataImageProvider extends ImageProvider {
super() super()
} }
public SourceIcon(_?: string): BaseUIElement { public SourceIcon(): BaseUIElement {
return Svg.wikidata_svg() return Svg.wikidata_svg()
} }

View file

@ -1,7 +1,6 @@
import ImageProvider, { ProvidedImage } from "./ImageProvider" import ImageProvider, { ProvidedImage } from "./ImageProvider"
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement"
import Svg from "../../Svg" import Svg from "../../Svg"
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" import Wikimedia from "../Web/Wikimedia"
@ -70,17 +69,8 @@ export class WikimediaImageProvider extends ImageProvider {
return WikimediaImageProvider.apiUrls return WikimediaImageProvider.apiUrls
} }
SourceIcon(backlink: string): BaseUIElement { SourceIcon(): BaseUIElement {
const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em") return Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
if (backlink === undefined) {
return img
}
return new Link(
Svg.wikimedia_commons_white_svg(),
`https://commons.wikimedia.org/wiki/${backlink}`,
true
)
} }
public PrepUrl(value: string): ProvidedImage { public PrepUrl(value: string): ProvidedImage {
@ -173,6 +163,6 @@ export class WikimediaImageProvider extends ImageProvider {
if (!image.startsWith("File:")) { if (!image.startsWith("File:")) {
image = "File:" + image image = "File:" + image
} }
return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this } return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this , id: image}
} }
} }

View file

@ -12,6 +12,10 @@ export class OsmPreferences {
"all-osm-preferences", "all-osm-preferences",
{} {}
) )
/**
* A map containing the individual preference sources
* @private
*/
private readonly preferenceSources = new Map<string, UIEventSource<string>>() private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private auth: any private auth: any
private userDetails: UIEventSource<UserDetails> private userDetails: UIEventSource<UserDetails>
@ -21,7 +25,10 @@ export class OsmPreferences {
this.auth = auth this.auth = auth
this.userDetails = osmConnection.userDetails this.userDetails = osmConnection.userDetails
const self = this const self = this
osmConnection.OnLoggedIn(() => self.UpdatePreferences()) osmConnection.OnLoggedIn(() => {
self.UpdatePreferences(true)
return true
})
} }
/** /**
@ -72,11 +79,19 @@ export class OsmPreferences {
let i = 0 let i = 0
while (str !== "") { while (str !== "") {
if (str === undefined || str === "undefined") { if (str === undefined || str === "undefined") {
source.setData(undefined)
throw ( throw (
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " + "Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
key key
) )
} }
if (str === "undefined") {
source.setData(undefined)
throw (
"Got a literal string containing 'undefined' for a long preference with name " +
key
)
}
if (i > 100) { if (i > 100) {
throw "This long preference is getting very long... " throw "This long preference is getting very long... "
} }
@ -197,7 +212,7 @@ export class OsmPreferences {
}) })
} }
private UpdatePreferences() { private UpdatePreferences(forceUpdate?: boolean) {
const self = this const self = this
this.auth.xhr( this.auth.xhr(
{ {
@ -210,11 +225,22 @@ export class OsmPreferences {
return return
} }
const prefs = value.getElementsByTagName("preference") const prefs = value.getElementsByTagName("preference")
const seenKeys = new Set<string>()
for (let i = 0; i < prefs.length; i++) { for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i] const pref = prefs[i]
const k = pref.getAttribute("k") const k = pref.getAttribute("k")
const v = pref.getAttribute("v") const v = pref.getAttribute("v")
self.preferences.data[k] = v self.preferences.data[k] = v
seenKeys.add(k)
}
if (forceUpdate) {
for (let key in self.preferences.data) {
if (seenKeys.has(key)) {
continue
}
console.log("Deleting key", key, "as we didn't find it upstream")
delete self.preferences.data[key]
}
} }
// We merge all the preferences: new keys are uploaded // We merge all the preferences: new keys are uploaded
@ -285,4 +311,14 @@ export class OsmPreferences {
} }
) )
} }
removeAllWithPrefix(prefix: string) {
for (const key in this.preferences.data) {
if (key.startsWith(prefix)) {
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
console.log("Clearing preference", key)
}
}
this.preferences.ping()
}
} }

View file

@ -39,7 +39,7 @@ export default class UserRelatedState {
public readonly installedUserThemes: Store<string[]> public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean> public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly showCrosshair: UIEventSource<"yes" | undefined> public readonly showCrosshair: UIEventSource<"yes" | "always" | "no" | undefined>
public readonly fixateNorth: UIEventSource<undefined | "yes"> public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly homeLocation: FeatureSource public readonly homeLocation: FeatureSource
/** /**
@ -294,6 +294,9 @@ export default class UserRelatedState {
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) { for (const k in newPrefs) {
const v = newPrefs[k] const v = newPrefs[k]
if (v === "undefined" || !v) {
continue
}
if (k.endsWith("-combined-length")) { if (k.endsWith("-combined-length")) {
const l = Number(v) const l = Number(v)
const key = k.substring(0, k.length - "length".length) const key = k.substring(0, k.length - "length".length)
@ -308,7 +311,6 @@ export default class UserRelatedState {
} }
amendedPrefs.ping() amendedPrefs.ping()
console.log("Amended prefs are:", amendedPrefs.data)
}) })
const translationMode = osmConnection.GetPreference("translation-mode") const translationMode = osmConnection.GetPreference("translation-mode")

View file

@ -4,39 +4,11 @@ export class ThemeMetaTagging {
public static readonly themeName = "usersettings" public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
feat.properties._description Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
?.at(1) Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
Utils.AddLazyProperty( feat.properties['__current_backgroun'] = 'initial_value'
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
} }
} }

View file

@ -3,6 +3,7 @@ import { Or } from "./Or"
import { TagUtils } from "./TagUtils" import { TagUtils } from "./TagUtils"
import { Tag } from "./Tag" import { Tag } from "./Tag"
import { RegexTag } from "./RegexTag" import { RegexTag } from "./RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class And extends TagsFilter { export class And extends TagsFilter {
public and: TagsFilter[] public and: TagsFilter[]
@ -72,6 +73,10 @@ export class And extends TagsFilter {
return allChoices return allChoices
} }
asJson(): TagConfigJson {
return { and: this.and.map((a) => a.asJson()) }
}
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
return this.and return this.and
.map((t) => { .map((t) => {
@ -228,6 +233,15 @@ export class And extends TagsFilter {
return And.construct(newAnds) return And.construct(newAnds)
} }
/**
* const raw = {"and": [{"or":["leisure=playground","playground!=forest"]},{"or":["leisure=playground","playground!=forest"]}]}
* const parsed = TagUtils.Tag(raw)
* parsed.optimize().asJson() // => {"or":["leisure=playground","playground!=forest"]}
*
* const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}]
* const parsed = TagUtils.Tag(raw)
* parsed.optimize().asJson() // => "advertising=screen"
*/
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
if (this.and.length === 0) { if (this.and.length === 0) {
return true return true
@ -289,9 +303,17 @@ export class And extends TagsFilter {
optimized.splice(i, 1) optimized.splice(i, 1)
i-- i--
} }
} else if (v !== opt.value) { } else {
// detected an internal conflict if (!v.match(opt.value)) {
// We _know_ that for the key of the RegexTag `opt`, the value will be `v`.
// As such, if `opt.value` cannot match `v`, we detected an internal conflict and can fail
return false return false
} else {
// Another tag already provided a _stricter_ value then this regex, so we can remove this one!
optimized.splice(i, 1)
i--
}
} }
} }
} }
@ -369,10 +391,13 @@ export class And extends TagsFilter {
const elements = containedOr.or.filter( const elements = containedOr.or.filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate)) (candidate) => !commonValues.some((cv) => cv.shadows(candidate))
) )
if (elements.length > 0) {
newOrs.push(Or.construct(elements)) newOrs.push(Or.construct(elements))
} }
}
if (newOrs.length > 0) {
commonValues.push(And.construct(newOrs)) commonValues.push(And.construct(newOrs))
}
const result = new Or(commonValues).optimize() const result = new Or(commonValues).optimize()
if (result === false) { if (result === false) {
return false return false

View file

@ -1,18 +1,23 @@
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { Tag } from "./Tag"
export default class ComparingTag implements TagsFilter { export default class ComparingTag implements TagsFilter {
private readonly _key: string private readonly _key: string
private readonly _predicate: (value: string) => boolean private readonly _predicate: (value: string) => boolean
private readonly _representation: string private readonly _representation: "<" | ">" | "<=" | ">="
private readonly _boundary: string
constructor( constructor(
key: string, key: string,
predicate: (value: string | undefined) => boolean, predicate: (value: string | undefined) => boolean,
representation: string = "" representation: "<" | ">" | "<=" | ">=",
boundary: string
) { ) {
this._key = key this._key = key
this._predicate = predicate this._predicate = predicate
this._representation = representation this._representation = representation
this._boundary = boundary
} }
asChange(properties: Record<string, string>): { k: string; v: string }[] { asChange(properties: Record<string, string>): { k: string; v: string }[] {
@ -20,15 +25,64 @@ export default class ComparingTag implements TagsFilter {
} }
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) { asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
return this._key + this._representation return this._key + this._representation + this._boundary
} }
asOverpass(): string[] { asOverpass(): string[] {
throw "A comparable tag can not be used as overpass filter" throw "A comparable tag can not be used as overpass filter"
} }
/**
* const tg = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
* const tg0 = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
* const tg1 = new ComparingTag("key", value => (Number(value) <= 42), "<=", "42")
* const against = new ComparingTag("key", value => (Number(value) > 0), ">", "0")
* tg.shadows(new Tag("key", "41")) // => true
* tg.shadows(new Tag("key", "0")) // => true
* tg.shadows(new Tag("key", "43")) // => false
* tg.shadows(new Tag("key", "0")) // => true
* tg.shadows(tg) // => true
* tg.shadows(tg0) // => true
* tg.shadows(against) // => false
* tg1.shadows(tg0) // => true
* tg0.shadows(tg1) // => false
*
*/
shadows(other: TagsFilter): boolean { shadows(other: TagsFilter): boolean {
return other === this if (other === this) {
return true
}
if (other instanceof ComparingTag) {
if (other._key !== this._key) {
return false
}
const selfDesc = this._representation === "<" || this._representation === "<="
const otherDesc = other._representation === "<" || other._representation === "<="
if (selfDesc !== otherDesc) {
return false
}
if (
this._boundary === other._boundary &&
this._representation === other._representation
) {
return true
}
if (this._predicate(other._boundary)) {
return true
}
return false
}
if (other instanceof Tag) {
if (other.key !== this._key) {
return false
}
if (this.matchesProperties({ [other.key]: other.value })) {
return true
}
}
return false
} }
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
@ -38,7 +92,7 @@ export default class ComparingTag implements TagsFilter {
/** /**
* Checks if the properties match * Checks if the properties match
* *
* const t = new ComparingTag("key", (x => Number(x) < 42)) * const t = new ComparingTag("key", (x => Number(x) < 42), "<", "42")
* t.matchesProperties({key: 42}) // => false * t.matchesProperties({key: 42}) // => false
* t.matchesProperties({key: 41}) // => true * t.matchesProperties({key: 41}) // => true
* t.matchesProperties({key: 0}) // => true * t.matchesProperties({key: 0}) // => true
@ -56,6 +110,10 @@ export default class ComparingTag implements TagsFilter {
return [] return []
} }
asJson(): TagConfigJson {
return this._key + this._representation
}
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
return this return this
} }

View file

@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagUtils } from "./TagUtils" import { TagUtils } from "./TagUtils"
import { And } from "./And" import { And } from "./And"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class Or extends TagsFilter { export class Or extends TagsFilter {
public or: TagsFilter[] public or: TagsFilter[]
@ -27,6 +28,10 @@ export class Or extends TagsFilter {
return false return false
} }
asJson(): TagConfigJson {
return { or: this.or.map((o) => o.asJson()) }
}
/** /**
* *
* import {Tag} from "./Tag"; * import {Tag} from "./Tag";
@ -157,6 +162,12 @@ export class Or extends TagsFilter {
return Or.construct(newOrs) return Or.construct(newOrs)
} }
/**
* const raw = {"or": [{"and":["leisure=playground","playground!=forest"]},{"and":["leisure=playground","playground!=forest"]}]}
* const parsed = TagUtils.Tag(raw)
* parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
*
*/
optimize(): TagsFilter | boolean { optimize(): TagsFilter | boolean {
if (this.or.length === 0) { if (this.or.length === 0) {
return false return false
@ -174,9 +185,9 @@ export class Or extends TagsFilter {
const newOrs: TagsFilter[] = [] const newOrs: TagsFilter[] = []
let containedAnds: And[] = [] let containedAnds: And[] = []
for (const tf of optimized) { for (const tf of optimized) {
if (tf instanceof Or) { if (tf["or"]) {
// expand all the nested ors... // expand all the nested ors...
newOrs.push(...tf.or) newOrs.push(...tf["or"])
} else if (tf instanceof And) { } else if (tf instanceof And) {
// partition of all the ands // partition of all the ands
containedAnds.push(tf) containedAnds.push(tf)
@ -191,7 +202,7 @@ export class Or extends TagsFilter {
const cleanedContainedANds: And[] = [] const cleanedContainedANds: And[] = []
outer: for (let containedAnd of containedAnds) { outer: for (let containedAnd of containedAnds) {
for (const known of newOrs) { for (const known of newOrs) {
// input for optimazation: (K=V | (X=Y & K=V)) // input for optimization: (K=V | (X=Y & K=V))
// containedAnd: (X=Y & K=V) // containedAnd: (X=Y & K=V)
// newOrs (and thus known): (K=V) --> false // newOrs (and thus known): (K=V) --> false
const cleaned = containedAnd.removePhraseConsideredKnown(known, false) const cleaned = containedAnd.removePhraseConsideredKnown(known, false)
@ -236,16 +247,21 @@ export class Or extends TagsFilter {
const elements = containedAnd.and.filter( const elements = containedAnd.and.filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate)) (candidate) => !commonValues.some((cv) => cv.shadows(candidate))
) )
if (elements.length == 0) {
continue
}
newAnds.push(And.construct(elements)) newAnds.push(And.construct(elements))
} }
if (newAnds.length > 0) {
commonValues.push(Or.construct(newAnds)) commonValues.push(Or.construct(newAnds))
}
const result = new And(commonValues).optimize() const result = new And(commonValues).optimize()
if (result === true) { if (result === true) {
return true return true
} else if (result === false) { } else if (result === false) {
// neutral element: skip // neutral element: skip
} else { } else if (commonValues.length > 0) {
newOrs.push(And.construct(commonValues)) newOrs.push(And.construct(commonValues))
} }
} }

View file

@ -1,5 +1,6 @@
import { Tag } from "./Tag" import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class RegexTag extends TagsFilter { export class RegexTag extends TagsFilter {
public readonly key: RegExp | string public readonly key: RegExp | string
@ -11,6 +12,9 @@ export class RegexTag extends TagsFilter {
super() super()
this.key = key this.key = key
this.value = value this.value = value
if (this.value instanceof RegExp && ("" + this.value).startsWith("^(^(")) {
throw "Detected a duplicate start marker ^(^( in a regextag:" + this.value
}
this.invert = invert this.invert = invert
this.matchesEmpty = RegexTag.doesMatch("", this.value) this.matchesEmpty = RegexTag.doesMatch("", this.value)
} }
@ -41,11 +45,21 @@ export class RegexTag extends TagsFilter {
return possibleRegex.test(fromTag) return possibleRegex.test(fromTag)
} }
private static source(r: string | RegExp) { private static source(r: string | RegExp, includeStartMarker: boolean = true) {
if (typeof r === "string") { if (typeof r === "string") {
return r return r
} }
return r.source if (r === undefined) {
return undefined
}
const src = r.source
if (includeStartMarker) {
return src
}
if (src.startsWith("^(") && src.endsWith(")$")) {
return src.substring(2, src.length - 2)
}
return src
} }
/** /**
@ -82,6 +96,24 @@ export class RegexTag extends TagsFilter {
} }
} }
/**
* import { TagUtils } from "./TagUtils";
*
* const t = TagUtils.Tag("a~b")
* t.asJson() // => "a~b"
*
* const t = TagUtils.Tag("a=")
* t.asJson() // => "a="
*/
asJson(): TagConfigJson {
const v = RegexTag.source(this.value, false)
if (typeof this.key === "string") {
const oper = typeof this.value === "string" ? "=" : "~"
return `${this.key}${this.invert ? "!" : ""}${oper}${v}`
}
return `${this.key.source}${this.invert ? "!" : ""}~~${v}`
}
isUsableAsAnswer(): boolean { isUsableAsAnswer(): boolean {
return false return false
} }
@ -293,7 +325,7 @@ export class RegexTag extends TagsFilter {
if (typeof this.key === "string") { if (typeof this.key === "string") {
return [this.key] return [this.key]
} }
throw "Key cannot be determined as it is a regex" return []
} }
usedTags(): { key: string; value: string }[] { usedTags(): { key: string; value: string }[] {

View file

@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { Tag } from "./Tag" import { Tag } from "./Tag"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
/** /**
* The substituting-tag uses the tags of a feature a variables and replaces them. * The substituting-tag uses the tags of a feature a variables and replaces them.
@ -45,6 +46,10 @@ export default class SubstitutingTag implements TagsFilter {
) )
} }
asJson(): TagConfigJson {
return this._key + (this._invert ? "!" : "") + ":=" + this._value
}
asOverpass(): string[] { asOverpass(): string[] {
throw "A variable with substitution can not be used to query overpass" throw "A variable with substitution can not be used to query overpass"
} }

View file

@ -1,5 +1,6 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class Tag extends TagsFilter { export class Tag extends TagsFilter {
public key: string public key: string
@ -67,6 +68,10 @@ export class Tag extends TagsFilter {
return [`["${this.key}"="${this.value}"]`] return [`["${this.key}"="${this.value}"]`]
} }
asJson(): TagConfigJson {
return this.key + "=" + this.value
}
/** /**
const t = new Tag("key", "value") const t = new Tag("key", "value")

View file

@ -15,8 +15,9 @@ type Tags = Record<string, string>
export type UploadableTag = Tag | SubstitutingTag | And export type UploadableTag = Tag | SubstitutingTag | And
export class TagUtils { export class TagUtils {
public static readonly comparators: ReadonlyArray<[string, (a: number, b: number) => boolean]> = public static readonly comparators: ReadonlyArray<
[ ["<" | ">" | "<=" | ">=", (a: number, b: number) => boolean]
> = [
["<=", (a, b) => a <= b], ["<=", (a, b) => a <= b],
[">=", (a, b) => a >= b], [">=", (a, b) => a >= b],
["<", (a, b) => a < b], ["<", (a, b) => a < b],
@ -324,6 +325,14 @@ export class TagUtils {
return tags return tags
} }
static optimzeJson(json: TagConfigJson): TagConfigJson | boolean {
const optimized = TagUtils.Tag(json).optimize()
if (optimized === true || optimized === false) {
return optimized
}
return optimized.asJson()
}
/** /**
* Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
* *
@ -735,11 +744,10 @@ export class TagUtils {
const tag = json as string const tag = json as string
for (const [operator, comparator] of TagUtils.comparators) { for (const [operator, comparator] of TagUtils.comparators) {
if (tag.indexOf(operator) >= 0) { if (tag.indexOf(operator) >= 0) {
const split = Utils.SplitFirst(tag, operator) const split = Utils.SplitFirst(tag, operator).map((v) => v.trim())
let val = Number(split[1])
let val = Number(split[1].trim())
if (isNaN(val)) { if (isNaN(val)) {
val = new Date(split[1].trim()).getTime() val = new Date(split[1]).getTime()
} }
const f = (value: string | number | undefined) => { const f = (value: string | number | undefined) => {
@ -762,7 +770,7 @@ export class TagUtils {
} }
return comparator(b, val) return comparator(b, val)
} }
return new ComparingTag(split[0], f, operator + val) return new ComparingTag(split[0], f, operator, "" + val)
} }
} }
@ -861,6 +869,27 @@ export class TagUtils {
return TagUtils.keyCounts.keys[key] return TagUtils.keyCounts.keys[key]
} }
public static GetPopularity(tag: TagsFilter): number | undefined {
if (tag instanceof And) {
return Math.min(...Utils.NoNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1
}
if (tag instanceof Or) {
return Math.max(...Utils.NoNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1
}
if (tag instanceof Tag) {
return TagUtils.GetCount(tag.key, tag.value)
}
if (tag instanceof RegexTag) {
const key = tag.key
if (key instanceof RegExp || tag.invert || tag.isNegative()) {
return undefined
}
return TagUtils.GetCount(key)
}
return undefined
}
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number { private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
const rta = a instanceof RegexTag const rta = a instanceof RegexTag
const rtb = b instanceof RegexTag const rtb = b instanceof RegexTag

View file

@ -1,3 +1,5 @@
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export abstract class TagsFilter { export abstract class TagsFilter {
abstract asOverpass(): string[] abstract asOverpass(): string[]
@ -17,6 +19,8 @@ export abstract class TagsFilter {
properties: Record<string, string> properties: Record<string, string>
): string ): string
abstract asJson(): TagConfigJson
abstract usedKeys(): string[] abstract usedKeys(): string[]
/** /**

View file

@ -31,7 +31,7 @@ export class Stores {
* @param promise * @param promise
* @constructor * @constructor
*/ */
public static FromPromise<T>(promise: Promise<T>): Store<T> { public static FromPromise<T>(promise: Promise<T>): Store<T | undefined> {
const src = new UIEventSource<T>(undefined) const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d)) promise?.then((d) => src.setData(d))
promise?.catch((err) => console.warn("Promise failed:", err)) promise?.catch((err) => console.warn("Promise failed:", err))
@ -97,7 +97,10 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(f: (t: T) => J): Store<J> abstract map<J>(f: (t: T) => J): Store<J>
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J> abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J>
public mapD<J>(f: (t: T) => J, extraStoresToWatch?: Store<any>[]): Store<J> { public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<any>[]
): Store<J> {
return this.map((t) => { return this.map((t) => {
if (t === undefined) { if (t === undefined) {
return undefined return undefined
@ -105,7 +108,7 @@ export abstract class Store<T> implements Readable<T> {
if (t === null) { if (t === null) {
return null return null
} }
return f(t) return f(<Exclude<T, undefined | null>>t)
}, extraStoresToWatch) }, extraStoresToWatch)
} }
@ -201,24 +204,36 @@ export abstract class Store<T> implements Readable<T> {
mapped.addCallbackAndRun((newEventSource) => { mapped.addCallbackAndRun((newEventSource) => {
if (newEventSource === null) { if (newEventSource === null) {
sink.setData(null) sink.setData(null)
} else if (newEventSource === undefined) { return
}
if (newEventSource === undefined) {
sink.setData(undefined) sink.setData(undefined)
} else if (!seenEventSources.has(newEventSource)) { return
}
if (seenEventSources.has(newEventSource)) {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
return
}
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)
}
}) })
return sink return sink
} }
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
return this.bind((t) => {
if (t === undefined || t === null) {
return <undefined | null>t
}
return f(<Exclude<T, undefined | null>>t)
})
}
public stabilized(millisToStabilize): Store<T> { public stabilized(millisToStabilize): Store<T> {
if (Utils.runningFromConsole) { if (Utils.runningFromConsole) {
return this return this
@ -603,7 +618,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/ */
public static FromPromiseWithErr<T>( public static FromPromiseWithErr<T>(
promise: Promise<T> promise: Promise<T>
): UIEventSource<{ success: T } | { error: any }> { ): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined) const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise?.then((d) => src.setData({ success: d })) promise?.then((d) => src.setData({ success: d }))
promise?.catch((err) => src.setData({ error: err })) promise?.catch((err) => src.setData({ error: err }))
@ -771,18 +786,26 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* Monoidal map which results in a read-only store. 'undefined' is passed 'as is' * Monoidal map which results in a read-only store. 'undefined' is passed 'as is'
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
*/ */
public mapD<J>(f: (t: T) => J, extraSources: Store<any>[] = []): Store<J | undefined> { public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources: Store<any>[] = []
): Store<J | undefined> {
return new MappedStore( return new MappedStore(
this, this,
(t) => { (t) => {
if (t === undefined) { if (t === undefined) {
return undefined return undefined
} }
return f(t) if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>>t)
}, },
extraSources, extraSources,
this._callbacks, this._callbacks,
this.data === undefined ? undefined : f(this.data) this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<any>this.data)
) )
} }

View file

@ -14,7 +14,7 @@ export class MangroveIdentity {
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined) const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
this.keypair = keypairEventSource this.keypair = keypairEventSource
mangroveIdentity.addCallbackAndRunD(async (data) => { mangroveIdentity.addCallbackAndRunD(async (data) => {
if (data === "") { if (!data) {
return return
} }
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))

View file

@ -40,15 +40,26 @@ export interface P4CPicture {
export default class NearbyImagesSearch { export default class NearbyImagesSearch {
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
public static readonly apiUrls = ["https://api.flickr.com"] public static readonly apiUrls = ["https://api.flickr.com"]
private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[] private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number } | undefined>[]
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([]) private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
public readonly store: Store<P4CPicture[]> = this._store public readonly store: Store<P4CPicture[]> = this._store
public readonly allDone: Store<boolean>
private readonly _options: NearbyImageOptions private readonly _options: NearbyImageOptions
constructor(options: NearbyImageOptions, features: IndexedFeatureSource) { constructor(options: NearbyImageOptions, features: IndexedFeatureSource) {
this.individualStores = NearbyImagesSearch.services.map((s) => this.individualStores = NearbyImagesSearch.services.map((s) =>
NearbyImagesSearch.buildPictureFetcher(options, s) NearbyImagesSearch.buildPictureFetcher(options, s)
) )
const allDone = new UIEventSource(false)
this.allDone = allDone
const self = this
function updateAllDone(){
const stillRunning = self.individualStores.some(store => store.data === undefined)
allDone.setData(!stillRunning)
}
self.individualStores.forEach(s => s.addCallback(_ => updateAllDone()))
this._options = options this._options = options
if (features !== undefined) { if (features !== undefined) {
const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({ const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({
@ -93,13 +104,17 @@ export default class NearbyImagesSearch {
private static buildPictureFetcher( private static buildPictureFetcher(
options: NearbyImageOptions, options: NearbyImageOptions,
fetcher: P4CService fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number }> { ): Store<{ images: P4CPicture[]; beforeFilter: number } | null | undefined> {
const p4cStore = Stores.FromPromise<P4CPicture[]>( const p4cStore = Stores.FromPromiseWithErr<P4CPicture[]>(
NearbyImagesSearch.fetchImages(options, fetcher) NearbyImagesSearch.fetchImages(options, fetcher)
) )
const searchRadius = options.searchRadius ?? 100 const searchRadius = options.searchRadius ?? 100
return p4cStore.map( return p4cStore.mapD(
(images) => { (imagesState) => {
if(imagesState["error"]){
return null
}
let images = imagesState["success"]
if (images === undefined) { if (images === undefined) {
return undefined return undefined
} }

View file

@ -23,6 +23,7 @@ export default class Constants {
"gps_track", "gps_track",
"range", "range",
"last_click", "last_click",
"favourite",
] as const ] as const
/** /**
* Special layers which are not included in a theme by default * Special layers which are not included in a theme by default
@ -131,6 +132,8 @@ export default class Constants {
"clock", "clock",
"invalid", "invalid",
"close", "close",
"heart",
"heart_outline",
] as const ] as const
public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons

View file

@ -1,10 +1,11 @@
import { Translation } from "../UI/i18n/Translation" import { Translation } from "../UI/i18n/Translation"
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson" import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
import Translations from "../UI/i18n/Translations" import Translations from "../UI/i18n/Translations"
import { Store } from "../Logic/UIEventSource"
import BaseUIElement from "../UI/BaseUIElement"
import Toggle from "../UI/Input/Toggle"
/**
* A 'denomination' is one way to write a certain quantity.
* For example, 'meter', 'kilometer', 'mile' and 'foot' are all possible ways to quantify 'length'
*/
export class Denomination { export class Denomination {
public readonly canonical: string public readonly canonical: string
public readonly _canonicalSingular: string public readonly _canonicalSingular: string
@ -53,8 +54,8 @@ export class Denomination {
/** /**
* Create a representation of the given value * Create a representation of the given value
* @param value: the value from OSM * @param value the value from OSM
* @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed * @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
* *
* const unit = new Denomination({ * const unit = new Denomination({
* canonicalDenomination: "m", * canonicalDenomination: "m",
@ -82,6 +83,8 @@ export class Denomination {
* unit.canonicalValue("42", true) // =>"42" * unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42" * unit.canonicalValue("42 m", true) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42" * unit.canonicalValue("42 meter", true) // =>"42"
*
*
*/ */
public canonicalValue(value: string, actAsDefault: boolean): string { public canonicalValue(value: string, actAsDefault: boolean): string {
if (value === undefined) { if (value === undefined) {

View file

@ -24,6 +24,7 @@ export class MenuState {
public static readonly _menuviewTabs = [ public static readonly _menuviewTabs = [
"about", "about",
"settings", "settings",
"favourites",
"community", "community",
"privacy", "privacy",
"advanced", "advanced",
@ -78,6 +79,11 @@ export class MenuState {
this.highlightedUserSetting.setData(undefined) this.highlightedUserSetting.setData(undefined)
} }
}) })
this.menuViewTab.addCallbackD((tab) => {
if (tab !== "settings") {
this.highlightedUserSetting.setData(undefined)
}
})
this.themeViewTab.addCallbackAndRun((tab) => { this.themeViewTab.addCallbackAndRun((tab) => {
if (tab !== "filters") { if (tab !== "filters") {
this.highlightedLayerInFilters.setData(undefined) this.highlightedLayerInFilters.setData(undefined)

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