diff --git a/langs/en.json b/langs/en.json index ca3fd035e..e7e4d1cc6 100644 --- a/langs/en.json +++ b/langs/en.json @@ -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": { @@ -873,4 +874,4 @@ "startsWithQ": "A wikidata identifier starts with Q and is followed by a number" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 0328e33b8..b98b93d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2b4996265..457371e9e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts index 476571c1e..8dabd4097 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -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> = new Map< - string, - UIEventSource - >() public static byName(name: string) { return AllImageProviders.providersByName[name.toLowerCase()] @@ -76,42 +73,25 @@ export default class AllImageProviders { tags: Store>, tagKey?: string[] ): Store { - 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[] = [] for (const imageProvider of AllImageProviders.ImageAttributionSource) { - - - const singleSource = imageProvider.GetRelevantUrls(tags, { - /* - By default, 'GetRelevantUrls' uses the defaultKeyPrefixes. - However, we override them if a custom image tag is set, e.g. 'image:menu' - */ - prefixes: tagKey ?? imageProvider.defaultKeyPrefixes, - }) + /* + By default, 'GetRelevantUrls' uses the defaultKeyPrefixes. + However, we override them if a custom image tag is set, e.g. 'image:menu' + */ + const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes + const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes)) allSources.push(singleSource) singleSource.addCallbackAndRunD((_) => { const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data)) - const uniq = [] - const seen = new Set() - 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 diff --git a/src/Logic/ImageProviders/GenericImageProvider.ts b/src/Logic/ImageProviders/GenericImageProvider.ts index f3b02b8d4..de369c478 100644 --- a/src/Logic/ImageProviders/GenericImageProvider.ts +++ b/src/Logic/ImageProviders/GenericImageProvider.ts @@ -15,26 +15,24 @@ export default class GenericImageProvider extends ImageProvider { this._valuePrefixBlacklist = valuePrefixBlacklist } - async ExtractUrls(key: string, value: string): Promise[]> { + ExtractUrls(key: string, value: string): undefined | ProvidedImage[] { if (this._valuePrefixBlacklist.some((prefix) => value.startsWith(prefix))) { - return [] + return undefined } try { new URL(value) } catch (_) { // Not a valid URL - return [] + return undefined } - return [ - Promise.resolve({ - key: key, - url: value, - provider: this, - id: value, - }), - ] + return [{ + key: key, + url: value, + provider: this, + id: value, + }] } SourceIcon() { diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 999516ee1..50878a94a 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -1,4 +1,4 @@ -import { Store, UIEventSource } from "../UIEventSource" +import { Store, Stores, UIEventSource } from "../UIEventSource" import BaseUIElement from "../../UI/BaseUIElement" import { LicenseInfo } from "./LicenseInfo" import { Utils } from "../../Utils" @@ -10,6 +10,7 @@ export interface ProvidedImage { provider: ImageProvider id: string date?: Date, + status?: string | "ready" /** * Compass angle of the taken image * 0 = north, 90° = East @@ -26,59 +27,45 @@ export default abstract class ImageProvider { public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement + /** - * Given a properties object, maps it onto _all_ the available pictures for this imageProvider. - * This iterates over _all_ tags and matches _anything_ that might be an image + * Gets all the relevant URLS for the given tags and for the given prefixes; + * extracts the necessary information + * @param tags + * @param prefixes */ - public GetRelevantUrls( - allTags: Store, - options?: { - prefixes?: string[] - } - ): UIEventSource { - 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, prefixes: string[]): Promise { + const relevantUrls: ProvidedImage[] = [] const seenValues = new Set() - allTags.addCallbackAndRunD((tags) => { - for (const key in tags) { - if(key === "panoramax"){ - console.log("Inspecting", key,"against", prefixes) - } - if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) { + + for (const key in tags) { + if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) { + continue + } + const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) + for (const value of values) { + if (seenValues.has(value)) { continue } - const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) - for (const value of values) { - if (seenValues.has(value)) { - continue - } - seenValues.add(value) - this.ExtractUrls(key, value).then((promises) => { - for (const promise of promises ?? []) { - if (promise === undefined) { - continue - } - promise.then((providedImage) => { - if (providedImage === undefined) { - return - } - relevantUrls.data.push(providedImage) - relevantUrls.ping() - }) - } - }) + seenValues.add(value) + let images = this.ExtractUrls(key, value) + if(!Array.isArray(images)){ + images = await images + } + if(images){ + relevantUrls.push(...images) } } - }) + } return relevantUrls } - public abstract ExtractUrls(key: string, value: string): Promise[]> + public getRelevantUrls(tags: Record, prefixes: string[]): Store { + return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes)) + } + + + public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise public abstract DownloadAttribution(providedImage: { url: string diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index f5b2c06b1..80acda10a 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -24,18 +24,18 @@ export class Imgur extends ImageProvider { return undefined } - public async ExtractUrls(key: string, value: string): Promise[]> { + 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 } /** diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index 3ce222714..5a26e31c2 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -131,8 +131,9 @@ export class Mapillary extends ImageProvider { return new SvelteUIElement(MapillaryIcon, { url }) } - async ExtractUrls(key: string, value: string): Promise[]> { - return [this.PrepareUrlAsync(key, value)] + async ExtractUrls(key: string, value: string): Promise { + const img = await this.PrepareUrlAsync(key, value) + return [img] } public async DownloadAttribution(providedImage: { id: string }): Promise { diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index 6f8291d0f..eb73b9636 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -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 & { - 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 = {} + private static knownMeta: Record = {} public SourceIcon(id?: string, location?: { lon: number; lat: number; }): BaseUIElement { return undefined } - public addKnownMeta(meta: ImageData){ - console.log("Adding known meta for", meta.id) - PanoramaxImageProvider.knownMeta[meta.id] = meta + public addKnownMeta(meta: ImageData) { + PanoramaxImageProvider.knownMeta[meta.id] = { data: meta, time: new Date() } } /** @@ -39,16 +35,14 @@ export default class PanoramaxImageProvider extends ImageProvider { */ private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> { const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence - const url = `https://panoramax.mapcomplete.org/api/collections/${sequence}/items/${id}` - const data = await Utils.downloadJsonCached(url, 60 * 60 * 1000) - return {url, data} + const url = `https://panoramax.mapcomplete.org/` + const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(sequence, id) + return { url, data } } private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> { - const url = "https://api.panoramax.xyz/api/search?limit=1&ids=" + imageId - const metaAll = await Utils.downloadJsonCached>(url, 1000 * 60 * 60) - const data= metaAll.features[0] - return {data, url} + const data = await PanoramaxImageProvider.xyz.imageInfo(imageId) + return { data, url: "https://api.panoramax.xyz/" } } @@ -57,17 +51,18 @@ export default class PanoramaxImageProvider extends ImageProvider { * @param meta * @private */ - private featureToImage(info: {data: ImageData, url: string}) { - const meta = info.data - const url = info.url + private featureToImage(info: { data: ImageData, url: string }) { + const meta = info?.data if (!meta) { return undefined } - function makeAbsolute(s: string){ - if(!s.startsWith("https://") && !s.startsWith("http://")){ - const parsed = new URL(url) - return parsed.protocol+"//"+parsed.host+s + const url = info.url + + function makeAbsolute(s: string) { + if (!s.startsWith("https://") && !s.startsWith("http://")) { + const parsed = new URL(url) + return parsed.protocol + "//" + parsed.host + s } return s } @@ -80,27 +75,64 @@ export default class PanoramaxImageProvider extends ImageProvider { lon, lat, key: "panoramax", provider: this, + status: meta.properties["geovisio:status"], rotation: Number(meta.properties["view:azimuth"]), date: new Date(meta.properties.datetime), } } private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> { - const cached= PanoramaxImageProvider.knownMeta[id] - console.log("Cached version", id, cached) - if(cached){ - return {data: cached, url: undefined} + if (!id.match(/^[a-zA-Z0-9-]+$/)) { + return undefined + } + const cached = PanoramaxImageProvider.knownMeta[id] + if (cached) { + if(new Date().getTime() - cached.time.getTime() < 1000){ + + return { data: cached.data, url: undefined } + } } try { return await this.getInfoFromMapComplete(id) } catch (e) { - return await this.getInfoFromXYZ(id) + console.debug(e) } + try { + return await this.getInfoFromXYZ(id) + } catch (e) { + console.debug(e) + } + return undefined } - public async ExtractUrls(key: string, value: string): Promise[]> { - return [this.getInfoFor(value).then(r => this.featureToImage(r))] + public async ExtractUrls(key: string, value: string): Promise { + return [await this.getInfoFor(value).then(r => this.featureToImage(r))] + } + + + getRelevantUrls(tags: Record, prefixes: string[]): Store { + 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 { @@ -139,7 +171,7 @@ export class PanoramaxUploader implements ImageUploader { const p = this._panoramax const defaultSequence = (await p.mySequences())[0] - const img = await p.addImage(blob, defaultSequence, { + const img = await p.addImage(blob, defaultSequence, { lat: !hasGPS ? lat : undefined, lon: !hasGPS ? lon : undefined, datetime: !hasDate ? new Date().toISOString() : undefined, @@ -149,11 +181,10 @@ export class PanoramaxUploader implements ImageUploader { }) PanoramaxImageProvider.singleton.addKnownMeta(img) - await Utils.waitFor(1250) return { key: "panoramax", value: img.id, - absoluteUrl: img.assets.hd.href + absoluteUrl: img.assets.hd.href, } } diff --git a/src/Logic/ImageProviders/WikidataImageProvider.ts b/src/Logic/ImageProviders/WikidataImageProvider.ts index ded3396da..c64896559 100644 --- a/src/Logic/ImageProviders/WikidataImageProvider.ts +++ b/src/Logic/ImageProviders/WikidataImageProvider.ts @@ -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[]> { + public async ExtractUrls(key: string, value: string): Promise { if (WikidataImageProvider.keyBlacklist.has(key)) { - return [] + return undefined } const entity = await Wikidata.LoadWikidataEntryAsync(value) if (entity === undefined) { - return [] + return undefined } - const allImages: Promise[] = [] + const allImages: Promise[] = [] // 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 { diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index 020f32b4f..631ea44f9 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -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[]> { + public async ExtractUrls(key: string, value: string): undefined | Promise { 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 { @@ -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 } diff --git a/src/UI/Image/AttributedImage.svelte b/src/UI/Image/AttributedImage.svelte index 60fc4bfc3..5af0aaf06 100644 --- a/src/UI/Image/AttributedImage.svelte +++ b/src/UI/Image/AttributedImage.svelte @@ -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 let fallbackImage: string = undefined @@ -30,7 +33,7 @@ let showBigPreview = new UIEventSource(false) onDestroy(showBigPreview.addCallbackAndRun(shown => { if (!shown) { - previewedImage.set(false) + previewedImage.set(undefined) } })) onDestroy(previewedImage.addCallbackAndRun(previewedImage => { @@ -49,12 +52,12 @@ type: "Feature", properties: { id: image.id, - rotation: image.rotation + rotation: image.rotation, }, geometry: { type: "Point", - coordinates: [image.lon, image.lat] - } + coordinates: [image.lon, image.lat], + }, } console.log(f) state?.geocodedImages.set([f]) @@ -73,36 +76,45 @@ on:click={() => {console.log("Closing");previewedImage.set(undefined)}}> -
-
highlight()} - on:mouseleave={() => highlight(false)} - > - (loaded = true)} - class={imgClass ?? ""} - class:cursor-zoom-in={canZoom} - on:click={() => { +{#if image.status !== undefined && image.status !== "ready"} +
+ + + +
+{:else} +
+
highlight()} + on:mouseleave={() => highlight(false)} + > + + (loaded = true)} + class={imgClass ?? ""} + class:cursor-zoom-in={canZoom} + on:click={() => { previewedImage?.set(image) }} - on:error={() => { + on:error={() => { if (fallbackImage) { imgEl.src = fallbackImage } }} - src={image.url} - /> + src={image.url} + /> - {#if canZoom && loaded} -
previewedImage.set(image)}> - -
- {/if} + {#if canZoom && loaded} +
previewedImage.set(image)}> + +
+ {/if} +
+
+ +
-
- -
-
+{/if} diff --git a/src/UI/Image/ImageCarousel.ts b/src/UI/Image/ImageCarousel.ts index 8c6d1e5b9..3b96b5bb6 100644 --- a/src/UI/Image/ImageCarousel.ts +++ b/src/UI/Image/ImageCarousel.ts @@ -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) diff --git a/src/Utils.ts b/src/Utils.ts index 2a23d76fd..b2b7f879f 100644 --- a/src/Utils.ts +++ b/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(arr: T[], toKey: ((t:T) => string) ): T[]{ + const uniq: T[] = [] + const seen = new Set() + 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 *