forked from MapComplete/MapComplete
		
	Feat: check if the image was blurred, attempt to reload if it is done; refactoring of ImageProvider code
This commit is contained in:
		
							parent
							
								
									590591dd31
								
							
						
					
					
						commit
						4650170db4
					
				
					 14 changed files with 224 additions and 190 deletions
				
			
		|  | @ -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": { | ||||
|  |  | |||
							
								
								
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -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", | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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<string, UIEventSource<ProvidedImage[]>> = new Map< | ||||
|         string, | ||||
|         UIEventSource<ProvidedImage[]> | ||||
|     >() | ||||
| 
 | ||||
|     public static byName(name: string) { | ||||
|         return AllImageProviders.providersByName[name.toLowerCase()] | ||||
|  | @ -76,42 +73,25 @@ export default class AllImageProviders { | |||
|         tags: Store<Record<string, string>>, | ||||
|         tagKey?: string[] | ||||
|     ): Store<ProvidedImage[]> { | ||||
|         if (tags.data.id === undefined) { | ||||
|         if (tags?.data?.id === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const cacheKey = tags.data.id + tagKey | ||||
|         const cached = this._cache.get(cacheKey) | ||||
|         if (cached !== undefined) { | ||||
|             return cached | ||||
|         } | ||||
| 
 | ||||
|         const source = new UIEventSource([]) | ||||
|         this._cache.set(cacheKey, source) | ||||
|         const allSources: Store<ProvidedImage[]>[] = [] | ||||
|         for (const imageProvider of AllImageProviders.ImageAttributionSource) { | ||||
| 
 | ||||
| 
 | ||||
|             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<string>() | ||||
|                 for (const img of all) { | ||||
|                     if (seen.has(img.url)) { | ||||
|                         continue | ||||
|                     } | ||||
|                     seen.add(img.url) | ||||
|                     uniq.push(img) | ||||
|                 } | ||||
|                 source.setData(uniq) | ||||
|                 const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url) | ||||
|                 source.set(dedup) | ||||
|             }) | ||||
|         } | ||||
|         return source | ||||
|  |  | |||
|  | @ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider { | |||
|         this._valuePrefixBlacklist = valuePrefixBlacklist | ||||
|     } | ||||
| 
 | ||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|     ExtractUrls(key: string, value: string): undefined | ProvidedImage[] { | ||||
|         if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) { | ||||
|             return [] | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             new URL(value) | ||||
|         } catch (_) { | ||||
|             // Not a valid URL
 | ||||
|             return [] | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         return [ | ||||
|             Promise.resolve({ | ||||
|                 key: key, | ||||
|                 url: value, | ||||
|                 provider: this, | ||||
|                 id: value, | ||||
|             }), | ||||
|         ] | ||||
|         return [{ | ||||
|             key: key, | ||||
|             url: value, | ||||
|             provider: this, | ||||
|             id: value, | ||||
|         }] | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon() { | ||||
|  |  | |||
|  | @ -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<any>, | ||||
|         options?: { | ||||
|             prefixes?: string[] | ||||
|         } | ||||
|     ): UIEventSource<ProvidedImage[]> { | ||||
|         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<string, string>, prefixes: string[]): Promise<ProvidedImage[]> { | ||||
|         const relevantUrls: ProvidedImage[] = [] | ||||
|         const seenValues = new Set<string>() | ||||
|         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<Promise<ProvidedImage>[]> | ||||
|     public getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> { | ||||
|         return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes)) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise<ProvidedImage[]> | ||||
| 
 | ||||
|     public abstract DownloadAttribution(providedImage: { | ||||
|         url: string | ||||
|  |  | |||
|  | @ -24,18 +24,18 @@ export class Imgur extends ImageProvider { | |||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|     public ExtractUrls(key: string, value: string): undefined | ProvidedImage[] { | ||||
|         if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) { | ||||
|             return [ | ||||
|                 Promise.resolve({ | ||||
|                 { | ||||
|                     url: value, | ||||
|                     key: key, | ||||
|                     provider: this, | ||||
|                     id: value, | ||||
|                 }), | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|         return [] | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -131,8 +131,9 @@ export class Mapillary extends ImageProvider { | |||
|         return new SvelteUIElement(MapillaryIcon, { url }) | ||||
|     } | ||||
| 
 | ||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|         return [this.PrepareUrlAsync(key, value)] | ||||
|     async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> { | ||||
|         const img = await this.PrepareUrlAsync(key, value) | ||||
|         return [img] | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> { | ||||
|  |  | |||
|  | @ -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<Point, { "geovisio:producer": string, "geovisio:license": string, "datetime": string }> & { | ||||
|     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<string, ImageData> = {} | ||||
|     private static knownMeta: Record<string, { data: ImageData, time: Date }> = {} | ||||
| 
 | ||||
|     public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     public addKnownMeta(meta: ImageData){ | ||||
|         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 = <any> 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<FeatureCollection<Point>>(url, 1000 * 60 * 60) | ||||
|         const data= <any>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<Promise<ProvidedImage>[]> { | ||||
|         return [this.getInfoFor(value).then(r => this.featureToImage(<any>r))] | ||||
|     public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> { | ||||
|         return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))] | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> { | ||||
|         const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes)) | ||||
| 
 | ||||
|         function hasLoading(data: ProvidedImage[]) { | ||||
|             if(data === undefined){ | ||||
|                 return true | ||||
|             } | ||||
|             return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken") | ||||
|         } | ||||
| 
 | ||||
|         Stores.Chronic(1500, () => | ||||
|             hasLoading(source.data), | ||||
|         ).addCallback(_ => { | ||||
|             console.log("UPdating... ") | ||||
|             super.getRelevantUrlsFor(tags, prefixes).then(data => { | ||||
|                 console.log("New panoramax data is", data, hasLoading(data)) | ||||
|                 source.set(data) | ||||
|                 return !hasLoading(data) | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         return source | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> { | ||||
|  | @ -139,7 +171,7 @@ export class PanoramaxUploader implements ImageUploader { | |||
| 
 | ||||
|         const p = this._panoramax | ||||
|         const defaultSequence = (await p.mySequences())[0] | ||||
|         const img = <ImageData> await p.addImage(blob, defaultSequence, { | ||||
|         const img = <ImageData>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, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import Wikidata from "../Web/Wikidata" | |||
| import SvelteUIElement from "../../UI/Base/SvelteUIElement" | ||||
| import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte" | ||||
| import { Utils } from "../../Utils" | ||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| 
 | ||||
| export class WikidataImageProvider extends ImageProvider { | ||||
|     public static readonly singleton = new WikidataImageProvider() | ||||
|  | @ -25,28 +26,28 @@ export class WikidataImageProvider extends ImageProvider { | |||
|         return new SvelteUIElement(Wikidata_icon) | ||||
|     } | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|     public async ExtractUrls(key: string, value: string):  Promise<ProvidedImage[]> { | ||||
|         if (WikidataImageProvider.keyBlacklist.has(key)) { | ||||
|             return [] | ||||
|             return undefined | ||||
|         } | ||||
|         const entity = await Wikidata.LoadWikidataEntryAsync(value) | ||||
|         if (entity === undefined) { | ||||
|             return [] | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const allImages: Promise<ProvidedImage>[] = [] | ||||
|         const allImages: Promise<ProvidedImage[]>[] = [] | ||||
|         // P18 is the claim 'depicted in this image'
 | ||||
|         for (const img of Array.from(entity.claims.get("P18") ?? [])) { | ||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img) | ||||
|             allImages.push(...promises) | ||||
|             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, img) | ||||
|             allImages.push(promises) | ||||
|         } | ||||
|         // P373 is 'commons category'
 | ||||
|         for (let cat of Array.from(entity.claims.get("P373") ?? [])) { | ||||
|             if (!cat.startsWith("Category:")) { | ||||
|                 cat = "Category:" + cat | ||||
|             } | ||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat) | ||||
|             allImages.push(...promises) | ||||
|             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, cat) | ||||
|             allImages.push(promises) | ||||
|         } | ||||
| 
 | ||||
|         const commons = entity.commons | ||||
|  | @ -54,10 +55,11 @@ export class WikidataImageProvider extends ImageProvider { | |||
|             commons !== undefined && | ||||
|             (commons.startsWith("Category:") || commons.startsWith("File:")) | ||||
|         ) { | ||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) | ||||
|             allImages.push(...promises) | ||||
|             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) | ||||
|             allImages.push(promises) | ||||
|         } | ||||
|         return allImages | ||||
|         const resolved = await Promise.all(Utils.NoNull(allImages)) | ||||
|         return [].concat(...resolved) | ||||
|     } | ||||
| 
 | ||||
|     public DownloadAttribution(_): Promise<any> { | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|             return value | ||||
|         } | ||||
|         const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent( | ||||
|             value | ||||
|             value, | ||||
|         )}` | ||||
|         if (useHd) { | ||||
|             return baseUrl | ||||
|  | @ -97,28 +97,27 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|         return this.UrlForImage("File:" + value) | ||||
|     } | ||||
| 
 | ||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { | ||||
|     public async ExtractUrls(key: string, value: string): undefined | Promise<ProvidedImage[]> { | ||||
|         const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value) | ||||
|         if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) { | ||||
|             return [] | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         value = WikimediaImageProvider.removeCommonsPrefix(value) | ||||
|         if (value.startsWith("Category:")) { | ||||
|             const urls = await Wikimedia.GetCategoryContents(value) | ||||
|             return urls | ||||
|                 .filter((url) => url.startsWith("File:")) | ||||
|                 .map((image) => Promise.resolve(this.UrlForImage(image))) | ||||
|             return urls.filter((url) => url.startsWith("File:")) | ||||
|                 .map((image) => this.UrlForImage(image)) | ||||
|         } | ||||
|         if (value.startsWith("File:")) { | ||||
|             return [Promise.resolve(this.UrlForImage(value))] | ||||
|             return [this.UrlForImage(value)] | ||||
|         } | ||||
|         if (value.startsWith("http")) { | ||||
|             // PRobably an error
 | ||||
|             return [] | ||||
|             // Probably an error
 | ||||
|             return undefined | ||||
|         } | ||||
|         // We do a last effort and assume this is a file
 | ||||
|         return [Promise.resolve(this.UrlForImage("File:" + value))] | ||||
|         return [(this.UrlForImage("File:" + value))] | ||||
|     } | ||||
| 
 | ||||
|     public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> { | ||||
|  | @ -148,7 +147,7 @@ export class WikimediaImageProvider extends ImageProvider { | |||
|             console.warn( | ||||
|                 "The file", | ||||
|                 filename, | ||||
|                 "has no usable metedata or license attached... Please fix the license info file yourself!" | ||||
|                 "has no usable metedata or license attached... Please fix the license info file yourself!", | ||||
|             ) | ||||
|             return undefined | ||||
|         } | ||||
|  |  | |||
|  | @ -13,6 +13,9 @@ | |||
|   import { onDestroy } from "svelte" | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import type { Feature, Point } from "geojson" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
| 
 | ||||
|   export let image: Partial<ProvidedImage> | ||||
|   let fallbackImage: string = undefined | ||||
|  | @ -30,7 +33,7 @@ | |||
|   let showBigPreview = new UIEventSource(false) | ||||
|   onDestroy(showBigPreview.addCallbackAndRun(shown => { | ||||
|     if (!shown) { | ||||
|       previewedImage.set(false) | ||||
|       previewedImage.set(undefined) | ||||
|     } | ||||
|   })) | ||||
|   onDestroy(previewedImage.addCallbackAndRun(previewedImage => { | ||||
|  | @ -49,12 +52,12 @@ | |||
|       type: "Feature", | ||||
|       properties: { | ||||
|         id: image.id, | ||||
|         rotation: image.rotation | ||||
|         rotation: image.rotation, | ||||
|       }, | ||||
|       geometry: { | ||||
|         type: "Point", | ||||
|         coordinates: [image.lon, image.lat] | ||||
|       } | ||||
|         coordinates: [image.lon, image.lat], | ||||
|       }, | ||||
|     } | ||||
|     console.log(f) | ||||
|     state?.geocodedImages.set([f]) | ||||
|  | @ -73,36 +76,45 @@ | |||
|                  on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton> | ||||
|   </div> | ||||
| </Popup> | ||||
| <div class="relative shrink-0"> | ||||
|   <div class="relative w-fit" | ||||
|        on:mouseenter={() => highlight()} | ||||
|        on:mouseleave={() => highlight(false)} | ||||
|   > | ||||
|     <img | ||||
|       bind:this={imgEl} | ||||
|       on:load={() => (loaded = true)} | ||||
|       class={imgClass ?? ""} | ||||
|       class:cursor-zoom-in={canZoom} | ||||
|       on:click={() => { | ||||
| {#if image.status !== undefined && image.status !== "ready"} | ||||
|   <div class="h-full flex flex-col justify-center"> | ||||
|     <Loading> | ||||
|       <Tr t={Translations.t.image.processing}/> | ||||
|     </Loading> | ||||
|   </div> | ||||
| {:else} | ||||
|   <div class="relative shrink-0"> | ||||
|     <div class="relative w-fit" | ||||
|          on:mouseenter={() => highlight()} | ||||
|          on:mouseleave={() => highlight(false)} | ||||
|     > | ||||
| 
 | ||||
|       <img | ||||
|         bind:this={imgEl} | ||||
|         on:load={() => (loaded = true)} | ||||
|         class={imgClass ?? ""} | ||||
|         class:cursor-zoom-in={canZoom} | ||||
|         on:click={() => { | ||||
|         previewedImage?.set(image) | ||||
|     }} | ||||
|       on:error={() => { | ||||
|         on:error={() => { | ||||
|         if (fallbackImage) { | ||||
|           imgEl.src = fallbackImage | ||||
|         } | ||||
|       }} | ||||
|       src={image.url} | ||||
|     /> | ||||
|         src={image.url} | ||||
|       /> | ||||
| 
 | ||||
|     {#if canZoom && loaded} | ||||
|       <div | ||||
|         class="bg-black-transparent absolute right-0 top-0 rounded-bl-full" | ||||
|         on:click={() => previewedImage.set(image)}> | ||||
|         <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> | ||||
|       </div> | ||||
|     {/if} | ||||
|       {#if canZoom && loaded} | ||||
|         <div | ||||
|           class="bg-black-transparent absolute right-0 top-0 rounded-bl-full" | ||||
|           on:click={() => previewedImage.set(image)}> | ||||
|           <MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" /> | ||||
|         </div> | ||||
|       {/if} | ||||
|     </div> | ||||
|     <div class="absolute bottom-0 left-0"> | ||||
|       <ImageAttribution {image} {attributionFormat} /> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="absolute bottom-0 left-0"> | ||||
|     <ImageAttribution {image} {attributionFormat} /> | ||||
|   </div> | ||||
| </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										23
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								src/Utils.ts
									
										
									
									
									
								
							|  | @ -414,6 +414,29 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         return items | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deduplicates the given array based on some ID-properties. | ||||
|      * Removes all falsey values | ||||
|      * @param arr | ||||
|      * @param toKey | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static DedupOnId<T>(arr: T[], toKey: ((t:T) => string) ): T[]{ | ||||
|         const uniq: T[] = [] | ||||
|         const seen = new Set<string>() | ||||
|         for (const img of arr) { | ||||
|             if(!img){ | ||||
|                 continue | ||||
|             } | ||||
|             const k = toKey(img) | ||||
|             if (!seen.has(k)) { | ||||
|                 seen.add(k) | ||||
|                 uniq.push(img) | ||||
|             } | ||||
|         } | ||||
|         return uniq | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finds all duplicates in a list of strings | ||||
|      * | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue