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
shell: bash
- name: generate layeroverview
run: npm run reset:layeroverview
- name: Prepare deploy
run: npm run prepare-deploy
shell: bash
- name: run tests
run: npm run test
shell: bash
- name: Prepare deploy
run: npm run prepare-deploy
shell: bash
- name: Clone deployment repo
env:
DEPLOY_KEY_PIETERVDVN: ${{ secrets.DEPLOY_KEY_PIETERVDVN }}

1
.gitignore vendored
View file

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

View file

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

View file

@ -18,5 +18,8 @@
"[svelte]": {
"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)
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?
---------------------
@ -227,49 +232,7 @@ The entire tagRendering will thus be:
```
The template
------------
[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:
## Make it official
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:

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": {
"and": [
"advertising!=poster_box",
"advertising!=column"
"advertising!=column",
"advertising!=billboard"
]
}
},

View file

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

View file

@ -329,15 +329,80 @@
"class": "medium"
},
"hideInAnswer": {
"or": [
"_country!=be",
"and": [
"_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!=ma",
"_country!=tn",
"_country!=pl",
"_country!=cs",
"_country!=pf",
"_country!=ge",
"_country!=gr",
"_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!=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": {
"path": "./assets/layers/charging_station/TypeE.svg",
"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"
]
}
@ -5102,16 +5213,15 @@
"tags": [
"amenity=charging_station",
"motorcar=no",
"bicycle=yes",
"socket:typee=1"
"bicycle=yes"
],
"title": {
"en": "a charging station for electrical bikes with a normal european wall plug <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (meant to charge electrical bikes)",
"nl": "een oplaadpunt voor elektrische fietsen met een gewoon Europees stopcontact <img src='./assets/layers/charging_station/typee.svg' style='width: 2rem; height: 2rem; float: left; background: white; border-radius: 1rem; margin-right: 0.5rem'/> (speciaal bedoeld voor fietsen)",
"en": "a charging station for electrical bikes",
"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)",
"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)",
"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)"
}
},

View file

@ -735,10 +735,12 @@
"point",
"centroid"
],
"marker": [{
"marker": [
{
"icon": "pin",
"color": "#fff"
},{
},
{
"icon": {
"render": "./assets/themes/charging_stations/plug.svg",
"mappings": [
@ -756,9 +758,9 @@
"then": "./assets/themes/charging_stations/car.svg"
}
]
}
}],
}
],
"iconBadges": [
{
"if": {
@ -802,12 +804,11 @@
"tags": [
"amenity=charging_station",
"motorcar=no",
"bicycle=yes",
"socket:typee=1"
"bicycle=yes"
],
"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'/>",
"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'/>"
"en": "charging station for electrical bikes",
"nl": "oplaadpunt voor elektrische fietsen"
}
},
{

View file

@ -91,7 +91,7 @@ function run(file, protojson) {
if (e.countryWhiteList.length > 0) {
// This is a 'hideInAnswer', thus _reverse_ logic!
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) {
const countries = e.countryBlackList.map(country => "_country=" + country) //HideInAnswer if it is in the wrong country
json["hideInAnswer"] = {or: countries}

View file

@ -1,6 +1,6 @@
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: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: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),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: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,

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)"
},
"reviews",
{
"id": "default_climbing_questions",
"builtin": [

View file

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

View file

@ -29,7 +29,8 @@
"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": [
"amenity=drinking_water",
"drinking_water=yes"
"drinking_water=yes",
"disused:amenity=drinking_water"
]
},
"man_made!=reservoir_covered",
@ -61,6 +62,11 @@
"cs": "Pitná voda"
}
},
"titleIcons": [
"icons.defaults",
"auto:type",
"auto:seasonal"
],
"pointRendering": [
{
"iconBadges": [
@ -68,10 +74,15 @@
"if": {
"or": [
"operational_status=broken",
"operational_status=closed"
"operational_status=closed",
"disused:amenity=drinking_water"
]
},
"then": "close:#c33"
},
{
"if": "tourism=artwork",
"then": "circle:white;./assets/layers/artwork/artwork.svg"
}
],
"iconSize": "40,40",
@ -147,6 +158,10 @@
"mappings": [
{
"if": "operational_status=",
"addExtraTags": [
"disused:amenity=",
"amenity=drinking_water"
],
"then": {
"en": "This drinking water works",
"nl": "Deze drinkwaterfontein werkt",
@ -190,6 +205,46 @@
],
"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": {
"en": "How easy is it to fill water bottles?",
@ -235,8 +290,153 @@
}
}
],
"condition": "fountain!=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",
"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>",
"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",
"license": "CC-BY-SA-4.0",
@ -12,5 +32,31 @@
"sources": [
"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": [
"(basis|lagere |middelbare |secondaire| secundaire)?school"
],
"en": [
"east",
"north",
"northeast",
"northwest",
"south",
"southeast",
"southwest",
"west"
],
"fr": [
"allée (des |de la |de l'|de |du |d')?",
"autoroute (des |de la |de l'|de |du |d')?",
@ -207,11 +217,22 @@
"en": [
"avenue",
"boulevard",
"circle",
"church",
"drive",
"expressway",
"freeway",
"highway",
"lane",
"parkway",
"path",
"plaza",
"road",
"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",
"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>",
"condition": {
@ -27,7 +28,7 @@
{
"#": "ignore-image-in-then",
"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"
},
{
"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",
"labels": [
"defaults"
"defaults",
"in_favourite"
],
"render": "<a href='tel:{phone}'><img textmode='📞' alt='phone' src='./assets/layers/questions/phone.svg'/></a>",
"mappings": [
@ -89,7 +137,8 @@
{
"id": "emaillink",
"labels": [
"defaults"
"defaults",
"in_favourite"
],
"render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/layers/questions/send_email.svg'/></a>",
"mappings": [
@ -109,7 +158,8 @@
{
"id": "websitelink",
"labels": [
"defaults"
"defaults",
"in_favourite"
],
"render": "<a href='{website}' target='_blank' rel='noopener'><img textmode='🌐' alt='website' src='./assets/layers/icons/website.svg'/></a>",
"condition": "website~*"
@ -117,7 +167,8 @@
{
"id": "smokingicon",
"labels": [
"defaults"
"defaults",
"in_favourite"
],
"mappings": [
{
@ -140,6 +191,16 @@
"render": "{share_link()}",
"metacondition": "_supports_sharing=yes"
},
{
"id": "favourite_title_icon",
"labels": [
"defaults"
],
"render": {
"*": "{favourite_icon()}"
},
"metacondition": "_loggedIn=true"
},
{
"id": "osmlink",
"labels": [
@ -162,7 +223,8 @@
{
"id": "dogicon",
"labels": [
"defaults"
"defaults",
"in_favourite"
],
"mappings": [
{
@ -193,6 +255,13 @@
"class": "w-20 mx-1 flex items-center"
},
"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": [
"images",
"reviews",
{
"question": {
"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"
]
},
{
"path": "center.svg",
"license": "CC0-1.0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "checkmark.svg",
"license": "CC0-1.0",

View file

@ -69,10 +69,12 @@
},
"+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>",
"condition": "climbing:length~*"
},
{
"id": "climbing_bolts",
"mappings": [
{
"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>"
},
{
"id": "difficulty",
"render": "<div class='flex justify-center rounded-full pl-1 pr-1 climbing-{__difficulty:char}'> {climbing:grade:french}</div>",
"condition": "__difficulty:char~*"
}
@ -345,8 +348,7 @@
"key": "access:description"
}
},
"questions",
"reviews"
"questions"
]
}
},

View file

@ -112,7 +112,15 @@
"lineRendering": [
{
"width": "4",
"color": "#00a703"
"color": {
"render": "#00a703",
"mappings": [
{
"if": "state=proposed",
"then": "#f0a513"
}
]
}
}
],
"pointRendering": null
@ -134,8 +142,9 @@
},
"source": {
"osmTags": {
"and": [
"rcn_ref~*"
"or": [
"rcn_ref~*",
"proposed:rcn_ref~*"
]
}
},
@ -146,14 +155,15 @@
"centroid"
],
"label": {
"render": "<div style='position: absolute; top: -30px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>?</div>",
"mappings": [
{
"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=",
"then": "<div style='position: absolute; top: -10px; right: -10px; color: white; background-color: #00a703; width: 20px; height: 20px; border-radius: 100%'>?</div>"
"if": "proposed:rcn_ref~*",
"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,
"title": {
"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>",
"de": "Fahrradknotenpunkt <strong>{rcn_ref}</strong>",
"es": "nodo ciclista <strong>{rcn_ref}</strong>",
@ -173,6 +197,15 @@
"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": [
{
"id": "node-rxn_ref",
@ -197,7 +230,8 @@
"nl": "Dit fietsknooppunt heeft referentienummer {rcn_ref}",
"de": "Knotenpunktnummer {rcn_ref} des Fahrradknotenpunktnetzwerks",
"cs": "Tento cyklistický uzel má referenční číslo {rcn_ref}"
}
},
"condition": "rcn_ref~*"
},
{
"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

View file

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

View file

@ -1,33 +1,13 @@
{
"id": "mapcomplete-changes",
"title": {
"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"
"en": "Changes made with MapComplete"
},
"shortDescription": {
"en": "Show changes made with 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"
"en": "Shows changes made by MapComplete"
},
"description": {
"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"
"en": "This maps shows all the changes made with MapComplete"
},
"icon": "./assets/svg/logo.svg",
"hideFromOverview": true,
@ -40,13 +20,7 @@
{
"id": "mapcomplete-changes",
"name": {
"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"
"en": "Changeset centers"
},
"minzoom": 0,
"source": {
@ -57,85 +31,41 @@
},
"title": {
"render": {
"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}"
"en": "Changeset for {theme}"
}
},
"description": {
"en": "Show 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"
"en": "Shows all MapComplete changes"
},
"tagRenderings": [
{
"id": "show_changeset_id",
"render": {
"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>"
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
}
},
{
"id": "contributor",
"question": {
"en": "Which contributor made 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?"
"en": "What contributor did make this change?"
},
"freeform": {
"key": "user"
},
"render": {
"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>"
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
}
},
{
"id": "theme-id",
"question": {
"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?"
"en": "What theme was used to make this change?"
},
"freeform": {
"key": "theme"
},
"render": {
"en": "Change with theme <a href='https://mapcomplete.osm.be/{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>"
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
}
},
{
@ -144,45 +74,19 @@
"key": "locale"
},
"question": {
"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ę?"
"en": "What locale (language) was this change made in?"
},
"render": {
"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}"
"en": "User locale is {locale}"
}
},
{
"id": "host",
"render": {
"en": "Change made 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>"
"en": "Change with with <a href='{host}'>{host}</a>"
},
"question": {
"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?"
"en": "What host (website) was this change made with?"
},
"freeform": {
"key": "host"
@ -203,22 +107,10 @@
{
"id": "version",
"question": {
"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ę?"
"en": "What version of MapComplete was used to make this change?"
},
"render": {
"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}"
"en": "Made with {editor}"
},
"freeform": {
"key": "editor"
@ -392,6 +284,10 @@
"if": "theme=kerbs_and_crossings",
"then": "./assets/layers/kerbs/KerbIcon.svg"
},
{
"if": "theme=mapcomplete-changes",
"then": "./assets/svg/logo.svg"
},
{
"if": "theme=maproulette",
"then": "./assets/layers/maproulette/logomark.svg"
@ -564,13 +460,23 @@
}
],
"question": {
"en": "Theme name 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}"
"en": "Themename contains {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": {
"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}"
"en": "Made by contributor {search}"
}
}
]
@ -608,13 +508,7 @@
}
],
"question": {
"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}"
"en": "<b>Not</b> made by contributor {search}"
}
}
]
@ -631,13 +525,7 @@
}
],
"question": {
"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}"
"en": "Made before {search}"
}
}
]
@ -654,13 +542,7 @@
}
],
"question": {
"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}"
"en": "Made after {search}"
}
}
]
@ -676,14 +558,7 @@
}
],
"question": {
"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}"
"en": "User language (iso-code) {search}"
}
}
]
@ -699,13 +574,7 @@
}
],
"question": {
"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}"
"en": "Made with host {search}"
}
}
]
@ -716,14 +585,29 @@
{
"osmTags": "add-image>0",
"question": {
"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",
"pl": "Zestaw zmian dodał co najmniej jedno zdjęcie"
"en": "Changeset added at least one image"
}
}
]
},
{
"id": "exclude_grb",
"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",
"render": {
"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>"
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</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",
"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",
"then": {
"en": "Yes, there is a sidewalk on this side of the road",
"de": "Ja, es gibt einen Bürgersteig auf dieser Straßenseite",
"da": "Ja, der er et fortov på denne side af vejen",
"nl": "Ja, er is een stoep aan deze kant van de weg",
"fr": "Oui, il y a un trottoir de ce côté de la route",
"ca": "Sí, hi ha una vorera a aquest costat del carrer",
"es": "Sí, hay una acera en este lado de la calle",
"cs": "Ano, na této straně silnice je chodník",
"it": "Sì, c'è un marciapiede su questo lato della strada",
"pl": "Tak, jest chodnik z boku drogi"
"en": "There is a sidewalk on this side of the road",
"de": "Es gibt einen Bürgersteig auf dieser Straßenseite",
"da": "Der er et fortov på denne side af vejen",
"nl": "Er is een stoep aan deze kant van de weg",
"fr": "Il y a un trottoir de ce côté de la route",
"ca": "Hi ha una vorera a aquest costat del carrer",
"es": "Hay una acera en este lado de la calle",
"cs": "Na této straně silnice je chodník",
"it": "C'è un marciapiede su questo lato della strada",
"pl": "Jest chodnik z boku drogi"
}
},
{
"if": "sidewalk:left|right=no",
"then": {
"en": "No, there is no sidewalk to walk on",
"de": "Nein, es gibt keinen Bürgersteig für Fußgänger",
"da": "Nej, der er ikke noget fortov at gå på",
"nl": "Nee, er is geen stoep om op te lopen",
"fr": "Non, il n'y a pas de trottoir où marcher",
"ca": "No, no hi ha vorera per la que caminar",
"es": "No, no hay acera por la que caminar",
"cs": "Ne, není tu žádný chodník",
"it": "No, non c'è un marciapiede su cui camminare",
"pl": "Nie, nie ma chodnika, którym można chodzić"
"en": "There is no sidewalk to walk on",
"de": "Es gibt keinen Bürgersteig für Fußgänger",
"da": "Der er ikke noget fortov at gå på",
"nl": "Er is geen stoep om op te lopen",
"fr": "Il n'y a pas de trottoir où marcher",
"ca": "No hi ha vorera per la que caminar",
"es": "No hay acera por la que caminar",
"cs": "Není tu žádný chodník",
"it": "Non c'è un marciapiede su cui camminare",
"pl": "Nie ma chodnika, którym można chodzić"
}
},
{

View file

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

View file

@ -50,6 +50,22 @@
"panelIntro": "<h3>Your personal theme</h3>Activate your favourite layers from all the official themes",
"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": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.org",
@ -404,6 +420,7 @@
"key": "Key combination",
"openLayersPanel": "Opens the layers and filters panel",
"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",
"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",
@ -419,6 +436,7 @@
"isDeleted": "Deleted",
"nearby": {
"link": "This picture shows the object",
"noNearbyImages": "No nearby images were found",
"seeNearby": "Browse and link nearby pictures",
"title": "Nearby streetview imagery"
},

View file

@ -1993,7 +1993,7 @@
"name": "Ladestationen",
"presets": {
"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": {
"title": "Eine Ladestation für Elektrofahrzeuge"

View file

@ -1993,7 +1993,7 @@
"name": "Charging stations",
"presets": {
"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": {
"title": "a charging station for cars"
@ -4218,6 +4218,9 @@
},
"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>"
},
"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?",
"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": "<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": {

View file

@ -1842,7 +1842,7 @@
"name": "Oplaadpunten",
"presets": {
"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": {
"title": "een oplaadstation voor elektrische auto's"
@ -4092,8 +4092,26 @@
"question": "Is deze drinkwaterkraan nog steeds werkende?",
"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": "<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": {

View file

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

View file

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

View file

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

View file

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

View file

@ -524,7 +524,15 @@
}
},
"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": {
@ -903,46 +911,67 @@
"1": {
"options": {
"0": {
"question": "Made by contributor {search}"
"question": "Theme name does <b>not</b> contain {search}"
}
}
},
"2": {
"options": {
"0": {
"question": "<b>Not</b> made by contributor {search}"
"question": "Made by contributor {search}"
}
}
},
"3": {
"options": {
"0": {
"question": "Made before {search}"
"question": "<b>Not</b> made by contributor {search}"
}
}
},
"4": {
"options": {
"0": {
"question": "Made after {search}"
"question": "Made before {search}"
}
}
},
"5": {
"options": {
"0": {
"question": "User language (iso-code) {search}"
"question": "Made after {search}"
}
}
},
"6": {
"options": {
"0": {
"question": "Made with host {search}"
"question": "User language (iso-code) {search}"
}
}
},
"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": {
"0": {
"question": "Changeset added at least one image"
@ -1199,10 +1228,10 @@
"1": {
"mappings": {
"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": {
"then": "No, there is no sidewalk to walk on"
"then": "There is no sidewalk to walk on"
},
"2": {
"then": "There is a separately mapped sidewalk to walk on"
@ -1422,7 +1451,15 @@
}
},
"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": {

View file

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

View file

@ -506,7 +506,12 @@
}
},
"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": {
"0": {
"filter": {
"5": {
"6": {
"options": {
"0": {
"question": "Langage utilisateur (code-ISO) {search}"
}
}
},
"7": {
"8": {
"options": {
"0": {
"question": "Le groupe de modifications a ajouté au moins une image"
@ -1102,10 +1107,10 @@
"1": {
"mappings": {
"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": {
"then": "Non, il n'y a pas de trottoir où marcher"
"then": "Il n'y a pas de trottoir où marcher"
},
"2": {
"then": "Il y a un trottoir où marcher cartographié séparément"

View file

@ -599,10 +599,10 @@
"1": {
"mappings": {
"0": {
"then": "Sì, c'è un marciapiede su questo lato della strada"
"then": "C'è un marciapiede su questo lato della strada"
},
"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": {
"name": "noder",
"title": {
"render": "sykkelnode <strong>{rcn_ref}</strong>"
"mappings": {
"0": {
"then": "sykkelnode <strong>{rcn_ref}</strong>"
}
},
"render": "sykkelnode"
}
}
},

View file

@ -475,7 +475,15 @@
}
},
"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": {
"0": {
"question": "Gemaakt door bijdrager {search}"
}
}
},
"2": {
"3": {
"options": {
"0": {
"question": "<b>Niet</b> gemaakt door bijdrager {search}"
}
}
},
"3": {
"4": {
"options": {
"0": {
"question": "Gemaakt voor {search}"
}
}
},
"4": {
"5": {
"options": {
"0": {
"question": "Gemaakt na {search}"
}
}
},
"5": {
"6": {
"options": {
"0": {
"question": "De taal van de bijdrager is {search}"
}
}
},
"6": {
"7": {
"options": {
"0": {
"question": "Gemaakt met host {search}"
}
}
},
"7": {
"8": {
"options": {
"0": {
"question": "Changeset bevat minstens één afbeelding"
@ -1156,10 +1164,10 @@
"1": {
"mappings": {
"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": {
"then": "Nee, er is geen stoep om op te lopen"
"then": "Er is geen stoep om op te lopen"
},
"2": {
"then": "Er is een apart ingetekende stoep om op te lopen"
@ -1424,7 +1432,15 @@
}
},
"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": {
"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": {
"0": {
"question": "Wykonane przez współautora {search}"
}
}
},
"2": {
"3": {
"options": {
"0": {
"question": "<b>Nie</b> wykonane przez współautora {search}"
}
}
},
"3": {
"4": {
"options": {
"0": {
"question": "Stworzone przed {search}"
}
}
},
"4": {
"5": {
"options": {
"0": {
"question": "Stworzone po {search}"
}
}
},
"5": {
"6": {
"options": {
"0": {
"question": "Język użytkownika (kod iso) {search}"
}
}
},
"6": {
"7": {
"options": {
"0": {
"question": "Wykonane z hostem {search}"
}
}
},
"7": {
"8": {
"options": {
"0": {
"question": "Zestaw zmian dodał co najmniej jedno zdjęcie"
@ -1163,10 +1168,10 @@
"1": {
"mappings": {
"0": {
"then": "Tak, jest chodnik z boku drogi"
"then": "Jest chodnik z boku drogi"
},
"1": {
"then": "Nie, nie ma chodnika, którym można chodzić"
"then": "Nie ma chodnika, którym można chodzić"
},
"2": {
"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",
"version": "0.35.1",
"version": "0.36.3",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"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 ",
"fix:schemas": "vite-node scripts/fixSchemas.ts ",
"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'",
"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",
"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",
"reset:layeroverview": "npm run prep:layeroverview && npm run generate:layeroverview && npm run refresh:layeroverview",
"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 -",
"prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh",
"lint": "npm run lint:prettier && npm run lint:eslint",
@ -133,6 +134,7 @@
"opening_hours": "^3.6.0",
"osm-auth": "^2.2.0",
"osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
"pic4carto": "^2.1.15",
"prompt-sync": "^4.2.0",
@ -141,6 +143,7 @@
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^1.13.1",
"tailwindcss": "^3.1.8",
"trap-focus-svelte": "^1.0.1",
"vite-node": "^0.28.3",
"vitest": "^0.28.3",
"wikibase-sdk": "^7.14.0",
@ -178,7 +181,7 @@
"prettier-plugin-tailwindcss": "^0.3.0",
"read-file": "^0.2.0",
"sass": "^1.58.0",
"sharp": "^0.30.5",
"sharp": "^0.32.6",
"svelte": "^3.55.1",
"svelte-check": "^3.0.2",
"svelte-preprocess": "^5.0.1",

View file

@ -729,6 +729,14 @@ video {
bottom: 0px;
}
.right-4 {
right: 1rem;
}
.top-4 {
top: 1rem;
}
.right-1\/3 {
right: 33.333333%;
}
@ -745,6 +753,10 @@ video {
top: 2.5rem;
}
.left-1\/4 {
left: 25%;
}
.isolate {
isolation: isolate;
}
@ -765,10 +777,6 @@ video {
float: left;
}
.m-8 {
margin: 2rem;
}
.m-4 {
margin: 1rem;
}
@ -781,6 +789,10 @@ video {
margin: 0px;
}
.m-8 {
margin: 2rem;
}
.m-2 {
margin: 0.5rem;
}
@ -793,10 +805,58 @@ video {
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 {
margin: 1.5rem;
}
.m-32 {
margin: 8rem;
}
.m-44 {
margin: 11rem;
}
.m-28 {
margin: 7rem;
}
.m-7 {
margin: 1.75rem;
}
.m-px {
margin: 1px;
}
@ -841,6 +901,10 @@ video {
margin-right: 3rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem;
}
@ -877,10 +941,6 @@ video {
margin-right: 0.25rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 {
margin-left: 0.25rem;
}
@ -929,6 +989,10 @@ video {
margin-right: 0.75rem;
}
.mt-12 {
margin-top: 3rem;
}
.mr-12 {
margin-right: 3rem;
}
@ -1025,14 +1089,14 @@ video {
height: 6rem;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.h-full {
height: 100%;
}
.h-32 {
height: 8rem;
}
@ -1084,6 +1148,10 @@ video {
height: 2.75rem;
}
.h-5 {
height: 1.25rem;
}
.h-48 {
height: 12rem;
}
@ -1104,14 +1172,14 @@ video {
height: 10rem;
}
.h-80 {
height: 20rem;
}
.h-64 {
height: 16rem;
}
.h-80 {
height: 20rem;
}
.max-h-12 {
max-height: 3rem;
}
@ -1120,6 +1188,10 @@ video {
max-height: 6rem;
}
.max-h-64 {
max-height: 16rem;
}
.max-h-7 {
max-height: 1.75rem;
}
@ -1194,6 +1266,18 @@ video {
width: 50%;
}
.w-14 {
width: 3.5rem;
}
.w-auto {
width: auto;
}
.w-5 {
width: 1.25rem;
}
.w-10 {
width: 2.5rem;
}
@ -1207,10 +1291,6 @@ video {
width: 12rem;
}
.w-auto {
width: auto;
}
.max-w-full {
max-width: 100%;
}
@ -1285,6 +1365,10 @@ video {
appearance: none;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@ -1345,6 +1429,10 @@ video {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.gap-1 {
gap: 0.25rem;
}
@ -1437,6 +1525,14 @@ video {
align-self: center;
}
.justify-self-start {
justify-self: start;
}
.justify-self-end {
justify-self: end;
}
.overflow-auto {
overflow: auto;
}
@ -1593,6 +1689,10 @@ video {
border-style: dotted;
}
.border-none {
border-style: none;
}
.border-black {
--tw-border-opacity: 1;
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));
}
.bg-white\/50 {
background-color: rgb(255 255 255 / 0.5);
}
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
@ -1692,14 +1796,14 @@ video {
padding: 0.25rem;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-0 {
padding: 0px;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-12 {
padding: 3rem;
}
@ -1764,6 +1868,10 @@ video {
padding-left: 1rem;
}
.pr-1 {
padding-right: 0.25rem;
}
.pl-3 {
padding-left: 0.75rem;
}
@ -1772,10 +1880,6 @@ video {
padding-right: 0px;
}
.pr-1 {
padding-right: 0.25rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
@ -1784,6 +1888,10 @@ video {
padding-bottom: 0.5rem;
}
.pt-1 {
padding-top: 0.25rem;
}
.text-center {
text-align: center;
}
@ -1945,6 +2053,10 @@ video {
opacity: 0.5;
}
.opacity-100 {
opacity: 1;
}
.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-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);
}
.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-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;
@ -2054,10 +2172,8 @@ video {
transition-duration: 150ms;
}
.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;
.duration-200 {
transition-duration: 200ms;
}
.ease-in-out {
@ -2126,6 +2242,10 @@ body {
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,
img {
box-sizing: content-box;
@ -2327,6 +2447,16 @@ button.disabled:hover, .button.disabled:hover {
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 {
fill: var(--interactive-foreground) !important;
}
@ -2696,6 +2826,15 @@ a.link-underline {
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 {
--tw-bg-opacity: 1;
background-color: rgb(199 210 254 / var(--tw-bg-opacity));
@ -2850,10 +2989,6 @@ a.link-underline {
padding: 1.5rem;
}
.md\:p-4 {
padding: 1rem;
}
.md\:p-3 {
padding: 0.75rem;
}

View file

@ -10,11 +10,15 @@ mkdir dist 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
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
if [ $? -ne 0 ]; then
@ -48,9 +52,10 @@ else
exit 1
fi
export NODE_OPTIONS=--max-old-space-size=7000
export NODE_OPTIONS=--max-old-space-size=16000
which vite
vite build --sourcemap
vite --version
vite build # --sourcemap
# Copy the layer files, as these might contain assets (e.g. svgs)
cp -r assets/layers/ dist/assets/layers/
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 { BBox } from "../src/Logic/BBox"
import * as fs from "fs"
import { writeFileSync } from "fs"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { Feature } from "geojson"
import ScriptUtils from "./ScriptUtils"
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> {
const filenameLong = url.replace(/[\/:.\-%]/g, "_") + ".jpg"
const targetPathLong = imagePath + "/" + filenameLong
@ -390,6 +442,7 @@ export default class GenerateImageAnalysis extends Script {
const imageBackupPath = args[0]
await this.downloadData(datapath, cached)
await this.downloadViews(datapath)
await this.downloadMetadata(datapath)
await this.downloadAllImages(datapath, imageBackupPath)
this.analyze(datapath)

View file

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

View file

@ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
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.
// It spits out an overview of those to be used to load them
@ -381,23 +382,20 @@ class LayerOverviewUtils extends Script {
forceReload
)
writeFileSync(
"./src/assets/generated/known_themes.json",
JSON.stringify({
themes: Array.from(sharedThemes.values()),
})
)
writeFileSync(
"./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"
if (
(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)
) {
// mapcomplete-changes shows an icon for each corresponding mapcomplete-theme
@ -428,6 +426,19 @@ class LayerOverviewUtils extends Script {
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 millisNeeded = end.getTime() - start.getTime()
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
@ -711,7 +722,9 @@ class LayerOverviewUtils extends Script {
ConversionContext.construct([themePath], ["PrepareLayer"])
)
try {
themeFile = new PrepareTheme(convertState).convertStrict(
themeFile = new PrepareTheme(convertState, {
skipDefaultLayers: true,
}).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
@ -791,4 +804,5 @@ class LayerOverviewUtils extends Script {
}
}
new GenerateFavouritesLayer().run()
new LayerOverviewUtils().run()

View file

@ -19,7 +19,7 @@ import ValidationUtils from "../src/Models/ThemeConfig/Conversion/ValidationUtil
const sharp = require("sharp")
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 {
return encodeURIComponent(str.toLowerCase())
@ -487,8 +487,19 @@ async function createIndexFor(theme: LayoutConfig) {
`import layout from "./src/assets/generated/themes/${theme.id}.json"`,
`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")
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)
}

