From f9e68f5710493a8defeb7cbe28f3c35e6530443f Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Apr 2022 21:36:46 +0200 Subject: [PATCH 01/16] Ignore VScode settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f3b001674..5c20f9502 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Folder.DotSettings.user index_*.ts .~lock.* *.doctest.ts +.vscode \ No newline at end of file From b4052b8f6355a2c6c8f5cbdce803d81e606807d3 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Apr 2022 21:37:31 +0200 Subject: [PATCH 02/16] First version of transit/bus theme --- assets/layers/shelter/license_info.json | 12 + assets/layers/shelter/shelter.json | 88 ++++++ assets/layers/shelter/shelter.svg | 1 + assets/layers/shops/shops.json | 44 +-- .../layers/transit_routes/transit_routes.json | 141 ++++++++++ assets/layers/transit_stops/bus_stop.svg | 3 + assets/layers/transit_stops/license_info.json | 15 + .../layers/transit_stops/transit_stops.json | 260 ++++++++++++++++++ .../mapcomplete-changes.json | 8 +- assets/themes/transit/transit.json | 51 ++++ langs/layers/en.json | 183 ++++++++++++ langs/themes/en.json | 4 + 12 files changed, 788 insertions(+), 22 deletions(-) create mode 100644 assets/layers/shelter/license_info.json create mode 100644 assets/layers/shelter/shelter.json create mode 100644 assets/layers/shelter/shelter.svg create mode 100644 assets/layers/transit_routes/transit_routes.json create mode 100644 assets/layers/transit_stops/bus_stop.svg create mode 100644 assets/layers/transit_stops/license_info.json create mode 100644 assets/layers/transit_stops/transit_stops.json create mode 100644 assets/themes/transit/transit.json diff --git a/assets/layers/shelter/license_info.json b/assets/layers/shelter/license_info.json new file mode 100644 index 000000000..6dab5c0d0 --- /dev/null +++ b/assets/layers/shelter/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "shelter.svg", + "license": "MIT", + "authors": [ + "Diemen Design" + ], + "sources": [ + "https://icon-icons.com/icon/map-shelter/158301" + ] + } +] \ No newline at end of file diff --git a/assets/layers/shelter/shelter.json b/assets/layers/shelter/shelter.json new file mode 100644 index 000000000..803b6ef42 --- /dev/null +++ b/assets/layers/shelter/shelter.json @@ -0,0 +1,88 @@ +{ + "id": "shelter", + "name": { + "en": "Shelter" + }, + "description": { + "en": "Layer showing shelter structures" + }, + "source": { + "osmTags": { + "and": [ + "amenity=shelter" + ] + } + }, + "minzoom": 13, + "title": { + "render": { + "en": "Shelter" + } + }, + "mapRendering": [ + { + "location": [ + "point", + "centroid" + ], + "icon": "./assets/layers/shelter/shelter.svg" + } + ], + "tagRenderings": [ + { + "id": "shelter-type", + "mappings": [ + { + "if": "shelter_type=public_transport", + "then": { + "en": "This is a shelter at a public transport stop." + } + }, + { + "if": "shelter_type=picnic_shelter", + "then": { + "en": "This is a shelter protecting from rain at a picnic site." + } + }, + { + "if": "shelter_type=gazebo", + "then": { + "en": "This is a gazebo." + } + }, + { + "if": "shelter_type=weather_shelter", + "then": { + "en": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads." + } + }, + { + "if": "shelter_type=lean_to", + "then": { + "en": "This is a shed with 3 walls, primarily intended for camping." + } + }, + { + "if": "shelter_type=pavilion", + "then": { + "en": "This is a pavilion" + } + }, + { + "if": "shelter_type=basic_hut", + "then": "This is a basic hut, providing basic shelter and sleeping facilities." + } + ], + "question": { + "en": "What kind of shelter is this?" + }, + "render": { + "en": "Shelter type: {shelter_type}" + }, + "freeform": { + "key": "shelter_type", + "type": "string" + } + } + ] +} \ No newline at end of file diff --git a/assets/layers/shelter/shelter.svg b/assets/layers/shelter/shelter.svg new file mode 100644 index 000000000..266a9eb7c --- /dev/null +++ b/assets/layers/shelter/shelter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/layers/shops/shops.json b/assets/layers/shops/shops.json index 9f5a32dce..401fd0c01 100644 --- a/assets/layers/shops/shops.json +++ b/assets/layers/shops/shops.json @@ -412,29 +412,37 @@ "filter": [ { "id": "shop-type", - "options": [{ - "fields": [{ - "name": "search", - "type": "string" - }], - "osmTags": "shop~^.*{search}.*$", - "question": { - "en": "Only show shops selling {search}" + "options": [ + { + "fields": [ + { + "name": "search", + "type": "string" + } + ], + "osmTags": "shop~^.*{search}.*$", + "question": { + "en": "Only show shops selling {search}" + } } - } ] }, { "id": "shop-name", - "options": [{ - "fields": [{ - "name": "search", - "type": "string" - }], - "osmTags": "name~^.*{search}.*$", - "question": { - "en": "Only show shops with name {search}" + "options": [ + { + "fields": [ + { + "name": "search", + "type": "string" + } + ], + "osmTags": "name~^.*{search}.*$", + "question": { + "en": "Only show shops with name {search}" + } } - }]} + ] + } ] } \ No newline at end of file diff --git a/assets/layers/transit_routes/transit_routes.json b/assets/layers/transit_routes/transit_routes.json new file mode 100644 index 000000000..b29464746 --- /dev/null +++ b/assets/layers/transit_routes/transit_routes.json @@ -0,0 +1,141 @@ +{ + "id": "transit_routes", + "name": { + "en": "Bus lines" + }, + "description": { + "en": "Layer showing bus lines" + }, + "source": { + "osmTags": { + "and": [ + "type=route", + "route=bus" + ] + } + }, + "minzoom": 15, + "title": { + "render": { + "en": "Bus line" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "en": "{name}" + } + } + ] + }, + "mapRendering": [ + { + "color": { + "render": { + "en": "#ff0000" + }, + "mappings": [ + { + "if": "colour~*", + "then": "{colour}" + } + ] + } + } + ], + "tagRenderings": [ + { + "id": "name", + "freeform": { + "key": "name", + "type": "string", + "placeholder": "Bus XX: From => Via => To" + }, + "render": "{name}", + "question": { + "en": "What is the name for this bus line? (i.e. Bus XX: From => Via => To)" + } + }, + { + "id": "from", + "freeform": { + "key": "from", + "type": "string", + "placeholder": "City, Stop Name" + }, + "render": { + "en": "This bus line begins at {from}" + }, + "question": { + "en": "What is the starting point for this bus line?" + } + }, + { + "id": "via", + "freeform": { + "key": "via", + "type": "string", + "placeholder": "City, Stop Name" + }, + "render": { + "en": "This bus line goes via {via}" + }, + "question": { + "en": "What is the via point for this bus line?" + } + }, + { + "id": "to", + "freeform": { + "key": "to", + "type": "string", + "placeholder": "City, Stop Name" + }, + "render": { + "en": "This bus line ends at {to}" + }, + "question": { + "en": "What is the ending point for this bus line?" + } + }, + { + "id": "colour", + "freeform": { + "key": "colour", + "type": "color" + }, + "render": { + "en": "This bus line has the color {colour}" + }, + "question": { + "en": "What is the colour for this bus line?" + } + }, + { + "id": "network", + "freeform": { + "key": "network", + "type": "string" + }, + "render": { + "en": "This bus line is part of the {network} network" + }, + "question": { + "en": "What network does this bus line belong to?" + } + }, + { + "id": "operator", + "freeform": { + "key": "operator", + "type": "string" + }, + "render": { + "en": "This bus line is operated by {operator}" + }, + "question": { + "en": "What company operates this bus line?" + } + } + ] +} \ No newline at end of file diff --git a/assets/layers/transit_stops/bus_stop.svg b/assets/layers/transit_stops/bus_stop.svg new file mode 100644 index 000000000..1b061c00b --- /dev/null +++ b/assets/layers/transit_stops/bus_stop.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/layers/transit_stops/license_info.json b/assets/layers/transit_stops/license_info.json new file mode 100644 index 000000000..47a736deb --- /dev/null +++ b/assets/layers/transit_stops/license_info.json @@ -0,0 +1,15 @@ +[ + { + "path": "bus_stop.svg", + "license": "CC0", + "authors": [ + "Andy Allan", + "Michael Glanznig", + "Paul Norman", + "Paul Dicker" + ], + "sources": [ + "https://github.com/gravitystorm/openstreetmap-carto/blob/master/symbols/highway/bus_stop.svg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/transit_stops/transit_stops.json b/assets/layers/transit_stops/transit_stops.json new file mode 100644 index 000000000..43daa2ada --- /dev/null +++ b/assets/layers/transit_stops/transit_stops.json @@ -0,0 +1,260 @@ +{ + "id": "transit_stops", + "name": { + "en": "Transit Stops" + }, + "description": { + "en": "Layer showing different types of transit stops." + }, + "source": { + "osmTags": { + "or": [ + "highway=bus_stop" + ] + } + }, + "minzoom": 15, + "title": { + "render": { + "en": "Transit Stop" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "en": "Stop {name}" + } + } + ] + }, + "mapRendering": [ + { + "location": [ + "point", + "centroid" + ], + "icon": { + "render": "./assets/layers/transit_stops/bus_stop.svg", + "mappings": [] + }, + "label": "
{name}
" + } + ], + "calculatedTags": [ + "_routes=feat.memberships()", + "_contained_routes_properties=feat.memberships().map(p => {return {id: p.relation.id, name: p.relation.properties.name} }).filter((v,i,a)=>a.findIndex(t=>(JSON.stringify(t) === JSON.stringify(v)))===i)", + "_contained_route_ids=JSON.parse(feat.properties._contained_routes_properties ?? '[]').map(p => p.id)", + "_contained_routes=JSON.parse(feat.properties._contained_routes_properties ?? '[]').map(p => `
  • ${p.name ?? 'bus route'}
  • `).join('')", + "_contained_routes_count=JSON.parse(feat.properties._contained_routes_properties ?? '[]').length" + ], + "tagRenderings": [ + { + "id": "stop_name", + "render": { + "en": "This stop is called {name}" + }, + "freeform": { + "key": "name", + "type": "string", + "addExtraTags": [ + "noname=" + ], + "placeholder": { + "en": "Name of the stop" + } + }, + "mappings": [ + { + "if": { + "and": [ + "noname=yes", + "name=" + ] + }, + "then": { + "en": "This stop has no name" + } + } + ], + "question": { + "en": "What is the name of this stop?" + }, + "placeholder": "Name of the stop" + }, + "images", + { + "id": "shelter", + "mappings": [ + { + "if": "shelter=yes", + "then": { + "en": "This stop has a shelter" + } + }, + { + "if": "shelter=no", + "then": { + "en": "This stop does not have a shelter" + } + }, + { + "if": "shelter=separate", + "then": { + "en": "This stop has a shelter, that's separately mapped" + }, + "hideInAnswer": true + } + ], + "question": { + "en": "Does this stop have a shelter?" + } + }, + { + "id": "bench", + "mappings": [ + { + "if": "bench=yes", + "then": { + "en": "This stop has a bench" + } + }, + { + "if": "bench=no", + "then": { + "en": "This stop does not have a bench" + } + }, + { + "if": "bench=separate", + "then": { + "en": "This stop has a bench, that's separately mapped" + }, + "hideInAnswer": true + } + ], + "question": { + "en": "Does this stop have a bench?" + } + }, + { + "id": "bin", + "mappings": [ + { + "if": "bin=yes", + "then": { + "en": "This stop has a bin" + } + }, + { + "if": "bin=no", + "then": { + "en": "This stop does not have a bin" + } + }, + { + "if": "bin=separate", + "then": { + "en": "This stop has a bin, that's separately mapped" + }, + "hideInAnswer": true + } + ], + "question": { + "en": "Does this stop have a bin?" + } + }, + "wheelchair-access", + { + "id": "tactile_paving", + "mappings": [ + { + "if": "tactile_paving=yes", + "then": { + "en": "This stop has tactile paving" + } + }, + { + "if": "tactile_paving=no", + "then": { + "en": "This stop does not have tactile paving" + } + } + ], + "question": { + "en": "Does this stop have tactile paving?" + } + }, + { + "id": "lit", + "mappings": [ + { + "if": "lit=yes", + "then": { + "en": "This stop is lit" + } + }, + { + "if": "lit=no", + "then": { + "en": "This stop is not lit" + } + } + ], + "question": { + "en": "Is this stop lit?" + } + }, + { + "id": "departures_board", + "mappings": [ + { + "if": "departures_board=yes", + "then": { + "en": "This stop has a departures board of unknown type" + }, + "hideInAnswer": true + }, + { + "if": "departures_board=realtime", + "then": { + "en": "This stop has a board showing realtime departure information" + } + }, + { + "if": "passenger_information_display=yes", + "then": { + "en": "This stop has a board showing realtime departure information" + }, + "hideInAnswer": true + }, + { + "if": "departures_board=timetable", + "then": { + "en": "This stop has a timetable showing regular departures" + } + }, + { + "if": "departures_board=interval", + "then": { + "en": "This stop has a timetable containing just the interval between departures" + } + }, + { + "if": "departures_board=no", + "then": { + "en": "This stop does not have a departures board" + } + } + ] + }, + { + "render": { + "en": "

    {_contained_routes_count} routes stop at this stop

    " + }, + "condition": "_contained_routes~*", + "id": "contained_routes" + } + ], + "filter": [], + "allowMove": false +} \ No newline at end of file diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index b03b39e7a..bb65497b7 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -231,10 +231,6 @@ "if": "theme=openwindpowermap", "then": "./assets/themes/openwindpowermap/logo.svg" }, - { - "if": "theme=parking-lanes", - "then": "./assets/svg/bug.svg" - }, { "if": "theme=parkings", "then": "./assets/themes/parkings/parkings.svg" @@ -295,6 +291,10 @@ "if": "theme=toilets", "then": "./assets/themes/toilets/toilets.svg" }, + { + "if": "theme=transit", + "then": "./assets/layers/transit_stops/bus_stop.svg" + }, { "if": "theme=trees", "then": "./assets/themes/trees/logo.svg" diff --git a/assets/themes/transit/transit.json b/assets/themes/transit/transit.json new file mode 100644 index 000000000..271510c2f --- /dev/null +++ b/assets/themes/transit/transit.json @@ -0,0 +1,51 @@ +{ + "id": "transit", + "maintainer": "Robin van der Linde", + "version": "20220406", + "title": { + "en": "Bus routes" + }, + "description": { + "en": "Plan your trip with the help of the public transport system." + }, + "icon": "./assets/layers/transit_stops/bus_stop.svg", + "startZoom": 20, + "startLat": 53.21333, + "startLon": 6.56963, + "layers": [ + "transit_stops", + "transit_routes", + { + "builtin": "bike_parking", + "override": { + "minzoom": 19, + "minzoomVisible": 19 + } + }, + { + "builtin": "parking", + "override": { + "minzoom": 19, + "minzoomVisible": 19 + } + }, + { + "builtin": "shelter", + "override": { + "minzoom": 19, + "minzoomVisible": 19, + "source": { + "osmTags": { + "and": [ + "amenity=shelter", + "shelter_type=public_transport" + ] + } + } + }, + "hideTagRenderingsWithLabels": [ + "shelter-type" + ] + } + ] +} \ No newline at end of file diff --git a/langs/layers/en.json b/langs/layers/en.json index ddeee9e6f..dd0c9fc87 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -4400,6 +4400,39 @@ "render": "Recycling facility" } }, + "shelter": { + "description": "Layer showing shelter structures", + "name": "Shelter", + "tagRenderings": { + "shelter-type": { + "mappings": { + "0": { + "then": "This is a shelter at a public transport stop." + }, + "1": { + "then": "This is a shelter protecting from rain at a picnic site." + }, + "2": { + "then": "This is a gazebo." + }, + "3": { + "then": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads." + }, + "4": { + "then": "This is a shed with 3 walls, primarily intended for camping." + }, + "5": { + "then": "This is a pavilion" + } + }, + "question": "What kind of shelter is this?", + "render": "Shelter type: {shelter_type}" + } + }, + "title": { + "render": "Shelter" + } + }, "shops": { "deletion": { "extraDeleteReasons": { @@ -4409,6 +4442,22 @@ } }, "description": "A shop", + "filter": { + "0": { + "options": { + "0": { + "question": "Only show shops selling {search}" + } + } + }, + "1": { + "options": { + "0": { + "question": "Only show shops with name {search}" + } + } + } + }, "name": "Shop", "presets": { "0": { @@ -5082,6 +5131,140 @@ "render": "Trail" } }, + "transit_routes": { + "description": "Layer showing bus lines", + "mapRendering": { + "0": { + "color": { + "render": "#ff0000" + } + } + }, + "name": "Bus lines", + "title": { + "mappings": { + "0": { + "then": "{name}" + } + }, + "render": "Bus line" + } + }, + "transit_stops": { + "description": "Layer showing different types of transit stops.", + "name": "Transit Stops", + "tagRenderings": { + "bench": { + "mappings": { + "0": { + "then": "This stop has a bench" + }, + "1": { + "then": "This stop does not have a bench" + }, + "2": { + "then": "This stop has a bench, that's separately mapped" + } + }, + "question": "Does this stop have a bench?" + }, + "bin": { + "mappings": { + "0": { + "then": "This stop has a bin" + }, + "1": { + "then": "This stop does not have a bin" + }, + "2": { + "then": "This stop has a bin, that's separately mapped" + } + }, + "question": "Does this stop have a bin?" + }, + "contained_routes": { + "render": "

    {_contained_routes_count} routes stop at this stop

    " + }, + "departures_board": { + "mappings": { + "0": { + "then": "This stop has a departures board of unknown type" + }, + "1": { + "then": "This stop has a board showing realtime departure information" + }, + "2": { + "then": "This stop has a board showing realtime departure information" + }, + "3": { + "then": "This stop has a timetable showing regular departures" + }, + "4": { + "then": "This stop has a timetable containing just the interval between departures" + }, + "5": { + "then": "This stop does not have a departures board" + } + } + }, + "lit": { + "mappings": { + "0": { + "then": "This stop is lit" + }, + "1": { + "then": "This stop is not lit" + } + }, + "question": "Is this stop lit?" + }, + "shelter": { + "mappings": { + "0": { + "then": "This stop has a shelter" + }, + "1": { + "then": "This stop does not have a shelter" + }, + "2": { + "then": "This stop has a shelter, that's separately mapped" + } + }, + "question": "Does this stop have a shelter?" + }, + "stop_name": { + "freeform": { + "placeholder": "Name of the stop" + }, + "mappings": { + "0": { + "then": "This stop has no name" + } + }, + "question": "What is the name of this stop?", + "render": "This stop is called {name}" + }, + "tactile_paving": { + "mappings": { + "0": { + "then": "This stop has tactile paving" + }, + "1": { + "then": "This stop does not have tactile paving" + } + }, + "question": "Does this stop have tactile paving?" + } + }, + "title": { + "mappings": { + "0": { + "then": "Stop {name}" + } + }, + "render": "Transit Stop" + } + }, "tree_node": { "description": "A layer showing trees", "name": "Tree", diff --git a/langs/themes/en.json b/langs/themes/en.json index 80a08d43f..cd6a854a6 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -1312,6 +1312,10 @@ "description": "A map of public toilets", "title": "Open Toilet Map" }, + "transit": { + "description": "Transit", + "title": "Transit" + }, "trees": { "description": "Map all the trees!", "shortDescription": "Map all the trees", From 606fd305fd77811fb0355522e376bc85b13bc07e Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Tue, 5 Jul 2022 15:02:08 +0000 Subject: [PATCH 03/16] Basic gitpod + vscode settings --- .gitignore | 14 +++++++++++++- .gitpod.yml | 13 +++++++++++++ .vscode/extensions.json | 6 ++++++ .vscode/settings.json | 19 +++++++++++++++++++ .vscode/tasks.json | 14 ++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .gitpod.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 4b310f095..ed8cb6f15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ dist/* node_modules .cache/* .idea/* -.vscode/* scratch assets/editor-layer-index.json assets/generated/* @@ -24,3 +23,16 @@ index_*.ts .~lock.* *.doctest.ts service-worker.js + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000..2c9725527 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,13 @@ +tasks: + - init: npm run init + command: npm run start + +ports: + - name: MapComplete Website + port: 1234 + onOpen: open-browser + +vscode: + extensions: + - "esbenp.prettier-vscode" + - "eamodio.gitlens" \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..ba4f9b1b8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "eamodio.gitlens" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0375d7080 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "json.schemas": [ + { + "fileMatch": [ + "/assets/layers/*/*.json", + "!/assets/layers/*/license_info.json" + ], + "url": "./Docs/Schemas/LayerConfigJson.schema.json" + }, + { + "fileMatch": [ + "/assets/themes/*/*.json", + "!/assets/themes/*/license_info.json" + ], + "url": "./Docs/Schemas/LayoutConfigJson.schema.json" + } + ], + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..2dfa42cd3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "path": "/", + "group": "build", + "problemMatcher": [], + "label": "MapComplete Dev", + "detail": "Run MapComplete Dev Server" + } + ] +} From 7270365cf54490507257a40efcda67eb531dfd2d Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Jul 2022 09:09:05 +0000 Subject: [PATCH 04/16] Add Github extension --- .gitpod.yml | 3 ++- .vscode/extensions.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index 2c9725527..d53368529 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -10,4 +10,5 @@ ports: vscode: extensions: - "esbenp.prettier-vscode" - - "eamodio.gitlens" \ No newline at end of file + - "eamodio.gitlens", + - "GitHub.vscode-pull-request-github" \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ba4f9b1b8..68d524f21 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "esbenp.prettier-vscode", - "eamodio.gitlens" + "eamodio.gitlens", + "GitHub.vscode-pull-request-github" ] } \ No newline at end of file From e34c935046b3be2c8bffc79a1350fae827f04fdd Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Jul 2022 09:39:28 +0000 Subject: [PATCH 05/16] Auto save on focus change --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0375d7080..ff6d1b68d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,6 @@ "url": "./Docs/Schemas/LayoutConfigJson.schema.json" } ], - "editor.tabSize": 2 + "editor.tabSize": 2, + "files.autoSave": "onFocusChange" } \ No newline at end of file From c8179a3a6264133d476f56e32c52103c9d2df393 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Jul 2022 09:46:52 +0000 Subject: [PATCH 06/16] Small fixes --- assets/layers/transit_stops/bus_stop.svg | 56 +++- langs/layers/en.json | 108 ++++--- langs/layers/it.json | 364 +++++++++++++++++++++++ langs/layers/nl.json | 8 +- langs/themes/en.json | 4 +- langs/themes/it.json | 4 + 6 files changed, 501 insertions(+), 43 deletions(-) diff --git a/assets/layers/transit_stops/bus_stop.svg b/assets/layers/transit_stops/bus_stop.svg index 1b061c00b..8d9c4d13b 100644 --- a/assets/layers/transit_stops/bus_stop.svg +++ b/assets/layers/transit_stops/bus_stop.svg @@ -1,3 +1,55 @@ - - + + + + + + image/svg+xml + + + + + + + + diff --git a/langs/layers/en.json b/langs/layers/en.json index 219cfe197..e077d30cd 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -3568,6 +3568,11 @@ "0": { "explanation": "{title()} has closed down permanently" } + }, + "nonDeleteMappings": { + "0": { + "then": "This is actually a pub" + } } }, "description": "A layer showing restaurants and fast-food amenities (with a special rendering for friteries)", @@ -5111,39 +5116,6 @@ "render": "Recycling facility" } }, - "shelter": { - "description": "Layer showing shelter structures", - "name": "Shelter", - "tagRenderings": { - "shelter-type": { - "mappings": { - "0": { - "then": "This is a shelter at a public transport stop." - }, - "1": { - "then": "This is a shelter protecting from rain at a picnic site." - }, - "2": { - "then": "This is a gazebo." - }, - "3": { - "then": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads." - }, - "4": { - "then": "This is a shed with 3 walls, primarily intended for camping." - }, - "5": { - "then": "This is a pavilion" - } - }, - "question": "What kind of shelter is this?", - "render": "Shelter type: {shelter_type}" - } - }, - "title": { - "render": "Shelter" - } - }, "school": { "name": "Primary and secondary schools", "presets": { @@ -5265,6 +5237,39 @@ "render": "School {name}" } }, + "shelter": { + "description": "Layer showing shelter structures", + "name": "Shelter", + "tagRenderings": { + "shelter-type": { + "mappings": { + "0": { + "then": "This is a shelter at a public transport stop." + }, + "1": { + "then": "This is a shelter protecting from rain at a picnic site." + }, + "2": { + "then": "This is a gazebo." + }, + "3": { + "then": "This is a small shelter, primarily intended for short breaks. Usually found in the mountains or alongside roads." + }, + "4": { + "then": "This is a shed with 3 walls, primarily intended for camping." + }, + "5": { + "then": "This is a pavilion" + } + }, + "question": "What kind of shelter is this?", + "render": "Shelter type: {shelter_type}" + } + }, + "title": { + "render": "Shelter" + } + }, "shops": { "deletion": { "extraDeleteReasons": { @@ -5312,11 +5317,13 @@ } }, "tagRenderings": { + "2": { + "override": { + "question": "What kind of shop is this?" + } + }, "shops-name": { "question": "What is the name of this shop?" - }, - "shops-type-from-id": { - "question": "What kind of shop is this?" } }, "title": { @@ -6005,6 +6012,35 @@ } }, "name": "Bus lines", + "tagRenderings": { + "colour": { + "question": "What is the colour for this bus line?", + "render": "This bus line has the color {colour}" + }, + "from": { + "question": "What is the starting point for this bus line?", + "render": "This bus line begins at {from}" + }, + "name": { + "question": "What is the name for this bus line? (i.e. Bus XX: From => Via => To)" + }, + "network": { + "question": "What network does this bus line belong to?", + "render": "This bus line is part of the {network} network" + }, + "operator": { + "question": "What company operates this bus line?", + "render": "This bus line is operated by {operator}" + }, + "to": { + "question": "What is the ending point for this bus line?", + "render": "This bus line ends at {to}" + }, + "via": { + "question": "What is the via point for this bus line?", + "render": "This bus line goes via {via}" + } + }, "title": { "mappings": { "0": { diff --git a/langs/layers/it.json b/langs/layers/it.json index a76a3b1d8..09efa26b6 100644 --- a/langs/layers/it.json +++ b/langs/layers/it.json @@ -1756,6 +1756,218 @@ "render": "Microbiblioteca" } }, + "recycling": { + "description": "Un livello con i contenitori e centri per la raccolta rifiuti riciclabili", + "filter": { + "0": { + "options": { + "0": { + "question": "Aperto ora" + } + } + }, + "1": { + "options": { + "0": { + "question": "Tutti i tipi di rifiuti" + }, + "1": { + "question": "Riciclo di batterie" + }, + "2": { + "question": "Riciclo di confezioni per bevande" + }, + "3": { + "question": "Riciclo di lattine" + }, + "4": { + "question": "Riciclo di abiti" + }, + "5": { + "question": "Riciclo di olio da cucina" + }, + "6": { + "question": "Riciclo di olio da motore" + }, + "7": { + "question": "Riciclo di umido" + }, + "8": { + "question": "Riciclo di bottiglie di vetro" + }, + "9": { + "question": "Riciclo di vetro" + }, + "10": { + "question": "Riciclo di giornali" + }, + "11": { + "question": "Riciclo di carta" + }, + "12": { + "question": "Riciclo di bottiglie di plastica" + }, + "13": { + "question": "Riciclo di confezioni di plastica" + }, + "14": { + "question": "Riciclo di plastica" + }, + "15": { + "question": "Riciclo di rottami metallici" + }, + "16": { + "question": "Riciclo di piccoli elettrodomestici" + }, + "17": { + "question": "Riciclo di secco" + } + } + } + }, + "name": "Riciclo", + "presets": { + "0": { + "title": "un contenitore per il riciclo" + }, + "1": { + "title": "un centro di riciclo" + } + }, + "tagRenderings": { + "container-location": { + "mappings": { + "0": { + "then": "E' un contenitore sotterraneo" + }, + "1": { + "then": "Questo contenitore è al chiuso" + }, + "2": { + "then": "Questo contenitore è all'aperto" + } + }, + "question": "Dove si trova questo contenitore?" + }, + "opening_hours": { + "mappings": { + "0": { + "then": "24/7" + } + }, + "question": "Quali sono gli orari di apertura di questo impianto di raccolta e riciclo?" + }, + "operator": { + "question": "Quale azienda gestisce questo impianto di raccolta e riciclo?", + "render": "Questa struttura di raccola e riciclo è gestita da {operator}" + }, + "recycling-accepts": { + "mappings": { + "0": { + "then": "Batterie" + }, + "1": { + "then": "Cartoni per bevande" + }, + "2": { + "then": "Lattine" + }, + "3": { + "then": "Abiti" + }, + "4": { + "then": "Olio da cucina" + }, + "5": { + "then": "Olio di motore" + }, + "6": { + "then": "Verde" + }, + "7": { + "then": "Umido" + }, + "8": { + "then": "Bottiglie di vetro" + }, + "9": { + "then": "Vetro" + }, + "10": { + "then": "Giornali" + }, + "11": { + "then": "Carta" + }, + "12": { + "then": "Bottiglie di platica" + }, + "13": { + "then": "Confezioni di plastica" + }, + "14": { + "then": "Plastica" + }, + "15": { + "then": "Rottami metallici" + }, + "16": { + "then": "Scarpe" + }, + "17": { + "then": "Piccoli elettrodomestici" + }, + "18": { + "then": "Piccoli elettrodomestici" + }, + "19": { + "then": "Aghi e oggetti appuntiti" + }, + "20": { + "then": "Secco" + } + }, + "question": "Cosa si può riciclare qui?" + }, + "recycling-centre-name": { + "mappings": { + "0": { + "then": "Questo centro raccolta e riciclo rifiuti non ha un nome specifico" + } + }, + "question": "Come si chiama questo centro raccolta e riciclo rifiuti?", + "render": "Questo centro raccolta e riciclo rifiuti si chiama {name}" + }, + "recycling-type": { + "mappings": { + "0": { + "then": "Questo è un contenitore per il riciclo di rifiuti" + }, + "1": { + "then": "Questo è un centro per la raccola e riciclo di rifiuti" + }, + "2": { + "then": "Contenitore per lo smaltimento del secco" + } + }, + "question": "Che tipo di raccolta è questo?" + } + }, + "title": { + "mappings": { + "0": { + "then": "Centro di riciclo rifiuti" + }, + "1": { + "then": "Centro di riciclo rifiuti" + }, + "2": { + "then": "Contenitore per il riciclo" + } + }, + "render": "Impianti di riciclo" + } + }, "slow_roads": { "tagRenderings": { "slow_roads-surface": { @@ -2259,6 +2471,158 @@ "render": "Punto panoramico" } }, + "waste_basket": { + "description": "Questo è un cestino dei rifiuti pubblico, un bidone della spazzatura, dove puoi buttare via la tua spazzatura", + "filter": { + "0": { + "options": { + "0": { + "question": "Tutti i tipi" + }, + "1": { + "question": "Cestino per sigarette" + }, + "2": { + "question": "Cestino per medicinali" + }, + "3": { + "question": "Cestino per escrementi dei cani" + }, + "4": { + "question": "Cestino per la spazzatura" + }, + "5": { + "question": "Cestino dei rifiuti per oggetti taglienti" + }, + "6": { + "question": "Cestino per la plastica" + } + } + }, + "1": { + "options": { + "0": { + "question": "Cestino per rifiuti con dispenser per sacchetti per escrementi dei cani" + } + } + } + }, + "mapRendering": { + "0": { + "iconSize": { + "mappings": { + "0": { + "then": "Cestino dei rifiuti" + } + } + } + } + }, + "name": "Cestino dei rifiuti", + "presets": { + "0": { + "title": "un cestino dei rifiuti" + } + }, + "tagRenderings": { + "dispensing_dog_bags": { + "mappings": { + "0": { + "then": "Questo cestino ha un distributore di sacchetti per escrementi dei cani" + }, + "1": { + "then": "Questo cestino non ha un distributore di sacchetti per escrementi dei cani" + }, + "2": { + "then": "Questo cestino non ha un distributore di sacchetti per escrementi dei cani" + } + }, + "question": "Questo cestino ha un distributore di sacchetti per escrementi dei cani?" + }, + "waste-basket-waste-types": { + "mappings": { + "0": { + "then": "Un cestino rifiuti per uso generico" + }, + "1": { + "then": "Un cestino rifiuti per uso generico" + }, + "2": { + "then": "Un cestino rifiuti per escrementi di cani" + }, + "3": { + "then": "Un cestino rifiuti per sigarette" + }, + "4": { + "then": "Un cestino rifiuti per medicinali" + }, + "5": { + "then": "Un cestino rifiuti per aghi e altri oggetti appuntiti" + }, + "6": { + "then": "Un cestino rifiuti per la plastica" + } + }, + "question": "Che tipo di cestino dei rifiuti è questo?" + } + }, + "title": { + "render": "Cestino dei rifiuti" + } + }, + "waste_disposal": { + "description": "Cestino per lo smaltimento dei rifiuti, contenitore di dimensioni medio grandi per lo smaltimento dei rifiuti (domestici)", + "filter": { + "0": { + "options": { + "0": { + "question": "Solo accesso pubblico" + } + } + } + }, + "name": "Contenitori per la raccolta differenziata", + "presets": { + "0": { + "description": "Cestino di dimensioni medio-grandi per lo smaltimento dei rifiuti (domestici)", + "title": "un raccoglitore per lo smaltimento rifiuti" + } + }, + "tagRenderings": { + "access": { + "mappings": { + "0": { + "then": "Questo cestino può essere usato da chiunque" + }, + "1": { + "then": "Questo cestino è privato" + }, + "2": { + "then": "Questo cestino è solo per residenti" + } + }, + "question": "Chi può utilizzare questo cestino per lo smaltimento dei rifiuti?", + "render": "Accesso: {access}" + }, + "disposal-location": { + "mappings": { + "0": { + "then": "Questo è un contenitore sotterraneo" + }, + "1": { + "then": "Questo contenitore è al chiuso" + }, + "2": { + "then": "Questo contenitore è all'aperto" + } + }, + "question": "Dove si trova questo contenitore?" + } + }, + "title": { + "render": "Smaltimento rifiuti" + } + }, "windturbine": { "name": "pala eolica", "presets": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 96d2abc4f..3380b5390 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -5183,11 +5183,13 @@ } }, "tagRenderings": { + "2": { + "override": { + "question": "Wat voor soort winkel is dit?" + } + }, "shops-name": { "question": "Wat is de naam van deze winkel?" - }, - "shops-type-from-id": { - "question": "Wat voor soort winkel is dit?" } }, "title": { diff --git a/langs/themes/en.json b/langs/themes/en.json index 80872ac08..4fb6f8226 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -954,8 +954,8 @@ "title": "Open Toilet Map" }, "transit": { - "description": "Transit", - "title": "Transit" + "description": "Plan your trip with the help of the public transport system.", + "title": "Bus routes" }, "trees": { "description": "Map all the trees!", diff --git a/langs/themes/it.json b/langs/themes/it.json index f9518d0d4..fbed99743 100644 --- a/langs/themes/it.json +++ b/langs/themes/it.json @@ -577,6 +577,10 @@ "shortDescription": "Mappa tutti gli alberi", "title": "Alberi" }, + "waste": { + "description": "Mappa dei cestini per i rifiuti e i centri di raccolta e riciclo rifiuti.", + "title": "Rifiuti" + }, "waste_basket": { "description": "In questa cartina troverai i cestini dei rifiuti nei tuoi paraggi. Se manca un cestino, puoi inserirlo tu stesso", "shortDescription": "Una cartina dei cestini dei rifiuti", From f27b60f80d9dbd7dba63aac1fe90a29e3260c8a2 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 11:14:19 +0200 Subject: [PATCH 07/16] Better errors when an image is missing --- Models/ThemeConfig/Conversion/Conversion.ts | 8 +++ Models/ThemeConfig/Conversion/Validation.ts | 76 +++++++++++++++------ scripts/generateLayerOverview.ts | 4 +- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/Models/ThemeConfig/Conversion/Conversion.ts b/Models/ThemeConfig/Conversion/Conversion.ts index 5fe52ebb0..2b7880c1f 100644 --- a/Models/ThemeConfig/Conversion/Conversion.ts +++ b/Models/ThemeConfig/Conversion/Conversion.ts @@ -39,6 +39,14 @@ export abstract class Conversion { return DesugaringStep.strict(fixed) } + public convertJoin(json: TIn, context: string, errors: string[], warnings?: string[], information?: string[]): TOut { + const fixed = this.convert(json, context) + errors?.push(...(fixed.errors ?? [])) + warnings?.push(...(fixed.warnings ?? [])) + information?.push(...(fixed.information ?? [])) + return fixed.result + } + public andThenF(f: (tout:TOut) => X ): Conversion{ return new Pipe( this, diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 79e4785f3..47250c36b 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -45,6 +45,53 @@ class ValidateLanguageCompleteness extends DesugaringStep { } } +export class DoesImageExist extends DesugaringStep{ + + private readonly _knownImagePaths: Set; + public static doesPathExist : (path: string) => boolean = undefined; + + constructor(knownImagePaths: Set) { + super("Checks if an image exists", [], "DoesImageExist"); + this._knownImagePaths = knownImagePaths; + } + + convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } { + const errors = [] + const warnings = [] + const information = [] + if (image.indexOf("{") >= 0) { + information.push("Ignoring image with { in the path: " + image) + return {result: image} + } + + if (image === "assets/SocialImage.png") { + return {result: image} + } + if (image.match(/[a-z]*/)) { + + if (Svg.All[image + ".svg"] !== undefined) { + // This is a builtin img, e.g. 'checkmark' or 'crosshair' + return {result: image}; + } + } + + if (this._knownImagePaths !== undefined && !this._knownImagePaths.has(image)) { + if(DoesImageExist.doesPathExist === undefined){ + errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`) + }else if(!DoesImageExist.doesPathExist(image)){ + errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`) + }else{ + errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`) + } + } + return { + result: image, + errors, warnings, information + } + } + +} + class ValidateTheme extends DesugaringStep { /** * The paths where this layer is originally saved. Triggers some extra checks @@ -54,13 +101,14 @@ class ValidateTheme extends DesugaringStep { private readonly knownImagePaths: Set; private readonly _isBuiltin: boolean; private _sharedTagRenderings: Map; - + private readonly _validateImage : DesugaringStep; constructor(knownImagePaths: Set, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme"); this.knownImagePaths = knownImagePaths; this._path = path; this._isBuiltin = isBuiltin; this._sharedTagRenderings = sharedTagRenderings; + this._validateImage = new DoesImageExist(this.knownImagePaths); } convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } { @@ -89,26 +137,7 @@ class ValidateTheme extends DesugaringStep { errors.push("Found a remote image: " + remoteImage + " in theme " + json.id + ", please download it.") } for (const image of images) { - if (image.indexOf("{") >= 0) { - information.push("Ignoring image with { in the path: " + image) - continue - } - - if (image === "assets/SocialImage.png") { - continue - } - if (image.match(/[a-z]*/)) { - - if (Svg.All[image + ".svg"] !== undefined) { - // This is a builtin img, e.g. 'checkmark' or 'crosshair' - continue;// => - } - } - - if (this.knownImagePaths !== undefined && !this.knownImagePaths.has(image)) { - const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}` - errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}`) - } + this._validateImage.convertJoin(image, context === undefined ? "" : ` in a layer defined in the theme ${context}`, errors, warnings, information) } if (json.icon.endsWith(".svg")) { @@ -433,8 +462,11 @@ export class DetectMappingsWithImages extends DesugaringStep i); + const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}` - errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}`) + errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}.\n Did you mean one of ${closeNames.slice(0, 3).join(", ")}`) } } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 81a67a69a..9a8d762e8 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -5,6 +5,7 @@ import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import Constants from "../Models/Constants"; import { + DoesImageExist, PrevalidateTheme, ValidateLayer, ValidateTagRenderings, @@ -152,6 +153,8 @@ class LayerOverviewUtils { main(_: string[]) { + DoesImageExist.doesPathExist = (path) => existsSync(path) + const licensePaths = new Set() for (const i in licenses) { licensePaths.add(licenses[i].path) @@ -261,7 +264,6 @@ class LayerOverviewUtils { tagRenderings: this.getSharedTagRenderings(knownImagePaths), publicLayers } - const nonDefaultLanguages : {theme: string, language: string}[] = [] for (const themeInfo of themeFiles) { let themeFile = themeInfo.parsed const themePath = themeInfo.path From a1ea9afac9189eeeaa63145b8cb602037a80fef4 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 12:26:37 +0200 Subject: [PATCH 08/16] Improve error message --- Models/ThemeConfig/TagRenderingConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index e0a482f8e..619e28c2f 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -352,7 +352,7 @@ export default class TagRenderingConfig { } if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) { - throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'` + throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. If a contributor were to pick this as an option, MapComplete wouldn't be able to determine which tags to add.\n Either change it or set 'hideInAnswer'` } } From b64a873c40e01c7bf6e3eae0e874f5bd125e6243 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 12:57:23 +0200 Subject: [PATCH 09/16] Wire 'doesImageExist' everywhere, fixes #928 --- Models/ThemeConfig/Conversion/Validation.ts | 84 ++++++++++----------- scripts/generateLayerOverview.ts | 27 +++---- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 47250c36b..103b8efd3 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -45,14 +45,15 @@ class ValidateLanguageCompleteness extends DesugaringStep { } } -export class DoesImageExist extends DesugaringStep{ - +export class DoesImageExist extends DesugaringStep { + private readonly _knownImagePaths: Set; - public static doesPathExist : (path: string) => boolean = undefined; - - constructor(knownImagePaths: Set) { + private readonly doesPathExist: (path: string) => boolean = undefined; + + constructor(knownImagePaths: Set, checkExistsSync: (path: string) => boolean = undefined) { super("Checks if an image exists", [], "DoesImageExist"); this._knownImagePaths = knownImagePaths; + this.doesPathExist = checkExistsSync; } convert(image: string, context: string): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } { @@ -76,11 +77,11 @@ export class DoesImageExist extends DesugaringStep{ } if (this._knownImagePaths !== undefined && !this._knownImagePaths.has(image)) { - if(DoesImageExist.doesPathExist === undefined){ + if (this.doesPathExist === undefined) { errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`) - }else if(!DoesImageExist.doesPathExist(image)){ + } else if (!this.doesPathExist(image)) { errors.push(`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`) - }else{ + } else { errors.push(`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`) } } @@ -89,7 +90,7 @@ export class DoesImageExist extends DesugaringStep{ errors, warnings, information } } - + } class ValidateTheme extends DesugaringStep { @@ -98,17 +99,16 @@ class ValidateTheme extends DesugaringStep { * @private */ private readonly _path?: string; - private readonly knownImagePaths: Set; private readonly _isBuiltin: boolean; private _sharedTagRenderings: Map; - private readonly _validateImage : DesugaringStep; - constructor(knownImagePaths: Set, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { + private readonly _validateImage: DesugaringStep; + + constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme"); - this.knownImagePaths = knownImagePaths; + this._validateImage = doesImageExist; this._path = path; this._isBuiltin = isBuiltin; this._sharedTagRenderings = sharedTagRenderings; - this._validateImage = new DoesImageExist(this.knownImagePaths); } convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } { @@ -179,9 +179,7 @@ class ValidateTheme extends DesugaringStep { if (theme.id !== filename) { errors.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + this._path + ")") } - if (!this.knownImagePaths.has(theme.icon)) { - errors.push("The theme image " + theme.icon + " is not attributed or not saved locally") - } + this._validateImage.convertJoin(theme.icon, context + ".icon", errors, warnings, information); const dups = Utils.Dupiclates(json.layers.map(layer => layer["id"])) if (dups.length > 0) { errors.push(`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`) @@ -195,16 +193,16 @@ class ValidateTheme extends DesugaringStep { // The first key in the the title-field must be english, otherwise the title in the loading page will be the different language const targetLanguage = theme.title.SupportedLanguages()[0] - if(targetLanguage !== "en"){ + if (targetLanguage !== "en") { warnings.push(`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`) } - + // Official, public themes must have a full english translation const checked = new ValidateLanguageCompleteness("en") .convert(theme, theme.id) errors.push(...checked.errors) - - + + } } catch (e) { @@ -221,10 +219,10 @@ class ValidateTheme extends DesugaringStep { } export class ValidateThemeAndLayers extends Fuse { - constructor(knownImagePaths: Set, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { + constructor(doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, sharedTagRenderings: Map) { super("Validates a theme and the contained layers", - new ValidateTheme(knownImagePaths, path, isBuiltin, sharedTagRenderings), - new On("layers", new Each(new ValidateLayer(undefined, false, knownImagePaths))) + new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings), + new On("layers", new Each(new ValidateLayer(undefined, false, doesImageExist))) ); } } @@ -383,7 +381,7 @@ export class DetectShadowedMappings extends DesugaringStep { - private knownImagePaths: Set; - constructor(knownImagePaths: Set) { + private readonly _doesImageExist: DoesImageExist; + + constructor(doesImageExist: DoesImageExist) { super("Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", [], "DetectMappingsWithImages"); - this.knownImagePaths = knownImagePaths; + this._doesImageExist = doesImageExist; } /** @@ -441,9 +440,9 @@ export class DetectMappingsWithImages extends DesugaringStep msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true */ convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[], information?: string[] } { - const errors = [] - const warnings = [] - const information = [] + const errors: string[] = [] + const warnings: string[] = [] + const information: string[] = [] if (json.mappings === undefined || json.mappings.length === 0) { return {result: json} } @@ -461,15 +460,10 @@ export class DetectMappingsWithImages extends DesugaringStep i); - - const ctx = context === undefined ? "" : ` in a layer defined in the theme ${context}` - errors.push(`Image with path ${image} not found or not attributed; it is used in ${json.id}${ctx}.\n Did you mean one of ${closeNames.slice(0, 3).join(", ")}`) - } + this._doesImageExist.convertJoin(image, ctx, errors, warnings, information); + } - + } } else if (ignore) { warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`) @@ -486,10 +480,10 @@ export class DetectMappingsWithImages extends DesugaringStep { - constructor(layerConfig?: LayerConfigJson, knownImagePaths?: Set) { + constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) { super("Various validation on tagRenderingConfigs", new DetectShadowedMappings(layerConfig), - new DetectMappingsWithImages(knownImagePaths) + new DetectMappingsWithImages(doesImageExist) ); } } @@ -501,13 +495,13 @@ export class ValidateLayer extends DesugaringStep { */ private readonly _path?: string; private readonly _isBuiltin: boolean; - private knownImagePaths: Set | undefined; + private readonly _doesImageExist: DoesImageExist; - constructor(path: string, isBuiltin: boolean, knownImagePaths: Set) { + constructor(path: string, isBuiltin: boolean, doesImageExist: DoesImageExist) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer"); this._path = path; this._isBuiltin = isBuiltin; - this.knownImagePaths = knownImagePaths + this._doesImageExist = doesImageExist } convert(json: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings?: string[], information?: string[] } { @@ -595,7 +589,7 @@ export class ValidateLayer extends DesugaringStep { } } if (json.tagRenderings !== undefined) { - const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this.knownImagePaths))).convert(json, context) + const r = new On("tagRenderings", new Each(new ValidateTagRenderings(json, this._doesImageExist))).convert(json, context) warnings.push(...(r.warnings ?? [])) errors.push(...(r.errors ?? [])) information.push(...(r.information ?? [])) diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 9a8d762e8..866155db8 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -83,10 +83,10 @@ class LayerOverviewUtils { writeFileSync(`./assets/generated/layers/${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8"); } - getSharedTagRenderings(knownImagePaths: Set): Map { + getSharedTagRenderings(doesImageExist: DoesImageExist): Map { const dict = new Map(); - const validator = new ValidateTagRenderings(undefined, knownImagePaths); + const validator = new ValidateTagRenderings(undefined, doesImageExist); for (const key in questions["default"]) { if (key === "id") { continue @@ -153,15 +153,13 @@ class LayerOverviewUtils { main(_: string[]) { - DoesImageExist.doesPathExist = (path) => existsSync(path) - const licensePaths = new Set() for (const i in licenses) { licensePaths.add(licenses[i].path) } - - const sharedLayers = this.buildLayerIndex(licensePaths); - const sharedThemes = this.buildThemeIndex(licensePaths, sharedLayers) + const doesImageExist = new DoesImageExist(licensePaths, existsSync) + const sharedLayers = this.buildLayerIndex(doesImageExist); + const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers) writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ "layers": Array.from(sharedLayers.values()), @@ -191,12 +189,12 @@ class LayerOverviewUtils { console.log(green("All done!")) } - private buildLayerIndex(knownImagePaths: Set): Map { + private buildLayerIndex(doesImageExist: DoesImageExist): Map { // First, we expand and validate all builtin layers. These are written to assets/generated/layers // At the same time, an index of available layers is built. console.log(" ---------- VALIDATING BUILTIN LAYERS ---------") - const sharedTagRenderings = this.getSharedTagRenderings(knownImagePaths); + const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist); const layerFiles = ScriptUtils.getLayerFiles(); const sharedLayers = new Map() const state: DesugaringContext = { @@ -212,7 +210,7 @@ class LayerOverviewUtils { fixed.source.osmTags = {"and": [fixed.source.osmTags]} } - const validator = new ValidateLayer(sharedLayerJson.path, true, knownImagePaths); + const validator = new ValidateLayer(sharedLayerJson.path, true, doesImageExist); validator.convertStrict(fixed, context) if (sharedLayers.has(fixed.id)) { @@ -252,7 +250,7 @@ class LayerOverviewUtils { return publicLayerIds } - private buildThemeIndex(knownImagePaths: Set, sharedLayers: Map): Map { + private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map): Map { console.log(" ---------- VALIDATING BUILTIN THEMES ---------") const themeFiles = ScriptUtils.getThemeFiles(); const fixed = new Map(); @@ -261,7 +259,7 @@ class LayerOverviewUtils { const convertState: DesugaringContext = { sharedLayers, - tagRenderings: this.getSharedTagRenderings(knownImagePaths), + tagRenderings: this.getSharedTagRenderings(doesImageExist), publicLayers } for (const themeInfo of themeFiles) { @@ -273,10 +271,7 @@ class LayerOverviewUtils { themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath) - if (knownImagePaths === undefined) { - throw "Could not load known images/licenses" - } - new ValidateThemeAndLayers(knownImagePaths, themePath, true, convertState.tagRenderings) + new ValidateThemeAndLayers(doesImageExist, themePath, true, convertState.tagRenderings) .convertStrict(themeFile, themePath) this.writeTheme(themeFile) From 608db0a173c58b8f8c4bc6414fdadc5c3a5548ff Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 6 Jul 2022 13:33:24 +0200 Subject: [PATCH 10/16] Update Development_deployment.md --- Docs/Development_deployment.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Docs/Development_deployment.md b/Docs/Development_deployment.md index 09313fd7d..64b3c9563 100644 --- a/Docs/Development_deployment.md +++ b/Docs/Development_deployment.md @@ -73,6 +73,11 @@ To use the WSL in Visual Studio Code: To use WSL without Visual Studio Code you can replace steps 7 and 8 by opening up a WSL terminal +On mac +------ + +Install the `Command line tools for XCode which you can find [here](https://developer.apple.com/download/all/). You might need an apple dev account for this. + Automatic deployment -------------------- From eca14c5d08c13d63fca20c1c439aa02a13dfd728 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 14:00:39 +0200 Subject: [PATCH 11/16] Fix tests --- Models/ThemeConfig/Conversion/Validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 103b8efd3..36da25536 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -420,7 +420,7 @@ export class DetectMappingsWithImages extends DesugaringStep()).convert({ + * const r = new DetectMappingsWithImages(new DoesImageExist(new Set())).convert({ * "mappings": [ * { * "if": "bicycle_parking=stands", From 063d7e463755e3c1c136e0de951c26e9fb99cda1 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 14:11:07 +0200 Subject: [PATCH 12/16] Fix tests --- test/UI/Popup/TagRenderingQuestion.spec.ts | 2 ++ test/scripts/GenerateCache.spec.ts | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/UI/Popup/TagRenderingQuestion.spec.ts b/test/UI/Popup/TagRenderingQuestion.spec.ts index c07048b49..c4bdd453f 100644 --- a/test/UI/Popup/TagRenderingQuestion.spec.ts +++ b/test/UI/Popup/TagRenderingQuestion.spec.ts @@ -4,6 +4,7 @@ import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion"; import {UIEventSource} from "../../../Logic/UIEventSource"; import { expect } from 'chai'; +import Locale from "../../../UI/i18n/Locale"; describe("TagRenderingQuestion", () => { @@ -27,6 +28,7 @@ describe("TagRenderingQuestion", () => { it("should have a freeform text field with a type explanation", () => { + Locale.language.setData("en") const configJson = { id: "test-tag-rendering", question: "Question?", diff --git a/test/scripts/GenerateCache.spec.ts b/test/scripts/GenerateCache.spec.ts index ea8acb4e5..0031079b3 100644 --- a/test/scripts/GenerateCache.spec.ts +++ b/test/scripts/GenerateCache.spec.ts @@ -28,23 +28,27 @@ function initDownloads(query: string){ describe("GenerateCache", () => { it("should generate a cached file for the Natuurpunt-theme", async () => { - if (existsSync("/tmp/np-cache")) { - ScriptUtils.readDirRecSync("/tmp/np-cache").forEach(p => unlinkSync(p)) - rmdirSync("/tmp/np-cache") + // We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this + if(!existsSync("/var/tmp")){ + console.log("Not executing caching test: no temp directory found") } - mkdirSync("/tmp/np-cache") + if (existsSync("/var/tmp/np-cache")) { + ScriptUtils.readDirRecSync("/var/tmp/np-cache").forEach(p => unlinkSync(p)) + rmdirSync("/var/tmp/np-cache") + } + mkdirSync("/var/tmp/np-cache") initDownloads( "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" ); await main([ "natuurpunt", "12", - "/tmp/np-cache", + "/var/tmp/np-cache", "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", "--generate-point-overview", "nature_reserve,visitor_information_centre" ]) await ScriptUtils.sleep(500) - const birdhides = JSON.parse(readFileSync("/tmp/np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) + const birdhides = JSON.parse(readFileSync("/var/tmp/np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) expect(birdhides.features.length).deep.equal(5) expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true From 9e3f89b874888437ea985e2402b9afc62262fa81 Mon Sep 17 00:00:00 2001 From: Robin van der Linde Date: Wed, 6 Jul 2022 12:58:28 +0000 Subject: [PATCH 13/16] Use gitignore for ignoring files in search --- .vscode/settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ff6d1b68d..25b1f4dbe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ } ], "editor.tabSize": 2, - "files.autoSave": "onFocusChange" -} \ No newline at end of file + "files.autoSave": "onFocusChange", + "search.useIgnoreFiles": true + } \ No newline at end of file From 8c036e159f0fbf5247f925ecd884f7c43aa40a13 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 13:58:56 +0200 Subject: [PATCH 14/16] Generate layer overview now only recompiles files that need to be recompiled --- package.json | 2 +- scripts/ScriptUtils.ts | 176 ++++++++++++++++--------------- scripts/build.sh | 2 +- scripts/generateLayerOverview.ts | 161 +++++++++++++++++++--------- 4 files changed, 204 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 58c8dabfa..e1a66f1c3 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../MapComplete-data/speelplekken_cache/ 51.20 4.35 51.09 4.56", "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../MapComplete-data/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre", "generate:cache:natuurpunt:mini": "ts-node scripts/generateCache.ts natuurpunt 12 ../../git/MapComplete-data/natuurpunt_cache_mini/ 51.00792239979105 4.497699737548828 51.0353492224462554 4.539070129394531 --generate-point-overview nature_reserve,visitor_information_centre", - "generate:layeroverview": "ts-node scripts/generateLayerOverview.ts --no-fail", + "generate:layeroverview": "ts-node scripts/generateLayerOverview.ts", "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index 651ad4351..a86f3b2d4 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -45,13 +45,103 @@ export default class ScriptUtils { }) } - - private static async DownloadJSON(url: string, headers?: any): Promise{ + + public static erasableLog(...text) { + process.stdout.write("\r " + text.join(" ") + " \r") + } + + public static sleep(ms) { + if (ms <= 0) { + process.stdout.write("\r \r") + return; + } + return new Promise((resolve) => { + process.stdout.write("\r Sleeping for " + (ms / 1000) + "s \r") + setTimeout(resolve, 1000); + }).then(() => ScriptUtils.sleep(ms - 1000)); + } + + public static getLayerPaths(): string[] { + return ScriptUtils.readDirRecSync("./assets/layers") + .filter(path => path.indexOf(".json") > 0) + .filter(path => path.indexOf(".proto.json") < 0) + .filter(path => path.indexOf("license_info.json") < 0) + } + + public static getLayerFiles(): { parsed: LayerConfigJson, path: string }[] { + return ScriptUtils.readDirRecSync("./assets/layers") + .filter(path => path.indexOf(".json") > 0) + .filter(path => path.indexOf(".proto.json") < 0) + .filter(path => path.indexOf("license_info.json") < 0) + .map(path => { + try { + const contents = readFileSync(path, "UTF8") + if (contents === "") { + throw "The file " + path + " is empty, did you properly save?" + } + + const parsed = JSON.parse(contents); + return {parsed, path} + } catch (e) { + console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e) + throw e + } + }) + } + + public static getThemePaths(): string[] { + return ScriptUtils.readDirRecSync("./assets/themes") + .filter(path => path.endsWith(".json") && !path.endsWith(".proto.json")) + .filter(path => path.indexOf("license_info.json") < 0) + } + + public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] { + return this.getThemePaths() + .map(path => { + try { + const contents = readFileSync(path, "UTF8"); + if (contents === "") { + throw "The file " + path + " is empty, did you properly save?" + } + const parsed = JSON.parse(contents); + return {parsed: parsed, path: path} + } catch (e) { + console.error("Could not read file ", path, "due to ", e) + throw e + } + }); + } + + public static TagInfoHistogram(key: string): Promise<{ + data: { count: number, value: string, fraction: number }[] + }> { + const url = `https://taginfo.openstreetmap.org/api/4/key/values?key=${key}&filter=all&lang=en&sortname=count&sortorder=desc&page=1&rp=17&qtype=value` + return ScriptUtils.DownloadJSON(url) + } + + public static async ReadSvg(path: string): Promise { + if (!existsSync(path)) { + throw "File not found: " + path + } + const root = await xml2js.parseStringPromise(readFileSync(path, "UTF8")) + return root.svg + } + + public static async ReadSvgSync(path: string, callback: ((svg: any) => void)): Promise { + xml2js.parseString(readFileSync(path, "UTF8"), {async: false}, (err, root) => { + if (err) { + throw err + } + callback(root["svg"]); + }) + } + + private static async DownloadJSON(url: string, headers?: any): Promise { const data = await ScriptUtils.Download(url, headers); return JSON.parse(data.content) } - private static Download(url, headers?: any): Promise<{content: string}> { + private static Download(url, headers?: any): Promise<{ content: string }> { return new Promise((resolve, reject) => { try { headers = headers ?? {} @@ -83,84 +173,4 @@ export default class ScriptUtils { } - public static erasableLog(...text) { - process.stdout.write("\r " + text.join(" ") + " \r") - } - - public static sleep(ms) { - if (ms <= 0) { - process.stdout.write("\r \r") - return; - } - return new Promise((resolve) => { - process.stdout.write("\r Sleeping for " + (ms / 1000) + "s \r") - setTimeout(resolve, 1000); - }).then(() => ScriptUtils.sleep(ms - 1000)); - } - - public static getLayerFiles(): { parsed: LayerConfigJson, path: string }[] { - return ScriptUtils.readDirRecSync("./assets/layers") - .filter(path => path.indexOf(".json") > 0) - .filter(path => path.indexOf(".proto.json") < 0) - .filter(path => path.indexOf("license_info.json") < 0) - .map(path => { - try { - const contents = readFileSync(path, "UTF8") - if (contents === "") { - throw "The file " + path + " is empty, did you properly save?" - } - - const parsed = JSON.parse(contents); - return {parsed, path} - } catch (e) { - console.error("Could not parse file ", "./assets/layers/" + path, "due to ", e) - throw e - } - }) - } - - public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] { - return ScriptUtils.readDirRecSync("./assets/themes") - .filter(path => path.endsWith(".json") && !path.endsWith(".proto.json")) - .filter(path => path.indexOf("license_info.json") < 0) - .map(path => { - try { - const contents = readFileSync(path, "UTF8"); - if (contents === "") { - throw "The file " + path + " is empty, did you properly save?" - } - const parsed = JSON.parse(contents); - return {parsed: parsed, path: path} - } catch (e) { - console.error("Could not read file ", path, "due to ", e) - throw e - } - }); - } - - - public static TagInfoHistogram(key: string): Promise<{ - data: { count: number, value: string, fraction: number }[] - }> { - const url = `https://taginfo.openstreetmap.org/api/4/key/values?key=${key}&filter=all&lang=en&sortname=count&sortorder=desc&page=1&rp=17&qtype=value` - return ScriptUtils.DownloadJSON(url) - } - - public static async ReadSvg(path: string): Promise{ - if(!existsSync(path)){ - throw "File not found: "+path - } - const root = await xml2js.parseStringPromise(readFileSync(path, "UTF8")) - return root.svg - } - - public static async ReadSvgSync(path: string, callback: ((svg: any) => void)): Promise{ - xml2js.parseString(readFileSync(path, "UTF8"),{async: false} , (err, root) => { - if(err){ - throw err - } - callback(root["svg"]); - }) - } - } diff --git a/scripts/build.sh b/scripts/build.sh index 804ce9b52..28bd83dee 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -12,7 +12,7 @@ mkdir dist/assets 2> /dev/null # 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 generate:layeroverview && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise +ts-node ./scripts/generateLayeroverview --force && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise npm run test && npm run generate:layouts diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 866155db8..99e733a20 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -1,5 +1,5 @@ import ScriptUtils from "./ScriptUtils"; -import {existsSync, mkdirSync, readFileSync, writeFileSync} from "fs"; +import {existsSync, mkdirSync, readFileSync, statSync, writeFileSync} from "fs"; import * as licenses from "../assets/generated/license_info.json" import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; @@ -26,6 +26,51 @@ import {Utils} from "../Utils"; class LayerOverviewUtils { + public static readonly layerPath = "./assets/generated/layers/" + public static readonly themePath = "./assets/generated/themes/" + + private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set { + const publicThemes = [].concat(...themefiles + .filter(th => !th.hideFromOverview)) + + return new Set([].concat(...publicThemes.map(th => this.extractLayerIdsFrom(th)))) + } + + private static extractLayerIdsFrom(themeFile: LayoutConfigJson, includeInlineLayers = true): string[] { + const publicLayerIds = [] + for (const publicLayer of themeFile.layers) { + if (typeof publicLayer === "string") { + publicLayerIds.push(publicLayer) + continue + } + if (publicLayer["builtin"] !== undefined) { + const bi = publicLayer["builtin"] + if (typeof bi === "string") { + publicLayerIds.push(bi) + continue + } + bi.forEach(id => publicLayerIds.push(id)) + continue + } + if (includeInlineLayers) { + publicLayerIds.push(publicLayer["id"]) + } + } + return publicLayerIds + } + + shouldBeUpdated(sourcefile: string | string[], targetfile: string): boolean { + if (!existsSync(targetfile)) { + return true; + } + const targetModified = statSync(targetfile).mtime + if (typeof sourcefile === "string") { + sourcefile = [sourcefile] + } + + return sourcefile.some(sourcefile => statSync(sourcefile).mtime > targetModified) + } + writeSmallOverview(themes: { id: string, title: any, shortDescription: any, icon: string, hideFromOverview: boolean, mustHaveLanguage: boolean, layers: (LayerConfigJson | string | { builtin })[] }[]) { const perId = new Map(); for (const theme of themes) { @@ -70,22 +115,22 @@ class LayerOverviewUtils { } writeTheme(theme: LayoutConfigJson) { - if (!existsSync("./assets/generated/themes")) { - mkdirSync("./assets/generated/themes"); + if (!existsSync(LayerOverviewUtils.themePath)) { + mkdirSync(LayerOverviewUtils.themePath); } - writeFileSync(`./assets/generated/themes/${theme.id}.json`, JSON.stringify(theme, null, " "), "UTF8"); + writeFileSync(`${LayerOverviewUtils.themePath}${theme.id}.json`, JSON.stringify(theme, null, " "), "UTF8"); } writeLayer(layer: LayerConfigJson) { - if (!existsSync("./assets/generated/layers")) { - mkdirSync("./assets/generated/layers"); + if (!existsSync(LayerOverviewUtils.layerPath)) { + mkdirSync(LayerOverviewUtils.layerPath); } - writeFileSync(`./assets/generated/layers/${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8"); + writeFileSync(`${LayerOverviewUtils.layerPath}${layer.id}.json`, JSON.stringify(layer, null, " "), "UTF8"); } getSharedTagRenderings(doesImageExist: DoesImageExist): Map { const dict = new Map(); - + const validator = new ValidateTagRenderings(undefined, doesImageExist); for (const key in questions["default"]) { if (key === "id") { @@ -94,7 +139,7 @@ class LayerOverviewUtils { questions[key].id = key; questions[key]["source"] = "shared-questions" const config = questions[key] - validator.convertStrict(config, "generate-layer-overview:tagRenderings/questions.json:"+key) + validator.convertStrict(config, "generate-layer-overview:tagRenderings/questions.json:" + key) dict.set(key, config) } for (const key in icons["default"]) { @@ -105,9 +150,9 @@ class LayerOverviewUtils { continue } icons[key].id = key; - const config = icons[key] - validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:"+key) - dict.set(key,config) + const config = icons[key] + validator.convertStrict(config, "generate-layer-overview:tagRenderings/icons.json:" + key) + dict.set(key, config) } dict.forEach((value, key) => { @@ -150,16 +195,18 @@ class LayerOverviewUtils { } } - - main(_: string[]) { + main(args: string[]) { + + const forceReload = args.some(a => a == "--force") const licensePaths = new Set() for (const i in licenses) { licensePaths.add(licenses[i].path) } const doesImageExist = new DoesImageExist(licensePaths, existsSync) - const sharedLayers = this.buildLayerIndex(doesImageExist); - const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers) + const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload); + const recompiledThemes : string[] = [] + const sharedThemes = this.buildThemeIndex(doesImageExist, sharedLayers, recompiledThemes, forceReload) writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify({ "layers": Array.from(sharedLayers.values()), @@ -169,7 +216,7 @@ class LayerOverviewUtils { writeFileSync("./assets/generated/known_layers.json", JSON.stringify({layers: Array.from(sharedLayers.values())})) - { + if(recompiledThemes.length > 0) { // mapcomplete-changes shows an icon for each corresponding mapcomplete-theme const iconsPerTheme = Array.from(sharedThemes.values()).map(th => ({ @@ -189,28 +236,42 @@ class LayerOverviewUtils { console.log(green("All done!")) } - private buildLayerIndex(doesImageExist: DoesImageExist): Map { + private buildLayerIndex(doesImageExist: DoesImageExist, forceReload: boolean): Map { // First, we expand and validate all builtin layers. These are written to assets/generated/layers // At the same time, an index of available layers is built. console.log(" ---------- VALIDATING BUILTIN LAYERS ---------") const sharedTagRenderings = this.getSharedTagRenderings(doesImageExist); - const layerFiles = ScriptUtils.getLayerFiles(); const sharedLayers = new Map() const state: DesugaringContext = { tagRenderings: sharedTagRenderings, sharedLayers } const prepLayer = new PrepareLayer(state); - for (const sharedLayerJson of layerFiles) { - const context = "While building builtin layer " + sharedLayerJson.path - const fixed = prepLayer.convertStrict(sharedLayerJson.parsed, context) + const skippedLayers: string[] = [] + const recompiledLayers: string[] = [] + for (const sharedLayerPath of ScriptUtils.getLayerPaths()) { + + { + const targetPath = LayerOverviewUtils.layerPath + sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/")) + if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) { + const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8")) + sharedLayers.set(sharedLayer.id, sharedLayer) + skippedLayers.push(sharedLayer.id) + continue; + } - if(fixed.source.osmTags["and"] === undefined){ + } + + const parsed = JSON.parse(readFileSync(sharedLayerPath, "utf8")) + const context = "While building builtin layer " + sharedLayerPath + const fixed = prepLayer.convertStrict(parsed, context) + + if (fixed.source.osmTags["and"] === undefined) { fixed.source.osmTags = {"and": [fixed.source.osmTags]} } - - const validator = new ValidateLayer(sharedLayerJson.path, true, doesImageExist); + + const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist); validator.convertStrict(fixed, context) if (sharedLayers.has(fixed.id)) { @@ -218,39 +279,18 @@ class LayerOverviewUtils { } sharedLayers.set(fixed.id, fixed) + recompiledLayers.push(fixed.id) this.writeLayer(fixed) } + + console.log("Recompiled layers " + recompiledLayers.join(", ") + " and skipped " + skippedLayers.length + " layers") + return sharedLayers; } - private static publicLayerIdsFrom(themefiles: LayoutConfigJson[]): Set { - const publicLayers = [].concat(...themefiles - .filter(th => !th.hideFromOverview) - .map(th => th.layers)) - - const publicLayerIds = new Set() - for (const publicLayer of publicLayers) { - if (typeof publicLayer === "string") { - publicLayerIds.add(publicLayer) - continue - } - if (publicLayer["builtin"] !== undefined) { - const bi = publicLayer["builtin"] - if (typeof bi === "string") { - publicLayerIds.add(bi) - continue - } - bi.forEach(id => publicLayerIds.add(id)) - continue - } - publicLayerIds.add(publicLayer.id) - } - return publicLayerIds - } - - private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map): Map { + private buildThemeIndex(doesImageExist: DoesImageExist, sharedLayers: Map, recompiledThemes: string[], forceReload: boolean): Map { console.log(" ---------- VALIDATING BUILTIN THEMES ---------") const themeFiles = ScriptUtils.getThemeFiles(); const fixed = new Map(); @@ -262,9 +302,23 @@ class LayerOverviewUtils { tagRenderings: this.getSharedTagRenderings(doesImageExist), publicLayers } + const skippedThemes: string[] = [] for (const themeInfo of themeFiles) { + + const themePath = themeInfo.path; let themeFile = themeInfo.parsed - const themePath = themeInfo.path + + { + const targetPath = LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/")) + const usedLayers = Array.from(LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)) + .map(id => LayerOverviewUtils.layerPath + id + ".json") + if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) { + fixed.set(themeFile.id, themeFile) + skippedThemes.push(themeFile.id) + continue; + } + recompiledThemes.push(themeFile.id) + } new PrevalidateTheme().convertStrict(themeFile, themePath) try { @@ -290,6 +344,9 @@ class LayerOverviewUtils { mustHaveLanguage: t.mustHaveLanguage?.length > 0, } })); + + console.log("Recompiled themes " + recompiledThemes.join(", ") + " and skipped " + skippedThemes.length + " themes") + return fixed; } From 9f41e719f25ee4e657558a17390468319b04e632 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 16:35:14 +0200 Subject: [PATCH 15/16] Fix build --- package.json | 2 +- scripts/build.sh | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e1a66f1c3..1d97a1908 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "generate:schemas": "ts2json-schema -p Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && ts-node scripts/fixSchemas.ts ", "generate:service-worker": "tsc service-worker.ts && git_hash=$(git rev-parse HEAD) && sed -i \"s/GITHUB-COMMIT/$git_hash/\" service-worker.js", "optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", - "reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json", + "reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json && rm ./asssets/generated/layers/* && rm ./assets/generated/themes/*", "generate": "mkdir -p ./assets/generated; npm run reset:layeroverview; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run generate:licenses; npm run generate:layeroverview; npm run generate:service-worker", "generate:charging-stations": "cd ./assets/layers/charging_station && ts-node csvToJson.ts && cd -", "prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh", diff --git a/scripts/build.sh b/scripts/build.sh index 28bd83dee..c194cfe9d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -10,9 +10,10 @@ mkdir dist 2> /dev/null mkdir dist/assets 2> /dev/null # This script ends every line with '&&' to chain everything. A failure will thus stop the build -npm run generate:editor-layer-index +npm run generate:editor-layer-index && +npm run reset:layeroverview npm run generate && -ts-node ./scripts/generateLayeroverview --force && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise +npm run generate:layeroverview --force && # generate:layeroverview has to be run twice: the personal theme won't pick up all the layers otherwise; first time happens in 'npm run generate' npm run test && npm run generate:layouts From a08a49abb2c77092dee157764b6b51378f64a882 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 6 Jul 2022 17:11:17 +0200 Subject: [PATCH 16/16] Fix partial compilation, fix tests --- Models/ThemeConfig/LayerConfig.ts | 53 +++++++++++++++--------------- scripts/generateLayerOverview.ts | 2 +- test/scripts/GenerateCache.spec.ts | 15 +++++---- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 635691fcd..4779f5ebc 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -31,6 +31,7 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement"; export default class LayerConfig extends WithContextLoader { + public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const; public readonly id: string; public readonly name: Translation; public readonly description: Translation; @@ -44,10 +45,8 @@ export default class LayerConfig extends WithContextLoader { public readonly maxzoom: number; public readonly title?: TagRenderingConfig; public readonly titleIcons: TagRenderingConfig[]; - public readonly mapRendering: PointRenderingConfig[] public readonly lineRendering: LineRenderingConfig[] - public readonly units: Unit[]; public readonly deletion: DeleteConfig | null; public readonly allowMove: MoveConfig | null @@ -57,15 +56,11 @@ export default class LayerConfig extends WithContextLoader { * In seconds */ public readonly maxAgeOfCache: number - public readonly presets: PresetConfig[]; - public readonly tagRenderings: TagRenderingConfig[]; public readonly filters: FilterConfig[]; public readonly filterIsSameAs: string; public readonly forceLoad: boolean; - - public static readonly syncSelectionAllowed = ["no" , "local" , "theme-only" , "global"] as const; public readonly syncSelection: (typeof LayerConfig.syncSelectionAllowed)[number] // this is a trick to conver a constant array of strings into a type union of these values constructor( @@ -74,18 +69,24 @@ export default class LayerConfig extends WithContextLoader { official: boolean = true ) { context = context + "." + json.id; - const translationContext = "layers:"+json.id + const translationContext = "layers:" + json.id super(json, context) this.id = json.id; + if (typeof json === "string") { + throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})` + } + + if (json.id === undefined) { - throw "Not a valid layer: id is undefined: " + JSON.stringify(json) + throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})` } if (json.source === undefined) { throw "Layer " + this.id + " does not define a source section (" + context + ")" } + if (json.source.osmTags === undefined) { throw "Layer " + this.id + " does not define a osmTags in the source section - these should always be present, even for geojson layers (" + context + ")" } @@ -98,8 +99,8 @@ export default class LayerConfig extends WithContextLoader { } this.maxAgeOfCache = json.source.maxCacheAge ?? 24 * 60 * 60 * 30 - if(json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0){ - throw context+ " Invalid sync-selection: must be one of "+LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ")+" but got '"+json.syncSelection+"'" + if (json.syncSelection !== undefined && LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0) { + throw context + " Invalid sync-selection: must be one of " + LayerConfig.syncSelectionAllowed.map(v => `'${v}'`).join(", ") + " but got '" + json.syncSelection + "'" } this.syncSelection = json.syncSelection ?? "no"; const osmTags = TagUtils.Tag( @@ -107,10 +108,10 @@ export default class LayerConfig extends WithContextLoader { context + "source.osmTags" ); - if(Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()){ - throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t"+osmTags.asHumanString(false, false, {}); + if (Constants.priviliged_layers.indexOf(this.id) < 0 && osmTags.isNegative()) { + throw context + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + osmTags.asHumanString(false, false, {}); } - + if (json.source["geoJsonSource"] !== undefined) { throw context + "Use 'geoJson' instead of 'geoJsonSource'"; } @@ -118,7 +119,7 @@ export default class LayerConfig extends WithContextLoader { if (json.source["geojson"] !== undefined) { throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"; } - + this.source = new SourceConfig( { @@ -138,8 +139,8 @@ export default class LayerConfig extends WithContextLoader { this.allowSplit = json.allowSplit ?? false; this.name = Translations.T(json.name, translationContext + ".name"); - if(json.units!==undefined && !Array.isArray(json.units)){ - throw "At "+context+".units: the 'units'-section should be a list; you probably have an object there" + if (json.units !== undefined && !Array.isArray(json.units)) { + throw "At " + context + ".units: the 'units'-section should be a list; you probably have an object there" } this.units = (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) @@ -167,8 +168,8 @@ export default class LayerConfig extends WithContextLoader { const index = kv.indexOf("="); let key = kv.substring(0, index).trim(); const r = "[a-z_][a-z0-9:]*" - if(key.match(r) === null){ - throw "At "+context+" invalid key for calculated tag: "+key+"; it should match "+r + if (key.match(r) === null) { + throw "At " + context + " invalid key for calculated tag: " + key + "; it should match " + r } const isStrict = key.endsWith(':') if (isStrict) { @@ -343,14 +344,14 @@ export default class LayerConfig extends WithContextLoader { } public GenerateDocumentation(usedInThemes: string[], layerIsNeededBy?: Map, dependencies: { - context?: string; - reason: string; - neededLayer: string; - }[] = [] - , addedByDefault = false, canBeIncluded = true): BaseUIElement { + context?: string; + reason: string; + neededLayer: string; + }[] = [] + , addedByDefault = false, canBeIncluded = true): BaseUIElement { const extraProps = [] - - extraProps.push("This layer is shown at zoomlevel **"+this.minzoom+"** and higher") + + extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher") if (canBeIncluded) { if (addedByDefault) { @@ -440,7 +441,7 @@ export default class LayerConfig extends WithContextLoader { let overpassLink: BaseUIElement = undefined; if (Constants.priviliged_layers.indexOf(this.id) < 0) { try { - overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink( new And(neededTags).optimize())) + overpassLink = new Link("Execute on overpass", Overpass.AsOverpassTurboLink(new And(neededTags).optimize())) } catch (e) { console.error("Could not generate overpasslink for " + this.id) } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 99e733a20..0a5a43d44 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -313,7 +313,7 @@ class LayerOverviewUtils { const usedLayers = Array.from(LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)) .map(id => LayerOverviewUtils.layerPath + id + ".json") if (!forceReload && !this.shouldBeUpdated([themePath, ...usedLayers], targetPath)) { - fixed.set(themeFile.id, themeFile) + fixed.set(themeFile.id, JSON.parse(readFileSync(LayerOverviewUtils.themePath+themeFile.id+".json", 'utf8'))) skippedThemes.push(themeFile.id) continue; } diff --git a/test/scripts/GenerateCache.spec.ts b/test/scripts/GenerateCache.spec.ts index 0031079b3..a57e598b9 100644 --- a/test/scripts/GenerateCache.spec.ts +++ b/test/scripts/GenerateCache.spec.ts @@ -29,26 +29,27 @@ describe("GenerateCache", () => { it("should generate a cached file for the Natuurpunt-theme", async () => { // We use /var/tmp instead of /tmp, as more OS's (such as MAC) have this - if(!existsSync("/var/tmp")){ + const dir = "/var/tmp/" + if(!existsSync(dir)){ console.log("Not executing caching test: no temp directory found") } - if (existsSync("/var/tmp/np-cache")) { - ScriptUtils.readDirRecSync("/var/tmp/np-cache").forEach(p => unlinkSync(p)) - rmdirSync("/var/tmp/np-cache") + if (existsSync(dir+"/np-cache")) { + ScriptUtils.readDirRecSync(dir+"np-cache").forEach(p => unlinkSync(p)) + rmdirSync(dir+"np-cache") } - mkdirSync("/var/tmp/np-cache") + mkdirSync(dir+"np-cache") initDownloads( "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*foot.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*hiking.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*bycicle.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22route%22~%22%5E.*horse.*%24%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E.*%5BnN%5Datuurpunt.*%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" ); await main([ "natuurpunt", "12", - "/var/tmp/np-cache", + dir+"np-cache", "51.15423567022531", "3.250579833984375", "51.162821593316934", "3.262810707092285", "--generate-point-overview", "nature_reserve,visitor_information_centre" ]) await ScriptUtils.sleep(500) - const birdhides = JSON.parse(readFileSync("/var/tmp/np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) + const birdhides = JSON.parse(readFileSync(dir+"np-cache/natuurpunt_birdhide_12_2085_1368.geojson", "UTF8")) expect(birdhides.features.length).deep.equal(5) expect(birdhides.features.some(f => f.properties.id === "node/5158056232"), "Didn't find birdhide node/5158056232 ").true