Merge branch 'develop' into feature/station-map

This commit is contained in:
Robin van der Linde 2022-10-29 22:07:56 +02:00
commit 1ead95029f
No known key found for this signature in database
GPG key ID: 53956B3252478F0D
63 changed files with 13359 additions and 42297 deletions

View file

@ -4,8 +4,8 @@
{{service_item
|name= [https://mapcomplete.osm.be/personal personal]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:gl|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: Create a personal theme based on all the available layers of all themes. In order to show some data, open [[#filter]]
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:gl|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:it|en}}, {{#language:da|en}}
|descr= A MapComplete theme: Create a personal theme based on all the available layers of all themes
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, personal
@ -13,38 +13,70 @@
{{service_item
|name= [https://mapcomplete.osm.be/cyclofix cyclofix]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:gl|en}}, {{#language:de|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}
|descr= A MapComplete theme: The goal of this map is to present cyclists with an easy-to-use solution to find the appropriate infrastructure for their needs.<br><br>You can track your precise location (mobile only) and select layers that are relevant for you in the bottom left corner. You can also use this tool to add or edit pins (points of interest) to the map and provide more data by answering the questions.<br><br>All changes you make will automatically be saved in the global database of OpenStreetMap and can be freely re-used by others.<br><br>For more information about the cyclofix project, go to [[https://cyclofix.osm.be/]].
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:gl|en}}, {{#language:de|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:da|en}}
|descr= A MapComplete theme: The goal of this map is to present cyclists with an easy-to-use solution to find the appropriate infrastructure for their needs
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, cyclofix
}}
{{service_item
|name= [https://mapcomplete.osm.be/waste waste]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:it|en}}, {{#language:ca|en}}, {{#language:da|en}}
|descr= A MapComplete theme: Map showing waste baskets and recycling facilities
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, waste
}}
{{service_item
|name= [https://mapcomplete.osm.be/etymology etymology]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:fr|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: What is the origin of a toponym?
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, etymology
}}
{{service_item
|name= [https://mapcomplete.osm.be/food food]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:de|en}}, {{#language:es|en}}, {{#language:nb_NO|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: Restaurants and fast food
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, food
}}
{{service_item
|name= [https://mapcomplete.osm.be/cafes_and_pubs cafes_and_pubs]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: Pubs and bars
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, cafes_and_pubs
}}
{{service_item
|name= [https://mapcomplete.osm.be/playgrounds playgrounds]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:nb_NO|en}}, {{#language:id|en}}, {{#language:hu|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map with playgrounds
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, playgrounds
}}
{{service_item
|name= [https://mapcomplete.osm.be/hailhydrant hailhydrant]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:fr|en}}, {{#language:nb_NO|en}}, {{#language:it|en}}, {{#language:id|en}}
|descr= A MapComplete theme: On this map you can find and update hydrants, fire stations, ambulance stations, and extinguishers in your favorite neighborhoods.
You can track your precise location (mobile only) and select layers that are relevant for you in the bottom left corner. You can also use this tool to add or edit pins (points of interest) to the map and provide additional details by answering available questions.
All changes you make will automatically be saved in the global database of OpenStreetMap and can be freely re-used by others.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Erwin Olario;]}}
|lang= {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:nl|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Map to show hydrants, extinguishers, fire stations, and ambulance stations.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, hailhydrant
}}
{{service_item
|name= [https://mapcomplete.osm.be/bookcases bookcases]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:pt_BR|en}}
|descr= A MapComplete theme: A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, bookcases
}}
{{service_item
|name= [https://mapcomplete.osm.be/toilets toilets]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:pl|en}}
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:pl|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:nb_NO|en}}, {{#language:da|en}}
|descr= A MapComplete theme: A map of public toilets
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
@ -53,17 +85,26 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/aed aed]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:hu|en}}, {{#language:id|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:sv|en}}, {{#language:pl|en}}, {{#language:pt_BR|en}}
|lang= {{#language:en|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:id|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:sv|en}}, {{#language:pl|en}}, {{#language:pt_BR|en}}, {{#language:nb_NO|en}}, {{#language:hu|en}}, {{#language:sl|en}}, {{#language:zh_Hans|en}}, {{#language:da|en}}, {{#language:fil|en}}, {{#language:cs|en}}
|descr= A MapComplete theme: On this map, one can find and mark nearby defibrillators
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, aed
}}
{{service_item
|name= [https://mapcomplete.osm.be/bookcases bookcases]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:ca|en}}
|descr= A MapComplete theme: A public bookcase is a small streetside cabinet, box, old phone booth or some other objects where books are stored
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, bookcases
}}
{{service_item
|name= [https://mapcomplete.osm.be/artwork artwork]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:hu|en}}, {{#language:id|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:sv|en}}, {{#language:pl|en}}, {{#language:es|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: Welcome to Open Artwork Map, a map of statues, busts, grafittis and other artwork all over the world
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:id|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:es|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:hu|en}}, {{#language:pl|en}}, {{#language:ca|en}}, {{#language:zh_Hans|en}}, {{#language:fil|en}}, {{#language:da|en}}, {{#language:cs|en}}
|descr= A MapComplete theme: An open map of statues, busts, graffitis and other artwork all over the world
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, artwork
@ -71,17 +112,26 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/benches benches]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:pt_BR|en}}
|descr= A MapComplete theme: This map shows all benches that are recorded in OpenStreetMap: Individual benches, and benches belonging to public transport stops or shelters. With an OpenStreetMap account, you can map new benches or edit details of existing benches.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Florian Edelmann;]}}
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:pt_BR|en}}, {{#language:hu|en}}, {{#language:id|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:zh_Hans|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map of benches
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, benches
}}
{{service_item
|name= [https://mapcomplete.osm.be/bicycle_rental bicycle_rental]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:id|en}}, {{#language:fr|en}}, {{#language:es|en}}, {{#language:nb_NO|en}}, {{#language:ca|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map with bicycle rental stations and bicycle rental shops
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, bicycle_rental
}}
{{service_item
|name= [https://mapcomplete.osm.be/bicyclelib bicyclelib]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:fr|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:de|en}}, {{#language:pt_BR|en}}
|descr= A MapComplete theme: A bicycle library is a place where bicycles can be lent, often for a small yearly fee. A notable use case are bicycle libraries for kids, which allows them to change for a bigger bike when they've outgrown their current bike
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:fr|en}}, {{#language:zh_Hant|en}}, {{#language:de|en}}, {{#language:hu|en}}, {{#language:nb_NO|en}}, {{#language:ca|en}}, {{#language:da|en}}
|descr= A MapComplete theme: A bicycle library is a place where bicycles can be lent, often for a small yearly fee
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, bicyclelib
@ -89,35 +139,35 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/binoculars binoculars]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: A map with binoculars fixed in place with a pole. It can typically be found on touristic locations, viewpoints, on top of panoramic towers or occasionally on a nature reserve.
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:nb_NO|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:fr|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map with fixed binoculars
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, binoculars
}}
{{service_item
|name= [https://mapcomplete.osm.be/cafes_and_pubs cafes_and_pubs]
|name= [https://mapcomplete.osm.be/blind_osm blind_osm]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:fr|en}}, {{#language:en|en}}
|descr= A MapComplete theme: Cafés, kroegen en drinkgelegenheden
|lang= {{#language:en|en}}
|descr= A MapComplete theme: Help to map features relevant for the blind
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, cafes_and_pubs
|genre= POI, editor, blind_osm
}}
{{service_item
|name= [https://mapcomplete.osm.be/campersite campersite]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:fr|en}}, {{#language:zh_Hant|en}}, {{#language:pt_BR|en}}, {{#language:id|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: This site collects all official camper stopover places and places where you can dump grey and black water. You can add details about the services provided and the cost. Add pictures and reviews. This is a website and a webapp. The data is stored in OpenStreetMap, so it will be free forever and can be re-used by any app.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by joost schouppe;]}}
|lang= {{#language:en|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:fr|en}}, {{#language:zh_Hant|en}}, {{#language:nl|en}}, {{#language:pt_BR|en}}, {{#language:de|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Find sites to spend the night with your camper
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, campersite
}}
{{service_item
|name= [https://mapcomplete.osm.be/charging_stations charging_stations]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:id|en}}, {{#language:it|en}}, {{#language:ja|en}}, {{#language:ru|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:nl|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: On this open map, one can find and mark information about charging stations
|lang= {{#language:en|en}}, {{#language:it|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:nb_NO|en}}, {{#language:ru|en}}, {{#language:hu|en}}, {{#language:fr|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A worldwide map of charging stations
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, charging_stations
@ -125,17 +175,17 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/climbing climbing]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:de|en}}, {{#language:en|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:it|en}}, {{#language:ca|en}}, {{#language:fr|en}}, {{#language:id|en}}
|descr= A MapComplete theme: On this map you will find various climbing opportunities such as climbing gyms, bouldering halls and rocks in nature.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Christian Neumann <christian@utopicode.de>;]}}
|lang= {{#language:nl|en}}, {{#language:de|en}}, {{#language:en|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:fr|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:nb_NO|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map you will find various climbing opportunities such as climbing gyms, bouldering halls and rocks in nature
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, climbing
}}
{{service_item
|name= [https://mapcomplete.osm.be/cycle_infra cycle_infra]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: A map where you can view and edit things related to the bicycle infrastructure. Made during #osoc21.
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:nb_NO|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:ca|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map where you can view and edit things related to the bicycle infrastructure.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, cycle_infra
@ -143,8 +193,8 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/cyclestreets cyclestreets]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:it|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: A cyclestreet is is a street where <b>motorized traffic is not allowed to overtake cyclists</b>. They are signposted by a special traffic sign. Cyclestreets can be found in the Netherlands and Belgium, but also in Germany and France.
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:nb_NO|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map of cyclestreets
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, cyclestreets
@ -152,35 +202,35 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/drinking_water drinking_water]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:de|en}}, {{#language:nb_NO|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:ca|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map, publicly accessible drinking water spots are shown and can be easily added
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, drinking_water
}}
{{service_item
|name= [https://mapcomplete.osm.be/education education]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:ca|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map, you'll find information about all types of schools and eduction and can easily add more information
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, education
}}
{{service_item
|name= [https://mapcomplete.osm.be/facadegardens facadegardens]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:fr|en}}, {{#language:nb_NO|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: [[https://nl.wikipedia.org/wiki/Geveltuin' target=_blank>Facade gardens</a>, green facades and trees in the city not only bring peace and quiet, but also a more beautiful city, greater biodiversity, a cooling effect and better air quality. <br/> Klimaan VZW and Mechelen Klimaatneutraal want to map existing and new facade gardens as an example for people who want to build their own garden or for city walkers who love nature.<br/>More info about the project at <a href='https://klimaan.be/' target=_blank>klimaan.be</a>.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by joost schouppe; stla;]}}
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:it|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: This map shows facade gardens with pictures and useful info about orientation, sunshine and plant types.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, facadegardens
}}
{{service_item
|name= [https://mapcomplete.osm.be/food food]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:fr|en}}, {{#language:en|en}}
|descr= A MapComplete theme: Restaurants en fast food
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, food
}}
{{service_item
|name= [https://mapcomplete.osm.be/fritures fritures]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:fr|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:ca|en}}, {{#language:id|en}}, {{#language:ru|en}}, {{#language:it|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: Op deze kaart vind je je favoriete frituur!
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map, you'll find your favourite fries shop!
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, fritures
@ -188,8 +238,8 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/ghostbikes ghostbikes]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:ja|en}}, {{#language:nb_NO|en}}, {{#language:zh_Hant|en}}, {{#language:fr|en}}, {{#language:eo|en}}, {{#language:es|en}}, {{#language:fi|en}}, {{#language:gl|en}}, {{#language:hu|en}}, {{#language:it|en}}, {{#language:pl|en}}, {{#language:pt_BR|en}}, {{#language:ru|en}}, {{#language:sv|en}}
|descr= A MapComplete theme: A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.<br/><br/>On this map, one can see all the ghost bikes which are known by OpenStreetMap. Is a ghost bike missing? Everyone can add or update information here - you only need to have a (free) OpenStreetMap account.
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:fr|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:da|en}}
|descr= A MapComplete theme: A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, ghostbikes
@ -197,71 +247,161 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/hackerspaces hackerspaces]
|region= Worldwide
|lang= {{#language:en|en}}
|descr= A MapComplete theme: On this map you can see hackerspaces, add a new hackerspace or update data directly
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map of hackerspaces
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, hackerspaces
}}
{{service_item
|name= [https://mapcomplete.osm.be/healthcare healthcare]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:ca|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: On this map, various healthcare related items are shown
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, healthcare
}}
{{service_item
|name= [https://mapcomplete.osm.be/hotels hotels]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:da|en}}, {{#language:nb_NO|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: On this map, you'll find hotels in your area
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, hotels
}}
{{service_item
|name= [https://mapcomplete.osm.be/indoors indoors]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map, publicly accessible indoor places are shown
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, indoors
}}
{{service_item
|name= [https://mapcomplete.osm.be/kerbs_and_crossings kerbs_and_crossings]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: A map showing kerbs and crossings
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, kerbs_and_crossings
}}
{{service_item
|name= [https://mapcomplete.osm.be/maps maps]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: On this map you can find all maps OpenStreetMap knows - typically a big map on an information board showing the area, city or region, e.g. a tourist map on the back of a billboard, a map of a nature reserve, a map of cycling networks in the region, ...) <br/><br/>If a map is missing, you can easily map this map on OpenStreetMap.
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: This theme shows all (touristic) maps that OpenStreetMap knows of
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, maps
}}
{{service_item
|name= [https://mapcomplete.osm.be/maxspeed maxspeed]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: This map shows the legally allowed maximum speed on every road.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, maxspeed
}}
{{service_item
|name= [https://mapcomplete.osm.be/nature nature]
|region= Worldwide
|lang= {{#language:nl|en}}
|descr= A MapComplete theme: Op deze kaart vind je informatie voor natuurliefhebbers, zoals info over het natuurgebied waar je inzit, vogelkijkhutten, informatieborden, ...
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map for nature lovers, with interesting POI's
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, nature
}}
{{service_item
|name= [https://mapcomplete.osm.be/notes notes]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:hu|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: A note is a pin on the map with some text to indicate something wrong
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, notes
}}
{{service_item
|name= [https://mapcomplete.osm.be/observation_towers observation_towers]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:nb_NO|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Publicly accessible towers to enjoy the view
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, observation_towers
}}
{{service_item
|name= [https://mapcomplete.osm.be/onwheels onwheels]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:nl|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map, publicly weelchair accessible places are shown and can be easily added
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, onwheels
}}
{{service_item
|name= [https://mapcomplete.osm.be/openwindpowermap openwindpowermap]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:fr|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: A map for showing and editing wind turbines.
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Seppe Santens;]}}
|lang= {{#language:en|en}}, {{#language:fr|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:nl|en}}, {{#language:da|en}}, {{#language:nb_NO|en}}
|descr= A MapComplete theme: A map for showing and editing wind turbines
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, openwindpowermap
}}
{{service_item
|name= [https://mapcomplete.osm.be/osm_community_index osm_community_index]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: An index of community resources for OpenStreetMap.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, osm_community_index
}}
{{service_item
|name= [https://mapcomplete.osm.be/parkings parkings]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:nb_NO|en}}, {{#language:zh_Hant|en}}, {{#language:id|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: This map shows different parking spots
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, parkings
}}
{{service_item
|name= [https://mapcomplete.osm.be/playgrounds playgrounds]
|name= [https://mapcomplete.osm.be/pets pets]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: On this map, you find playgrounds and can add more information
|lang= {{#language:en|en}}, {{#language:da|en}}, {{#language:de|en}}, {{#language:nl|en}}, {{#language:fr|en}}
|descr= A MapComplete theme: On this map, you'll find various interesting places for you pets: veterinarians, dog parks, pet shops, dog-friendly restaurants,
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, playgrounds
|genre= POI, editor, pets
}}
{{service_item
|name= [https://mapcomplete.osm.be/postboxes postboxes]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:nb_NO|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map showing postboxes and post offices
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, postboxes
}}
{{service_item
|name= [https://mapcomplete.osm.be/rainbow_crossings rainbow_crossings]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: On this map, rainbow-painted pedestrian crossings are shown and can be easily added
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, rainbow_crossings
}}
{{service_item
|name= [https://mapcomplete.osm.be/shops shops]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:nl|en}}, {{#language:ca|en}}, {{#language:id|en}}
|descr= A MapComplete theme: On this map, one can mark basic information about shops, add opening hours and phone numbers
|lang= {{#language:en|en}}, {{#language:fr|en}}, {{#language:ja|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:nl|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: An editable map with basic shop information
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, shops
@ -269,44 +409,62 @@ All changes you make will automatically be saved in the global database of OpenS
{{service_item
|name= [https://mapcomplete.osm.be/sport_pitches sport_pitches]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:fr|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}
|descr= A MapComplete theme: A sport pitch is an area where sports are played
|lang= {{#language:nl|en}}, {{#language:fr|en}}, {{#language:en|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map showing sport pitches
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, sport_pitches
}}
{{service_item
|name= [https://mapcomplete.osm.be/sports sports]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Map showing sport facilities.
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, sports
}}
{{service_item
|name= [https://mapcomplete.osm.be/street_lighting street_lighting]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:de|en}}, {{#language:es|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: On this map you can find everything about street lighting
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, street_lighting
}}
{{service_item
|name= [https://mapcomplete.osm.be/surveillance surveillance]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:fr|en}}, {{#language:pl|en}}
|descr= A MapComplete theme: On this open map, you can find surveillance cameras.
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:fr|en}}, {{#language:pl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:hu|en}}, {{#language:da|en}}, {{#language:nb_NO|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Surveillance cameras and other means of surveillance
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, surveillance
}}
{{service_item
|name= [https://mapcomplete.osm.be/transit transit]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:de|en}}, {{#language:fr|en}}, {{#language:da|en}}
|descr= A MapComplete theme: Plan your trip with the help of the public transport system
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, transit
}}
{{service_item
|name= [https://mapcomplete.osm.be/trees trees]
|region= Worldwide
|lang= {{#language:nl|en}}, {{#language:en|en}}, {{#language:fr|en}}, {{#language:it|en}}, {{#language:ru|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:pl|en}}
|descr= A MapComplete theme: Map all the trees!
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Midgard;]}}
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:fr|en}}, {{#language:it|en}}, {{#language:ja|en}}, {{#language:zh_Hant|en}}, {{#language:ru|en}}, {{#language:pl|en}}, {{#language:de|en}}, {{#language:nb_NO|en}}, {{#language:hu|en}}, {{#language:ca|en}}, {{#language:es|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: Map all the trees
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, trees
}}
{{service_item
|name= [https://mapcomplete.osm.be/uk_addresses uk_addresses]
|region= Worldwide
|lang= {{#language:en|en}}
|descr= A MapComplete theme: Contribute to OpenStreetMap by filling out address information
|material= {{yes|[https://mapcomplete.osm.be/ Yes, by Pieter Vander Vennet, Rob Nickerson, Russ Garrett;]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, uk_addresses
}}
{{service_item
|name= [https://mapcomplete.osm.be/waste_basket waste_basket]
|region= Worldwide
|lang= {{#language:en|en}}, {{#language:nl|en}}
|descr= A MapComplete theme: On this map, you'll find waste baskets near you. If a waste basket is missing on this map, you can add it yourself
|lang= {{#language:en|en}}, {{#language:nl|en}}, {{#language:de|en}}, {{#language:it|en}}, {{#language:zh_Hant|en}}, {{#language:hu|en}}, {{#language:fr|en}}, {{#language:nb_NO|en}}, {{#language:da|en}}, {{#language:_context|en}}
|descr= A MapComplete theme: A map with waste baskets
|material= {{yes|[https://mapcomplete.osm.be/ Yes]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, waste_basket

View file

@ -1,20 +1,10 @@
import * as turf from "@turf/turf"
import { BBox } from "./BBox"
import togpx from "togpx"
import Constants from "../Models/Constants"
import {BBox} from "./BBox"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import {
AllGeoJSON,
booleanWithin,
Coord,
Feature,
Geometry,
Lines,
MultiPolygon,
Polygon,
Properties,
} from "@turf/turf"
import { GeoJSON, LineString, Point } from "geojson"
import * as turf from "@turf/turf"
import {AllGeoJSON, booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon,} from "@turf/turf"
import {LineString, Point} from "geojson"
import togpx from "togpx"
import Constants from "../Models/Constants";
export class GeoOperations {
private static readonly _earthRadius = 6378137
@ -393,21 +383,22 @@ export class GeoOperations {
.features.map((p) => <[number, number]>p.geometry.coordinates)
}
public static AsGpx(feature, generatedWithLayer?: LayerConfig) {
const metadata = {}
public static AsGpx(feature: Feature, options?: {layer?: LayerConfig, gpxMetadata?: any }) : string{
const metadata = options?.gpxMetadata ?? {}
metadata["time"] = metadata["time"] ?? new Date().toISOString()
const tags = feature.properties
if (generatedWithLayer !== undefined) {
metadata["name"] = generatedWithLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
metadata["desc"] = "Generated with MapComplete layer " + generatedWithLayer.id
if (options?.layer !== undefined) {
metadata["name"] = options?.layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
metadata["desc"] = "Generated with MapComplete layer " + options?.layer.id
if (tags._backend?.contains("openstreetmap")) {
metadata["copyright"] =
"Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
metadata["author"] = tags["_last_edit:contributor"]
metadata["link"] = "https://www.openstreetmap.org/" + tags.id
metadata["time"] = tags["_last_edit:timestamp"]
} else {
metadata["time"] = new Date().toISOString()
}
}

View file

@ -27,10 +27,12 @@ export default class UserDetails {
export class OsmConnection {
public static readonly oauth_configs = {
osm: {
oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem",
oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI",
url: "https://www.openstreetmap.org",
"osm": {
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
url: "https://www.openstreetmap.org"
// OAUTH 1.0 application
// https://www.openstreetmap.org/user/Pieter%20Vander%20Vennet/oauth_clients/7404
},
"osm-test": {
oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2",
@ -333,6 +335,80 @@ export class OsmConnection {
})
}
public async uploadGpxTrack(gpx: string, options: {
description: string,
visibility: "private" | "public" | "trackable" | "identifiable",
filename?: string
/**
* Some words to give some properties;
*
* Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words.
*/
labels: string[]
}): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
return new Promise<{ id: number }>((ok, error) => {
window.setTimeout(() => ok({id: Math.floor(Math.random() * 1000)}), Math.random() * 5000)
});
}
const contents = {
"file": gpx,
"description": options.description ?? "",
"tags": options.labels?.join(",") ?? "",
"visibility": options.visibility
}
const extras = {
"file": "; filename=\""+(options.filename ?? ("gpx_track_mapcomplete_"+(new Date().toISOString())))+"\"\r\nContent-Type: application/gpx+xml"
}
const auth = this.auth;
const boundary ="987654"
let body = ""
for (const key in contents) {
body += "--" + boundary + "\r\n"
body += "Content-Disposition: form-data; name=\"" + key + "\""
if(extras[key] !== undefined){
body += extras[key]
}
body += "\r\n\r\n"
body += contents[key] + "\r\n"
}
body += "--" + boundary + "--\r\n"
return new Promise((ok, error) => {
auth.xhr({
method: 'POST',
path: `/api/0.6/gpx/create`,
options: {
header:
{
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length
}
},
content: body
}, function (
err,
response: string) {
console.log("RESPONSE IS", response)
if (err !== null) {
error(err)
} else {
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
ok({id: parsed})
}
})
})
}
public addCommentToNote(id: number | string, text: string): Promise<void> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id)

View file

@ -68,7 +68,7 @@ export default class MapState extends UserRelatedState {
public currentUserLocation: SimpleFeatureSource
/**
* All previously visited points
* All previously visited points, with their metadata
*/
public historicalUserLocations: SimpleFeatureSource
/**
@ -79,6 +79,11 @@ export default class MapState extends UserRelatedState {
7 * 24 * 60 * 60,
"gps_location_retention"
)
/**
* A featureSource containing a single linestring which has the GPS-history of the user.
* However, metadata (such as when every single point was visited) is lost here (but is kept in `historicalUserLocations`.
* Note that this featureSource is _derived_ from 'historicalUserLocations'
*/
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled
/**

View file

@ -526,7 +526,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* // should warn for unexpected keys
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, errors, "test") // => {'*': "{image_carousel()}"}
* errors // => ["The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"]
* errors // => ["At test: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"]
*
* // should give an error on unknown visualisations
* const errors = []
@ -593,7 +593,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
...Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
return `At ${context}: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
)
@ -610,7 +610,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
argNamesList,
(x) => x
)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
return `At ${context}: Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
@ -623,7 +623,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
const param = special[arg.name]
if (param === undefined) {
errors.push(`Obligated parameter '${arg.name}' not found`)
errors.push(`At ${context}: Obligated parameter '${arg.name}' in special rendering of type ${vis.funcName} not found.\n${arg.doc}`)
}
}
@ -735,7 +735,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
continue
}
Utils.WalkPath(path.path, json, (leaf, travelled) =>
RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))
RewriteSpecial.convertIfNeeded(leaf, errors, context + ":" + travelled.join("."))
)
}

View file

@ -1,19 +1,19 @@
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import {DesugaringStep, Each, Fuse, On} from "./Conversion"
import {LayerConfigJson} from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
import {Utils} from "../../../Utils"
import Constants from "../../Constants"
import { Translation } from "../../../UI/i18n/Translation"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import {Translation} from "../../../UI/i18n/Translation"
import {LayoutConfigJson} from "../Json/LayoutConfigJson"
import LayoutConfig from "../LayoutConfig"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExtractImages } from "./FixImages"
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"
import {TagUtils} from "../../../Logic/Tags/TagUtils"
import {ExtractImages} from "./FixImages"
import ScriptUtils from "../../../scripts/ScriptUtils"
import { And } from "../../../Logic/Tags/And"
import {And} from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
import Svg from "../../../Svg"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson"
import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig"
@ -617,6 +617,24 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
}
}
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
constructor() {
super("Miscellanious checks on the tagrendering", ["special"], "MiscTagREnderingChecksRew");
}
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const errors = []
if(json["special"] !== undefined){
errors.push("At "+context+": detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`")
}
return {
result: json,
errors
};
}
}
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
super(

View file

@ -1,8 +1,8 @@
import { SpecialVisualization } from "../../UI/SpecialVisualizations"
import { SubstitutedTranslation } from "../../UI/SubstitutedTranslation"
import TagRenderingConfig from "./TagRenderingConfig"
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
import LayerConfig from "./LayerConfig"
import {SpecialVisualization} from "../../UI/SpecialVisualization";
export default class DependencyCalculator {
public static GetTagRenderingDependencies(tr: TagRenderingConfig): string[] {

View file

@ -66,21 +66,24 @@ export default class FilterConfig {
})
for (const field of fields) {
question.OnEveryLanguage((txt, language) => {
for (let ln in question.translations) {
const txt = question.translations[ln]
if(ln.startsWith("_")){
continue
}
if (txt.indexOf("{" + field.name + "}") < 0) {
throw (
"Error in filter with fields at " +
context +
".question." +
language +
ln +
": The question text should contain every field, but it doesn't contain `{" +
field +
"}`: " +
txt
)
}
return txt
})
}
}
if (option.default) {

View file

@ -239,6 +239,9 @@ export default class TagRenderingConfig {
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
}
for (const ln in this.render.translations) {
if(ln.startsWith("_")){
continue
}
const txt: string = this.render.translations[ln]
if (txt === "") {
throw context + " Rendering for language " + ln + " is empty"

View file

@ -73,13 +73,11 @@ export class SubtleButton extends UIElement {
}
})
const loading = new Lazy(() => new Loading(loadingText))
return new VariableUiElement(
state.map((st) => {
if (st === "idle") {
return button
}
return loading
})
)
return new VariableUiElement(state.map(st => {
if(st === "idle"){
return button
}
return loading
}))
}
}

View file

@ -1,20 +1,20 @@
import Combine from "../Base/Combine"
import { FlowPanelFactory, FlowStep } from "../ImportFlow/FlowStep"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import { InputElement } from "../Input/InputElement"
import { SvgToPdf, SvgToPdfOptions } from "../../Utils/svgToPdf"
import { FixedInputElement } from "../Input/FixedInputElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep"
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
import {InputElement} from "../Input/InputElement"
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf"
import {FixedInputElement} from "../Input/FixedInputElement"
import {FixedUiElement} from "../Base/FixedUiElement"
import FileSelectorButton from "../Input/FileSelectorButton"
import InputElementMap from "../Input/InputElementMap"
import { RadioButton } from "../Input/RadioButton"
import { Utils } from "../../Utils"
import { VariableUiElement } from "../Base/VariableUIElement"
import {RadioButton} from "../Input/RadioButton"
import {Utils} from "../../Utils"
import {VariableUiElement} from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import Title from "../Base/Title"
import { CheckBox } from "../Input/Checkboxes"
import {CheckBox} from "../Input/Checkboxes"
import Minimap from "../Base/Minimap"
import SearchAndGo from "./SearchAndGo"
import Toggle from "../Input/Toggle"
@ -25,9 +25,7 @@ import Toggleable from "../Base/Toggleable"
import Lazy from "../Base/Lazy"
import LinkToWeblate from "../Base/LinkToWeblate"
import Link from "../Base/Link"
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import * as languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation"
import {AllLanguagesSelector} from "../Popup/AllLanguagesSelector";
class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> {
readonly IsValid: Store<boolean>
@ -203,23 +201,7 @@ class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf; langu
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
const svgToPdf = new SvgToPdf(title, pages, options)
const languageOptions = [
new FixedInputElement("Nederlands", "nl"),
new FixedInputElement("English", "en"),
]
const langs: string[] = Array.from(Object.keys(languages["default"] ?? languages))
console.log("Available languages are:", langs)
const languageSelector = new SearchablePillsSelector(
langs.map((l) => ({
show: new Translation(languages[l]),
value: l,
mainTerm: languages[l],
})),
{
mode: "select-many",
}
)
const languageSelector = new AllLanguagesSelector( )
const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
super([

View file

@ -0,0 +1,118 @@
import Toggle from "../Input/Toggle";
import {RadioButton} from "../Input/RadioButton";
import {FixedInputElement} from "../Input/FixedInputElement";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import {TextField} from "../Input/TextField";
import {UIEventSource} from "../../Logic/UIEventSource";
import Title from "../Base/Title";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Translation} from "../i18n/Translation";
export default class UploadTraceToOsmUI extends Toggle {
private static createDefault(s: string, defaultValue: string){
if(defaultValue.length < 1){
throw "Default value should have some characters"
}
if(s === undefined || s === null || s === ""){
return defaultValue
}
return s
}
constructor(
trace: (title: string) => string,
state: {
layoutToUse: LayoutConfig;
osmConnection: OsmConnection
}, options?: {
whenUploaded?: () => void | Promise<void>
}) {
const t = Translations.t.general.uploadGpx
const uploadFinished = new UIEventSource(false)
const traceVisibilities: {
key: "private" | "public",
name: Translation,
docs: Translation
}[] = [
{
key: "private",
...t.modes.private
},
{
key: "public",
...t.modes.public
}
]
const dropdown = new RadioButton<"private" | "public">(
traceVisibilities.map(tv => new FixedInputElement<"private" | "public">(
new Combine([Translations.W(
tv.name
).SetClass("font-bold"), tv.docs]).SetClass("flex flex-col")
, tv.key)),
{
value: <any>state?.osmConnection?.GetPreference("gps.trace.visibility")
}
)
const description = new TextField({
placeholder: t.meta.descriptionPlaceHolder
})
const title = new TextField({
placeholder: t.meta.titlePlaceholder
})
const clicked = new UIEventSource<boolean>(false)
const confirmPanel = new Combine([
new Title(t.title),
t.intro0,
t.intro1,
t.choosePermission,
dropdown,
new Title(t.meta.title, 4),
t.meta.intro,
title,
t.meta.descriptionIntro,
description,
new Combine([
new SubtleButton(Svg.close_svg(), Translations.t.general.cancel).onClick(() => {
clicked.setData(false)
}).SetClass(""),
new SubtleButton(Svg.upload_svg(), t.confirm).OnClickWithLoading(t.uploading, async () => {
const titleStr = UploadTraceToOsmUI.createDefault(title.GetValue().data, "Track with mapcomplete")
const descriptionStr = UploadTraceToOsmUI.createDefault(description.GetValue().data, "Track created with MapComplete with theme "+state?.layoutToUse?.id)
await state?.osmConnection?.uploadGpxTrack(trace(title.GetValue().data), {
visibility: dropdown.GetValue().data,
description: descriptionStr,
filename: titleStr +".gpx",
labels: ["MapComplete", state?.layoutToUse?.id]
})
if (options?.whenUploaded !== undefined) {
await options.whenUploaded()
}
uploadFinished.setData(true)
})
]).SetClass("flex flex-wrap flex-wrap-reverse justify-between items-stretch")
]).SetClass("flex flex-col p-4 rounded border-2 m-2 border-subtle")
super(
new Combine([Svg.confirm_svg().SetClass("w-12 h-12 mr-2"),
t.uploadFinished])
.SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"),
new Toggle(
confirmPanel,
new SubtleButton(Svg.upload_svg(), t.title)
.onClick(() => clicked.setData(true)),
clicked
), uploadFinished)
}
}

View file

@ -13,15 +13,16 @@ export class RadioButton<T> extends InputElement<T> {
constructor(
elements: InputElement<T>[],
options?: {
selectFirstAsDefault?: true | boolean
dontStyle?: boolean
selectFirstAsDefault?: true | boolean,
dontStyle?: boolean,
value?: UIEventSource<T>
}
) {
super()
options = options ?? {}
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true
this._elements = Utils.NoNull(elements)
this.value = new UIEventSource<T>(undefined)
this.value = options?.value ?? new UIEventSource<T>(undefined)
this._dontStyle = options.dontStyle ?? false
}

View file

@ -1,23 +1,25 @@
import { UIElement } from "../UIElement"
import { InputElement } from "./InputElement"
import {UIElement} from "../UIElement"
import {InputElement} from "./InputElement"
import BaseUIElement from "../BaseUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import {Store, UIEventSource} from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Locale from "../i18n/Locale"
import Combine from "../Base/Combine"
import { TextField } from "./TextField"
import {TextField} from "./TextField"
import Svg from "../../Svg"
import { VariableUiElement } from "../Base/VariableUIElement"
import {VariableUiElement} from "../Base/VariableUIElement"
/**
* A single 'pill' which can hide itself if the search criteria is not met
*/
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
private readonly _shown: BaseUIElement
public readonly _selected: UIEventSource<boolean>
public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
public readonly matchesSearchCriteria: Store<boolean>
public readonly forceSelected: UIEventSource<boolean>
private readonly _shown: BaseUIElement
private readonly _squared: boolean
public constructor(
shown: string | BaseUIElement,
mainTerm: Record<string, string>,
@ -26,7 +28,9 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
searchTerms?: Record<string, string[]>
selected?: UIEventSource<boolean>
forceSelected?: UIEventSource<boolean>
squared?: boolean
squared?: boolean,
/* Hide, if not selected*/
hide?: Store<boolean>
}
) {
super()
@ -49,24 +53,31 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
const forceSelected = (this.forceSelected =
options?.forceSelected ?? new UIEventSource<boolean>(false))
this.isShown = search.map(
(s) => {
if (s === undefined || s.length === 0) {
return true
}
this.matchesSearchCriteria = search.map(s => {
if (s === undefined || s.length === 0) {
return true
}
s = s?.trim()?.toLowerCase()
if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
return true
}
if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
return true
}
return false
})
this.isShown = this.matchesSearchCriteria.map(
(matchesSearch) => {
if (selected.data && !forceSelected.data) {
return true
}
s = s?.trim()?.toLowerCase()
if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
return true
if (options?.hide?.data) {
return false
}
if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
return true
}
return false
return matchesSearch
},
[selected, Locale.language]
[selected, Locale.language, options?.hide]
)
const self = this
@ -128,13 +139,12 @@ class SelfHidingToggle extends UIElement implements InputElement<boolean> {
* A searchfield can be used to filter the values
*/
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
private readonly selectedElements: UIEventSource<T[]>
public readonly someMatchFound: Store<boolean>
private readonly selectedElements: UIEventSource<T[]>
/**
*
* @param values
* @param values: the values that can be selected
* @param options
*/
constructor(
@ -142,38 +152,57 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
show: BaseUIElement
value: T
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
searchTerms?: Record<string, string[]>,
/* If there are more then 200 elements, should this element still be shown? */
hasPriority?: Store<boolean>
}[],
options?: {
/*
* If one single value can be selected (like a radio button) or if many values can be selected (like checkboxes)
*/
mode?: "select-one" | "select-many"
/**
* The values of the selected elements.
* Use this to tie input elements together
*/
selectedElements?: UIEventSource<T[]>
/**
* The search bar. Use this to seed the search value or to tie to another value
*/
searchValue?: UIEventSource<string>
/**
* What is shown if the search yielded no results.
* By default: a translated "no search results"
*/
onNoMatches?: BaseUIElement
/**
* An element that is shown if no search is entered
* Default behaviour is to show all options
*/
onNoSearchMade?: BaseUIElement
/**
* Shows this if there are many (>200) possible mappings
* Extra element to show if there are many (>200) possible mappings and when non-priority mappings are hidden
*
*/
onManyElements?: BaseUIElement
onManyElementsValue?: UIEventSource<T[]>
selectIfSingle?: false | boolean
searchAreaClass?: string
hideSearchBar?: false | boolean
}
) {
const search = new TextField({ value: options?.searchValue })
const search = new TextField({value: options?.searchValue})
const searchBar = options?.hideSearchBar
? undefined
: new Combine([
Svg.search_svg().SetClass("w-8 normal-background"),
search.SetClass("w-full"),
]).SetClass("flex items-center border-2 border-black m-2")
Svg.search_svg().SetClass("w-8 normal-background"),
search.SetClass("w-full"),
]).SetClass("flex items-center border-2 border-black m-2")
const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
const mode = options?.mode ?? "select-one"
const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
const forceHide = new UIEventSource(false)
const mappedValues: {
show: SelfHidingToggle
mainTerm: Record<string, string>
@ -209,6 +238,7 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
searchTerms: v.searchTerms,
selected: vIsSelected,
squared: mode === "select-many",
hide: v.hasPriority === undefined ? forceHide : forceHide.map(fh => fh && !v.hasPriority?.data, [v.hasPriority])
})
return {
@ -217,62 +247,18 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
}
})
// The total number of elements that would be displayed based on the search criteria alone
let totalShown: Store<number>
if (options.selectIfSingle) {
let forcedSelection: { value: T; show: SelfHidingToggle } = undefined
totalShown = searchValue.map(
(_) => {
let totalShown = 0
let lastShownValue: { value: T; show: SelfHidingToggle }
for (const mv of mappedValues) {
const valueIsShown = mv.show.isShown.data
if (valueIsShown) {
totalShown++
lastShownValue = mv
}
}
if (totalShown == 1) {
if (selectedElements.data?.indexOf(lastShownValue.value) < 0) {
selectedElements.setData([lastShownValue.value])
lastShownValue.show.forceSelected.setData(true)
forcedSelection = lastShownValue
}
} else if (forcedSelection != undefined) {
forcedSelection?.show?.forceSelected?.setData(false)
forcedSelection = undefined
selectedElements.setData([])
}
return totalShown
},
mappedValues.map((mv) => mv.show.GetValue())
)
} else {
totalShown = searchValue.map(
(_) => mappedValues.filter((mv) => mv.show.isShown.data).length,
mappedValues.map((mv) => mv.show.GetValue())
)
}
const tooMuchElementsCutoff = 200
options?.onManyElementsValue?.map(
(value) => {
console.log("Installing toMuchElementsValue", value)
if (tooMuchElementsCutoff <= totalShown.data) {
selectedElements.setData(value)
selectedElements.ping()
}
},
[totalShown]
)
totalShown = searchValue.map((_) => mappedValues.filter((mv) => mv.show.matchesSearchCriteria.data).length)
const tooMuchElementsCutoff = 40
totalShown.addCallbackAndRunD(shown => forceHide.setData(tooMuchElementsCutoff < shown))
super([
searchBar,
new VariableUiElement(
Locale.language.map(
(lng) => {
if (totalShown.data >= 200) {
return options?.onManyElements ?? Translations.t.general.useSearch
}
if (
options?.onNoSearchMade !== undefined &&
(searchValue.data === undefined || searchValue.data.length === 0)
@ -284,9 +270,14 @@ export class SearchablePillsSelector<T> extends Combine implements InputElement<
}
mappedValues.sort((a, b) => (a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1))
return new Combine(mappedValues.map((e) => e.show))
let pills = new Combine(mappedValues.map((e) => e.show))
.SetClass("flex flex-wrap w-full content-start")
.SetClass(options?.searchAreaClass ?? "")
if (totalShown.data >= tooMuchElementsCutoff) {
pills = new Combine([options?.onManyElements ?? Translations.t.general.useSearch, pills])
}
return pills
},
[totalShown, searchValue]
)

View file

@ -42,6 +42,9 @@ export default class LanguagePicker extends Toggle {
return new Translation({ "*": nativeText })
}
for (const key in trans) {
if(key.startsWith("_")){
continue
}
const translationInKey = allTranslations[lang][key]
if (nativeText.toLowerCase() === translationInKey.toLowerCase()) {
translation[key] = nativeText

View file

@ -0,0 +1,119 @@
import Translations from "../i18n/Translations";
import {TextField} from "../Input/TextField";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import NoteCommentElement from "./NoteCommentElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import {LoginToggle} from "./LoginButton";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import {SpecialVisualization} from "../SpecialVisualization";
export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment"
docs = "A textfield to add a comment to a node (with the option to close the note)."
args = [
{
name: "Id-key",
doc: "The property name where the ID of the note to close can be found",
defaultValue: "id",
},
]
public constr(state, tags, args) {
const t = Translations.t.notes
const textField = new TextField({
placeholder: t.addCommentPlaceholder,
inputStyle: "width: 100%; height: 6rem;",
textAreaRows: 3,
htmlType: "area",
})
textField.SetClass("rounded-l border border-grey")
const txt = textField.GetValue()
const addCommentButton = new SubtleButton(
Svg.speech_bubble_svg().SetClass("max-h-7"),
t.addCommentPlaceholder
).onClick(async () => {
const id = tags.data[args[1] ?? "id"]
if ((txt.data ?? "") == "") {
return
}
if (isClosed.data) {
await state.osmConnection.reopenNote(id, txt.data)
await state.osmConnection.closeNote(id)
} else {
await state.osmConnection.addCommentToNote(id, txt.data)
}
NoteCommentElement.addCommentTo(txt.data, tags, state)
txt.setData("")
})
const close = new SubtleButton(
Svg.resolved_svg().SetClass("max-h-7"),
new VariableUiElement(
txt.map((txt) => {
if (txt === undefined || txt === "") {
return t.closeNote
}
return t.addCommentAndClose
})
)
).onClick(() => {
const id = tags.data[args[1] ?? "id"]
state.osmConnection.closeNote(id, txt.data).then((_) => {
tags.data["closed_at"] = new Date().toISOString()
tags.ping()
})
})
const reopen = new SubtleButton(
Svg.note_svg().SetClass("max-h-7"),
new VariableUiElement(
txt.map((txt) => {
if (txt === undefined || txt === "") {
return t.reopenNote
}
return t.reopenNoteAndComment
})
)
).onClick(() => {
const id = tags.data[args[1] ?? "id"]
state.osmConnection.reopenNote(id, txt.data).then((_) => {
tags.data["closed_at"] = undefined
tags.ping()
})
})
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
const stateButtons = new Toggle(
new Toggle(reopen, close, isClosed),
undefined,
state.osmConnection.isLoggedIn
)
return new LoginToggle(
new Combine([
new Title(t.addAComment),
textField,
new Combine([
stateButtons.SetClass("sm:mr-2"),
new Toggle(
addCommentButton,
new Combine([t.typeText]).SetClass(
"flex items-center h-full subtle"
),
textField
.GetValue()
.map((t) => t !== undefined && t.length >= 1)
).SetClass("sm:mr-2"),
]).SetClass("sm:flex sm:justify-between sm:items-stretch"),
]).SetClass("border-2 border-black rounded-xl p-4 block"),
t.loginToAddComment,
state
)
}
}

View file

@ -0,0 +1,45 @@
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
import {Store} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import * as all_languages from "../../assets/language_translations.json";
import {Translation} from "../i18n/Translation";
export class AllLanguagesSelector extends SearchablePillsSelector <string> {
constructor(options?: {
mode?: "select-many" | "select-one"
currentCountry?: Store<string>,
supportedLanguages?: Record<string, string> & { _meta?: { countries?: string[] } }
}) {
const possibleValues: {
show: BaseUIElement
value: string
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>,
hasPriority?: Store<boolean>
}[] = []
const langs = options?.supportedLanguages ?? all_languages["default"] ?? all_languages
for (const ln in langs) {
let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } = all_languages[ln]
const countries = languageInfo._meta?.countries?.map(c => c.toLowerCase())
languageInfo = {...languageInfo}
delete languageInfo._meta
const term = {
show: new Translation(languageInfo),
value: ln,
mainTerm: languageInfo,
searchTerms: {"*": [ln]},
hasPriority: countries === undefined ? undefined : options?.currentCountry?.map(country => countries?.indexOf(country.toLowerCase()) >= 0)
}
possibleValues.push(term)
}
super(possibleValues,
{
mode: options?.mode ?? 'select-many'
});
}
}

View file

@ -1,4 +1,3 @@
import { SpecialVisualization } from "../SpecialVisualizations"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import BaseUIElement from "../BaseUIElement"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
@ -24,6 +23,7 @@ import FilteredLayer from "../../Models/FilteredLayer"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Lazy from "../Base/Lazy"
import List from "../Base/List"
import {SpecialVisualization} from "../SpecialVisualization";
export interface AutoAction extends SpecialVisualization {
supportsAutoAction: boolean

View file

@ -0,0 +1,96 @@
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import BaseUIElement from "../BaseUIElement";
import Translations from "../i18n/Translations";
import {Utils} from "../../Utils";
import Svg from "../../Svg";
import Img from "../Base/Img";
import {SubtleButton} from "../Base/SubtleButton";
import Toggle from "../Input/Toggle";
import {LoginToggle} from "./LoginButton";
import {SpecialVisualization} from "../SpecialVisualization";
export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note"
public readonly docs =
"Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text."
public readonly args = [
{
name: "text",
doc: "Text to show on this button",
required: true,
},
{
name: "icon",
doc: "Icon to show",
defaultValue: "checkmark.svg",
},
{
name: "idkey",
doc: "The property name where the ID of the note to close can be found",
defaultValue: "id",
},
{
name: "comment",
doc: "Text to add onto the note when closing",
},
{
name: "minZoom",
doc: "If set, only show the closenote button if zoomed in enough",
},
{
name: "zoomButton",
doc: "Text to show if not zoomed in enough",
},
]
public constr(state: FeaturePipelineState, tags, args): BaseUIElement {
const t = Translations.t.notes
const params: {
text: string
icon: string
idkey: string
comment: string
minZoom: string
zoomButton: string
} = Utils.ParseVisArgs(this.args, args)
let icon = Svg.checkmark_svg()
if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") {
icon = new Img(args[1])
}
let textToShow = t.closeNote
if ((params.text ?? "") !== "") {
textToShow = Translations.T(args[0])
}
let closeButton: BaseUIElement = new SubtleButton(icon, textToShow)
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
closeButton.onClick(() => {
const id = tags.data[args[2] ?? "id"]
state.osmConnection.closeNote(id, args[3])?.then((_) => {
tags.data["closed_at"] = new Date().toISOString()
tags.ping()
})
})
if ((params.minZoom ?? "") !== "" && !isNaN(Number(params.minZoom))) {
closeButton = new Toggle(
closeButton,
params.zoomButton ?? "",
state.locationControl.map((l) => l.zoom >= Number(params.minZoom))
)
}
return new LoginToggle(
new Toggle(
t.isClosed.SetClass("thanks"),
closeButton,
isClosed
),
t.loginToClose,
state
)
}
}

View file

@ -10,7 +10,7 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Unit } from "../../Models/Unit"
import Lazy from "../Base/Lazy"
import { FixedUiElement } from "../Base/FixedUiElement"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import {EditButton} from "./SaveButton";
export default class EditableTagRendering extends Toggle {
constructor(
@ -57,7 +57,7 @@ export default class EditableTagRendering extends Toggle {
}
private static CreateRendering(
state: FeaturePipelineState,
state: any /*FeaturePipelineState*/,
tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit[],
@ -71,16 +71,9 @@ export default class EditableTagRendering extends Toggle {
// We have a question and editing is enabled
const answerWithEditButton = new Combine([
answer,
new Toggle(
new Combine([Svg.pencil_ui()])
.SetClass("block relative h-10 w-10 p-2 float-right")
.SetStyle("border: 1px solid black; border-radius: 0.7em")
.onClick(() => {
editMode.setData(true)
}),
undefined,
state.osmConnection.isLoggedIn
),
new EditButton(state.osmConnection,() => {
editMode.setData(true)
}),
]).SetClass("flex justify-between w-full")
const question = new Lazy(

View file

@ -0,0 +1,41 @@
import Translations from "../i18n/Translations";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import {GeoOperations} from "../../Logic/GeoOperations";
import {Utils} from "../../Utils";
import {SpecialVisualization} from "../SpecialVisualization";
export class ExportAsGpxViz implements SpecialVisualization {
funcName = "export_as_gpx"
docs = "Exports the selected feature as GPX-file"
args = []
constr(state, tagSource) {
const t = Translations.t.general.download
return new SubtleButton(
Svg.download_ui(),
new Combine([
t.downloadFeatureAsGpx.SetClass("font-bold text-lg"),
t.downloadGpxHelper.SetClass("subtle"),
]).SetClass("flex flex-col")
).onClick(() => {
console.log("Exporting as GPX!")
const tags = tagSource.data
const feature = state.allElements.ContainingFeatures.get(tags.id)
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags)
const gpx = GeoOperations.AsGpx(feature, matchingLayer)
const title =
matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ??
"gpx_track"
Utils.offerContentsAsDownloadableFile(
gpx,
title + "_mapcomplete_export.gpx",
{
mimetype: "{gpx=application/gpx+xml}",
}
)
})
}
}

75
UI/Popup/HistogramViz.ts Normal file
View file

@ -0,0 +1,75 @@
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {FixedUiElement} from "../Base/FixedUiElement";
// import Histogram from "../BigComponents/Histogram";
// import {SpecialVisualization} from "../SpecialVisualization";
export class HistogramViz {
funcName = "histogram"
docs = "Create a histogram for a list of given values, read from the properties."
example =
"`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram"
args = [
{
name: "key",
doc: "The key to be read and to generate a histogram from",
required: true,
},
{
name: "title",
doc: "This text will be placed above the texts (in the first column of the visulasition)",
defaultValue: "",
},
{
name: "countHeader",
doc: "This text will be placed above the bars",
defaultValue: "",
},
{
name: "colors*",
doc: "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`",
},
];
constr(state, tagSource: UIEventSource<any>, args: string[]) {
let assignColors = undefined
if (args.length >= 3) {
const colors = [...args]
colors.splice(0, 3)
const mapping = colors.map((c) => {
const splitted = c.split(":")
const value = splitted.pop()
const regex = splitted.join(":")
return {regex: "^" + regex + "$", color: value}
})
assignColors = (key) => {
for (const kv of mapping) {
if (key.match(kv.regex) !== null) {
return kv.color
}
}
return undefined
}
}
const listSource: Store<string[]> = tagSource.map((tags) => {
try {
const value = tags[args[0]]
if (value === "" || value === undefined) {
return undefined
}
return JSON.parse(value)
} catch (e) {
console.error(
"Could not load histogram: parsing of the list failed: ",
e
)
return undefined
}
})
return new FixedUiElement("HISTORGRAM")
/*
return new Histogram(listSource, args[1], args[2], {
assignColor: assignColors,
})*/
}
}

View file

@ -12,7 +12,6 @@ import Lazy from "../Base/Lazy"
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
import Img from "../Base/Img"
import FilteredLayer from "../../Models/FilteredLayer"
import SpecialVisualizations from "../SpecialVisualizations"
import { FixedUiElement } from "../Base/FixedUiElement"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
@ -45,12 +44,13 @@ import { Changes } from "../../Logic/Osm/Changes"
import { ElementStorage } from "../../Logic/ElementStorage"
import Hash from "../../Logic/Web/Hash"
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
import {SpecialVisualization} from "../SpecialVisualization";
/**
* A helper class for the various import-flows.
* An import-flow always starts with a 'Import this'-button. Upon click, a custom confirmation panel is provided
*/
abstract class AbstractImportButton implements SpecialVisualizations {
abstract class AbstractImportButton implements SpecialVisualization {
protected static importedIds = new Set<string>()
public readonly funcName: string
public readonly docs: string

228
UI/Popup/LanguageElement.ts Normal file
View file

@ -0,0 +1,228 @@
import {SpecialVisualization} from "../SpecialVisualization";
import BaseUIElement from "../BaseUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import {VariableUiElement} from "../Base/VariableUIElement";
import {OsmTags} from "../../Models/OsmFeature";
import * as all_languages from "../../assets/language_translations.json"
import {Translation} from "../i18n/Translation";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import Lazy from "../Base/Lazy";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import List from "../Base/List";
import {AllLanguagesSelector} from "./AllLanguagesSelector";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../../Logic/Tags/And";
import {Tag} from "../../Logic/Tags/Tag";
import {EditButton, SaveButton} from "./SaveButton";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import Toggle from "../Input/Toggle";
import {On} from "../../Models/ThemeConfig/Conversion/Conversion";
export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser"
docs: string | BaseUIElement = "The language element allows to show and pick all known (modern) languages. The key can be set";
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] =
[{
name: "key",
required: true,
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked "
},
{
name: "question",
required: true,
doc: "What to ask if no questions are known"
},
{
name: "render_list_item",
doc: "How a single language will be shown in the list of languages. Use `{language}` to indicate the language (which it must contain).",
defaultValue: "{language()}"
},
{
name: "render_single_language",
doc: "What will be shown if the feature only supports a single language",
required: true
},
{
name: "render_all",
doc: "The full rendering. Use `{list}` to show where the list of languages must come. Optional if mode=single",
defaultValue: "{list()}"
},
{
name: "no_known_languages",
doc: "The text that is shown if no languages are known for this key. If this text is omitted, the languages will be prompted instead"
},
{
name: 'mode',
doc: "If one or many languages can be selected. Should be 'multi' or 'single'",
defaultValue: 'multi'
}
]
;
example: `
\`\`\`json
{"special":
"type": "language_chooser",
"key": "school:language",
"question": {"en": "What are the main (and administrative) languages spoken in this school?"},
"render_single_language": {"en": "{language()} is spoken on this school"},
"render_list_item": {"en": "{language()}"},
"render_all": {"en": "The following languages are spoken here:{list()}"}
"mode":"multi"
}
\`\`\`
`
constr(state: FeaturePipelineState, tagSource: UIEventSource<OsmTags>, argument: string[]): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = argument
if (mode === undefined || mode.length == 0) {
mode = "multi"
}
if (item_render === undefined) {
item_render = "{language()}"
}
if (all_render === undefined || all_render.length == 0) {
all_render = "{list()}"
}
if (mode !== "single" && mode !== "multi") {
throw "Error while calling language_chooser: mode must be either 'single' or 'multi' but it is " + mode
}
if (single_render.indexOf("{language()") < 0 || item_render.indexOf("{language()") < 0) {
throw "Error while calling language_chooser: render_single_language and render_list_item must contain '{language()}'"
}
if (all_render.indexOf("{list()") < 0) {
throw "Error while calling language_chooser: render_all must contain '{list()}'"
}
const prefix = key + ":"
const foundLanguages = tagSource
.map(tags => {
const foundLanguages: string[] = []
for (const k in tags) {
const v = tags[k]
if (v !== "yes") {
continue
}
if (k.startsWith(prefix)) {
foundLanguages.push(k.substring(prefix.length))
}
}
return foundLanguages
})
const forceInputMode = new UIEventSource(false);
const inputEl = new Lazy(() => {
const selector = new AllLanguagesSelector(
{
mode: mode === "single" ? "select-one" : "select-many",
currentCountry: tagSource.map(tgs => tgs["_country"])
}
)
const cancelButton = Toggle.If(forceInputMode,
() => Translations.t.general.cancel
.Clone()
.SetClass("btn btn-secondary").onClick(() => forceInputMode.setData(false)))
const saveButton = new SaveButton(
selector.GetValue().map(lngs => lngs.length > 0 ? "true" : undefined),
state.osmConnection,
).onClick(() => {
const selectedLanguages = selector.GetValue().data
const currentLanguages = foundLanguages.data
const selection: Tag[] = selectedLanguages.map(ln => new Tag(prefix + ln, "yes"));
for (const currentLanguage of currentLanguages) {
if (selectedLanguages.indexOf(currentLanguage) >= 0) {
continue
}
// Erase language that is not spoken anymore
selection.push(new Tag(prefix + currentLanguage, ""))
}
if (state.featureSwitchIsTesting.data) {
for (const tag of selection) {
tagSource.data[tag.key] = tag.value
}
tagSource.ping()
} else {
(state?.changes)
.applyAction(
new ChangeTagAction(tagSource.data.id, new And(selection), tagSource.data, {
theme: state?.layoutToUse?.id ?? "unkown",
changeType: "answer",
})
)
.then((_) => {
console.log("Tagchanges applied")
})
}
forceInputMode.setData(false)
})
return new Combine([new Title(question), selector,
new Combine([cancelButton, saveButton]).SetClass("flex justify-end")
]).SetClass("flex flex-col question disable-links");
})
const editButton = new EditButton(state.osmConnection, () => forceInputMode.setData(true))
return new VariableUiElement(foundLanguages
.map(foundLanguages => {
if (forceInputMode.data) {
return inputEl
}
if (foundLanguages.length === 0) {
// No languages found - we show the question and the input element
if (on_no_known_languages !== undefined && on_no_known_languages.length > 0) {
return new Combine([on_no_known_languages, editButton]).SetClass("flex justify-end")
}
return inputEl
}
let rendered: BaseUIElement;
if (foundLanguages.length === 1) {
const ln = foundLanguages[0]
let mapping = new Map<string, BaseUIElement>();
mapping.set("language", new Translation(all_languages[ln]))
rendered = new SubstitutedTranslation(
new Translation({"*": single_render}, undefined),
tagSource, state, mapping
)
} else {
let mapping = new Map<string, BaseUIElement>();
const languagesList = new List(
foundLanguages.map(ln => {
let mappingLn = new Map<string, BaseUIElement>();
mappingLn.set("language", new Translation(all_languages[ln]))
return new SubstitutedTranslation(
new Translation({"*": item_render}, undefined),
tagSource, state, mappingLn
)
})
);
mapping.set("list", languagesList)
rendered = new SubstitutedTranslation(
new Translation({'*': all_render}, undefined), tagSource,
state, mapping
)
}
return new Combine([rendered, editButton]).SetClass("flex justify-between")
}, [forceInputMode]));
}
}

View file

@ -0,0 +1,33 @@
import {GeoOperations} from "../../Logic/GeoOperations";
import {MapillaryLink} from "../BigComponents/MapillaryLink";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import {SpecialVisualization} from "../SpecialVisualization";
export class MapillaryLinkVis implements SpecialVisualization {
funcName = "mapillary_link"
docs = "Adds a button to open mapillary on the specified location"
args = [
{
name: "zoom",
doc: "The startzoom of mapillary",
defaultValue: "18",
},
]
public constr(state, tagsSource, args) {
const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id)
const [lon, lat] = GeoOperations.centerpointCoordinates(feat)
let zoom = Number(args[0])
if (isNaN(zoom)) {
zoom = 18
}
return new MapillaryLink({
locationControl: new UIEventSource<Loc>({
lat,
lon,
zoom,
}),
})
}
}

99
UI/Popup/MinimapViz.ts Normal file
View file

@ -0,0 +1,99 @@
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import Minimap from "../Base/Minimap";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {SpecialVisualization} from "../SpecialVisualization";
export class MinimapViz implements SpecialVisualization {
funcName = "minimap"
docs = "A small map showing the selected feature."
args = [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
name: "zoomlevel",
defaultValue: "18",
},
{
doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap. (Note: if the key is 'id', list interpration is disabled)",
name: "idKey",
defaultValue: "id",
},
]
example:
"`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`"
constr(state, tagSource, args, _) {
if (state === undefined) {
return undefined
}
const keys = [...args]
keys.splice(0, 1)
const featureStore = state.allElements.ContainingFeatures
const featuresToShow: Store<{ freshness: Date; feature: any }[]> =
tagSource.map((properties) => {
const features: { freshness: Date; feature: any }[] = []
for (const key of keys) {
const value = properties[key]
if (value === undefined || value === null) {
continue
}
let idList = [value]
if (key !== "id" && value.startsWith("[")) {
// This is a list of values
idList = JSON.parse(value)
}
for (const id of idList) {
const feature = featureStore.get(id)
if (feature === undefined) {
console.warn("No feature found for id ", id)
continue
}
features.push({
freshness: new Date(),
feature,
})
}
}
return features
})
const properties = tagSource.data
let zoom = 18
if (args[0]) {
const parsed = Number(args[0])
if (!isNaN(parsed) && parsed > 0 && parsed < 25) {
zoom = parsed
}
}
const locationSource = new UIEventSource<Loc>({
lat: Number(properties._lat),
lon: Number(properties._lon),
zoom: zoom,
})
const minimap = Minimap.createMiniMap({
background: state.backgroundLayer,
location: locationSource,
allowMoving: false,
})
locationSource.addCallback((loc) => {
if (loc.zoom > zoom) {
// We zoom back
locationSource.data.zoom = zoom
locationSource.ping()
}
})
new ShowDataMultiLayer({
leafletMap: minimap["leafletMap"],
zoomToFeatures: true,
layers: state.filteredLayers,
features: new StaticFeatureSource(featuresToShow),
})
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap
}
}

68
UI/Popup/MultiApplyViz.ts Normal file
View file

@ -0,0 +1,68 @@
import {Store} from "../../Logic/UIEventSource";
import MultiApply from "./MultiApply";
import {SpecialVisualization} from "../SpecialVisualization";
export class MultiApplyViz implements SpecialVisualization {
funcName = "multi_apply"
docs = "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags"
args = [
{
name: "feature_ids",
doc: "A JSON-serialized list of IDs of features to apply the tagging on",
},
{
name: "keys",
doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.",
required: true,
},
{ name: "text", doc: "The text to show on the button" },
{
name: "autoapply",
doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown",
required: true,
},
{
name: "overwrite",
doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change",
required: true,
},
]
example =
"{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}"
constr(state, tagsSource, args) {
const featureIdsKey = args[0]
const keysToApply = args[1].split(";")
const text = args[2]
const autoapply = args[3]?.toLowerCase() === "true"
const overwrite = args[4]?.toLowerCase() === "true"
const featureIds: Store<string[]> = tagsSource.map((tags) => {
const ids = tags[featureIdsKey]
try {
if (ids === undefined) {
return []
}
return JSON.parse(ids)
} catch (e) {
console.warn(
"Could not parse ",
ids,
"as JSON to extract IDS which should be shown on the map."
)
return []
}
})
return new MultiApply(
{
featureIds,
keysToApply,
text,
autoapply,
overwrite,
tagsSource,
state
}
);
}
}

147
UI/Popup/NearbyImageVis.ts Normal file
View file

@ -0,0 +1,147 @@
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import {UIEventSource} from "../../Logic/UIEventSource";
import {DefaultGuiState} from "../DefaultGuiState";
import BaseUIElement from "../BaseUIElement";
import Translations from "../i18n/Translations";
import {GeoOperations} from "../../Logic/GeoOperations";
import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage} from "./NearbyImages";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {Tag} from "../../Logic/Tags/Tag";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../../Logic/Tags/And";
import {SaveButton} from "./SaveButton";
import Lazy from "../Base/Lazy";
import {CheckBox} from "../Input/Checkboxes";
import Slider from "../Input/Slider";
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import Title from "../Base/Title";
import {MapillaryLinkVis} from "./MapillaryLinkVis";
import {SpecialVisualization} from "../SpecialVisualization";
export class NearbyImageVis implements SpecialVisualization {
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
{
name: "mode",
defaultValue: "expandable",
doc: "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them.",
},
{
name: "mapillary",
defaultValue: "true",
doc: "If 'true', includes a link to mapillary on this location.",
},
]
docs =
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images"
constr(
state: FeaturePipelineState,
tagSource: UIEventSource<any>,
args: string[],
guistate: DefaultGuiState
): BaseUIElement {
const t = Translations.t.image.nearbyPictures
const mode: "open" | "expandable" | "collapsable" = <any>args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
const id: string = tagSource.data["id"]
const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+")
const selectedImage = new UIEventSource<P4CPicture>(undefined)
let saveButton: BaseUIElement = undefined
if (canBeEdited) {
const confirmText: BaseUIElement = new SubstitutedTranslation(
t.confirm,
tagSource,
state
)
const onSave = async () => {
console.log("Selected a picture...", selectedImage.data)
const osmTags = selectedImage.data.osmTags
const tags: Tag[] = []
for (const key in osmTags) {
tags.push(new Tag(key, osmTags[key]))
}
await state?.changes?.applyAction(
new ChangeTagAction(id, new And(tags), tagSource.data, {
theme: state?.layoutToUse.id,
changeType: "link-image",
})
)
}
saveButton = new SaveButton(
selectedImage,
state.osmConnection,
confirmText,
t.noImageSelected
)
.onClick(onSave)
.SetClass("flex justify-end")
}
const nearby = new Lazy(() => {
const towardsCenter = new CheckBox(t.onlyTowards, false)
const radiusValue =
state?.osmConnection?.GetPreference("nearby-images-radius", "300").sync(
(s) => Number(s),
[],
(i) => "" + i
) ?? new UIEventSource(300)
const radius = new Slider(25, 500, {
value: radiusValue,
step: 25,
})
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
const options: NearbyImageOptions & { value } = {
lon,
lat,
searchRadius: 500,
shownRadius: radius.GetValue(),
value: selectedImage,
blacklist: alreadyInTheImage,
towardscenter: towardsCenter.GetValue(),
maxDaysOld: 365 * 5,
}
const slideshow = canBeEdited
? new SelectOneNearbyImage(options, state)
: new NearbyImages(options, state)
const controls = new Combine([
towardsCenter,
new Combine([
new VariableUiElement(
radius.GetValue().map((radius) => t.withinRadius.Subs({radius}))
),
radius,
]).SetClass("flex justify-between"),
]).SetClass("flex flex-col")
return new Combine([
slideshow,
controls,
saveButton,
new MapillaryLinkVis().constr(state, tagSource, []).SetClass("mt-6"),
])
})
let withEdit: BaseUIElement = nearby
if (canBeEdited) {
withEdit = new Combine([t.hasMatchingPicture, nearby]).SetClass("flex flex-col")
}
if (mode === "open") {
return withEdit
}
const toggleState = new UIEventSource<boolean>(mode === "collapsable")
return new Toggle(
new Combine([new Title(t.title), withEdit]),
new Title(t.browseNearby).onClick(() => toggleState.setData(true)),
toggleState
)
}
}

View file

@ -0,0 +1,80 @@
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Toggle from "../Input/Toggle";
import Lazy from "../Base/Lazy";
import {ProvidedImage} from "../../Logic/ImageProviders/ImageProvider";
import PlantNetSpeciesSearch from "../BigComponents/PlantNetSpeciesSearch";
import Wikidata from "../../Logic/Web/Wikidata";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../../Logic/Tags/And";
import {Tag} from "../../Logic/Tags/Tag";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
import {SpecialVisualization} from "../SpecialVisualization";
export class PlantNetDetectionViz implements SpecialVisualization {
funcName = "plantnet_detection"
docs = "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). "
args = [
{
name: "image_key",
defaultValue: AllImageProviders.defaultKeys.join(","),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
}
]
public constr(state, tags, args) {
let imagePrefixes: string[] = undefined
if (args.length > 0) {
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
}
const detect = new UIEventSource(false)
const toggle = new Toggle(
new Lazy(() => {
const allProvidedImages: Store<ProvidedImage[]> =
AllImageProviders.LoadImagesFor(tags, imagePrefixes)
const allImages: Store<string[]> = allProvidedImages.map((pi) =>
pi.map((pi) => pi.url)
)
return new PlantNetSpeciesSearch(
allImages,
async (selectedWikidata) => {
selectedWikidata = Wikidata.ExtractKey(selectedWikidata)
const change = new ChangeTagAction(
tags.data.id,
new And([
new Tag("species:wikidata", selectedWikidata),
new Tag("source:species:wikidata", "PlantNet.org AI"),
]),
tags.data,
{
theme: state.layoutToUse.id,
changeType: "plantnet-ai-detection",
}
)
await state.changes.applyAction(change)
}
)
}),
new SubtleButton(
undefined,
"Detect plant species with plantnet.org"
).onClick(() => detect.setData(true)),
detect
)
return new Combine([
toggle,
new Combine([
Svg.plantnet_logo_svg().SetClass(
"w-10 h-10 p-1 mr-1 bg-white rounded-full"
),
Translations.t.plantDetection.poweredByPlantnet,
]).SetClass("flex p-2 bg-gray-200 rounded-xl self-end"),
]).SetClass("flex flex-col")
}
}

View file

@ -3,6 +3,21 @@ import Translations from "../i18n/Translations"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine";
import Svg from "../../Svg";
export class EditButton extends Toggle {
constructor(osmConnection: OsmConnection, onClick: () => void) {
super(
new Combine([Svg.pencil_ui()])
.SetClass("block relative h-10 w-10 p-2 float-right")
.SetStyle("border: 1px solid black; border-radius: 0.7em")
.onClick(onClick),
undefined,
osmConnection.isLoggedIn
)
}
}
export class SaveButton extends Toggle {
constructor(

56
UI/Popup/ShareLinkViz.ts Normal file
View file

@ -0,0 +1,56 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ShareButton from "../BigComponents/ShareButton";
import Svg from "../../Svg";
import {FixedUiElement} from "../Base/FixedUiElement";
import {SpecialVisualization} from "../SpecialVisualization";
export class ShareLinkViz implements SpecialVisualization {
funcName = "share_link"
docs = "Creates a link that (attempts to) open the native 'share'-screen"
example =
"{share_link()} to share the current page, {share_link(<some_url>)} to share the given url"
args = [
{
name: "url",
doc: "The url to share (default: current URL)",
},
]
public constr(state, tagSource: UIEventSource<any>, args) {
if (window.navigator.share) {
const generateShareData = () => {
const title = state?.layoutToUse?.title?.txt ?? "MapComplete"
let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer(
tagSource?.data
)
let name =
matchingLayer?.title?.GetRenderValue(tagSource.data)?.txt ??
tagSource.data?.name ??
"POI"
if (name) {
name = `${name} (${title})`
} else {
name = title
}
let url = args[0] ?? ""
if (url === "") {
url = window.location.href
}
return {
title: name,
url: url,
text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete",
}
}
return new ShareButton(
Svg.share_svg().SetClass("w-8 h-8"),
generateShareData
)
} else {
return new FixedUiElement("")
}
}
}

55
UI/Popup/SidedMinimap.ts Normal file
View file

@ -0,0 +1,55 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import * as left_right_style_json from "../../assets/layers/left_right_style/left_right_style.json";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {SpecialVisualization} from "../SpecialVisualization";
export class SidedMinimap implements SpecialVisualization {
funcName = "sided_minimap"
docs = "A small map showing _only one side_ the selected feature. *This features requires to have linerenderings with offset* as only linerenderings with a postive or negative offset will be shown. Note: in most cases, this map will be automatically introduced"
args = [
{
doc: "The side to show, either `left` or `right`",
name: "side",
required: true,
},
]
example = "`{sided_minimap(left)}`"
public constr(state, tagSource, args) {
const properties = tagSource.data
const locationSource = new UIEventSource<Loc>({
lat: Number(properties._lat),
lon: Number(properties._lon),
zoom: 18,
})
const minimap = Minimap.createMiniMap({
background: state.backgroundLayer,
location: locationSource,
allowMoving: false,
})
const side = args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id)
const copy = {...feature}
copy.properties = {
id: side,
}
new ShowDataLayer({
leafletMap: minimap["leafletMap"],
zoomToFeatures: true,
layerToShow: new LayerConfig(
left_right_style_json,
"all_known_layers",
true
),
features: StaticFeatureSource.fromGeojson([copy]),
state,
})
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap
}
}

72
UI/Popup/StealViz.ts Normal file
View file

@ -0,0 +1,72 @@
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {VariableUiElement} from "../Base/VariableUIElement";
import BaseUIElement from "../BaseUIElement";
import EditableTagRendering from "./EditableTagRendering";
import Combine from "../Base/Combine";
import {SpecialVisualization} from "../SpecialVisualization";
export class StealViz implements SpecialVisualization {
funcName = "steal"
docs = "Shows a tagRendering from a different object as if this was the object itself"
args = [
{
name: "featureId",
doc: "The key of the attribute which contains the id of the feature from which to use the tags",
required: true,
},
{
name: "tagRenderingId",
doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection",
required: true,
},
]
constr(state, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) {
const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".")
const layer = state.layoutToUse.layers.find((l) => l.id === layerId)
const tagRendering = layer.tagRenderings.find(
(tr) => tr.id === tagRenderingId
)
tagRenderings.push([layer, tagRendering])
}
if (tagRenderings.length === 0) {
throw "Could not create stolen tagrenddering: tagRenderings not found"
}
return new VariableUiElement(
featureTags.map((tags) => {
const featureId = tags[featureIdKey]
if (featureId === undefined) {
return undefined
}
const otherTags = state.allElements.getEventSourceById(featureId)
const elements: BaseUIElement[] = []
for (const [layer, tagRendering] of tagRenderings) {
const el = new EditableTagRendering(
otherTags,
tagRendering,
layer.units,
state,
{}
)
elements.push(el)
}
if (elements.length === 1) {
return elements[0]
}
return new Combine(elements).SetClass("flex flex-col")
})
)
}
getLayerDependencies(args): string[] {
const [_, tagRenderingId] = args
if (tagRenderingId.indexOf(".") < 0) {
throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot"
}
const [layerId, __] = tagRenderingId.split(".")
return [layerId]
}
}

View file

@ -14,8 +14,9 @@ import { Tag } from "../../Logic/Tags/Tag"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
import {SpecialVisualization} from "../SpecialVisualization";
export default class TagApplyButton implements AutoAction {
export default class TagApplyButton implements AutoAction , SpecialVisualization{
public readonly funcName = "tag_apply"
public readonly docs =
"Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" +

View file

@ -105,7 +105,7 @@ export default class TagRenderingQuestion extends Combine {
TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)
)
if (selection) {
;(state?.changes)
(state?.changes)
.applyAction(
new ChangeTagAction(tags.data.id, selection, tags.data, {
theme: state?.layoutToUse?.id ?? "unkown",
@ -288,14 +288,16 @@ export default class TagRenderingQuestion extends Combine {
value: number
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
original: Mapping
original: Mapping,
hasPriority?: Store<boolean>
}[] {
const values: {
show: BaseUIElement
value: number
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
original: Mapping
original: Mapping,
hasPriority?: Store<boolean>
}[] = []
const addIcons = applicableMappings.some((m) => m.icon !== undefined)
for (let i = 0; i < applicableMappings.length; i++) {
@ -317,6 +319,7 @@ export default class TagRenderingQuestion extends Combine {
mainTerm: tr.translations,
searchTerms: mapping.searchTerms,
original: mapping,
hasPriority: tagsSource.map(tags => mapping.priorityIf?.matchesProperties(tags))
})
}
return values
@ -397,7 +400,7 @@ export default class TagRenderingQuestion extends Combine {
const values = TagRenderingQuestion.MappingToPillValue(
applicableMappings,
tagsSource,
state
state,
)
const searchValue: UIEventSource<string> =
@ -411,47 +414,12 @@ export default class TagRenderingQuestion extends Combine {
}
const mode = configuration.multiAnswer ? "select-many" : "select-one"
const tooMuchElementsValue = new UIEventSource<number[]>([])
let priorityPresets: BaseUIElement = undefined
const classes = "h-64 overflow-scroll"
if (applicableMappings.some((m) => m.priorityIf !== undefined)) {
const priorityValues = tagsSource.map((tags) =>
TagRenderingQuestion.MappingToPillValue(
applicableMappings,
tagsSource,
state
).filter((v) => v.original.priorityIf?.matchesProperties(tags))
)
priorityPresets = new VariableUiElement(
priorityValues.map((priority) => {
if (priority.length === 0) {
return Translations.t.general.useSearch
}
return new Combine([
Translations.t.general.useSearchForMore.Subs({
total: applicableMappings.length,
}),
new SearchablePillsSelector(priority, {
selectedElements: tooMuchElementsValue,
hideSearchBar: true,
mode,
}),
])
.SetClass("flex flex-col items-center ")
.SetClass(classes)
})
)
}
const presetSearch = new SearchablePillsSelector<number>(values, {
selectIfSingle: true,
mode,
searchValue,
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
searchAreaClass: classes,
onManyElementsValue: tooMuchElementsValue,
onManyElements: priorityPresets,
searchAreaClass: classes
})
const fallbackTag = searchValue.map((s) => {
if (s === undefined || ff?.key === undefined) {

View file

@ -0,0 +1,45 @@
import {Utils} from "../../Utils";
import {Feature} from "geojson";
import {Point} from "@turf/turf";
import {GeoLocationPointProperties} from "../../Logic/Actors/GeoLocationHandler";
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI";
import {SpecialVisualization} from "../SpecialVisualization";
/**
* Wrapper around 'UploadTraceToOsmUI'
*/
export class UploadToOsmViz implements SpecialVisualization {
funcName = "upload_to_osm"
docs = "Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored."
args = []
constr(state, featureTags, args) {
function getTrace(title: string) {
title = title?.trim()
if (title === undefined || title === "") {
title = "Uploaded with MapComplete"
}
title = Utils.EncodeXmlValue(title)
const userLocations: Feature<Point, GeoLocationPointProperties>[] = state.historicalUserLocations.features.data.map(f => f.feature)
const trackPoints: string[] = []
for (const l of userLocations) {
let trkpt = ` <trkpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
trkpt += ` <time>${l.properties.date}</time>`
if (l.properties.altitude !== null && l.properties.altitude !== undefined) {
trkpt += ` <ele>${l.properties.altitude}</ele>`
}
trkpt += " </trkpt>"
trackPoints.push(trkpt)
}
const header = '<gpx version="1.1" creator="MapComplete track uploader" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
return header + "\n<name>" + title + "</name>\n<trk><trkseg>\n" + trackPoints.join("\n") + "\n</trkseg></trk></gpx>"
}
return new UploadTraceToOsmUI(getTrace, state, {
whenUploaded: async () => {
state.historicalUserLocations.features.setData([])
}
})
}
}

View file

@ -0,0 +1,18 @@
import {UIEventSource} from "../Logic/UIEventSource";
import BaseUIElement from "./BaseUIElement";
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import {DefaultGuiState} from "./DefaultGuiState";
export interface SpecialVisualization {
funcName: string
constr: (
state: FeaturePipelineState,
tagSource: UIEventSource<any>,
argument: string[],
guistate: DefaultGuiState
) => BaseUIElement
docs: string | BaseUIElement
example?: string
args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[]
getLayerDependencies?: (argument: string[]) => string[]
}

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ import { UIEventSource } from "../Logic/UIEventSource"
import { Translation } from "./i18n/Translation"
import Locale from "./i18n/Locale"
import { FixedUiElement } from "./Base/FixedUiElement"
import SpecialVisualizations, { SpecialVisualization } from "./SpecialVisualizations"
import SpecialVisualizations from "./SpecialVisualizations"
import { Utils } from "../Utils"
import { VariableUiElement } from "./Base/VariableUIElement"
import Combine from "./Base/Combine"
@ -10,6 +10,7 @@ import BaseUIElement from "./BaseUIElement"
import { DefaultGuiState } from "./DefaultGuiState"
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import LinkToWeblate from "./Base/LinkToWeblate"
import {SpecialVisualization} from "./SpecialVisualization";
export class SubstitutedTranslation extends VariableUiElement {
public constructor(

View file

@ -16,10 +16,6 @@ export class Translation extends BaseUIElement {
throw `Translation without content (${context})`
}
this.context = translations["_context"] ?? context
if (translations["_context"] !== undefined) {
translations = { ...translations }
delete translations["_context"]
}
if (typeof translations === "string") {
translations = { "*": translations }
}
@ -28,7 +24,7 @@ export class Translation extends BaseUIElement {
if (!translations.hasOwnProperty(translationsKey)) {
continue
}
if (translationsKey === "_context") {
if (translationsKey === "_context" || translationsKey === "_meta") {
continue
}
count++

View file

@ -398,7 +398,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
while (match) {
const key = match[1]
let v = tags === undefined ? undefined : tags[key]
if (v !== undefined) {
if (v !== undefined && v !== null) {
if (v["toISOString"] != undefined) {
// This is a date, probably the timestamp of the object
// @ts-ignore

View file

@ -16,11 +16,11 @@ export default class WikidataUtils {
* @param remapLanguages
*/
public static extractLanguageData(
data: { lang: { value: string }; code: { value: string }; label: { value: string } }[],
data: { lang: { value: string }; code: { value: string }; label: { value: string }; directionalityLabel?: { value?: string } }[],
remapLanguages: Record<string, string>
): Map<string, Map<string, string>> {
): Map<string, { translations: Map<string, string>, directionality?: string[] }> {
console.log("Got " + data.length + " entries")
const perId = new Map<string, Map<string, string>>()
const perId = new Map<string, { translations: Map<string, string>, directionality?: string[] }>()
for (const element of data) {
let id = element.code.value
id = remapLanguages[id] ?? id
@ -28,9 +28,16 @@ export default class WikidataUtils {
labelLang = remapLanguages[labelLang] ?? labelLang
const value = element.label.value
if (!perId.has(id)) {
perId.set(id, new Map<string, string>())
perId.set(id, {translations: new Map<string, string>(), directionality: []})
}
const entry = perId.get(id)
entry.translations.set(labelLang, value)
const dir = element.directionalityLabel?.value
if (dir) {
if(entry.directionality.indexOf(dir) < 0) {
entry.directionality.push(dir)
}
}
perId.get(id).set(labelLang, value)
}
console.log("Got " + perId.size + " languages")

View file

@ -132,6 +132,7 @@
"en"
],
"CN": [
"zh",
"zh"
],
"CO": [
@ -148,6 +149,7 @@
],
"CY": [
"tr",
"el",
"el"
],
"CZ": [
@ -247,6 +249,9 @@
"es",
"pt"
],
"GR": [
"el"
],
"GT": [
"es"
],
@ -453,8 +458,7 @@
"fr"
],
"NG": [
"en",
"yo"
"en"
],
"NI": [
"es"
@ -502,9 +506,7 @@
"en"
],
"PK": [
"ur",
"en",
"ar"
"ur"
],
"PL": [
"pl",
@ -559,7 +561,6 @@
"ar"
],
"SE": [
"sv",
"sv"
],
"SG": [
@ -648,6 +649,9 @@
"en",
"en"
],
"TW": [
"zh"
],
"TZ": [
"en",
"sw"
@ -693,16 +697,16 @@
"ar"
],
"ZA": [
"en",
"zu",
"xh",
"af",
"ve",
"ss",
"tn",
"ts",
"st",
"nr",
"en",
"zu",
"xh"
"nr"
],
"ZM": [
"en"

View file

@ -1,5 +1,6 @@
{
"ca": "català",
"cs": "čeština",
"da": "dansk",
"de": "Deutsch",
"en": "English",

File diff suppressed because it is too large Load diff

View file

@ -136,13 +136,23 @@
},
"induction-loop",
{
"builtin": "wikidata.tactile_writing-braille",
"override": {
"id": "tactile_writing_language",
"render": {
"special": {
"type": "language_chooser",
"key": "tactile_writing:braille:language",
"question": {
"en": "In which languages does this elevator have tactile writing (braille)?"
},
"render_list_item": {
"en": "This elevator has tactile writing in {language():font-bold}"
},
"render_single_language": {
"en": "This elevator has tactile writing in {language():font-bold}"
}
}
}
}}
],
"mapRendering": [
{
@ -218,4 +228,4 @@
]
}
]
}
}

View file

@ -11,7 +11,7 @@
"mapRendering": [
{
"icon": {
"render":"crosshair:var(--catch-detail-color)",
"render": "crosshair:var(--catch-detail-color)",
"mappings": [
{
"if": "speed>2",
@ -22,12 +22,17 @@
"iconSize": "40,40,center",
"rotation": {
"render": "0deg",
"mappings": [{
"if": {
"and":["speed>2","heading!=NaN"]
},
"then": "{heading}deg"
}]
"mappings": [
{
"if": {
"and": [
"speed>2",
"heading!=NaN"
]
},
"then": "{heading}deg"
}
]
},
"location": [
"point",
@ -35,4 +40,4 @@
]
}
]
}
}

View file

@ -1,11 +1,22 @@
{
"id": "gps_location_history",
"description": "Meta layer which contains the previous locations of the user as single points. This is mainly for technical reasons, e.g. to keep match the distance to the modified object",
"minzoom": 0,
"minzoom": 1,
"name": null,
"source": {
"osmTags": "user:location=yes",
"#": "Cache is disabled here as these points are kept seperately",
"maxCacheAge": 0
},
"mapRendering": null
"shownByDefault": false,
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": "square:red",
"iconSize": "5,5,center"
}
]
}

View file

@ -1,6 +1,6 @@
{
"id": "gps_track",
"description": "Meta layer showing the previous locations of the user as single line. Add this to your theme and override the icon to change the appearance of the current location.",
"description": "Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track.",
"minzoom": 0,
"source": {
"osmTags": "id=location_track",
@ -22,6 +22,7 @@
},
"export_as_gpx",
"export_as_geojson",
"{upload_to_osm()}",
"minimap",
{
"id": "delete",

View file

@ -270,4 +270,4 @@
}
}
]
}
}

View file

@ -296,28 +296,31 @@
"phone",
"email",
{
"builtin": "wikidata.school-language",
"override": {
"+mappings": [
{
"if": "school:language=",
"hideInAnswer": true,
"then": {
"en": "The main language of this school is unknown",
"nl": "De voertaal van deze school is niet gekend",
"de": "Die Unterrichtssprache der Schule ist unbekannt",
"fr": "La langue principale de cette école est inconnue"
}
}
],
"id": "school-language",
"render": {
"special": {
"type": "language_chooser",
"key": "language",
"render_all": {
"en": "The following languages are used in this school:{list()}"
},
"render_single_language": {
"en": "{language():font-bold} is the main language of this school"
},
"question": {
"en": "What is the main language of this school?<div class='subtle'>What language is spoken with the students in non-language related courses and with the administration?</div>",
"nl": "Wat is de voertaal van deze school?<div class='subtle'>Welke taal wordt met de studenten gesproken in niet-taal-gerelateerde vakken en met de administratie?</div>",
"de": "Was ist die Hauptsprache dieser Schule?<div class='subtle'>Welche Sprache wird mit den Schülern in den nicht sprachbezogenen Kursen und mit der Verwaltung gesprochen?</div>",
"fr": "Quelle est la langue principale de cette école ?<div class='subtle'>Quelle langue est parlée avec les élèves des cours non linguistiques et avec l'administration ?</div>"
},
"no_known_languages": {
"en": "The main language of this school is unknown",
"nl": "De voertaal van deze school is niet gekend",
"de": "Die Unterrichtssprache der Schule ist unbekannt",
"fr": "La langue principale de cette école est inconnue"
}
}
}
}}
],
"presets": [
{
@ -360,4 +363,4 @@
"width": 1
}
]
}
}

View file

@ -51,6 +51,74 @@
}
]
},
{
"id": "tactile_writing",
"condition": "handrail=yes",
"question": {
"en": "Do these stairs have tactile writing on the handrail?"
},
"mappings": [
{
"if": "tactile_writing=yes",
"then": {
"en": "There is tactile writing on the handrail"
}
},
{
"if": "tactile_writing=no",
"then": {
"en": "There is no tactile writing on the handrail"
}
}
]
},
{
"id": "tactile_writing_language",
"condition": "tactile_writing:braille:language=yes",
"render": {
"special": {
"type": "language_chooser",
"key": "tactile_writing:braille:language",
"question": {
"en": "In which languages is there tactile writing (braille) for navigation? <img src='./assets/layers/stairs/Braille_stairs.jpg' style='height: 300px; width: auto; display: block;' />"
},
"render_list_item": {
"en": "These stairs have tactile writing in {language():font-bold}"
},
"render_single_language": {
"en": "These stairs have tactile writing in {language():font-bold}"
}
}
}
},
{
"id": "conveying",
"mappings": [
{
"if": "conveying=yes",
"then": {
"en": "This is an escalator",
"nl": "Dit is een roltrap"
}
},
{
"if": "conveying=no",
"then": {
"en": "This is not an escalator",
"nl": "Dit is geen roltrap"
}
},
{
"if": "conveying=",
"then": {
"en": "This is not an escalator",
"nl": "Dit is geen roltrap"
},
"hideInAnswer": true
}
]
},
{
"id": "ramp",
"question": {
@ -94,33 +162,6 @@
}
}
]
},
{
"builtin": "wikidata.tactile_writing-braille",
"override": {
"question": {
"en": "In which languages is there tactile writing (braille) for navigation? <img src='./assets/layers/stairs/Braille_stairs.jpg' style='height: 300px; width: auto; display: block;' />"
}
}
},
{
"id": "conveying",
"mappings": [
{
"if": "conveying=yes",
"then": {
"en": "This is an escalator",
"nl": "Dit is een roltrap"
}
},
{
"if": "conveying=no",
"then": {
"en": "This is not an escalator",
"nl": "Dit is geen roltrap"
}
}
]
}
],
"mapRendering": [

File diff suppressed because it is too large Load diff

View file

@ -728,6 +728,10 @@ video {
margin: 0.25rem;
}
.m-2 {
margin: 0.5rem;
}
.m-4 {
margin: 1rem;
}
@ -736,10 +740,6 @@ video {
margin: 1.25rem;
}
.m-2 {
margin: 0.5rem;
}
.m-0\.5 {
margin: 0.125rem;
}
@ -1052,6 +1052,10 @@ video {
width: 2rem;
}
.w-1\/3 {
width: 33.333333%;
}
.w-4 {
width: 1rem;
}
@ -1313,14 +1317,14 @@ video {
border-radius: 9999px;
}
.rounded-3xl {
border-radius: 1.5rem;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-3xl {
border-radius: 1.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
@ -1342,14 +1346,14 @@ video {
border-bottom-left-radius: 0.25rem;
}
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border {
border-width: 1px;
}
.border-4 {
border-width: 4px;
}

View file

@ -11,7 +11,6 @@ import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerI
import { DefaultGuiState } from "./UI/DefaultGuiState"
import { QueryParameters } from "./Logic/Web/QueryParameters"
import DashboardGui from "./UI/DashboardGui"
import StatisticsGUI from "./UI/StatisticsGUI"
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console
MinimapImplementation.initialize()

View file

@ -280,6 +280,32 @@
"skip": "Skip this question",
"skippedQuestions": "Some questions are skipped",
"testing": "Testing - changes won't be saved",
"uploadGpx": {
"choosePermission": "Choose below if your track should be shared:",
"confirm": "Confirm upload",
"intro0": "By uploading your track, OpenStreetMap.org will retain a full copy of the track.",
"intro1": "You will be able to download your track again and to load them into OpenStreetMap editing programs",
"meta": {
"descriptionIntro": "Optionally, you can enter a description of your trace:",
"descriptionPlaceHolder": "Enter a description of your trace",
"intro": "Add a title for your track:",
"title": "Title and description",
"titlePlaceholder": "Enter the title of your trace"
},
"modes": {
"private": {
"docs": "The points of your track will be shared and aggregated among other tracks. The full track will be visible to you and you will be able to load it into other editing programs. OpenStreetMap.org retains a copy of your trace",
"name": "Anonymous"
},
"public": {
"docs": "Your trace will be visible to everyone, both on your user profile and on the list of GPS-traces on openstreetmap.org",
"name": "Public"
}
},
"title": "Upload your track to OpenStreetMap.org",
"uploadFinished": "Your track has been uploaded!",
"uploading": "Uploading your trace..."
},
"useSearch": "Use the search above to see presets",
"useSearchForMore": "Use the search function to search within {total} more values....",
"weekdays": {

View file

@ -45,7 +45,7 @@
"format": "npx prettier --write '**/*.ts'",
"lint": "tslint --project . -c tslint.json '**.ts' ",
"clean:tests": "(find . -type f -name \"*.doctest.ts\" | xargs rm)",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|import_viewer\\|theme\\).html\" | xargs rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs rm) && (ls | grep \".*.webmanifest$\" | grep -v \"manifest.webmanifest\" | xargs rm)",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|import_viewer\\|theme\\).html\" | xargs rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs rm) && (ls | grep \".*.webmanifest$\" | grep -v \"manifest.webmanifest\" | xargs rm)",
"generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot",
"script": "ts-node",
"weblate-merge": "./scripts/automerge-translations.sh",
@ -54,7 +54,7 @@
"weblate-fix-heavy": "git remote rm weblate-layers; git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layers/; git remote update weblate-layers; git merge weblate-layers/master",
"housekeeping": "npm run generate && npm run generate:docs && npm run generate:contributor-list && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* && git commit -m 'Housekeeping...'",
"parseSchools": "ts-node scripts/schools/amendSchoolData.ts",
"steal": "ts-node scripts/thieves/stealLanguages.ts"
"steal": "ts-node scripts/fetchLanguages.ts"
},
"keywords": [
"OpenStreetMap",

View file

@ -14,6 +14,7 @@ npm run generate:editor-layer-index &&
npm run generate &&
npm run generate:translations &&
npm run test &&
npm run generate:layeroverview &&
npm run generate:layouts
if [ $? -ne 0 ]; then

View file

@ -1,5 +1,6 @@
/**
* Fetches all 'modern languages' from wikidata, then exports their names in every language
* Fetches all 'modern languages' from wikidata, then exports their names in every language.
* Some meta-info (e.g. RTL) is exported too
*/
import * as wds from "wikidata-sdk"
@ -21,12 +22,15 @@ async function fetchRegularLanguages() {
console.log("Fetching languages")
const sparql =
"SELECT ?lang ?label ?code \n" +
"SELECT ?lang ?label ?code ?directionalityLabel \n" +
"WHERE \n" +
"{ \n" +
" ?lang wdt:P31 wd:Q1288568. \n" + // language instanceOf (p31) modern language(Q1288568)
" ?lang rdfs:label ?label. \n" +
" ?lang wdt:P424 ?code" + // Wikimedia language code seems to be close to the weblate entries
" ?lang wdt:P282 ?writing_system. \n"+
" ?writing_system wdt:P1406 ?directionality. \n" +
" ?lang wdt:P424 ?code. \n" +// Wikimedia language code seems to be close to the weblate entries
" SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\". } \n" +
"} "
const url = wds.sparqlQuery(sparql)
@ -67,16 +71,19 @@ async function fetchSpecial(id: number, code: string) {
return bindings
}
function getNativeList(langs: Map<string, Map<string, string>>) {
function getNativeList(langs: Map<string, { translations: Map<string, string> }>) {
const native = {}
const keys: string[] = Array.from(langs.keys())
keys.sort()
for (const key of keys) {
const translations: Map<string, string> = langs.get(key)
const translations: Map<string, string> = langs.get(key).translations
if (!LanguageUtils.usedLanguages.has(key)) {
continue
}
native[key] = translations.get(key)
if(native[key] === undefined){
console.log("No native translation found for "+key)
}
}
return native
}
@ -108,33 +115,7 @@ async function getOfficialLanguagesPerCountry(): Promise<Map<string, string[]>>
return lngs
}
async function main(wipeCache = false) {
const cacheFile = "./assets/generated/languages-wd.json"
if (wipeCache || !existsSync(cacheFile)) {
console.log("Refreshing cache")
await fetch(cacheFile)
} else {
console.log("Reusing the cached file")
}
const data = JSON.parse(readFileSync(cacheFile, "UTF8"))
const perId = WikidataUtils.extractLanguageData(data, WikidataUtils.languageRemapping)
const nativeList = getNativeList(perId)
writeFileSync("./assets/language_native.json", JSON.stringify(nativeList, null, " "))
const translations = Utils.MapToObj(perId, (value, key) => {
if (!LanguageUtils.usedLanguages.has(key)) {
return undefined // Remove unused languages
}
return Utils.MapToObj(value, (v, k) => {
if (!LanguageUtils.usedLanguages.has(k)) {
return undefined
}
return v
})
})
writeFileSync("./assets/language_translations.json", JSON.stringify(translations, null, " "))
async function getOfficialLanguagesPerCountryCached(wipeCache: boolean): Promise<Record<string /*Country code*/, string[] /*Language codes*/>>{
let officialLanguages: Record<string, string[]>
const officialLanguagesPath = "./assets/language_in_country.json"
if (existsSync("./assets/languages_in_country.json") && !wipeCache) {
@ -143,37 +124,48 @@ async function main(wipeCache = false) {
officialLanguages = Utils.MapToObj(await getOfficialLanguagesPerCountry(), (t) => t)
writeFileSync(officialLanguagesPath, JSON.stringify(officialLanguages, null, " "))
}
return officialLanguages
}
const perLanguage = Utils.TransposeMap(officialLanguages)
console.log(JSON.stringify(perLanguage, null, " "))
const mappings: { if: string; then: Record<string, string>; hideInAnswer: string }[] = []
for (const language of Object.keys(perLanguage)) {
const countries = Utils.Dedup(perLanguage[language].map((c) => c.toLowerCase()))
mappings.push({
if: "language=" + language,
then: translations[language],
hideInAnswer: "_country=" + countries.join("|"),
async function main(wipeCache = false) {
const cacheFile = "./assets/generated/languages-wd.json"
if (wipeCache || !existsSync(cacheFile)) {
console.log("Refreshing cache")
await fetch(cacheFile)
} else {
console.log("Reusing the cached file")
}
const data = JSON.parse(readFileSync(cacheFile, "UTF8"))
const perId = WikidataUtils.extractLanguageData(data, WikidataUtils.languageRemapping)
const nativeList = getNativeList(perId)
writeFileSync("./assets/language_native.json", JSON.stringify(nativeList, null, " "))
const languagesPerCountry = Utils.TransposeMap(await getOfficialLanguagesPerCountryCached(wipeCache))
const translations = Utils.MapToObj(perId, (value, key) => {
// We keep all language codes in the list...
const translatedForId : Record<string, string | {countries?: string[], dir: string[]}> = Utils.MapToObj(value.translations, (v, k) => {
if (!LanguageUtils.usedLanguages.has(k)) {
// ... but don't keep translations if we don't have a displayed language for them
return undefined
}
return v
})
}
const tagRenderings = <QuestionableTagRenderingConfigJson>{
id: "official-language",
mappings,
question: "What languages are spoken here?",
}
translatedForId["_meta"] = {
countries : Utils.Dedup( languagesPerCountry[key]),
dir: value.directionality
}
return translatedForId
})
writeFileSync("./assets/language_translations.json", JSON.stringify(translations, null, " "))
writeFileSync(
"./assets/layers/language/language.json",
JSON.stringify(
<LayerConfigJson>{
id: "language",
description: "Various tagRenderings to help language tooling",
tagRenderings,
},
null,
" "
)
)
}
const forceRefresh = process.argv[2] === "--force-refresh"

View file

@ -1,6 +1,5 @@
import Combine from "../UI/Base/Combine"
import BaseUIElement from "../UI/BaseUIElement"
import Translations from "../UI/i18n/Translations"
import { existsSync, mkdirSync, writeFileSync } from "fs"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
import TableOfContents from "../UI/Base/TableOfContents"
@ -14,6 +13,9 @@ import QueryParameterDocumentation from "../UI/QueryParameterDocumentation"
import ScriptUtils from "./ScriptUtils"
import List from "../UI/Base/List"
import SharedTagRenderings from "../Customizations/SharedTagRenderings"
import {writeFile} from "fs";
import Translations from "../UI/i18n/Translations";
import * as themeOverview from "../assets/generated/theme_overview.json"
function WriteFile(
filename,
@ -58,7 +60,61 @@ function WriteFile(
writeFileSync(filename, md)
}
/**
* The wikitable is updated as some tools show an overview of apps based on the wiki.
*/
function generateWikipage(){
function generateWikiEntry(layout: { hideFromOverview: boolean, id: string, shortDescription: any }) {
if (layout.hideFromOverview) {
return "";
}
const languagesInDescr = []
for (const shortDescriptionKey in layout.shortDescription) {
languagesInDescr.push(shortDescriptionKey)
}
const languages = languagesInDescr.map(ln => `{{#language:${ln}|en}}`).join(", ")
let auth = "Yes";
return `{{service_item
|name= [https://mapcomplete.osm.be/${layout.id} ${layout.id}]
|region= Worldwide
|lang= ${languages}
|descr= A MapComplete theme: ${Translations.T(layout.shortDescription)
.textFor("en")
.replace("<a href='", "[[")
.replace(/'>.*<\/a>/, "]]")
}
|material= {{yes|[https://mapcomplete.osm.be/ ${auth}]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, ${layout.id}
}}`
}
let wikiPage = "{|class=\"wikitable sortable\"\n" +
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
"|-";
for (const layout of (themeOverview["default"] ?? themeOverview)) {
if (layout.hideFromOverview) {
continue;
}
wikiPage += "\n" + generateWikiEntry(layout);
}
wikiPage += "\n|}"
writeFile("Docs/wikiIndex.txt", wikiPage, (err) => {
if (err !== null) {
console.log("Could not save wikiindex", err);
}
});
}
console.log("Starting documentation generation...")
generateWikipage()
AllKnownLayouts.GenOverviewsForSingleLayer((layer, element, inlineSource) => {
console.log("Exporting ", layer.id)
if (!existsSync("./Docs/Layers")) {

View file

@ -1,127 +0,0 @@
/*
* Uses the languages in and to every translation from wikidata to generate a language question in wikidata/wikidata
* */
import WikidataUtils from "../../Utils/WikidataUtils"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import LanguageUtils from "../../Utils/LanguageUtils"
import * as perCountry from "../../assets/language_in_country.json"
import { Utils } from "../../Utils"
function main() {
const sourcepath = "assets/generated/languages-wd.json"
console.log(`Converting language data file '${sourcepath}' into a tagMapping`)
const languages = WikidataUtils.extractLanguageData(
JSON.parse(readFileSync(sourcepath, "utf8")),
{}
)
const mappings: MappingConfigJson[] = []
const schoolmappings: MappingConfigJson[] = []
const brailemappings: MappingConfigJson[] = []
const countryToLanguage: Record<string, string[]> = perCountry
const officialLanguagesPerCountry = Utils.TransposeMap(countryToLanguage)
languages.forEach((l, code) => {
const then: Record<string, string> = {}
l.forEach((tr, lng) => {
const languageCodeWeblate = WikidataUtils.languageRemapping[lng] ?? lng
if (!LanguageUtils.usedLanguages.has(languageCodeWeblate)) {
return
}
then[languageCodeWeblate] = tr
})
const officialCountries = Utils.Dedup(
officialLanguagesPerCountry[code]?.map((s) => s.toLowerCase()) ?? []
)
const prioritySearch =
officialCountries.length > 0
? "_country~" + officialCountries.map((c) => "((^|;)" + c + "($|;))").join("|")
: undefined
mappings.push(<MappingConfigJson>{
if: "language:" + code + "=yes",
ifnot: "language:" + code + "=",
searchTerms: {
"*": [code],
},
then,
priorityIf: prioritySearch,
})
schoolmappings.push(<MappingConfigJson>{
if: "school:language=" + code,
then,
priorityIf: prioritySearch,
searchTerms: {
"*": [code],
},
})
brailemappings.push(<MappingConfigJson>{
if: "tactile_writing:braille:" + code + "=yes",
ifnot: "tactile_writing:braille:" + code + "=",
searchTerms: {
"*": [code],
},
then,
priorityIf: prioritySearch,
})
})
const wikidataLayer = <LayerConfigJson>{
id: "wikidata",
description: {
en: "Various tagrenderings which are generated from Wikidata. Automatically generated with a script, don't edit manually",
},
"#dont-translate": "*",
source: {
osmTags: "id~*",
},
title: null,
mapRendering: null,
tagRenderings: [
{
id: "language",
// @ts-ignore
description: "Enables to pick *a single* 'language:<lng>=yes' within the mappings",
mappings,
},
{
builtin: "wikidata.language",
override: {
id: "language-multi",
// @ts-ignore
description:
"Enables to pick *multiple* 'language:<lng>=yes' within the mappings",
multiAnswer: true,
},
},
{
id: "school-language",
// @ts-ignore
description: "Enables to pick a single 'school:language=<lng>' within the mappings",
multiAnswer: true,
mappings: schoolmappings,
},
{
id: "tactile_writing-braille",
// @ts-ignore
description:
"Enables to pick *multiple* 'tactile_writing:braille=<lng>' within the mappings",
multiAnswer: true,
mappings: brailemappings,
},
],
}
const dir = "./assets/layers/wikidata/"
if (!existsSync(dir)) {
mkdirSync(dir)
}
const path = dir + "wikidata.json"
writeFileSync(path, JSON.stringify(wikidataLayer, null, " "))
console.log("Written " + path)
}
main()

24
test.ts
View file

@ -0,0 +1,24 @@
import {LanguageElement} from "./UI/Popup/LanguageElement";
import {ImmutableStore, UIEventSource} from "./Logic/UIEventSource";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import Locale from "./UI/i18n/Locale";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
const tgs = new UIEventSource({
"name": "xyz",
"id": "node/1234",
"_country" : "BE",
})
Locale.language.setData("nl")
console.log(tgs)
console.log("Locale", Locale.language)
const conn = new OsmConnection({})
new LanguageElement().constr(<any> {osmConnection: conn, featureSwitchIsTesting: new ImmutableStore(true)}, tgs, [
"language",
"What languages are spoken here?",
"{language()} is spoken here",
"{language()} is the only language spoken here",
"The following languages are spoken here: {list()}"
]).AttachTo("maindiv")
new VariableUiElement(tgs.map(JSON.stringify)).AttachTo("extradiv")

View file

@ -11,6 +11,11 @@ describe("SpecialVisualisations", () => {
"type",
"A special visualisation is not allowed to be named 'type', as this will conflict with the 'special'-blocks"
)
if(special.args === undefined){
throw "The field 'args' is undefined for special visualisation "+special.funcName
}
for (const arg of special.args) {
expect(arg.name).not.eq(
"type",