diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index fbfa9bfd97..2be4b4763a 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 e4e1cbda72..1c68d81949 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 be13f6370c..2e4e434890 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 0bdfe22db0..f1c23f80cf 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 05ad002e25..4304c0bd90 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 a89ee86005..7fe3b7b02e 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 4b88623b78..798008f199 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 40601892be..fbfb56abcd 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 dbc2ff44c2..f5b2c06b12 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 99998e75d2..da82a9b37b 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 0000000000..67e82e0a99 --- /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 6c6ffc3dd6..48d67b845e 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 203168d000..611207a47a 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 1ab6694705..bb03af6b3e 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 20190e43c8..96d1ffa9f9 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 1e27300144..b052e56aee 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 43dc42fec7..f07cd5f382 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 8d58c14fd2..37aa7229e5 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 5783ba2ca7..cc5a886914 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], - })*/ + }) }, }, {