forked from MapComplete/MapComplete
Merge branch 'develop' into RobinLinde-patch-1
This commit is contained in:
parent
25ab170a2b
commit
b20e887f9a
111 changed files with 2398 additions and 1928 deletions
|
@ -504,4 +504,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -363,5 +363,6 @@
|
|||
"fr": "Un vélo café est un café à destination des cyclistes avec, par exemple, des services tels qu’une pompe, et de nombreuses décorations liées aux vélos, etc.",
|
||||
"cs": "Cyklokavárna je kavárna zaměřená na cyklisty, například se službami, jako je pumpa, se spoustou výzdoby související s jízdními koly, …",
|
||||
"ca": "Un cafè ciclista és un cafè enfocat a ciclistes, per exemple, amb serveis com una manxa, amb molta decoració relacionada amb el ciclisme, …"
|
||||
}
|
||||
},
|
||||
"deletion": true
|
||||
}
|
||||
|
|
|
@ -5294,4 +5294,4 @@
|
|||
},
|
||||
"neededChangesets": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,22 +213,26 @@
|
|||
{
|
||||
"id": "speech_output_available",
|
||||
"question": {
|
||||
"en": "Has this elevator speech output?"
|
||||
"en": "Has this elevator speech output?",
|
||||
"de": "Verfügt der Aufzug über eine Sprachausgabe?"
|
||||
},
|
||||
"questionHint": {
|
||||
"en": "E.g. it announces the current floor"
|
||||
"en": "E.g. it announces the current floor",
|
||||
"de": "Z.B. werden Stockwerke angesagt"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"if": "speech_output=yes",
|
||||
"then": {
|
||||
"en": "This elevator has speech output"
|
||||
"en": "This elevator has speech output",
|
||||
"de": "Der Aufzug verfügt über eine Sprachausgabe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "speech_output=no",
|
||||
"then": {
|
||||
"en": "This elevator does not have speech output"
|
||||
"en": "This elevator does not have speech output",
|
||||
"de": "Der Aufzug verfügt über keine Sprachausgabe"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -386,4 +386,4 @@
|
|||
"accepts_debit_cards",
|
||||
"accepts_credit_cards"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1012,13 +1012,7 @@
|
|||
"options": [
|
||||
{
|
||||
"question": {
|
||||
"en": "Has a vegetarian menu",
|
||||
"de": "Vegetarische Gerichte im Angebot",
|
||||
"es": "Tiene menú vegetariano",
|
||||
"fr": "A un menu végétarien",
|
||||
"nl": "Heeft een vegetarisch menu",
|
||||
"pl": "Ma menu wegetariańskie",
|
||||
"ca": "Té menú vegetarià"
|
||||
"en": "Restaurants and fast food businesses"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1048,13 +1042,8 @@
|
|||
"options": [
|
||||
{
|
||||
"question": {
|
||||
"en": "Has a vegan menu",
|
||||
"nl": "Heeft een veganistisch menu",
|
||||
"de": "Vegane Gerichte im Angebot",
|
||||
"es": "Tiene menú vegano",
|
||||
"fr": "A un menu végétalien",
|
||||
"pl": "Ma menu wegańskie",
|
||||
"ca": "Té menú vegà"
|
||||
"en": "Has a vegetarian menu",
|
||||
"nl": "Heeft een vegetarisch menu"
|
||||
},
|
||||
"osmTags": {
|
||||
"or": [
|
||||
|
@ -1072,13 +1061,13 @@
|
|||
"options": [
|
||||
{
|
||||
"question": {
|
||||
"en": "Has a halal menu",
|
||||
"nl": "Heeft een halal menu",
|
||||
"de": "Halal Gerichte im Angebot",
|
||||
"es": "Tiene menú halah",
|
||||
"fr": "A un menu halal",
|
||||
"da": "Har en halalmenu",
|
||||
"ca": "Té menú halal"
|
||||
"en": "Has a vegan menu",
|
||||
"nl": "Heeft een veganistisch menu",
|
||||
"de": "Vegane Gerichte im Angebot",
|
||||
"es": "Tiene menú vegano",
|
||||
"fr": "A un menu végétalien",
|
||||
"pl": "Ma menu wegańskie",
|
||||
"ca": "Té menú vegà"
|
||||
},
|
||||
"osmTags": {
|
||||
"or": [
|
||||
|
|
|
@ -59,7 +59,8 @@
|
|||
"if": "map_type=topo",
|
||||
"then": {
|
||||
"en": "Topographical map <p class='subtle'>The map contains contour lines. </p>",
|
||||
"de": "Topographische Katte <p class='subtle'> Die Karte enthält Höhenlinien. </p>"
|
||||
"de": "Topographische Katte <p class='subtle'> Die Karte enthält Höhenlinien. </p>",
|
||||
"ca": "Mapa topogràfic <p class='subtle'>El mapa conté línies de contorn. </p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -75,7 +76,8 @@
|
|||
"then": {
|
||||
"en": "This is a schematic map. <p class='subtle'>A sketched map with only important ways and POIs. The angles, distances etc. are merely illustrative, not accurate.</p> ",
|
||||
"de": "Dies ist eine schematische Karte. <p class='subtle'>Eine skizzierte Karte mit nur wichtigen Wegen und POIs. Die Winkel, Entfernungen usw. sind lediglich illustrativ, nicht genau.</p> ",
|
||||
"pl": "To jest mapa schematyczna. <p class='subtle'>Mapa-szkic z tylko ważnymi drogami i POI. Kąty, odległości itp. są tylko ilustratywne, niedokładne.</p> "
|
||||
"pl": "To jest mapa schematyczna. <p class='subtle'>Mapa-szkic z tylko ważnymi drogami i POI. Kąty, odległości itp. są tylko ilustratywne, niedokładne.</p> ",
|
||||
"ca": "Això és un mapa esquemàtic. <p class='subtle'>Un mapa esbossat amb només camins importants i PDI. Els angles, els trajectes etc. són merament il·lustratius, no acurat.</p> "
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -92,7 +94,8 @@
|
|||
"question": {
|
||||
"en": "What is the size of the shown area on the map?",
|
||||
"de": "Was wird von der Fläche abgedeckt?",
|
||||
"pl": "Jaki jest rozmiar obszaru pokazanego na tej mapie?"
|
||||
"pl": "Jaki jest rozmiar obszaru pokazanego na tej mapie?",
|
||||
"ca": "Quina és la mida de l'àrea mostrada en el mapa?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
@ -100,14 +103,16 @@
|
|||
"then": {
|
||||
"en": "A map of the rooms within a building",
|
||||
"de": "Eine Karte der Räume innerhalb eines Gebäudes",
|
||||
"pl": "Plan pomieszczeń w budynku"
|
||||
"pl": "Plan pomieszczeń w budynku",
|
||||
"ca": "Un mapa de les habitacions dins d'un edifici"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "map_size=site",
|
||||
"then": {
|
||||
"en": "A map of special site, like of a historical castle, a park, a campus, a forest, ....",
|
||||
"de": "Örtlichkeit (z.B. Burg)"
|
||||
"de": "Örtlichkeit (z.B. Burg)",
|
||||
"ca": "Un mapa d'un lloc especial, com un castell històric, un parc, un campus, un bosc, …"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -115,7 +120,8 @@
|
|||
"then": {
|
||||
"en": "A map showing the village or town",
|
||||
"de": "Eine Karte, die das Dorf oder die Stadt anzeigt",
|
||||
"pl": "Mapa pokazująca wieś lub niewielkie miasto"
|
||||
"pl": "Mapa pokazująca wieś lub niewielkie miasto",
|
||||
"ca": "Un mapa que mostra el poble o la ciutat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -123,7 +129,8 @@
|
|||
"then": {
|
||||
"en": " A map of a city",
|
||||
"de": "Stadt",
|
||||
"pl": " Mapa miasta"
|
||||
"pl": " Mapa miasta",
|
||||
"ca": " Un mapa d'una ciutat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -131,7 +138,8 @@
|
|||
"then": {
|
||||
"en": "The map of an entire region, showing multiple cities and villages",
|
||||
"de": "Region",
|
||||
"pl": "Mapa całego regionu, pokazująca wiele miast i wsi"
|
||||
"pl": "Mapa całego regionu, pokazująca wiele miast i wsi",
|
||||
"ca": "El mapa d'una regió sencera, mostrant múltiples ciutats i pobles"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"en": "OpenStreetMap notes",
|
||||
"nl": "OpenStreetMap Notes",
|
||||
"de": "OpenStreetMap-Hinweise",
|
||||
"es": "Notas de OpenStreetMap"
|
||||
"es": "Notas de OpenStreetMap",
|
||||
"ca": "Notes d'OpenStreetMap"
|
||||
},
|
||||
"description": "This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)",
|
||||
"source": {
|
||||
|
@ -33,7 +34,8 @@
|
|||
"nl": "Gesloten Note",
|
||||
"de": "Geschlossene Notiz",
|
||||
"es": "Nota cerrada",
|
||||
"pl": "Zamknięta notatka"
|
||||
"pl": "Zamknięta notatka",
|
||||
"ca": "Nota tancada"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -72,7 +74,8 @@
|
|||
"en": "<h3>Nearby images</h3>The pictures below are nearby geotagged images and might be helpful to handle this note.",
|
||||
"de": "<h3>Bilder aus der Nähe</h3>Die folgenden Bilder sind mit Geotags versehene Bilder aus der Nähe und könnten für die Bearbeitung dieser Notiz hilfreich sein.",
|
||||
"es": "<h3>Imágenes cercanas</h3>Las imágenes de debajo son imágenes geoetiquetadas cercanas y pueden ser útiles para encargarse de esta nota.",
|
||||
"nl": "<h3>Afbeeldingen in de buurt</h3>Onderstaande afbeeldingen zijn afbeeldingen met geo-referentie en die in de buurt genomen zijn. Mogelijks zijn ze nuttig om deze kaartnota af te handelen."
|
||||
"nl": "<h3>Afbeeldingen in de buurt</h3>Onderstaande afbeeldingen zijn afbeeldingen met geo-referentie en die in de buurt genomen zijn. Mogelijks zijn ze nuttig om deze kaartnota af te handelen.",
|
||||
"ca": "<h3>Imatges properes</h3>Les imatges de sota són imatges geoetiquetades properes i poden ser útils per a encarregar-se d'aquesta nota."
|
||||
},
|
||||
"special": {
|
||||
"type": "nearby_images",
|
||||
|
@ -86,7 +89,8 @@
|
|||
"en": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Report {_first_user} for spam or inappropriate messages</a>",
|
||||
"nl": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>{_first_user} melden voor spam of ongepaste opmerkingen</a>",
|
||||
"de": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle' {_first_user} für Spam oder unangemessene Nachrichten melden</a>",
|
||||
"es": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Reportar {_first_user}"
|
||||
"es": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Reportar {_first_user}",
|
||||
"ca": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Reporta {_first_user} per spam o missatges inapropiats</a>"
|
||||
},
|
||||
"condition": "_opened_by_anonymous_user=false"
|
||||
},
|
||||
|
@ -96,7 +100,8 @@
|
|||
"en": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Report this note as spam or inappropriate</a>",
|
||||
"nl": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Deze note melden als spam of ongepast</a>",
|
||||
"de": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Notiz als Spam oder unangemessen melden</a>",
|
||||
"es": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Reporta esta nota como spam o inapropiada</a>"
|
||||
"es": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Reporta esta nota como spam o inapropiada</a>",
|
||||
"ca": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Reporta aquesta nota com spam o inapropiada</a>"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -330,7 +335,8 @@
|
|||
"en": "Hide import notes",
|
||||
"nl": "Verberg import Notes",
|
||||
"de": "Importnotizen ausblenden",
|
||||
"es": "Ocultar las nostras de importación"
|
||||
"es": "Ocultar las notas de importación",
|
||||
"ca": "Oculta les notes d'importació"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -339,7 +345,8 @@
|
|||
"en": "Show only import Notes",
|
||||
"nl": "Toon enkel import Notes",
|
||||
"de": "Nur Importnotizen anzeigen",
|
||||
"es": "Solo mostrar las notas de importación"
|
||||
"es": "Solo mostrar las notas de importación",
|
||||
"ca": "Mostrar només les notes d'importació"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
"nl": "Torens om van het uitzicht te genieten",
|
||||
"de": "Türme zur Aussicht auf die umgebende Landschaft",
|
||||
"es": "Torres con vista panorámica",
|
||||
"pl": "Wieże z panoramicznym widokiem"
|
||||
"pl": "Wieże z panoramicznym widokiem",
|
||||
"ca": "Torres amb vista panoràmica"
|
||||
},
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
|
@ -93,7 +94,8 @@
|
|||
"nl": "Deze toren is {height} hoog",
|
||||
"de": "Dieser Turm ist {height} hoch",
|
||||
"es": "Esta torre mide {height}",
|
||||
"pl": "Ta wieża ma wysokość {height}"
|
||||
"pl": "Ta wieża ma wysokość {height}",
|
||||
"ca": "Aquesta torre fa {height}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "height",
|
||||
|
@ -141,14 +143,16 @@
|
|||
"nl": "Hoeveel moet men betalen om deze toren te bezoeken?",
|
||||
"de": "Was kostet der Zugang zu diesem Turm?",
|
||||
"es": "¿Cuánto hay que pagar para entrar en esta torre?",
|
||||
"pl": "Ile kosztuje wstęp na tę wieżę?"
|
||||
"pl": "Ile kosztuje wstęp na tę wieżę?",
|
||||
"ca": "Quant hi ha que pagar per entrar a aquesta torre?"
|
||||
},
|
||||
"render": {
|
||||
"en": "Visiting this tower costs <b>{charge}</b>",
|
||||
"nl": "Deze toren bezoeken kost <b>{charge}</b>",
|
||||
"de": "Der Besuch des Turms kostet <b>{charge}</b>",
|
||||
"es": "Visitar esta torre cuesta <b>{charge}</b>",
|
||||
"pl": "Wizyta na tej wieży kosztuje <b>{charge}</b>"
|
||||
"pl": "Wizyta na tej wieży kosztuje <b>{charge}</b>",
|
||||
"ca": "Visitar aquesta torre costa <b>{charge}</b>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "charge",
|
||||
|
@ -228,8 +232,9 @@
|
|||
"en": "Does this tower have an elevator?",
|
||||
"nl": "Heeft deze toren een lift?",
|
||||
"de": "Hat dieser Turm einen Aufzug?",
|
||||
"es": "¿Tiene ascensor esta torre?",
|
||||
"pl": "Czy ta wieża ma windę?"
|
||||
"es": "¿Esta torre tiene ascensor?",
|
||||
"pl": "Czy ta wieża ma windę?",
|
||||
"ca": "Aquesta torre té ascensor?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
@ -239,7 +244,8 @@
|
|||
"nl": "Deze toren heeft een lift die bezoekers naar de top van de toren brengt",
|
||||
"de": "Dieser Turm verfügt über einen Aufzug, der die Besucher nach oben bringt",
|
||||
"es": "Esta torre tiene un ascensor que lleva a los visitantes a la cima",
|
||||
"pl": "Ta wieża ma windę, która zabiera zwiedzających na górę"
|
||||
"pl": "Ta wieża ma windę, która zabiera zwiedzających na górę",
|
||||
"ca": "Aquesta torre té un ascensor que porta els visitants al cim"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -249,7 +255,8 @@
|
|||
"nl": "Deze toren heeft geen lift",
|
||||
"de": "Dieser Turm hat keinen Aufzug",
|
||||
"es": "Esta torre no tiene ascensor",
|
||||
"pl": "Ta wieża nie ma windy"
|
||||
"pl": "Ta wieża nie ma windy",
|
||||
"ca": "Aquesta torre no té ascensor"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -274,7 +281,8 @@
|
|||
"en": "Maintained by <b>{operator}</b>",
|
||||
"de": "Betrieben von <b>{operator}</b>",
|
||||
"es": "Mantenida por <b>{operator}</b>",
|
||||
"pl": "Obsługiwana przez <b>{operator}</b>"
|
||||
"pl": "Obsługiwana przez <b>{operator}</b>",
|
||||
"ca": "Mantés per {operator}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "operator"
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"de": "Parkplatz",
|
||||
"es": "aparcamiento de coches",
|
||||
"fr": "Lieu de stationnement",
|
||||
"pl": "Parking samochodowy"
|
||||
"pl": "Parking samochodowy",
|
||||
"ca": "Aparcament de cotxes"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
|
@ -55,7 +56,8 @@
|
|||
"en": "This is a parking bay next to a street",
|
||||
"nl": "Dit is een parkeerplek langs een weg",
|
||||
"de": "Dies ist eine Parkbucht neben einer Straße",
|
||||
"fr": "C'est un lieu de stationnement à côté d'une route"
|
||||
"fr": "C'est un lieu de stationnement à côté d'une route",
|
||||
"ca": "Es tracta d'un aparcament al costat d'un carrer"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -76,7 +78,8 @@
|
|||
"nl": "Dit is een bovengrondse parkeergarage met meerdere verdiepingen",
|
||||
"de": "Dies ist ein mehrstöckiges oberirdisches Parkhaus",
|
||||
"fr": "C'est un parking à plusieurs étages",
|
||||
"pl": "To jest wielopiętrowy parking"
|
||||
"pl": "To jest wielopiętrowy parking",
|
||||
"ca": "Es tracta d'un garatge de diverses plantes"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -95,7 +98,8 @@
|
|||
"nl": "Dit is een strook voor parkeren op de weg",
|
||||
"de": "Dies ist eine Fahrspur zum Parken auf der Straße",
|
||||
"fr": "C'est une voie de stationnement sur la route",
|
||||
"pl": "To jest pas do parkowania na jezdni"
|
||||
"pl": "To jest pas do parkowania na jezdni",
|
||||
"ca": "Aquest és un carril per aparcar al carrer"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -122,7 +126,8 @@
|
|||
"en": "This is a parking on a layby",
|
||||
"nl": "Dit is een parkeerplek op een layby",
|
||||
"de": "Hier gibt es Parkmöglichkeiten auf einem kleinen Rastplatz",
|
||||
"fr": "C'est un parking sur une aire de stationnement"
|
||||
"fr": "C'est un parking sur une aire de stationnement",
|
||||
"ca": "Aquest és un aparcament en una zona de descans"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -178,7 +183,8 @@
|
|||
"nl": "Er zijn geen parkeerplaatsen voor gehandicapten",
|
||||
"de": "Es gibt keine barrierefreien Stellplätze",
|
||||
"fr": "Il n'y a pas de places de stationnement pour personnes à mobilité réduite",
|
||||
"pl": "Nie ma tutaj żadnych miejsc parkingowych dla niepełnosprawnych"
|
||||
"pl": "Nie ma tutaj żadnych miejsc parkingowych dla niepełnosprawnych",
|
||||
"ca": "No hi ha places d'aparcament per a minusvàlids"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
},
|
||||
|
|
|
@ -4,13 +4,15 @@
|
|||
"en": "Parking Spaces",
|
||||
"de": "Stellplätze",
|
||||
"nl": "Parkeerplekken",
|
||||
"pl": "Miejsca parkingowe"
|
||||
"pl": "Miejsca parkingowe",
|
||||
"ca": "Places d'aparcament"
|
||||
},
|
||||
"description": {
|
||||
"en": "Layer showing individual parking spaces.",
|
||||
"de": "Ebene mit den einzelnen PKW Stellplätzen.",
|
||||
"nl": "Laag met individuele parkeerplekken.",
|
||||
"pl": "Warstwa pokazująca pojedyncze miejsca parkingowe."
|
||||
"pl": "Warstwa pokazująca pojedyncze miejsca parkingowe.",
|
||||
"ca": "Capa que mostra aparcaments de cotxes individuals."
|
||||
},
|
||||
"minzoom": 19,
|
||||
"source": {
|
||||
|
@ -43,7 +45,8 @@
|
|||
"en": "This is a normal parking space.",
|
||||
"de": "Dies ist ein normaler Stellplatz.",
|
||||
"nl": "Dit is een normale parkeerplek.",
|
||||
"pl": "To jest zwykłe miejsce parkingowe."
|
||||
"pl": "To jest zwykłe miejsce parkingowe.",
|
||||
"ca": "Aquesta és una plaça d'aparcament normal."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -52,7 +55,8 @@
|
|||
"en": "This is a disabled parking space.",
|
||||
"de": "Dies ist ein Behindertenstellplatz.",
|
||||
"nl": "Dit is een gehandicaptenparkeerplaats.",
|
||||
"pl": "To jest miejsce parkingowe dla niepełnosprawnych."
|
||||
"pl": "To jest miejsce parkingowe dla niepełnosprawnych.",
|
||||
"ca": "Aquesta és una plaça d'aparcament per a minusvàlids."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -61,7 +65,8 @@
|
|||
"en": "This is a private parking space.",
|
||||
"de": "Dies ist ein privater Stellplatz.",
|
||||
"nl": "Dit is een privéparkeerplek.",
|
||||
"pl": "To jest prywatne miejsce parkingowe."
|
||||
"pl": "To jest prywatne miejsce parkingowe.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament privada."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -70,7 +75,8 @@
|
|||
"en": "This is parking space reserved for charging vehicles.",
|
||||
"de": "Dies ist ein Stellplatz, der für das Laden von Fahrzeugen reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor het opladen van voertuigen.",
|
||||
"pl": "To miejsce parkingowe jest zarezerwowane dla ładowania pojazdów."
|
||||
"pl": "To miejsce parkingowe jest zarezerwowane dla ładowania pojazdów.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada per a la recàrrega de vehicles."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -79,7 +85,8 @@
|
|||
"en": "This is parking space reserved for deliveries.",
|
||||
"de": "Dies ist ein Stellplatz, der für Lieferfahrzeuge reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor leveringen.",
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla dostaw."
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla dostaw.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada per a repartidors."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -104,7 +111,8 @@
|
|||
"en": "This is parking space reserved for buses.",
|
||||
"de": "Dies ist ein Stellplatz, der für Busse reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor bussen.",
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla busów."
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla busów.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada per a autobusos."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -113,7 +121,8 @@
|
|||
"en": "This is parking space reserved for motorcycles.",
|
||||
"de": "Dies ist ein Stellplatz, der für Motorräder reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor motoren.",
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla motocykli."
|
||||
"pl": "To miejsce parkingowe jest przeznaczone dla motocykli.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada per a motos."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -122,7 +131,8 @@
|
|||
"en": "This is a parking space reserved for parents with children.",
|
||||
"de": "Dies ist ein Stellplatz, der für Eltern mit Kindern reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor ouders met kinderen.",
|
||||
"pl": "To miejsce jest przeznaczone dla rodziców z dziećmi."
|
||||
"pl": "To miejsce jest przeznaczone dla rodziców z dziećmi.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada per a pares amb fills."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -131,7 +141,8 @@
|
|||
"en": "This is a parking space reserved for staff.",
|
||||
"de": "Dies ist ein Stellplatz, der für das Personal reserviert ist.",
|
||||
"nl": "Deze parkeerplek is gereserveerd voor personeel.",
|
||||
"pl": "To jest miejsce parkingowe przeznaczone dla pracowników."
|
||||
"pl": "To jest miejsce parkingowe przeznaczone dla pracowników.",
|
||||
"ca": "Es tracta d'una plaça d'aparcament reservada al personal."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -59,7 +59,8 @@
|
|||
},
|
||||
"then": {
|
||||
"en": "Post partner at {name}",
|
||||
"de": "Postfiliale im {name}"
|
||||
"de": "Postfiliale im {name}",
|
||||
"ca": "Col·laborador postal a {name}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -415,7 +416,8 @@
|
|||
"question": {
|
||||
"en": "Does this post office have an ATM?",
|
||||
"nl": "Heeft dit postkantoor een bankautomaat?",
|
||||
"de": "Verfügt die Postfiliale über einen Geldautomat?"
|
||||
"de": "Verfügt die Postfiliale über einen Geldautomat?",
|
||||
"ca": "Aquesta oficina postal té un caixer automàtic?"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
|
@ -423,7 +425,8 @@
|
|||
"then": {
|
||||
"en": "This post office has an ATM",
|
||||
"nl": "Dit postkantoor heeft een bankautomaat",
|
||||
"de": "Die Postfiliale verfügt über einen Geldautomat"
|
||||
"de": "Die Postfiliale verfügt über einen Geldautomat",
|
||||
"ca": "Aquesta oficina postal té un caixer automàtic"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -431,7 +434,8 @@
|
|||
"then": {
|
||||
"en": "This post office does <b>not</b> have an ATM",
|
||||
"nl": "Dit postkantoor heeft <b>geen</b> bankautomaaat",
|
||||
"de": "Die Postfiliale verfügt <b>nicht</b> über einen Geldautomat"
|
||||
"de": "Die Postfiliale verfügt <b>nicht</b> über einen Geldautomat",
|
||||
"ca": "Aquesta oficina postal <b>no</b> té un caixer automàtic"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -439,7 +443,8 @@
|
|||
"then": {
|
||||
"en": "This post office does have an ATM, but it is mapped as a different icon",
|
||||
"nl": "Dit postkantoor heeft een bankautomaat, maar deze staat apart op de kaart aangeduid",
|
||||
"de": "Die Postfiliale verfügt über einen Geldautomat, der aber bereits separat kartiert ist"
|
||||
"de": "Die Postfiliale verfügt über einen Geldautomat, der aber bereits separat kartiert ist",
|
||||
"ca": "Aquesta oficina postal té un caixer automàtic, però està mapejat com a un element diferent"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -134,4 +134,4 @@
|
|||
"lineCap": "square"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -244,4 +244,4 @@
|
|||
"fr": "Une couche affichant les douches (publiques)",
|
||||
"ca": "Una capa que mostra dutxes (públiques)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
"_d=feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? ''",
|
||||
"_mastodon_candidate_a=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ",
|
||||
"_mastodon_link=(feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName(\"a\")).filter(a => a.getAttribute(\"rel\")?.indexOf('me') >= 0)[0]?.href})(feat) ",
|
||||
"_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a"
|
||||
"_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a",
|
||||
"__current_background:='initial_value'"
|
||||
],
|
||||
"tagRenderings": [
|
||||
{
|
||||
|
@ -103,6 +104,72 @@
|
|||
"*": "{logout()}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "background-layer-readonly",
|
||||
"condition": {
|
||||
"and": [
|
||||
"_theme:backgroundLayer~*",
|
||||
"mapcomplete-preferred-background-layer~*",
|
||||
"_theme:backgroundLayer!:={mapcomplete-preferred-background-layer}"
|
||||
]
|
||||
},
|
||||
"render": {
|
||||
"en": "This thematic map has a predefined background layer set. Your default theme setting does not apply"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "background-layer",
|
||||
"question": {
|
||||
"en": "What background layer should be shown by default?"
|
||||
},
|
||||
"condition": "_theme:backgroundLayer=",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer=",
|
||||
"then": {
|
||||
"en": "Use the default background layer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer=osm",
|
||||
"then": {
|
||||
"en": "Use OpenStreetMap-carto as default layer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer=photo",
|
||||
"then": {
|
||||
"en": "Use aerial imagery as default background"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer=map",
|
||||
"then": {
|
||||
"en": "Use a non-openstreetmap based map as default background"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer:={__current_background}",
|
||||
"then": {
|
||||
"en": "Use the current background layer (<span class='code'>{__current_background}</span>) as default background"
|
||||
},
|
||||
"hideInAnswer": {
|
||||
"or": [
|
||||
"__current_background=",
|
||||
"__current_background=osm",
|
||||
"__current_background=initial_value"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "mapcomplete-preferred-background-layer~*",
|
||||
"then": {
|
||||
"en": "Use background layer <span class='code'>{mapcomplete-preferred-background-layer}</span> as default background"
|
||||
},
|
||||
"hideInAnswer": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "picture-license",
|
||||
"description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants",
|
||||
|
|
9
assets/layers/vending_machine/condom.svg
Normal file
9
assets/layers/vending_machine/condom.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="23.578 0 52.845 100" enable-background="new 23.578 0 52.845 100" xml:space="preserve"><g><g><g><path d="M67.671,100H32.329c-4.826,0-8.751-3.925-8.751-8.752c0-3.762,2.385-6.978,5.723-8.212V26.423
|
||||
c0-8.142,4.747-15.439,12.019-18.792C41.871,3.333,45.554,0,50,0c4.445,0,8.131,3.332,8.681,7.632
|
||||
c7.271,3.352,12.02,10.649,12.02,18.792v56.613c3.337,1.234,5.723,4.451,5.723,8.211C76.423,96.075,72.498,100,67.671,100z
|
||||
M32.329,88.555c-1.485,0-2.694,1.208-2.694,2.693c0,1.486,1.208,2.694,2.694,2.694h35.342c1.485,0,2.693-1.208,2.693-2.694
|
||||
c0-1.485-1.208-2.693-2.693-2.693h-3.029V26.423c0-6.291-4.006-11.866-9.968-13.875l-2.432-0.818l0.41-2.535
|
||||
c0.028-0.178,0.041-0.322,0.041-0.442c0-1.486-1.208-2.695-2.693-2.695s-2.694,1.208-2.694,2.695
|
||||
c0,0.122,0.014,0.265,0.042,0.438l0.414,2.538l-2.437,0.822c-5.962,2.009-9.967,7.584-9.967,13.875v62.131h-3.029V88.555z"></path></g><g><path d="M46.718,68.833c-1.673,0-3.03-1.356-3.03-3.029V49.984c0-1.673,1.357-3.03,3.03-3.03c1.673,0,3.03,1.357,3.03,3.03
|
||||
v15.819C49.747,67.477,48.391,68.833,46.718,68.833z"></path></g><g><path d="M46.718,43.084c-1.673,0-3.03-1.357-3.03-3.03V26.423c0-1.672,1.357-3.029,3.03-3.029c1.673,0,3.03,1.356,3.03,3.029
|
||||
v13.631C49.747,41.728,48.391,43.084,46.718,43.084z"></path></g></g></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
2
assets/layers/vending_machine/condom.svg.license
Normal file
2
assets/layers/vending_machine/condom.svg.license
Normal file
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Jesus Jezzini De Anda
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -1,4 +1,14 @@
|
|||
[
|
||||
{
|
||||
"path": "condom.svg",
|
||||
"license": "CC0-1.0",
|
||||
"authors": [
|
||||
" \tJesus Jezzini De Anda"
|
||||
],
|
||||
"sources": [
|
||||
"https://commons.wikimedia.org/wiki/File:Condom_-_The_Noun_Project.svg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "cow.svg",
|
||||
"license": "CC-BY-4.0",
|
||||
|
|
|
@ -130,7 +130,8 @@
|
|||
"de": "Kondome werden verkauft",
|
||||
"fr": "Vent des préservatifs",
|
||||
"ca": "Es venen preservatius"
|
||||
}
|
||||
},
|
||||
"icon": "./assets/layers/vending_machine/condom.svg"
|
||||
},
|
||||
{
|
||||
"if": "vending=coffee",
|
||||
|
@ -287,6 +288,14 @@
|
|||
"ca": "Es venen bitllets de transport públic"
|
||||
},
|
||||
"icon": "./assets/themes/stations/public_transport_tickets.svg"
|
||||
},
|
||||
{
|
||||
"if": "vending=meat",
|
||||
"then": {
|
||||
"en": "Meat products are being sold",
|
||||
"nl": "Vleesproducten worden verkocht"
|
||||
},
|
||||
"icon": "./assets/layers/id_presets/temaki-meat.svg"
|
||||
}
|
||||
],
|
||||
"multiAnswer": true
|
||||
|
@ -365,7 +374,18 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"level"
|
||||
"level",
|
||||
{
|
||||
"builtin": "phone",
|
||||
"override": {
|
||||
"question": {
|
||||
"en": "What is the phone number of the operator of this vending machine?"
|
||||
},
|
||||
"questionHint": {
|
||||
"en": "This is the number you can call in case of problems with the vending machine"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"calculatedTags": [
|
||||
"_vending_count=feat.properties.vending.split(';').length"
|
||||
|
@ -448,6 +468,10 @@
|
|||
{
|
||||
"if": "vending=flowers",
|
||||
"then": "circle:white;./assets/layers/id_presets/maki-florist.svg"
|
||||
},
|
||||
{
|
||||
"if": "vending=condoms",
|
||||
"then": "circle:white;./assets/layers/vending_machine/condom.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -797,6 +821,24 @@
|
|||
"ca": "Venda de flors"
|
||||
},
|
||||
"osmTags": "vending~i~.*flowers.*"
|
||||
},
|
||||
{
|
||||
"osmTags": "vending~i~.*parking_tickets.*",
|
||||
"question": {
|
||||
"en": "Sale of parking tickets"
|
||||
}
|
||||
},
|
||||
{
|
||||
"osmTags": "vending=elongated_coin",
|
||||
"question": {
|
||||
"en": "Sale of pressed pennies"
|
||||
}
|
||||
},
|
||||
{
|
||||
"osmTags": "vending~i~.*public_transport_tickets.*",
|
||||
"question": {
|
||||
"en": "Sale of public transport tickets"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -172,4 +172,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
"startLat": 52.99238,
|
||||
"startLon": 6.570614,
|
||||
"startZoom": 20,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
{
|
||||
"builtin": "cycleways_and_roads",
|
||||
|
|
|
@ -1607,4 +1607,4 @@
|
|||
]
|
||||
},
|
||||
"credits": "joost schouppe"
|
||||
}
|
||||
}
|
|
@ -57,7 +57,6 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 1.5,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"layers": [
|
||||
"charging_station"
|
||||
]
|
||||
|
|
|
@ -465,4 +465,4 @@
|
|||
"toilet"
|
||||
],
|
||||
"credits": "Christian Neumann <christian@utopicode.de>"
|
||||
}
|
||||
}
|
|
@ -265,6 +265,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"credits": "L'imaginaire"
|
||||
}
|
|
@ -45,7 +45,6 @@
|
|||
"cs": "Mapa, kde můžete prohlížet a upravovat věci související s cyklistickou infrastrukturou. Vytvořeno během #osoc21."
|
||||
},
|
||||
"hideFromOverview": false,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"icon": "./assets/themes/cycle_infra/cycle-infra.svg",
|
||||
"startLat": 51,
|
||||
"startLon": 3.75,
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
"credits": "Originally created during Open Summer of Code by Pieter Fiers, Thibault Declercq, Pierre Barban, Joost Schouppe and Pieter Vander Vennet",
|
||||
"icon": "./assets/themes/cyclofix/logo.svg",
|
||||
"startLat": 0,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/drinking_water/logo.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"eu": "Hezkuntza",
|
||||
"pl": "Edukacja"
|
||||
},
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 0,
|
||||
|
|
|
@ -19,4 +19,4 @@
|
|||
"startLat": 53.0565,
|
||||
"startLon": 8.7492,
|
||||
"startZoom": 11
|
||||
}
|
||||
}
|
|
@ -288,4 +288,4 @@
|
|||
}
|
||||
],
|
||||
"hideFromOverview": false
|
||||
}
|
||||
}
|
|
@ -69,4 +69,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@
|
|||
"layers": [
|
||||
"ghost_bike"
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"clustering": {
|
||||
"maxZoom": 0
|
||||
}
|
||||
|
|
|
@ -773,4 +773,4 @@
|
|||
"overpassMaxZoom": 15,
|
||||
"osmApiTileSize": 17,
|
||||
"credits": "Pieter Vander Vennet"
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@
|
|||
},
|
||||
"icon": "./assets/layers/doctors/doctors.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
},
|
||||
"icon": "./assets/layers/entrance/entrance.svg",
|
||||
"startLat": 51.17181,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.144383,
|
||||
"startZoom": 14,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
"render": {
|
||||
"en": "Change made with <a href='{host}'>{host}</a>",
|
||||
"ca": "Canviat fet amb <a href='{host}'>{host}</a>",
|
||||
"de": "Änderung über <a href='{host}'>{host}</a>",
|
||||
"de": "Geändert über <a href='{host}'>{host}</a>",
|
||||
"fr": "Modification faite avec <a href='{host}'>{host}</a>",
|
||||
"nl": "Wijziging gemaakt met <a href='{host}'>{host}</a>"
|
||||
},
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 5,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
"map"
|
||||
]
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/onwheels/crest.svg",
|
||||
"startLat": 50.86622,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.350103,
|
||||
"startZoom": 17,
|
||||
"widenFactor": 2,
|
||||
|
@ -525,4 +524,4 @@
|
|||
]
|
||||
},
|
||||
"enableDownload": true
|
||||
}
|
||||
}
|
|
@ -41,6 +41,5 @@
|
|||
"layers": [
|
||||
"windturbine"
|
||||
],
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"credits": "Seppe Santens"
|
||||
}
|
|
@ -29,7 +29,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/osm_community_index/osm.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"clustering": false,
|
||||
|
|
|
@ -144,14 +144,7 @@
|
|||
"width": 5
|
||||
}
|
||||
],
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"shop=yes",
|
||||
"dog=yes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"=presets": [],
|
||||
"source": {
|
||||
"=osmTags": {
|
||||
"and": [
|
||||
|
|
|
@ -71,4 +71,4 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -46,7 +46,6 @@
|
|||
"startLon": 9.9937,
|
||||
"startZoom": 13,
|
||||
"widenFactor": 1.5,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"clustering": {
|
||||
"maxZoom": 14,
|
||||
"minNeededElements": 100
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
},
|
||||
"icon": "./assets/themes/rainbow_crossings/logo.svg",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"startZoom": 12,
|
||||
"widenFactor": 1.2,
|
||||
"socialImage": "./assets/themes/speelplekken/social_image.jpg",
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
{
|
||||
"id": "shadow",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 0,
|
||||
"hideFromOverview": true,
|
||||
"defaultBackgroundId": "CartoDB.Positron",
|
||||
"defaultBackgroundId": "maptiler.backdrop",
|
||||
"layers": [
|
||||
{
|
||||
"builtin": "indoors",
|
||||
|
@ -412,4 +412,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -54,8 +54,8 @@
|
|||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 2,
|
||||
"defaultBackgroundId": "osm",
|
||||
"defaultBackgroundId": "maptiler.carto",
|
||||
"layers": [
|
||||
"surveillance_camera"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,4 +239,4 @@
|
|||
"hideFromOverview": true,
|
||||
"enableMoreQuests": false,
|
||||
"enableShareScreen": false
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
"sameAs": "vending_machine"
|
||||
},
|
||||
"minzoom": 18,
|
||||
"=presets": [],
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
|
@ -36,8 +37,7 @@
|
|||
"vending!~(parking_tickets|elongated_coin|public_transport_tickets)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"=presets": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
},
|
||||
"icon": "./assets/layers/walls_and_buildings/walls_and_buildings.png",
|
||||
"startLat": 50.8465573,
|
||||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 2,
|
||||
|
|
|
@ -271,4 +271,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -405,6 +405,9 @@
|
|||
"doDelete": "Esborrar imatge",
|
||||
"dontDelete": "Cancel·lar",
|
||||
"isDeleted": "Esborrada",
|
||||
"nearby": {
|
||||
"seeNearby": "Explora i vincula imatges properes"
|
||||
},
|
||||
"pleaseLogin": "Entrar per pujar una foto",
|
||||
"respectPrivacy": "Respecta la privacitat. No fotografiïs gent o matrícules. No facis servir imatges de Google Maps, Google Streetview o altres fonts amb copyright.",
|
||||
"toBig": "La teva imatge és massa gran ara que medeix {actual_size}. Usa imatges de com a molt {max_size}",
|
||||
|
|
|
@ -405,6 +405,11 @@
|
|||
"doDelete": "Bild entfernen",
|
||||
"dontDelete": "Abbrechen",
|
||||
"isDeleted": "Gelöscht",
|
||||
"nearby": {
|
||||
"link": "Dieses Bild zeigt das Objekt",
|
||||
"seeNearby": "Bilder in der Nähe durchsuchen und verlinken",
|
||||
"title": "Straßenbilder in der Nähe"
|
||||
},
|
||||
"pleaseLogin": "Bitte anmelden, um ein Bild hinzuzufügen",
|
||||
"respectPrivacy": "Bitte respektieren Sie die Privatsphäre. Fotografieren Sie weder Personen noch Nummernschilder. Benutzen Sie keine urheberrechtlich geschützten Quellen wie z.B. Google Maps oder Google Streetview.",
|
||||
"toBig": "Ihr Bild ist mit {actual_size} zu groß. Die maximale Bildgröße ist {max_size}",
|
||||
|
|
|
@ -344,6 +344,8 @@
|
|||
},
|
||||
"useSearch": "Use the search above to see presets",
|
||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
|
||||
"waitingForLocation": "Searching your current location…",
|
||||
"weekdays": {
|
||||
"abbreviations": {
|
||||
"friday": "Fri",
|
||||
|
@ -414,6 +416,22 @@
|
|||
"pleaseLogin": "Please log in to add a picture",
|
||||
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
|
||||
"toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}",
|
||||
"upload": {
|
||||
"failReasons": "You might have lost connection to the internet",
|
||||
"failReasonsAdvanced": "Alternatively, make sure your browser and extensions do not block third-party API's.",
|
||||
"multiple": {
|
||||
"done": "{count} images are successfully uploaded. Thank you!",
|
||||
"partiallyDone": "{count} images are getting uploaded, {done} images are done…",
|
||||
"someFailed": "Sorry, we could not upload {count} images",
|
||||
"uploading": "{count} images are getting uploaded…"
|
||||
},
|
||||
"one": {
|
||||
"done": "Your image was successfully uploaded. Thank you!",
|
||||
"failed": "Sorry, we could not upload your image",
|
||||
"retrying": "Your image is getting uploaded again…",
|
||||
"uploading": "Your image is getting uploaded…"
|
||||
}
|
||||
},
|
||||
"uploadDone": "Your picture has been added. Thanks for helping out!",
|
||||
"uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.",
|
||||
"uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!",
|
||||
|
|
|
@ -279,7 +279,8 @@
|
|||
"public": {
|
||||
"name": "Publique"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Envoyer votre trace sur OpenStreetMap.org"
|
||||
},
|
||||
"weekdays": {
|
||||
"abbreviations": {
|
||||
|
|
|
@ -3940,9 +3940,6 @@
|
|||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Té menú vegetarià"
|
||||
},
|
||||
"1": {
|
||||
"question": "Només negocis de menjar ràpid"
|
||||
},
|
||||
|
@ -3951,17 +3948,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Té menú vegà"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Té menú halal"
|
||||
"question": "Té menú vegà"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4795,6 +4785,36 @@
|
|||
}
|
||||
},
|
||||
"question": "En quines dades es basa aquest mapa?"
|
||||
},
|
||||
"map_size": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Un mapa de les habitacions dins d'un edifici"
|
||||
},
|
||||
"1": {
|
||||
"then": "Un mapa d'un lloc especial, com un castell històric, un parc, un campus, un bosc, …"
|
||||
},
|
||||
"2": {
|
||||
"then": "Un mapa que mostra el poble o la ciutat"
|
||||
},
|
||||
"3": {
|
||||
"then": " Un mapa d'una ciutat"
|
||||
},
|
||||
"4": {
|
||||
"then": "El mapa d'una regió sencera, mostrant múltiples ciutats i pobles"
|
||||
}
|
||||
},
|
||||
"question": "Quina és la mida de l'àrea mostrada en el mapa?"
|
||||
},
|
||||
"map_type": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Mapa topogràfic <p class='subtle'>El mapa conté línies de contorn. </p>"
|
||||
},
|
||||
"2": {
|
||||
"then": "Això és un mapa esquemàtic. <p class='subtle'>Un mapa esbossat amb només camins importants i PDI. Els angles, els trajectes etc. són merament il·lustratius, no acurat.</p> "
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
@ -5061,18 +5081,56 @@
|
|||
}
|
||||
},
|
||||
"note": {
|
||||
"filter": {
|
||||
"10": {
|
||||
"options": {
|
||||
"1": {
|
||||
"question": "Oculta les notes d'importació"
|
||||
},
|
||||
"2": {
|
||||
"question": "Mostrar només les notes d'importació"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Notes d'OpenStreetMap",
|
||||
"tagRenderings": {
|
||||
"nearby-images": {
|
||||
"render": {
|
||||
"before": "<h3>Imatges properes</h3>Les imatges de sota són imatges geoetiquetades properes i poden ser útils per a encarregar-se d'aquesta nota."
|
||||
}
|
||||
},
|
||||
"report-contributor": {
|
||||
"render": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Reporta {_first_user} per spam o missatges inapropiats</a>"
|
||||
},
|
||||
"report-note": {
|
||||
"render": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Reporta aquesta nota com spam o inapropiada</a>"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Nota tancada"
|
||||
}
|
||||
},
|
||||
"render": "Nota"
|
||||
}
|
||||
},
|
||||
"observation_tower": {
|
||||
"description": "Torres amb vista panoràmica",
|
||||
"name": "Torres d'observació",
|
||||
"tagRenderings": {
|
||||
"Fee": {
|
||||
"question": "Quant hi ha que pagar per entrar a aquesta torre?",
|
||||
"render": "Visitar aquesta torre costa <b>{charge}</b>"
|
||||
},
|
||||
"Height": {
|
||||
"question": "Quina és l'alçada d'aquesta torre?"
|
||||
"question": "Quina és l'alçada d'aquesta torre?",
|
||||
"render": "Aquesta torre fa {height}"
|
||||
},
|
||||
"Operator": {
|
||||
"question": "Qui manté aquesta torre?"
|
||||
"question": "Qui manté aquesta torre?",
|
||||
"render": "Mantés per {operator}"
|
||||
},
|
||||
"access": {
|
||||
"mappings": {
|
||||
|
@ -5085,6 +5143,17 @@
|
|||
},
|
||||
"question": "Es pot visitar aquesta torre?"
|
||||
},
|
||||
"elevator": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Aquesta torre té un ascensor que porta els visitants al cim"
|
||||
},
|
||||
"1": {
|
||||
"then": "Aquesta torre no té ascensor"
|
||||
}
|
||||
},
|
||||
"question": "Aquesta torre té ascensor?"
|
||||
},
|
||||
"name": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -5272,6 +5341,9 @@
|
|||
"0": {
|
||||
"then": "Hi ha places d'aparcament per a gent amb mobilitat reduïda, però no es sap quantes"
|
||||
},
|
||||
"1": {
|
||||
"then": "No hi ha places d'aparcament per a minusvàlids"
|
||||
},
|
||||
"2": {
|
||||
"then": "No hi ha places d'aparcament per a persones amb mobilitat reduïda"
|
||||
}
|
||||
|
@ -5284,15 +5356,32 @@
|
|||
"0": {
|
||||
"then": "Aquest és un aparcament en superfície"
|
||||
},
|
||||
"1": {
|
||||
"then": "Es tracta d'un aparcament al costat d'un carrer"
|
||||
},
|
||||
"2": {
|
||||
"then": "Aquest és un aparcament subterrani"
|
||||
},
|
||||
"3": {
|
||||
"then": "Es tracta d'un garatge de diverses plantes"
|
||||
},
|
||||
"5": {
|
||||
"then": "Aquest és un carril per aparcar al carrer"
|
||||
},
|
||||
"8": {
|
||||
"then": "Aquest és un aparcament en una zona de descans"
|
||||
}
|
||||
},
|
||||
"question": "Quin tipus d'aparcament és aquest?"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": "Aparcament de cotxes"
|
||||
}
|
||||
},
|
||||
"parking_spaces": {
|
||||
"description": "Capa que mostra aparcaments de cotxes individuals.",
|
||||
"name": "Places d'aparcament",
|
||||
"tagRenderings": {
|
||||
"capacity": {
|
||||
"mappings": {
|
||||
|
@ -5306,6 +5395,33 @@
|
|||
"mappings": {
|
||||
"0": {
|
||||
"then": "És un lloc normal d'aparcament."
|
||||
},
|
||||
"1": {
|
||||
"then": "Aquesta és una plaça d'aparcament normal."
|
||||
},
|
||||
"2": {
|
||||
"then": "Aquesta és una plaça d'aparcament per a minusvàlids."
|
||||
},
|
||||
"3": {
|
||||
"then": "Es tracta d'una plaça d'aparcament privada."
|
||||
},
|
||||
"4": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada per a la recàrrega de vehicles."
|
||||
},
|
||||
"5": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada per a repartidors."
|
||||
},
|
||||
"8": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada per a autobusos."
|
||||
},
|
||||
"9": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada per a motos."
|
||||
},
|
||||
"10": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada per a pares amb fills."
|
||||
},
|
||||
"11": {
|
||||
"then": "Es tracta d'una plaça d'aparcament reservada al personal."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5512,6 +5628,20 @@
|
|||
}
|
||||
},
|
||||
"tagRenderings": {
|
||||
"has_atm": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Aquesta oficina postal té un caixer automàtic"
|
||||
},
|
||||
"1": {
|
||||
"then": "Aquesta oficina postal <b>no</b> té un caixer automàtic"
|
||||
},
|
||||
"2": {
|
||||
"then": "Aquesta oficina postal té un caixer automàtic, però està mapejat com a un element diferent"
|
||||
}
|
||||
},
|
||||
"question": "Aquesta oficina postal té un caixer automàtic?"
|
||||
},
|
||||
"letter-from": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -5606,6 +5736,11 @@
|
|||
}
|
||||
},
|
||||
"title": {
|
||||
"mappings": {
|
||||
"1": {
|
||||
"then": "Col·laborador postal a {name}"
|
||||
}
|
||||
},
|
||||
"render": "Oficina de correus"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2063,13 +2063,6 @@
|
|||
},
|
||||
"food": {
|
||||
"filter": {
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Har en halalmenu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"options": {
|
||||
"0": {
|
||||
|
|
|
@ -4202,6 +4202,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"speech_output_available": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Der Aufzug verfügt über eine Sprachausgabe"
|
||||
},
|
||||
"1": {
|
||||
"then": "Der Aufzug verfügt über keine Sprachausgabe"
|
||||
}
|
||||
},
|
||||
"question": "Verfügt der Aufzug über eine Sprachausgabe?",
|
||||
"questionHint": "Z.B. werden Stockwerke angesagt"
|
||||
},
|
||||
"tactile_writing_language": {
|
||||
"render": {
|
||||
"special": {
|
||||
|
@ -4839,9 +4851,6 @@
|
|||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Vegetarische Gerichte im Angebot"
|
||||
},
|
||||
"1": {
|
||||
"question": "Nur Fastfood-Geschäfte"
|
||||
},
|
||||
|
@ -4850,17 +4859,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Vegane Gerichte im Angebot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Halal Gerichte im Angebot"
|
||||
"question": "Vegane Gerichte im Angebot"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4892,7 +4892,7 @@
|
|||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Has a vegetarian menu"
|
||||
"question": "Restaurants and fast food businesses"
|
||||
},
|
||||
"1": {
|
||||
"question": "Only fastfood businesses"
|
||||
|
@ -4905,14 +4905,14 @@
|
|||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Has a vegan menu"
|
||||
"question": "Has a vegetarian menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Has a halal menu"
|
||||
"question": "Has a vegan menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8872,7 +8872,7 @@
|
|||
"has_alpr": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "This is a normal camera"
|
||||
"then": "This is a camera without number plate recognition."
|
||||
},
|
||||
"1": {
|
||||
"then": "This is an ALPR (Automatic License Plate Reader)"
|
||||
|
@ -9652,6 +9652,32 @@
|
|||
},
|
||||
"question": "Should questions for unknown data fields appear one-by-one or together?"
|
||||
},
|
||||
"background-layer": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Use the default background layer"
|
||||
},
|
||||
"1": {
|
||||
"then": "Use OpenStreetMap-carto as default layer"
|
||||
},
|
||||
"2": {
|
||||
"then": "Use aerial imagery as default background"
|
||||
},
|
||||
"3": {
|
||||
"then": "Use a non-openstreetmap based map as default background"
|
||||
},
|
||||
"4": {
|
||||
"then": "Use the current background layer (<span class='code'>{__current_background}</span>) as default background"
|
||||
},
|
||||
"5": {
|
||||
"then": "Use background layer <span class='code'>{mapcomplete-preferred-background-layer}</span> as default background"
|
||||
}
|
||||
},
|
||||
"question": "What background layer should be shown by default?"
|
||||
},
|
||||
"background-layer-readonly": {
|
||||
"render": "This thematic map has a predefined background layer set. Your default theme setting does not apply"
|
||||
},
|
||||
"contributor-thanks": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -9870,8 +9896,20 @@
|
|||
"15": {
|
||||
"question": "Sale of potatoes"
|
||||
},
|
||||
"16": {
|
||||
"question": "Sale of meat"
|
||||
},
|
||||
"17": {
|
||||
"question": "Sale of flowers"
|
||||
},
|
||||
"18": {
|
||||
"question": "Sale of parking tickets"
|
||||
},
|
||||
"19": {
|
||||
"question": "Sale of pressed pennies"
|
||||
},
|
||||
"20": {
|
||||
"question": "Sale of public transport tickets"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9904,6 +9942,12 @@
|
|||
"question": "Who operates this vending machine?",
|
||||
"render": "This vending machine is operated by {operator}"
|
||||
},
|
||||
"phone": {
|
||||
"override": {
|
||||
"question": "What is the phone number of the operator of this vending machine?",
|
||||
"questionHint": "This is the number you can call in case of problems with the vending machine"
|
||||
}
|
||||
},
|
||||
"vending": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
@ -9965,6 +10009,9 @@
|
|||
},
|
||||
"19": {
|
||||
"then": "Public transport tickets are sold"
|
||||
},
|
||||
"20": {
|
||||
"then": "Meat products are being sold"
|
||||
}
|
||||
},
|
||||
"question": "What does this vending machine sell?",
|
||||
|
|
|
@ -2679,24 +2679,10 @@
|
|||
},
|
||||
"description": "Una capa mostrando restaurantes y locales de comida rápida (con un renderizado especial para friterías)",
|
||||
"filter": {
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Tiene menú vegetariano"
|
||||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Tiene menú vegano"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Tiene menú halah"
|
||||
"question": "Tiene menú vegano"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3215,7 +3201,7 @@
|
|||
"question": "Todas las notas"
|
||||
},
|
||||
"1": {
|
||||
"question": "Ocultar las nostras de importación"
|
||||
"question": "Ocultar las notas de importación"
|
||||
},
|
||||
"2": {
|
||||
"question": "Solo mostrar las notas de importación"
|
||||
|
@ -3282,7 +3268,7 @@
|
|||
"then": "Esta torre no tiene ascensor"
|
||||
}
|
||||
},
|
||||
"question": "¿Tiene ascensor esta torre?"
|
||||
"question": "¿Esta torre tiene ascensor?"
|
||||
},
|
||||
"name": {
|
||||
"mappings": {
|
||||
|
|
|
@ -3409,9 +3409,6 @@
|
|||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "A un menu végétarien"
|
||||
},
|
||||
"1": {
|
||||
"question": "Seulement les fastfood"
|
||||
},
|
||||
|
@ -3420,17 +3417,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "A un menu végétalien"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "A un menu halal"
|
||||
"question": "A un menu végétalien"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4526,24 +4526,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Heeft een vegetarisch menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Heeft een veganistisch menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Heeft een halal menu"
|
||||
"question": "Heeft een veganistisch menu"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -9153,6 +9146,9 @@
|
|||
},
|
||||
"19": {
|
||||
"then": "Openbaar vervoerkaartjes worden verkocht"
|
||||
},
|
||||
"20": {
|
||||
"then": "Vleesproducten worden verkocht"
|
||||
}
|
||||
},
|
||||
"question": "Wat verkoopt deze verkoopautomaat?",
|
||||
|
|
|
@ -1473,9 +1473,6 @@
|
|||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Ma menu wegetariańskie"
|
||||
},
|
||||
"1": {
|
||||
"question": "Tylko fast-foody"
|
||||
},
|
||||
|
@ -1484,7 +1481,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"4": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Ma menu wegańskie"
|
||||
|
|
|
@ -925,7 +925,7 @@
|
|||
},
|
||||
"host": {
|
||||
"question": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?",
|
||||
"render": "Änderung über <a href='{host}'>{host}</a>"
|
||||
"render": "Geändert über <a href='{host}'>{host}</a>"
|
||||
},
|
||||
"locale": {
|
||||
"question": "In welcher Benutzersprache wurde diese Änderung vorgenommen?",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mapcomplete",
|
||||
"version": "0.33.3",
|
||||
"version": "0.33.4",
|
||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||
"description": "A small website to edit OSM easily",
|
||||
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
||||
|
|
|
@ -858,6 +858,10 @@ video {
|
|||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
@ -886,10 +890,6 @@ video {
|
|||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@ -2662,6 +2662,26 @@ a.link-underline {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@-webkit-keyframes spin {
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.motion-safe\:animate-spin {
|
||||
-webkit-animation: spin 1s linear infinite;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.max-\[480px\]\:w-full {
|
||||
width: 100%;
|
||||
|
|
|
@ -3,10 +3,10 @@ import { writeFileSync } from "fs"
|
|||
import {
|
||||
FixLegacyTheme,
|
||||
UpdateLegacyLayer,
|
||||
} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
|
||||
import Translations from "../UI/i18n/Translations"
|
||||
import { Translation } from "../UI/i18n/Translation"
|
||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
} from "../src/Models/ThemeConfig/Conversion/LegacyJsonConvert"
|
||||
import Translations from "../src/UI/i18n/Translations"
|
||||
import { Translation } from "../src/UI/i18n/Translation"
|
||||
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
||||
/*
|
||||
* This script reads all theme and layer files and reformats them inplace
|
||||
|
|
67
src/Logic/Actors/PreferredRasterLayerSelector.ts
Normal file
67
src/Logic/Actors/PreferredRasterLayerSelector.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource";
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
|
||||
/**
|
||||
* Selects the appropriate raster layer as background for the given query parameter, theme setting, user preference or default value.
|
||||
*
|
||||
* It the requested layer is not available, a layer of the same type will be selected.
|
||||
*/
|
||||
export class PreferredRasterLayerSelector {
|
||||
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>;
|
||||
private readonly _availableLayers: Store<RasterLayerPolygon[]>;
|
||||
private readonly _preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>;
|
||||
private readonly _queryParameter: UIEventSource<string>;
|
||||
|
||||
constructor(rasterLayerSetting: UIEventSource<RasterLayerPolygon>, availableLayers: Store<RasterLayerPolygon[]>, queryParameter: UIEventSource<string>, preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>) {
|
||||
this._rasterLayerSetting = rasterLayerSetting;
|
||||
this._availableLayers = availableLayers;
|
||||
this._queryParameter = queryParameter;
|
||||
this._preferredBackgroundLayer = preferredBackgroundLayer;
|
||||
const self = this;
|
||||
|
||||
this._rasterLayerSetting.addCallbackD(layer => {
|
||||
if (layer.properties.id !== this._queryParameter.data) {
|
||||
this._queryParameter.setData(undefined);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this._queryParameter.addCallbackAndRunD(_ => {
|
||||
const isApplied = self.updateLayer();
|
||||
if (!isApplied) {
|
||||
// A different layer was set as background
|
||||
// We remove this queryParameter instead
|
||||
self._queryParameter.setData(undefined);
|
||||
return true; // Unregister
|
||||
}
|
||||
});
|
||||
|
||||
this._preferredBackgroundLayer.addCallbackD(_ => self.updateLayer());
|
||||
|
||||
this._availableLayers.addCallbackD(_ => self.updateLayer());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'true' if the target layer is set or is the current layer
|
||||
* @private
|
||||
*/
|
||||
private updateLayer() {
|
||||
|
||||
// What is the ID of the layer we have to (try to) load?
|
||||
const targetLayerId = this._queryParameter.data ?? this._preferredBackgroundLayer.data;
|
||||
const available = this._availableLayers.data;
|
||||
const isCategory = targetLayerId === "photo" || targetLayerId === "osmbasedmap" || targetLayerId === "map"
|
||||
const foundLayer = isCategory ? available.find(l => l.properties.category === targetLayerId) : available.find(l => l.properties.id === targetLayerId);
|
||||
if (foundLayer) {
|
||||
this._rasterLayerSetting.setData(foundLayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The current layer is not in view
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@ import { OsmTags } from "../../../Models/OsmFeature"
|
|||
*/
|
||||
export default class FeaturePropertiesStore {
|
||||
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
||||
|
||||
public readonly aliases = new Map<string, string>()
|
||||
constructor(...sources: FeatureSource[]) {
|
||||
for (const source of sources) {
|
||||
this.trackFeatureSource(source)
|
||||
|
@ -92,7 +92,6 @@ export default class FeaturePropertiesStore {
|
|||
})
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public addAlias(oldId: string, newId: string): void {
|
||||
if (newId === undefined) {
|
||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||
|
@ -112,6 +111,7 @@ export default class FeaturePropertiesStore {
|
|||
}
|
||||
element.data.id = newId
|
||||
this._elements.set(newId, element)
|
||||
this.aliases.set(newId, oldId)
|
||||
element.ping()
|
||||
}
|
||||
|
||||
|
|
159
src/Logic/ImageProviders/ImageUploadManager.ts
Normal file
159
src/Logic/ImageProviders/ImageUploadManager.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { ImageUploader } from "./ImageUploader";
|
||||
import LinkImageAction from "../Osm/Actions/LinkImageAction";
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
||||
import { OsmId, OsmTags } from "../../Models/OsmFeature";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import { Store, UIEventSource } from "../UIEventSource";
|
||||
import { OsmConnection } from "../Osm/OsmConnection";
|
||||
import { Changes } from "../Osm/Changes";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import NoteCommentElement from "../../UI/Popup/NoteCommentElement";
|
||||
|
||||
|
||||
/**
|
||||
* The ImageUploadManager has a
|
||||
*/
|
||||
export class ImageUploadManager {
|
||||
|
||||
private readonly _uploader: ImageUploader;
|
||||
private readonly _featureProperties: FeaturePropertiesStore;
|
||||
private readonly _layout: LayoutConfig;
|
||||
|
||||
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _osmConnection: OsmConnection;
|
||||
private readonly _changes: Changes;
|
||||
|
||||
constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) {
|
||||
this._uploader = uploader;
|
||||
this._featureProperties = featureProperties;
|
||||
this._layout = layout;
|
||||
this._osmConnection = osmConnection;
|
||||
this._changes = changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets various counters.
|
||||
* Note that counters can only increase
|
||||
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
|
||||
* @param featureId: the id of the feature you want information for. '*' has a global counter
|
||||
*/
|
||||
public getCountsFor(featureId: string | "*"): {
|
||||
retried: Store<number>;
|
||||
uploadStarted: Store<number>;
|
||||
retrySuccess: Store<number>;
|
||||
failed: Store<number>;
|
||||
uploadFinished: Store<number>
|
||||
} {
|
||||
return {
|
||||
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
|
||||
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
|
||||
retried: this.getCounterFor(this._uploadRetried, featureId),
|
||||
failed: this.getCounterFor(this._uploadFailed, featureId),
|
||||
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given image, applies the correct title and license for the known user.
|
||||
* Will then add this image to the OSM-feature or the OSM-note
|
||||
*/
|
||||
public async uploadImageAndApply(file: File, tagsStore: UIEventSource<OsmTags>) : Promise<void>{
|
||||
|
||||
const sizeInBytes = file.size;
|
||||
const tags= tagsStore.data
|
||||
const featureId = <OsmId>tags.id;
|
||||
const self = this;
|
||||
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
this.increaseCountFor(this._uploadStarted, featureId);
|
||||
this.increaseCountFor(this._uploadFailed, featureId);
|
||||
throw (
|
||||
Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: self._uploader.maxFileSizeInMegabytes + "MB"
|
||||
}).txt
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0");
|
||||
const license = licenseStore?.data ?? "CC0";
|
||||
|
||||
const matchingLayer = this._layout?.getMatchingLayer(tags);
|
||||
|
||||
const title =
|
||||
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ??
|
||||
tags.name ??
|
||||
"https//osm.org/" + tags.id;
|
||||
const description = [
|
||||
"author:" + this._osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id
|
||||
].join("\n");
|
||||
|
||||
console.log("Upload done, creating ");
|
||||
const action = await this.uploadImageWithLicense(featureId, title, description, file);
|
||||
if(!isNaN(Number( featureId))){
|
||||
// THis is a map note
|
||||
const url = action._url
|
||||
await this._osmConnection.addCommentToNote(featureId, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>> tagsStore, {osmConnection: this._osmConnection})
|
||||
return
|
||||
}
|
||||
await this._changes.applyAction(action);
|
||||
}
|
||||
|
||||
private async uploadImageWithLicense(
|
||||
featureId: OsmId,
|
||||
title: string, description: string, blob: File
|
||||
): Promise<LinkImageAction> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId);
|
||||
const properties = this._featureProperties.getStore(featureId);
|
||||
let key: string;
|
||||
let value: string;
|
||||
try {
|
||||
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||
} catch (e) {
|
||||
this.increaseCountFor(this._uploadRetried, featureId);
|
||||
console.error("Could not upload image, trying again:", e);
|
||||
try {
|
||||
|
||||
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||
this.increaseCountFor(this._uploadRetriedSuccess, featureId);
|
||||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e);
|
||||
this.increaseCountFor(this._uploadFailed, featureId);
|
||||
}
|
||||
|
||||
}
|
||||
console.log("Uploading done, creating action for", featureId);
|
||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||
theme: this._layout.id,
|
||||
changeType: "add-image"
|
||||
});
|
||||
this.increaseCountFor(this._uploadFinished, featureId);
|
||||
return action;
|
||||
}
|
||||
|
||||
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
if (this._featureProperties.aliases.has(key)) {
|
||||
key = this._featureProperties.aliases.get(key);
|
||||
}
|
||||
if (!collection.has(key)) {
|
||||
collection.set(key, new UIEventSource<number>(0));
|
||||
}
|
||||
return collection.get(key);
|
||||
}
|
||||
|
||||
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
const counter = this.getCounterFor(collection, key);
|
||||
counter.setData(counter.data + 1);
|
||||
const global = this.getCounterFor(collection, "*");
|
||||
global.setData(counter.data + 1);
|
||||
}
|
||||
|
||||
}
|
15
src/Logic/ImageProviders/ImageUploader.ts
Normal file
15
src/Logic/ImageProviders/ImageUploader.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface ImageUploader {
|
||||
maxFileSizeInMegabytes?: number;
|
||||
/**
|
||||
* Uploads the 'blob' as image, with some metadata.
|
||||
* Returns the URL to be linked + the appropriate key to add this to OSM
|
||||
* @param title
|
||||
* @param description
|
||||
* @param blob
|
||||
*/
|
||||
uploadImage(
|
||||
title: string,
|
||||
description: string,
|
||||
blob: File
|
||||
): Promise<{ key: string, value: string }>;
|
||||
}
|
|
@ -1,60 +1,30 @@
|
|||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import { Utils } from "../../Utils";
|
||||
import Constants from "../../Models/Constants";
|
||||
import { LicenseInfo } from "./LicenseInfo";
|
||||
import { ImageUploader } from "./ImageUploader";
|
||||
|
||||
export class Imgur extends ImageProvider {
|
||||
export class Imgur extends ImageProvider implements ImageUploader{
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
public static readonly singleton = new Imgur()
|
||||
public readonly defaultKeyPrefixes: string[] = ["image"]
|
||||
|
||||
public readonly maxFileSizeInMegabytes = 10
|
||||
private constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
static uploadMultiple(
|
||||
/**
|
||||
* Uploads an image, returns the URL where to find the image
|
||||
* @param title
|
||||
* @param description
|
||||
* @param blob
|
||||
*/
|
||||
public async uploadImage(
|
||||
title: string,
|
||||
description: string,
|
||||
blobs: FileList,
|
||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
||||
allDone: () => void,
|
||||
onFail: (reason: string) => void,
|
||||
offset: number = 0
|
||||
) {
|
||||
if (blobs.length == offset) {
|
||||
allDone()
|
||||
return
|
||||
}
|
||||
const blob = blobs.item(offset)
|
||||
const self = this
|
||||
this.uploadImage(
|
||||
title,
|
||||
description,
|
||||
blob,
|
||||
async (imageUrl) => {
|
||||
await handleSuccessfullUpload(imageUrl)
|
||||
self.uploadMultiple(
|
||||
title,
|
||||
description,
|
||||
blobs,
|
||||
handleSuccessfullUpload,
|
||||
allDone,
|
||||
onFail,
|
||||
offset + 1
|
||||
)
|
||||
},
|
||||
onFail
|
||||
)
|
||||
}
|
||||
|
||||
static uploadImage(
|
||||
title: string,
|
||||
description: string,
|
||||
blob: File,
|
||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
||||
onFail: (reason: string) => void
|
||||
) {
|
||||
blob: File
|
||||
): Promise<{ key: string, value: string }> {
|
||||
const apiUrl = "https://api.imgur.com/3/image"
|
||||
const apiKey = Constants.ImgurApiKey
|
||||
|
||||
|
@ -63,6 +33,7 @@ export class Imgur extends ImageProvider {
|
|||
formData.append("title", title)
|
||||
formData.append("description", description)
|
||||
|
||||
|
||||
const settings: RequestInit = {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
|
@ -74,17 +45,9 @@ export class Imgur extends ImageProvider {
|
|||
}
|
||||
|
||||
// Response contains stringified JSON
|
||||
// Image URL available at response.data.link
|
||||
fetch(apiUrl, settings)
|
||||
.then(async function (response) {
|
||||
const content = await response.json()
|
||||
await handleSuccessfullUpload(content.data.link)
|
||||
})
|
||||
.catch((reason) => {
|
||||
console.log("Uploading to IMGUR failed", reason)
|
||||
// @ts-ignore
|
||||
onFail(reason)
|
||||
})
|
||||
const response = await fetch(apiUrl, settings)
|
||||
const content = await response.json()
|
||||
return { key: "image", value: content.data.link }
|
||||
}
|
||||
|
||||
SourceIcon(): BaseUIElement {
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Imgur } from "./Imgur"
|
||||
|
||||
export default class ImgurUploader {
|
||||
public readonly queue: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public readonly failed: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public readonly success: UIEventSource<string[]> = new UIEventSource<string[]>([])
|
||||
public maxFileSizeInMegabytes = 10
|
||||
private readonly _handleSuccessUrl: (string) => Promise<void>
|
||||
|
||||
constructor(handleSuccessUrl: (string) => Promise<void>) {
|
||||
this._handleSuccessUrl = handleSuccessUrl
|
||||
}
|
||||
|
||||
public uploadMany(title: string, description: string, files: FileList): void {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this.queue.data.push(files.item(i).name)
|
||||
}
|
||||
this.queue.ping()
|
||||
|
||||
const self = this
|
||||
this.queue.setData([...self.queue.data])
|
||||
Imgur.uploadMultiple(
|
||||
title,
|
||||
description,
|
||||
files,
|
||||
async function (url) {
|
||||
console.log("File saved at", url)
|
||||
self.success.data.push(url)
|
||||
self.success.ping()
|
||||
await self._handleSuccessUrl(url)
|
||||
},
|
||||
function () {
|
||||
console.log("All uploads completed")
|
||||
},
|
||||
|
||||
function (failReason) {
|
||||
console.log("Upload failed due to ", failReason)
|
||||
self.failed.setData([...self.failed.data, failReason])
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
54
src/Logic/Osm/Actions/LinkImageAction.ts
Normal file
54
src/Logic/Osm/Actions/LinkImageAction.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import ChangeTagAction from "./ChangeTagAction";
|
||||
import { Tag } from "../../Tags/Tag";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import { Changes } from "../Changes";
|
||||
import { ChangeDescription } from "./ChangeDescription";
|
||||
import { Store } from "../../UIEventSource";
|
||||
|
||||
export default class LinkImageAction extends OsmChangeAction {
|
||||
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string;
|
||||
public readonly _url: string;
|
||||
private readonly _currentTags: Store<Record<string, string>>;
|
||||
private readonly _meta: { theme: string; changeType: "add-image" | "link-image" };
|
||||
|
||||
/**
|
||||
* Adds an image-link to a feature
|
||||
* @param elementId
|
||||
* @param proposedKey a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
|
||||
* @param url
|
||||
* @param currentTags
|
||||
* @param meta
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
elementId: string,
|
||||
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
|
||||
url: string,
|
||||
currentTags: Store<Record<string, string>>,
|
||||
meta: {
|
||||
theme: string
|
||||
changeType: "add-image" | "link-image"
|
||||
}
|
||||
) {
|
||||
super(elementId, true)
|
||||
this._proposedKey = proposedKey;
|
||||
this._url = url;
|
||||
this._currentTags = currentTags;
|
||||
this._meta = meta;
|
||||
}
|
||||
|
||||
protected CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||
let key = this._proposedKey
|
||||
let i = 0
|
||||
const currentTags = this._currentTags.data
|
||||
const url = this._url
|
||||
while (currentTags[key] !== undefined && currentTags[key] !== url) {
|
||||
key = this._proposedKey + ":" + i
|
||||
i++
|
||||
}
|
||||
const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta)
|
||||
return tagChangeAction.CreateChangeDescriptions()
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import ChangeTagAction from "./ChangeTagAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
|
||||
export default class LinkPicture extends ChangeTagAction {
|
||||
/**
|
||||
* Adds a link to an image
|
||||
* @param elementId
|
||||
* @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
|
||||
* @param url
|
||||
* @param currentTags
|
||||
* @param meta
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
elementId: string,
|
||||
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
|
||||
url: string,
|
||||
currentTags: Record<string, string>,
|
||||
meta: {
|
||||
theme: string
|
||||
changeType: "add-image" | "link-image"
|
||||
}
|
||||
) {
|
||||
let key = proposedKey
|
||||
let i = 0
|
||||
while (currentTags[key] !== undefined && currentTags[key] !== url) {
|
||||
key = proposedKey + ":" + i
|
||||
i++
|
||||
}
|
||||
super(elementId, new Tag(key, url), currentTags, meta)
|
||||
}
|
||||
}
|
|
@ -19,6 +19,9 @@ export default abstract class OsmChangeAction {
|
|||
constructor(mainObjectId: string, trackStatistics: boolean = true) {
|
||||
this.trackStatistics = trackStatistics
|
||||
this.mainObjectId = mainObjectId
|
||||
if(mainObjectId === undefined || mainObjectId === null){
|
||||
throw "OsmObject received '"+mainObjectId+"' as mainObjectId"
|
||||
}
|
||||
}
|
||||
|
||||
public async Perform(changes: Changes) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import Locale from "../../UI/i18n/Locale"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { Changes } from "./Changes"
|
||||
import { Utils } from "../../Utils"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
||||
|
||||
export interface ChangesetTag {
|
||||
key: string
|
||||
|
@ -13,7 +14,7 @@ export interface ChangesetTag {
|
|||
}
|
||||
|
||||
export class ChangesetHandler {
|
||||
private readonly allElements: { addAlias: (id0: String, id1: string) => void }
|
||||
private readonly allElements: FeaturePropertiesStore
|
||||
private osmConnection: OsmConnection
|
||||
private readonly changes: Changes
|
||||
private readonly _dryRun: Store<boolean>
|
||||
|
@ -29,11 +30,11 @@ export class ChangesetHandler {
|
|||
constructor(
|
||||
dryRun: Store<boolean>,
|
||||
osmConnection: OsmConnection,
|
||||
allElements: { addAlias: (id0: string, id1: string) => void } | undefined,
|
||||
allElements: FeaturePropertiesStore | { addAlias: (id0: string, id1: string) => void } | undefined,
|
||||
changes: Changes
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this.allElements = allElements
|
||||
this.allElements = <FeaturePropertiesStore> allElements
|
||||
this.changes = changes
|
||||
this._dryRun = dryRun
|
||||
this.userDetails = osmConnection.userDetails
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -198,7 +198,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
|
||||
this.backgroundLayerId = QueryParameters.GetQueryParameter(
|
||||
"background",
|
||||
layoutToUse?.defaultBackgroundId ?? "osm",
|
||||
layoutToUse?.defaultBackgroundId,
|
||||
"The id of the background layer to start with"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { UIEventSource } from "../UIEventSource";
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||
import { QueryParameters } from "../Web/QueryParameters";
|
||||
|
||||
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
||||
|
||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||
id: "gps"
|
||||
"user:location": "yes"
|
||||
date: string
|
||||
id: "gps";
|
||||
"user:location": "yes";
|
||||
date: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,22 +23,22 @@ export class GeoLocationState {
|
|||
*/
|
||||
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
|
||||
"prompt"
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Important to determine e.g. if we move automatically on fix or not
|
||||
*/
|
||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
|
||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined);
|
||||
/**
|
||||
* If true: the map will center (and re-center) to this location
|
||||
*/
|
||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
|
||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
|
||||
/**
|
||||
* The latest GeoLocationCoordinates, as given by the WebAPI
|
||||
*/
|
||||
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
|
||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
||||
|
@ -50,69 +50,49 @@ export class GeoLocationState {
|
|||
*/
|
||||
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
|
||||
LocalStorageSource.Get("geolocation-permissions")
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Used to detect a permission retraction
|
||||
*/
|
||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
const self = this
|
||||
const self = this;
|
||||
|
||||
this.permission.addCallbackAndRunD(async (state) => {
|
||||
if (state === "granted") {
|
||||
self._previousLocationGrant.setData("true")
|
||||
self._grantedThisSession.setData(true)
|
||||
self._previousLocationGrant.setData("true");
|
||||
self._grantedThisSession.setData(true);
|
||||
}
|
||||
if (state === "prompt" && self._grantedThisSession.data) {
|
||||
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
|
||||
// This means that the rights have been revoked again!
|
||||
// self.permission.setData("denied")
|
||||
self._previousLocationGrant.setData("false")
|
||||
self.permission.setData("denied")
|
||||
self.currentGPSLocation.setData(undefined)
|
||||
console.warn("Detected a downgrade in permissions!")
|
||||
self._previousLocationGrant.setData("false");
|
||||
self.permission.setData("denied");
|
||||
self.currentGPSLocation.setData(undefined);
|
||||
console.warn("Detected a downgrade in permissions!");
|
||||
}
|
||||
if (state === "denied") {
|
||||
self._previousLocationGrant.setData("false")
|
||||
self._previousLocationGrant.setData("false");
|
||||
}
|
||||
})
|
||||
console.log("Previous location grant:", this._previousLocationGrant.data)
|
||||
});
|
||||
console.log("Previous location grant:", this._previousLocationGrant.data);
|
||||
if (this._previousLocationGrant.data === "true") {
|
||||
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
|
||||
|
||||
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
|
||||
this._previousLocationGrant.setData("false")
|
||||
console.log("Requesting access to GPS as this was previously granted")
|
||||
this._previousLocationGrant.setData("false");
|
||||
console.log("Requesting access to GPS as this was previously granted");
|
||||
const latLonGivenViaUrl =
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
|
||||
if (!latLonGivenViaUrl) {
|
||||
this.requestMoment.setData(new Date())
|
||||
this.requestMoment.setData(new Date());
|
||||
}
|
||||
this.requestPermission()
|
||||
this.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the listener for updates
|
||||
* @private
|
||||
*/
|
||||
private async startWatching() {
|
||||
const self = this
|
||||
navigator.geolocation.watchPosition(
|
||||
function (position) {
|
||||
self.currentGPSLocation.setData(position.coords)
|
||||
self._previousLocationGrant.setData("true")
|
||||
},
|
||||
function () {
|
||||
console.warn("Could not get location with navigator.geolocation")
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the user to allow access to their position.
|
||||
* When granted, will be written to the 'geolocationState'.
|
||||
|
@ -121,33 +101,57 @@ export class GeoLocationState {
|
|||
public requestPermission() {
|
||||
if (typeof navigator === "undefined") {
|
||||
// Not compatible with this browser
|
||||
this.permission.setData("denied")
|
||||
return
|
||||
this.permission.setData("denied");
|
||||
return;
|
||||
}
|
||||
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
|
||||
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
|
||||
// Hence that we continue the flow if it is "requested"
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
this.permission.setData("requested")
|
||||
this.permission.setData("requested");
|
||||
try {
|
||||
navigator?.permissions
|
||||
?.query({ name: "geolocation" })
|
||||
.then((status) => {
|
||||
console.log("Status update: received geolocation permission is ", status.state)
|
||||
this.permission.setData(status.state)
|
||||
const self = this
|
||||
status.onchange = function () {
|
||||
const self = this;
|
||||
if(status.state === "granted" || status.state === "denied"){
|
||||
self.permission.setData(status.state)
|
||||
return
|
||||
}
|
||||
status.addEventListener("change", (e) => {
|
||||
self.permission.setData(status.state);
|
||||
|
||||
});
|
||||
// The code above might have reset it to 'prompt', but we _did_ request permission!
|
||||
this.permission.setData("requested")
|
||||
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
|
||||
self.startWatching()
|
||||
self.startWatching();
|
||||
})
|
||||
.catch((e) => console.error("Could not get geopermission", e))
|
||||
.catch((e) => console.error("Could not get geopermission", e));
|
||||
} catch (e) {
|
||||
console.error("Could not get permission:", e)
|
||||
console.error("Could not get permission:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the listener for updates
|
||||
* @private
|
||||
*/
|
||||
private async startWatching() {
|
||||
const self = this;
|
||||
navigator.geolocation.watchPosition(
|
||||
function(position) {
|
||||
self.currentGPSLocation.setData(position.coords);
|
||||
self._previousLocationGrant.setData("true");
|
||||
},
|
||||
function() {
|
||||
console.warn("Could not get location with navigator.geolocation");
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Utils } from "../../Utils"
|
||||
import translators from "../../assets/translators.json"
|
||||
import codeContributors from "../../assets/contributors.json"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import usersettings from "../../../src/assets/generated/layers/usersettings.json"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
||||
import FeatureSwitchState from "./FeatureSwitchState"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import { OsmConnection } from "../Osm/OsmConnection";
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews";
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource";
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource";
|
||||
import { Feature } from "geojson";
|
||||
import { Utils } from "../../Utils";
|
||||
import translators from "../../assets/translators.json";
|
||||
import codeContributors from "../../assets/contributors.json";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import usersettings from "../../../src/assets/generated/layers/usersettings.json";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate";
|
||||
import FeatureSwitchState from "./FeatureSwitchState";
|
||||
import Constants from "../../Models/Constants";
|
||||
import { QueryParameters } from "../Web/QueryParameters";
|
||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging";
|
||||
import { MapProperties } from "../../Models/MapProperties";
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
|
@ -41,6 +42,8 @@ export default class UserRelatedState {
|
|||
public readonly fixateNorth: UIEventSource<undefined | "yes">
|
||||
public readonly homeLocation: FeatureSource
|
||||
public readonly language: UIEventSource<string>
|
||||
public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
|
||||
public readonly imageLicense : UIEventSource<string>
|
||||
/**
|
||||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
|
@ -52,16 +55,23 @@ export default class UserRelatedState {
|
|||
/**
|
||||
* Preferences as tags exposes many preferences and state properties as record.
|
||||
* This is used to bridge the internal state with the usersettings.json layerconfig file
|
||||
*
|
||||
* Some metainformation that should not be edited starts with a single underscore
|
||||
* Constants and query parameters start with two underscores
|
||||
* Note: these are linked via OsmConnection.preferences which exports all preferences as UIEventSource
|
||||
*/
|
||||
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||
private readonly _mapProperties: MapProperties;
|
||||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
availableLanguages?: string[],
|
||||
layout?: LayoutConfig,
|
||||
featureSwitches?: FeatureSwitchState
|
||||
featureSwitches?: FeatureSwitchState,
|
||||
mapProperties?: MapProperties
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this._mapProperties = mapProperties;
|
||||
{
|
||||
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
||||
this.osmConnection.GetPreference("translation-mode", "false")
|
||||
|
@ -90,11 +100,17 @@ export default class UserRelatedState {
|
|||
)
|
||||
this.language = this.osmConnection.GetPreference("language")
|
||||
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
|
||||
this.fixateNorth = <any>this.osmConnection.GetPreference("fixate-north")
|
||||
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
)
|
||||
this.preferredBackgroundLayer= this.osmConnection.GetPreference("preferred-background-layer", undefined, {
|
||||
documentation: "The ID of a layer or layer category that MapComplete uses by default"
|
||||
})
|
||||
|
||||
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
||||
documentation: "The license under which new images are uploaded"
|
||||
})
|
||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||
|
||||
this.homeLocation = this.initHomeLocation()
|
||||
|
@ -246,6 +262,7 @@ export default class UserRelatedState {
|
|||
): UIEventSource<Record<string, string>> {
|
||||
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
||||
_theme: layout?.id,
|
||||
"_theme:backgroundLayer": layout?.defaultBackgroundId,
|
||||
_backend: this.osmConnection.Backend(),
|
||||
_applicationOpened: new Date().toISOString(),
|
||||
_supports_sharing:
|
||||
|
@ -260,6 +277,7 @@ export default class UserRelatedState {
|
|||
amendedPrefs.data["__url_parameter_initialized:" + key] = "yes"
|
||||
}
|
||||
|
||||
|
||||
const osmConnection = this.osmConnection
|
||||
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
||||
for (const k in newPrefs) {
|
||||
|
@ -280,7 +298,6 @@ export default class UserRelatedState {
|
|||
amendedPrefs.ping()
|
||||
console.log("Amended prefs are:", amendedPrefs.data)
|
||||
})
|
||||
const usersettingsConfig = UserRelatedState.usersettingsConfig
|
||||
const translationMode = osmConnection.GetPreference("translation-mode")
|
||||
|
||||
Locale.language.mapD(
|
||||
|
@ -335,25 +352,6 @@ export default class UserRelatedState {
|
|||
}
|
||||
|
||||
usersettingMetaTagging.metaTaggging_for_usersettings({ properties: amendedPrefs.data })
|
||||
/*for (const [name, code, _] of usersettingsConfig.calculatedTags) {
|
||||
try {
|
||||
let result = new Function("feat", "return " + code + ";")({
|
||||
properties: amendedPrefs.data,
|
||||
})
|
||||
if (result !== undefined && result !== "" && result !== null) {
|
||||
if (typeof result !== "string") {
|
||||
result = JSON.stringify(result)
|
||||
}
|
||||
amendedPrefs.data[name] = result
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Calculating a tag for userprofile-settings failed for variable",
|
||||
name,
|
||||
e
|
||||
)
|
||||
}
|
||||
}*/
|
||||
|
||||
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
|
||||
const isTranslator = translators.contributors.find(
|
||||
|
@ -407,6 +405,13 @@ export default class UserRelatedState {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
this._mapProperties?.rasterLayer?.addCallbackAndRun(l => {
|
||||
amendedPrefs.data["__current_background"] = l?.properties?.id
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
|
||||
|
||||
return amendedPrefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ export class ThemeMetaTagging {
|
|||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ export class TagUtils {
|
|||
"Invalid type to flatten the multiAnswer: key is a regex too",
|
||||
tagsFilter
|
||||
)
|
||||
throw "Invalid type to FlattenMultiAnswer"
|
||||
throw "Invalid type to FlattenMultiAnswer: key is a regex too"
|
||||
}
|
||||
const keystr = <string>key
|
||||
if (keyValues[keystr] === undefined) {
|
||||
|
@ -109,7 +109,10 @@ export class TagUtils {
|
|||
}
|
||||
|
||||
console.error("Invalid type to flatten the multiAnswer", tagsFilter)
|
||||
throw "Invalid type to FlattenMultiAnswer"
|
||||
throw (
|
||||
"Invalid type to FlattenMultiAnswer, not one of RegexTag, Tag or And: " +
|
||||
tagsFilter.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
return keyValues
|
||||
}
|
||||
|
|
|
@ -357,14 +357,18 @@ class ListenerTracker<T> {
|
|||
let toDelete = undefined
|
||||
let startTime = new Date().getTime() / 1000
|
||||
for (const callback of this._callbacks) {
|
||||
if (callback(data) === true) {
|
||||
// This callback wants to be deleted
|
||||
// Note: it has to return precisely true in order to avoid accidental deletions
|
||||
if (toDelete === undefined) {
|
||||
toDelete = [callback]
|
||||
} else {
|
||||
toDelete.push(callback)
|
||||
try {
|
||||
if (callback(data) === true) {
|
||||
// This callback wants to be deleted
|
||||
// Note: it has to return precisely true in order to avoid accidental deletions
|
||||
if (toDelete === undefined) {
|
||||
toDelete = [callback]
|
||||
} else {
|
||||
toDelete.push(callback)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Got an error while running a callback:", e)
|
||||
}
|
||||
}
|
||||
let endTime = new Date().getTime() / 1000
|
||||
|
@ -511,7 +515,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
}
|
||||
|
||||
private unregisterFromUpstream() {
|
||||
console.log("Unregistering callbacks for", this.tag)
|
||||
console.debug("Unregistering callbacks for", this.tag)
|
||||
this._callbacksAreRegistered = false
|
||||
this._unregisterFromUpstream()
|
||||
this._unregisterFromExtraStores?.forEach((unr) => unr())
|
||||
|
|
|
@ -1,43 +1,43 @@
|
|||
import { Feature, Polygon } from "geojson"
|
||||
import * as editorlayerindex from "../assets/editor-layer-index.json"
|
||||
import * as globallayers from "../assets/global-raster-layers.json"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import { Store, Stores } from "../Logic/UIEventSource"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
import { RasterLayerProperties } from "./RasterLayerProperties"
|
||||
import { Feature, Polygon } from "geojson";
|
||||
import * as editorlayerindex from "../assets/editor-layer-index.json";
|
||||
import * as globallayers from "../assets/global-raster-layers.json";
|
||||
import { BBox } from "../Logic/BBox";
|
||||
import { Store, Stores } from "../Logic/UIEventSource";
|
||||
import { GeoOperations } from "../Logic/GeoOperations";
|
||||
import { RasterLayerProperties } from "./RasterLayerProperties";
|
||||
|
||||
export class AvailableRasterLayers {
|
||||
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
|
||||
RasterLayerPolygon)[] = <any>editorlayerindex.features
|
||||
RasterLayerPolygon)[] = <any>editorlayerindex.features;
|
||||
public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map(
|
||||
(properties) =>
|
||||
<RasterLayerPolygon>{
|
||||
type: "Feature",
|
||||
properties,
|
||||
geometry: BBox.global.asGeometry(),
|
||||
geometry: BBox.global.asGeometry()
|
||||
}
|
||||
)
|
||||
);
|
||||
public static readonly osmCartoProperties: RasterLayerProperties = {
|
||||
id: "osm",
|
||||
name: "OpenStreetMap",
|
||||
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: {
|
||||
text: "OpenStreetMap",
|
||||
url: "https://openStreetMap.org/copyright",
|
||||
url: "https://openStreetMap.org/copyright"
|
||||
},
|
||||
best: true,
|
||||
max_zoom: 19,
|
||||
min_zoom: 0,
|
||||
category: "osmbasedmap",
|
||||
}
|
||||
category: "osmbasedmap"
|
||||
};
|
||||
|
||||
public static readonly osmCarto: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: AvailableRasterLayers.osmCartoProperties,
|
||||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
geometry: BBox.global.asGeometry()
|
||||
};
|
||||
|
||||
public static readonly maplibre: RasterLayerPolygon = {
|
||||
public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: "MapTiler",
|
||||
|
@ -47,12 +47,43 @@ export class AvailableRasterLayers {
|
|||
type: "vector",
|
||||
attribution: {
|
||||
text: "Maptiler",
|
||||
url: "https://www.maptiler.com/copyright/",
|
||||
},
|
||||
url: "https://www.maptiler.com/copyright/"
|
||||
}
|
||||
},
|
||||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
geometry: BBox.global.asGeometry()
|
||||
};
|
||||
|
||||
public static readonly maptilerCarto: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: "MapTiler Carto",
|
||||
url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||
category: "osmbasedmap",
|
||||
id: "maptiler.carto",
|
||||
type: "vector",
|
||||
attribution: {
|
||||
text: "Maptiler",
|
||||
url: "https://www.maptiler.com/copyright/"
|
||||
}
|
||||
},
|
||||
geometry: BBox.global.asGeometry()
|
||||
};
|
||||
|
||||
public static readonly maptilerBackdrop: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: "MapTiler Backdrop",
|
||||
url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||
category: "osmbasedmap",
|
||||
id: "maptiler.backdrop",
|
||||
type: "vector",
|
||||
attribution: {
|
||||
text: "Maptiler",
|
||||
url: "https://www.maptiler.com/copyright/"
|
||||
}
|
||||
},
|
||||
geometry: BBox.global.asGeometry()
|
||||
};
|
||||
public static readonly americana: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
|
@ -63,41 +94,43 @@ export class AvailableRasterLayers {
|
|||
type: "vector",
|
||||
attribution: {
|
||||
text: "Americana",
|
||||
url: "https://github.com/ZeLonewolf/openstreetmap-americana/",
|
||||
},
|
||||
url: "https://github.com/ZeLonewolf/openstreetmap-americana/"
|
||||
}
|
||||
},
|
||||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
geometry: BBox.global.asGeometry()
|
||||
};
|
||||
|
||||
public static layersAvailableAt(
|
||||
location: Store<{ lon: number; lat: number }>
|
||||
): Store<RasterLayerPolygon[]> {
|
||||
const availableLayersBboxes = Stores.ListStabilized(
|
||||
location.mapD((loc) => {
|
||||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||
const lonlat: [number, number] = [loc.lon, loc.lat];
|
||||
return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) =>
|
||||
BBox.get(eliPolygon).contains(lonlat)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
const available = Stores.ListStabilized(
|
||||
availableLayersBboxes.map((eliPolygons) => {
|
||||
const loc = location.data
|
||||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||
const loc = location.data;
|
||||
const lonlat: [number, number] = [loc.lon, loc.lat];
|
||||
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
||||
if (eliPolygon.geometry === null) {
|
||||
return true // global ELI-layer
|
||||
return true; // global ELI-layer
|
||||
}
|
||||
return GeoOperations.inside(lonlat, eliPolygon)
|
||||
})
|
||||
matching.unshift(AvailableRasterLayers.osmCarto)
|
||||
matching.unshift(AvailableRasterLayers.americana)
|
||||
matching.unshift(AvailableRasterLayers.maplibre)
|
||||
matching.push(...AvailableRasterLayers.globalLayers)
|
||||
return matching
|
||||
return GeoOperations.inside(lonlat, eliPolygon);
|
||||
});
|
||||
matching.push(...AvailableRasterLayers.globalLayers);
|
||||
matching.unshift(AvailableRasterLayers.maptilerDefaultLayer,
|
||||
AvailableRasterLayers.osmCarto,
|
||||
AvailableRasterLayers.maptilerCarto,
|
||||
AvailableRasterLayers.maptilerBackdrop,
|
||||
AvailableRasterLayers.americana);
|
||||
return matching;
|
||||
})
|
||||
)
|
||||
return available
|
||||
);
|
||||
return available;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,22 +148,22 @@ export class RasterLayerUtils {
|
|||
preferredCategory: string,
|
||||
ignoreLayer?: RasterLayerPolygon
|
||||
): RasterLayerPolygon {
|
||||
let secondBest: RasterLayerPolygon = undefined
|
||||
let secondBest: RasterLayerPolygon = undefined;
|
||||
for (const rasterLayer of available) {
|
||||
if (rasterLayer === ignoreLayer) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
const p = rasterLayer.properties
|
||||
const p = rasterLayer.properties;
|
||||
if (p.category === preferredCategory) {
|
||||
if (p.best) {
|
||||
return rasterLayer
|
||||
return rasterLayer;
|
||||
}
|
||||
if (!secondBest) {
|
||||
secondBest = rasterLayer
|
||||
secondBest = rasterLayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return secondBest
|
||||
return secondBest;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,11 +179,11 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
|
|||
/**
|
||||
* The name of the imagery source
|
||||
*/
|
||||
readonly name: string
|
||||
readonly name: string;
|
||||
/**
|
||||
* Whether the imagery name should be translated
|
||||
*/
|
||||
readonly i18n?: boolean
|
||||
readonly i18n?: boolean;
|
||||
readonly type:
|
||||
| "tms"
|
||||
| "wms"
|
||||
|
@ -158,7 +191,7 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
|
|||
| "scanex"
|
||||
| "wms_endpoint"
|
||||
| "wmts"
|
||||
| "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */
|
||||
| "vector"; /* Vector is not actually part of the ELI-spec, we add it for vector layers */
|
||||
/**
|
||||
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
|
||||
*/
|
||||
|
@ -170,53 +203,53 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
|
|||
| "historicphoto"
|
||||
| "qa"
|
||||
| "elevation"
|
||||
| "other"
|
||||
| "other";
|
||||
/**
|
||||
* A URL template for imagery tiles
|
||||
*/
|
||||
readonly url: string
|
||||
readonly min_zoom?: number
|
||||
readonly max_zoom?: number
|
||||
readonly url: string;
|
||||
readonly min_zoom?: number;
|
||||
readonly max_zoom?: number;
|
||||
/**
|
||||
* explicit/implicit permission by the owner for use in OSM
|
||||
*/
|
||||
readonly permission_osm?: "explicit" | "implicit" | "no"
|
||||
readonly permission_osm?: "explicit" | "implicit" | "no";
|
||||
/**
|
||||
* A URL for the license or permissions for the imagery
|
||||
*/
|
||||
readonly license_url?: string
|
||||
readonly license_url?: string;
|
||||
/**
|
||||
* A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery.
|
||||
*/
|
||||
readonly privacy_policy_url?: string | boolean
|
||||
readonly privacy_policy_url?: string | boolean;
|
||||
/**
|
||||
* A unique identifier for the source; used in imagery_used changeset tag
|
||||
*/
|
||||
readonly id: string
|
||||
readonly id: string;
|
||||
/**
|
||||
* A short English-language description of the source
|
||||
*/
|
||||
readonly description?: string
|
||||
readonly description?: string;
|
||||
/**
|
||||
* The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple.
|
||||
*/
|
||||
readonly country_code?: string
|
||||
readonly country_code?: string;
|
||||
/**
|
||||
* Whether this imagery should be shown in the default world-wide menu
|
||||
*/
|
||||
readonly default?: boolean
|
||||
readonly default?: boolean;
|
||||
/**
|
||||
* Whether this imagery is the best source for the region
|
||||
*/
|
||||
readonly best?: boolean
|
||||
readonly best?: boolean;
|
||||
/**
|
||||
* The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one
|
||||
*/
|
||||
readonly start_date?: string
|
||||
readonly start_date?: string;
|
||||
/**
|
||||
* The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one
|
||||
*/
|
||||
readonly end_date?: string
|
||||
readonly end_date?: string;
|
||||
/**
|
||||
* HTTP header to check for information if the tile is invalid
|
||||
*/
|
||||
|
@ -226,61 +259,61 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
|
|||
* via the `patternProperty` "^.*$".
|
||||
*/
|
||||
[k: string]: string[] | null
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 'true' if tiles are transparent and can be overlaid on another source
|
||||
*/
|
||||
readonly overlay?: boolean & string
|
||||
readonly available_projections?: string[]
|
||||
readonly overlay?: boolean & string;
|
||||
readonly available_projections?: string[];
|
||||
readonly attribution?: {
|
||||
readonly url?: string
|
||||
readonly text?: string
|
||||
readonly html?: string
|
||||
readonly required?: boolean
|
||||
}
|
||||
};
|
||||
/**
|
||||
* A URL for an image, that can be displayed in the list of imagery layers next to the name
|
||||
*/
|
||||
readonly icon?: string
|
||||
readonly icon?: string;
|
||||
/**
|
||||
* A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text.
|
||||
*/
|
||||
readonly eula?: string
|
||||
readonly eula?: string;
|
||||
/**
|
||||
* A URL for an image, that is displayed in the mapview for attribution
|
||||
*/
|
||||
readonly "logo-image"?: string
|
||||
readonly "logo-image"?: string;
|
||||
/**
|
||||
* Customized text for the terms of use link (default is "Background Terms of Use")
|
||||
*/
|
||||
readonly "terms-of-use-text"?: string
|
||||
readonly "terms-of-use-text"?: string;
|
||||
/**
|
||||
* Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm.
|
||||
*/
|
||||
readonly "no-tile-checksum"?: string
|
||||
readonly "no-tile-checksum"?: string;
|
||||
/**
|
||||
* header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog
|
||||
*/
|
||||
readonly "metadata-header"?: string
|
||||
readonly "metadata-header"?: string;
|
||||
/**
|
||||
* Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too.
|
||||
*/
|
||||
readonly "valid-georeference"?: boolean
|
||||
readonly "valid-georeference"?: boolean;
|
||||
/**
|
||||
* Size of individual tiles delivered by a TMS service
|
||||
*/
|
||||
readonly "tile-size"?: number
|
||||
readonly "tile-size"?: number;
|
||||
/**
|
||||
* Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty.
|
||||
*/
|
||||
readonly "mod-tile-features"?: string
|
||||
readonly "mod-tile-features"?: string;
|
||||
/**
|
||||
* HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times.
|
||||
*/
|
||||
readonly "custom-http-headers"?: {
|
||||
readonly "header-name"?: string
|
||||
readonly "header-value"?: string
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute)
|
||||
*/
|
||||
|
@ -291,17 +324,17 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
|
|||
[k: string]: unknown
|
||||
}
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
}[];
|
||||
/**
|
||||
* format to use when connecting tile server (when using WMS_ENDPOINT type)
|
||||
*/
|
||||
readonly format?: string
|
||||
readonly format?: string;
|
||||
/**
|
||||
* If `true` transparent tiles will be requested from WMS server
|
||||
*/
|
||||
readonly transparent?: boolean & string
|
||||
readonly transparent?: boolean & string;
|
||||
/**
|
||||
* minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid
|
||||
*/
|
||||
readonly "minimum-tile-expire"?: number
|
||||
readonly "minimum-tile-expire"?: number;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
|||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import { del } from "idb-keyval";
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<
|
||||
LayerConfigJson | string | { builtin; override }
|
||||
|
@ -41,7 +42,6 @@ export class UpdateLegacyLayer extends DesugaringStep<
|
|||
delete preset["preciseInput"]
|
||||
} else if (preciseInput !== undefined) {
|
||||
delete preciseInput["preferredBackground"]
|
||||
console.log("Precise input:", preciseInput)
|
||||
preset.snapToLayer = preciseInput.snapToLayer
|
||||
delete preciseInput.snapToLayer
|
||||
if (preciseInput.maxSnapDistance) {
|
||||
|
@ -146,7 +146,6 @@ export class UpdateLegacyLayer extends DesugaringStep<
|
|||
}
|
||||
const pr = <PointRenderingConfigJson>rendering
|
||||
let iconSize = pr.iconSize
|
||||
console.log("Iconsize is", iconSize)
|
||||
|
||||
if (Object.keys(pr.iconSize).length === 1 && pr.iconSize["render"] !== undefined) {
|
||||
iconSize = pr.iconSize["render"]
|
||||
|
@ -198,6 +197,10 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
delete oldThemeConfig.socialImage
|
||||
}
|
||||
|
||||
if(oldThemeConfig.defaultBackgroundId === "osm"){
|
||||
console.log("Removing old background in", json.id)
|
||||
}
|
||||
|
||||
if (oldThemeConfig["roamingRenderings"] !== undefined) {
|
||||
if (oldThemeConfig["roamingRenderings"].length == 0) {
|
||||
delete oldThemeConfig["roamingRenderings"]
|
||||
|
|
|
@ -18,6 +18,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
|
|||
import Validators from "../../../UI/InputElement/Validators"
|
||||
import TagRenderingConfig from "../TagRenderingConfig"
|
||||
import { parse as parse_html } from "node-html-parser"
|
||||
import PresetConfig from "../PresetConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||
private readonly _languages: string[]
|
||||
|
@ -167,9 +169,9 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
json: LayoutConfigJson,
|
||||
context: string
|
||||
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const information = []
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
const information: string[] = []
|
||||
|
||||
const theme = new LayoutConfig(json, this._isBuiltin)
|
||||
|
||||
|
@ -245,7 +247,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
information
|
||||
)
|
||||
}
|
||||
const dups = Utils.Dupiclates(json.layers.map((layer) => layer["id"]))
|
||||
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
|
||||
if (dups.length > 0) {
|
||||
errors.push(
|
||||
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
|
||||
|
@ -275,6 +277,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
errors.push(e)
|
||||
}
|
||||
|
||||
if (theme.id !== "personal") {
|
||||
new DetectDuplicatePresets().convertJoin(theme, context, errors, warnings, information)
|
||||
}
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors,
|
||||
|
@ -889,7 +895,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
{
|
||||
// duplicate ids in tagrenderings check
|
||||
const duplicates = Utils.Dedup(
|
||||
Utils.Dupiclates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
|
||||
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
|
||||
)
|
||||
if (duplicates.length > 0) {
|
||||
console.log(json.tagRenderings)
|
||||
|
@ -985,7 +991,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Dupiclates(
|
||||
const duplicateIds = Utils.Duplicates(
|
||||
(json.tagRenderings ?? [])
|
||||
?.map((f) => f["id"])
|
||||
.filter((id) => id !== "questions")
|
||||
|
@ -1243,3 +1249,68 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
|
||||
constructor() {
|
||||
super(
|
||||
"Detects mappings which have identical (english) names or identical mappings.",
|
||||
["presets"],
|
||||
"DetectDuplicatePresets"
|
||||
)
|
||||
}
|
||||
convert(
|
||||
json: LayoutConfig,
|
||||
context: string
|
||||
): {
|
||||
result: LayoutConfig
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
const presets: PresetConfig[] = [].concat(...json.layers.map((l) => l.presets))
|
||||
|
||||
const errors = []
|
||||
const enNames = presets.map((p) => p.title.textFor("en"))
|
||||
if (new Set(enNames).size != enNames.length) {
|
||||
const dups = Utils.Duplicates(enNames)
|
||||
const layersWithDup = json.layers.filter((l) =>
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
|
||||
)
|
||||
const layerIds = layersWithDup.map((l) => l.id)
|
||||
errors.push(
|
||||
`At ${context}: this themes has multiple presets which are named:${dups}, namely layers ${layerIds.join(
|
||||
", "
|
||||
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
|
||||
)
|
||||
}
|
||||
|
||||
const optimizedTags = <TagsFilter[]>presets.map((p) => new And(p.tags).optimize())
|
||||
for (let i = 0; i < presets.length; i++) {
|
||||
const presetATags = optimizedTags[i]
|
||||
const presetA = presets[i]
|
||||
for (let j = i + 1; j < presets.length; j++) {
|
||||
const presetBTags = optimizedTags[j]
|
||||
const presetB = presets[j]
|
||||
if (
|
||||
Utils.SameObject(presetATags, presetBTags) &&
|
||||
Utils.sameList(
|
||||
presetA.preciseInput.snapToLayers,
|
||||
presetB.preciseInput.snapToLayers
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`At ${context}: this themes has multiple presets with the same tags: ${presetATags.asHumanString(
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
|
||||
j
|
||||
].title.textFor("en")}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { errors, result: json }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -359,7 +359,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
{
|
||||
const duplicateIds = Utils.Dupiclates(this.filters.map((f) => f.id))
|
||||
const duplicateIds = Utils.Duplicates(this.filters.map((f) => f.id))
|
||||
if (duplicateIds.length > 0) {
|
||||
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
|
||||
}
|
||||
|
|
|
@ -1,59 +1,58 @@
|
|||
import LayoutConfig from "./ThemeConfig/LayoutConfig"
|
||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import LayoutConfig from "./ThemeConfig/LayoutConfig";
|
||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization";
|
||||
import { Changes } from "../Logic/Osm/Changes";
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { ExportableMap, MapProperties } from "./MapProperties";
|
||||
import LayerState from "../Logic/State/LayerState";
|
||||
import { Feature, Point, Polygon } from "geojson";
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning";
|
||||
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor";
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig";
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler";
|
||||
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers";
|
||||
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource";
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore";
|
||||
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
|
||||
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource";
|
||||
import ShowDataLayer from "../UI/Map/ShowDataLayer";
|
||||
import TitleHandler from "../Logic/Actors/TitleHandler";
|
||||
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor";
|
||||
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader";
|
||||
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
|
||||
import { BBox } from "../Logic/BBox";
|
||||
import Constants from "./Constants";
|
||||
import Hotkeys from "../UI/Base/Hotkeys";
|
||||
import Translations from "../UI/i18n/Translations";
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource";
|
||||
import { MenuState } from "./MenuState";
|
||||
import MetaTagging from "../Logic/MetaTagging";
|
||||
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator";
|
||||
import {
|
||||
FeatureSource,
|
||||
IndexedFeatureSource,
|
||||
WritableFeatureSource,
|
||||
} from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { ExportableMap, MapProperties } from "./MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature, Point, Polygon } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
|
||||
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
|
||||
import { GeoLocationState } from "../Logic/State/GeoLocationState"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"
|
||||
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||
import ShowDataLayer from "../UI/Map/ShowDataLayer"
|
||||
import TitleHandler from "../Logic/Actors/TitleHandler"
|
||||
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
|
||||
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
|
||||
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import Constants from "./Constants"
|
||||
import Hotkeys from "../UI/Base/Hotkeys"
|
||||
import Translations from "../UI/i18n/Translations"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import { MenuState } from "./MenuState"
|
||||
import MetaTagging from "../Logic/MetaTagging"
|
||||
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
|
||||
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
|
||||
import { Utils } from "../Utils"
|
||||
import { EliCategory } from "./RasterLayerProperties"
|
||||
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
|
||||
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
|
||||
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
|
||||
import NoElementsInViewDetector, {
|
||||
FeatureViewState,
|
||||
} from "../Logic/Actors/NoElementsInViewDetector"
|
||||
import FilteredLayer from "./FilteredLayer"
|
||||
NewGeometryFromChangesFeatureSource
|
||||
} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource";
|
||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
|
||||
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer";
|
||||
import { Utils } from "../Utils";
|
||||
import { EliCategory } from "./RasterLayerProperties";
|
||||
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter";
|
||||
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage";
|
||||
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource";
|
||||
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor";
|
||||
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector";
|
||||
import FilteredLayer from "./FilteredLayer";
|
||||
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector";
|
||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
|
||||
import { Imgur } from "../Logic/ImageProviders/Imgur";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -64,69 +63,71 @@ import FilteredLayer from "./FilteredLayer"
|
|||
* It ties up all the needed elements and starts some actors.
|
||||
*/
|
||||
export default class ThemeViewState implements SpecialVisualizationState {
|
||||
readonly layout: LayoutConfig
|
||||
readonly map: UIEventSource<MlMap>
|
||||
readonly changes: Changes
|
||||
readonly featureSwitches: FeatureSwitchState
|
||||
readonly featureSwitchIsTesting: Store<boolean>
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
readonly layout: LayoutConfig;
|
||||
readonly map: UIEventSource<MlMap>;
|
||||
readonly changes: Changes;
|
||||
readonly featureSwitches: FeatureSwitchState;
|
||||
readonly featureSwitchIsTesting: Store<boolean>;
|
||||
readonly featureSwitchUserbadge: Store<boolean>;
|
||||
|
||||
readonly featureProperties: FeaturePropertiesStore
|
||||
readonly featureProperties: FeaturePropertiesStore;
|
||||
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly selectedElement: UIEventSource<Feature>
|
||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
||||
readonly mapProperties: MapProperties & ExportableMap
|
||||
readonly osmObjectDownloader: OsmObjectDownloader
|
||||
readonly osmConnection: OsmConnection;
|
||||
readonly selectedElement: UIEventSource<Feature>;
|
||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
|
||||
readonly mapProperties: MapProperties & ExportableMap;
|
||||
readonly osmObjectDownloader: OsmObjectDownloader;
|
||||
|
||||
readonly dataIsLoading: Store<boolean>
|
||||
readonly dataIsLoading: Store<boolean>;
|
||||
/**
|
||||
* Indicates if there is _some_ data in view, even if it is not shown due to the filters
|
||||
*/
|
||||
readonly hasDataInView: Store<FeatureViewState>
|
||||
readonly hasDataInView: Store<FeatureViewState>;
|
||||
|
||||
readonly guistate: MenuState
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
readonly guistate: MenuState;
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource;
|
||||
|
||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||
readonly featuresInView: FeatureSource
|
||||
readonly newFeatures: WritableFeatureSource
|
||||
readonly layerState: LayerState
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>
|
||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>;
|
||||
readonly indexedFeatures: IndexedFeatureSource & LayoutSource;
|
||||
readonly currentView: FeatureSource<Feature<Polygon>>;
|
||||
readonly featuresInView: FeatureSource;
|
||||
readonly newFeatures: WritableFeatureSource;
|
||||
readonly layerState: LayerState;
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>;
|
||||
readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource>;
|
||||
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||
readonly selectedLayer: UIEventSource<LayerConfig>
|
||||
readonly userRelatedState: UserRelatedState
|
||||
readonly geolocation: GeoLocationHandler
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>;
|
||||
readonly selectedLayer: UIEventSource<LayerConfig>;
|
||||
readonly userRelatedState: UserRelatedState;
|
||||
readonly geolocation: GeoLocationHandler;
|
||||
|
||||
readonly lastClickObject: WritableFeatureSource
|
||||
readonly imageUploadManager: ImageUploadManager
|
||||
|
||||
readonly lastClickObject: WritableFeatureSource;
|
||||
readonly overlayLayerStates: ReadonlyMap<
|
||||
string,
|
||||
{ readonly isDisplayed: UIEventSource<boolean> }
|
||||
>
|
||||
>;
|
||||
/**
|
||||
* All 'level'-tags that are available with the current features
|
||||
*/
|
||||
readonly floors: Store<string[]>
|
||||
readonly floors: Store<string[]>;
|
||||
|
||||
constructor(layout: LayoutConfig) {
|
||||
Utils.initDomPurify()
|
||||
this.layout = layout
|
||||
this.featureSwitches = new FeatureSwitchState(layout)
|
||||
Utils.initDomPurify();
|
||||
this.layout = layout;
|
||||
this.featureSwitches = new FeatureSwitchState(layout);
|
||||
this.guistate = new MenuState(
|
||||
this.featureSwitches.featureSwitchWelcomeMessage.data,
|
||||
layout.id
|
||||
)
|
||||
this.map = new UIEventSource<MlMap>(undefined)
|
||||
const initial = new InitialMapPositioning(layout)
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
||||
const geolocationState = new GeoLocationState()
|
||||
);
|
||||
this.map = new UIEventSource<MlMap>(undefined);
|
||||
const initial = new InitialMapPositioning(layout);
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, initial);
|
||||
const geolocationState = new GeoLocationState();
|
||||
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting;
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin;
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
|
@ -136,65 +137,68 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
|
||||
})
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data
|
||||
});
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
layout?.language,
|
||||
layout,
|
||||
this.featureSwitches
|
||||
)
|
||||
this.featureSwitches,
|
||||
this.mapProperties
|
||||
);
|
||||
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
|
||||
this.mapProperties.allowRotating.setData(fixated !== "yes")
|
||||
})
|
||||
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
|
||||
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
|
||||
this.mapProperties.allowRotating.setData(fixated !== "yes");
|
||||
});
|
||||
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element");
|
||||
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer");
|
||||
|
||||
this.selectedElementAndLayer = this.selectedElement.mapD(
|
||||
(feature) => {
|
||||
const layer = this.selectedLayer.data
|
||||
const layer = this.selectedLayer.data;
|
||||
if (!layer) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
return { layer, feature }
|
||||
return { layer, feature };
|
||||
},
|
||||
[this.selectedLayer]
|
||||
)
|
||||
);
|
||||
|
||||
this.geolocation = new GeoLocationHandler(
|
||||
geolocationState,
|
||||
this.selectedElement,
|
||||
this.mapProperties,
|
||||
this.userRelatedState.gpsLocationHistoryRetentionTime
|
||||
)
|
||||
);
|
||||
|
||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
|
||||
|
||||
const self = this
|
||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
||||
|
||||
const self = this;
|
||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
|
||||
|
||||
{
|
||||
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>()
|
||||
const overlayLayerStates = new Map<string, { isDisplayed: UIEventSource<boolean> }>();
|
||||
for (const rasterInfo of this.layout.tileLayerSources) {
|
||||
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||
"overlay-" + rasterInfo.id,
|
||||
rasterInfo.defaultState ?? true,
|
||||
"Wether or not overlayer layer " + rasterInfo.id + " is shown"
|
||||
)
|
||||
const state = { isDisplayed }
|
||||
overlayLayerStates.set(rasterInfo.id, state)
|
||||
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
|
||||
);
|
||||
const state = { isDisplayed };
|
||||
overlayLayerStates.set(rasterInfo.id, state);
|
||||
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state);
|
||||
}
|
||||
this.overlayLayerStates = overlayLayerStates
|
||||
this.overlayLayerStates = overlayLayerStates;
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
/* Setup the layout source
|
||||
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
|
||||
*/
|
||||
|
||||
if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource();
|
||||
}
|
||||
|
||||
const layoutSource = new LayoutSource(
|
||||
|
@ -204,49 +208,49 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.osmConnection.Backend(),
|
||||
(id) => self.layerState.filteredLayers.get(id).isDisplayed,
|
||||
this.fullNodeDatabase
|
||||
)
|
||||
);
|
||||
|
||||
this.indexedFeatures = layoutSource
|
||||
this.indexedFeatures = layoutSource;
|
||||
|
||||
const empty = []
|
||||
let currentViewIndex = 0
|
||||
const empty = [];
|
||||
let currentViewIndex = 0;
|
||||
this.currentView = new StaticFeatureSource(
|
||||
this.mapProperties.bounds.map((bbox) => {
|
||||
if (!bbox) {
|
||||
return empty
|
||||
return empty;
|
||||
}
|
||||
currentViewIndex++
|
||||
currentViewIndex++;
|
||||
return <Feature[]>[
|
||||
bbox.asGeoJson({
|
||||
zoom: this.mapProperties.zoom.data,
|
||||
...this.mapProperties.location.data,
|
||||
id: "current_view",
|
||||
}),
|
||||
]
|
||||
id: "current_view"
|
||||
})
|
||||
];
|
||||
})
|
||||
)
|
||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||
this.dataIsLoading = layoutSource.isLoading
|
||||
);
|
||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds);
|
||||
this.dataIsLoading = layoutSource.isLoading;
|
||||
|
||||
const indexedElements = this.indexedFeatures
|
||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||
const indexedElements = this.indexedFeatures;
|
||||
this.featureProperties = new FeaturePropertiesStore(indexedElements);
|
||||
this.changes = new Changes(
|
||||
{
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
allElements: indexedElements,
|
||||
featurePropertiesStore: this.featureProperties,
|
||||
osmConnection: this.osmConnection,
|
||||
historicalUserLocations: this.geolocation.historicalUserLocations,
|
||||
historicalUserLocations: this.geolocation.historicalUserLocations
|
||||
},
|
||||
layout?.isLeftRightSensitive() ?? false
|
||||
)
|
||||
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||
);
|
||||
this.historicalUserLocations = this.geolocation.historicalUserLocations;
|
||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||
this.changes,
|
||||
indexedElements,
|
||||
this.featureProperties
|
||||
)
|
||||
layoutSource.addSource(this.newFeatures)
|
||||
);
|
||||
layoutSource.addSource(this.newFeatures);
|
||||
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||
|
@ -262,11 +266,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
features.length,
|
||||
"leftover features, such as",
|
||||
features[0].properties
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
this.perLayer = perLayer.perLayer
|
||||
);
|
||||
this.perLayer = perLayer.perLayer;
|
||||
}
|
||||
this.perLayer.forEach((fs) => {
|
||||
new SaveFeatureSourceToLocalStorage(
|
||||
|
@ -276,73 +280,74 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
fs,
|
||||
this.featureProperties,
|
||||
fs.layer.layerDef.maxAgeOfCache
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.floors = this.featuresInView.features.stabilized(500).map((features) => {
|
||||
if (!features) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
const floors = new Set<string>()
|
||||
const floors = new Set<string>();
|
||||
for (const feature of features) {
|
||||
const level = feature.properties["level"]
|
||||
const level = feature.properties["level"];
|
||||
if (level) {
|
||||
const levels = level.split(";")
|
||||
const levels = level.split(";");
|
||||
for (const l of levels) {
|
||||
floors.add(l)
|
||||
floors.add(l);
|
||||
}
|
||||
} else {
|
||||
floors.add("0") // '0' is the default and is thus _always_ present
|
||||
floors.add("0"); // '0' is the default and is thus _always_ present
|
||||
}
|
||||
}
|
||||
const sorted = Array.from(floors)
|
||||
const sorted = Array.from(floors);
|
||||
// Sort alphabetically first, to deal with floor "A", "B" and "C"
|
||||
sorted.sort()
|
||||
sorted.sort();
|
||||
sorted.sort((a, b) => {
|
||||
// We use the laxer 'parseInt' to deal with floor '1A'
|
||||
const na = parseInt(a)
|
||||
const nb = parseInt(b)
|
||||
const na = parseInt(a);
|
||||
const nb = parseInt(b);
|
||||
if (isNaN(na) || isNaN(nb)) {
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
return na - nb
|
||||
})
|
||||
sorted.reverse(/* new list, no side-effects */)
|
||||
return sorted
|
||||
})
|
||||
return na - nb;
|
||||
});
|
||||
sorted.reverse(/* new list, no side-effects */);
|
||||
return sorted;
|
||||
});
|
||||
|
||||
const lastClick = (this.lastClickObject = new LastClickFeatureSource(
|
||||
this.mapProperties.lastClickLocation,
|
||||
this.layout
|
||||
))
|
||||
));
|
||||
|
||||
this.osmObjectDownloader = new OsmObjectDownloader(
|
||||
this.osmConnection.Backend(),
|
||||
this.changes
|
||||
)
|
||||
);
|
||||
|
||||
this.perLayerFiltered = this.showNormalDataOn(this.map)
|
||||
this.perLayerFiltered = this.showNormalDataOn(this.map);
|
||||
|
||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
|
||||
this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes)
|
||||
|
||||
this.initActors()
|
||||
this.addLastClick(lastClick)
|
||||
this.drawSpecialLayers()
|
||||
this.initHotkeys()
|
||||
this.miscSetup()
|
||||
this.initActors();
|
||||
this.addLastClick(lastClick);
|
||||
this.drawSpecialLayers();
|
||||
this.initHotkeys();
|
||||
this.miscSetup();
|
||||
if (!Utils.runningFromConsole) {
|
||||
console.log("State setup completed", this)
|
||||
console.log("State setup completed", this);
|
||||
}
|
||||
}
|
||||
|
||||
public showNormalDataOn(map: Store<MlMap>): ReadonlyMap<string, FilteringFeatureSource> {
|
||||
const filteringFeatureSource = new Map<string, FilteringFeatureSource>()
|
||||
const filteringFeatureSource = new Map<string, FilteringFeatureSource>();
|
||||
this.perLayer.forEach((fs, layerName) => {
|
||||
const doShowLayer = this.mapProperties.zoom.map(
|
||||
(z) =>
|
||||
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
|
||||
[fs.layer.isDisplayed]
|
||||
)
|
||||
);
|
||||
|
||||
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
|
||||
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
|
||||
|
@ -352,15 +357,15 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
* Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
|
||||
* However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
|
||||
* */
|
||||
return
|
||||
return;
|
||||
}
|
||||
const filtered = new FilteringFeatureSource(
|
||||
fs.layer,
|
||||
fs,
|
||||
(id) => this.featureProperties.getStore(id),
|
||||
this.layerState.globalFilters
|
||||
)
|
||||
filteringFeatureSource.set(layerName, filtered)
|
||||
);
|
||||
filteringFeatureSource.set(layerName, filtered);
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
layer: fs.layer.layerDef,
|
||||
|
@ -368,30 +373,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
doShowLayer,
|
||||
selectedElement: this.selectedElement,
|
||||
selectedLayer: this.selectedLayer,
|
||||
fetchStore: (id) => this.featureProperties.getStore(id),
|
||||
})
|
||||
})
|
||||
return filteringFeatureSource
|
||||
fetchStore: (id) => this.featureProperties.getStore(id)
|
||||
});
|
||||
});
|
||||
return filteringFeatureSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Various small methods that need to be called
|
||||
*/
|
||||
private miscSetup() {
|
||||
this.userRelatedState.markLayoutAsVisited(this.layout)
|
||||
this.userRelatedState.markLayoutAsVisited(this.layout);
|
||||
|
||||
this.selectedElement.addCallbackAndRunD((feature) => {
|
||||
// As soon as we have a selected element, we clear the selected element
|
||||
// This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature
|
||||
// The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear
|
||||
if (feature.properties.id === "last_click") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
this.lastClickObject.features.setData([])
|
||||
})
|
||||
this.lastClickObject.features.setData([]);
|
||||
});
|
||||
|
||||
if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
|
||||
Utils.LoadCustomCss(this.layout.customCss)
|
||||
Utils.LoadCustomCss(this.layout.customCss);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -400,74 +405,74 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
{ nomod: "Escape", onUp: true },
|
||||
Translations.t.hotkeyDocumentation.closeSidebar,
|
||||
() => {
|
||||
this.selectedElement.setData(undefined)
|
||||
this.guistate.closeAll()
|
||||
this.selectedElement.setData(undefined);
|
||||
this.guistate.closeAll();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: "b",
|
||||
nomod: "b"
|
||||
},
|
||||
Translations.t.hotkeyDocumentation.openLayersPanel,
|
||||
() => {
|
||||
if (this.featureSwitches.featureSwitchFilter.data) {
|
||||
this.guistate.openFilterView()
|
||||
this.guistate.openFilterView();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "O" },
|
||||
Translations.t.hotkeyDocumentation.selectMapnik,
|
||||
() => {
|
||||
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
|
||||
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto);
|
||||
}
|
||||
)
|
||||
);
|
||||
const setLayerCategory = (category: EliCategory) => {
|
||||
const available = this.availableLayers.data
|
||||
const current = this.mapProperties.rasterLayer
|
||||
const available = this.availableLayers.data;
|
||||
const current = this.mapProperties.rasterLayer;
|
||||
const best = RasterLayerUtils.SelectBestLayerAccordingTo(
|
||||
available,
|
||||
category,
|
||||
current.data
|
||||
)
|
||||
console.log("Best layer for category", category, "is", best.properties.id)
|
||||
current.setData(best)
|
||||
}
|
||||
);
|
||||
console.log("Best layer for category", category, "is", best.properties.id);
|
||||
current.setData(best);
|
||||
};
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "O" },
|
||||
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap")
|
||||
)
|
||||
);
|
||||
|
||||
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
|
||||
setLayerCategory("map")
|
||||
)
|
||||
);
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "P" },
|
||||
Translations.t.hotkeyDocumentation.selectAerial,
|
||||
() => setLayerCategory("photo")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private addLastClick(last_click: LastClickFeatureSource) {
|
||||
// The last_click gets a _very_ special treatment as it interacts with various parts
|
||||
|
||||
const last_click_layer = this.layerState.filteredLayers.get("last_click")
|
||||
this.featureProperties.trackFeatureSource(last_click)
|
||||
this.indexedFeatures.addSource(last_click)
|
||||
const last_click_layer = this.layerState.filteredLayers.get("last_click");
|
||||
this.featureProperties.trackFeatureSource(last_click);
|
||||
this.indexedFeatures.addSource(last_click);
|
||||
|
||||
last_click.features.addCallbackAndRunD((features) => {
|
||||
if (this.selectedLayer.data?.id === "last_click") {
|
||||
// The last-click location moved, but we have selected the last click of the previous location
|
||||
// So, we update _after_ clearing the selection to make sure no stray data is sticking around
|
||||
this.selectedElement.setData(undefined)
|
||||
this.selectedElement.setData(features[0])
|
||||
this.selectedElement.setData(undefined);
|
||||
this.selectedElement.setData(features[0]);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
new ShowDataLayer(this.map, {
|
||||
features: new FilteringFeatureSource(last_click_layer, last_click),
|
||||
|
@ -479,18 +484,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) {
|
||||
this.map.data.flyTo({
|
||||
zoom: Constants.minZoomLevelToAddNewPoint,
|
||||
center: this.mapProperties.lastClickLocation.data,
|
||||
})
|
||||
return
|
||||
center: this.mapProperties.lastClickLocation.data
|
||||
});
|
||||
return;
|
||||
}
|
||||
// We first clear the selection to make sure no weird state is around
|
||||
this.selectedLayer.setData(undefined)
|
||||
this.selectedElement.setData(undefined)
|
||||
this.selectedLayer.setData(undefined);
|
||||
this.selectedElement.setData(undefined);
|
||||
|
||||
this.selectedElement.setData(feature)
|
||||
this.selectedLayer.setData(last_click_layer.layerDef)
|
||||
},
|
||||
})
|
||||
this.selectedElement.setData(feature);
|
||||
this.selectedLayer.setData(last_click_layer.layerDef);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -498,7 +503,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
*/
|
||||
private drawSpecialLayers() {
|
||||
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
|
||||
const empty = []
|
||||
const empty = [];
|
||||
/**
|
||||
* A listing which maps the layerId onto the featureSource
|
||||
*/
|
||||
|
@ -518,21 +523,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
|
||||
)
|
||||
),
|
||||
current_view: this.currentView,
|
||||
}
|
||||
current_view: this.currentView
|
||||
};
|
||||
if (this.layout?.lockLocation) {
|
||||
const bbox = new BBox(this.layout.lockLocation)
|
||||
this.mapProperties.maxbounds.setData(bbox)
|
||||
const bbox = new BBox(this.layout.lockLocation);
|
||||
this.mapProperties.maxbounds.setData(bbox);
|
||||
ShowDataLayer.showRange(
|
||||
this.map,
|
||||
new StaticFeatureSource([bbox.asGeoJson({})]),
|
||||
this.featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
);
|
||||
}
|
||||
const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view")
|
||||
const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view");
|
||||
if (currentViewLayer?.tagRenderings?.length > 0) {
|
||||
const params = MetaTagging.createExtraFuncParams(this)
|
||||
this.featureProperties.trackFeatureSource(specialLayers.current_view)
|
||||
const params = MetaTagging.createExtraFuncParams(this);
|
||||
this.featureProperties.trackFeatureSource(specialLayers.current_view);
|
||||
specialLayers.current_view.features.addCallbackAndRunD((features) => {
|
||||
MetaTagging.addMetatags(
|
||||
features,
|
||||
|
@ -541,37 +546,37 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.layout,
|
||||
this.osmObjectDownloader,
|
||||
this.featureProperties
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range")
|
||||
const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range");
|
||||
|
||||
const rangeIsDisplayed = rangeFLayer?.isDisplayed
|
||||
const rangeIsDisplayed = rangeFLayer?.isDisplayed;
|
||||
|
||||
if (
|
||||
!QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef))
|
||||
) {
|
||||
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
|
||||
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true);
|
||||
}
|
||||
|
||||
this.layerState.filteredLayers.forEach((flayer) => {
|
||||
const id = flayer.layerDef.id
|
||||
const features: FeatureSource = specialLayers[id]
|
||||
const id = flayer.layerDef.id;
|
||||
const features: FeatureSource = specialLayers[id];
|
||||
if (features === undefined) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
this.featureProperties.trackFeatureSource(features)
|
||||
this.featureProperties.trackFeatureSource(features);
|
||||
// this.indexedFeatures.addSource(features)
|
||||
new ShowDataLayer(this.map, {
|
||||
features,
|
||||
doShowLayer: flayer.isDisplayed,
|
||||
layer: flayer.layerDef,
|
||||
selectedElement: this.selectedElement,
|
||||
selectedLayer: this.selectedLayer,
|
||||
})
|
||||
})
|
||||
selectedLayer: this.selectedLayer
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -580,29 +585,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
private initActors() {
|
||||
// Unselect the selected element if it is panned out of view
|
||||
this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => {
|
||||
const selected = this.selectedElement.data
|
||||
const selected = this.selectedElement.data;
|
||||
if (selected === undefined) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const bbox = BBox.get(selected)
|
||||
const bbox = BBox.get(selected);
|
||||
if (!bbox.overlapsWith(bounds)) {
|
||||
this.selectedElement.setData(undefined)
|
||||
this.selectedElement.setData(undefined);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.selectedElement.addCallback((selected) => {
|
||||
if (selected === undefined) {
|
||||
// We did _unselect_ an item - we always remove the lastclick-object
|
||||
this.lastClickObject.features.setData([])
|
||||
this.selectedLayer.setData(undefined)
|
||||
this.lastClickObject.features.setData([]);
|
||||
this.selectedLayer.setData(undefined);
|
||||
}
|
||||
})
|
||||
new ThemeViewStateHashActor(this)
|
||||
new MetaTagging(this)
|
||||
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this)
|
||||
new ChangeToElementsActor(this.changes, this.featureProperties)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement)
|
||||
new SelectedElementTagsUpdater(this)
|
||||
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
|
||||
});
|
||||
new ThemeViewStateHashActor(this);
|
||||
new MetaTagging(this);
|
||||
new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this);
|
||||
new ChangeToElementsActor(this.changes, this.featureProperties);
|
||||
new PendingChangesUploader(this.changes, this.selectedElement);
|
||||
new SelectedElementTagsUpdater(this);
|
||||
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers);
|
||||
new PreferredRasterLayerSelector(this.mapProperties.rasterLayer, this.availableLayers, this.featureSwitches.backgroundLayerId, this.userRelatedState.preferredBackgroundLayer)
|
||||
}
|
||||
}
|
||||
|
|
40
src/UI/Base/FileSelector.svelte
Normal file
40
src/UI/Base/FileSelector.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let accept: string;
|
||||
export let multiple: boolean = true;
|
||||
|
||||
const dispatcher = createEventDispatcher<{ submit: FileList }>();
|
||||
export let cls: string = "";
|
||||
let drawAttention = false;
|
||||
let inputElement: HTMLInputElement;
|
||||
let id = Math.random() * 1000000000 + "";
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}>
|
||||
<slot />
|
||||
|
||||
</label>
|
||||
<input {accept} bind:this={inputElement} class="hidden" id={"fileinput" + id} {multiple} name="file-input"
|
||||
on:change|preventDefault={() => {
|
||||
drawAttention = false;
|
||||
dispatcher("submit", inputElement.files)}}
|
||||
|
||||
on:dragend={ () => {drawAttention = false}}
|
||||
on:dragover|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging over!")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={ () => {drawAttention = false}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
type="file"
|
||||
>
|
||||
</form>
|
|
@ -1,9 +1,12 @@
|
|||
<script>
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
<script lang="ts">
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let cls : string = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex p-1 pl-2">
|
||||
<div class={twMerge( "flex p-1 pl-2", cls)}>
|
||||
<div class="min-w-6 h-6 w-6 animate-spin self-center">
|
||||
<ToSvelte construct={Svg.loading_svg()} />
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
function updatedAltLayer() {
|
||||
const available = availableRasterLayers.data
|
||||
const current = rasterLayer.data
|
||||
const defaultLayer = AvailableRasterLayers.maplibre
|
||||
const defaultLayer = AvailableRasterLayers.maptilerDefaultLayer
|
||||
const firstOther = available.find((l) => l !== defaultLayer)
|
||||
const secondOther = available.find((l) => l !== defaultLayer && l !== firstOther)
|
||||
raster0.setData(firstOther === current ? defaultLayer : firstOther)
|
||||
|
|
|
@ -1,40 +1,44 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import IfNot from "../Base/IfNot.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import If from "../Base/If.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Geosearch from "./Geosearch.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import { Utils } from "../../Utils";
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
|
||||
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
let selectedElement = state.selectedElement;
|
||||
let selectedLayer = state.selectedLayer;
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
|
||||
let searchEnabled = false;
|
||||
|
||||
let geopermission: Store<GeolocationPermissionState> = state.geolocation.geolocationState.permission;
|
||||
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
|
||||
|
||||
geopermission.addCallback(perm => console.log(">>>> Permission", perm));
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState
|
||||
const glstate = state.geolocation.geolocationState;
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
|
||||
state.guistate.themeIsOpened.setData(false);
|
||||
const coor = { lon: c.longitude, lat: c.latitude };
|
||||
state.mapProperties.location.setData(coor);
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission()
|
||||
return
|
||||
glstate.requestPermission();
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -58,12 +62,24 @@
|
|||
</NextButton>
|
||||
|
||||
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<IfNot condition={state.geolocation.geolocationState.permission.map((p) => p === "denied")}>
|
||||
{#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
|
||||
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
|
||||
<Tr t={Translations.t.general.openTheMapAtGeolocation} />
|
||||
</button>
|
||||
</IfNot>
|
||||
<!-- No geolocation granted - we don't show the button -->
|
||||
{:else if $geopermission === "requested"}
|
||||
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
|
||||
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} />
|
||||
<Tr t={Translations.t.general.waitingForGeopermission} />
|
||||
</button>
|
||||
{:else if $geopermission !== "denied"}
|
||||
<button class="flex w-full items-center gap-x-2 disabled">
|
||||
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("motion-safe:animate-spin")} />
|
||||
<Tr t={Translations.t.general.waitingForLocation} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
|
||||
<div class="w-full">
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
const templateUrls = SvgToPdf.templates[templateName].pages
|
||||
const templates: string[] = await Promise.all(templateUrls.map((url) => Utils.download(url)))
|
||||
console.log("Templates are", templates)
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maptilerDefaultLayer
|
||||
const creator = new SvgToPdf(title, templates, {
|
||||
state,
|
||||
freeComponentId: "belowmap",
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import FileSelectorButton from "../Input/FileSelectorButton"
|
||||
import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
||||
constructor(
|
||||
tagsSource: Store<any>,
|
||||
state: SpecialVisualizationState,
|
||||
imagePrefix: string = "image",
|
||||
text: string = undefined
|
||||
) {
|
||||
const perId = ImageUploadFlow.uploadCountsPerId
|
||||
const id = tagsSource.data.id
|
||||
if (!perId.has(id)) {
|
||||
perId.set(id, new UIEventSource<number>(0))
|
||||
}
|
||||
const uploadedCount = perId.get(id)
|
||||
const uploader = new ImgurUploader(async (url) => {
|
||||
// A file was uploaded - we add it to the tags of the object
|
||||
|
||||
const tags = tagsSource.data
|
||||
let key = imagePrefix
|
||||
if (tags[imagePrefix] !== undefined) {
|
||||
let freeIndex = 0
|
||||
while (tags[imagePrefix + ":" + freeIndex] !== undefined) {
|
||||
freeIndex++
|
||||
}
|
||||
key = imagePrefix + ":" + freeIndex
|
||||
}
|
||||
|
||||
await state.changes.applyAction(
|
||||
new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, {
|
||||
changeType: "add-image",
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
console.log("Adding image:" + key, url)
|
||||
uploadedCount.data++
|
||||
uploadedCount.ping()
|
||||
})
|
||||
|
||||
const t = Translations.t.image
|
||||
|
||||
let labelContent: BaseUIElement
|
||||
if (text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture
|
||||
.Clone()
|
||||
.SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
} else {
|
||||
labelContent = new FixedUiElement(text).SetClass(
|
||||
"block align-middle mt-1 ml-3 text-2xl "
|
||||
)
|
||||
}
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl "),
|
||||
labelContent,
|
||||
]).SetClass("w-full flex justify-center items-center")
|
||||
|
||||
const licenseStore = state?.osmConnection?.GetPreference("pictures-license", "CC0")
|
||||
|
||||
const fileSelector = new FileSelectorButton(label, {
|
||||
acceptType: "image/*",
|
||||
allowMultiple: true,
|
||||
labelClasses: "rounded-full border-2 border-black font-bold",
|
||||
})
|
||||
/* fileSelector.SetClass(
|
||||
"p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center"
|
||||
)
|
||||
.SetStyle(" border-color: var(--foreground-color);")*/
|
||||
fileSelector.GetValue().addCallback((filelist) => {
|
||||
if (filelist === undefined || filelist.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (var i = 0; i < filelist.length; i++) {
|
||||
const sizeInBytes = filelist[i].size
|
||||
console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes")
|
||||
if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
alert(
|
||||
Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: uploader.maxFileSizeInMegabytes + "MB",
|
||||
}).txt
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const license = licenseStore?.data ?? "CC0"
|
||||
|
||||
const tags = tagsSource.data
|
||||
|
||||
const layout = state?.layout
|
||||
let matchingLayer: LayerConfig = undefined
|
||||
for (const layer of layout?.layers ?? []) {
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
matchingLayer = layer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const title =
|
||||
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.ConstructElement()
|
||||
?.textContent ??
|
||||
tags.name ??
|
||||
"https//osm.org/" + tags.id
|
||||
const description = [
|
||||
"author:" + state.osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id,
|
||||
].join("\n")
|
||||
|
||||
uploader.uploadMany(title, description, filelist)
|
||||
})
|
||||
|
||||
const uploadFlow: BaseUIElement = new Combine([
|
||||
new VariableUiElement(
|
||||
uploader.queue
|
||||
.map((q) => q.length)
|
||||
.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
if (l == 1) {
|
||||
return new Loading(t.uploadingPicture).SetClass("alert")
|
||||
} else {
|
||||
return new Loading(
|
||||
t.uploadingMultiple.Subs({ count: "" + l })
|
||||
).SetClass("alert")
|
||||
}
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
uploader.failed
|
||||
.map((q) => q.length)
|
||||
.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
console.log(l)
|
||||
return t.uploadFailed.SetClass("block alert")
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
uploadedCount.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
if (l == 1) {
|
||||
return t.uploadDone.Clone().SetClass("thanks block")
|
||||
}
|
||||
return t.uploadMultipleDone.Subs({ count: l }).SetClass("thanks block")
|
||||
})
|
||||
),
|
||||
|
||||
fileSelector,
|
||||
new Combine([
|
||||
Translations.t.image.respectPrivacy,
|
||||
new VariableUiElement(
|
||||
licenseStore.map((license) =>
|
||||
Translations.t.image.currentLicense.Subs({ license })
|
||||
)
|
||||
)
|
||||
.onClick(() => {
|
||||
console.log("Opening the license settings... ")
|
||||
state.guistate.openUsersettings("picture-license")
|
||||
})
|
||||
.SetClass("underline"),
|
||||
]).SetStyle("font-size:small;"),
|
||||
]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none")
|
||||
|
||||
super(
|
||||
new LoginToggle(
|
||||
/*We can show the actual upload button!*/
|
||||
uploadFlow,
|
||||
/* User not logged in*/ t.pleaseLogin.Clone(),
|
||||
state
|
||||
),
|
||||
undefined /* Nothing as the user badge is disabled*/,
|
||||
state?.featureSwitchUserbadge
|
||||
)
|
||||
}
|
||||
}
|
77
src/UI/Image/UploadImage.svelte
Normal file
77
src/UI/Image/UploadImage.svelte
Normal file
|
@ -0,0 +1,77 @@
|
|||
<script lang="ts">/**
|
||||
* Shows an 'upload'-button which will start the upload for this feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import UploadingImageCounter from "./UploadingImageCounter.svelte";
|
||||
import FileSelector from "../Base/FileSelector.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
*/
|
||||
export let image: string = undefined;
|
||||
if (image === "") {
|
||||
image = undefined;
|
||||
}
|
||||
export let labelText: string = undefined;
|
||||
const t = Translations.t.image;
|
||||
|
||||
let licenseStore = state.userRelatedState.imageLicense;
|
||||
|
||||
function handleFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i);
|
||||
console.log("Got file", file.name)
|
||||
try {
|
||||
state.imageUploadManager.uploadImageAndApply(file, tags);
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
<Tr slot="not-logged-in" t={t.pleaseLogin} />
|
||||
<div class="flex flex-col">
|
||||
|
||||
<UploadingImageCounter {state} {tags} />
|
||||
<FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true}
|
||||
on:submit={e => handleFiles(e.detail)}>
|
||||
<div class="flex items-center">
|
||||
|
||||
{#if image !== undefined}
|
||||
<img src={image} />
|
||||
{:else}
|
||||
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} />
|
||||
{/if}
|
||||
{#if labelText}
|
||||
{labelText}
|
||||
{:else}
|
||||
<Tr t={t.addPicture} />
|
||||
{/if}
|
||||
</div>
|
||||
</FileSelector>
|
||||
<div class="text-sm">
|
||||
<Tr t={t.respectPrivacy} />
|
||||
<a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}>
|
||||
<Tr t={t.currentLicense.Subs({license: $licenseStore})} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</LoginToggle>
|
67
src/UI/Image/UploadingImageCounter.svelte
Normal file
67
src/UI/Image/UploadingImageCounter.svelte
Normal file
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">/**
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let tags: Store<OsmTags>;
|
||||
const featureId = tags.data.id;
|
||||
const {
|
||||
uploadStarted,
|
||||
uploadFinished,
|
||||
retried,
|
||||
failed
|
||||
} = state.imageUploadManager.getCountsFor(featureId);
|
||||
const t = Translations.t.image;
|
||||
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted == 1}
|
||||
{#if $uploadFinished == 1 }
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{:else if $failed == 1}
|
||||
<div class="flex flex-col alert">
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
{:else if $retried == 1}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
{:else }
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} />
|
||||
{:else if $uploadFinished == 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} />
|
||||
</Loading>
|
||||
{:else if $uploadFinished > 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted - $uploadFinished, done: $uploadFinished})} />
|
||||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<div class="flex flex-col alert">
|
||||
{#if failed === 1}
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({count: $failed})} />
|
||||
|
||||
{/if}
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
|
@ -1,111 +0,0 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export default class FileSelectorButton extends InputElement<FileList> {
|
||||
private static _nextid = 0
|
||||
private readonly _value = new UIEventSource<FileList>(undefined)
|
||||
private readonly _label: BaseUIElement
|
||||
private readonly _acceptType: string
|
||||
private readonly allowMultiple: boolean
|
||||
private readonly _labelClasses: string
|
||||
|
||||
constructor(
|
||||
label: BaseUIElement,
|
||||
options?: {
|
||||
acceptType: "image/*" | string
|
||||
allowMultiple: true | boolean
|
||||
labelClasses?: string
|
||||
}
|
||||
) {
|
||||
super()
|
||||
this._label = label
|
||||
this._acceptType = options?.acceptType ?? "image/*"
|
||||
this._labelClasses = options?.labelClasses ?? ""
|
||||
this.SetClass("block cursor-pointer")
|
||||
label.SetClass("cursor-pointer")
|
||||
this.allowMultiple = options?.allowMultiple ?? true
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<FileList> {
|
||||
return this._value
|
||||
}
|
||||
|
||||
IsValid(t: FileList): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const self = this
|
||||
const el = document.createElement("form")
|
||||
const label = document.createElement("label")
|
||||
label.appendChild(this._label.ConstructElement())
|
||||
label.classList.add(...this._labelClasses.split(" ").filter((t) => t !== ""))
|
||||
el.appendChild(label)
|
||||
|
||||
const actualInputElement = document.createElement("input")
|
||||
actualInputElement.style.cssText = "display:none"
|
||||
actualInputElement.type = "file"
|
||||
actualInputElement.accept = this._acceptType
|
||||
actualInputElement.name = "picField"
|
||||
actualInputElement.multiple = this.allowMultiple
|
||||
actualInputElement.id = "fileselector" + FileSelectorButton._nextid
|
||||
FileSelectorButton._nextid++
|
||||
|
||||
label.htmlFor = actualInputElement.id
|
||||
|
||||
actualInputElement.onchange = () => {
|
||||
if (actualInputElement.files !== null) {
|
||||
self._value.setData(actualInputElement.files)
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener("submit", (e) => {
|
||||
if (actualInputElement.files !== null) {
|
||||
self._value.setData(actualInputElement.files)
|
||||
}
|
||||
actualInputElement.classList.remove("glowing-shadow")
|
||||
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
el.appendChild(actualInputElement)
|
||||
|
||||
function setDrawAttention(isOn: boolean) {
|
||||
if (isOn) {
|
||||
label.classList.add("glowing-shadow")
|
||||
} else {
|
||||
label.classList.remove("glowing-shadow")
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener("dragover", (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
setDrawAttention(true)
|
||||
// Style the drag-and-drop as a "copy file" operation.
|
||||
event.dataTransfer.dropEffect = "copy"
|
||||
})
|
||||
|
||||
window.document.addEventListener("dragenter", () => {
|
||||
setDrawAttention(true)
|
||||
})
|
||||
|
||||
window.document.addEventListener("dragend", () => {
|
||||
setDrawAttention(false)
|
||||
})
|
||||
|
||||
el.addEventListener("drop", (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
label.classList.remove("glowing-shadow")
|
||||
const fileList = event.dataTransfer.files
|
||||
this._value.setData(fileList)
|
||||
})
|
||||
|
||||
return el
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export default class Slider extends InputElement<number> {
|
||||
private readonly _value: UIEventSource<number>
|
||||
private readonly min: number
|
||||
private readonly max: number
|
||||
private readonly step: number
|
||||
private readonly vertical: boolean
|
||||
|
||||
/**
|
||||
* Constructs a slider input element for natural numbers
|
||||
* @param min: the minimum value that is allowed, inclusive
|
||||
* @param max: the max value that is allowed, inclusive
|
||||
* @param options: value: injectable value; step: the step size of the slider
|
||||
*/
|
||||
constructor(
|
||||
min: number,
|
||||
max: number,
|
||||
options?: {
|
||||
value?: UIEventSource<number>
|
||||
step?: 1 | number
|
||||
vertical?: false | boolean
|
||||
}
|
||||
) {
|
||||
super()
|
||||
this.max = max
|
||||
this.min = min
|
||||
this._value = options?.value ?? new UIEventSource<number>(min)
|
||||
this.step = options?.step ?? 1
|
||||
this.vertical = options?.vertical ?? false
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<number> {
|
||||
return this._value
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("input")
|
||||
el.type = "range"
|
||||
el.min = "" + this.min
|
||||
el.max = "" + this.max
|
||||
el.step = "" + this.step
|
||||
const valuestore = this._value
|
||||
el.oninput = () => {
|
||||
valuestore.setData(Number(el.value))
|
||||
}
|
||||
if (this.vertical) {
|
||||
el.classList.add("vertical")
|
||||
el.setAttribute("orient", "vertical") // firefox only workaround...
|
||||
}
|
||||
valuestore.addCallbackAndRunD((v) => (el.value = "" + valuestore.data))
|
||||
return el
|
||||
}
|
||||
|
||||
IsValid(t: number): boolean {
|
||||
return Math.round(t) == t && t >= this.min && t <= this.max
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue