forked from MapComplete/MapComplete
		
	Merge master
This commit is contained in:
		
							parent
							
								
									768e709312
								
							
						
					
					
						commit
						c20013c5f5
					
				
					 8 changed files with 274 additions and 207 deletions
				
			
		|  | @ -562,6 +562,7 @@ | |||
|         "isDeleted": "Deleted", | ||||
|         "nearby": { | ||||
|             "close": "Collapse panel with nearby images", | ||||
|             "failed": "Fetching images from {service} failed", | ||||
|             "link": "This picture shows the object", | ||||
|             "noNearbyImages": "No nearby images were found", | ||||
|             "seeNearby": "Browse and link nearby pictures", | ||||
|  |  | |||
|  | @ -1,21 +1,48 @@ | |||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import P4C from "pic4carto" | ||||
| import { Tiles } from "../../Models/TileRange" | ||||
| import { BBox } from "../BBox" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Point } from "geojson" | ||||
| 
 | ||||
| export interface NearbyImageOptions { | ||||
|     lon: number | ||||
|     lat: number | ||||
|     // Radius of the upstream search
 | ||||
|     searchRadius?: 500 | number | ||||
|     maxDaysOld?: 1095 | number | ||||
|     blacklist: Store<{ url: string }[]> | ||||
|     shownImagesCount?: UIEventSource<number> | ||||
|     towardscenter?: UIEventSource<boolean> | ||||
|     allowSpherical?: UIEventSource<boolean> | ||||
|     // Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
 | ||||
|     shownRadius?: UIEventSource<number> | ||||
| 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 | ||||
|     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 { | ||||
|  | @ -34,181 +61,72 @@ export interface P4CPicture { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Uses Pic4Carto to fetch nearby images from various providers | ||||
|  */ | ||||
| export default class NearbyImagesSearch { | ||||
|     public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const | ||||
|     public static readonly apiUrls = ["https://api.flickr.com"] | ||||
|     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] | ||||
| 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 | ||||
| 
 | ||||
|         }) | ||||
|         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 | ||||
|  */ | ||||
| class ImagesInLoadedDataFetcher { | ||||
| class ImagesInLoadedDataFetcher implements ImageFetcher { | ||||
|     private indexedFeatures: IndexedFeatureSource | ||||
|     private readonly _searchRadius: number | ||||
|     public readonly name = "inLoadedData" | ||||
| 
 | ||||
|     constructor(indexedFeatures: IndexedFeatureSource) { | ||||
|     constructor(indexedFeatures: IndexedFeatureSource, searchRadius: number = 500) { | ||||
|         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[] = [] | ||||
|         this.indexedFeatures.features.data.forEach((feature) => { | ||||
|             const props = feature.properties | ||||
|  | @ -225,8 +143,8 @@ class ImagesInLoadedDataFetcher { | |||
|                 return | ||||
|             } | ||||
|             const centerpoint = GeoOperations.centerpointCoordinates(feature) | ||||
|             const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat]) | ||||
|             if (loc.searchRadius !== undefined && d > loc.searchRadius) { | ||||
|             const d = GeoOperations.distanceBetween(centerpoint, [lon, lat]) | ||||
|             if (this._searchRadius !== undefined && d > this._searchRadius) { | ||||
|                 return | ||||
|             } | ||||
|             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 } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson" | |||
| import Locale from "../UI/i18n/Locale" | ||||
| import Hash from "../Logic/Web/Hash" | ||||
| 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 toCacheSavers: ReadonlyMap<string, SaveFeatureSourceToLocalStorage> | ||||
| 
 | ||||
|     public readonly nearbyImageSearcher | ||||
| 
 | ||||
|     constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) { | ||||
|         Utils.initDomPurify() | ||||
|         this.layout = layout | ||||
|  | @ -369,6 +372,9 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             this.changes, | ||||
|         ) | ||||
|         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( | ||||
|             new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer", true), | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <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} | ||||
|     <div class="flex flex-col"> | ||||
|       <Tr t={Translations.t.general.attribution.openMapillary} /> | ||||
|  |  | |||
|  | @ -3,10 +3,9 @@ | |||
|    * Show nearby images which can be clicked | ||||
|    */ | ||||
|   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 { P4CPicture } from "../../Logic/Web/NearbyImagesSearch" | ||||
|   import NearbyImagesSearch from "../../Logic/Web/NearbyImagesSearch" | ||||
|   import LinkableImage from "./LinkableImage.svelte" | ||||
|   import type { Feature } from "geojson" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|  | @ -14,7 +13,7 @@ | |||
|   import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import MapillaryLink from "../BigComponents/MapillaryLink.svelte" | ||||
| 
 | ||||
|   export let tags: UIEventSource<OsmTags> | ||||
|   export let state: SpecialVisualizationState | ||||
|  | @ -25,36 +24,49 @@ | |||
|   export let linkable: boolean = true | ||||
|   export let layer: LayerConfig | ||||
| 
 | ||||
|   let imagesProvider = new NearbyImagesSearch( | ||||
|     { | ||||
|       lon, | ||||
|       lat, | ||||
|       allowSpherical: new UIEventSource<boolean>(false), | ||||
|       blacklist: AllImageProviders.LoadImagesFor(tags), | ||||
|     }, | ||||
|     state.indexedFeatures | ||||
|   ) | ||||
|   let imagesProvider = state.nearbyImageSearcher | ||||
| 
 | ||||
|   let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(loaded => new Set(loaded.map(img => img.url))) | ||||
|   let imageState = imagesProvider.getImagesAround(lon, lat) | ||||
|   let result: Store<P4CPicture[]> = imageState.images.mapD((pics: P4CPicture[]) => pics.filter((p: P4CPicture) => | ||||
|     !loadedImages.data.has(p.pictureUrl)  // We don't show any image which is already linked | ||||
|     && !p.details.isSpherical, | ||||
|   ).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> | ||||
| 
 | ||||
| <div class="flex justify-between"> | ||||
|   <h4> | ||||
|     <Tr t={Translations.t.image.nearby.title} /> | ||||
|   </h4> | ||||
|   <slot name="corner" /> | ||||
| </div> | ||||
| {#if !$allDone} | ||||
|   <Loading /> | ||||
| {:else if $images.length === 0} | ||||
|   <Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" /> | ||||
| {:else} | ||||
|   <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> | ||||
|     {#each $images as image (image.pictureUrl)} | ||||
| <div class="flex flex-col"> | ||||
| 
 | ||||
|   {#if $result.length === 0} | ||||
|     {#if $someLoading} | ||||
|       <div class="flex justify-center m-4"> | ||||
|         <Loading /> | ||||
|       </div> | ||||
|     {:else } | ||||
|       <Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" /> | ||||
|     {/if} | ||||
|   {:else} | ||||
|     <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> | ||||
|       {#each $result as image (image.pictureUrl)} | ||||
|       <span class="w-fit shrink-0" style="scroll-snap-align: start"> | ||||
|         <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> | ||||
|       </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> | ||||
| {/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -97,6 +97,7 @@ | |||
| 
 | ||||
| 
 | ||||
|   onDestroy(async () => { | ||||
| <<<<<<< HEAD | ||||
|     await Utils.waitFor(100) | ||||
|     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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSo | |||
| import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" | ||||
| import { Map as MlMap } from "maplibre-gl" | ||||
| import ShowDataLayer from "./Map/ShowDataLayer" | ||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||
| 
 | ||||
| /** | ||||
|  * The state needed to render a special Visualisation. | ||||
|  | @ -92,6 +93,7 @@ export interface SpecialVisualizationState { | |||
|     readonly imageUploadManager: ImageUploadManager | ||||
| 
 | ||||
|     readonly previewedImage: UIEventSource<ProvidedImage> | ||||
|     readonly nearbyImageSearcher: CombinedFetcher | ||||
|     readonly geolocation: GeoLocationHandler | ||||
| 
 | ||||
|     showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer | ||||
|  |  | |||
|  | @ -59,7 +59,6 @@ import { Imgur } from "../Logic/ImageProviders/Imgur" | |||
| import Constants from "../Models/Constants" | ||||
| import { MangroveReviews } from "mangrove-reviews-typescript" | ||||
| import Wikipedia from "../Logic/Web/Wikipedia" | ||||
| import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch" | ||||
| import AllReviews from "./Reviews/AllReviews.svelte" | ||||
| import StarsBarIcon from "./Reviews/StarsBarIcon.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 Trash from "@babeard/svelte-heroicons/mini/Trash" | ||||
| import NothingKnown from "./Popup/NothingKnown.svelte" | ||||
| import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" | ||||
| 
 | ||||
| class NearbyImageVis implements SpecialVisualization { | ||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||
|  | @ -116,7 +116,7 @@ class NearbyImageVis implements SpecialVisualization { | |||
|     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" | ||||
|     funcName = "nearby_images" | ||||
|     needsUrls = NearbyImagesSearch.apiUrls | ||||
|     needsUrls = CombinedFetcher.apiUrls | ||||
|     svelteBased = true | ||||
| 
 | ||||
|     constr( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue