Merge master

This commit is contained in:
Pieter Vander Vennet 2024-09-28 02:23:19 +02:00
commit cc4db080aa
45 changed files with 619 additions and 812 deletions

View file

@ -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))
* **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)

View file

@ -1117,13 +1117,15 @@
{
"if": "advertising=tilework",
"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",
"then": {
"en": "This is a relief"
"en": "This is a relief",
"de": "Dies ist ein Relief"
}
}
]
@ -1652,13 +1654,15 @@
"if": "historic=advertising",
"alsoShowIf": "historic=yes",
"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=",
"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)"
}
}
]

View file

@ -577,7 +577,7 @@
"title": "Straßenbilder in der Nähe"
},
"pleaseLogin": "Bitte anmelden, um ein Bild hinzuzufügen",
"respectPrivacy": "Bitte respektieren Sie die Privatsphäre. Fotografieren Sie weder Personen noch Nummernschilder. Benutzen Sie keine urheberrechtlich geschützten Quellen wie z.B. Google Maps oder Google Streetview.",
"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}",
"upload": {
"failReasons": "Keine Internetverbindung",
@ -709,7 +709,7 @@
"preset_type": {
"question": "Von welcher Art ist dieses Objekt?",
"typeDescription": "Dies ist <b>{title}</b>. <div class='subtle'>{description}</div>",
"typeTitle": "Dies ist {title}"
"typeTitle": "Dies ist <b>{title}</b>"
},
"privacy": {
"editingIntro": "Ihre Änderungen werden auf OpenStreetMap gespeichert und sind öffentlich zugänglich. Ein mit MapComplete erstellter Änderungssatz enthält folgende Daten:",

View file

@ -593,7 +593,8 @@
"title": "Nearby streetview imagery"
},
"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}",
"upload": {
"failReasons": "You might have lost connection to the internet",

View file

@ -397,7 +397,7 @@
"seeNearby": "Buscar y enlazar fotos cercanas"
},
"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}",
"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.",

View file

@ -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": {
"mappings": {
"0": {

View file

@ -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": {
"mappings": {
"0": {

View file

@ -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": {
"render": {
"special": {

View file

@ -116,6 +116,14 @@
"question": "Werden mehrere Werbungen abwechselnd angezeigt?"
},
"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?"
},
"luminous_or_lit_advertising": {
@ -181,6 +189,12 @@
"10": {
"then": "Dies ist eine Wandmalerei"
},
"11": {
"then": "Dies ist eine Kachelarbeit - die Werbung ist auf Fliesen gemalt"
},
"12": {
"then": "Dies ist ein Relief"
},
"2": {
"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": {
"mappings": {
"0": {

View file

@ -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": {
"mappings": {
"0": {

View file

@ -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": {
"render": "Las traducciones para {_theme} en {_language} están al {_translation_percentage}%: {_translation_translated_count} cadenas de {_translation_total} están traducidas"
},

View file

@ -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": {
"mappings": {
"0": {

View file

@ -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": {
"mappings": {
"0": {

View file

@ -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": {
"mappings": {
"0": {

View file

@ -2063,17 +2063,6 @@
}
}
},
"usersettings": {
"tagRenderings": {
"picture-license": {
"mappings": {
"1": {
"then": "Изображения будут опубликованы под лицензией <b>CC0</b> и перейдут в общественное достояние. Это значит, что кто угодно имеет право использовать их без ограничений."
}
}
}
}
},
"vending_machine": {
"tagRenderings": {
"operational_status": {

88
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mapcomplete",
"version": "0.47.2",
"version": "0.47.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mapcomplete",
"version": "0.47.2",
"version": "0.47.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@comunica/core": "^3.0.1",
@ -39,6 +39,7 @@
"dompurify": "^3.0.5",
"email-validator": "^2.0.4",
"escape-html": "^1.0.3",
"exifreader": "^4.23.5",
"fake-dom": "^1.0.4",
"flowbite-svelte": "^0.46.2",
"follow-redirects": "^1.15.6",
@ -62,6 +63,7 @@
"opening_hours": "^3.6.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
"panoramax-js": "^0.1.4",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
"pbf": "^3.2.1",
@ -4908,6 +4910,19 @@
"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": {
"version": "2.8.2",
"dev": true,
@ -10048,6 +10063,24 @@
"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": {
"version": "2.0.3",
"dev": true,
@ -15960,6 +15993,17 @@
"version": "1.0.0",
"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": {
"version": "9.4.3",
"license": "MIT",
@ -24680,6 +24724,19 @@
"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": {
"version": "2.8.2",
"dev": true
@ -28121,6 +28178,22 @@
"events": {
"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": {
"version": "2.0.3",
"dev": true
@ -31983,6 +32056,17 @@
"packet-reader": {
"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": {
"version": "9.4.3",
"requires": {

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.47.2",
"version": "0.47.3",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
@ -41,6 +41,10 @@
"imgur": "7070e7167f0a25a",
"mapillary_v4": "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
},
"panoramax": {
"url": "https://panoramax.mapcomplete.org",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnZW92aXNpbyIsInN1YiI6IjU5ZjgzOGI0LTM4ZjAtNDdjYi04OWYyLTM3NDQ3MWMxNTUxOCJ9.0rBioZS_48NTjnkIyN9497c3fQdTqtGgH1HDqlz1bWs"
},
"default_overpass_urls": [
"https://overpass-api.de/api/interpreter",
"https://overpass.private.coffee/api/interpreter",
@ -180,6 +184,7 @@
"dompurify": "^3.0.5",
"email-validator": "^2.0.4",
"escape-html": "^1.0.3",
"exifreader": "^4.23.5",
"fake-dom": "^1.0.4",
"flowbite-svelte": "^0.46.2",
"follow-redirects": "^1.15.6",
@ -203,6 +208,7 @@
"opening_hours": "^3.6.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
"panoramax-js": "^0.1.4",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
"pbf": "^3.2.1",

View file

@ -5,6 +5,8 @@ import GenericImageProvider from "./GenericImageProvider"
import { Store, UIEventSource } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider"
import Panoramax from "./Panoramax"
import { Utils } from "../../Utils"
/**
* A generic 'from the interwebz' image picker, without attribution
@ -28,6 +30,7 @@ export default class AllImageProviders {
Mapillary.singleton,
WikidataImageProvider.singleton,
WikimediaImageProvider.singleton,
Panoramax.singleton,
AllImageProviders.genericImageProvider,
]
public static apiUrls: string[] = [].concat(
@ -41,11 +44,8 @@ export default class AllImageProviders {
mapillary: Mapillary.singleton,
wikidata: WikidataImageProvider.singleton,
wikimedia: WikimediaImageProvider.singleton,
panoramax: Panoramax.singleton
}
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<
string,
UIEventSource<ProvidedImage[]>
>()
public static byName(name: string) {
return AllImageProviders.providersByName[name.toLowerCase()]
@ -66,45 +66,32 @@ export default class AllImageProviders {
return AllImageProviders.genericImageProvider
}
/**
* Tries to extract all image data for this image
*/
public static LoadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[]
): Store<ProvidedImage[]> {
if (tags.data.id === undefined) {
if (tags?.data?.id === undefined) {
return undefined
}
const cacheKey = tags.data.id + tagKey
const cached = this._cache.get(cacheKey)
if (cached !== undefined) {
return cached
}
const source = new UIEventSource([])
this._cache.set(cacheKey, source)
const allSources: Store<ProvidedImage[]>[] = []
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
let prefixes = imageProvider.defaultKeyPrefixes
if (tagKey !== undefined) {
prefixes = tagKey
}
const singleSource = imageProvider.GetRelevantUrls(tags, {
prefixes: prefixes,
})
/*
By default, 'GetRelevantUrls' uses the defaultKeyPrefixes.
However, we override them if a custom image tag is set, e.g. 'image:menu'
*/
const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes
const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes))
allSources.push(singleSource)
singleSource.addCallbackAndRunD((_) => {
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
const uniq = []
const seen = new Set<string>()
for (const img of all) {
if (seen.has(img.url)) {
continue
}
seen.add(img.url)
uniq.push(img)
}
source.setData(uniq)
const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url)
source.set(dedup)
})
}
return source

View file

@ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider {
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))) {
return []
return undefined
}
try {
new URL(value)
} catch (_) {
// Not a valid URL
return []
return undefined
}
return [
Promise.resolve({
key: key,
url: value,
provider: this,
id: value,
}),
]
return [{
key: key,
url: value,
provider: this,
id: value,
}]
}
SourceIcon() {

View file

@ -1,4 +1,4 @@
import { Store, UIEventSource } from "../UIEventSource"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import BaseUIElement from "../../UI/BaseUIElement"
import { LicenseInfo } from "./LicenseInfo"
import { Utils } from "../../Utils"
@ -10,6 +10,7 @@ export interface ProvidedImage {
provider: ImageProvider
id: string
date?: Date,
status?: string | "ready"
/**
* Compass angle of the taken image
* 0 = north, 90° = East
@ -26,56 +27,45 @@ export default abstract class ImageProvider {
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.
* This iterates over _all_ tags and matches _anything_ that might be an image
* Gets all the relevant URLS for the given tags and for the given prefixes;
* extracts the necessary information
* @param tags
* @param prefixes
*/
public GetRelevantUrls(
allTags: Store<any>,
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 }[]
>([])
public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> {
const relevantUrls: ProvidedImage[] = []
const seenValues = new Set<string>()
allTags.addCallbackAndRunD((tags) => {
for (const key in tags) {
if (!prefixes.some((prefix) => key.startsWith(prefix))) {
for (const key in tags) {
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
}
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
for (const value of values) {
if (seenValues.has(value)) {
continue
}
seenValues.add(value)
this.ExtractUrls(key, value).then((promises) => {
for (const promise of promises ?? []) {
if (promise === undefined) {
continue
}
promise.then((providedImage) => {
if (providedImage === undefined) {
return
}
relevantUrls.data.push(providedImage)
relevantUrls.ping()
})
}
})
seenValues.add(value)
let images = this.ExtractUrls(key, value)
if(!Array.isArray(images)){
images = await images
}
if(images){
relevantUrls.push(...images)
}
}
})
}
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: {
url: string

View file

@ -9,6 +9,8 @@ import { Changes } from "../Osm/Changes"
import Translations from "../../UI/i18n/Translations"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import { Translation } from "../../UI/i18n/Translation"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoOperations } from "../GeoOperations"
/**
* The ImageUploadManager has a
@ -17,7 +19,8 @@ export class ImageUploadManager {
private readonly _uploader: ImageUploader
private readonly _featureProperties: FeaturePropertiesStore
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 _uploadFinished: Map<string, UIEventSource<number>> = new Map()
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map()
@ -32,13 +35,17 @@ export class ImageUploadManager {
uploader: ImageUploader,
featureProperties: FeaturePropertiesStore,
osmConnection: OsmConnection,
changes: Changes
changes: Changes,
gpsLocation: Store<GeolocationCoordinates | undefined>,
allFeatures: IndexedFeatureSource,
) {
this._uploader = uploader
this._featureProperties = featureProperties
this._layout = layout
this._osmConnection = osmConnection
this._changes = changes
this._indexedFeatures = allFeatures
this._gps = gpsLocation
const failed = this.getCounterFor(this._uploadFailed, "*")
const done = this.getCounterFor(this._uploadFinished, "*")
@ -47,7 +54,7 @@ export class ImageUploadManager {
(startedCount) => {
return startedCount > failed.data + done.data
},
[failed, done]
[failed, done],
)
}
@ -55,7 +62,7 @@ export class ImageUploadManager {
* Gets various counters.
* Note that counters can only increase
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
* @param featureId: the id of the feature you want information for. '*' has a global counter
* @param featureId the id of the feature you want information for. '*' has a global counter
*/
public getCountsFor(featureId: string | "*"): {
retried: Store<number>
@ -96,7 +103,7 @@ export class ImageUploadManager {
public async uploadImageAndApply(
file: File,
tagsStore: UIEventSource<OsmTags>,
targetKey?: string
targetKey?: string,
): Promise<void> {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
@ -105,28 +112,15 @@ export class ImageUploadManager {
const tags = tagsStore.data
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 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 author = this._osmConnection.userDetails.data.name
const action = await this.uploadImageWithLicense(
featureId,
title,
description,
author,
file,
targetKey,
tags?.data?.["_orig_theme"]
tags?.data?.["_orig_theme"],
)
if (!action) {
@ -146,23 +140,31 @@ export class ImageUploadManager {
private async uploadImageWithLicense(
featureId: OsmId,
title: string,
description: string,
author: string,
blob: File,
targetKey: string | undefined,
theme?: string
theme?: string,
): Promise<LinkImageAction> {
this.increaseCountFor(this._uploadStarted, featureId)
const properties = this._featureProperties.getStore(featureId)
let key: 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 {
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(blob, location, author))
} catch (e) {
this.increaseCountFor(this._uploadRetried, featureId)
console.error("Could not upload image, trying again:", e)
try {
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
;({ key, value , absoluteUrl} = await this._uploader.uploadImage(blob, location, author))
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
} catch (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)
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)
const action = new LinkImageAction(featureId, key, value, properties, {
return new LinkImageAction(featureId, key, value, properties, {
theme: theme ?? this._layout.id,
changeType: "add-image",
})
return action
}
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {

View file

@ -3,13 +3,10 @@ export interface ImageUploader {
/**
* Uploads the 'blob' as image, with some metadata.
* Returns the URL to be linked + the appropriate key to add this to OSM
* @param title
* @param description
* @param blob
*/
uploadImage(
title: string,
description: string,
blob: File
): Promise<{ key: string; value: string }>
blob: File,
currentGps: [number,number],
author: string
): Promise<{ key: string; value: string, absoluteUrl: string }>
}

View file

@ -3,14 +3,12 @@ import BaseUIElement from "../../UI/BaseUIElement"
import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import { LicenseInfo } from "./LicenseInfo"
import { ImageUploader } from "./ImageUploader"
export class Imgur extends ImageProvider implements ImageUploader {
export class Imgur extends ImageProvider {
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
public static readonly singleton = new Imgur()
public readonly name = "Imgur"
public readonly defaultKeyPrefixes: string[] = ["image"]
public readonly maxFileSizeInMegabytes = 10
public static readonly apiUrl = "https://api.imgur.com/3/image"
public static readonly supportingUrls = ["https://i.imgur.com"]
private constructor() {
@ -21,57 +19,23 @@ export class Imgur extends ImageProvider implements ImageUploader {
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 {
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))) {
return [
Promise.resolve({
{
url: value,
key: key,
provider: this,
id: value,
}),
}
]
}
return []
return undefined
}
/**

View file

@ -1,14 +1,14 @@
export class LicenseInfo {
title: string = ""
title?: string = ""
artist: string = ""
license: string = undefined
licenseShortName: string = ""
usageTerms: string = ""
attributionRequired: boolean = false
copyrighted: boolean = false
credit: string = ""
description: string = ""
informationLocation: URL = undefined
license?: string = undefined
licenseShortName?: string = ""
usageTerms?: string = ""
attributionRequired?: boolean = false
copyrighted?: boolean = false
credit?: string = ""
description?: string = ""
informationLocation?: URL = undefined
date?: Date
views?: number
}

View file

@ -131,8 +131,9 @@ export class Mapillary extends ImageProvider {
return new SvelteUIElement(MapillaryIcon, { url })
}
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
return [this.PrepareUrlAsync(key, value)]
async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
const img = await this.PrepareUrlAsync(key, value)
return [img]
}
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {

View 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,
}
}
}

View file

@ -5,6 +5,7 @@ import Wikidata from "../Web/Wikidata"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte"
import { Utils } from "../../Utils"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
export class WikidataImageProvider extends ImageProvider {
public static readonly singleton = new WikidataImageProvider()
@ -25,28 +26,28 @@ export class WikidataImageProvider extends ImageProvider {
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)) {
return []
return undefined
}
const entity = await Wikidata.LoadWikidataEntryAsync(value)
if (entity === undefined) {
return []
return undefined
}
const allImages: Promise<ProvidedImage>[] = []
const allImages: Promise<ProvidedImage[]>[] = []
// P18 is the claim 'depicted in this image'
for (const img of Array.from(entity.claims.get("P18") ?? [])) {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
allImages.push(...promises)
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, img)
allImages.push(promises)
}
// P373 is 'commons category'
for (let cat of Array.from(entity.claims.get("P373") ?? [])) {
if (!cat.startsWith("Category:")) {
cat = "Category:" + cat
}
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
allImages.push(...promises)
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, cat)
allImages.push(promises)
}
const commons = entity.commons
@ -54,10 +55,11 @@ export class WikidataImageProvider extends ImageProvider {
commons !== undefined &&
(commons.startsWith("Category:") || commons.startsWith("File:"))
) {
const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
allImages.push(...promises)
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
allImages.push(promises)
}
return allImages
const resolved = await Promise.all(Utils.NoNull(allImages))
return [].concat(...resolved)
}
public DownloadAttribution(_): Promise<any> {

View file

@ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider {
return value
}
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
value
value,
)}`
if (useHd) {
return baseUrl
@ -97,28 +97,27 @@ export class WikimediaImageProvider extends ImageProvider {
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)
if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) {
return []
return undefined
}
value = WikimediaImageProvider.removeCommonsPrefix(value)
if (value.startsWith("Category:")) {
const urls = await Wikimedia.GetCategoryContents(value)
return urls
.filter((url) => url.startsWith("File:"))
.map((image) => Promise.resolve(this.UrlForImage(image)))
return urls.filter((url) => url.startsWith("File:"))
.map((image) => this.UrlForImage(image))
}
if (value.startsWith("File:")) {
return [Promise.resolve(this.UrlForImage(value))]
return [this.UrlForImage(value)]
}
if (value.startsWith("http")) {
// PRobably an error
return []
// Probably an error
return undefined
}
// 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> {
@ -148,7 +147,7 @@ export class WikimediaImageProvider extends ImageProvider {
console.warn(
"The file",
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
}

View file

@ -69,16 +69,14 @@ export default class DeleteAction extends OsmChangeAction {
* const obj : OsmNode= new OsmNode(1)
* 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 state = { dryRun: new ImmutableStore(true), osmConnection: new OsmConnection() }
* const descr = await da.CreateChangeDescriptions(new Changes(state), obj)
* const descr = await da.CreateChangeDescriptions(Changes.createTestObject(), obj)
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase",changeType: "deletion"}, type: "node",id: 1 }
*
* // Must not crash if softDeletionTags are undefined
* const da = new DeleteAction("node/1", undefined, {theme: "test", specialMotivation: "Testcase"}, true)
* const obj : OsmNode= new OsmNode(1)
* obj.tags = {id:"node/1",name:"Monte Piselli - San Giacomo"}
* const state = { dryRun: new ImmutableStore(true), osmConnection: new OsmConnection() }
* const descr = await da.CreateChangeDescriptions(new Changes(state), obj)
* const descr = await da.CreateChangeDescriptions(Changes.createTestObject(), obj)
* descr[0] // => {doDelete: true, meta: {theme: "test", specialMotivation: "Testcase", changeType: "deletion"}, type: "node",id: 1 }
*/
public async CreateChangeDescriptions(

View file

@ -1,5 +1,5 @@
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
import { Store, UIEventSource } from "../UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import Constants from "../../Models/Constants"
import OsmChangeAction from "./Actions/OsmChangeAction"
import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
@ -11,13 +11,12 @@ import { GeoLocationPointProperties } from "../State/GeoLocationState"
import { GeoOperations } from "../GeoOperations"
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
import { OsmConnection } from "./OsmConnection"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import OsmObjectDownloader from "./OsmObjectDownloader"
import ChangeLocationAction from "./Actions/ChangeLocationAction"
import ChangeTagAction from "./Actions/ChangeTagAction"
import FeatureSwitchState from "../State/FeatureSwitchState"
import DeleteAction from "./Actions/DeleteAction"
import MarkdownUtils from "../../Utils/MarkdownUtils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
/**
* Handles all changes made to OSM.
@ -30,7 +29,9 @@ export class Changes {
public readonly state: {
allElements?: IndexedFeatureSource
osmConnection: OsmConnection
featureSwitches?: FeatureSwitchState
featureSwitches?: {
featureSwitchMorePrivacy?: Store<boolean>
}
}
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
public readonly backend: string
@ -45,12 +46,15 @@ export class Changes {
constructor(
state: {
dryRun: Store<boolean>
featureSwitches: {
featureSwitchMorePrivacy?: Store<boolean>
featureSwitchIsTesting?: Store<boolean>
},
osmConnection: OsmConnection,
reportError?: (error: string) => void,
featureProperties?: FeaturePropertiesStore,
historicalUserLocations?: FeatureSource,
allElements?: IndexedFeatureSource
featurePropertiesStore?: FeaturePropertiesStore
osmConnection: OsmConnection
historicalUserLocations?: FeatureSource
featureSwitches?: FeatureSwitchState
},
leftRightSensitive: boolean = false,
reportError?: (string: string | Error, extramessage?: string) => void
@ -59,14 +63,18 @@ export class Changes {
// We keep track of all changes just as well
this.allChanges.setData([...this.pendingChanges.data])
// 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.backend = state.osmConnection.Backend()
this._reportError = reportError
this._changesetHandler = new ChangesetHandler(
state.dryRun,
state.featureSwitches.featureSwitchIsTesting,
state.osmConnection,
state.featurePropertiesStore,
state.featureProperties,
this,
(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
}
public static createTestObject(): Changes{
return new Changes({
osmConnection: new OsmConnection(),
featureSwitches:{
featureSwitchIsTesting: new ImmutableStore(true)
}
})
}
static buildChangesetXML(
csId: string,
allChanges: {

View file

@ -1,10 +1,6 @@
import { GeoOperations } from "./GeoOperations"
import { Utils } from "../Utils"
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 { CountryCoder } from "latlon2country"
import Constants from "../Models/Constants"

View file

@ -49,6 +49,9 @@ export default class Constants {
...Constants.added_by_default,
...Constants.no_include,
] as const
public static panoramax: { url: string, token: string } = packagefile.config.panoramax
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
moreScreenUnlock: 1,

View file

@ -50,7 +50,6 @@ import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoEl
import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { Imgur } from "../Logic/ImageProviders/Imgur"
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
@ -70,6 +69,7 @@ import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider"
import SearchState from "../Logic/State/SearchState"
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.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: layoutSource,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
featureSwitches: this.featureSwitches,
},
this,
layout?.isLeftRightSensitive() ?? false,
(e, extraMsg) => this.reportError(e, extraMsg),
)
@ -366,10 +359,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.imageUploadManager = new ImageUploadManager(
layout,
Imgur.singleton,
new PanoramaxUploader(Constants.panoramax.url, Constants.panoramax.token),
this.featureProperties,
this.osmConnection,
this.changes,
this.geolocation.geolocationState.currentGPSLocation,
this.indexedFeatures
)
this.favourites = new FavouritesFeatureSource(this)
const longAgo = new Date()

View file

@ -13,6 +13,9 @@
import { onDestroy } from "svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
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>
let fallbackImage: string = undefined
@ -30,7 +33,7 @@
let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown => {
if (!shown) {
previewedImage.set(false)
previewedImage.set(undefined)
}
}))
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
@ -49,12 +52,12 @@
type: "Feature",
properties: {
id: image.id,
rotation: image.rotation
rotation: image.rotation,
},
geometry: {
type: "Point",
coordinates: [image.lon, image.lat]
}
coordinates: [image.lon, image.lat],
},
}
console.log(f)
state?.geocodedImages.set([f])
@ -73,36 +76,45 @@
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
</div>
</Popup>
<div class="relative shrink-0">
<div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}
class:cursor-zoom-in={canZoom}
on:click={() => {
{#if image.status !== undefined && image.status !== "ready"}
<div class="h-full flex flex-col justify-center">
<Loading>
<Tr t={Translations.t.image.processing}/>
</Loading>
</div>
{:else}
<div class="relative shrink-0">
<div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}
class:cursor-zoom-in={canZoom}
on:click={() => {
previewedImage?.set(image)
}}
on:error={() => {
on:error={() => {
if (fallbackImage) {
imgEl.src = fallbackImage
}
}}
src={image.url}
/>
src={image.url}
/>
{#if canZoom && loaded}
<div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div>
{/if}
{#if canZoom && loaded}
<div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div>
{/if}
</div>
<div class="absolute bottom-0 left-0">
<ImageAttribution {image} {attributionFormat} />
</div>
</div>
<div class="absolute bottom-0 left-0">
<ImageAttribution {image} {attributionFormat} />
</div>
</div>
{/if}

View file

@ -31,7 +31,7 @@ export class ImageCarousel extends Toggle {
image: url,
state,
previewedImage: state?.previewedImage,
})
}).SetClass("h-full")
if (url.key !== undefined) {
image = new Combine([
@ -42,8 +42,8 @@ export class ImageCarousel extends Toggle {
]).SetClass("relative")
}
image
.SetClass("w-full block cursor-zoom-in")
.SetStyle("min-width: 50px; background: grey;")
.SetClass("w-full h-full block cursor-zoom-in low-interaction")
.SetStyle("min-width: 50px;")
uiElements.push(image)
} catch (e) {
console.error("Could not generate image element for", url.url, "due to", e)

View file

@ -83,16 +83,6 @@
</div>
</FileSelector>
<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} />
</div>
</div>

View file

@ -15,7 +15,6 @@ import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { MenuState } from "../Models/MenuState"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
@ -27,6 +26,7 @@ import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import SearchState from "../Logic/State/SearchState"
import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState"
import GeocodeResult from "./Search/GeocodeResult.svelte"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
/**
* The state needed to render a special Visualisation.
@ -37,10 +37,8 @@ export interface SpecialVisualizationState {
readonly featureSwitches: FeatureSwitchState
readonly layerState: LayerState
readonly featureProperties: {
getStore(id: string): UIEventSource<Record<string, string>>,
trackFeature?(feature: { properties: OsmTags })
}
readonly featureSummary: SummaryTileSourceRewriter
readonly featureProperties: FeaturePropertiesStore
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
/**

View file

@ -714,7 +714,7 @@ export default class SpecialVisualizations {
},
],
constr: (state, tags, args) => {
const targetKey = args[0] === "" ? undefined : args[0]
const targetKey = args[0] === "" ? undefined : args[0]
return new SvelteUIElement(UploadImage, {
state,
tags,

View file

@ -414,6 +414,29 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
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
*

View file

@ -1,18 +1,11 @@
import { ExtraFuncParams, ExtraFunctions } from "../../src/Logic/ExtraFunctions"
import { OsmFeature } from "../../src/Models/OsmFeature"
import { describe, expect, it } from "vitest"
import { Feature } from "geojson"
import { OsmConnection } from "../../src/Logic/Osm/OsmConnection"
import { ImmutableStore, UIEventSource } from "../../src/Logic/UIEventSource"
import { UIEventSource } from "../../src/Logic/UIEventSource"
import { Changes } from "../../src/Logic/Osm/Changes"
import LinkImageAction from "../../src/Logic/Osm/Actions/LinkImageAction"
import FeaturePropertiesStore from "../../src/Logic/FeatureSource/Actors/FeaturePropertiesStore"
describe("Changes", () => {
it("should correctly apply the image tag if an image gets linked in between", async () => {
const dryRun = new ImmutableStore(true)
const osmConnection = new OsmConnection({ dryRun })
const changes = new Changes({ osmConnection, dryRun })
const changes = Changes.createTestObject()
const id = "node/42"
const tags = new UIEventSource({ id, amenity: "shop" })
const addImage = new LinkImageAction(

View file

@ -1,14 +1,9 @@
import { Utils } from "../../../../src/Utils"
import { OsmRelation } from "../../../../src/Logic/Osm/OsmObject"
import {
InPlaceReplacedmentRTSH,
TurnRestrictionRSH,
} from "../../../../src/Logic/Osm/Actions/RelationSplitHandler"
import { InPlaceReplacedmentRTSH, TurnRestrictionRSH } from "../../../../src/Logic/Osm/Actions/RelationSplitHandler"
import { Changes } from "../../../../src/Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
import OsmObjectDownloader from "../../../../src/Logic/Osm/OsmObjectDownloader"
import { ImmutableStore } from "../../../../src/Logic/UIEventSource"
import { OsmConnection } from "../../../../src/Logic/Osm/OsmConnection"
describe("RelationSplitHandler", () => {
Utils.injectJsonDownloadForTests("https://api.openstreetmap.org/api/0.6/node/1124134958/ways", {
@ -653,10 +648,7 @@ describe("RelationSplitHandler", () => {
downloader
)
const changeDescription = await splitter.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(false),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",")
const expected = "687866206,295132739,-1,690497698"
@ -710,10 +702,7 @@ describe("RelationSplitHandler", () => {
downloader
)
const changeDescription = await splitter.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(false),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
const allIds = changeDescription[0].changes["members"]
.map((m) => m.type + "/" + m.ref + "-->" + m.role)
@ -734,10 +723,7 @@ describe("RelationSplitHandler", () => {
downloader
)
const changesReverse = await splitterReverse.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(false),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
expect(changesReverse.length).toEqual(0)
})

View file

@ -1,5 +1,4 @@
import { Utils } from "../../../../src/Utils"
import LayoutConfig from "../../../../src/Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../../../src/Logic/BBox"
import ReplaceGeometryAction from "../../../../src/Logic/Osm/Actions/ReplaceGeometryAction"
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"
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][]>[
[3.216690793633461, 51.21474084112525],
@ -890,10 +590,7 @@ describe("ReplaceGeometryAction", () => {
const data = await Utils.downloadJson(url)
const fullNodeDatabase = new FullNodeDatabaseSource()
fullNodeDatabase.handleOsmJson(data, 0, 0, 0)
const changes = new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
const changes = Changes.createTestObject()
const osmConnection = new OsmConnection({
dryRun: new ImmutableStore(true),
})

View file

@ -2,8 +2,6 @@ import { Utils } from "../../../../src/Utils"
import SplitAction from "../../../../src/Logic/Osm/Actions/SplitAction"
import { Changes } from "../../../../src/Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
import { OsmConnection } from "../../../../src/Logic/Osm/OsmConnection"
import { ImmutableStore } from "../../../../src/Logic/UIEventSource"
describe("SplitAction", () => {
{
@ -2690,10 +2688,7 @@ describe("SplitAction", () => {
theme: "test",
})
const changeDescription = await splitter.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
expect(changeDescription[0].type).toBe("node")
@ -2720,10 +2715,7 @@ describe("SplitAction", () => {
theme: "test",
})
const changeDescription = await splitter.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
expect(changeDescription.length).toBe(2)
@ -2742,10 +2734,7 @@ describe("SplitAction", () => {
theme: "test",
})
const changeDescription = await splitter.CreateChangeDescriptions(
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
// Should be a new node
@ -2760,10 +2749,7 @@ describe("SplitAction", () => {
theme: "test",
})
const changes = await splitAction.Perform(
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
console.log(changes)
// 8715440368 is the expected point of the split
@ -2803,10 +2789,7 @@ describe("SplitAction", () => {
1
)
const changes = await splitAction.Perform(
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
Changes.createTestObject()
)
// THe first change is the creation of the new node

View file

@ -1,8 +1,6 @@
import { ChangeDescription } from "../../../src/Logic/Osm/Actions/ChangeDescription"
import { Changes } from "../../../src/Logic/Osm/Changes"
import { expect, it } from "vitest"
import { ImmutableStore } from "../../../src/Logic/UIEventSource"
import { OsmConnection } from "../../../src/Logic/Osm/OsmConnection"
it("Generate preXML from changeDescriptions", () => {
const changeDescrs: ChangeDescription[] = [
@ -29,11 +27,7 @@ it("Generate preXML from changeDescriptions", () => {
},
},
]
const c = new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
const descr = c.CreateChangesetObjects(changeDescrs, [])
const descr = Changes.createTestObject().CreateChangesetObjects(changeDescrs, [])
expect(descr.modifiedObjects).toHaveLength(0)
expect(descr.deletedObjects).toHaveLength(0)
expect(descr.newObjects).toHaveLength(1)

View file

@ -6,21 +6,27 @@ import { Changes } from "../../../src/Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
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("RewriteTagsOf", () => {
it("should insert new tags", () => {
const changesetHandler = new ChangesetHandler(
new UIEventSource<boolean>(true),
new OsmConnection({}),
elstorage(),
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
)
const changesetHandler = createChangesetHandler()
const oldChangesetMeta = {
type: "changeset",
@ -57,13 +63,13 @@ describe("ChangesetHanlder", () => {
},
],
new Map<string, string>(),
oldChangesetMeta
oldChangesetMeta,
)
const d = Utils.asDict(rewritten)
expect(d.size).toEqual(10)
expect(d.get("answer")).toEqual("5")
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("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
@ -74,15 +80,7 @@ describe("ChangesetHanlder", () => {
expect(d.get("newTag")).toEqual("newValue")
})
it("should aggregate numeric tags", () => {
const changesetHandler = new ChangesetHandler(
new UIEventSource<boolean>(true),
new OsmConnection({}),
elstorage(),
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
)
const changesetHandler = createChangesetHandler()
const oldChangesetMeta = {
type: "changeset",
id: 118443748,
@ -118,14 +116,14 @@ describe("ChangesetHanlder", () => {
},
],
new Map<string, string>(),
oldChangesetMeta
oldChangesetMeta,
)
const d = Utils.asDict(rewritten)
expect(d.size).toEqual(9)
expect(d.get("answer")).toEqual("42")
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("host")).toEqual("https://mapcomplete.org/toerisme_vlaanderen.html")
@ -135,15 +133,7 @@ describe("ChangesetHanlder", () => {
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
})
it("should rewrite special reasons with the correct ID", () => {
const changesetHandler = new ChangesetHandler(
new UIEventSource<boolean>(true),
new OsmConnection({}),
elstorage(),
new Changes({
dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection(),
})
)
const changesetHandler = createChangesetHandler()
const oldChangesetMeta = {
type: "changeset",
id: 118443748,
@ -173,14 +163,14 @@ describe("ChangesetHanlder", () => {
const rewritten = changesetHandler.RewriteTagsOf(
[],
new Map<string, string>([["node/-1", "node/42"]]),
oldChangesetMeta
oldChangesetMeta,
)
const d = Utils.asDict(rewritten)
expect(d.size).toEqual(9)
expect(d.get("answer")).toEqual("5")
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("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 hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
extraMetaTags,
changes
changes,
)
// "Special rewrite did not trigger"
expect(hasSpecialMotivationChanges).toBe(true)