Merge master

This commit is contained in:
Pieter Vander Vennet 2024-07-19 11:37:20 +02:00
parent 768e709312
commit c20013c5f5
8 changed files with 274 additions and 207 deletions

View file

@ -562,6 +562,7 @@
"isDeleted": "Deleted", "isDeleted": "Deleted",
"nearby": { "nearby": {
"close": "Collapse panel with nearby images", "close": "Collapse panel with nearby images",
"failed": "Fetching images from {service} failed",
"link": "This picture shows the object", "link": "This picture shows the object",
"noNearbyImages": "No nearby images were found", "noNearbyImages": "No nearby images were found",
"seeNearby": "Browse and link nearby pictures", "seeNearby": "Browse and link nearby pictures",

View file

@ -1,21 +1,48 @@
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import P4C from "pic4carto" import P4C from "pic4carto"
import { Tiles } from "../../Models/TileRange"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Point } from "geojson"
export interface NearbyImageOptions { interface ImageFetcher {
lon: number /**
lat: number * Returns images, null if an error happened
// Radius of the upstream search * @param lat
searchRadius?: 500 | number * @param lon
maxDaysOld?: 1095 | number */
blacklist: Store<{ url: string }[]> fetchImages(lat: number, lon: number): Promise<P4CPicture[]>
shownImagesCount?: UIEventSource<number>
towardscenter?: UIEventSource<boolean> readonly name: string
allowSpherical?: UIEventSource<boolean>
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius }
shownRadius?: UIEventSource<number>
class CachedFetcher implements ImageFetcher {
private readonly _fetcher: ImageFetcher
private readonly _zoomlevel: number
private readonly cache: Map<number, Promise<P4CPicture[]>> = new Map<number, Promise<P4CPicture[]>>()
public readonly name: string
constructor(fetcher: ImageFetcher, zoomlevel: number = 19) {
this._fetcher = fetcher
this._zoomlevel = zoomlevel
this.name = fetcher.name
}
fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const tile = Tiles.embedded_tile(lat, lon, this._zoomlevel)
const tileIndex = Tiles.tile_index(tile.z, tile.x, tile.y)
if (this.cache.has(tileIndex)) {
return this.cache.get(tileIndex)
}
const call = this._fetcher.fetchImages(lat, lon)
this.cache.set(tileIndex, call)
return call
}
} }
export interface P4CPicture { export interface P4CPicture {
@ -34,181 +61,72 @@ export interface P4CPicture {
} }
} }
/** class NearbyImageUtils {
* Uses Pic4Carto to fetch nearby images from various providers /**
*/ * In place sorting of the given array, by distance. Closest element will be first
export default class NearbyImagesSearch { */
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const public static sortByDistance(result: P4CPicture[], lon: number, lat: number) {
public static readonly apiUrls = ["https://api.flickr.com"] const c = [lon, lat]
private readonly individualStores: Store<
{ images: P4CPicture[]; beforeFilter: number } | undefined
>[]
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
public readonly store: Store<P4CPicture[]> = this._store
public readonly allDone: Store<boolean>
private readonly _options: NearbyImageOptions
constructor(options: NearbyImageOptions, features: IndexedFeatureSource) {
this.individualStores = NearbyImagesSearch.services
.filter((s) => s !== "kartaview" /*DEAD*/)
.map((s) => NearbyImagesSearch.buildPictureFetcher(options, s))
const allDone = new UIEventSource(false)
this.allDone = allDone
const self = this
function updateAllDone() {
const stillRunning = self.individualStores.some((store) => store.data === undefined)
allDone.setData(!stillRunning)
}
self.individualStores.forEach((s) => s.addCallback((_) => updateAllDone()))
this._options = options
if (features !== undefined) {
const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({
lat: options.lat,
lon: options.lon,
searchRadius: options.searchRadius ?? 100,
})
this.individualStores.push(
new ImmutableStore({ images: osmImages, beforeFilter: osmImages.length })
)
}
for (const source of this.individualStores) {
source.addCallback(() => this.update())
}
this.update()
}
private static async fetchImages(
options: NearbyImageOptions,
fetcher: P4CService
): Promise<P4CPicture[]> {
const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] })
const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000
const searchRadius = options.searchRadius ?? 100
try {
const pics: P4CPicture[] = await picManager.startPicsRetrievalAround(
new P4C.LatLng(options.lat, options.lon),
searchRadius,
{
mindate: new Date().getTime() - maxAgeSeconds,
towardscenter: false,
}
)
return pics
} catch (e) {
console.warn("Could not fetch images from service", fetcher, e)
return []
}
}
private static buildPictureFetcher(
options: NearbyImageOptions,
fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number } | null | undefined> {
const p4cStore = Stores.FromPromiseWithErr<P4CPicture[]>(
NearbyImagesSearch.fetchImages(options, fetcher)
)
const searchRadius = options.searchRadius ?? 100
return p4cStore.mapD(
(imagesState) => {
if (imagesState["error"]) {
return null
}
let images = imagesState["success"]
if (images === undefined) {
return undefined
}
const beforeFilterCount = images.length
if (!options?.allowSpherical?.data) {
images = images?.filter((i) => i.details.isSpherical !== true)
}
const shownRadius = options?.shownRadius?.data ?? searchRadius
if (shownRadius !== searchRadius) {
images = images.filter((i) => {
const d = GeoOperations.distanceBetween(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
return d <= shownRadius
})
}
if (options.towardscenter?.data) {
images = images.filter((i) => {
if (i.direction === undefined || isNaN(i.direction)) {
return false
}
const bearing = GeoOperations.bearing(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
const diff = Math.abs((i.direction - bearing) % 360)
return diff < 40
})
}
images?.sort((a, b) => {
const distanceA = GeoOperations.distanceBetween(
[a.coordinates.lng, a.coordinates.lat],
[options.lon, options.lat]
)
const distanceB = GeoOperations.distanceBetween(
[b.coordinates.lng, b.coordinates.lat],
[options.lon, options.lat]
)
return distanceA - distanceB
})
return { images, beforeFilter: beforeFilterCount }
},
[options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]
)
}
private update() {
const seen: Set<string> = new Set<string>(this._options.blacklist.data.map((d) => d.url))
let beforeFilter = 0
let result: P4CPicture[] = []
for (const source of this.individualStores) {
const imgs = source.data
if (imgs === undefined) {
continue
}
beforeFilter = beforeFilter + imgs.beforeFilter
for (const img of imgs.images) {
if (seen.has(img.pictureUrl)) {
continue
}
seen.add(img.pictureUrl)
result.push(img)
}
}
const c = [this._options.lon, this._options.lat]
result.sort((a, b) => { result.sort((a, b) => {
const da = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], c) const da = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], c)
const db = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], c) const db = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], c)
return da - db return da - db
}) })
if (Utils.sameList(result, this._store.data)) {
// return
}
this._store.setData(result)
} }
} }
class P4CImageFetcher implements ImageFetcher {
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
public static readonly apiUrls = ["https://api.flickr.com"]
private _options: { maxDaysOld: number, searchRadius: number }
public readonly name: P4CService
constructor(service: P4CService, options?: { maxDaysOld: number, searchRadius: number }) {
this.name = service
this._options = options
}
async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const picManager = new P4C.PicturesManager({ usefetchers: [this.name] })
const maxAgeSeconds = (this._options?.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000
const searchRadius = this._options?.searchRadius ?? 100
try {
return await picManager.startPicsRetrievalAround(
new P4C.LatLng(lat, lon),
searchRadius,
{
mindate: new Date().getTime() - maxAgeSeconds,
towardscenter: false,
},
)
} catch (e) {
console.log("P4C image fetcher failed with", e)
throw e
}
}
}
/** /**
* Extracts pictures from currently loaded features * Extracts pictures from currently loaded features
*/ */
class ImagesInLoadedDataFetcher { class ImagesInLoadedDataFetcher implements ImageFetcher {
private indexedFeatures: IndexedFeatureSource private indexedFeatures: IndexedFeatureSource
private readonly _searchRadius: number
public readonly name = "inLoadedData"
constructor(indexedFeatures: IndexedFeatureSource) { constructor(indexedFeatures: IndexedFeatureSource, searchRadius: number = 500) {
this.indexedFeatures = indexedFeatures this.indexedFeatures = indexedFeatures
this._searchRadius = searchRadius
} }
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const foundImages: P4CPicture[] = [] const foundImages: P4CPicture[] = []
this.indexedFeatures.features.data.forEach((feature) => { this.indexedFeatures.features.data.forEach((feature) => {
const props = feature.properties const props = feature.properties
@ -225,8 +143,8 @@ class ImagesInLoadedDataFetcher {
return return
} }
const centerpoint = GeoOperations.centerpointCoordinates(feature) const centerpoint = GeoOperations.centerpointCoordinates(feature)
const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat]) const d = GeoOperations.distanceBetween(centerpoint, [lon, lat])
if (loc.searchRadius !== undefined && d > loc.searchRadius) { if (this._searchRadius !== undefined && d > this._searchRadius) {
return return
} }
for (const image of images) { for (const image of images) {
@ -247,4 +165,122 @@ class ImagesInLoadedDataFetcher {
} }
} }
type P4CService = (typeof NearbyImagesSearch.services)[number]
class MapillaryFetcher implements ImageFetcher {
public readonly name = "mapillary_new"
private readonly _panoramas: "only" | "no" | undefined
private readonly _max_images: 100 | number
private readonly start_captured_at?: Date
private readonly end_captured_at?: Date
constructor(options?: {
panoramas: undefined | "only" | "no",
max_images?: 100 | number,
start_captured_at?: Date,
end_captured_at?: Date
}) {
this._panoramas = options?.panoramas
this._max_images = options?.max_images ?? 100
this.start_captured_at = options?.start_captured_at
this.end_captured_at = options?.end_captured_at
}
async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const boundingBox = new BBox([[lon, lat]]).padAbsolute(0.003)
let url = "https://graph.mapillary.com/images?fields=computed_geometry,creator,id,thumb_256_url,thumb_original_url,compass_angle&bbox="
+ [boundingBox.getWest(), boundingBox.getSouth(), boundingBox.getEast(), boundingBox.getNorth()].join(",")
+ "&access_token=" + encodeURIComponent(Constants.mapillary_client_token_v4)
+ "&limit=" + this._max_images
{
if (this._panoramas === "no") {
url += "&is_pano=false"
} else if (this._panoramas === "only") {
url += "&is_pano=true"
}
if (this.start_captured_at) {
url += "&start_captured_at="+ this.start_captured_at?.toISOString()
}
if (this.end_captured_at) {
url += "&end_captured_at="+ this.end_captured_at?.toISOString()
}
}
const response = await Utils.downloadJson<{
data: { id: string, creator: string, computed_geometry: Point, is_pano: boolean,thumb_256_url: string, thumb_original_url: string, compass_angle: number }[]
}>(url)
const pics: P4CPicture[] = []
for (const img of response.data) {
const c = img.computed_geometry.coordinates
pics.push({
pictureUrl: img.thumb_original_url,
provider: "Mapillary",
coordinates: { lng: c[0], lat: c[1] },
thumbUrl: img.thumb_256_url,
osmTags: {
"mapillary":img.id
},
details: {
isSpherical: img.is_pano,
},
})
}
return pics
}
}
type P4CService = (typeof P4CImageFetcher.services)[number]
export class CombinedFetcher {
private readonly sources: ReadonlyArray<CachedFetcher>
public static apiUrls = P4CImageFetcher.apiUrls
constructor(radius: number, maxage: Date, indexedFeatures: IndexedFeatureSource) {
this.sources = [
new ImagesInLoadedDataFetcher(indexedFeatures, radius),
new MapillaryFetcher({
panoramas: "no",
max_images: 25,
start_captured_at : maxage
}),
new P4CImageFetcher("mapillary"),
new P4CImageFetcher("wikicommons"),
].map(f => new CachedFetcher(f))
}
public getImagesAround(lon: number, lat: number): {
images: Store<P4CPicture[]>,
state: Store<Record<string, "loading" | "done" | "error">>
} {
const src = new UIEventSource<P4CPicture[]>([])
const state = new UIEventSource<Record<string, "loading" | "done" | "error">>({})
for (const source of this.sources) {
state.data[source.name] = "loading"
state.ping()
source.fetchImages(lat, lon)
.then(pics => {
console.log(source.name,"==>>",pics)
state.data[source.name] = "done"
state.ping()
if (src.data === undefined) {
src.setData(pics)
} else {
const newList = [...src.data, ...pics]
NearbyImageUtils.sortByDistance(newList, lon, lat)
src.setData(newList)
}
}, err => {
console.error("Could not load images from", source.name, "due to", err)
state.data[source.name] = "error"
state.ping()
})
}
return { images: src, state }
}
}

View file

@ -73,6 +73,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
import Locale from "../UI/i18n/Locale" import Locale from "../UI/i18n/Locale"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
import { GeoOperations } from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
/** /**
* *
@ -152,6 +153,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false) public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> public readonly toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage>
public readonly nearbyImageSearcher
constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) {
Utils.initDomPurify() Utils.initDomPurify()
this.layout = layout this.layout = layout
@ -369,6 +372,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes, this.changes,
) )
this.favourites = new FavouritesFeatureSource(this) this.favourites = new FavouritesFeatureSource(this)
const longAgo = new Date()
longAgo.setTime(new Date().getTime() - 5 * 365 * 24 * 60 * 60 * 1000 )
this.nearbyImageSearcher = new CombinedFetcher(50, longAgo, this.indexedFeatures)
this.featureSummary = this.setupSummaryLayer( this.featureSummary = this.setupSummaryLayer(
new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true), new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true),

View file

@ -21,7 +21,7 @@
</script> </script>
<a class="flex items-center" href={mapillaryLink} target="_blank"> <a class="flex items-center" href={mapillaryLink} target="_blank">
<Mapillary_black class={twMerge("shrink-0", large ? "m-2 mr-4 h-12 w-12" : "h-6 w-6 pr-2")} /> <Mapillary_black class={twMerge("shrink-0", large ? "m-2 mr-4 h-12 w-12" : "h-5 w-5 pr-1")} />
{#if large} {#if large}
<div class="flex flex-col"> <div class="flex flex-col">
<Tr t={Translations.t.general.attribution.openMapillary} /> <Tr t={Translations.t.general.attribution.openMapillary} />

View file

@ -3,10 +3,9 @@
* Show nearby images which can be clicked * Show nearby images which can be clicked
*/ */
import type { OsmTags } from "../../Models/OsmFeature" import type { OsmTags } from "../../Models/OsmFeature"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch" import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch"
import NearbyImagesSearch from "../../Logic/Web/NearbyImagesSearch"
import LinkableImage from "./LinkableImage.svelte" import LinkableImage from "./LinkableImage.svelte"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
@ -14,7 +13,7 @@
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import LoginToggle from "../Base/LoginToggle.svelte" import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
export let tags: UIEventSource<OsmTags> export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -25,36 +24,49 @@
export let linkable: boolean = true export let linkable: boolean = true
export let layer: LayerConfig export let layer: LayerConfig
let imagesProvider = new NearbyImagesSearch( let imagesProvider = state.nearbyImageSearcher
{
lon, let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(loaded => new Set(loaded.map(img => img.url)))
lat, let imageState = imagesProvider.getImagesAround(lon, lat)
allowSpherical: new UIEventSource<boolean>(false), let result: Store<P4CPicture[]> = imageState.images.mapD((pics: P4CPicture[]) => pics.filter((p: P4CPicture) =>
blacklist: AllImageProviders.LoadImagesFor(tags), !loadedImages.data.has(p.pictureUrl) // We don't show any image which is already linked
}, && !p.details.isSpherical,
state.indexedFeatures ).slice(0, 25), [loadedImages])
)
let someLoading = imageState.state.mapD(stateRecord => Object.values(stateRecord).some(v => v === "loading"))
let errors = imageState.state.mapD(stateRecord => Object.keys(stateRecord).filter(k => stateRecord[k] === "error"))
let images: Store<P4CPicture[]> = imagesProvider.store.map((images) => images.slice(0, 20))
let allDone = imagesProvider.allDone
</script> </script>
<div class="flex justify-between"> <div class="flex flex-col">
<h4>
<Tr t={Translations.t.image.nearby.title} /> {#if $result.length === 0}
</h4> {#if $someLoading}
<slot name="corner" /> <div class="flex justify-center m-4">
</div> <Loading />
{#if !$allDone} </div>
<Loading /> {:else }
{:else if $images.length === 0} <Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" />
<Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" /> {/if}
{:else} {:else}
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $images as image (image.pictureUrl)} {#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start"> <span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span> </span>
{/each} {/each}
</div>
{/if}
<div class="flex justify-between my-2">
<div>
{#if $someLoading && $result.length > 0}
<Loading />
{/if}
{#if $errors.length > 0}
<Tr cls="alert font-sm block" t={Translations.t.image.nearby.failed.Subs({service: $errors.join(", ")}) } />
{/if}
</div>
<MapillaryLink large={false}
mapProperties={{zoom: new ImmutableStore(16), location: new ImmutableStore({lon, lat})}} />
</div> </div>
{/if} </div>

View file

@ -97,6 +97,7 @@
onDestroy(async () => { onDestroy(async () => {
<<<<<<< HEAD
await Utils.waitFor(100) await Utils.waitFor(100)
requestAnimationFrame( requestAnimationFrame(
() => { () => {
@ -109,6 +110,15 @@
} }
} }
) )
=======
await Utils.waitFor(250)
try {
_map?.remove()
map = null
} catch (e) {
console.error("Could not destroy map")
}
>>>>>>> 6093ac3ea (Refactoring: rework image fetching code, improve nearby images UI. Fix #2026, #2027)
}) })
</script> </script>

View file

@ -28,6 +28,7 @@ import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSo
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import { Map as MlMap } from "maplibre-gl" import { Map as MlMap } from "maplibre-gl"
import ShowDataLayer from "./Map/ShowDataLayer" import ShowDataLayer from "./Map/ShowDataLayer"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
/** /**
* The state needed to render a special Visualisation. * The state needed to render a special Visualisation.
@ -92,6 +93,7 @@ export interface SpecialVisualizationState {
readonly imageUploadManager: ImageUploadManager readonly imageUploadManager: ImageUploadManager
readonly previewedImage: UIEventSource<ProvidedImage> readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler readonly geolocation: GeoLocationHandler
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer

View file

@ -59,7 +59,6 @@ import { Imgur } from "../Logic/ImageProviders/Imgur"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
import { MangroveReviews } from "mangrove-reviews-typescript" import { MangroveReviews } from "mangrove-reviews-typescript"
import Wikipedia from "../Logic/Web/Wikipedia" import Wikipedia from "../Logic/Web/Wikipedia"
import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch"
import AllReviews from "./Reviews/AllReviews.svelte" import AllReviews from "./Reviews/AllReviews.svelte"
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte" import StarsBarIcon from "./Reviews/StarsBarIcon.svelte"
import ReviewForm from "./Reviews/ReviewForm.svelte" import ReviewForm from "./Reviews/ReviewForm.svelte"
@ -98,6 +97,7 @@ import MarkdownUtils from "../Utils/MarkdownUtils"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray" import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import Trash from "@babeard/svelte-heroicons/mini/Trash" import Trash from "@babeard/svelte-heroicons/mini/Trash"
import NothingKnown from "./Popup/NothingKnown.svelte" import NothingKnown from "./Popup/NothingKnown.svelte"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -116,7 +116,7 @@ class NearbyImageVis implements SpecialVisualization {
docs = docs =
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images" funcName = "nearby_images"
needsUrls = NearbyImagesSearch.apiUrls needsUrls = CombinedFetcher.apiUrls
svelteBased = true svelteBased = true
constr( constr(