View file

@ -4,6 +4,8 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { Utils } from "../src/Utils"
import { writeFileSync } from "fs"
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 */
@ -21,7 +23,12 @@ async function main(includeTags = true) {
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()
for (const key of allKeys) {
if (!keysAndTags.has(key)) {
@ -68,6 +75,8 @@ async function main(includeTags = true) {
"./src/assets/key_totals.json",
JSON.stringify(
{
"#": "Generated with generateStats.ts",
date: new Date().toISOString(),
keys: Utils.MapToObj(keyTotal, (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
}
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) {
let body = ""
@ -53,6 +83,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
await new Promise((resolve) => req.on("end", resolve))
console.log(new Date().toISOString())
let parsed: any
try {
parsed = JSON.parse(body)
@ -84,7 +115,7 @@ async function handlePost(req: http.IncomingMessage, res: ServerResponse) {
http.createServer(async (req: http.IncomingMessage, res) => {
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(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
@ -101,6 +132,12 @@ http.createServer(async (req: http.IncomingMessage, res) => {
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)
console.log("URL pathname is")
if (url.pathname.endsWith("overview")) {

View file

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

View file

@ -6,13 +6,22 @@ import { Changes } from "../Osm/Changes"
import { OsmConnection } from "../Osm/OsmConnection"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SimpleMetaTagger from "../SimpleMetaTagger"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { Feature } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
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 {
private static readonly metatags = new Set([
"timestamp",
@ -23,38 +32,96 @@ export default class SelectedElementTagsUpdater {
"id",
])
private readonly state: {
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
constructor(state: TagsUpdaterState) {
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (!isLoggedIn && !Utils.runningFromConsole) {
return
}
this.installCallback()
this.installCallback(state)
// We only have to do this once...
return true
})
}
private installCallback() {
const state = this.state
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
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) => {
let id = s.properties?.id
if (!id) {
@ -94,7 +161,7 @@ export default class SelectedElementTagsUpdater {
oldFeature.geometry = newGeometry
state.featureProperties.getStore(id)?.ping()
}
this.applyUpdate(latestTags, id)
SelectedElementTagsUpdater.applyUpdate(latestTags, id, state)
console.log("Updated", id)
} 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 iconTr: string | TagRenderingConfigJson = <any>(
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)
)
) ?? "bug"
const icon = new TagRenderingConfig(iconTr).render.txt
json = {
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"
export default class NearbyFeatureSource implements FeatureSource {
private readonly _result = new UIEventSource<Feature[]>(undefined)
public readonly features: Store<Feature[]>
private readonly _targetPoint: Store<{ lon: number; lat: number }>
private readonly _numberOfNeededFeatures: number
private readonly _layerState?: LayerState
private readonly _currentZoom: Store<number>
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
constructor(
targetPoint: Store<{ lon: number; lat: number }>,
@ -18,41 +22,44 @@ export default class NearbyFeatureSource implements FeatureSource {
layerState?: LayerState,
currentZoom?: Store<number>
) {
this._layerState = layerState
this._targetPoint = targetPoint.stabilized(100)
this._numberOfNeededFeatures = numberOfNeededFeatures
this._currentZoom = currentZoom.stabilized(500)
const allSources: Store<{ feat: Feature; d: number }[]>[] = []
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))
}
this.features = Stores.ListStabilized(this._result)
sources.forEach((source, layer) => {
const flayer = layerState?.filteredLayers.get(layer)
minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
this.registerSource(source, layer)
})
}
public registerSource(source: FeatureSource, layerId: string) {
const flayer = this._layerState?.filteredLayers.get(layerId)
if (!flayer) {
return
}
const calcSource = this.createSource(
source.features,
flayer.layerDef.minzoom,
flayer.isDisplayed
)
calcSource.addCallbackAndRunD((features) => {
update()
})
allSources.push(calcSource)
this.update()
})
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
segmentShardWith: number[]
coordinates: []
}[] {
// 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])
type edge = {
start: [number, number]
end: [number, number]
intermediate: [number, number][]
members: { index: number; isReversed: boolean }[]
/**
* Given a list of points, convert into a GPX-list, e.g. for favourites
* @param locations
* @param title
*/
public static toGpxPoints(
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
title?: string
) {
title = title?.trim()
if (title === undefined || title === "") {
title = "Created with MapComplete"
}
// The strategy:
// 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
// 2. Join these edges back together - as long as their membership groups are the same
// 3. Convert to results
const allEdgesByKey = new Map<string, edge>()
for (let index = 0; index < coordinatess.length; index++) {
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: [],
title = Utils.EncodeXmlValue(title)
const trackPoints: string[] = []
for (const l of locations) {
let trkpt = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
for (const key in l.properties) {
const keyCleaned = key.replaceAll(":", "__")
trkpt += ` <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n`
if (key === "website") {
trkpt += ` <link>${l.properties[key]}</link>\n`
}
}
allEdgesByKey.set(key, edge)
trkpt += " </wpt>\n"
trackPoints.push(trkpt)
}
}
// Lets merge them back together!
let didMergeSomething = false
let allMergedEdges = Array.from(allEdgesByKey.values())
const allEdgesByStartPoint = new Map<string, edge[]>()
for (const edge of allMergedEdges) {
edge.members.sort((m0, m1) => m0.index - m1.index)
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 []
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">'
return (
header +
"\n<name>" +
title +
"</name>\n<trk><trkseg>\n" +
trackPoints.join("\n") +
"\n</trkseg></trk></gpx>"
)
}
/**

View file

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

View file

@ -5,14 +5,16 @@ import { Utils } from "../../Utils"
export interface ProvidedImage {
url: string
url_hd?: string
key: string
provider: ImageProvider
id: string
}
export default abstract class ImageProvider {
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
@ -28,7 +30,7 @@ export default abstract class ImageProvider {
throw "No `defaultKeyPrefixes` defined by this image provider"
}
const relevantUrls = new UIEventSource<
{ url: string; key: string; provider: ImageProvider }[]
{ id: string; url: string; key: string; provider: ImageProvider }[]
>([])
const seenValues = new Set<string>()
allTags.addCallbackAndRunD((tags) => {
@ -67,4 +69,8 @@ export default abstract class ImageProvider {
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
public abstract apiUrls(): string[]
public backlink(): string | undefined {
return undefined
}
}

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Constants from "../../Models/Constants"
import Link from "../../UI/Base/Link"
export class Mapillary extends ImageProvider {
public static readonly singleton = new Mapillary()
@ -17,10 +18,6 @@ export class Mapillary extends ImageProvider {
]
defaultKeyPrefixes = ["mapillary", "image"]
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
/**
* Indicates that this is the same URL
* Ignores 'stp' parameter
@ -57,6 +54,30 @@ export class Mapillary extends ImageProvider {
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
*/
@ -80,8 +101,22 @@ export class Mapillary extends ImageProvider {
return undefined
}
SourceIcon(backlinkSource?: string): BaseUIElement {
return Svg.mapillary_svg()
apiUrls(): string[] {
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>[]> {
@ -106,14 +141,18 @@ export class Mapillary extends ImageProvider {
const metadataUrl =
"https://graph.mapillary.com/" +
mapillaryId +
"?fields=thumb_1024_url&&access_token=" +
"?fields=thumb_1024_url,thumb_original_url&access_token=" +
Constants.mapillary_client_token_v4
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
const url = <string>response["thumb_1024_url"]
console.log(response)
const url_hd = <string>response["thumb_original_url"]
return {
url: url,
id: "" + mapillaryId,
url,
url_hd,
provider: this,
key: key,
key,
}
}
}

View file

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

View file

@ -1,7 +1,6 @@
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import BaseUIElement from "../../UI/BaseUIElement"
import Svg from "../../Svg"
import Link from "../../UI/Base/Link"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Wikimedia from "../Web/Wikimedia"
@ -70,17 +69,8 @@ export class WikimediaImageProvider extends ImageProvider {
return WikimediaImageProvider.apiUrls
}
SourceIcon(backlink: string): BaseUIElement {
const img = 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
)
SourceIcon(): BaseUIElement {
return Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
}
public PrepUrl(value: string): ProvidedImage {
@ -173,6 +163,6 @@ export class WikimediaImageProvider extends ImageProvider {
if (!image.startsWith("File:")) {
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",
{}
)
/**
* A map containing the individual preference sources
* @private
*/
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private auth: any
private userDetails: UIEventSource<UserDetails>
@ -21,7 +25,10 @@ export class OsmPreferences {
this.auth = auth
this.userDetails = osmConnection.userDetails
const self = this
osmConnection.OnLoggedIn(() => self.UpdatePreferences())
osmConnection.OnLoggedIn(() => {
self.UpdatePreferences(true)
return true
})
}
/**
@ -72,11 +79,19 @@ export class OsmPreferences {
let i = 0
while (str !== "") {
if (str === undefined || str === "undefined") {
source.setData(undefined)
throw (
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
key
)
}
if (str === "undefined") {
source.setData(undefined)
throw (
"Got a literal string containing 'undefined' for a long preference with name " +
key
)
}
if (i > 100) {
throw "This long preference is getting very long... "
}
@ -197,7 +212,7 @@ export class OsmPreferences {
})
}
private UpdatePreferences() {
private UpdatePreferences(forceUpdate?: boolean) {
const self = this
this.auth.xhr(
{
@ -210,11 +225,22 @@ export class OsmPreferences {
return
}
const prefs = value.getElementsByTagName("preference")
const seenKeys = new Set<string>()
for (let i = 0; i < prefs.length; i++) {
const pref = prefs[i]
const k = pref.getAttribute("k")
const v = pref.getAttribute("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
@ -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 showAllQuestionsAtOnce: UIEventSource<boolean>
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 homeLocation: FeatureSource
/**
@ -294,6 +294,9 @@ export default class UserRelatedState {
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) {
const v = newPrefs[k]
if (v === "undefined" || !v) {
continue
}
if (k.endsWith("-combined-length")) {
const l = Number(v)
const key = k.substring(0, k.length - "length".length)
@ -308,7 +311,6 @@ export default class UserRelatedState {
}
amendedPrefs.ping()
console.log("Amended prefs are:", amendedPrefs.data)
})
const translationMode = osmConnection.GetPreference("translation-mode")

View file

@ -4,39 +4,11 @@ export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
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"
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(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 { Tag } from "./Tag"
import { RegexTag } from "./RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class And extends TagsFilter {
public and: TagsFilter[]
@ -72,6 +73,10 @@ export class And extends TagsFilter {
return allChoices
}
asJson(): TagConfigJson {
return { and: this.and.map((a) => a.asJson()) }
}
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
return this.and
.map((t) => {
@ -228,6 +233,15 @@ export class And extends TagsFilter {
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 {
if (this.and.length === 0) {
return true
@ -289,9 +303,17 @@ export class And extends TagsFilter {
optimized.splice(i, 1)
i--
}
} else if (v !== opt.value) {
// detected an internal conflict
} else {
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
} 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(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
)
if (elements.length > 0) {
newOrs.push(Or.construct(elements))
}
}
if (newOrs.length > 0) {
commonValues.push(And.construct(newOrs))
}
const result = new Or(commonValues).optimize()
if (result === false) {
return false

View file

@ -1,18 +1,23 @@
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { Tag } from "./Tag"
export default class ComparingTag implements TagsFilter {
private readonly _key: string
private readonly _predicate: (value: string) => boolean
private readonly _representation: string
private readonly _representation: "<" | ">" | "<=" | ">="
private readonly _boundary: string
constructor(
key: string,
predicate: (value: string | undefined) => boolean,
representation: string = ""
representation: "<" | ">" | "<=" | ">=",
boundary: string
) {
this._key = key
this._predicate = predicate
this._representation = representation
this._boundary = boundary
}
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>) {
return this._key + this._representation
return this._key + this._representation + this._boundary
}
asOverpass(): string[] {
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 {
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 {
@ -38,7 +92,7 @@ export default class ComparingTag implements TagsFilter {
/**
* 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: 41}) // => true
* t.matchesProperties({key: 0}) // => true
@ -56,6 +110,10 @@ export default class ComparingTag implements TagsFilter {
return []
}
asJson(): TagConfigJson {
return this._key + this._representation
}
optimize(): TagsFilter | boolean {
return this
}

View file

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

View file

@ -1,5 +1,6 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class RegexTag extends TagsFilter {
public readonly key: RegExp | string
@ -11,6 +12,9 @@ export class RegexTag extends TagsFilter {
super()
this.key = key
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.matchesEmpty = RegexTag.doesMatch("", this.value)
}
@ -41,11 +45,21 @@ export class RegexTag extends TagsFilter {
return possibleRegex.test(fromTag)
}
private static source(r: string | RegExp) {
private static source(r: string | RegExp, includeStartMarker: boolean = true) {
if (typeof r === "string") {
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 {
return false
}
@ -293,7 +325,7 @@ export class RegexTag extends TagsFilter {
if (typeof this.key === "string") {
return [this.key]
}
throw "Key cannot be determined as it is a regex"
return []
}
usedTags(): { key: string; value: string }[] {

View file

@ -1,6 +1,7 @@
import { TagsFilter } from "./TagsFilter"
import { Tag } from "./Tag"
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.
@ -45,6 +46,10 @@ export default class SubstitutingTag implements TagsFilter {
)
}
asJson(): TagConfigJson {
return this._key + (this._invert ? "!" : "") + ":=" + this._value
}
asOverpass(): string[] {
throw "A variable with substitution can not be used to query overpass"
}

View file

@ -1,5 +1,6 @@
import { Utils } from "../../Utils"
import { TagsFilter } from "./TagsFilter"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
export class Tag extends TagsFilter {
public key: string
@ -67,6 +68,10 @@ export class Tag extends TagsFilter {
return [`["${this.key}"="${this.value}"]`]
}
asJson(): TagConfigJson {
return this.key + "=" + this.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 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],
@ -324,6 +325,14 @@ export class TagUtils {
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.
*
@ -735,11 +744,10 @@ export class TagUtils {
const tag = json as string
for (const [operator, comparator] of TagUtils.comparators) {
if (tag.indexOf(operator) >= 0) {
const split = Utils.SplitFirst(tag, operator)
let val = Number(split[1].trim())
const split = Utils.SplitFirst(tag, operator).map((v) => v.trim())
let val = Number(split[1])
if (isNaN(val)) {
val = new Date(split[1].trim()).getTime()
val = new Date(split[1]).getTime()
}
const f = (value: string | number | undefined) => {
@ -762,7 +770,7 @@ export class TagUtils {
}
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]
}
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 {
const rta = a instanceof RegexTag
const rtb = b instanceof RegexTag

View file

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

View file

@ -31,7 +31,7 @@ export class Stores {
* @param promise
* @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)
promise?.then((d) => src.setData(d))
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, 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) => {
if (t === undefined) {
return undefined
@ -105,7 +108,7 @@ export abstract class Store<T> implements Readable<T> {
if (t === null) {
return null
}
return f(t)
return f(<Exclude<T, undefined | null>>t)
}, extraStoresToWatch)
}
@ -201,24 +204,36 @@ export abstract class Store<T> implements Readable<T> {
mapped.addCallbackAndRun((newEventSource) => {
if (newEventSource === null) {
sink.setData(null)
} else if (newEventSource === undefined) {
return
}
if (newEventSource === 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)
newEventSource.addCallbackAndRun((resultData) => {
if (mapped.data === newEventSource) {
sink.setData(resultData)
}
})
} else {
// Already seen, so we don't have to add a callback, just update the value
sink.setData(newEventSource.data)
}
})
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> {
if (Utils.runningFromConsole) {
return this
@ -603,7 +618,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/
public static FromPromiseWithErr<T>(
promise: Promise<T>
): UIEventSource<{ success: T } | { error: any }> {
): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise?.then((d) => src.setData({ success: d }))
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'
* 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(
this,
(t) => {
if (t === undefined) {
return undefined
}
return f(t)
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>>t)
},
extraSources,
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)
this.keypair = keypairEventSource
mangroveIdentity.addCallbackAndRunD(async (data) => {
if (data === "") {
if (!data) {
return
}
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))

View file

@ -40,15 +40,26 @@ export interface P4CPicture {
export default class NearbyImagesSearch {
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
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[]>([])
public readonly store: Store<P4CPicture[]> = this._store
public readonly allDone: Store<boolean>
private readonly _options: NearbyImageOptions
constructor(options: NearbyImageOptions, features: IndexedFeatureSource) {
this.individualStores = NearbyImagesSearch.services.map((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
if (features !== undefined) {
const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({
@ -93,13 +104,17 @@ export default class NearbyImagesSearch {
private static buildPictureFetcher(
options: NearbyImageOptions,
fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number }> {
const p4cStore = Stores.FromPromise<P4CPicture[]>(
): Store<{ images: P4CPicture[]; beforeFilter: number } | null | undefined> {
const p4cStore = Stores.FromPromiseWithErr<P4CPicture[]>(
NearbyImagesSearch.fetchImages(options, fetcher)
)
const searchRadius = options.searchRadius ?? 100
return p4cStore.map(
(images) => {
return p4cStore.mapD(
(imagesState) => {
if(imagesState["error"]){
return null
}
let images = imagesState["success"]
if (images === undefined) {
return undefined
}

View file

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

View file

@ -1,10 +1,11 @@
import { Translation } from "../UI/i18n/Translation"
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
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 {
public readonly canonical: string
public readonly _canonicalSingular: string
@ -53,8 +54,8 @@ export class Denomination {
/**
* Create a representation of the given value
* @param value: the value from OSM
* @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed
* @param value the value from OSM
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
*
* const unit = new Denomination({
* canonicalDenomination: "m",
@ -82,6 +83,8 @@ export class Denomination {
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42"
*
*
*/
public canonicalValue(value: string, actAsDefault: boolean): string {
if (value === undefined) {

View file

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

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