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" |             "title": "Nearby streetview imagery" | ||||||
|         }, |         }, | ||||||
|         "pleaseLogin": "Please log in to add a picture", |         "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.", |         "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}", |         "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", | ||||||
|         "upload": { |         "upload": { | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -63,7 +63,7 @@ | ||||||
|         "opening_hours": "^3.6.0", |         "opening_hours": "^3.6.0", | ||||||
|         "osm-auth": "^2.5.0", |         "osm-auth": "^2.5.0", | ||||||
|         "osmtogeojson": "^3.0.0-beta.5", |         "osmtogeojson": "^3.0.0-beta.5", | ||||||
|         "panoramax-js": "^0.1.1", |         "panoramax-js": "^0.1.4", | ||||||
|         "panzoom": "^9.4.3", |         "panzoom": "^9.4.3", | ||||||
|         "papaparse": "^5.3.1", |         "papaparse": "^5.3.1", | ||||||
|         "pbf": "^3.2.1", |         "pbf": "^3.2.1", | ||||||
|  | @ -15994,9 +15994,9 @@ | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/panoramax-js": { |     "node_modules/panoramax-js": { | ||||||
|       "version": "0.1.1", |       "version": "0.1.4", | ||||||
|       "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", |       "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz", | ||||||
|       "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", |       "integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@ogcapi-js/features": "^1.1.1", |         "@ogcapi-js/features": "^1.1.1", | ||||||
|         "@ogcapi-js/shared": "^1.1.1", |         "@ogcapi-js/shared": "^1.1.1", | ||||||
|  | @ -32056,9 +32056,9 @@ | ||||||
|       "version": "1.0.0" |       "version": "1.0.0" | ||||||
|     }, |     }, | ||||||
|     "panoramax-js": { |     "panoramax-js": { | ||||||
|       "version": "0.1.1", |       "version": "0.1.4", | ||||||
|       "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.1.tgz", |       "resolved": "https://registry.npmjs.org/panoramax-js/-/panoramax-js-0.1.4.tgz", | ||||||
|       "integrity": "sha512-6R/Bo89Nwln92zG0TwqxGhtjn6dyDrxMEO/lTTtgTZc1lkEF2znHfDXKJa4YfTPUz14FtNVOV1IWmPsp/YULYw==", |       "integrity": "sha512-X7plFMH1ndxiiyVFEluDloNiEBH0nEkurCPJ7zAInxbgv21pp/EGFwu3ynmF5ETyyXB9zu0n309juyjTdJ5pnQ==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@ogcapi-js/features": "^1.1.1", |         "@ogcapi-js/features": "^1.1.1", | ||||||
|         "@ogcapi-js/shared": "^1.1.1", |         "@ogcapi-js/shared": "^1.1.1", | ||||||
|  |  | ||||||
|  | @ -205,7 +205,7 @@ | ||||||
|     "opening_hours": "^3.6.0", |     "opening_hours": "^3.6.0", | ||||||
|     "osm-auth": "^2.5.0", |     "osm-auth": "^2.5.0", | ||||||
|     "osmtogeojson": "^3.0.0-beta.5", |     "osmtogeojson": "^3.0.0-beta.5", | ||||||
|     "panoramax-js": "^0.1.1", |     "panoramax-js": "^0.1.4", | ||||||
|     "panzoom": "^9.4.3", |     "panzoom": "^9.4.3", | ||||||
|     "papaparse": "^5.3.1", |     "papaparse": "^5.3.1", | ||||||
|     "pbf": "^3.2.1", |     "pbf": "^3.2.1", | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { Store, UIEventSource } from "../UIEventSource" | ||||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import { WikidataImageProvider } from "./WikidataImageProvider" | import { WikidataImageProvider } from "./WikidataImageProvider" | ||||||
| import Panoramax from "./Panoramax" | import Panoramax from "./Panoramax" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A generic 'from the interwebz' image picker, without attribution |  * A generic 'from the interwebz' image picker, without attribution | ||||||
|  | @ -45,10 +46,6 @@ export default class AllImageProviders { | ||||||
|         wikimedia: WikimediaImageProvider.singleton, |         wikimedia: WikimediaImageProvider.singleton, | ||||||
|         panoramax: Panoramax.singleton |         panoramax: Panoramax.singleton | ||||||
|     } |     } | ||||||
|     private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map< |  | ||||||
|         string, |  | ||||||
|         UIEventSource<ProvidedImage[]> |  | ||||||
|     >() |  | ||||||
| 
 | 
 | ||||||
|     public static byName(name: string) { |     public static byName(name: string) { | ||||||
|         return AllImageProviders.providersByName[name.toLowerCase()] |         return AllImageProviders.providersByName[name.toLowerCase()] | ||||||
|  | @ -76,42 +73,25 @@ export default class AllImageProviders { | ||||||
|         tags: Store<Record<string, string>>, |         tags: Store<Record<string, string>>, | ||||||
|         tagKey?: string[] |         tagKey?: string[] | ||||||
|     ): Store<ProvidedImage[]> { |     ): Store<ProvidedImage[]> { | ||||||
|         if (tags.data.id === undefined) { |         if (tags?.data?.id === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const cacheKey = tags.data.id + tagKey |  | ||||||
|         const cached = this._cache.get(cacheKey) |  | ||||||
|         if (cached !== undefined) { |  | ||||||
|             return cached |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         const source = new UIEventSource([]) |         const source = new UIEventSource([]) | ||||||
|         this._cache.set(cacheKey, source) |  | ||||||
|         const allSources: Store<ProvidedImage[]>[] = [] |         const allSources: Store<ProvidedImage[]>[] = [] | ||||||
|         for (const imageProvider of AllImageProviders.ImageAttributionSource) { |         for (const imageProvider of AllImageProviders.ImageAttributionSource) { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             const singleSource = imageProvider.GetRelevantUrls(tags, { |  | ||||||
|             /* |             /* | ||||||
|                 By default, 'GetRelevantUrls' uses the defaultKeyPrefixes. |                 By default, 'GetRelevantUrls' uses the defaultKeyPrefixes. | ||||||
|                 However, we override them if a custom image tag is set, e.g. 'image:menu' |                 However, we override them if a custom image tag is set, e.g. 'image:menu' | ||||||
|                */ |                */ | ||||||
|                 prefixes: tagKey ?? imageProvider.defaultKeyPrefixes, |             const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes | ||||||
|             }) |             const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes)) | ||||||
|             allSources.push(singleSource) |             allSources.push(singleSource) | ||||||
|             singleSource.addCallbackAndRunD((_) => { |             singleSource.addCallbackAndRunD((_) => { | ||||||
|                 const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) |                 const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) | ||||||
|                 const uniq = [] |                 const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url) | ||||||
|                 const seen = new Set<string>() |                 source.set(dedup) | ||||||
|                 for (const img of all) { |  | ||||||
|                     if (seen.has(img.url)) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     seen.add(img.url) |  | ||||||
|                     uniq.push(img) |  | ||||||
|                 } |  | ||||||
|                 source.setData(uniq) |  | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|         return source |         return source | ||||||
|  |  | ||||||
|  | @ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider { | ||||||
|         this._valuePrefixBlacklist = valuePrefixBlacklist |         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))) { |         if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) { | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             new URL(value) |             new URL(value) | ||||||
|         } catch (_) { |         } catch (_) { | ||||||
|             // Not a valid URL
 |             // Not a valid URL
 | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return [ |         return [{ | ||||||
|             Promise.resolve({ |  | ||||||
|             key: key, |             key: key, | ||||||
|             url: value, |             url: value, | ||||||
|             provider: this, |             provider: this, | ||||||
|             id: value, |             id: value, | ||||||
|             }), |         }] | ||||||
|         ] |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon() { |     SourceIcon() { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Store, UIEventSource } from "../UIEventSource" | import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement" | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import { LicenseInfo } from "./LicenseInfo" | import { LicenseInfo } from "./LicenseInfo" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | @ -10,6 +10,7 @@ export interface ProvidedImage { | ||||||
|     provider: ImageProvider |     provider: ImageProvider | ||||||
|     id: string |     id: string | ||||||
|     date?: Date, |     date?: Date, | ||||||
|  |     status?: string | "ready" | ||||||
|     /** |     /** | ||||||
|      * Compass angle of the taken image |      * Compass angle of the taken image | ||||||
|      * 0 = north, 90° = East |      * 0 = north, 90° = East | ||||||
|  | @ -26,29 +27,18 @@ export default abstract class ImageProvider { | ||||||
| 
 | 
 | ||||||
|     public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement |     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. |      * Gets all the relevant URLS for the given tags and for the given prefixes; | ||||||
|      * This iterates over _all_ tags and matches _anything_ that might be an image |      * extracts the necessary information | ||||||
|  |      * @param tags | ||||||
|  |      * @param prefixes | ||||||
|      */ |      */ | ||||||
|     public GetRelevantUrls( |     public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> { | ||||||
|         allTags: Store<any>, |         const relevantUrls: ProvidedImage[] = [] | ||||||
|         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 }[] |  | ||||||
|         >([]) |  | ||||||
|         const seenValues = new Set<string>() |         const seenValues = new Set<string>() | ||||||
|         allTags.addCallbackAndRunD((tags) => { | 
 | ||||||
|         for (const key in 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]+")))) { |             if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) { | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
|  | @ -58,27 +48,24 @@ export default abstract class ImageProvider { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 seenValues.add(value) |                 seenValues.add(value) | ||||||
|                     this.ExtractUrls(key, value).then((promises) => { |                 let images = this.ExtractUrls(key, value) | ||||||
|                         for (const promise of promises ?? []) { |                 if(!Array.isArray(images)){ | ||||||
|                             if (promise === undefined) { |                     images = await  images | ||||||
|                                 continue |                 } | ||||||
|  |                 if(images){ | ||||||
|  |                     relevantUrls.push(...images) | ||||||
|                 } |                 } | ||||||
|                             promise.then((providedImage) => { |  | ||||||
|                                 if (providedImage === undefined) { |  | ||||||
|                                     return |  | ||||||
|                                 } |  | ||||||
|                                 relevantUrls.data.push(providedImage) |  | ||||||
|                                 relevantUrls.ping() |  | ||||||
|                             }) |  | ||||||
|                         } |  | ||||||
|                     }) |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         }) |  | ||||||
|         return relevantUrls |         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: { |     public abstract DownloadAttribution(providedImage: { | ||||||
|         url: string |         url: string | ||||||
|  |  | ||||||
|  | @ -24,18 +24,18 @@ export class Imgur extends ImageProvider { | ||||||
|         return undefined |         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))) { |         if (Imgur.defaultValuePrefix.some((prefix) => value.startsWith(prefix))) { | ||||||
|             return [ |             return [ | ||||||
|                 Promise.resolve({ |                 { | ||||||
|                     url: value, |                     url: value, | ||||||
|                     key: key, |                     key: key, | ||||||
|                     provider: this, |                     provider: this, | ||||||
|                     id: value, |                     id: value, | ||||||
|                 }), |                 } | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|         return [] |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -131,8 +131,9 @@ export class Mapillary extends ImageProvider { | ||||||
|         return new SvelteUIElement(MapillaryIcon, { url }) |         return new SvelteUIElement(MapillaryIcon, { url }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> { | ||||||
|         return [this.PrepareUrlAsync(key, value)] |         const img = await this.PrepareUrlAsync(key, value) | ||||||
|  |         return [img] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> { |     public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> { | ||||||
|  |  | ||||||
|  | @ -1,35 +1,31 @@ | ||||||
| import { ImageUploader } from "./ImageUploader" | import { ImageUploader } from "./ImageUploader" | ||||||
| import { AuthorizedPanoramax } from "panoramax-js/dist" | import { AuthorizedPanoramax, PanoramaxXYZ, ImageData } from "panoramax-js/dist" | ||||||
| import ExifReader from "exifreader" | import ExifReader from "exifreader" | ||||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement" | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import { LicenseInfo } from "./LicenseInfo" | import { LicenseInfo } from "./LicenseInfo" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { Feature, FeatureCollection, Point } from "geojson" |  | ||||||
| import { GeoOperations } from "../GeoOperations" | 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 { | export default class PanoramaxImageProvider extends ImageProvider { | ||||||
| 
 | 
 | ||||||
|     public static readonly singleton = new PanoramaxImageProvider() |     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 defaultKeyPrefixes: string[] = ["panoramax"] | ||||||
|     public readonly name: 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 { |     public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addKnownMeta(meta: ImageData) { |     public addKnownMeta(meta: ImageData) { | ||||||
|         console.log("Adding known meta for", meta.id) |         PanoramaxImageProvider.knownMeta[meta.id] = { data: meta, time: new Date() } | ||||||
|         PanoramaxImageProvider.knownMeta[meta.id] = meta |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -39,16 +35,14 @@ export default class PanoramaxImageProvider extends ImageProvider { | ||||||
|      */ |      */ | ||||||
|     private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> { |     private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> { | ||||||
|         const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
 |         const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
 | ||||||
|         const url = `https://panoramax.mapcomplete.org/api/collections/${sequence}/items/${id}` |         const url = `https://panoramax.mapcomplete.org/` | ||||||
|         const data = <any> await Utils.downloadJsonCached(url, 60 * 60 * 1000) |         const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(sequence, id) | ||||||
|         return { url, data } |         return { url, data } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> { |     private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> { | ||||||
|         const url = "https://api.panoramax.xyz/api/search?limit=1&ids=" + imageId |         const data = await PanoramaxImageProvider.xyz.imageInfo(imageId) | ||||||
|         const metaAll = await Utils.downloadJsonCached<FeatureCollection<Point>>(url, 1000 * 60 * 60) |         return { data, url: "https://api.panoramax.xyz/" } | ||||||
|         const data= <any>metaAll.features[0] |  | ||||||
|         return {data, url} |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -58,12 +52,13 @@ export default class PanoramaxImageProvider extends ImageProvider { | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private featureToImage(info: { data: ImageData, url: string }) { |     private featureToImage(info: { data: ImageData, url: string }) { | ||||||
|         const meta = info.data |         const meta = info?.data | ||||||
|         const url = info.url |  | ||||||
|         if (!meta) { |         if (!meta) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const url = info.url | ||||||
|  | 
 | ||||||
|         function makeAbsolute(s: string) { |         function makeAbsolute(s: string) { | ||||||
|             if (!s.startsWith("https://") && !s.startsWith("http://")) { |             if (!s.startsWith("https://") && !s.startsWith("http://")) { | ||||||
|                 const parsed = new URL(url) |                 const parsed = new URL(url) | ||||||
|  | @ -80,27 +75,64 @@ export default class PanoramaxImageProvider extends ImageProvider { | ||||||
|             lon, lat, |             lon, lat, | ||||||
|             key: "panoramax", |             key: "panoramax", | ||||||
|             provider: this, |             provider: this, | ||||||
|  |             status: meta.properties["geovisio:status"], | ||||||
|             rotation: Number(meta.properties["view:azimuth"]), |             rotation: Number(meta.properties["view:azimuth"]), | ||||||
|             date: new Date(meta.properties.datetime), |             date: new Date(meta.properties.datetime), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> { |     private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> { | ||||||
|  |         if (!id.match(/^[a-zA-Z0-9-]+$/)) { | ||||||
|  |             return undefined | ||||||
|  |         } | ||||||
|         const cached = PanoramaxImageProvider.knownMeta[id] |         const cached = PanoramaxImageProvider.knownMeta[id] | ||||||
|         console.log("Cached version", id, cached) |  | ||||||
|         if (cached) { |         if (cached) { | ||||||
|             return {data: cached, url: undefined} |             if(new Date().getTime() - cached.time.getTime() < 1000){ | ||||||
|  | 
 | ||||||
|  |             return { data: cached.data, url: undefined } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         try { |         try { | ||||||
|             return await this.getInfoFromMapComplete(id) |             return await this.getInfoFromMapComplete(id) | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|  |             console.debug(e) | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|             return await this.getInfoFromXYZ(id) |             return await this.getInfoFromXYZ(id) | ||||||
|  |         } catch (e) { | ||||||
|  |                 console.debug(e) | ||||||
|         } |         } | ||||||
|  |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> { |     public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> { | ||||||
|         return [this.getInfoFor(value).then(r => this.featureToImage(<any>r))] |         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> { |     public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> { | ||||||
|  | @ -149,11 +181,10 @@ export class PanoramaxUploader implements ImageUploader { | ||||||
| 
 | 
 | ||||||
|         }) |         }) | ||||||
|         PanoramaxImageProvider.singleton.addKnownMeta(img) |         PanoramaxImageProvider.singleton.addKnownMeta(img) | ||||||
|         await Utils.waitFor(1250) |  | ||||||
|         return { |         return { | ||||||
|             key: "panoramax", |             key: "panoramax", | ||||||
|             value: img.id, |             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 SvelteUIElement from "../../UI/Base/SvelteUIElement" | ||||||
| import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte" | import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
|  | import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| 
 | 
 | ||||||
| export class WikidataImageProvider extends ImageProvider { | export class WikidataImageProvider extends ImageProvider { | ||||||
|     public static readonly singleton = new WikidataImageProvider() |     public static readonly singleton = new WikidataImageProvider() | ||||||
|  | @ -25,28 +26,28 @@ export class WikidataImageProvider extends ImageProvider { | ||||||
|         return new SvelteUIElement(Wikidata_icon) |         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)) { |         if (WikidataImageProvider.keyBlacklist.has(key)) { | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
|         const entity = await Wikidata.LoadWikidataEntryAsync(value) |         const entity = await Wikidata.LoadWikidataEntryAsync(value) | ||||||
|         if (entity === undefined) { |         if (entity === undefined) { | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const allImages: Promise<ProvidedImage>[] = [] |         const allImages: Promise<ProvidedImage[]>[] = [] | ||||||
|         // P18 is the claim 'depicted in this image'
 |         // P18 is the claim 'depicted in this image'
 | ||||||
|         for (const img of Array.from(entity.claims.get("P18") ?? [])) { |         for (const img of Array.from(entity.claims.get("P18") ?? [])) { | ||||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, img) |             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, img) | ||||||
|             allImages.push(...promises) |             allImages.push(promises) | ||||||
|         } |         } | ||||||
|         // P373 is 'commons category'
 |         // P373 is 'commons category'
 | ||||||
|         for (let cat of Array.from(entity.claims.get("P373") ?? [])) { |         for (let cat of Array.from(entity.claims.get("P373") ?? [])) { | ||||||
|             if (!cat.startsWith("Category:")) { |             if (!cat.startsWith("Category:")) { | ||||||
|                 cat = "Category:" + cat |                 cat = "Category:" + cat | ||||||
|             } |             } | ||||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, cat) |             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, cat) | ||||||
|             allImages.push(...promises) |             allImages.push(promises) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const commons = entity.commons |         const commons = entity.commons | ||||||
|  | @ -54,10 +55,11 @@ export class WikidataImageProvider extends ImageProvider { | ||||||
|             commons !== undefined && |             commons !== undefined && | ||||||
|             (commons.startsWith("Category:") || commons.startsWith("File:")) |             (commons.startsWith("Category:") || commons.startsWith("File:")) | ||||||
|         ) { |         ) { | ||||||
|             const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) |             const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) | ||||||
|             allImages.push(...promises) |             allImages.push(promises) | ||||||
|         } |         } | ||||||
|         return allImages |         const resolved = await Promise.all(Utils.NoNull(allImages)) | ||||||
|  |         return [].concat(...resolved) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public DownloadAttribution(_): Promise<any> { |     public DownloadAttribution(_): Promise<any> { | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|             return value |             return value | ||||||
|         } |         } | ||||||
|         const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent( |         const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent( | ||||||
|             value |             value, | ||||||
|         )}` |         )}` | ||||||
|         if (useHd) { |         if (useHd) { | ||||||
|             return baseUrl |             return baseUrl | ||||||
|  | @ -97,28 +97,27 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|         return this.UrlForImage("File:" + value) |         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) |         const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value) | ||||||
|         if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) { |         if (key !== undefined && key !== this.commons_key && !hasCommonsPrefix) { | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         value = WikimediaImageProvider.removeCommonsPrefix(value) |         value = WikimediaImageProvider.removeCommonsPrefix(value) | ||||||
|         if (value.startsWith("Category:")) { |         if (value.startsWith("Category:")) { | ||||||
|             const urls = await Wikimedia.GetCategoryContents(value) |             const urls = await Wikimedia.GetCategoryContents(value) | ||||||
|             return urls |             return urls.filter((url) => url.startsWith("File:")) | ||||||
|                 .filter((url) => url.startsWith("File:")) |                 .map((image) => this.UrlForImage(image)) | ||||||
|                 .map((image) => Promise.resolve(this.UrlForImage(image))) |  | ||||||
|         } |         } | ||||||
|         if (value.startsWith("File:")) { |         if (value.startsWith("File:")) { | ||||||
|             return [Promise.resolve(this.UrlForImage(value))] |             return [this.UrlForImage(value)] | ||||||
|         } |         } | ||||||
|         if (value.startsWith("http")) { |         if (value.startsWith("http")) { | ||||||
|             // PRobably an error
 |             // Probably an error
 | ||||||
|             return [] |             return undefined | ||||||
|         } |         } | ||||||
|         // We do a last effort and assume this is a file
 |         // 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> { |     public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> { | ||||||
|  | @ -148,7 +147,7 @@ export class WikimediaImageProvider extends ImageProvider { | ||||||
|             console.warn( |             console.warn( | ||||||
|                 "The file", |                 "The file", | ||||||
|                 filename, |                 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 |             return undefined | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -13,6 +13,9 @@ | ||||||
|   import { onDestroy } from "svelte" |   import { onDestroy } from "svelte" | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import type { Feature, Point } from "geojson" |   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> |   export let image: Partial<ProvidedImage> | ||||||
|   let fallbackImage: string = undefined |   let fallbackImage: string = undefined | ||||||
|  | @ -30,7 +33,7 @@ | ||||||
|   let showBigPreview = new UIEventSource(false) |   let showBigPreview = new UIEventSource(false) | ||||||
|   onDestroy(showBigPreview.addCallbackAndRun(shown => { |   onDestroy(showBigPreview.addCallbackAndRun(shown => { | ||||||
|     if (!shown) { |     if (!shown) { | ||||||
|       previewedImage.set(false) |       previewedImage.set(undefined) | ||||||
|     } |     } | ||||||
|   })) |   })) | ||||||
|   onDestroy(previewedImage.addCallbackAndRun(previewedImage => { |   onDestroy(previewedImage.addCallbackAndRun(previewedImage => { | ||||||
|  | @ -49,12 +52,12 @@ | ||||||
|       type: "Feature", |       type: "Feature", | ||||||
|       properties: { |       properties: { | ||||||
|         id: image.id, |         id: image.id, | ||||||
|         rotation: image.rotation |         rotation: image.rotation, | ||||||
|       }, |       }, | ||||||
|       geometry: { |       geometry: { | ||||||
|         type: "Point", |         type: "Point", | ||||||
|         coordinates: [image.lon, image.lat] |         coordinates: [image.lon, image.lat], | ||||||
|       } |       }, | ||||||
|     } |     } | ||||||
|     console.log(f) |     console.log(f) | ||||||
|     state?.geocodedImages.set([f]) |     state?.geocodedImages.set([f]) | ||||||
|  | @ -73,11 +76,19 @@ | ||||||
|                  on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton> |                  on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton> | ||||||
|   </div> |   </div> | ||||||
| </Popup> | </Popup> | ||||||
|  | {#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 shrink-0"> | ||||||
|     <div class="relative w-fit" |     <div class="relative w-fit" | ||||||
|          on:mouseenter={() => highlight()} |          on:mouseenter={() => highlight()} | ||||||
|          on:mouseleave={() => highlight(false)} |          on:mouseleave={() => highlight(false)} | ||||||
|     > |     > | ||||||
|  | 
 | ||||||
|       <img |       <img | ||||||
|         bind:this={imgEl} |         bind:this={imgEl} | ||||||
|         on:load={() => (loaded = true)} |         on:load={() => (loaded = true)} | ||||||
|  | @ -106,3 +117,4 @@ | ||||||
|       <ImageAttribution {image} {attributionFormat} /> |       <ImageAttribution {image} {attributionFormat} /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ export class ImageCarousel extends Toggle { | ||||||
|                             image: url, |                             image: url, | ||||||
|                             state, |                             state, | ||||||
|                             previewedImage: state?.previewedImage, |                             previewedImage: state?.previewedImage, | ||||||
|                         }) |                         }).SetClass("h-full") | ||||||
| 
 | 
 | ||||||
|                         if (url.key !== undefined) { |                         if (url.key !== undefined) { | ||||||
|                             image = new Combine([ |                             image = new Combine([ | ||||||
|  | @ -42,8 +42,8 @@ export class ImageCarousel extends Toggle { | ||||||
|                             ]).SetClass("relative") |                             ]).SetClass("relative") | ||||||
|                         } |                         } | ||||||
|                         image |                         image | ||||||
|                             .SetClass("w-full block cursor-zoom-in") |                             .SetClass("w-full h-full block cursor-zoom-in low-interaction") | ||||||
|                             .SetStyle("min-width: 50px; background: grey;") |                             .SetStyle("min-width: 50px;") | ||||||
|                         uiElements.push(image) |                         uiElements.push(image) | ||||||
|                     } catch (e) { |                     } catch (e) { | ||||||
|                         console.error("Could not generate image element for", url.url, "due to", 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 |         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 |      * Finds all duplicates in a list of strings | ||||||
|      * |      * | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue