diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26ec21fc6..c978a0746 100644
--- a/CHANGELOG.md
+++ b/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))
* **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)
diff --git a/assets/layers/advertising/advertising.json b/assets/layers/advertising/advertising.json
index 5639c2708..8e8f28f32 100644
--- a/assets/layers/advertising/advertising.json
+++ b/assets/layers/advertising/advertising.json
@@ -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)"
}
}
]
diff --git a/langs/de.json b/langs/de.json
index 0c730c525..3ec9ccae4 100644
--- a/langs/de.json
+++ b/langs/de.json
@@ -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 {title}.
{description}
",
- "typeTitle": "Dies ist {title}"
+ "typeTitle": "Dies ist {title}"
},
"privacy": {
"editingIntro": "Ihre Änderungen werden auf OpenStreetMap gespeichert und sind öffentlich zugänglich. Ein mit MapComplete erstellter Änderungssatz enthält folgende Daten:",
diff --git a/langs/en.json b/langs/en.json
index c0bf04a25..dcc97ecc7 100644
--- a/langs/en.json
+++ b/langs/en.json
@@ -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",
diff --git a/langs/es.json b/langs/es.json
index 07f6f6e7d..894a9d0b8 100644
--- a/langs/es.json
+++ b/langs/es.json
@@ -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.",
diff --git a/langs/layers/ca.json b/langs/layers/ca.json
index 1212ae08a..58d6b44a3 100644
--- a/langs/layers/ca.json
+++ b/langs/layers/ca.json
@@ -8957,23 +8957,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "0": {
- "then": "Les imatges que feu tindran llicència CC0 i s'afegiran al domini públic. Això vol dir que tothom pot utilitzar les vostres imatges per a qualsevol propòsit. Aquesta és l'opció predeterminada. "
- },
- "1": {
- "then": "Les imatges que feu tindran llicència CC0 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 CC-BY 4.0 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 CC-BY-SA 4.0 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": {
diff --git a/langs/layers/cs.json b/langs/layers/cs.json
index f5f685abb..9555c9236 100644
--- a/langs/layers/cs.json
+++ b/langs/layers/cs.json
@@ -8981,23 +8981,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "0": {
- "then": "Pořízené fotografie budou licencovány pod CC0 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. Toto je výchozí volba."
- },
- "1": {
- "then": "Pořízené fotografie budou licencovány pod CC0 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 CC-BY 4.0, což vyžaduje, aby vás uvedl každý, kdo použije vaší fotku"
- },
- "3": {
- "then": "Pořízené fotografie budou licencovány pod CC-BY-SA 4.0, 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": {
diff --git a/langs/layers/da.json b/langs/layers/da.json
index aea73eae3..f428c36c1 100644
--- a/langs/layers/da.json
+++ b/langs/layers/da.json
@@ -2757,14 +2757,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "1": {
- "then": "Billeder, som du har taget, vil blive udgivet under CC0-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": {
diff --git a/langs/layers/de.json b/langs/layers/de.json
index f20f17272..8e952dceb 100644
--- a/langs/layers/de.json
+++ b/langs/layers/de.json
@@ -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 CC0 lizenziert und der Public Domain hinzugefügt. Das bedeutet, dass jeder Ihre Bilder für jeden Zweck verwenden kann. Dies ist die Standardeinstellung."
- },
- "1": {
- "then": "Ihre aufgenommenen Bilder werden mit CC0 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 CC-BY 4.0 lizenziert, was bedeutet, dass jeder, der Ihr Bild verwendet, Sie als Urheber nennen muss"
- },
- "3": {
- "then": "Die von Ihnen aufgenommenen Bilder werden mit CC-BY-SA 4.0 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": {
diff --git a/langs/layers/en.json b/langs/layers/en.json
index 5c6433935..d39ceb361 100644
--- a/langs/layers/en.json
+++ b/langs/layers/en.json
@@ -11767,23 +11767,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "0": {
- "then": "Pictures you take will be licensed with CC0 and added to the public domain. This means that everyone can use your pictures for any purpose. This is the default choice."
- },
- "1": {
- "then": "Pictures you take will be licensed with CC0 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 CC-BY 4.0 which requires everyone using your picture that they have to attribute you"
- },
- "3": {
- "then": "Pictures you take will be licensed with CC-BY-SA 4.0 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": {
diff --git a/langs/layers/es.json b/langs/layers/es.json
index 40d7fc0c1..7c78b72fa 100644
--- a/langs/layers/es.json
+++ b/langs/layers/es.json
@@ -4923,13 +4923,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "1": {
- "then": "Las fotografías que tome tendrán una licencia con CC0 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"
},
diff --git a/langs/layers/fr.json b/langs/layers/fr.json
index 2552314fd..5353bddc3 100644
--- a/langs/layers/fr.json
+++ b/langs/layers/fr.json
@@ -7124,16 +7124,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "1": {
- "then": "Les photos que vous avez ajoutées seront sous licence CC0 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 CC-BY-SA 4.0 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": {
diff --git a/langs/layers/nl.json b/langs/layers/nl.json
index 3208a2e5a..c2ecbfe44 100644
--- a/langs/layers/nl.json
+++ b/langs/layers/nl.json
@@ -9229,23 +9229,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "0": {
- "then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de CC0-licentie en dus aan het publieke domein toegevoegd worden. Dit betekent dat iedereen je afbeeldingen kan gebruiken voor elk mogelijks gebruik. Dit is de standaard-instelling"
- },
- "1": {
- "then": "Afbeeldingen die je toevoegt zullen gepubliceerd worden met de CC0-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 CC-BY 4.0-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 CC-BY-SA 4.0-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": {
diff --git a/langs/layers/pt.json b/langs/layers/pt.json
index 9a422c5ca..415571b23 100644
--- a/langs/layers/pt.json
+++ b/langs/layers/pt.json
@@ -1912,23 +1912,6 @@
}
}
},
- "picture-license": {
- "mappings": {
- "0": {
- "then": "As fotos que você tirar serão licenciadas com CC0 e adicionadas ao domínio público. Isso significa que todos podem usar suas fotos para qualquer finalidade. Esta é a escolha padrão."
- },
- "1": {
- "then": "As fotos que você tirar serão licenciadas com CC0 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 CC-BY 4.0, que exige que todos que usam sua foto atribuam a você"
- },
- "3": {
- "then": "As fotos que você tirar serão licenciadas com CC-BY-SA 4.0, 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": {
diff --git a/langs/layers/ru.json b/langs/layers/ru.json
index 7202e84a3..55d838032 100644
--- a/langs/layers/ru.json
+++ b/langs/layers/ru.json
@@ -2063,17 +2063,6 @@
}
}
},
- "usersettings": {
- "tagRenderings": {
- "picture-license": {
- "mappings": {
- "1": {
- "then": "Изображения будут опубликованы под лицензией CC0 и перейдут в общественное достояние. Это значит, что кто угодно имеет право использовать их без ограничений."
- }
- }
- }
- }
- },
"vending_machine": {
"tagRenderings": {
"operational_status": {
diff --git a/package-lock.json b/package-lock.json
index 3c7f75732..e6f06be57 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index 234e6247c..4d8e670ff 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts
index 05ad002e2..8dabd4097 100644
--- a/src/Logic/ImageProviders/AllImageProviders.ts
+++ b/src/Logic/ImageProviders/AllImageProviders.ts
@@ -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> = new Map<
- string,
- UIEventSource
- >()
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>,
tagKey?: string[]
): Store {
- 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[] = []
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()
- 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
diff --git a/src/Logic/ImageProviders/GenericImageProvider.ts b/src/Logic/ImageProviders/GenericImageProvider.ts
index f3b02b8d4..de369c478 100644
--- a/src/Logic/ImageProviders/GenericImageProvider.ts
+++ b/src/Logic/ImageProviders/GenericImageProvider.ts
@@ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider {
this._valuePrefixBlacklist = valuePrefixBlacklist
}
- async ExtractUrls(key: string, value: string): Promise[]> {
+ 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() {
diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts
index a89ee8600..50878a94a 100644
--- a/src/Logic/ImageProviders/ImageProvider.ts
+++ b/src/Logic/ImageProviders/ImageProvider.ts
@@ -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,
- options?: {
- prefixes?: string[]
- }
- ): UIEventSource {
- 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, prefixes: string[]): Promise {
+ const relevantUrls: ProvidedImage[] = []
const seenValues = new Set()
- 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[]>
+ public getRelevantUrls(tags: Record, prefixes: string[]): Store {
+ return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
+ }
+
+
+ public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise
public abstract DownloadAttribution(providedImage: {
url: string
diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts
index 4b88623b7..d726e10a2 100644
--- a/src/Logic/ImageProviders/ImageUploadManager.ts
+++ b/src/Logic/ImageProviders/ImageUploadManager.ts
@@ -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
private readonly _uploadStarted: Map> = new Map()
private readonly _uploadFinished: Map> = new Map()
private readonly _uploadFailed: Map> = new Map()
@@ -32,13 +35,17 @@ export class ImageUploadManager {
uploader: ImageUploader,
featureProperties: FeaturePropertiesStore,
osmConnection: OsmConnection,
- changes: Changes
+ changes: Changes,
+ gpsLocation: Store,
+ 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
@@ -96,7 +103,7 @@ export class ImageUploadManager {
public async uploadImageAndApply(
file: File,
tagsStore: UIEventSource,
- targetKey?: string
+ targetKey?: string,
): Promise {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
@@ -105,28 +112,15 @@ export class ImageUploadManager {
const tags = tagsStore.data
const featureId = 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 {
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>, key: string | "*") {
diff --git a/src/Logic/ImageProviders/ImageUploader.ts b/src/Logic/ImageProviders/ImageUploader.ts
index 40601892b..dc9babe20 100644
--- a/src/Logic/ImageProviders/ImageUploader.ts
+++ b/src/Logic/ImageProviders/ImageUploader.ts
@@ -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 }>
}
diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts
index dbc2ff44c..80acda10a 100644
--- a/src/Logic/ImageProviders/Imgur.ts
+++ b/src/Logic/ImageProviders/Imgur.ts
@@ -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[]> {
+ 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
}
/**
diff --git a/src/Logic/ImageProviders/LicenseInfo.ts b/src/Logic/ImageProviders/LicenseInfo.ts
index 99998e75d..da82a9b37 100644
--- a/src/Logic/ImageProviders/LicenseInfo.ts
+++ b/src/Logic/ImageProviders/LicenseInfo.ts
@@ -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
}
diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts
index 3ce222714..5a26e31c2 100644
--- a/src/Logic/ImageProviders/Mapillary.ts
+++ b/src/Logic/ImageProviders/Mapillary.ts
@@ -131,8 +131,9 @@ export class Mapillary extends ImageProvider {
return new SvelteUIElement(MapillaryIcon, { url })
}
- async ExtractUrls(key: string, value: string): Promise[]> {
- return [this.PrepareUrlAsync(key, value)]
+ async ExtractUrls(key: string, value: string): Promise {
+ const img = await this.PrepareUrlAsync(key, value)
+ return [img]
}
public async DownloadAttribution(providedImage: { id: string }): Promise {
diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts
new file mode 100644
index 000000000..eb73b9636
--- /dev/null
+++ b/src/Logic/ImageProviders/Panoramax.ts
@@ -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 = {}
+
+ 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 {
+ 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 {
+ return [await this.getInfoFor(value).then(r => this.featureToImage(r))]
+ }
+
+
+ getRelevantUrls(tags: Record, prefixes: string[]): Store {
+ 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 {
+ 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 = 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,
+ }
+ }
+
+}
diff --git a/src/Logic/ImageProviders/WikidataImageProvider.ts b/src/Logic/ImageProviders/WikidataImageProvider.ts
index ded3396da..c64896559 100644
--- a/src/Logic/ImageProviders/WikidataImageProvider.ts
+++ b/src/Logic/ImageProviders/WikidataImageProvider.ts
@@ -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[]> {
+ public async ExtractUrls(key: string, value: string): Promise {
if (WikidataImageProvider.keyBlacklist.has(key)) {
- return []
+ return undefined
}
const entity = await Wikidata.LoadWikidataEntryAsync(value)
if (entity === undefined) {
- return []
+ return undefined
}
- const allImages: Promise[] = []
+ const allImages: Promise[] = []
// 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 {
diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts
index 020f32b4f..631ea44f9 100644
--- a/src/Logic/ImageProviders/WikimediaImageProvider.ts
+++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts
@@ -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[]> {
+ public async ExtractUrls(key: string, value: string): undefined | Promise {
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 {
@@ -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
}
diff --git a/src/Logic/Osm/Actions/DeleteAction.ts b/src/Logic/Osm/Actions/DeleteAction.ts
index fde6e867d..128b9acee 100644
--- a/src/Logic/Osm/Actions/DeleteAction.ts
+++ b/src/Logic/Osm/Actions/DeleteAction.ts
@@ -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(
diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts
index 03b971b83..c84fa9b18 100644
--- a/src/Logic/Osm/Changes.ts
+++ b/src/Logic/Osm/Changes.ts
@@ -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
+ }
}
public readonly extraComment: UIEventSource = new UIEventSource(undefined)
public readonly backend: string
@@ -45,12 +46,15 @@ export class Changes {
constructor(
state: {
- dryRun: Store
+ featureSwitches: {
+ featureSwitchMorePrivacy?: Store
+ featureSwitchIsTesting?: Store
+ },
+ 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: {
diff --git a/src/Logic/SimpleMetaTagger.ts b/src/Logic/SimpleMetaTagger.ts
index 203168d00..611207a47 100644
--- a/src/Logic/SimpleMetaTagger.ts
+++ b/src/Logic/SimpleMetaTagger.ts
@@ -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"
diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts
index d2e3d39ce..afefc9cef 100644
--- a/src/Models/Constants.ts
+++ b/src/Models/Constants.ts
@@ -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,
diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts
index bd70b1b7e..7a7ea4f50 100644
--- a/src/Models/ThemeViewState.ts
+++ b/src/Models/ThemeViewState.ts
@@ -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()
diff --git a/src/UI/Image/AttributedImage.svelte b/src/UI/Image/AttributedImage.svelte
index 60fc4bfc3..5af0aaf06 100644
--- a/src/UI/Image/AttributedImage.svelte
+++ b/src/UI/Image/AttributedImage.svelte
@@ -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
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)}}>
-