Feat: check if the image was blurred, attempt to reload if it is done; refactoring of ImageProvider code

This commit is contained in:
Pieter Vander Vennet 2024-09-28 02:04:14 +02:00
parent 590591dd31
commit 4650170db4
14 changed files with 224 additions and 190 deletions

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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() {

View file

@ -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

View file

@ -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
} }
/** /**

View file

@ -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> {

View file

@ -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,
} }
} }

View file

@ -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> {

View file

@ -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
} }

View file

@ -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}

View file

@ -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)

View file

@ -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
* *