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,
|
||||
})
|
||||
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({
|
||||
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,29 +27,18 @@ 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]+")))) {
|
||||
continue
|
||||
}
|
||||
|
@ -58,27 +48,24 @@ export default abstract class ImageProvider {
|
|||
continue
|
||||
}
|
||||
seenValues.add(value)
|
||||
this.ExtractUrls(key, value).then((promises) => {
|
||||
for (const promise of promises ?? []) {
|
||||
if (promise === undefined) {
|
||||
continue
|
||||
let images = this.ExtractUrls(key, value)
|
||||
if(!Array.isArray(images)){
|
||||
images = await images
|
||||
}
|
||||
if(images){
|
||||
relevantUrls.push(...images)
|
||||
}
|
||||
promise.then((providedImage) => {
|
||||
if (providedImage === undefined) {
|
||||
return
|
||||
}
|
||||
relevantUrls.data.push(providedImage)
|
||||
relevantUrls.ping()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
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
|
||||
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)
|
||||
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/" }
|
||||
}
|
||||
|
||||
|
||||
|
@ -58,12 +52,13 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
* @private
|
||||
*/
|
||||
private featureToImage(info: { data: ImageData, url: string }) {
|
||||
const meta = info.data
|
||||
const url = info.url
|
||||
const meta = info?.data
|
||||
if (!meta) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const url = info.url
|
||||
|
||||
function makeAbsolute(s: string) {
|
||||
if (!s.startsWith("https://") && !s.startsWith("http://")) {
|
||||
const parsed = new URL(url)
|
||||
|
@ -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 }> {
|
||||
if (!id.match(/^[a-zA-Z0-9-]+$/)) {
|
||||
return undefined
|
||||
}
|
||||
const cached = PanoramaxImageProvider.knownMeta[id]
|
||||
console.log("Cached version", id, cached)
|
||||
if (cached) {
|
||||
return {data: cached, url: undefined}
|
||||
if(new Date().getTime() - cached.time.getTime() < 1000){
|
||||
|
||||
return { data: cached.data, url: undefined }
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await this.getInfoFromMapComplete(id)
|
||||
} catch (e) {
|
||||
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> {
|
||||
|
@ -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,11 +76,19 @@
|
|||
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
|
||||
</div>
|
||||
</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 w-fit"
|
||||
on:mouseenter={() => highlight()}
|
||||
on:mouseleave={() => highlight(false)}
|
||||
>
|
||||
|
||||
<img
|
||||
bind:this={imgEl}
|
||||
on:load={() => (loaded = true)}
|
||||
|
@ -106,3 +117,4 @@
|
|||
<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…
Reference in a new issue