From 91844ee00f149a0818f643f0c6fdf0fabfed7f35 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 25 Sep 2024 11:54:17 +0200 Subject: [PATCH 01/36] Error: attempt to figure out why an ID is sometimes not generated --- src/Logic/Osm/Changes.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 03b971b83..6c6ffc3dd 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -18,6 +18,7 @@ import ChangeTagAction from "./Actions/ChangeTagAction" import FeatureSwitchState from "../State/FeatureSwitchState" import DeleteAction from "./Actions/DeleteAction" import MarkdownUtils from "../../Utils/MarkdownUtils" +import { SpecialVisualizationState } from "../../UI/SpecialVisualization" /** * Handles all changes made to OSM. @@ -44,14 +45,7 @@ export class Changes { private readonly _reportError?: (string: string | Error, extramessage?: string) => void constructor( - state: { - dryRun: Store - allElements?: IndexedFeatureSource - featurePropertiesStore?: FeaturePropertiesStore - osmConnection: OsmConnection - historicalUserLocations?: FeatureSource - featureSwitches?: FeatureSwitchState - }, + state: SpecialVisualizationState, leftRightSensitive: boolean = false, reportError?: (string: string | Error, extramessage?: string) => void ) { @@ -59,7 +53,11 @@ 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("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 From cf74296d23de9ae6dab902205ebe860490627c00 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 25 Sep 2024 21:22:55 +0200 Subject: [PATCH 02/36] Fix: disable image upload button (see #2178) --- src/UI/SpecialVisualizations.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 5e04a6947..5783ba2ca 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -714,14 +714,15 @@ export default class SpecialVisualizations { }, ], constr: (state, tags, args) => { - const targetKey = args[0] === "" ? undefined : args[0] + return new FixedUiElement("Due to a technical limitation, image uploads are currently not possible").SetClass("subtle low-interaction p-4np") + /*const targetKey = args[0] === "" ? undefined : args[0] return new SvelteUIElement(UploadImage, { state, tags, targetKey, labelText: args[1], image: args[2], - }) + })*/ }, }, { From 0bdc1aec61ec742d141bb3882be07b6d99df654e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 26 Sep 2024 19:15:20 +0200 Subject: [PATCH 03/36] Feat: Use panoramax to upload to. Will contain bugs --- assets/layers/usersettings/usersettings.json | 70 +------- langs/en.json | 4 +- package-lock.json | 84 ++++++++++ package.json | 6 + src/Logic/ImageProviders/AllImageProviders.ts | 6 + src/Logic/ImageProviders/ImageProvider.ts | 5 +- .../ImageProviders/ImageUploadManager.ts | 51 +++--- src/Logic/ImageProviders/ImageUploader.ts | 11 +- src/Logic/ImageProviders/Imgur.ts | 38 +---- src/Logic/ImageProviders/LicenseInfo.ts | 18 +- src/Logic/ImageProviders/Panoramax.ts | 158 ++++++++++++++++++ src/Logic/Osm/Changes.ts | 7 +- src/Logic/SimpleMetaTagger.ts | 4 - src/Logic/Web/NameSuggestionIndex.ts | 4 +- src/Models/Constants.ts | 3 + src/Models/ThemeViewState.ts | 29 +--- src/UI/Image/UploadImage.svelte | 1 - src/UI/SpecialVisualization.ts | 14 +- src/UI/SpecialVisualizations.ts | 5 +- 19 files changed, 325 insertions(+), 193 deletions(-) create mode 100644 src/Logic/ImageProviders/Panoramax.ts diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index fbfa9bfd9..2be4b4763 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -498,75 +498,7 @@ } ] }, - { - "id": "picture-license", - "description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants", - "question": { - "en": "Under what license do you want to publish your pictures?", - "de": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?", - "nl": "Met welke licentie wil je je afbeeldingen toevoegen?", - "ca": "Sota quina llicència vols publicar les teves fotos?", - "pt": "Sob que licença você deseja publicar suas fotos?", - "cs": "Pod jakou licencí chcete své fotografie zveřejnit?", - "da": "Under hvilken licens vil du frigive dine billeder?" - }, - "mappings": [ - { - "if": "mapcomplete-pictures-license=", - "icon": "./assets/layers/usersettings/scale.svg", - "then": { - "en": "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.", - "de": "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.", - "nl": "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", - "cs": "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.", - "ca": "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. ", - "pt": "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." - }, - "hideInAnswer": true - }, - { - "if": "mapcomplete-pictures-license=CC0", - "icon": "./assets/layers/usersettings/scale.svg", - "then": { - "en": "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.", - "de": "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.", - "nl": "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.", - "ru": "Изображения будут опубликованы под лицензией CC0 и перейдут в общественное достояние. Это значит, что кто угодно имеет право использовать их без ограничений.", - "cs": "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.", - "ca": "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.", - "es": "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.", - "pt": "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.", - "da": "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.", - "fr": "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." - } - }, - { - "if": "mapcomplete-pictures-license=CC-BY 4.0", - "icon": "./assets/layers/usersettings/scale.svg", - "then": { - "en": "Pictures you take will be licensed with CC-BY 4.0 which requires everyone using your picture that they have to attribute you", - "ca": "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", - "de": "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", - "nl": "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", - "cs": "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", - "pt": "As fotos que você tirar serão licenciadas com CC-BY 4.0, que exige que todos que usam sua foto atribuam a você" - } - }, - { - "if": "mapcomplete-pictures-license=CC-BY-SA 4.0", - "icon": "./assets/layers/usersettings/scale.svg", - "then": { - "en": "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.", - "de": "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.", - "nl": "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.", - "cs": "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í.", - "ca": "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.", - "fr": "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.", - "pt": "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." - } - } - ] - }, + { "id": "show_tags", "question": { diff --git a/langs/en.json b/langs/en.json index e4e1cbda7..1c68d8194 100644 --- a/langs/en.json +++ b/langs/en.json @@ -577,7 +577,7 @@ "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.", + "respectPrivacy": "Do not upload Google Maps, Google Streetview or other copyrighted sources.", "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", "upload": { "failReasons": "You might have lost connection to the internet", @@ -873,4 +873,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index be13f6370..2e4e43489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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.1", "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.1", + "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", + "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", + "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", @@ -24679,6 +24723,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 @@ -28120,6 +28177,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 @@ -31982,6 +32055,17 @@ "packet-reader": { "version": "1.0.0" }, + "panoramax-js": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", + "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", + "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 0bdfe22db..f1c23f80c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,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", @@ -177,6 +181,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", @@ -200,6 +205,7 @@ "opening_hours": "^3.6.0", "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", + "panoramax-js": "^0.1.1", "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..4304c0bd9 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -5,6 +5,7 @@ import GenericImageProvider from "./GenericImageProvider" import { Store, UIEventSource } from "../UIEventSource" import ImageProvider, { ProvidedImage } from "./ImageProvider" import { WikidataImageProvider } from "./WikidataImageProvider" +import Panoramax from "./Panoramax" /** * A generic 'from the interwebz' image picker, without attribution @@ -28,6 +29,7 @@ export default class AllImageProviders { Mapillary.singleton, WikidataImageProvider.singleton, WikimediaImageProvider.singleton, + Panoramax.singleton, AllImageProviders.genericImageProvider, ] public static apiUrls: string[] = [].concat( @@ -41,6 +43,7 @@ export default class AllImageProviders { mapillary: Mapillary.singleton, wikidata: WikidataImageProvider.singleton, wikimedia: WikimediaImageProvider.singleton, + panoramax: Panoramax.singleton } private static _cache: Map> = new Map< string, @@ -66,6 +69,9 @@ export default class AllImageProviders { return AllImageProviders.genericImageProvider } + /** + * Tries to extract all image data for this image + */ public static LoadImagesFor( tags: Store>, tagKey?: string[] diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index a89ee8600..7fe3b7b02 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -36,7 +36,7 @@ export default abstract class ImageProvider { prefixes?: string[] } ): UIEventSource { - const prefixes = options?.prefixes ?? this.defaultKeyPrefixes + const prefixes = Utils.Dedup(options?.prefixes ?? this.defaultKeyPrefixes) if (prefixes === undefined) { throw "No `defaultKeyPrefixes` defined by this image provider" } @@ -46,6 +46,9 @@ export default abstract class ImageProvider { const seenValues = new Set() allTags.addCallbackAndRunD((tags) => { for (const key in tags) { + if(key === "panoramax"){ + console.log("Inspecting", key,"against", prefixes) + } if (!prefixes.some((prefix) => key.startsWith(prefix))) { continue } diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 4b88623b7..798008f19 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], ) } @@ -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,30 @@ 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) + } try { - ;({ key, value } = await this._uploader.uploadImage(title, description, blob)) + ;({ key, value } = 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 } = await this._uploader.uploadImage(blob, location, author)) this.increaseCountFor(this._uploadRetriedSuccess, featureId) } catch (e) { console.error("Could again not upload image due to", e) diff --git a/src/Logic/ImageProviders/ImageUploader.ts b/src/Logic/ImageProviders/ImageUploader.ts index 40601892b..fbfb56abc 100644 --- a/src/Logic/ImageProviders/ImageUploader.ts +++ b/src/Logic/ImageProviders/ImageUploader.ts @@ -1,15 +1,14 @@ +import { Feature } from "geojson" + export interface ImageUploader { maxFileSizeInMegabytes?: number /** * Uploads the 'blob' as image, with some metadata. * Returns the URL to be linked + the appropriate key to add this to OSM - * @param title - * @param description - * @param blob */ uploadImage( - title: string, - description: string, - blob: File + blob: File, + currentGps: [number,number], + author: string ): Promise<{ key: string; value: string }> } diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index dbc2ff44c..f5b2c06b1 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,40 +19,6 @@ 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 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/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts new file mode 100644 index 000000000..67e82e0a9 --- /dev/null +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -0,0 +1,158 @@ +import { ImageUploader } from "./ImageUploader" +import { AuthorizedPanoramax } 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 { Feature, FeatureCollection, Point } from "geojson" +import { GeoOperations } from "../GeoOperations" + +type ImageData = Feature & { + id: string, + assets: { hd: { href: string }, sd: { href: string } }, + providers: {name: string}[] +} + +export default class PanoramaxImageProvider extends ImageProvider { + + public static readonly singleton = new PanoramaxImageProvider() + + public defaultKeyPrefixes: string[] = ["panoramax", "image"] + 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){ + console.log("Adding known meta for", meta.id) + PanoramaxImageProvider.knownMeta[meta.id] = meta + } + + /** + * 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/api/collections/${sequence}/items/${id}` + const data = await Utils.downloadJsonCached(url, 60 * 60 * 1000) + return {url, data} + } + + private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> { + const url = "https://api.panoramax.xyz/api/search?limit=1&ids=" + imageId + const metaAll = await Utils.downloadJsonCached>(url, 1000 * 60 * 60) + const data= metaAll.features[0] + return {data, url} + } + + + /** + * 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 + const url = info.url + if (!meta) { + return undefined + } + + 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, + rotation: Number(meta.properties["view:azimuth"]), + date: new Date(meta.properties.datetime), + } + } + + private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> { + const cached= PanoramaxImageProvider.knownMeta[id] + console.log("Cached version", id, cached) + if(cached){ + return {data: cached, url: undefined} + } + try { + return await this.getInfoFromMapComplete(id) + } catch (e) { + return await this.getInfoFromXYZ(id) + } + } + + + public async ExtractUrls(key: string, value: string): Promise[]> { + return [this.getInfoFor(value).then(r => this.featureToImage(r))] + } + + 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; + }> { + + 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) + await Utils.waitFor(1250) + return { + key: "panoramax", + value: img.id, + } + } + +} diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 6c6ffc3dd..48d67b845 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 { UIEventSource } from "../UIEventSource" import Constants from "../../Models/Constants" import OsmChangeAction from "./Actions/OsmChangeAction" import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription" @@ -11,7 +11,6 @@ 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" @@ -62,9 +61,9 @@ export class Changes { 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) ) 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/Logic/Web/NameSuggestionIndex.ts b/src/Logic/Web/NameSuggestionIndex.ts index 1ab669470..bb03af6b3 100644 --- a/src/Logic/Web/NameSuggestionIndex.ts +++ b/src/Logic/Web/NameSuggestionIndex.ts @@ -291,6 +291,8 @@ export default class NameSuggestionIndex { if (location === undefined) { return true } + console.log("Resolving location", i.locationSet) + /* const resolvedSet = NameSuggestionIndex.loco.resolveLocationSet(i.locationSet) if (resolvedSet) { @@ -299,7 +301,7 @@ export default class NameSuggestionIndex { const setFeature: Feature = resolvedSet.feature return turf.booleanPointInPolygon(location, setFeature.geometry) } - +*/ return false }) } diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index 20190e43c..96d1ffa9f 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -47,6 +47,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 1e2730014..b052e56ae 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -2,11 +2,7 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig" import { SpecialVisualizationState } from "../UI/SpecialVisualization" import { Changes } from "../Logic/Osm/Changes" import { Store, UIEventSource } from "../Logic/UIEventSource" -import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource -} from "../Logic/FeatureSource/FeatureSource" +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { ExportableMap, MapProperties } from "./MapProperties" import LayerState from "../Logic/State/LayerState" @@ -50,13 +46,10 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" -import NoElementsInViewDetector, { - FeatureViewState -} from "../Logic/Actors/NoElementsInViewDetector" +import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector" import FilteredLayer from "./FilteredLayer" import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" -import { Imgur } from "../Logic/ImageProviders/Imgur" import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource" import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" @@ -64,7 +57,7 @@ import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl" import Zoomcontrol from "../UI/Zoomcontrol" import { SummaryTileSource, - SummaryTileSourceRewriter + SummaryTileSourceRewriter, } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" import summaryLayer from "../assets/generated/layers/summary.json" import last_click_layerconfig from "../assets/generated/layers/last_click.json" @@ -73,6 +66,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" import Hash from "../Logic/Web/Hash" import { GeoOperations } from "../Logic/GeoOperations" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" +import { PanoramaxUploader } from "../Logic/ImageProviders/Panoramax" /** * @@ -272,14 +266,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) ) @@ -368,10 +355,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.changes, + this.geolocation.geolocationState.currentGPSLocation, + this.indexedFeatures ) this.favourites = new FavouritesFeatureSource(this) const longAgo = new Date() diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index 43dc42fec..f07cd5f38 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -90,7 +90,6 @@ state.guistate.openUsersettings("picture-license") }} > -
diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 8d58c14fd..37aa7229e 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -1,11 +1,7 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" import BaseUIElement from "./BaseUIElement" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" -import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource, -} from "../Logic/FeatureSource/FeatureSource" +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { Changes } from "../Logic/Osm/Changes" import { ExportableMap, MapProperties } from "../Models/MapProperties" @@ -18,9 +14,7 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig" import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import { MenuState } from "../Models/MenuState" import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" -import { RasterLayerPolygon } from "../Models/RasterLayers" 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" @@ -29,6 +23,7 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" import { Map as MlMap } from "maplibre-gl" import ShowDataLayer from "./Map/ShowDataLayer" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" +import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" /** * The state needed to render a special Visualisation. @@ -40,10 +35,7 @@ export interface SpecialVisualizationState { readonly layerState: LayerState readonly featureSummary: SummaryTileSourceRewriter - readonly featureProperties: { - getStore(id: string): UIEventSource> - trackFeature?(feature: { properties: OsmTags }) - } + readonly featureProperties: FeaturePropertiesStore readonly indexedFeatures: IndexedFeatureSource & LayoutSource /** diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 5783ba2ca..cc5a88691 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -714,15 +714,14 @@ export default class SpecialVisualizations { }, ], constr: (state, tags, args) => { - return new FixedUiElement("Due to a technical limitation, image uploads are currently not possible").SetClass("subtle low-interaction p-4np") - /*const targetKey = args[0] === "" ? undefined : args[0] + const targetKey = args[0] === "" ? undefined : args[0] return new SvelteUIElement(UploadImage, { state, tags, targetKey, labelText: args[1], image: args[2], - })*/ + }) }, }, { From f1329634856cb8280de1c5ead760b15cec167f4e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 26 Sep 2024 19:17:18 +0200 Subject: [PATCH 04/36] chore(release): 0.46.10 --- CHANGELOG.md | 23 +++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb16a3e6..a319cdb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [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) diff --git a/package-lock.json b/package-lock.json index 2e4e43489..0328e33b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.46.9", + "version": "0.46.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.46.9", + "version": "0.46.10", "license": "GPL-3.0-or-later", "dependencies": { "@comunica/core": "^3.0.1", diff --git a/package.json b/package.json index f1c23f80c..2b4996265 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.46.9", + "version": "0.46.10", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", From 0b992e75a481ffa5156f68d9a9fa3495a1fec4c9 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 27 Sep 2024 03:04:05 +0200 Subject: [PATCH 05/36] Fix: fix tests with some refactoring --- src/Logic/Osm/Changes.ts | 32 +- test/Logic/ActionInteraction.spec.ts | 11 +- .../OSM/Actions/RelationSplitHandler.spec.ts | 22 +- .../OSM/Actions/ReplaceGeometryAction.spec.ts | 305 +----------------- test/Logic/OSM/Actions/SplitAction.spec.ts | 27 +- test/Logic/OSM/Changes.spec.ts | 8 +- test/Logic/OSM/ChangesetHandler.spec.ts | 60 ++-- 7 files changed, 64 insertions(+), 401 deletions(-) diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 48d67b845..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 { 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" @@ -14,10 +14,9 @@ import { OsmConnection } from "./OsmConnection" 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 { SpecialVisualizationState } from "../../UI/SpecialVisualization" +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 @@ -44,7 +45,17 @@ export class Changes { private readonly _reportError?: (string: string | Error, extramessage?: string) => void constructor( - state: SpecialVisualizationState, + state: { + featureSwitches: { + featureSwitchMorePrivacy?: Store + featureSwitchIsTesting?: Store + }, + osmConnection: OsmConnection, + reportError?: (error: string) => void, + featureProperties?: FeaturePropertiesStore, + historicalUserLocations?: FeatureSource, + allElements?: IndexedFeatureSource + }, leftRightSensitive: boolean = false, reportError?: (string: string | Error, extramessage?: string) => void ) { @@ -53,7 +64,7 @@ export class Changes { 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 ?? 0) ?? [])) - if(isNaN(this._nextId)){ + 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 } @@ -73,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/test/Logic/ActionInteraction.spec.ts b/test/Logic/ActionInteraction.spec.ts index 64f1aa8f4..8f89c4849 100644 --- a/test/Logic/ActionInteraction.spec.ts +++ b/test/Logic/ActionInteraction.spec.ts @@ -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( diff --git a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts index 8b3dede8b..64a30a5f6 100644 --- a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts +++ b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts @@ -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) }) diff --git a/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts b/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts index 6e2fb73a8..e7b16d64b 100644 --- a/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts +++ b/test/Logic/OSM/Actions/ReplaceGeometryAction.spec.ts @@ -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 {building}", - 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 {addr:housenumber}", - }, - 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 {addr:unit} ", - }, - freeform: { - key: "addr:unit", - }, - mappings: [ - { - if: "addr:unit=", - then: "Geen wooneenheid-nummer", - }, - ], - }, - { - id: "grb-street", - render: { - nl: "De straat is {addr:street}", - }, - freeform: { - key: "addr:street", - }, - question: { - nl: "Wat is de straat?", - }, - }, - { - id: "grb-fixme", - render: { - nl: "De fixme is {fixme}", - }, - 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 => \"\" + f.feat.properties.id + \"\").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), }) diff --git a/test/Logic/OSM/Actions/SplitAction.spec.ts b/test/Logic/OSM/Actions/SplitAction.spec.ts index 35605a0c2..bb9ba625e 100644 --- a/test/Logic/OSM/Actions/SplitAction.spec.ts +++ b/test/Logic/OSM/Actions/SplitAction.spec.ts @@ -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 diff --git a/test/Logic/OSM/Changes.spec.ts b/test/Logic/OSM/Changes.spec.ts index 1e8e273e1..503943553 100644 --- a/test/Logic/OSM/Changes.spec.ts +++ b/test/Logic/OSM/Changes.spec.ts @@ -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) diff --git a/test/Logic/OSM/ChangesetHandler.spec.ts b/test/Logic/OSM/ChangesetHandler.spec.ts index eba47ed6b..682ec61f2 100644 --- a/test/Logic/OSM/ChangesetHandler.spec.ts +++ b/test/Logic/OSM/ChangesetHandler.spec.ts @@ -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(true), + new OsmConnection({}), + elstorage(), + changes, + e => console.error(e), + ) } describe("ChangesetHanlder", () => { describe("RewriteTagsOf", () => { it("should insert new tags", () => { - const changesetHandler = new ChangesetHandler( - new UIEventSource(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(), - 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(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(), - 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(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([["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([["node/-1", "node/42"]]) const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( extraMetaTags, - changes + changes, ) // "Special rewrite did not trigger" expect(hasSpecialMotivationChanges).toBe(true) From 7212624fadfe8e49c6ffb4e5e18d1a963c84f878 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 27 Sep 2024 03:09:49 +0200 Subject: [PATCH 06/36] Refactoring: remove 'picture license' from Usersettings --- assets/layers/ghostsign/ghostsign.json | 4 ---- assets/layers/usersettings/usersettings.json | 1 - langs/en.json | 2 +- langs/layers/ca.json | 17 ----------------- langs/layers/cs.json | 17 ----------------- langs/layers/da.json | 8 -------- langs/layers/de.json | 17 ----------------- langs/layers/en.json | 17 ----------------- langs/layers/es.json | 7 ------- langs/layers/fr.json | 10 ---------- langs/layers/nl.json | 17 ----------------- langs/layers/pt.json | 17 ----------------- langs/layers/ru.json | 11 ----------- src/UI/Image/UploadImage.svelte | 9 --------- 14 files changed, 1 insertion(+), 153 deletions(-) diff --git a/assets/layers/ghostsign/ghostsign.json b/assets/layers/ghostsign/ghostsign.json index fb43a456d..3e25ee744 100644 --- a/assets/layers/ghostsign/ghostsign.json +++ b/assets/layers/ghostsign/ghostsign.json @@ -13,7 +13,6 @@ "en": "Layer showing disused signs on buildings", "de": "Ebene, die ungenutzte Zeichen auf Gebäuden zeigt", "es": "Capa que muestra carteles en desuso en edificios", - "ca": "Capa que mostra rètols en desús dels edificis", "cs": "Vrstva zobrazující nepoužívané nápisy na budovách" }, @@ -37,7 +36,6 @@ "enableRelocation": false }, "tagRenderings": [ - "images", "advertising.historic", "advertising.type", @@ -48,7 +46,6 @@ "de": "Was ist der Text auf dem Schild?", "es": "¿Cuál es el texto del cartel?", "ca": "Quin és el text del rètol?" - }, "freeform": { "key": "inscription", @@ -95,7 +92,6 @@ "cs": "Tato cedule byla vyrobena pro: {brand}" } } - ], "lineRendering": [ { diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 2be4b4763..4b5106da3 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -498,7 +498,6 @@ } ] }, - { "id": "show_tags", "question": { diff --git a/langs/en.json b/langs/en.json index 1c68d8194..4a9478bca 100644 --- a/langs/en.json +++ b/langs/en.json @@ -873,4 +873,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} +} \ No newline at end of file diff --git a/langs/layers/ca.json b/langs/layers/ca.json index df447a3c3..26a059e00 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -8978,23 +8978,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 8ff5ead0a..afa82cb6e 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -9010,23 +9010,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 9ea5bbee4..cdc926f2e 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -2743,14 +2743,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 a818655e7..512912d02 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -11683,23 +11683,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 471c5c387..9158b0c40 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -11697,23 +11697,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 cd546c1cb..a23d9efc8 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -4932,13 +4932,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 42d05b6af..1d3ee0819 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -7133,16 +7133,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 693637837..d1fecaf88 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -9225,23 +9225,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 07aa7ac40..d2a342ae8 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/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index f07cd5f38..ab74d4b52 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -83,15 +83,6 @@
- -
From 967f2f16171e3cc9afb338b553154b8374f09b1e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 27 Sep 2024 03:15:48 +0200 Subject: [PATCH 07/36] Chore: fix tests --- src/Logic/Osm/Actions/DeleteAction.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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( From 55cfd65f3bca63dc64c9a60ff6f92e2fa4320038 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 27 Sep 2024 03:26:17 +0200 Subject: [PATCH 08/36] Feature: add support for panoramax picturs for non-default keys (e.g. image:menu) --- src/Logic/ImageProviders/AllImageProviders.ts | 11 ++++++----- src/Logic/ImageProviders/ImageProvider.ts | 2 +- src/Logic/ImageProviders/ImageUploadManager.ts | 14 +++++++++----- src/Logic/ImageProviders/ImageUploader.ts | 4 +--- src/Logic/ImageProviders/Panoramax.ts | 4 +++- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts index 4304c0bd9..476571c1e 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -90,13 +90,14 @@ export default class AllImageProviders { 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' + */ + prefixes: tagKey ?? imageProvider.defaultKeyPrefixes, }) allSources.push(singleSource) singleSource.addCallbackAndRunD((_) => { diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 7fe3b7b02..999516ee1 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -49,7 +49,7 @@ export default abstract class ImageProvider { if(key === "panoramax"){ console.log("Inspecting", key,"against", prefixes) } - if (!prefixes.some((prefix) => key.startsWith(prefix))) { + if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) { continue } const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 798008f19..d726e10a2 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -62,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 @@ -157,13 +157,14 @@ export class ImageUploadManager { const feature = this._indexedFeatures.featuresById.data.get(featureId) location = GeoOperations.centerpointCoordinates(feature) } + let absoluteUrl: string try { - ;({ key, value } = await this._uploader.uploadImage(blob, location, author)) + ;({ 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(blob, location, author)) + ;({ 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) @@ -173,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 fbfb56abc..dc9babe20 100644 --- a/src/Logic/ImageProviders/ImageUploader.ts +++ b/src/Logic/ImageProviders/ImageUploader.ts @@ -1,5 +1,3 @@ -import { Feature } from "geojson" - export interface ImageUploader { maxFileSizeInMegabytes?: number /** @@ -10,5 +8,5 @@ export interface ImageUploader { blob: File, currentGps: [number,number], author: string - ): Promise<{ key: string; value: string }> + ): Promise<{ key: string; value: string, absoluteUrl: string }> } diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index 67e82e0a9..6f8291d0f 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -18,7 +18,7 @@ export default class PanoramaxImageProvider extends ImageProvider { public static readonly singleton = new PanoramaxImageProvider() - public defaultKeyPrefixes: string[] = ["panoramax", "image"] + public defaultKeyPrefixes: string[] = ["panoramax"] public readonly name: string = "panoramax" private static knownMeta: Record = {} @@ -128,6 +128,7 @@ export class PanoramaxUploader implements ImageUploader { async uploadImage(blob: File, currentGps: [number, number], author: string): Promise<{ key: string; value: string; + absoluteUrl: string }> { const tags = await ExifReader.load(blob) @@ -152,6 +153,7 @@ export class PanoramaxUploader implements ImageUploader { return { key: "panoramax", value: img.id, + absoluteUrl: img.assets.hd.href } } From ba857d945bfea8b35ea4bd6590c035ea7b6e97a5 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:21 +0000 Subject: [PATCH 09/36] Translated using Weblate (Catalan) Currently translated at 73.7% (503 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/ca/ --- langs/ca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/ca.json b/langs/ca.json index e4d21759b..b56265de1 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -699,4 +699,4 @@ "description": "Un identificador de Wikidata" } } -} \ No newline at end of file +} From 23c7f2850df9602526398bb62d9c34384cb29e62 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:22 +0000 Subject: [PATCH 10/36] Translated using Weblate (German) Currently translated at 99.8% (681 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/de/ --- langs/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/de.json b/langs/de.json index 0c730c525..a6d0c7bec 100644 --- a/langs/de.json +++ b/langs/de.json @@ -873,4 +873,4 @@ "startsWithQ": "Ein Wikidata-Identifikator beginnt mit Q und wird von einer Zahl gefolgt" } } -} \ No newline at end of file +} From d22d4b284295dc89948787c4bf956e455f03fccd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:23 +0000 Subject: [PATCH 11/36] Translated using Weblate (Spanish) Currently translated at 64.9% (443 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/es/ --- langs/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/es.json b/langs/es.json index 07f6f6e7d..e615ea35d 100644 --- a/langs/es.json +++ b/langs/es.json @@ -593,4 +593,4 @@ "description": "Un identificador de Wikidata" } } -} \ No newline at end of file +} From cf7ff5a6ac8946fd03812573a6623fefc2f84fbd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:23 +0000 Subject: [PATCH 12/36] Translated using Weblate (French) Currently translated at 53.2% (363 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/fr/ --- langs/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/fr.json b/langs/fr.json index e3d6e3b19..c041a4d7e 100644 --- a/langs/fr.json +++ b/langs/fr.json @@ -551,4 +551,4 @@ "feedback": "Ceci n'est pas une adresse web valide" } } -} \ No newline at end of file +} From 0e7686eb22695374eabbb0541dce053b4da9c3be Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:24 +0000 Subject: [PATCH 13/36] Translated using Weblate (Galician) Currently translated at 13.0% (89 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/gl/ --- langs/gl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/gl.json b/langs/gl.json index cdcb7bd7d..45f11dbcb 100644 --- a/langs/gl.json +++ b/langs/gl.json @@ -146,4 +146,4 @@ "title_singular": "Unha recensión", "write_a_comment": "Deixa unha recensión…" } -} \ No newline at end of file +} From afeacca92c72587ff3a51747cbd2a5375c744313 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:26 +0000 Subject: [PATCH 14/36] Translated using Weblate (Dutch) Currently translated at 77.1% (526 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/nl/ --- langs/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/nl.json b/langs/nl.json index 58681dd2b..e3c9b8329 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -716,4 +716,4 @@ "description": "Een Wikidata-code" } } -} \ No newline at end of file +} From 12e06c9efc627192edc4b2a354d62aa7213c71d3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:26 +0000 Subject: [PATCH 15/36] Translated using Weblate (Polish) Currently translated at 70.8% (483 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/pl/ --- langs/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/pl.json b/langs/pl.json index eaabe52bf..43f0c9033 100644 --- a/langs/pl.json +++ b/langs/pl.json @@ -668,4 +668,4 @@ "description": "Identyfikator Wikidanych" } } -} \ No newline at end of file +} From c69c824cadb7a32308b0fcc1f169f71f5731d1c3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:25 +0000 Subject: [PATCH 16/36] Translated using Weblate (Italian) Currently translated at 55.7% (380 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/it/ --- langs/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/it.json b/langs/it.json index fecd93e44..4560f7eb0 100644 --- a/langs/it.json +++ b/langs/it.json @@ -531,4 +531,4 @@ "feedback": "Questo non è un numero di telefono valido" } } -} \ No newline at end of file +} From 677f3300fd9a44184dcb3702a9db720006a0479c Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:24 +0000 Subject: [PATCH 17/36] Translated using Weblate (Hungarian) Currently translated at 32.2% (220 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/hu/ --- langs/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/hu.json b/langs/hu.json index 94287046c..e6a6a2cc7 100644 --- a/langs/hu.json +++ b/langs/hu.json @@ -312,4 +312,4 @@ "split": "Szétvágás", "splitTitle": "Válaszd ki a térképen, hogy az út hol legyen elvágva" } -} \ No newline at end of file +} From d81f8f60d3413da487c51972cf262cccf7c53046 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:27 +0000 Subject: [PATCH 18/36] Translated using Weblate (Russian) Currently translated at 17.5% (120 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/ru/ --- langs/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/ru.json b/langs/ru.json index 5084c1959..1772a6f68 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -202,4 +202,4 @@ "description": "Идентификатор Wikidata" } } -} \ No newline at end of file +} From 28ac568ba337ce2dbfe6d117ce1d4d76d12c95d5 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:27 +0000 Subject: [PATCH 19/36] Translated using Weblate (Portuguese (Brazil)) Currently translated at 14.5% (99 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/pt_BR/ --- langs/pt_BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/pt_BR.json b/langs/pt_BR.json index d847a272c..40e3e369b 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -170,4 +170,4 @@ "tos": "Se você criar um comentário, você concorda com o TOS e a política de privacidade de Mangrove.reviews ", "write_a_comment": "Deixe um comentário…" } -} \ No newline at end of file +} From c1e8f2323c91232a6a3deba733ea259f2f259be8 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:28 +0000 Subject: [PATCH 20/36] Translated using Weblate (Swedish) Currently translated at 6.0% (41 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/sv/ --- langs/sv.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/sv.json b/langs/sv.json index 389c625bc..fd2298aae 100644 --- a/langs/sv.json +++ b/langs/sv.json @@ -66,4 +66,4 @@ "cancel": "Avbryt", "split": "Dela" } -} \ No newline at end of file +} From 9c3a8d9472911ef81939168476d48df24679c02d Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:25 +0000 Subject: [PATCH 21/36] Translated using Weblate (Japanese) Currently translated at 13.1% (90 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/ja/ --- langs/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/ja.json b/langs/ja.json index 0823819fe..c3a8dad50 100644 --- a/langs/ja.json +++ b/langs/ja.json @@ -149,4 +149,4 @@ "tos": "レビューを作成する場合は、Mangrove.reviewsのTOSおよびプライバシーポリシーに同意します。", "write_a_comment": "レビューを残す…" } -} \ No newline at end of file +} From c14fc1bf9c91794ee5d7fea139ee95209923abe3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:28 +0000 Subject: [PATCH 22/36] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 96.1% (656 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/zh_Hant/ --- langs/zh_Hant.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index 2027183fc..c8a747652 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -848,4 +848,4 @@ "startsWithQ": "維基數據編號以 Q 開頭後面接數字" } } -} \ No newline at end of file +} From c1ca7fc1b13ac5db0327727b19db795e757aa8d3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:26 +0000 Subject: [PATCH 23/36] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 41.6% (284 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/nb_NO/ --- langs/nb_NO.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 1b8b9c914..4c85f6505 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -488,4 +488,4 @@ "description": "En Wikidata-identifikator" } } -} \ No newline at end of file +} From 09681dad184ef53026c3799932d4ba01d95e3d43 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:23 +0000 Subject: [PATCH 24/36] Translated using Weblate (Finnish) Currently translated at 77.2% (527 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/fi/ --- langs/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/fi.json b/langs/fi.json index ad4194fb5..c7f3b4a7b 100644 --- a/langs/fi.json +++ b/langs/fi.json @@ -707,4 +707,4 @@ "description": "Wikidata-tunniste" } } -} \ No newline at end of file +} From d8c18f449ecfb733a948fdebea71e8a437548ebc Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:27 +0000 Subject: [PATCH 25/36] Translated using Weblate (Portuguese) Currently translated at 91.6% (625 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/pt/ --- langs/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/pt.json b/langs/pt.json index 01d2beba7..6572135ae 100644 --- a/langs/pt.json +++ b/langs/pt.json @@ -830,4 +830,4 @@ "startsWithQ": "Um identificador wikidata começa por Q e é seguido de um número" } } -} \ No newline at end of file +} From df38ec40f2468183f02622fae76f30efde8fb40a Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:22 +0000 Subject: [PATCH 26/36] Translated using Weblate (Czech) Currently translated at 97.9% (668 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/cs/ --- langs/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/cs.json b/langs/cs.json index 121337b98..e5294a30c 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -859,4 +859,4 @@ "startsWithQ": "Identifikátor wikidat začíná písmenem Q a následuje za ním číslo" } } -} \ No newline at end of file +} From e9623aa01ce8828a96ef43d7b7b48b03f0ed6085 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:28 +0000 Subject: [PATCH 27/36] Translated using Weblate (Slovenian) Currently translated at 14.3% (98 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/sl/ --- langs/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/sl.json b/langs/sl.json index dff241b67..1cdbd23fb 100644 --- a/langs/sl.json +++ b/langs/sl.json @@ -149,4 +149,4 @@ "partOfRelation": "Ta element je del relacije. Premaknete ga lahko z drugim urejevalnikom.", "whyMove": "Zakaj želite premakniti ta element?" } -} \ No newline at end of file +} From 01499060cd635d32f56d9351f81e6826aa465898 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 26 Sep 2024 17:17:22 +0000 Subject: [PATCH 28/36] Translated using Weblate (Danish) Currently translated at 55.4% (378 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/da/ --- langs/da.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/da.json b/langs/da.json index a5e17c221..6f144dfc9 100644 --- a/langs/da.json +++ b/langs/da.json @@ -588,4 +588,4 @@ "description": "En Wikidata identifier" } } -} \ No newline at end of file +} From 594b020a6e901a15b6ae3b56bc4b7b89942a9b46 Mon Sep 17 00:00:00 2001 From: kjon Date: Thu, 26 Sep 2024 18:59:50 +0000 Subject: [PATCH 29/36] Translated using Weblate (English) Currently translated at 100.0% (682 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/en/ --- langs/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/en.json b/langs/en.json index 4a9478bca..ca3fd035e 100644 --- a/langs/en.json +++ b/langs/en.json @@ -577,7 +577,7 @@ "title": "Nearby streetview imagery" }, "pleaseLogin": "Please log in to add a picture", - "respectPrivacy": "Do not upload Google Maps, Google Streetview or other copyrighted sources.", + "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", From 93fe9c907bd96db5e64ce3b327a79520a5cd3f2d Mon Sep 17 00:00:00 2001 From: kjon Date: Thu, 26 Sep 2024 18:59:31 +0000 Subject: [PATCH 30/36] Translated using Weblate (German) Currently translated at 100.0% (682 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/de/ --- langs/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/de.json b/langs/de.json index a6d0c7bec..05c5e9115 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", From afdca97c97b1b0d231f89d4213e56fd9ec34c07f Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 26 Sep 2024 18:54:44 +0000 Subject: [PATCH 31/36] Translated using Weblate (Spanish) Currently translated at 65.1% (444 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/es/ --- langs/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/es.json b/langs/es.json index e615ea35d..612c3daff 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 de 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.", From 9d3fd0c8e40d712825904cafa5af7e569a833494 Mon Sep 17 00:00:00 2001 From: kjon Date: Thu, 26 Sep 2024 19:01:22 +0000 Subject: [PATCH 32/36] Translated using Weblate (German) Currently translated at 100.0% (682 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/de/ --- langs/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/de.json b/langs/de.json index 05c5e9115..b6adaf12c 100644 --- a/langs/de.json +++ b/langs/de.json @@ -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:", From 4c153f10f6cfa68cb3204304a280d65584fc8522 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 26 Sep 2024 19:02:32 +0000 Subject: [PATCH 33/36] Translated using Weblate (Spanish) Currently translated at 65.1% (444 of 682 strings) Translation: MapComplete/Core Translate-URL: https://hosted.weblate.org/projects/mapcomplete/core/es/ --- langs/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langs/es.json b/langs/es.json index 612c3daff..4b3802d5d 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 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.", From a7939da98f6cd7c24dbb3b998aeea492f74bc8e1 Mon Sep 17 00:00:00 2001 From: kjon Date: Wed, 25 Sep 2024 19:53:21 +0000 Subject: [PATCH 34/36] Translated using Weblate (German) Currently translated at 100.0% (3782 of 3782 strings) Translation: MapComplete/Layer translations Translate-URL: https://hosted.weblate.org/projects/mapcomplete/layers/de/ --- langs/layers/de.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/langs/layers/de.json b/langs/layers/de.json index 512912d02..219051d2e 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" }, @@ -12358,4 +12372,4 @@ "render": "Windrad" } } -} \ No newline at end of file +} From 4650170db4bdd3b9e4fbd1900147a7433652dd6f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 28 Sep 2024 02:04:14 +0200 Subject: [PATCH 35/36] Feat: check if the image was blurred, attempt to reload if it is done; refactoring of ImageProvider code --- langs/en.json | 3 +- package-lock.json | 14 +-- package.json | 2 +- src/Logic/ImageProviders/AllImageProviders.ts | 40 ++----- .../ImageProviders/GenericImageProvider.ts | 20 ++-- src/Logic/ImageProviders/ImageProvider.ts | 75 ++++++------- src/Logic/ImageProviders/Imgur.ts | 8 +- src/Logic/ImageProviders/Mapillary.ts | 5 +- src/Logic/ImageProviders/Panoramax.ts | 103 ++++++++++++------ .../ImageProviders/WikidataImageProvider.ts | 24 ++-- .../ImageProviders/WikimediaImageProvider.ts | 21 ++-- src/UI/Image/AttributedImage.svelte | 70 +++++++----- src/UI/Image/ImageCarousel.ts | 6 +- src/Utils.ts | 23 ++++ 14 files changed, 224 insertions(+), 190 deletions(-) diff --git a/langs/en.json b/langs/en.json index ca3fd035e..e7e4d1cc6 100644 --- a/langs/en.json +++ b/langs/en.json @@ -577,6 +577,7 @@ "title": "Nearby streetview imagery" }, "pleaseLogin": "Please log in to add a picture", + "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": { @@ -873,4 +874,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 0328e33b8..b98b93d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "opening_hours": "^3.6.0", "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", - "panoramax-js": "^0.1.1", + "panoramax-js": "^0.1.4", "panzoom": "^9.4.3", "papaparse": "^5.3.1", "pbf": "^3.2.1", @@ -15994,9 +15994,9 @@ "license": "MIT" }, "node_modules/panoramax-js": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", - "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", + "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", @@ -32056,9 +32056,9 @@ "version": "1.0.0" }, "panoramax-js": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", - "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", + "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", diff --git a/package.json b/package.json index 2b4996265..457371e9e 100644 --- a/package.json +++ b/package.json @@ -205,7 +205,7 @@ "opening_hours": "^3.6.0", "osm-auth": "^2.5.0", "osmtogeojson": "^3.0.0-beta.5", - "panoramax-js": "^0.1.1", + "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 476571c1e..8dabd4097 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -6,6 +6,7 @@ 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 @@ -45,10 +46,6 @@ export default class AllImageProviders { wikimedia: WikimediaImageProvider.singleton, panoramax: Panoramax.singleton } - private static _cache: Map> = new Map< - string, - UIEventSource - >() public static byName(name: string) { return AllImageProviders.providersByName[name.toLowerCase()] @@ -76,42 +73,25 @@ export default class AllImageProviders { 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) { - - - const singleSource = imageProvider.GetRelevantUrls(tags, { - /* - By default, 'GetRelevantUrls' uses the defaultKeyPrefixes. - However, we override them if a custom image tag is set, e.g. 'image:menu' - */ - prefixes: tagKey ?? imageProvider.defaultKeyPrefixes, - }) + /* + 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 999516ee1..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,59 +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 = Utils.Dedup(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(key === "panoramax"){ - console.log("Inspecting", key,"against", prefixes) - } - if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) { + + 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/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index f5b2c06b1..80acda10a 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -24,18 +24,18 @@ export class Imgur extends ImageProvider { 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/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 index 6f8291d0f..eb73b9636 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -1,35 +1,31 @@ import { ImageUploader } from "./ImageUploader" -import { AuthorizedPanoramax } from "panoramax-js/dist" +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 { Feature, FeatureCollection, Point } from "geojson" import { GeoOperations } from "../GeoOperations" +import Constants from "../../Models/Constants" +import { Store, Stores, UIEventSource } from "../UIEventSource" -type ImageData = Feature & { - id: string, - assets: { hd: { href: string }, sd: { href: string } }, - providers: {name: string}[] -} 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 = {} + private static knownMeta: Record = {} public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { return undefined } - public addKnownMeta(meta: ImageData){ - console.log("Adding known meta for", meta.id) - PanoramaxImageProvider.knownMeta[meta.id] = meta + public addKnownMeta(meta: ImageData) { + PanoramaxImageProvider.knownMeta[meta.id] = { data: meta, time: new Date() } } /** @@ -39,16 +35,14 @@ export default class PanoramaxImageProvider extends ImageProvider { */ 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/api/collections/${sequence}/items/${id}` - const data = await Utils.downloadJsonCached(url, 60 * 60 * 1000) - return {url, data} + 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 url = "https://api.panoramax.xyz/api/search?limit=1&ids=" + imageId - const metaAll = await Utils.downloadJsonCached>(url, 1000 * 60 * 60) - const data= metaAll.features[0] - return {data, url} + const data = await PanoramaxImageProvider.xyz.imageInfo(imageId) + return { data, url: "https://api.panoramax.xyz/" } } @@ -57,17 +51,18 @@ export default class PanoramaxImageProvider extends ImageProvider { * @param meta * @private */ - private featureToImage(info: {data: ImageData, url: string}) { - const meta = info.data - const url = info.url + private featureToImage(info: { data: ImageData, url: string }) { + const meta = info?.data if (!meta) { return undefined } - function makeAbsolute(s: string){ - if(!s.startsWith("https://") && !s.startsWith("http://")){ - const parsed = new URL(url) - return parsed.protocol+"//"+parsed.host+s + 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 } @@ -80,27 +75,64 @@ export default class PanoramaxImageProvider extends ImageProvider { 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 }> { - const cached= PanoramaxImageProvider.knownMeta[id] - console.log("Cached version", id, cached) - if(cached){ - return {data: cached, url: undefined} + 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) { - return await this.getInfoFromXYZ(id) + 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 [this.getInfoFor(value).then(r => this.featureToImage(r))] + 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 { @@ -139,7 +171,7 @@ export class PanoramaxUploader implements ImageUploader { const p = this._panoramax const defaultSequence = (await p.mySequences())[0] - const img = await p.addImage(blob, defaultSequence, { + const img = await p.addImage(blob, defaultSequence, { lat: !hasGPS ? lat : undefined, lon: !hasGPS ? lon : undefined, datetime: !hasDate ? new Date().toISOString() : undefined, @@ -149,11 +181,10 @@ export class PanoramaxUploader implements ImageUploader { }) PanoramaxImageProvider.singleton.addKnownMeta(img) - await Utils.waitFor(1250) return { key: "panoramax", value: img.id, - absoluteUrl: img.assets.hd.href + 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/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)}}> -
-
highlight()} - on:mouseleave={() => highlight(false)} - > - (loaded = true)} - class={imgClass ?? ""} - class:cursor-zoom-in={canZoom} - on:click={() => { +{#if image.status !== undefined && image.status !== "ready"} +
+ + + +
+{:else} +
+
highlight()} + on:mouseleave={() => highlight(false)} + > + + (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} -
previewedImage.set(image)}> - -
- {/if} + {#if canZoom && loaded} +
previewedImage.set(image)}> + +
+ {/if} +
+
+ +
-
- -
-
+{/if} diff --git a/src/UI/Image/ImageCarousel.ts b/src/UI/Image/ImageCarousel.ts index 8c6d1e5b9..3b96b5bb6 100644 --- a/src/UI/Image/ImageCarousel.ts +++ b/src/UI/Image/ImageCarousel.ts @@ -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) diff --git a/src/Utils.ts b/src/Utils.ts index 2a23d76fd..b2b7f879f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -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(arr: T[], toKey: ((t:T) => string) ): T[]{ + const uniq: T[] = [] + const seen = new Set() + 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 * From 4395e883903345a2fd20e916e58a8e92914a4996 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 28 Sep 2024 02:15:46 +0200 Subject: [PATCH 36/36] Chore: translation sync --- assets/layers/advertising/advertising.json | 12 ++++++++---- langs/ca.json | 2 +- langs/cs.json | 2 +- langs/da.json | 2 +- langs/de.json | 2 +- langs/en.json | 2 +- langs/es.json | 2 +- langs/fi.json | 2 +- langs/fr.json | 2 +- langs/gl.json | 2 +- langs/hu.json | 2 +- langs/it.json | 2 +- langs/ja.json | 2 +- langs/layers/de.json | 2 +- langs/nb_NO.json | 2 +- langs/nl.json | 2 +- langs/pl.json | 2 +- langs/pt.json | 2 +- langs/pt_BR.json | 2 +- langs/ru.json | 2 +- langs/sl.json | 2 +- langs/sv.json | 2 +- langs/zh_Hant.json | 2 +- 23 files changed, 30 insertions(+), 26 deletions(-) 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/ca.json b/langs/ca.json index b56265de1..e4d21759b 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -699,4 +699,4 @@ "description": "Un identificador de Wikidata" } } -} +} \ No newline at end of file diff --git a/langs/cs.json b/langs/cs.json index e5294a30c..121337b98 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -859,4 +859,4 @@ "startsWithQ": "Identifikátor wikidat začíná písmenem Q a následuje za ním číslo" } } -} +} \ No newline at end of file diff --git a/langs/da.json b/langs/da.json index 6f144dfc9..a5e17c221 100644 --- a/langs/da.json +++ b/langs/da.json @@ -588,4 +588,4 @@ "description": "En Wikidata identifier" } } -} +} \ No newline at end of file diff --git a/langs/de.json b/langs/de.json index b6adaf12c..3ec9ccae4 100644 --- a/langs/de.json +++ b/langs/de.json @@ -873,4 +873,4 @@ "startsWithQ": "Ein Wikidata-Identifikator beginnt mit Q und wird von einer Zahl gefolgt" } } -} +} \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index e7e4d1cc6..300dca8a2 100644 --- a/langs/en.json +++ b/langs/en.json @@ -874,4 +874,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} +} \ No newline at end of file diff --git a/langs/es.json b/langs/es.json index 4b3802d5d..894a9d0b8 100644 --- a/langs/es.json +++ b/langs/es.json @@ -593,4 +593,4 @@ "description": "Un identificador de Wikidata" } } -} +} \ No newline at end of file diff --git a/langs/fi.json b/langs/fi.json index c7f3b4a7b..ad4194fb5 100644 --- a/langs/fi.json +++ b/langs/fi.json @@ -707,4 +707,4 @@ "description": "Wikidata-tunniste" } } -} +} \ No newline at end of file diff --git a/langs/fr.json b/langs/fr.json index c041a4d7e..e3d6e3b19 100644 --- a/langs/fr.json +++ b/langs/fr.json @@ -551,4 +551,4 @@ "feedback": "Ceci n'est pas une adresse web valide" } } -} +} \ No newline at end of file diff --git a/langs/gl.json b/langs/gl.json index 45f11dbcb..cdcb7bd7d 100644 --- a/langs/gl.json +++ b/langs/gl.json @@ -146,4 +146,4 @@ "title_singular": "Unha recensión", "write_a_comment": "Deixa unha recensión…" } -} +} \ No newline at end of file diff --git a/langs/hu.json b/langs/hu.json index e6a6a2cc7..94287046c 100644 --- a/langs/hu.json +++ b/langs/hu.json @@ -312,4 +312,4 @@ "split": "Szétvágás", "splitTitle": "Válaszd ki a térképen, hogy az út hol legyen elvágva" } -} +} \ No newline at end of file diff --git a/langs/it.json b/langs/it.json index 4560f7eb0..fecd93e44 100644 --- a/langs/it.json +++ b/langs/it.json @@ -531,4 +531,4 @@ "feedback": "Questo non è un numero di telefono valido" } } -} +} \ No newline at end of file diff --git a/langs/ja.json b/langs/ja.json index c3a8dad50..0823819fe 100644 --- a/langs/ja.json +++ b/langs/ja.json @@ -149,4 +149,4 @@ "tos": "レビューを作成する場合は、Mangrove.reviewsのTOSおよびプライバシーポリシーに同意します。", "write_a_comment": "レビューを残す…" } -} +} \ No newline at end of file diff --git a/langs/layers/de.json b/langs/layers/de.json index 219051d2e..d27197073 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -12372,4 +12372,4 @@ "render": "Windrad" } } -} +} \ No newline at end of file diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 4c85f6505..1b8b9c914 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -488,4 +488,4 @@ "description": "En Wikidata-identifikator" } } -} +} \ No newline at end of file diff --git a/langs/nl.json b/langs/nl.json index e3c9b8329..58681dd2b 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -716,4 +716,4 @@ "description": "Een Wikidata-code" } } -} +} \ No newline at end of file diff --git a/langs/pl.json b/langs/pl.json index 43f0c9033..eaabe52bf 100644 --- a/langs/pl.json +++ b/langs/pl.json @@ -668,4 +668,4 @@ "description": "Identyfikator Wikidanych" } } -} +} \ No newline at end of file diff --git a/langs/pt.json b/langs/pt.json index 6572135ae..01d2beba7 100644 --- a/langs/pt.json +++ b/langs/pt.json @@ -830,4 +830,4 @@ "startsWithQ": "Um identificador wikidata começa por Q e é seguido de um número" } } -} +} \ No newline at end of file diff --git a/langs/pt_BR.json b/langs/pt_BR.json index 40e3e369b..d847a272c 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -170,4 +170,4 @@ "tos": "Se você criar um comentário, você concorda com o TOS e a política de privacidade de Mangrove.reviews ", "write_a_comment": "Deixe um comentário…" } -} +} \ No newline at end of file diff --git a/langs/ru.json b/langs/ru.json index 1772a6f68..5084c1959 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -202,4 +202,4 @@ "description": "Идентификатор Wikidata" } } -} +} \ No newline at end of file diff --git a/langs/sl.json b/langs/sl.json index 1cdbd23fb..dff241b67 100644 --- a/langs/sl.json +++ b/langs/sl.json @@ -149,4 +149,4 @@ "partOfRelation": "Ta element je del relacije. Premaknete ga lahko z drugim urejevalnikom.", "whyMove": "Zakaj želite premakniti ta element?" } -} +} \ No newline at end of file diff --git a/langs/sv.json b/langs/sv.json index fd2298aae..389c625bc 100644 --- a/langs/sv.json +++ b/langs/sv.json @@ -66,4 +66,4 @@ "cancel": "Avbryt", "split": "Dela" } -} +} \ No newline at end of file diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index c8a747652..2027183fc 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -848,4 +848,4 @@ "startsWithQ": "維基數據編號以 Q 開頭後面接數字" } } -} +} \ No newline at end of file