2023-09-16 02:30:01 +02:00
|
|
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
|
|
|
import { GeoOperations } from "../GeoOperations"
|
2024-07-19 11:37:20 +02:00
|
|
|
import { Store, UIEventSource } from "../UIEventSource"
|
2023-09-16 02:30:01 +02:00
|
|
|
import P4C from "pic4carto"
|
2024-07-19 11:37:20 +02:00
|
|
|
import { Tiles } from "../../Models/TileRange"
|
|
|
|
import { BBox } from "../BBox"
|
|
|
|
import Constants from "../../Models/Constants"
|
2023-09-16 02:30:01 +02:00
|
|
|
import { Utils } from "../../Utils"
|
2024-07-19 11:37:20 +02:00
|
|
|
import { Point } from "geojson"
|
2024-09-18 23:07:26 +02:00
|
|
|
import { Imgur } from "../ImageProviders/Imgur"
|
2025-05-06 01:46:06 +02:00
|
|
|
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
|
2025-05-12 11:37:26 +02:00
|
|
|
import { Mapillary } from "../ImageProviders/Mapillary"
|
2024-07-19 11:37:20 +02:00
|
|
|
|
|
|
|
interface ImageFetcher {
|
|
|
|
/**
|
|
|
|
* Returns images, null if an error happened
|
|
|
|
* @param lat
|
|
|
|
* @param lon
|
|
|
|
*/
|
|
|
|
fetchImages(lat: number, lon: number): Promise<P4CPicture[]>
|
|
|
|
|
|
|
|
readonly name: string
|
|
|
|
}
|
|
|
|
|
|
|
|
class CachedFetcher implements ImageFetcher {
|
|
|
|
private readonly _fetcher: ImageFetcher
|
|
|
|
private readonly _zoomlevel: number
|
2024-07-21 10:52:51 +02:00
|
|
|
private readonly cache: Map<number, Promise<P4CPicture[]>> = new Map<
|
|
|
|
number,
|
|
|
|
Promise<P4CPicture[]>
|
|
|
|
>()
|
2024-07-19 11:37:20 +02:00
|
|
|
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
|
|
|
|
}
|
2023-09-16 02:30:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface P4CPicture {
|
|
|
|
pictureUrl: string
|
|
|
|
date?: number
|
|
|
|
coordinates: { lat: number; lng: number }
|
|
|
|
provider: "Mapillary" | string
|
|
|
|
author?
|
|
|
|
license?
|
|
|
|
detailsUrl?: string
|
2024-10-19 14:44:55 +02:00
|
|
|
direction?: number
|
2023-09-16 02:30:01 +02:00
|
|
|
osmTags?: object /*To copy straight into OSM!*/
|
|
|
|
thumbUrl: string
|
|
|
|
details: {
|
|
|
|
isSpherical: boolean
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-19 11:37:20 +02:00
|
|
|
class NearbyImageUtils {
|
|
|
|
/**
|
|
|
|
* In place sorting of the given array, by distance. Closest element will be first
|
|
|
|
*/
|
|
|
|
public static sortByDistance(result: P4CPicture[], lon: number, lat: number) {
|
|
|
|
const c = [lon, lat]
|
|
|
|
result.sort((a, b) => {
|
|
|
|
const da = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], c)
|
|
|
|
const db = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], c)
|
|
|
|
return da - db
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class P4CImageFetcher implements ImageFetcher {
|
2023-09-27 22:21:35 +02:00
|
|
|
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
|
|
|
|
public static readonly apiUrls = ["https://api.flickr.com"]
|
2024-07-21 10:52:51 +02:00
|
|
|
private _options: { maxDaysOld: number; searchRadius: number }
|
2024-07-19 11:37:20 +02:00
|
|
|
public readonly name: P4CService
|
2023-12-03 03:51:18 +01:00
|
|
|
|
2024-07-21 10:52:51 +02:00
|
|
|
constructor(service: P4CService, options?: { maxDaysOld: number; searchRadius: number }) {
|
2024-07-19 11:37:20 +02:00
|
|
|
this.name = service
|
2023-09-16 02:30:01 +02:00
|
|
|
this._options = options
|
|
|
|
}
|
|
|
|
|
2024-07-19 11:37:20 +02:00
|
|
|
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
|
2023-09-16 02:30:01 +02:00
|
|
|
|
2023-09-27 22:21:35 +02:00
|
|
|
try {
|
2024-07-19 11:37:20 +02:00
|
|
|
return await picManager.startPicsRetrievalAround(
|
|
|
|
new P4C.LatLng(lat, lon),
|
2023-09-16 02:30:01 +02:00
|
|
|
searchRadius,
|
|
|
|
{
|
|
|
|
mindate: new Date().getTime() - maxAgeSeconds,
|
|
|
|
towardscenter: false,
|
2024-10-19 14:44:55 +02:00
|
|
|
}
|
2023-09-16 02:30:01 +02:00
|
|
|
)
|
2023-09-27 22:21:35 +02:00
|
|
|
} catch (e) {
|
2024-07-19 11:37:20 +02:00
|
|
|
console.log("P4C image fetcher failed with", e)
|
|
|
|
throw e
|
2023-09-27 22:21:35 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-16 02:30:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-07-27 12:59:38 +02:00
|
|
|
* Extracts pictures from features which are currently loaded on the local machine, probably features of the same layer
|
2023-09-16 02:30:01 +02:00
|
|
|
*/
|
2024-07-19 11:37:20 +02:00
|
|
|
class ImagesInLoadedDataFetcher implements ImageFetcher {
|
2023-09-16 02:30:01 +02:00
|
|
|
private indexedFeatures: IndexedFeatureSource
|
2024-07-19 11:37:20 +02:00
|
|
|
private readonly _searchRadius: number
|
|
|
|
public readonly name = "inLoadedData"
|
2023-09-16 02:30:01 +02:00
|
|
|
|
2024-07-19 11:37:20 +02:00
|
|
|
constructor(indexedFeatures: IndexedFeatureSource, searchRadius: number = 500) {
|
2023-09-16 02:30:01 +02:00
|
|
|
this.indexedFeatures = indexedFeatures
|
2024-07-19 11:37:20 +02:00
|
|
|
this._searchRadius = searchRadius
|
2023-09-16 02:30:01 +02:00
|
|
|
}
|
|
|
|
|
2024-07-19 11:37:20 +02:00
|
|
|
async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
|
2023-09-16 02:30:01 +02:00
|
|
|
const foundImages: P4CPicture[] = []
|
|
|
|
this.indexedFeatures.features.data.forEach((feature) => {
|
|
|
|
const props = feature.properties
|
|
|
|
const images = []
|
|
|
|
if (props.image) {
|
|
|
|
images.push(props.image)
|
|
|
|
}
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
if (props["image:" + i]) {
|
|
|
|
images.push(props["image:" + i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (images.length == 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
2024-07-19 11:37:20 +02:00
|
|
|
const d = GeoOperations.distanceBetween(centerpoint, [lon, lat])
|
|
|
|
if (this._searchRadius !== undefined && d > this._searchRadius) {
|
2023-09-16 02:30:01 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
for (const image of images) {
|
|
|
|
foundImages.push({
|
|
|
|
pictureUrl: image,
|
|
|
|
thumbUrl: image,
|
|
|
|
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
|
|
|
|
provider: "OpenStreetMap",
|
|
|
|
details: {
|
2024-08-09 16:55:08 +02:00
|
|
|
isSpherical: false,
|
2023-09-16 02:30:01 +02:00
|
|
|
},
|
2024-08-09 16:55:08 +02:00
|
|
|
osmTags: { image },
|
2023-09-16 02:30:01 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return foundImages
|
|
|
|
}
|
|
|
|
}
|
2023-09-27 22:21:35 +02:00
|
|
|
|
2024-09-30 01:08:07 +02:00
|
|
|
class ImagesFromPanoramaxFetcher implements ImageFetcher {
|
|
|
|
private readonly _radius: number
|
|
|
|
private readonly _panoramax: Panoramax
|
|
|
|
name: string = "panoramax"
|
2025-04-15 18:18:44 +02:00
|
|
|
public static readonly apiUrls: ReadonlyArray<string> = [
|
|
|
|
"https://panoramax.openstreetmap.fr",
|
|
|
|
"https://api.panoramax.xyz",
|
|
|
|
"https://panoramax.mapcomplete.org",
|
|
|
|
]
|
2024-09-30 01:08:07 +02:00
|
|
|
|
2025-04-22 02:53:31 +02:00
|
|
|
constructor(url?: string, radius: number = 50) {
|
2024-09-30 01:08:07 +02:00
|
|
|
this._radius = radius
|
|
|
|
if (url) {
|
|
|
|
this._panoramax = new Panoramax(url)
|
|
|
|
} else {
|
|
|
|
this._panoramax = new PanoramaxXYZ()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-06 01:46:06 +02:00
|
|
|
private static convert(imageData: ImageData): P4CPicture {
|
|
|
|
const [lng, lat] = imageData.geometry.coordinates
|
|
|
|
return {
|
|
|
|
pictureUrl: imageData.assets.sd.href,
|
|
|
|
coordinates: { lng, lat },
|
|
|
|
|
|
|
|
provider: "panoramax",
|
|
|
|
direction: imageData.properties["view:azimuth"],
|
|
|
|
osmTags: {
|
2025-05-08 11:44:03 +02:00
|
|
|
panoramax: imageData.id,
|
2025-05-06 01:46:06 +02:00
|
|
|
},
|
|
|
|
thumbUrl: imageData.assets.thumb.href,
|
|
|
|
date: new Date(imageData.properties.datetime).getTime(),
|
|
|
|
license: imageData.properties["geovisio:license"],
|
|
|
|
author: imageData.providers.at(-1).name,
|
|
|
|
detailsUrl: imageData.id,
|
|
|
|
details: {
|
|
|
|
isSpherical:
|
2025-05-08 11:44:03 +02:00
|
|
|
imageData.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular",
|
|
|
|
},
|
2025-05-06 01:46:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-30 01:08:07 +02:00
|
|
|
public async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
|
2025-05-08 11:44:03 +02:00
|
|
|
const radiusSettings = [
|
|
|
|
{
|
|
|
|
place_fov_tolerance: 180,
|
|
|
|
radius: 15,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
place_fov_tolerance: 180,
|
|
|
|
radius: 25,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
place_fov_tolerance: 90,
|
|
|
|
radius: 50,
|
|
|
|
},
|
|
|
|
]
|
2025-05-06 01:46:06 +02:00
|
|
|
const promises: Promise<ImageData[]>[] = []
|
|
|
|
const maxRadius = this._radius
|
|
|
|
let prevRadius = 0
|
2025-05-12 11:37:26 +02:00
|
|
|
|
|
|
|
const nearby = this._panoramax.search({
|
|
|
|
bbox: new BBox([[lon, lat]]).pad(0.001).toLngLatFlat()
|
|
|
|
})
|
|
|
|
promises.push(nearby) // We do a nearby search with bbox, see https://source.mapcomplete.org/MapComplete/MapComplete/issues/2384
|
2025-05-06 01:46:06 +02:00
|
|
|
for (const radiusSetting of radiusSettings) {
|
|
|
|
const promise = this._panoramax.search({
|
|
|
|
place: [lon, lat],
|
|
|
|
place_distance: [prevRadius, Math.min(maxRadius, radiusSetting.radius)],
|
|
|
|
place_fov_tolerance: radiusSetting.place_fov_tolerance,
|
2025-05-08 11:44:03 +02:00
|
|
|
limit: 50,
|
2025-05-06 01:46:06 +02:00
|
|
|
})
|
|
|
|
promises.push(promise)
|
|
|
|
prevRadius = radiusSetting.radius
|
|
|
|
if (radiusSetting.radius >= maxRadius) {
|
|
|
|
break
|
2024-10-19 14:44:55 +02:00
|
|
|
}
|
2025-05-06 01:46:06 +02:00
|
|
|
}
|
|
|
|
const images = await Promise.all(promises)
|
|
|
|
|
|
|
|
return [].concat(...images).map((i) => ImagesFromPanoramaxFetcher.convert(i))
|
2024-09-30 01:08:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-19 11:37:20 +02:00
|
|
|
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
|
2025-03-30 15:18:20 +02:00
|
|
|
static apiUrls: string[] = ["*.fbcdn.net", "https://graph.mapillary.com"]
|
2024-07-19 11:37:20 +02:00
|
|
|
|
|
|
|
constructor(options?: {
|
2025-03-16 16:31:38 +01:00
|
|
|
panoramas?: undefined | "only" | "no"
|
2024-07-21 10:52:51 +02:00
|
|
|
max_images?: 100 | number
|
|
|
|
start_captured_at?: Date
|
2024-07-19 11:37:20 +02:00
|
|
|
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)
|
2024-07-21 10:52:51 +02:00
|
|
|
let url =
|
2025-05-12 11:37:26 +02:00
|
|
|
"https://graph.mapillary.com/images?fields=geometry,computed_geometry,creator,id,captured_at,thumb_256_url,thumb_original_url,compass_angle&bbox=" +
|
2024-07-21 10:52:51 +02:00
|
|
|
[
|
|
|
|
boundingBox.getWest(),
|
|
|
|
boundingBox.getSouth(),
|
|
|
|
boundingBox.getEast(),
|
|
|
|
boundingBox.getNorth(),
|
|
|
|
].join(",") +
|
|
|
|
"&access_token=" +
|
|
|
|
encodeURIComponent(Constants.mapillary_client_token_v4) +
|
|
|
|
"&limit=" +
|
|
|
|
this._max_images
|
2024-07-19 11:37:20 +02:00
|
|
|
{
|
|
|
|
if (this._panoramas === "no") {
|
|
|
|
url += "&is_pano=false"
|
|
|
|
} else if (this._panoramas === "only") {
|
|
|
|
url += "&is_pano=true"
|
|
|
|
}
|
|
|
|
if (this.start_captured_at) {
|
2024-07-27 12:59:38 +02:00
|
|
|
url += "&start_captured_at=" + this.start_captured_at?.toISOString()
|
2024-07-19 11:37:20 +02:00
|
|
|
}
|
|
|
|
if (this.end_captured_at) {
|
2024-07-27 12:59:38 +02:00
|
|
|
url += "&end_captured_at=" + this.end_captured_at?.toISOString()
|
2024-07-19 11:37:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await Utils.downloadJson<{
|
2024-07-27 12:59:38 +02:00
|
|
|
data: {
|
2024-08-09 16:55:08 +02:00
|
|
|
id: string
|
2025-05-12 11:37:26 +02:00
|
|
|
creator: { username: string }
|
2025-04-09 23:30:39 +02:00
|
|
|
geometry: Point
|
2024-08-09 16:55:08 +02:00
|
|
|
computed_geometry: Point
|
|
|
|
is_pano: boolean
|
|
|
|
thumb_256_url: string
|
|
|
|
thumb_original_url: string
|
2024-07-27 12:59:38 +02:00
|
|
|
compass_angle: number
|
2025-05-12 11:37:26 +02:00
|
|
|
captured_at: number
|
2024-07-27 12:59:38 +02:00
|
|
|
}[]
|
2024-07-19 11:37:20 +02:00
|
|
|
}>(url)
|
|
|
|
const pics: P4CPicture[] = []
|
|
|
|
for (const img of response.data) {
|
2025-04-09 23:30:39 +02:00
|
|
|
const c = img.computed_geometry?.coordinates ?? img.geometry.coordinates
|
2024-07-27 12:59:38 +02:00
|
|
|
if (img.thumb_original_url === undefined) {
|
2024-07-19 11:57:53 +02:00
|
|
|
continue
|
|
|
|
}
|
2025-05-12 11:37:26 +02:00
|
|
|
const [lon, lat] = img.computed_geometry.coordinates
|
2024-07-19 11:37:20 +02:00
|
|
|
pics.push({
|
|
|
|
pictureUrl: img.thumb_original_url,
|
|
|
|
provider: "Mapillary",
|
|
|
|
coordinates: { lng: c[0], lat: c[1] },
|
|
|
|
thumbUrl: img.thumb_256_url,
|
|
|
|
osmTags: {
|
2024-07-21 10:52:51 +02:00
|
|
|
mapillary: img.id,
|
2024-07-19 11:37:20 +02:00
|
|
|
},
|
|
|
|
details: {
|
2025-04-15 18:18:44 +02:00
|
|
|
isSpherical: this._panoramas === "only",
|
2024-08-09 16:55:08 +02:00
|
|
|
},
|
2025-05-12 11:37:26 +02:00
|
|
|
|
|
|
|
detailsUrl: Mapillary.singleton.visitUrl(img, { lon, lat }),
|
|
|
|
date: img.captured_at,
|
|
|
|
license: "CC-BY-SA",
|
|
|
|
author: img.creator.username,
|
|
|
|
direction: img.compass_angle
|
2024-07-19 11:37:20 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
return pics
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type P4CService = (typeof P4CImageFetcher.services)[number]
|
|
|
|
|
|
|
|
export class CombinedFetcher {
|
|
|
|
private readonly sources: ReadonlyArray<CachedFetcher>
|
2025-04-15 18:18:44 +02:00
|
|
|
public static apiUrls = [
|
|
|
|
...P4CImageFetcher.apiUrls,
|
|
|
|
Imgur.apiUrl,
|
|
|
|
...Imgur.supportingUrls,
|
2025-03-30 15:18:20 +02:00
|
|
|
...MapillaryFetcher.apiUrls,
|
2025-04-15 18:18:44 +02:00
|
|
|
...ImagesFromPanoramaxFetcher.apiUrls,
|
2025-03-30 15:18:20 +02:00
|
|
|
]
|
2024-07-19 11:37:20 +02:00
|
|
|
|
|
|
|
constructor(radius: number, maxage: Date, indexedFeatures: IndexedFeatureSource) {
|
|
|
|
this.sources = [
|
2024-07-27 15:13:13 +02:00
|
|
|
new ImagesInLoadedDataFetcher(indexedFeatures, radius),
|
2024-09-30 01:08:07 +02:00
|
|
|
new ImagesFromPanoramaxFetcher(),
|
|
|
|
new ImagesFromPanoramaxFetcher(Constants.panoramax.url),
|
2025-03-30 03:10:29 +02:00
|
|
|
// For mapillary, we need to query both with and without panoramas. See https://www.mapillary.com/developer/api-documentation/
|
2024-07-27 15:13:13 +02:00
|
|
|
new MapillaryFetcher({
|
2024-08-09 16:55:08 +02:00
|
|
|
max_images: 25,
|
|
|
|
start_captured_at: maxage,
|
2025-04-15 18:18:44 +02:00
|
|
|
panoramas: "only",
|
2024-08-09 16:55:08 +02:00
|
|
|
}),
|
2025-03-30 03:10:29 +02:00
|
|
|
new MapillaryFetcher({
|
|
|
|
max_images: 25,
|
|
|
|
start_captured_at: maxage,
|
2025-04-15 18:18:44 +02:00
|
|
|
panoramas: "no",
|
|
|
|
}),
|
|
|
|
new P4CImageFetcher("mapillary"),
|
|
|
|
new P4CImageFetcher("wikicommons"),
|
2024-08-09 16:55:08 +02:00
|
|
|
].map((f) => new CachedFetcher(f))
|
2024-07-19 11:37:20 +02:00
|
|
|
}
|
|
|
|
|
2024-08-09 16:55:08 +02:00
|
|
|
private async fetchImage(
|
|
|
|
source: CachedFetcher,
|
|
|
|
lat: number,
|
|
|
|
lon: number,
|
|
|
|
state: UIEventSource<Record<string, "loading" | "done" | "error">>,
|
2024-10-19 14:44:55 +02:00
|
|
|
sink: UIEventSource<P4CPicture[]>
|
2024-08-09 16:55:08 +02:00
|
|
|
): Promise<void> {
|
2024-07-27 12:59:38 +02:00
|
|
|
try {
|
|
|
|
const pics = await source.fetchImages(lat, lon)
|
|
|
|
state.data[source.name] = "done"
|
|
|
|
state.ping()
|
|
|
|
|
|
|
|
if (sink.data === undefined) {
|
|
|
|
sink.setData(pics)
|
|
|
|
} else {
|
|
|
|
const newList = []
|
|
|
|
const seenIds = new Set<string>()
|
|
|
|
for (const p4CPicture of [...sink.data, ...pics]) {
|
|
|
|
const id = p4CPicture.pictureUrl
|
|
|
|
if (seenIds.has(id)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
newList.push(p4CPicture)
|
|
|
|
seenIds.add(id)
|
|
|
|
}
|
|
|
|
NearbyImageUtils.sortByDistance(newList, lon, lat)
|
|
|
|
sink.setData(newList)
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Could not load images from", source.name, "due to", e)
|
|
|
|
state.data[source.name] = "error"
|
|
|
|
state.ping()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-09 16:55:08 +02:00
|
|
|
public getImagesAround(
|
|
|
|
lon: number,
|
2024-10-19 14:44:55 +02:00
|
|
|
lat: number
|
2024-08-09 16:55:08 +02:00
|
|
|
): {
|
|
|
|
images: Store<P4CPicture[]>
|
2024-07-19 11:37:20 +02:00
|
|
|
state: Store<Record<string, "loading" | "done" | "error">>
|
|
|
|
} {
|
2024-07-27 12:59:38 +02:00
|
|
|
const sink = new UIEventSource<P4CPicture[]>([])
|
2024-07-19 11:37:20 +02:00
|
|
|
const state = new UIEventSource<Record<string, "loading" | "done" | "error">>({})
|
|
|
|
for (const source of this.sources) {
|
|
|
|
state.data[source.name] = "loading"
|
|
|
|
state.ping()
|
2024-07-27 12:59:38 +02:00
|
|
|
this.fetchImage(source, lat, lon, state, sink)
|
2024-07-19 11:37:20 +02:00
|
|
|
}
|
2024-07-27 12:59:38 +02:00
|
|
|
return { images: sink, state }
|
2024-07-19 11:37:20 +02:00
|
|
|
}
|
|
|
|
}
|