forked from MapComplete/MapComplete
Merge master
This commit is contained in:
commit
cc4db080aa
45 changed files with 619 additions and 812 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -61,6 +61,33 @@ All notable changes to this project will be documented in this file. See [standa
|
||||||
* some tweaks for the search feature ([bc52c05](https://github.com/pietervdvn/MapComplete/commit/bc52c05a9b47ba6dbf8c3f79a131f8281b8c5197))
|
* some tweaks for the search feature ([bc52c05](https://github.com/pietervdvn/MapComplete/commit/bc52c05a9b47ba6dbf8c3f79a131f8281b8c5197))
|
||||||
* **waste:** add filter for 'recycling centre' ([5da63bf](https://github.com/pietervdvn/MapComplete/commit/5da63bf83aa7d8b230c8dbc082be3fba33344289))
|
* **waste:** add filter for 'recycling centre' ([5da63bf](https://github.com/pietervdvn/MapComplete/commit/5da63bf83aa7d8b230c8dbc082be3fba33344289))
|
||||||
|
|
||||||
|
|
||||||
|
### [0.46.10](https://github.com/USERNAME/REPOSITORY_NAME/compare/v0.46.9...v0.46.10) (2024-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Use panoramax to upload to. Will contain bugs ([0bdc1ae](https://github.com/USERNAME/REPOSITORY_NAME/commits0bdc1aec61ec742d141bb3882be07b6d99df654e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* disable image upload button (see [#2178](https://github.com/pietervdvn/MapComplete/issues/2178)) ([cf74296](https://github.com/USERNAME/REPOSITORY_NAME/commitscf74296d23de9ae6dab902205ebe860490627c00))
|
||||||
|
* filtering for dates now works again ([bea9f66](https://github.com/USERNAME/REPOSITORY_NAME/commitsbea9f66b9aac9d2f13bca74b7a35cde7dd217e12))
|
||||||
|
* fix loading images for CSP, fix [#2161](https://github.com/pietervdvn/MapComplete/issues/2161) ([2569d0c](https://github.com/USERNAME/REPOSITORY_NAME/commits2569d0cb66e411228d9d25cf50dc3278a83d0de5))
|
||||||
|
* search fields in a filter are now wrapped into parentheses, allowing for OR as regex ([fb250fb](https://github.com/USERNAME/REPOSITORY_NAME/commitsfb250fb928da576b5649d398272387da72e89e5c))
|
||||||
|
* studio now handles arrays better (might fix [#2102](https://github.com/pietervdvn/MapComplete/issues/2102)) ([0c9e41a](https://github.com/USERNAME/REPOSITORY_NAME/commits0c9e41a6ce4508ba3bc767f5eb5bd3cdb88201b2))
|
||||||
|
|
||||||
|
|
||||||
|
### Theme improvements
|
||||||
|
|
||||||
|
* **ghostsigns:** streamline ghostsigns theme, fix [#2168](https://github.com/pietervdvn/MapComplete/issues/2168), fix [#2167](https://github.com/pietervdvn/MapComplete/issues/2167) ([392fe3b](https://github.com/USERNAME/REPOSITORY_NAME/commits392fe3b190975b9e3c5cb4aadb4d1543aa686d9e))
|
||||||
|
* **note:** add filter removing anything matching one or more keywords ([9c09da3](https://github.com/USERNAME/REPOSITORY_NAME/commits9c09da3c137a6af88b935108fe55aa8e1163ed2c))
|
||||||
|
* **vending_machine:** add better 'fixme' if freeform for 'vending' is used ([dfce217](https://github.com/USERNAME/REPOSITORY_NAME/commitsdfce217288957be2b27c198d640fd2dd5d53c9fb))
|
||||||
|
|
||||||
|
### [0.46.9](https://github.com/USERNAME/REPOSITORY_NAME/compare/v0.46.8...v0.46.9) (2024-09-14)
|
||||||
|
|
||||||
|
|
||||||
### [0.46.9](https://github.com/pietervdvn/MapComplete/compare/v0.46.8...v0.46.9) (2024-09-14)
|
### [0.46.9](https://github.com/pietervdvn/MapComplete/compare/v0.46.8...v0.46.9) (2024-09-14)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1117,13 +1117,15 @@
|
||||||
{
|
{
|
||||||
"if": "advertising=tilework",
|
"if": "advertising=tilework",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "This is tilework - the advertisement is painted on tiles"
|
"en": "This is tilework - the advertisement is painted on tiles",
|
||||||
|
"de": "Dies ist eine Kachelarbeit - die Werbung ist auf Fliesen gemalt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "advertising=relief",
|
"if": "advertising=relief",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "This is a relief"
|
"en": "This is a relief",
|
||||||
|
"de": "Dies ist ein Relief"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1652,13 +1654,15 @@
|
||||||
"if": "historic=advertising",
|
"if": "historic=advertising",
|
||||||
"alsoShowIf": "historic=yes",
|
"alsoShowIf": "historic=yes",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "This is a historic advertisement sign (an advertisement for a business that no longer exists or a very old sign with heritage value)"
|
"en": "This is a historic advertisement sign (an advertisement for a business that no longer exists or a very old sign with heritage value)",
|
||||||
|
"de": "Es handelt sich um ein historisches Werbeschild (eine Werbung für ein Unternehmen, das nicht mehr existiert, oder ein sehr altes Schild mit historischem Wert)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "historic=",
|
"if": "historic=",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "This advertisement sign has no historic value (the business still exists and has no heritage value)"
|
"en": "This advertisement sign has no historic value (the business still exists and has no heritage value)",
|
||||||
|
"de": "Dieses Werbeschild hat keinen historischen Wert (das Unternehmen existiert noch und hat keinen denkmalpflegerischen Wert)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -577,7 +577,7 @@
|
||||||
"title": "Straßenbilder in der Nähe"
|
"title": "Straßenbilder in der Nähe"
|
||||||
},
|
},
|
||||||
"pleaseLogin": "Bitte anmelden, um ein Bild hinzuzufügen",
|
"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.",
|
"respectPrivacy": "Laden Sie keine Bilder von Google Maps, Google Streetview oder anderen urheberrechtlich geschützten Quellen hoch.",
|
||||||
"toBig": "Ihr Bild ist mit {actual_size} zu groß. Die maximale Bildgröße ist {max_size}",
|
"toBig": "Ihr Bild ist mit {actual_size} zu groß. Die maximale Bildgröße ist {max_size}",
|
||||||
"upload": {
|
"upload": {
|
||||||
"failReasons": "Keine Internetverbindung",
|
"failReasons": "Keine Internetverbindung",
|
||||||
|
@ -709,7 +709,7 @@
|
||||||
"preset_type": {
|
"preset_type": {
|
||||||
"question": "Von welcher Art ist dieses Objekt?",
|
"question": "Von welcher Art ist dieses Objekt?",
|
||||||
"typeDescription": "Dies ist <b>{title}</b>. <div class='subtle'>{description}</div>",
|
"typeDescription": "Dies ist <b>{title}</b>. <div class='subtle'>{description}</div>",
|
||||||
"typeTitle": "Dies ist {title}"
|
"typeTitle": "Dies ist <b>{title}</b>"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"editingIntro": "Ihre Änderungen werden auf OpenStreetMap gespeichert und sind öffentlich zugänglich. Ein mit MapComplete erstellter Änderungssatz enthält folgende Daten:",
|
"editingIntro": "Ihre Änderungen werden auf OpenStreetMap gespeichert und sind öffentlich zugänglich. Ein mit MapComplete erstellter Änderungssatz enthält folgende Daten:",
|
||||||
|
|
|
@ -593,7 +593,8 @@
|
||||||
"title": "Nearby streetview imagery"
|
"title": "Nearby streetview imagery"
|
||||||
},
|
},
|
||||||
"pleaseLogin": "Please log in to add a picture",
|
"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.",
|
"processing": "The server is processing your image",
|
||||||
|
"respectPrivacy": "Do not upload from 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}",
|
"toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}",
|
||||||
"upload": {
|
"upload": {
|
||||||
"failReasons": "You might have lost connection to the internet",
|
"failReasons": "You might have lost connection to the internet",
|
||||||
|
|
|
@ -397,7 +397,7 @@
|
||||||
"seeNearby": "Buscar y enlazar fotos cercanas"
|
"seeNearby": "Buscar y enlazar fotos cercanas"
|
||||||
},
|
},
|
||||||
"pleaseLogin": "Acceda para cargar una imagen",
|
"pleaseLogin": "Acceda para cargar una imagen",
|
||||||
"respectPrivacy": "No fotografíe personas ni matrículas. No cargue datos de Google Maps, Google StreetView u otras fuentes protegidas por derechos de autor.",
|
"respectPrivacy": "No cargue datos desde Google Maps, Google StreetView u otras fuentes protegidas por derechos de autor.",
|
||||||
"toBig": "Tu imagen es demasiado grande, ya que pesa {actual_size}. Por favor utiliza imágenes de como máximo {max_size}",
|
"toBig": "Tu imagen es demasiado grande, ya que pesa {actual_size}. Por favor utiliza imágenes de como máximo {max_size}",
|
||||||
"uploadDone": "Se ha añadido la imagen. Gracias por ayudar!",
|
"uploadDone": "Se ha añadido la imagen. Gracias por ayudar!",
|
||||||
"uploadFailed": "No se pudo cargar la imagen. ¿Tiene Internet y se permiten las API de terceros? El navegador Brave o uMatrix podría bloquearlas.",
|
"uploadFailed": "No se pudo cargar la imagen. ¿Tiene Internet y se permiten las API de terceros? El navegador Brave o uMatrix podría bloquearlas.",
|
||||||
|
|
|
@ -8957,23 +8957,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "Les imatges que feu tindran llicència <b>CC0</b> i s'afegiran al domini públic. Això vol dir que tothom pot utilitzar les vostres imatges per a qualsevol propòsit. <span class='subtle'>Aquesta és l'opció predeterminada. </span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "Les imatges que feu tindran llicència <b>CC0</b> i s'afegiran al domini públic. Això vol dir que tothom pot utilitzar les vostres imatges per a qualsevol propòsit."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "Les fotografies que facis es publicaran sota <b>CC-BY 4.0</b> que requereix que qualsevol que utilitzi la vostra imatge us ha de donar crèdits"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Les imatges que feu tindran una llicència amb <b>CC-BY-SA 4.0</b> el que significa que tothom que utilitzi la vostra imatge us ha d'atribuir i que els derivats de la vostra imatge s'han de tornar a compartir amb la mateixa llicència."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Sota quina llicència vols publicar les teves fotos?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -8981,23 +8981,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "Pořízené fotografie budou licencovány pod <b>CC0</b> a přidány do veřejné domény. To znamená, že kdokoli může vaše snímky použít k jakémukoli účelu. <span class='subtle'>Toto je výchozí volba.</span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "Pořízené fotografie budou licencovány pod <b>CC0</b> a přidány do veřejné domény. To znamená, že kdokoli může vaše snímky použít k jakémukoli účelu."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "Pořízené fotografie budou licencovány pod <b>CC-BY 4.0</b>, což vyžaduje, aby vás uvedl každý, kdo použije vaší fotku"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Pořízené fotografie budou licencovány pod <b>CC-BY-SA 4.0</b>, což vyžaduje, aby vás uvedl každý, kdo použije vaší fotku a že odvozené fotky musí být dále sdíleny se stejnou licencí."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Pod jakou licencí chcete své fotografie zveřejnit?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -2757,14 +2757,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"1": {
|
|
||||||
"then": "Billeder, som du har taget, vil blive udgivet under <b>CC0</b>-licensen og lagt ud i fælleseje. Det betyder, at alle kan bruge dine billeder til ethvert formål."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Under hvilken licens vil du frigive dine billeder?"
|
|
||||||
},
|
|
||||||
"settings-link": {
|
"settings-link": {
|
||||||
"render": {
|
"render": {
|
||||||
"special": {
|
"special": {
|
||||||
|
|
|
@ -116,6 +116,14 @@
|
||||||
"question": "Werden mehrere Werbungen abwechselnd angezeigt?"
|
"question": "Werden mehrere Werbungen abwechselnd angezeigt?"
|
||||||
},
|
},
|
||||||
"historic": {
|
"historic": {
|
||||||
|
"mappings": {
|
||||||
|
"0": {
|
||||||
|
"then": "Es handelt sich um ein historisches Werbeschild (eine Werbung für ein Unternehmen, das nicht mehr existiert, oder ein sehr altes Schild mit historischem Wert)"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"then": "Dieses Werbeschild hat keinen historischen Wert (das Unternehmen existiert noch und hat keinen denkmalpflegerischen Wert)"
|
||||||
|
}
|
||||||
|
},
|
||||||
"question": "Ist dieses Schild für ein Geschäft, das nicht mehr existiert oder nicht mehr gepflegt wird?"
|
"question": "Ist dieses Schild für ein Geschäft, das nicht mehr existiert oder nicht mehr gepflegt wird?"
|
||||||
},
|
},
|
||||||
"luminous_or_lit_advertising": {
|
"luminous_or_lit_advertising": {
|
||||||
|
@ -181,6 +189,12 @@
|
||||||
"10": {
|
"10": {
|
||||||
"then": "Dies ist eine Wandmalerei"
|
"then": "Dies ist eine Wandmalerei"
|
||||||
},
|
},
|
||||||
|
"11": {
|
||||||
|
"then": "Dies ist eine Kachelarbeit - die Werbung ist auf Fliesen gemalt"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"then": "Dies ist ein Relief"
|
||||||
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Dies ist eine Litfaßsäule"
|
"then": "Dies ist eine Litfaßsäule"
|
||||||
},
|
},
|
||||||
|
@ -11661,23 +11675,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "Die von Ihnen aufgenommenen Bilder werden mit <b>CC0</b> lizenziert und der Public Domain hinzugefügt. Das bedeutet, dass jeder Ihre Bilder für jeden Zweck verwenden kann. <span class='subtle'>Dies ist die Standardeinstellung.</span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "Ihre aufgenommenen Bilder werden mit <b>CC0</b> lizenziert und der Public Domain hinzugefügt. Das bedeutet, dass jeder Ihre Bilder für jeden Zweck verwenden kann."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "Die von Ihnen aufgenommenen Bilder werden mit <b>CC-BY 4.0</b> lizenziert, was bedeutet, dass jeder, der Ihr Bild verwendet, Sie als Urheber nennen muss"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Die von Ihnen aufgenommenen Bilder werden mit <b>CC-BY-SA 4.0</b> lizenziert, was bedeutet, dass jeder, der Ihr Bild verwendet, Sie als Urheber nennen muss und dass Ableitungen Ihres Bildes mit der gleichen Lizenz weitergegeben werden müssen."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -11767,23 +11767,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "Pictures you take will be licensed with <b>CC0</b> and added to the public domain. This means that everyone can use your pictures for any purpose. <span class='subtle'>This is the default choice.</span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "Pictures you take will be licensed with <b>CC0</b> and added to the public domain. This means that everyone can use your pictures for any purpose."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "Pictures you take will be licensed with <b>CC-BY 4.0</b> which requires everyone using your picture that they have to attribute you"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Pictures you take will be licensed with <b>CC-BY-SA 4.0</b> which means that everyone using your picture must attribute you and that derivatives of your picture must be reshared with the same license."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Under what license do you want to publish your pictures?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -4923,13 +4923,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"1": {
|
|
||||||
"then": "Las fotografías que tome tendrán una licencia con <b>CC0</b> y se agregarán al dominio público. Esto significa que todos pueden usar sus imágenes para cualquier propósito."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"translation-completeness": {
|
"translation-completeness": {
|
||||||
"render": "Las traducciones para {_theme} en {_language} están al {_translation_percentage}%: {_translation_translated_count} cadenas de {_translation_total} están traducidas"
|
"render": "Las traducciones para {_theme} en {_language} están al {_translation_percentage}%: {_translation_translated_count} cadenas de {_translation_total} están traducidas"
|
||||||
},
|
},
|
||||||
|
|
|
@ -7124,16 +7124,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"1": {
|
|
||||||
"then": "Les photos que vous avez ajoutées seront sous licence <b>CC0</b> et mises dans le domaine public. Cela signifie que n'importe qui pourra les utiliser, quel qu'en soit l'usage."
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Les photos que vous prenez seront sous la licence <b>CC-BY-SA 4.0</b> ce qui signifie que quiconque utilisant votre photo doit vous créditer et que les modifications apportées à votre photo doivent être repartagées avec la même licence."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"show_tags": {
|
"show_tags": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -9229,23 +9229,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de <b>CC0</b>-licentie en dus aan het publieke domein toegevoegd worden. Dit betekent dat iedereen je afbeeldingen kan gebruiken voor elk mogelijks gebruik. <span class='subtle'>Dit is de standaard-instelling</span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de <b>CC0</b>-licentie en dus aan het publieke domein toegevoegd worden. Dit betekent dat iedereen je afbeeldingen kan gebruiken voor elk mogelijks gebruik."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de <b>CC-BY 4.0</b>-licentie. Dit betekent dat iedereen je afbeelding mag gebruiken voor elke toepassing mits het vermelden van je naam"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de <b>CC-BY-SA 4.0</b>-licentie. Dit betekent dat iedereen je afbeelding mag gebruiken voor elke toepassing mits het vermelden van je naam en dat afgeleide werken van je afbeelding ook ondere deze licentie moeten gepubliceerd worden."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Met welke licentie wil je je afbeeldingen toevoegen?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -1912,23 +1912,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"0": {
|
|
||||||
"then": "As fotos que você tirar serão licenciadas com <b>CC0</b> e adicionadas ao domínio público. Isso significa que todos podem usar suas fotos para qualquer finalidade. <span class='subtle'>Esta é a escolha padrão.</span>"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"then": "As fotos que você tirar serão licenciadas com <b>CC0</b> e adicionadas ao domínio público. Isso significa que todos podem usar suas fotos para qualquer finalidade."
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"then": "As fotos que você tirar serão licenciadas com <b>CC-BY 4.0</b>, que exige que todos que usam sua foto atribuam a você"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "As fotos que você tirar serão licenciadas com <b>CC-BY-SA 4.0</b>, o que significa que todos que usarem sua foto devem atribuí-lo e que os derivados de sua foto devem ser compartilhados novamente com a mesma licença."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"question": "Sob que licença você deseja publicar suas fotos?"
|
|
||||||
},
|
|
||||||
"profile-description": {
|
"profile-description": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"0": {
|
"0": {
|
||||||
|
|
|
@ -2063,17 +2063,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usersettings": {
|
|
||||||
"tagRenderings": {
|
|
||||||
"picture-license": {
|
|
||||||
"mappings": {
|
|
||||||
"1": {
|
|
||||||
"then": "Изображения будут опубликованы под лицензией <b>CC0</b> и перейдут в общественное достояние. Это значит, что кто угодно имеет право использовать их без ограничений."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vending_machine": {
|
"vending_machine": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"operational_status": {
|
"operational_status": {
|
||||||
|
|
88
package-lock.json
generated
88
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.47.2",
|
"version": "0.47.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.47.2",
|
"version": "0.47.3",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@comunica/core": "^3.0.1",
|
"@comunica/core": "^3.0.1",
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.0.5",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"exifreader": "^4.23.5",
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"flowbite-svelte": "^0.46.2",
|
"flowbite-svelte": "^0.46.2",
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
"opening_hours": "^3.6.0",
|
"opening_hours": "^3.6.0",
|
||||||
"osm-auth": "^2.5.0",
|
"osm-auth": "^2.5.0",
|
||||||
"osmtogeojson": "^3.0.0-beta.5",
|
"osmtogeojson": "^3.0.0-beta.5",
|
||||||
|
"panoramax-js": "^0.1.4",
|
||||||
"panzoom": "^9.4.3",
|
"panzoom": "^9.4.3",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.3.1",
|
||||||
"pbf": "^3.2.1",
|
"pbf": "^3.2.1",
|
||||||
|
@ -4908,6 +4910,19 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ogcapi-js/features": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ogcapi-js/features/-/features-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-/w6kFvAXWO+F0/nLC5m11tuOw0LX+gVz/OCLiDkElXO9ko9F9OA3AbzKZxJaE5Buu0KUGn+TRxS6w1xhZc4KRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ogcapi-js/shared": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ogcapi-js/shared": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ogcapi-js/shared/-/shared-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-EQ6T4iVXwIMnBcdpR2C0YnNNCxtNWHpWg0Hs9uEvH4BPZI2xT87gV+WRw8/hYAe8EtrK6j57iluBoSyHiAQweQ=="
|
||||||
|
},
|
||||||
"node_modules/@parcel/service-worker": {
|
"node_modules/@parcel/service-worker": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -10048,6 +10063,24 @@
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exifreader": {
|
||||||
|
"version": "4.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.23.5.tgz",
|
||||||
|
"integrity": "sha512-Gy9FXSBW+4ivu4aNtthGHAPEfVJ72z4aN9Iusr3YiIOy+ZCh7NWfoswCXZV/CH8MpOJE2Ij4hmmKQPGvo4Vf9g==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.8.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/exifreader/node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||||
|
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expand-template": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -15960,6 +15993,17 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/panoramax-js": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ogcapi-js/features": "^1.1.1",
|
||||||
|
"@ogcapi-js/shared": "^1.1.1",
|
||||||
|
"@types/geojson": "^7946.0.14",
|
||||||
|
"json-schema": "^0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/panzoom": {
|
"node_modules/panzoom": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -24680,6 +24724,19 @@
|
||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@ogcapi-js/features": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ogcapi-js/features/-/features-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-/w6kFvAXWO+F0/nLC5m11tuOw0LX+gVz/OCLiDkElXO9ko9F9OA3AbzKZxJaE5Buu0KUGn+TRxS6w1xhZc4KRA==",
|
||||||
|
"requires": {
|
||||||
|
"@ogcapi-js/shared": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ogcapi-js/shared": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ogcapi-js/shared/-/shared-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-EQ6T4iVXwIMnBcdpR2C0YnNNCxtNWHpWg0Hs9uEvH4BPZI2xT87gV+WRw8/hYAe8EtrK6j57iluBoSyHiAQweQ=="
|
||||||
|
},
|
||||||
"@parcel/service-worker": {
|
"@parcel/service-worker": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.2",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
@ -28121,6 +28178,22 @@
|
||||||
"events": {
|
"events": {
|
||||||
"version": "3.3.0"
|
"version": "3.3.0"
|
||||||
},
|
},
|
||||||
|
"exifreader": {
|
||||||
|
"version": "4.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.23.5.tgz",
|
||||||
|
"integrity": "sha512-Gy9FXSBW+4ivu4aNtthGHAPEfVJ72z4aN9Iusr3YiIOy+ZCh7NWfoswCXZV/CH8MpOJE2Ij4hmmKQPGvo4Vf9g==",
|
||||||
|
"requires": {
|
||||||
|
"@xmldom/xmldom": "^0.8.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": {
|
||||||
|
"version": "0.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||||
|
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"expand-template": {
|
"expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
@ -31983,6 +32056,17 @@
|
||||||
"packet-reader": {
|
"packet-reader": {
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
|
"panoramax-js": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==",
|
||||||
|
"requires": {
|
||||||
|
"@ogcapi-js/features": "^1.1.1",
|
||||||
|
"@ogcapi-js/shared": "^1.1.1",
|
||||||
|
"@types/geojson": "^7946.0.14",
|
||||||
|
"json-schema": "^0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"panzoom": {
|
"panzoom": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.47.2",
|
"version": "0.47.3",
|
||||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||||
"description": "A small website to edit OSM easily",
|
"description": "A small website to edit OSM easily",
|
||||||
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
||||||
|
@ -41,6 +41,10 @@
|
||||||
"imgur": "7070e7167f0a25a",
|
"imgur": "7070e7167f0a25a",
|
||||||
"mapillary_v4": "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
"mapillary_v4": "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
},
|
},
|
||||||
|
"panoramax": {
|
||||||
|
"url": "https://panoramax.mapcomplete.org",
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnZW92aXNpbyIsInN1YiI6IjU5ZjgzOGI0LTM4ZjAtNDdjYi04OWYyLTM3NDQ3MWMxNTUxOCJ9.0rBioZS_48NTjnkIyN9497c3fQdTqtGgH1HDqlz1bWs"
|
||||||
|
},
|
||||||
"default_overpass_urls": [
|
"default_overpass_urls": [
|
||||||
"https://overpass-api.de/api/interpreter",
|
"https://overpass-api.de/api/interpreter",
|
||||||
"https://overpass.private.coffee/api/interpreter",
|
"https://overpass.private.coffee/api/interpreter",
|
||||||
|
@ -180,6 +184,7 @@
|
||||||
"dompurify": "^3.0.5",
|
"dompurify": "^3.0.5",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"exifreader": "^4.23.5",
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"flowbite-svelte": "^0.46.2",
|
"flowbite-svelte": "^0.46.2",
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
|
@ -203,6 +208,7 @@
|
||||||
"opening_hours": "^3.6.0",
|
"opening_hours": "^3.6.0",
|
||||||
"osm-auth": "^2.5.0",
|
"osm-auth": "^2.5.0",
|
||||||
"osmtogeojson": "^3.0.0-beta.5",
|
"osmtogeojson": "^3.0.0-beta.5",
|
||||||
|
"panoramax-js": "^0.1.4",
|
||||||
"panzoom": "^9.4.3",
|
"panzoom": "^9.4.3",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.3.1",
|
||||||
"pbf": "^3.2.1",
|
"pbf": "^3.2.1",
|
||||||
|
|
|
@ -5,6 +5,8 @@ import GenericImageProvider from "./GenericImageProvider"
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||||
import { WikidataImageProvider } from "./WikidataImageProvider"
|
import { WikidataImageProvider } from "./WikidataImageProvider"
|
||||||
|
import Panoramax from "./Panoramax"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic 'from the interwebz' image picker, without attribution
|
* A generic 'from the interwebz' image picker, without attribution
|
||||||
|
@ -28,6 +30,7 @@ export default class AllImageProviders {
|
||||||
Mapillary.singleton,
|
Mapillary.singleton,
|
||||||
WikidataImageProvider.singleton,
|
WikidataImageProvider.singleton,
|
||||||
WikimediaImageProvider.singleton,
|
WikimediaImageProvider.singleton,
|
||||||
|
Panoramax.singleton,
|
||||||
AllImageProviders.genericImageProvider,
|
AllImageProviders.genericImageProvider,
|
||||||
]
|
]
|
||||||
public static apiUrls: string[] = [].concat(
|
public static apiUrls: string[] = [].concat(
|
||||||
|
@ -41,11 +44,8 @@ export default class AllImageProviders {
|
||||||
mapillary: Mapillary.singleton,
|
mapillary: Mapillary.singleton,
|
||||||
wikidata: WikidataImageProvider.singleton,
|
wikidata: WikidataImageProvider.singleton,
|
||||||
wikimedia: WikimediaImageProvider.singleton,
|
wikimedia: WikimediaImageProvider.singleton,
|
||||||
|
panoramax: Panoramax.singleton
|
||||||
}
|
}
|
||||||
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<
|
|
||||||
string,
|
|
||||||
UIEventSource<ProvidedImage[]>
|
|
||||||
>()
|
|
||||||
|
|
||||||
public static byName(name: string) {
|
public static byName(name: string) {
|
||||||
return AllImageProviders.providersByName[name.toLowerCase()]
|
return AllImageProviders.providersByName[name.toLowerCase()]
|
||||||
|
@ -66,45 +66,32 @@ export default class AllImageProviders {
|
||||||
return AllImageProviders.genericImageProvider
|
return AllImageProviders.genericImageProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to extract all image data for this image
|
||||||
|
*/
|
||||||
public static LoadImagesFor(
|
public static LoadImagesFor(
|
||||||
tags: Store<Record<string, string>>,
|
tags: Store<Record<string, string>>,
|
||||||
tagKey?: string[]
|
tagKey?: string[]
|
||||||
): Store<ProvidedImage[]> {
|
): Store<ProvidedImage[]> {
|
||||||
if (tags.data.id === undefined) {
|
if (tags?.data?.id === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = tags.data.id + tagKey
|
|
||||||
const cached = this._cache.get(cacheKey)
|
|
||||||
if (cached !== undefined) {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = new UIEventSource([])
|
const source = new UIEventSource([])
|
||||||
this._cache.set(cacheKey, source)
|
|
||||||
const allSources: Store<ProvidedImage[]>[] = []
|
const allSources: Store<ProvidedImage[]>[] = []
|
||||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||||
let prefixes = imageProvider.defaultKeyPrefixes
|
/*
|
||||||
if (tagKey !== undefined) {
|
By default, 'GetRelevantUrls' uses the defaultKeyPrefixes.
|
||||||
prefixes = tagKey
|
However, we override them if a custom image tag is set, e.g. 'image:menu'
|
||||||
}
|
*/
|
||||||
|
const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes
|
||||||
const singleSource = imageProvider.GetRelevantUrls(tags, {
|
const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes))
|
||||||
prefixes: prefixes,
|
|
||||||
})
|
|
||||||
allSources.push(singleSource)
|
allSources.push(singleSource)
|
||||||
singleSource.addCallbackAndRunD((_) => {
|
singleSource.addCallbackAndRunD((_) => {
|
||||||
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
|
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
|
||||||
const uniq = []
|
const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url)
|
||||||
const seen = new Set<string>()
|
source.set(dedup)
|
||||||
for (const img of all) {
|
|
||||||
if (seen.has(img.url)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen.add(img.url)
|
|
||||||
uniq.push(img)
|
|
||||||
}
|
|
||||||
source.setData(uniq)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return source
|
return source
|
||||||
|
|
|
@ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider {
|
||||||
this._valuePrefixBlacklist = valuePrefixBlacklist
|
this._valuePrefixBlacklist = valuePrefixBlacklist
|
||||||
}
|
}
|
||||||
|
|
||||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
ExtractUrls(key: string, value: string): undefined | ProvidedImage[] {
|
||||||
if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) {
|
if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) {
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(value)
|
new URL(value)
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Not a valid URL
|
// Not a valid URL
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [{
|
||||||
Promise.resolve({
|
key: key,
|
||||||
key: key,
|
url: value,
|
||||||
url: value,
|
provider: this,
|
||||||
provider: this,
|
id: value,
|
||||||
id: value,
|
}]
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceIcon() {
|
SourceIcon() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import BaseUIElement from "../../UI/BaseUIElement"
|
import BaseUIElement from "../../UI/BaseUIElement"
|
||||||
import { LicenseInfo } from "./LicenseInfo"
|
import { LicenseInfo } from "./LicenseInfo"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
@ -10,6 +10,7 @@ export interface ProvidedImage {
|
||||||
provider: ImageProvider
|
provider: ImageProvider
|
||||||
id: string
|
id: string
|
||||||
date?: Date,
|
date?: Date,
|
||||||
|
status?: string | "ready"
|
||||||
/**
|
/**
|
||||||
* Compass angle of the taken image
|
* Compass angle of the taken image
|
||||||
* 0 = north, 90° = East
|
* 0 = north, 90° = East
|
||||||
|
@ -26,56 +27,45 @@ export default abstract class ImageProvider {
|
||||||
|
|
||||||
public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement
|
public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a properties object, maps it onto _all_ the available pictures for this imageProvider.
|
* Gets all the relevant URLS for the given tags and for the given prefixes;
|
||||||
* This iterates over _all_ tags and matches _anything_ that might be an image
|
* extracts the necessary information
|
||||||
|
* @param tags
|
||||||
|
* @param prefixes
|
||||||
*/
|
*/
|
||||||
public GetRelevantUrls(
|
public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> {
|
||||||
allTags: Store<any>,
|
const relevantUrls: ProvidedImage[] = []
|
||||||
options?: {
|
|
||||||
prefixes?: string[]
|
|
||||||
}
|
|
||||||
): UIEventSource<ProvidedImage[]> {
|
|
||||||
const prefixes = options?.prefixes ?? this.defaultKeyPrefixes
|
|
||||||
if (prefixes === undefined) {
|
|
||||||
throw "No `defaultKeyPrefixes` defined by this image provider"
|
|
||||||
}
|
|
||||||
const relevantUrls = new UIEventSource<
|
|
||||||
{ id: string; url: string; key: string; provider: ImageProvider }[]
|
|
||||||
>([])
|
|
||||||
const seenValues = new Set<string>()
|
const seenValues = new Set<string>()
|
||||||
allTags.addCallbackAndRunD((tags) => {
|
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
if (!prefixes.some((prefix) => key.startsWith(prefix))) {
|
if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
|
||||||
|
for (const value of values) {
|
||||||
|
if (seenValues.has(value)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
|
seenValues.add(value)
|
||||||
for (const value of values) {
|
let images = this.ExtractUrls(key, value)
|
||||||
if (seenValues.has(value)) {
|
if(!Array.isArray(images)){
|
||||||
continue
|
images = await images
|
||||||
}
|
}
|
||||||
seenValues.add(value)
|
if(images){
|
||||||
this.ExtractUrls(key, value).then((promises) => {
|
relevantUrls.push(...images)
|
||||||
for (const promise of promises ?? []) {
|
|
||||||
if (promise === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
promise.then((providedImage) => {
|
|
||||||
if (providedImage === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
relevantUrls.data.push(providedImage)
|
|
||||||
relevantUrls.ping()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return relevantUrls
|
return relevantUrls
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>
|
public getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||||
|
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
|
||||||
|
|
||||||
public abstract DownloadAttribution(providedImage: {
|
public abstract DownloadAttribution(providedImage: {
|
||||||
url: string
|
url: string
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { Changes } from "../Osm/Changes"
|
||||||
import Translations from "../../UI/i18n/Translations"
|
import Translations from "../../UI/i18n/Translations"
|
||||||
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
|
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
|
||||||
import { Translation } from "../../UI/i18n/Translation"
|
import { Translation } from "../../UI/i18n/Translation"
|
||||||
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
|
import { GeoOperations } from "../GeoOperations"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ImageUploadManager has a
|
* The ImageUploadManager has a
|
||||||
|
@ -17,7 +19,8 @@ export class ImageUploadManager {
|
||||||
private readonly _uploader: ImageUploader
|
private readonly _uploader: ImageUploader
|
||||||
private readonly _featureProperties: FeaturePropertiesStore
|
private readonly _featureProperties: FeaturePropertiesStore
|
||||||
private readonly _layout: LayoutConfig
|
private readonly _layout: LayoutConfig
|
||||||
|
private readonly _indexedFeatures: IndexedFeatureSource
|
||||||
|
private readonly _gps: Store<GeolocationCoordinates | undefined>
|
||||||
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map()
|
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map()
|
||||||
private readonly _uploadFinished: 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 _uploadFailed: Map<string, UIEventSource<number>> = new Map()
|
||||||
|
@ -32,13 +35,17 @@ export class ImageUploadManager {
|
||||||
uploader: ImageUploader,
|
uploader: ImageUploader,
|
||||||
featureProperties: FeaturePropertiesStore,
|
featureProperties: FeaturePropertiesStore,
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
changes: Changes
|
changes: Changes,
|
||||||
|
gpsLocation: Store<GeolocationCoordinates | undefined>,
|
||||||
|
allFeatures: IndexedFeatureSource,
|
||||||
) {
|
) {
|
||||||
this._uploader = uploader
|
this._uploader = uploader
|
||||||
this._featureProperties = featureProperties
|
this._featureProperties = featureProperties
|
||||||
this._layout = layout
|
this._layout = layout
|
||||||
this._osmConnection = osmConnection
|
this._osmConnection = osmConnection
|
||||||
this._changes = changes
|
this._changes = changes
|
||||||
|
this._indexedFeatures = allFeatures
|
||||||
|
this._gps = gpsLocation
|
||||||
|
|
||||||
const failed = this.getCounterFor(this._uploadFailed, "*")
|
const failed = this.getCounterFor(this._uploadFailed, "*")
|
||||||
const done = this.getCounterFor(this._uploadFinished, "*")
|
const done = this.getCounterFor(this._uploadFinished, "*")
|
||||||
|
@ -47,7 +54,7 @@ export class ImageUploadManager {
|
||||||
(startedCount) => {
|
(startedCount) => {
|
||||||
return startedCount > failed.data + done.data
|
return startedCount > failed.data + done.data
|
||||||
},
|
},
|
||||||
[failed, done]
|
[failed, done],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +62,7 @@ export class ImageUploadManager {
|
||||||
* Gets various counters.
|
* Gets various counters.
|
||||||
* Note that counters can only increase
|
* Note that counters can only increase
|
||||||
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
|
* 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
|
* @param featureId the id of the feature you want information for. '*' has a global counter
|
||||||
*/
|
*/
|
||||||
public getCountsFor(featureId: string | "*"): {
|
public getCountsFor(featureId: string | "*"): {
|
||||||
retried: Store<number>
|
retried: Store<number>
|
||||||
|
@ -96,7 +103,7 @@ export class ImageUploadManager {
|
||||||
public async uploadImageAndApply(
|
public async uploadImageAndApply(
|
||||||
file: File,
|
file: File,
|
||||||
tagsStore: UIEventSource<OsmTags>,
|
tagsStore: UIEventSource<OsmTags>,
|
||||||
targetKey?: string
|
targetKey?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const canBeUploaded = this.canBeUploaded(file)
|
const canBeUploaded = this.canBeUploaded(file)
|
||||||
if (canBeUploaded !== true) {
|
if (canBeUploaded !== true) {
|
||||||
|
@ -105,28 +112,15 @@ export class ImageUploadManager {
|
||||||
|
|
||||||
const tags = tagsStore.data
|
const tags = tagsStore.data
|
||||||
const featureId = <OsmId>tags.id
|
const featureId = <OsmId>tags.id
|
||||||
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0")
|
|
||||||
const license = licenseStore?.data ?? "CC0"
|
|
||||||
|
|
||||||
const matchingLayer = this._layout?.getMatchingLayer(tags)
|
const author = this._osmConnection.userDetails.data.name
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
const action = await this.uploadImageWithLicense(
|
const action = await this.uploadImageWithLicense(
|
||||||
featureId,
|
featureId,
|
||||||
title,
|
author,
|
||||||
description,
|
|
||||||
file,
|
file,
|
||||||
targetKey,
|
targetKey,
|
||||||
tags?.data?.["_orig_theme"]
|
tags?.data?.["_orig_theme"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!action) {
|
if (!action) {
|
||||||
|
@ -146,23 +140,31 @@ export class ImageUploadManager {
|
||||||
|
|
||||||
private async uploadImageWithLicense(
|
private async uploadImageWithLicense(
|
||||||
featureId: OsmId,
|
featureId: OsmId,
|
||||||
title: string,
|
author: string,
|
||||||
description: string,
|
|
||||||
blob: File,
|
blob: File,
|
||||||
targetKey: string | undefined,
|
targetKey: string | undefined,
|
||||||
theme?: string
|
theme?: string,
|
||||||
): Promise<LinkImageAction> {
|
): Promise<LinkImageAction> {
|
||||||
this.increaseCountFor(this._uploadStarted, featureId)
|
this.increaseCountFor(this._uploadStarted, featureId)
|
||||||
const properties = this._featureProperties.getStore(featureId)
|
const properties = this._featureProperties.getStore(featureId)
|
||||||
let key: string
|
let key: string
|
||||||
let value: string
|
let value: string
|
||||||
|
let location: [number, number] = undefined
|
||||||
|
if (this._gps.data) {
|
||||||
|
location = [this._gps.data.longitude, this._gps.data.latitude]
|
||||||
|
}
|
||||||
|
if (location === undefined || location?.some(l => l === undefined)) {
|
||||||
|
const feature = this._indexedFeatures.featuresById.data.get(featureId)
|
||||||
|
location = GeoOperations.centerpointCoordinates(feature)
|
||||||
|
}
|
||||||
|
let absoluteUrl: string
|
||||||
try {
|
try {
|
||||||
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
|
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(blob, location, author))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.increaseCountFor(this._uploadRetried, featureId)
|
this.increaseCountFor(this._uploadRetried, featureId)
|
||||||
console.error("Could not upload image, trying again:", e)
|
console.error("Could not upload image, trying again:", e)
|
||||||
try {
|
try {
|
||||||
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
|
;({ key, value , absoluteUrl} = await this._uploader.uploadImage(blob, location, author))
|
||||||
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
|
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could again not upload image due to", e)
|
console.error("Could again not upload image due to", e)
|
||||||
|
@ -172,12 +174,15 @@ export class ImageUploadManager {
|
||||||
}
|
}
|
||||||
console.log("Uploading image done, creating action for", featureId)
|
console.log("Uploading image done, creating action for", featureId)
|
||||||
key = targetKey ?? key
|
key = targetKey ?? key
|
||||||
|
if(targetKey){
|
||||||
|
// This is a non-standard key, so we use the image link directly
|
||||||
|
value = absoluteUrl
|
||||||
|
}
|
||||||
this.increaseCountFor(this._uploadFinished, featureId)
|
this.increaseCountFor(this._uploadFinished, featureId)
|
||||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
return new LinkImageAction(featureId, key, value, properties, {
|
||||||
theme: theme ?? this._layout.id,
|
theme: theme ?? this._layout.id,
|
||||||
changeType: "add-image",
|
changeType: "add-image",
|
||||||
})
|
})
|
||||||
return action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||||
|
|
|
@ -3,13 +3,10 @@ export interface ImageUploader {
|
||||||
/**
|
/**
|
||||||
* Uploads the 'blob' as image, with some metadata.
|
* Uploads the 'blob' as image, with some metadata.
|
||||||
* Returns the URL to be linked + the appropriate key to add this to OSM
|
* Returns the URL to be linked + the appropriate key to add this to OSM
|
||||||
* @param title
|
|
||||||
* @param description
|
|
||||||
* @param blob
|
|
||||||
*/
|
*/
|
||||||
uploadImage(
|
uploadImage(
|
||||||
title: string,
|
blob: File,
|
||||||
description: string,
|
currentGps: [number,number],
|
||||||
blob: File
|
author: string
|
||||||
): Promise<{ key: string; value: string }>
|
): Promise<{ key: string; value: string, absoluteUrl: string }>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,12 @@ import BaseUIElement from "../../UI/BaseUIElement"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { LicenseInfo } from "./LicenseInfo"
|
import { LicenseInfo } from "./LicenseInfo"
|
||||||
import { ImageUploader } from "./ImageUploader"
|
|
||||||
|
|
||||||
export class Imgur extends ImageProvider implements ImageUploader {
|
export class Imgur extends ImageProvider {
|
||||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||||
public static readonly singleton = new Imgur()
|
public static readonly singleton = new Imgur()
|
||||||
public readonly name = "Imgur"
|
public readonly name = "Imgur"
|
||||||
public readonly defaultKeyPrefixes: string[] = ["image"]
|
public readonly defaultKeyPrefixes: string[] = ["image"]
|
||||||
public readonly maxFileSizeInMegabytes = 10
|
|
||||||
public static readonly apiUrl = "https://api.imgur.com/3/image"
|
public static readonly apiUrl = "https://api.imgur.com/3/image"
|
||||||
public static readonly supportingUrls = ["https://i.imgur.com"]
|
public static readonly supportingUrls = ["https://i.imgur.com"]
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
@ -21,57 +19,23 @@ export class Imgur extends ImageProvider implements ImageUploader {
|
||||||
return [Imgur.apiUrl]
|
return [Imgur.apiUrl]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads an image, returns the URL where to find the image
|
|
||||||
* @param title
|
|
||||||
* @param description
|
|
||||||
* @param blob
|
|
||||||
*/
|
|
||||||
public async uploadImage(
|
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
blob: File
|
|
||||||
): Promise<{ key: string; value: string }> {
|
|
||||||
const apiUrl = Imgur.apiUrl
|
|
||||||
const apiKey = Constants.ImgurApiKey
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append("image", blob)
|
|
||||||
formData.append("title", title)
|
|
||||||
formData.append("description", description)
|
|
||||||
|
|
||||||
const settings: RequestInit = {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
redirect: "follow",
|
|
||||||
headers: new Headers({
|
|
||||||
Authorization: `Client-ID ${apiKey}`,
|
|
||||||
Accept: "application/json",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response contains stringified JSON
|
|
||||||
const response = await fetch(apiUrl, settings)
|
|
||||||
const content = await response.json()
|
|
||||||
return { key: "image", value: content.data.link }
|
|
||||||
}
|
|
||||||
|
|
||||||
SourceIcon(): BaseUIElement {
|
SourceIcon(): BaseUIElement {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
public ExtractUrls(key: string, value: string): undefined | ProvidedImage[] {
|
||||||
if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) {
|
if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) {
|
||||||
return [
|
return [
|
||||||
Promise.resolve({
|
{
|
||||||
url: value,
|
url: value,
|
||||||
key: key,
|
key: key,
|
||||||
provider: this,
|
provider: this,
|
||||||
id: value,
|
id: value,
|
||||||
}),
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
export class LicenseInfo {
|
export class LicenseInfo {
|
||||||
title: string = ""
|
title?: string = ""
|
||||||
artist: string = ""
|
artist: string = ""
|
||||||
license: string = undefined
|
license?: string = undefined
|
||||||
licenseShortName: string = ""
|
licenseShortName?: string = ""
|
||||||
usageTerms: string = ""
|
usageTerms?: string = ""
|
||||||
attributionRequired: boolean = false
|
attributionRequired?: boolean = false
|
||||||
copyrighted: boolean = false
|
copyrighted?: boolean = false
|
||||||
credit: string = ""
|
credit?: string = ""
|
||||||
description: string = ""
|
description?: string = ""
|
||||||
informationLocation: URL = undefined
|
informationLocation?: URL = undefined
|
||||||
date?: Date
|
date?: Date
|
||||||
views?: number
|
views?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,8 +131,9 @@ export class Mapillary extends ImageProvider {
|
||||||
return new SvelteUIElement(MapillaryIcon, { url })
|
return new SvelteUIElement(MapillaryIcon, { url })
|
||||||
}
|
}
|
||||||
|
|
||||||
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
|
||||||
return [this.PrepareUrlAsync(key, value)]
|
const img = await this.PrepareUrlAsync(key, value)
|
||||||
|
return [img]
|
||||||
}
|
}
|
||||||
|
|
||||||
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {
|
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {
|
||||||
|
|
191
src/Logic/ImageProviders/Panoramax.ts
Normal file
191
src/Logic/ImageProviders/Panoramax.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import { ImageUploader } from "./ImageUploader"
|
||||||
|
import { AuthorizedPanoramax, PanoramaxXYZ, ImageData } from "panoramax-js/dist"
|
||||||
|
import ExifReader from "exifreader"
|
||||||
|
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||||
|
import BaseUIElement from "../../UI/BaseUIElement"
|
||||||
|
import { LicenseInfo } from "./LicenseInfo"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
import { GeoOperations } from "../GeoOperations"
|
||||||
|
import Constants from "../../Models/Constants"
|
||||||
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
|
|
||||||
|
|
||||||
|
export default class PanoramaxImageProvider extends ImageProvider {
|
||||||
|
|
||||||
|
public static readonly singleton = new PanoramaxImageProvider()
|
||||||
|
private static readonly xyz = new PanoramaxXYZ()
|
||||||
|
private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token)
|
||||||
|
public defaultKeyPrefixes: string[] = ["panoramax"]
|
||||||
|
public readonly name: string = "panoramax"
|
||||||
|
|
||||||
|
private static knownMeta: Record<string, { data: ImageData, time: Date }> = {}
|
||||||
|
|
||||||
|
public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
public addKnownMeta(meta: ImageData) {
|
||||||
|
PanoramaxImageProvider.knownMeta[meta.id] = { data: meta, time: new Date() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to get the entry from the mapcomplete-panoramax instance. Might return undefined
|
||||||
|
* @param id
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> {
|
||||||
|
const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
|
||||||
|
const url = `https://panoramax.mapcomplete.org/`
|
||||||
|
const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(sequence, id)
|
||||||
|
return { url, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> {
|
||||||
|
const data = await PanoramaxImageProvider.xyz.imageInfo(imageId)
|
||||||
|
return { data, url: "https://api.panoramax.xyz/" }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a geovisio-somewhat-looking-like-geojson object and converts it to a provided image
|
||||||
|
* @param meta
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private featureToImage(info: { data: ImageData, url: string }) {
|
||||||
|
const meta = info?.data
|
||||||
|
if (!meta) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = info.url
|
||||||
|
|
||||||
|
function makeAbsolute(s: string) {
|
||||||
|
if (!s.startsWith("https://") && !s.startsWith("http://")) {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
return parsed.protocol + "//" + parsed.host + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
const [lon, lat] = GeoOperations.centerpointCoordinates(meta)
|
||||||
|
return <ProvidedImage>{
|
||||||
|
id: meta.id,
|
||||||
|
url: makeAbsolute(meta.assets.sd.href),
|
||||||
|
url_hd: makeAbsolute(meta.assets.hd.href),
|
||||||
|
lon, lat,
|
||||||
|
key: "panoramax",
|
||||||
|
provider: this,
|
||||||
|
status: meta.properties["geovisio:status"],
|
||||||
|
rotation: Number(meta.properties["view:azimuth"]),
|
||||||
|
date: new Date(meta.properties.datetime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> {
|
||||||
|
if (!id.match(/^[a-zA-Z0-9-]+$/)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const cached = PanoramaxImageProvider.knownMeta[id]
|
||||||
|
if (cached) {
|
||||||
|
if(new Date().getTime() - cached.time.getTime() < 1000){
|
||||||
|
|
||||||
|
return { data: cached.data, url: undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.getInfoFromMapComplete(id)
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.getInfoFromXYZ(id)
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(e)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
|
||||||
|
return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||||
|
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
|
||||||
|
|
||||||
|
function hasLoading(data: ProvidedImage[]) {
|
||||||
|
if(data === undefined){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken")
|
||||||
|
}
|
||||||
|
|
||||||
|
Stores.Chronic(1500, () =>
|
||||||
|
hasLoading(source.data),
|
||||||
|
).addCallback(_ => {
|
||||||
|
console.log("UPdating... ")
|
||||||
|
super.getRelevantUrlsFor(tags, prefixes).then(data => {
|
||||||
|
console.log("New panoramax data is", data, hasLoading(data))
|
||||||
|
source.set(data)
|
||||||
|
return !hasLoading(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> {
|
||||||
|
const meta = await this.getInfoFor(providedImage.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader
|
||||||
|
date: new Date(meta.data.properties["datetime"]),
|
||||||
|
licenseShortName: meta.data.properties["geovisio:license"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public apiUrls(): string[] {
|
||||||
|
return ["https://panoramax.mapcomplete.org", "https://panoramax.xyz"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanoramaxUploader implements ImageUploader {
|
||||||
|
private readonly _panoramax: AuthorizedPanoramax
|
||||||
|
|
||||||
|
constructor(url: string, token: string) {
|
||||||
|
this._panoramax = new AuthorizedPanoramax(url, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImage(blob: File, currentGps: [number, number], author: string): Promise<{
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
absoluteUrl: string
|
||||||
|
}> {
|
||||||
|
|
||||||
|
const tags = await ExifReader.load(blob)
|
||||||
|
const hasDate = tags.DateTime !== undefined
|
||||||
|
const hasGPS = tags.GPSLatitude !== undefined && tags.GPSLongitude !== undefined
|
||||||
|
|
||||||
|
const [lon, lat] = currentGps
|
||||||
|
|
||||||
|
const p = this._panoramax
|
||||||
|
const defaultSequence = (await p.mySequences())[0]
|
||||||
|
const img = <ImageData>await p.addImage(blob, defaultSequence, {
|
||||||
|
lat: !hasGPS ? lat : undefined,
|
||||||
|
lon: !hasGPS ? lon : undefined,
|
||||||
|
datetime: !hasDate ? new Date().toISOString() : undefined,
|
||||||
|
exifOverride: {
|
||||||
|
Artist: author,
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
PanoramaxImageProvider.singleton.addKnownMeta(img)
|
||||||
|
return {
|
||||||
|
key: "panoramax",
|
||||||
|
value: img.id,
|
||||||
|
absoluteUrl: img.assets.hd.href,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import Wikidata from "../Web/Wikidata"
|
||||||
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
||||||
import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte"
|
import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
|
|
||||||
export class WikidataImageProvider extends ImageProvider {
|
export class WikidataImageProvider extends ImageProvider {
|
||||||
public static readonly singleton = new WikidataImageProvider()
|
public static readonly singleton = new WikidataImageProvider()
|
||||||
|
@ -25,28 +26,28 @@ export class WikidataImageProvider extends ImageProvider {
|
||||||
return new SvelteUIElement(Wikidata_icon)
|
return new SvelteUIElement(Wikidata_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
|
||||||
if (WikidataImageProvider.keyBlacklist.has(key)) {
|
if (WikidataImageProvider.keyBlacklist.has(key)) {
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
const entity = await Wikidata.LoadWikidataEntryAsync(value)
|
const entity = await Wikidata.LoadWikidataEntryAsync(value)
|
||||||
if (entity === undefined) {
|
if (entity === undefined) {
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const allImages: Promise<ProvidedImage>[] = []
|
const allImages: Promise<ProvidedImage[]>[] = []
|
||||||
// P18 is the claim 'depicted in this image'
|
// P18 is the claim 'depicted in this image'
|
||||||
for (const img of Array.from(entity.claims.get("P18") ?? [])) {
|
for (const img of Array.from(entity.claims.get("P18") ?? [])) {
|
||||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
|
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
|
||||||
allImages.push(...promises)
|
allImages.push(promises)
|
||||||
}
|
}
|
||||||
// P373 is 'commons category'
|
// P373 is 'commons category'
|
||||||
for (let cat of Array.from(entity.claims.get("P373") ?? [])) {
|
for (let cat of Array.from(entity.claims.get("P373") ?? [])) {
|
||||||
if (!cat.startsWith("Category:")) {
|
if (!cat.startsWith("Category:")) {
|
||||||
cat = "Category:" + cat
|
cat = "Category:" + cat
|
||||||
}
|
}
|
||||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
|
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
|
||||||
allImages.push(...promises)
|
allImages.push(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
const commons = entity.commons
|
const commons = entity.commons
|
||||||
|
@ -54,10 +55,11 @@ export class WikidataImageProvider extends ImageProvider {
|
||||||
commons !== undefined &&
|
commons !== undefined &&
|
||||||
(commons.startsWith("Category:") || commons.startsWith("File:"))
|
(commons.startsWith("Category:") || commons.startsWith("File:"))
|
||||||
) {
|
) {
|
||||||
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
|
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
|
||||||
allImages.push(...promises)
|
allImages.push(promises)
|
||||||
}
|
}
|
||||||
return allImages
|
const resolved = await Promise.all(Utils.NoNull(allImages))
|
||||||
|
return [].concat(...resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadAttribution(_): Promise<any> {
|
public DownloadAttribution(_): Promise<any> {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
||||||
value
|
value,
|
||||||
)}`
|
)}`
|
||||||
if (useHd) {
|
if (useHd) {
|
||||||
return baseUrl
|
return baseUrl
|
||||||
|
@ -97,28 +97,27 @@ export class WikimediaImageProvider extends ImageProvider {
|
||||||
return this.UrlForImage("File:" + value)
|
return this.UrlForImage("File:" + value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
public async ExtractUrls(key: string, value: string): undefined | Promise<ProvidedImage[]> {
|
||||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||||
if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) {
|
if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) {
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||||
if (value.startsWith("Category:")) {
|
if (value.startsWith("Category:")) {
|
||||||
const urls = await Wikimedia.GetCategoryContents(value)
|
const urls = await Wikimedia.GetCategoryContents(value)
|
||||||
return urls
|
return urls.filter((url) => url.startsWith("File:"))
|
||||||
.filter((url) => url.startsWith("File:"))
|
.map((image) => this.UrlForImage(image))
|
||||||
.map((image) => Promise.resolve(this.UrlForImage(image)))
|
|
||||||
}
|
}
|
||||||
if (value.startsWith("File:")) {
|
if (value.startsWith("File:")) {
|
||||||
return [Promise.resolve(this.UrlForImage(value))]
|
return [this.UrlForImage(value)]
|
||||||
}
|
}
|
||||||
if (value.startsWith("http")) {
|
if (value.startsWith("http")) {
|
||||||
// PRobably an error
|
// Probably an error
|
||||||
return []
|
return undefined
|
||||||
}
|
}
|
||||||
// We do a last effort and assume this is a file
|
// We do a last effort and assume this is a file
|
||||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
return [(this.UrlForImage("File:" + value))]
|
||||||
}
|
}
|
||||||
|
|
||||||
public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> {
|
public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> {
|
||||||
|
@ -148,7 +147,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
||||||
console.warn(
|
console.warn(
|
||||||
"The file",
|
"The file",
|
||||||
filename,
|
filename,
|
||||||
"has no usable metedata or license attached... Please fix the license info file yourself!"
|
"has no usable metedata or license attached... Please fix the license info file yourself!",
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,16 +69,14 @@ export default class DeleteAction extends OsmChangeAction {
|
||||||
* const obj : OsmNode= new OsmNode(1)
|
* const obj : OsmNode= new OsmNode(1)
|
||||||
* obj.tags = {id:"node/1",name:"Monte Piselli - San Giacomo"}
|
* obj.tags = {id:"node/1",name:"Monte Piselli - San Giacomo"}
|
||||||
* const da = new DeleteAction("node/1", new Tag("man_made",""), {theme: "test", specialMotivation: "Testcase"}, true)
|
* const da = new DeleteAction("node/1", new Tag("man_made",""), {theme: "test", specialMotivation: "Testcase"}, true)
|
||||||
* const state = { dryRun: new ImmutableStore(true), osmConnection: new OsmConnection() }
|
* const descr = await da.CreateChangeDescriptions(Changes.createTestObject(), obj)
|
||||||
* const descr = await da.CreateChangeDescriptions(new Changes(state), obj)
|
|
||||||
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase",changeType: "deletion"}, type: "node",id: 1 }
|
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase",changeType: "deletion"}, type: "node",id: 1 }
|
||||||
*
|
*
|
||||||
* // Must not crash if softDeletionTags are undefined
|
* // Must not crash if softDeletionTags are undefined
|
||||||
* const da = new DeleteAction("node/1", undefined, {theme: "test", specialMotivation: "Testcase"}, true)
|
* const da = new DeleteAction("node/1", undefined, {theme: "test", specialMotivation: "Testcase"}, true)
|
||||||
* const obj : OsmNode= new OsmNode(1)
|
* const obj : OsmNode= new OsmNode(1)
|
||||||
* obj.tags = {id:"node/1",name:"Monte Piselli - San Giacomo"}
|
* obj.tags = {id:"node/1",name:"Monte Piselli - San Giacomo"}
|
||||||
* const state = { dryRun: new ImmutableStore(true), osmConnection: new OsmConnection() }
|
* const descr = await da.CreateChangeDescriptions(Changes.createTestObject(), obj)
|
||||||
* const descr = await da.CreateChangeDescriptions(new Changes(state), obj)
|
|
||||||
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase", changeType: "deletion"}, type: "node",id: 1 }
|
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase", changeType: "deletion"}, type: "node",id: 1 }
|
||||||
*/
|
*/
|
||||||
public async CreateChangeDescriptions(
|
public async CreateChangeDescriptions(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
|
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import OsmChangeAction from "./Actions/OsmChangeAction"
|
import OsmChangeAction from "./Actions/OsmChangeAction"
|
||||||
import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
|
import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
|
||||||
|
@ -11,13 +11,12 @@ import { GeoLocationPointProperties } from "../State/GeoLocationState"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
||||||
import { OsmConnection } from "./OsmConnection"
|
import { OsmConnection } from "./OsmConnection"
|
||||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
|
||||||
import OsmObjectDownloader from "./OsmObjectDownloader"
|
import OsmObjectDownloader from "./OsmObjectDownloader"
|
||||||
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
||||||
import ChangeTagAction from "./Actions/ChangeTagAction"
|
import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||||
import FeatureSwitchState from "../State/FeatureSwitchState"
|
|
||||||
import DeleteAction from "./Actions/DeleteAction"
|
import DeleteAction from "./Actions/DeleteAction"
|
||||||
import MarkdownUtils from "../../Utils/MarkdownUtils"
|
import MarkdownUtils from "../../Utils/MarkdownUtils"
|
||||||
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all changes made to OSM.
|
* Handles all changes made to OSM.
|
||||||
|
@ -30,7 +29,9 @@ export class Changes {
|
||||||
public readonly state: {
|
public readonly state: {
|
||||||
allElements?: IndexedFeatureSource
|
allElements?: IndexedFeatureSource
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
featureSwitches?: FeatureSwitchState
|
featureSwitches?: {
|
||||||
|
featureSwitchMorePrivacy?: Store<boolean>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
|
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
|
||||||
public readonly backend: string
|
public readonly backend: string
|
||||||
|
@ -45,12 +46,15 @@ export class Changes {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: {
|
||||||
dryRun: Store<boolean>
|
featureSwitches: {
|
||||||
|
featureSwitchMorePrivacy?: Store<boolean>
|
||||||
|
featureSwitchIsTesting?: Store<boolean>
|
||||||
|
},
|
||||||
|
osmConnection: OsmConnection,
|
||||||
|
reportError?: (error: string) => void,
|
||||||
|
featureProperties?: FeaturePropertiesStore,
|
||||||
|
historicalUserLocations?: FeatureSource,
|
||||||
allElements?: IndexedFeatureSource
|
allElements?: IndexedFeatureSource
|
||||||
featurePropertiesStore?: FeaturePropertiesStore
|
|
||||||
osmConnection: OsmConnection
|
|
||||||
historicalUserLocations?: FeatureSource
|
|
||||||
featureSwitches?: FeatureSwitchState
|
|
||||||
},
|
},
|
||||||
leftRightSensitive: boolean = false,
|
leftRightSensitive: boolean = false,
|
||||||
reportError?: (string: string | Error, extramessage?: string) => void
|
reportError?: (string: string | Error, extramessage?: string) => void
|
||||||
|
@ -59,14 +63,18 @@ export class Changes {
|
||||||
// We keep track of all changes just as well
|
// We keep track of all changes just as well
|
||||||
this.allChanges.setData([...this.pendingChanges.data])
|
this.allChanges.setData([...this.pendingChanges.data])
|
||||||
// If a pending change contains a negative ID, we save that
|
// If a pending change contains a negative ID, we save that
|
||||||
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? []))
|
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id ?? 0) ?? []))
|
||||||
|
if(isNaN(this._nextId) && state.reportError !== undefined){
|
||||||
|
state.reportError("Got a NaN as nextID. Pending changes IDs are:" +this.pendingChanges.data?.map(pch => pch?.id).join("."))
|
||||||
|
this._nextId = -100
|
||||||
|
}
|
||||||
this.state = state
|
this.state = state
|
||||||
this.backend = state.osmConnection.Backend()
|
this.backend = state.osmConnection.Backend()
|
||||||
this._reportError = reportError
|
this._reportError = reportError
|
||||||
this._changesetHandler = new ChangesetHandler(
|
this._changesetHandler = new ChangesetHandler(
|
||||||
state.dryRun,
|
state.featureSwitches.featureSwitchIsTesting,
|
||||||
state.osmConnection,
|
state.osmConnection,
|
||||||
state.featurePropertiesStore,
|
state.featureProperties,
|
||||||
this,
|
this,
|
||||||
(e, extramessage: string) => this._reportError(e, extramessage)
|
(e, extramessage: string) => this._reportError(e, extramessage)
|
||||||
)
|
)
|
||||||
|
@ -76,6 +84,15 @@ export class Changes {
|
||||||
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static createTestObject(): Changes{
|
||||||
|
return new Changes({
|
||||||
|
osmConnection: new OsmConnection(),
|
||||||
|
featureSwitches:{
|
||||||
|
featureSwitchIsTesting: new ImmutableStore(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
static buildChangesetXML(
|
static buildChangesetXML(
|
||||||
csId: string,
|
csId: string,
|
||||||
allChanges: {
|
allChanges: {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { GeoOperations } from "./GeoOperations"
|
import { GeoOperations } from "./GeoOperations"
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
import opening_hours from "opening_hours"
|
import opening_hours from "opening_hours"
|
||||||
import Combine from "../UI/Base/Combine"
|
|
||||||
import BaseUIElement from "../UI/BaseUIElement"
|
|
||||||
import Title from "../UI/Base/Title"
|
|
||||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import { CountryCoder } from "latlon2country"
|
import { CountryCoder } from "latlon2country"
|
||||||
import Constants from "../Models/Constants"
|
import Constants from "../Models/Constants"
|
||||||
|
|
|
@ -49,6 +49,9 @@ export default class Constants {
|
||||||
...Constants.added_by_default,
|
...Constants.added_by_default,
|
||||||
...Constants.no_include,
|
...Constants.no_include,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
public static panoramax: { url: string, token: string } = packagefile.config.panoramax
|
||||||
|
|
||||||
// The user journey states thresholds when a new feature gets unlocked
|
// The user journey states thresholds when a new feature gets unlocked
|
||||||
public static userJourney = {
|
public static userJourney = {
|
||||||
moreScreenUnlock: 1,
|
moreScreenUnlock: 1,
|
||||||
|
|
|
@ -50,7 +50,6 @@ import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoEl
|
||||||
import FilteredLayer from "./FilteredLayer"
|
import FilteredLayer from "./FilteredLayer"
|
||||||
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
|
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
|
||||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||||
import { Imgur } from "../Logic/ImageProviders/Imgur"
|
|
||||||
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
||||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||||
|
@ -70,6 +69,7 @@ import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||||
import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider"
|
import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider"
|
||||||
import SearchState from "../Logic/State/SearchState"
|
import SearchState from "../Logic/State/SearchState"
|
||||||
import { ShowDataLayerOptions } from "../UI/Map/ShowDataLayerOptions"
|
import { ShowDataLayerOptions } from "../UI/Map/ShowDataLayerOptions"
|
||||||
|
import { PanoramaxUploader } from "../Logic/ImageProviders/Panoramax"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -270,14 +270,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.featureProperties = new FeaturePropertiesStore(layoutSource)
|
this.featureProperties = new FeaturePropertiesStore(layoutSource)
|
||||||
|
|
||||||
this.changes = new Changes(
|
this.changes = new Changes(
|
||||||
{
|
this,
|
||||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
|
||||||
allElements: layoutSource,
|
|
||||||
featurePropertiesStore: this.featureProperties,
|
|
||||||
osmConnection: this.osmConnection,
|
|
||||||
historicalUserLocations: this.geolocation.historicalUserLocations,
|
|
||||||
featureSwitches: this.featureSwitches,
|
|
||||||
},
|
|
||||||
layout?.isLeftRightSensitive() ?? false,
|
layout?.isLeftRightSensitive() ?? false,
|
||||||
(e, extraMsg) => this.reportError(e, extraMsg),
|
(e, extraMsg) => this.reportError(e, extraMsg),
|
||||||
)
|
)
|
||||||
|
@ -366,10 +359,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||||
this.imageUploadManager = new ImageUploadManager(
|
this.imageUploadManager = new ImageUploadManager(
|
||||||
layout,
|
layout,
|
||||||
Imgur.singleton,
|
new PanoramaxUploader(Constants.panoramax.url, Constants.panoramax.token),
|
||||||
this.featureProperties,
|
this.featureProperties,
|
||||||
this.osmConnection,
|
this.osmConnection,
|
||||||
this.changes,
|
this.changes,
|
||||||
|
this.geolocation.geolocationState.currentGPSLocation,
|
||||||
|
this.indexedFeatures
|
||||||
)
|
)
|
||||||
this.favourites = new FavouritesFeatureSource(this)
|
this.favourites = new FavouritesFeatureSource(this)
|
||||||
const longAgo = new Date()
|
const longAgo = new Date()
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import type { Feature, Point } from "geojson"
|
import type { Feature, Point } from "geojson"
|
||||||
|
import Loading from "../Base/Loading.svelte"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
|
||||||
export let image: Partial<ProvidedImage>
|
export let image: Partial<ProvidedImage>
|
||||||
let fallbackImage: string = undefined
|
let fallbackImage: string = undefined
|
||||||
|
@ -30,7 +33,7 @@
|
||||||
let showBigPreview = new UIEventSource(false)
|
let showBigPreview = new UIEventSource(false)
|
||||||
onDestroy(showBigPreview.addCallbackAndRun(shown => {
|
onDestroy(showBigPreview.addCallbackAndRun(shown => {
|
||||||
if (!shown) {
|
if (!shown) {
|
||||||
previewedImage.set(false)
|
previewedImage.set(undefined)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
|
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
|
||||||
|
@ -49,12 +52,12 @@
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
id: image.id,
|
id: image.id,
|
||||||
rotation: image.rotation
|
rotation: image.rotation,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: [image.lon, image.lat]
|
coordinates: [image.lon, image.lat],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
console.log(f)
|
console.log(f)
|
||||||
state?.geocodedImages.set([f])
|
state?.geocodedImages.set([f])
|
||||||
|
@ -73,36 +76,45 @@
|
||||||
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
|
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
<div class="relative shrink-0">
|
{#if image.status !== undefined && image.status !== "ready"}
|
||||||
<div class="relative w-fit"
|
<div class="h-full flex flex-col justify-center">
|
||||||
on:mouseenter={() => highlight()}
|
<Loading>
|
||||||
on:mouseleave={() => highlight(false)}
|
<Tr t={Translations.t.image.processing}/>
|
||||||
>
|
</Loading>
|
||||||
<img
|
</div>
|
||||||
bind:this={imgEl}
|
{:else}
|
||||||
on:load={() => (loaded = true)}
|
<div class="relative shrink-0">
|
||||||
class={imgClass ?? ""}
|
<div class="relative w-fit"
|
||||||
class:cursor-zoom-in={canZoom}
|
on:mouseenter={() => highlight()}
|
||||||
on:click={() => {
|
on:mouseleave={() => highlight(false)}
|
||||||
|
>
|
||||||
|
|
||||||
|
<img
|
||||||
|
bind:this={imgEl}
|
||||||
|
on:load={() => (loaded = true)}
|
||||||
|
class={imgClass ?? ""}
|
||||||
|
class:cursor-zoom-in={canZoom}
|
||||||
|
on:click={() => {
|
||||||
previewedImage?.set(image)
|
previewedImage?.set(image)
|
||||||
}}
|
}}
|
||||||
on:error={() => {
|
on:error={() => {
|
||||||
if (fallbackImage) {
|
if (fallbackImage) {
|
||||||
imgEl.src = fallbackImage
|
imgEl.src = fallbackImage
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
src={image.url}
|
src={image.url}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if canZoom && loaded}
|
{#if canZoom && loaded}
|
||||||
<div
|
<div
|
||||||
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
|
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
|
||||||
on:click={() => previewedImage.set(image)}>
|
on:click={() => previewedImage.set(image)}>
|
||||||
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
|
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0">
|
||||||
|
<ImageAttribution {image} {attributionFormat} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 left-0">
|
{/if}
|
||||||
<ImageAttribution {image} {attributionFormat} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class ImageCarousel extends Toggle {
|
||||||
image: url,
|
image: url,
|
||||||
state,
|
state,
|
||||||
previewedImage: state?.previewedImage,
|
previewedImage: state?.previewedImage,
|
||||||
})
|
}).SetClass("h-full")
|
||||||
|
|
||||||
if (url.key !== undefined) {
|
if (url.key !== undefined) {
|
||||||
image = new Combine([
|
image = new Combine([
|
||||||
|
@ -42,8 +42,8 @@ export class ImageCarousel extends Toggle {
|
||||||
]).SetClass("relative")
|
]).SetClass("relative")
|
||||||
}
|
}
|
||||||
image
|
image
|
||||||
.SetClass("w-full block cursor-zoom-in")
|
.SetClass("w-full h-full block cursor-zoom-in low-interaction")
|
||||||
.SetStyle("min-width: 50px; background: grey;")
|
.SetStyle("min-width: 50px;")
|
||||||
uiElements.push(image)
|
uiElements.push(image)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not generate image element for", url.url, "due to", e)
|
console.error("Could not generate image element for", url.url, "due to", e)
|
||||||
|
|
|
@ -83,16 +83,6 @@
|
||||||
</div>
|
</div>
|
||||||
</FileSelector>
|
</FileSelector>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<button
|
|
||||||
class="as-link"
|
|
||||||
style="margin: 0; padding: 0"
|
|
||||||
on:click={() => {
|
|
||||||
state.guistate.openUsersettings("picture-license")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tr t={t.currentLicense.Subs({ license: $licenseStore })} />
|
|
||||||
</button>
|
|
||||||
<br />
|
|
||||||
<Tr cls="subtle italic" t={t.respectPrivacy} />
|
<Tr cls="subtle italic" t={t.respectPrivacy} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,6 @@ import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||||
import { MenuState } from "../Models/MenuState"
|
import { MenuState } from "../Models/MenuState"
|
||||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||||
import { OsmTags } from "../Models/OsmFeature"
|
|
||||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||||
|
@ -27,6 +26,7 @@ import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||||
import SearchState from "../Logic/State/SearchState"
|
import SearchState from "../Logic/State/SearchState"
|
||||||
import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState"
|
import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState"
|
||||||
import GeocodeResult from "./Search/GeocodeResult.svelte"
|
import GeocodeResult from "./Search/GeocodeResult.svelte"
|
||||||
|
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state needed to render a special Visualisation.
|
* The state needed to render a special Visualisation.
|
||||||
|
@ -37,10 +37,8 @@ export interface SpecialVisualizationState {
|
||||||
readonly featureSwitches: FeatureSwitchState
|
readonly featureSwitches: FeatureSwitchState
|
||||||
|
|
||||||
readonly layerState: LayerState
|
readonly layerState: LayerState
|
||||||
readonly featureProperties: {
|
readonly featureSummary: SummaryTileSourceRewriter
|
||||||
getStore(id: string): UIEventSource<Record<string, string>>,
|
readonly featureProperties: FeaturePropertiesStore
|
||||||
trackFeature?(feature: { properties: OsmTags })
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -714,7 +714,7 @@ export default class SpecialVisualizations {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
constr: (state, tags, args) => {
|
constr: (state, tags, args) => {
|
||||||
const targetKey = args[0] === "" ? undefined : args[0]
|
const targetKey = args[0] === "" ? undefined : args[0]
|
||||||
return new SvelteUIElement(UploadImage, {
|
return new SvelteUIElement(UploadImage, {
|
||||||
state,
|
state,
|
||||||
tags,
|
tags,
|
||||||
|
|
23
src/Utils.ts
23
src/Utils.ts
|
@ -414,6 +414,29 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicates the given array based on some ID-properties.
|
||||||
|
* Removes all falsey values
|
||||||
|
* @param arr
|
||||||
|
* @param toKey
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public static DedupOnId<T>(arr: T[], toKey: ((t:T) => string) ): T[]{
|
||||||
|
const uniq: T[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const img of arr) {
|
||||||
|
if(!img){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const k = toKey(img)
|
||||||
|
if (!seen.has(k)) {
|
||||||
|
seen.add(k)
|
||||||
|
uniq.push(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniq
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all duplicates in a list of strings
|
* Finds all duplicates in a list of strings
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import { ExtraFuncParams, ExtraFunctions } from "../../src/Logic/ExtraFunctions"
|
|
||||||
import { OsmFeature } from "../../src/Models/OsmFeature"
|
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import { Feature } from "geojson"
|
import { UIEventSource } from "../../src/Logic/UIEventSource"
|
||||||
import { OsmConnection } from "../../src/Logic/Osm/OsmConnection"
|
|
||||||
import { ImmutableStore, UIEventSource } from "../../src/Logic/UIEventSource"
|
|
||||||
import { Changes } from "../../src/Logic/Osm/Changes"
|
import { Changes } from "../../src/Logic/Osm/Changes"
|
||||||
import LinkImageAction from "../../src/Logic/Osm/Actions/LinkImageAction"
|
import LinkImageAction from "../../src/Logic/Osm/Actions/LinkImageAction"
|
||||||
import FeaturePropertiesStore from "../../src/Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
|
||||||
|
|
||||||
describe("Changes", () => {
|
describe("Changes", () => {
|
||||||
it("should correctly apply the image tag if an image gets linked in between", async () => {
|
it("should correctly apply the image tag if an image gets linked in between", async () => {
|
||||||
const dryRun = new ImmutableStore(true)
|
const changes = Changes.createTestObject()
|
||||||
const osmConnection = new OsmConnection({ dryRun })
|
|
||||||
const changes = new Changes({ osmConnection, dryRun })
|
|
||||||
const id = "node/42"
|
const id = "node/42"
|
||||||
const tags = new UIEventSource({ id, amenity: "shop" })
|
const tags = new UIEventSource({ id, amenity: "shop" })
|
||||||
const addImage = new LinkImageAction(
|
const addImage = new LinkImageAction(
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { Utils } from "../../../../src/Utils"
|
import { Utils } from "../../../../src/Utils"
|
||||||
import { OsmRelation } from "../../../../src/Logic/Osm/OsmObject"
|
import { OsmRelation } from "../../../../src/Logic/Osm/OsmObject"
|
||||||
import {
|
import { InPlaceReplacedmentRTSH, TurnRestrictionRSH } from "../../../../src/Logic/Osm/Actions/RelationSplitHandler"
|
||||||
InPlaceReplacedmentRTSH,
|
|
||||||
TurnRestrictionRSH,
|
|
||||||
} from "../../../../src/Logic/Osm/Actions/RelationSplitHandler"
|
|
||||||
import { Changes } from "../../../../src/Logic/Osm/Changes"
|
import { Changes } from "../../../../src/Logic/Osm/Changes"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import OsmObjectDownloader from "../../../../src/Logic/Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../../../../src/Logic/Osm/OsmObjectDownloader"
|
||||||
import { ImmutableStore } from "../../../../src/Logic/UIEventSource"
|
|
||||||
import { OsmConnection } from "../../../../src/Logic/Osm/OsmConnection"
|
|
||||||
|
|
||||||
describe("RelationSplitHandler", () => {
|
describe("RelationSplitHandler", () => {
|
||||||
Utils.injectJsonDownloadForTests("https://api.openstreetmap.org/api/0.6/node/1124134958/ways", {
|
Utils.injectJsonDownloadForTests("https://api.openstreetmap.org/api/0.6/node/1124134958/ways", {
|
||||||
|
@ -653,10 +648,7 @@ describe("RelationSplitHandler", () => {
|
||||||
downloader
|
downloader
|
||||||
)
|
)
|
||||||
const changeDescription = await splitter.CreateChangeDescriptions(
|
const changeDescription = await splitter.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(false),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",")
|
const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",")
|
||||||
const expected = "687866206,295132739,-1,690497698"
|
const expected = "687866206,295132739,-1,690497698"
|
||||||
|
@ -710,10 +702,7 @@ describe("RelationSplitHandler", () => {
|
||||||
downloader
|
downloader
|
||||||
)
|
)
|
||||||
const changeDescription = await splitter.CreateChangeDescriptions(
|
const changeDescription = await splitter.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(false),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
const allIds = changeDescription[0].changes["members"]
|
const allIds = changeDescription[0].changes["members"]
|
||||||
.map((m) => m.type + "/" + m.ref + "-->" + m.role)
|
.map((m) => m.type + "/" + m.ref + "-->" + m.role)
|
||||||
|
@ -734,10 +723,7 @@ describe("RelationSplitHandler", () => {
|
||||||
downloader
|
downloader
|
||||||
)
|
)
|
||||||
const changesReverse = await splitterReverse.CreateChangeDescriptions(
|
const changesReverse = await splitterReverse.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(false),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
expect(changesReverse.length).toEqual(0)
|
expect(changesReverse.length).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Utils } from "../../../../src/Utils"
|
import { Utils } from "../../../../src/Utils"
|
||||||
import LayoutConfig from "../../../../src/Models/ThemeConfig/LayoutConfig"
|
|
||||||
import { BBox } from "../../../../src/Logic/BBox"
|
import { BBox } from "../../../../src/Logic/BBox"
|
||||||
import ReplaceGeometryAction from "../../../../src/Logic/Osm/Actions/ReplaceGeometryAction"
|
import ReplaceGeometryAction from "../../../../src/Logic/Osm/Actions/ReplaceGeometryAction"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
@ -9,305 +8,6 @@ import { Changes } from "../../../../src/Logic/Osm/Changes"
|
||||||
import FullNodeDatabaseSource from "../../../../src/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../../../../src/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
|
|
||||||
describe("ReplaceGeometryAction", () => {
|
describe("ReplaceGeometryAction", () => {
|
||||||
const grbStripped = {
|
|
||||||
id: "grb",
|
|
||||||
title: {
|
|
||||||
nl: "GRB import helper",
|
|
||||||
},
|
|
||||||
description: "Smaller version of the GRB theme",
|
|
||||||
language: ["nl", "en"],
|
|
||||||
socialImage: "img.jpg",
|
|
||||||
version: "0",
|
|
||||||
startLat: 51.0249,
|
|
||||||
startLon: 4.026489,
|
|
||||||
startZoom: 9,
|
|
||||||
clustering: false,
|
|
||||||
overrideAll: {
|
|
||||||
minzoom: 19,
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "type_node",
|
|
||||||
source: {
|
|
||||||
osmTags: "type=node",
|
|
||||||
},
|
|
||||||
pointRendering: null,
|
|
||||||
lineRendering: [{}],
|
|
||||||
override: {
|
|
||||||
calculatedTags: [
|
|
||||||
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
|
|
||||||
"_is_part_of_grb_building=feat.get('parent_ways')?.some(p => p['source:geometry:ref'] !== undefined) ?? false",
|
|
||||||
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
|
|
||||||
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
|
|
||||||
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false",
|
|
||||||
"_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')",
|
|
||||||
],
|
|
||||||
pointRendering: [
|
|
||||||
{
|
|
||||||
marker: [
|
|
||||||
{
|
|
||||||
icon: "square",
|
|
||||||
color: "#cc0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
iconSize: "5,5",
|
|
||||||
location: ["point"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
passAllFeatures: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "osm-buildings",
|
|
||||||
name: "All OSM-buildings",
|
|
||||||
source: {
|
|
||||||
osmTags: "building~*",
|
|
||||||
},
|
|
||||||
calculatedTags: ["_surface:strict:=feat.get('_surface')"],
|
|
||||||
lineRendering: [
|
|
||||||
{
|
|
||||||
width: {
|
|
||||||
render: "2",
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: "fixme~*",
|
|
||||||
then: "5",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
render: "#00c",
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: "fixme~*",
|
|
||||||
then: "#ff00ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=house",
|
|
||||||
then: "#a00",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=shed",
|
|
||||||
then: "#563e02",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: {
|
|
||||||
or: ["building=garage", "building=garages"],
|
|
||||||
},
|
|
||||||
then: "#f9bfbb",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=yes",
|
|
||||||
then: "#0774f2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: "OSM-gebouw",
|
|
||||||
tagRenderings: [
|
|
||||||
{
|
|
||||||
id: "building type",
|
|
||||||
freeform: {
|
|
||||||
key: "building",
|
|
||||||
},
|
|
||||||
render: "The building type is <b>{building}</b>",
|
|
||||||
question: {
|
|
||||||
en: "What kind of building is this?",
|
|
||||||
},
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: "building=house",
|
|
||||||
then: "A normal house",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=detached",
|
|
||||||
then: "A house detached from other building",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=semidetached_house",
|
|
||||||
then: "A house sharing only one wall with another house",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=apartments",
|
|
||||||
then: "An apartment building - highrise for living",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=office",
|
|
||||||
then: "An office building - highrise for work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=apartments",
|
|
||||||
then: "An apartment building",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=shed",
|
|
||||||
then: "A small shed, e.g. in a garden",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=garage",
|
|
||||||
then: "A single garage to park a car",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=garages",
|
|
||||||
then: "A building containing only garages; typically they are all identical",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if: "building=yes",
|
|
||||||
then: "A building - no specification",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb-housenumber",
|
|
||||||
render: {
|
|
||||||
nl: "Het huisnummer is <b>{addr:housenumber}</b>",
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
nl: "Wat is het huisnummer?",
|
|
||||||
},
|
|
||||||
freeform: {
|
|
||||||
key: "addr:housenumber",
|
|
||||||
},
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: {
|
|
||||||
and: ["not:addr:housenumber=yes", "addr:housenumber="],
|
|
||||||
},
|
|
||||||
then: {
|
|
||||||
nl: "Geen huisnummer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb-unit",
|
|
||||||
question: "Wat is de wooneenheid-aanduiding?",
|
|
||||||
render: {
|
|
||||||
nl: "De wooneenheid-aanduiding is <b>{addr:unit}</b> ",
|
|
||||||
},
|
|
||||||
freeform: {
|
|
||||||
key: "addr:unit",
|
|
||||||
},
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: "addr:unit=",
|
|
||||||
then: "Geen wooneenheid-nummer",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb-street",
|
|
||||||
render: {
|
|
||||||
nl: "De straat is <b>{addr:street}</b>",
|
|
||||||
},
|
|
||||||
freeform: {
|
|
||||||
key: "addr:street",
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
nl: "Wat is de straat?",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb-fixme",
|
|
||||||
render: {
|
|
||||||
nl: "De fixme is <b>{fixme}</b>",
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
nl: "Wat zegt de fixme?",
|
|
||||||
},
|
|
||||||
freeform: {
|
|
||||||
key: "fixme",
|
|
||||||
},
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
if: {
|
|
||||||
and: ["fixme="],
|
|
||||||
},
|
|
||||||
then: {
|
|
||||||
nl: "Geen fixme",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb-min-level",
|
|
||||||
render: {
|
|
||||||
nl: "Dit gebouw begint maar op de {building:min_level} verdieping",
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
nl: "Hoeveel verdiepingen ontbreken?",
|
|
||||||
},
|
|
||||||
freeform: {
|
|
||||||
key: "building:min_level",
|
|
||||||
type: "pnat",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"all_tags",
|
|
||||||
],
|
|
||||||
filter: [
|
|
||||||
{
|
|
||||||
id: "has-fixme",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
osmTags: "fixme~*",
|
|
||||||
question: "Heeft een FIXME",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "grb",
|
|
||||||
description: "Geometry which comes from GRB with tools to import them",
|
|
||||||
source: {
|
|
||||||
osmTags: {
|
|
||||||
and: ["HUISNR~*", "man_made!=mast"],
|
|
||||||
},
|
|
||||||
geoJson:
|
|
||||||
"https://betadata.grbosm.site/grb?bbox={x_min},{y_min},{x_max},{y_max}",
|
|
||||||
geoJsonZoomLevel: 18,
|
|
||||||
mercatorCrs: true,
|
|
||||||
},
|
|
||||||
name: "GRB geometries",
|
|
||||||
title: "GRB outline",
|
|
||||||
calculatedTags: [
|
|
||||||
"_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
|
|
||||||
"_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''",
|
|
||||||
"_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']",
|
|
||||||
"_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties?.id",
|
|
||||||
"_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')",
|
|
||||||
"_osm_obj:building=feat.get('_overlaps_with')?.feat?.properties?.building",
|
|
||||||
"_osm_obj:addr:street=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:street']",
|
|
||||||
"_osm_obj:addr:housenumber=(feat.get('_overlaps_with')?.feat?.properties ?? {})['addr:housenumber']",
|
|
||||||
"_osm_obj:surface=(feat.get('_overlaps_with')?.feat?.properties ?? {})['_surface:strict']",
|
|
||||||
|
|
||||||
"_overlap_absolute=feat.get('_overlaps_with')?.overlap",
|
|
||||||
"_reverse_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface'))",
|
|
||||||
"_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_osm_obj:surface'))",
|
|
||||||
"_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']",
|
|
||||||
"_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref",
|
|
||||||
"_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')",
|
|
||||||
"_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date",
|
|
||||||
"_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)",
|
|
||||||
"_building:min_level= feat.properties['fixme']?.startsWith('verdieping, correct the building tag, add building:level and building:min_level before upload in JOSM!') ? '1' : ''",
|
|
||||||
"_intersects_with_other_features=feat.intersectionsWith('generic_osm_object').map(f => \"<a href='https://osm.org/\"+f.feat.properties.id+\"' target='_blank'>\" + f.feat.properties.id + \"</a>\").join(', ')",
|
|
||||||
],
|
|
||||||
tagRenderings: [],
|
|
||||||
pointRendering: [
|
|
||||||
{
|
|
||||||
marker: [
|
|
||||||
{
|
|
||||||
icon: "./assets/themes/grb/housenumber_blank.svg",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
iconSize: "50,50",
|
|
||||||
location: ["point", "centroid"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const coordinates = <[number, number][]>[
|
const coordinates = <[number, number][]>[
|
||||||
[3.216690793633461, 51.21474084112525],
|
[3.216690793633461, 51.21474084112525],
|
||||||
|
@ -890,10 +590,7 @@ describe("ReplaceGeometryAction", () => {
|
||||||
const data = await Utils.downloadJson(url)
|
const data = await Utils.downloadJson(url)
|
||||||
const fullNodeDatabase = new FullNodeDatabaseSource()
|
const fullNodeDatabase = new FullNodeDatabaseSource()
|
||||||
fullNodeDatabase.handleOsmJson(data, 0, 0, 0)
|
fullNodeDatabase.handleOsmJson(data, 0, 0, 0)
|
||||||
const changes = new Changes({
|
const changes = Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
const osmConnection = new OsmConnection({
|
const osmConnection = new OsmConnection({
|
||||||
dryRun: new ImmutableStore(true),
|
dryRun: new ImmutableStore(true),
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { Utils } from "../../../../src/Utils"
|
||||||
import SplitAction from "../../../../src/Logic/Osm/Actions/SplitAction"
|
import SplitAction from "../../../../src/Logic/Osm/Actions/SplitAction"
|
||||||
import { Changes } from "../../../../src/Logic/Osm/Changes"
|
import { Changes } from "../../../../src/Logic/Osm/Changes"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import { OsmConnection } from "../../../../src/Logic/Osm/OsmConnection"
|
|
||||||
import { ImmutableStore } from "../../../../src/Logic/UIEventSource"
|
|
||||||
|
|
||||||
describe("SplitAction", () => {
|
describe("SplitAction", () => {
|
||||||
{
|
{
|
||||||
|
@ -2690,10 +2688,7 @@ describe("SplitAction", () => {
|
||||||
theme: "test",
|
theme: "test",
|
||||||
})
|
})
|
||||||
const changeDescription = await splitter.CreateChangeDescriptions(
|
const changeDescription = await splitter.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(changeDescription[0].type).toBe("node")
|
expect(changeDescription[0].type).toBe("node")
|
||||||
|
@ -2720,10 +2715,7 @@ describe("SplitAction", () => {
|
||||||
theme: "test",
|
theme: "test",
|
||||||
})
|
})
|
||||||
const changeDescription = await splitter.CreateChangeDescriptions(
|
const changeDescription = await splitter.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(changeDescription.length).toBe(2)
|
expect(changeDescription.length).toBe(2)
|
||||||
|
@ -2742,10 +2734,7 @@ describe("SplitAction", () => {
|
||||||
theme: "test",
|
theme: "test",
|
||||||
})
|
})
|
||||||
const changeDescription = await splitter.CreateChangeDescriptions(
|
const changeDescription = await splitter.CreateChangeDescriptions(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Should be a new node
|
// Should be a new node
|
||||||
|
@ -2760,10 +2749,7 @@ describe("SplitAction", () => {
|
||||||
theme: "test",
|
theme: "test",
|
||||||
})
|
})
|
||||||
const changes = await splitAction.Perform(
|
const changes = await splitAction.Perform(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
console.log(changes)
|
console.log(changes)
|
||||||
// 8715440368 is the expected point of the split
|
// 8715440368 is the expected point of the split
|
||||||
|
@ -2803,10 +2789,7 @@ describe("SplitAction", () => {
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
const changes = await splitAction.Perform(
|
const changes = await splitAction.Perform(
|
||||||
new Changes({
|
Changes.createTestObject()
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// THe first change is the creation of the new node
|
// THe first change is the creation of the new node
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { ChangeDescription } from "../../../src/Logic/Osm/Actions/ChangeDescription"
|
import { ChangeDescription } from "../../../src/Logic/Osm/Actions/ChangeDescription"
|
||||||
import { Changes } from "../../../src/Logic/Osm/Changes"
|
import { Changes } from "../../../src/Logic/Osm/Changes"
|
||||||
import { expect, it } from "vitest"
|
import { expect, it } from "vitest"
|
||||||
import { ImmutableStore } from "../../../src/Logic/UIEventSource"
|
|
||||||
import { OsmConnection } from "../../../src/Logic/Osm/OsmConnection"
|
|
||||||
|
|
||||||
it("Generate preXML from changeDescriptions", () => {
|
it("Generate preXML from changeDescriptions", () => {
|
||||||
const changeDescrs: ChangeDescription[] = [
|
const changeDescrs: ChangeDescription[] = [
|
||||||
|
@ -29,11 +27,7 @@ it("Generate preXML from changeDescriptions", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const c = new Changes({
|
const descr = Changes.createTestObject().CreateChangesetObjects(changeDescrs, [])
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
const descr = c.CreateChangesetObjects(changeDescrs, [])
|
|
||||||
expect(descr.modifiedObjects).toHaveLength(0)
|
expect(descr.modifiedObjects).toHaveLength(0)
|
||||||
expect(descr.deletedObjects).toHaveLength(0)
|
expect(descr.deletedObjects).toHaveLength(0)
|
||||||
expect(descr.newObjects).toHaveLength(1)
|
expect(descr.newObjects).toHaveLength(1)
|
||||||
|
|
|
@ -6,21 +6,27 @@ import { Changes } from "../../../src/Logic/Osm/Changes"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
function elstorage() {
|
function elstorage() {
|
||||||
return { addAlias: (_, __) => {} }
|
return {
|
||||||
|
addAlias: (_, __) => {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChangesetHandler(): ChangesetHandler {
|
||||||
|
const changes = Changes.createTestObject()
|
||||||
|
return new ChangesetHandler(
|
||||||
|
new UIEventSource<boolean>(true),
|
||||||
|
new OsmConnection({}),
|
||||||
|
elstorage(),
|
||||||
|
changes,
|
||||||
|
e => console.error(e),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("ChangesetHanlder", () => {
|
describe("ChangesetHanlder", () => {
|
||||||
describe("RewriteTagsOf", () => {
|
describe("RewriteTagsOf", () => {
|
||||||
it("should insert new tags", () => {
|
it("should insert new tags", () => {
|
||||||
const changesetHandler = new ChangesetHandler(
|
const changesetHandler = createChangesetHandler()
|
||||||
new UIEventSource<boolean>(true),
|
|
||||||
new OsmConnection({}),
|
|
||||||
elstorage(),
|
|
||||||
new Changes({
|
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const oldChangesetMeta = {
|
const oldChangesetMeta = {
|
||||||
type: "changeset",
|
type: "changeset",
|
||||||
|
@ -57,13 +63,13 @@ describe("ChangesetHanlder", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
new Map<string, string>(),
|
new Map<string, string>(),
|
||||||
oldChangesetMeta
|
oldChangesetMeta,
|
||||||
)
|
)
|
||||||
const d = Utils.asDict(rewritten)
|
const d = Utils.asDict(rewritten)
|
||||||
expect(d.size).toEqual(10)
|
expect(d.size).toEqual(10)
|
||||||
expect(d.get("answer")).toEqual("5")
|
expect(d.get("answer")).toEqual("5")
|
||||||
expect(d.get("comment")).toEqual(
|
expect(d.get("comment")).toEqual(
|
||||||
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
|
"Adding data with #MapComplete for theme #toerisme_vlaanderen",
|
||||||
)
|
)
|
||||||
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
||||||
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
||||||
|
@ -74,15 +80,7 @@ describe("ChangesetHanlder", () => {
|
||||||
expect(d.get("newTag")).toEqual("newValue")
|
expect(d.get("newTag")).toEqual("newValue")
|
||||||
})
|
})
|
||||||
it("should aggregate numeric tags", () => {
|
it("should aggregate numeric tags", () => {
|
||||||
const changesetHandler = new ChangesetHandler(
|
const changesetHandler = createChangesetHandler()
|
||||||
new UIEventSource<boolean>(true),
|
|
||||||
new OsmConnection({}),
|
|
||||||
elstorage(),
|
|
||||||
new Changes({
|
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const oldChangesetMeta = {
|
const oldChangesetMeta = {
|
||||||
type: "changeset",
|
type: "changeset",
|
||||||
id: 118443748,
|
id: 118443748,
|
||||||
|
@ -118,14 +116,14 @@ describe("ChangesetHanlder", () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
new Map<string, string>(),
|
new Map<string, string>(),
|
||||||
oldChangesetMeta
|
oldChangesetMeta,
|
||||||
)
|
)
|
||||||
const d = Utils.asDict(rewritten)
|
const d = Utils.asDict(rewritten)
|
||||||
|
|
||||||
expect(d.size).toEqual(9)
|
expect(d.size).toEqual(9)
|
||||||
expect(d.get("answer")).toEqual("42")
|
expect(d.get("answer")).toEqual("42")
|
||||||
expect(d.get("comment")).toEqual(
|
expect(d.get("comment")).toEqual(
|
||||||
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
|
"Adding data with #MapComplete for theme #toerisme_vlaanderen",
|
||||||
)
|
)
|
||||||
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
||||||
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
||||||
|
@ -135,15 +133,7 @@ describe("ChangesetHanlder", () => {
|
||||||
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
|
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
|
||||||
})
|
})
|
||||||
it("should rewrite special reasons with the correct ID", () => {
|
it("should rewrite special reasons with the correct ID", () => {
|
||||||
const changesetHandler = new ChangesetHandler(
|
const changesetHandler = createChangesetHandler()
|
||||||
new UIEventSource<boolean>(true),
|
|
||||||
new OsmConnection({}),
|
|
||||||
elstorage(),
|
|
||||||
new Changes({
|
|
||||||
dryRun: new ImmutableStore(true),
|
|
||||||
osmConnection: new OsmConnection(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const oldChangesetMeta = {
|
const oldChangesetMeta = {
|
||||||
type: "changeset",
|
type: "changeset",
|
||||||
id: 118443748,
|
id: 118443748,
|
||||||
|
@ -173,14 +163,14 @@ describe("ChangesetHanlder", () => {
|
||||||
const rewritten = changesetHandler.RewriteTagsOf(
|
const rewritten = changesetHandler.RewriteTagsOf(
|
||||||
[],
|
[],
|
||||||
new Map<string, string>([["node/-1", "node/42"]]),
|
new Map<string, string>([["node/-1", "node/42"]]),
|
||||||
oldChangesetMeta
|
oldChangesetMeta,
|
||||||
)
|
)
|
||||||
const d = Utils.asDict(rewritten)
|
const d = Utils.asDict(rewritten)
|
||||||
|
|
||||||
expect(d.size).toEqual(9)
|
expect(d.size).toEqual(9)
|
||||||
expect(d.get("answer")).toEqual("5")
|
expect(d.get("answer")).toEqual("5")
|
||||||
expect(d.get("comment")).toEqual(
|
expect(d.get("comment")).toEqual(
|
||||||
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
|
"Adding data with #MapComplete for theme #toerisme_vlaanderen",
|
||||||
)
|
)
|
||||||
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
|
||||||
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
expect(d.get("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
|
||||||
|
@ -206,7 +196,7 @@ describe("ChangesetHanlder", () => {
|
||||||
const changes = new Map<string, string>([["node/-1", "node/42"]])
|
const changes = new Map<string, string>([["node/-1", "node/42"]])
|
||||||
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
|
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
|
||||||
extraMetaTags,
|
extraMetaTags,
|
||||||
changes
|
changes,
|
||||||
)
|
)
|
||||||
// "Special rewrite did not trigger"
|
// "Special rewrite did not trigger"
|
||||||
expect(hasSpecialMotivationChanges).toBe(true)
|
expect(hasSpecialMotivationChanges).toBe(true)
|
||||||
|
|
Loading…
Reference in a new issue