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…
Reference in a new issue