diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts index ed66dcd51..f74111d57 100644 --- a/src/Logic/FeatureSource/Sources/MvtSource.ts +++ b/src/Logic/FeatureSource/Sources/MvtSource.ts @@ -135,12 +135,12 @@ class MvtFeatureBuilder { private encodeGeometry(geometry: number[]): Coords[] { let cX = 0 let cY = 0 - let coordss: Coords[] = [] + const coordss: Coords[] = [] let currentRing: Coords = [] for (let i = 0; i < geometry.length; i++) { - let commandInteger = geometry[i] - let commandId = commandInteger & 0x7 - let commandCount = commandInteger >> 3 + const commandInteger = geometry[i] + const commandId = commandInteger & 0x7 + const commandCount = commandInteger >> 3 /* Command Id Parameters Parameter Count MoveTo 1 dX, dY 2 @@ -165,7 +165,11 @@ class MvtFeatureBuilder { i += commandCount * 2 } if (commandId === 7) { - currentRing.push([...currentRing[0]]) + if(currentRing.length === 0){ + console.error("Invalid MVT file: got a 'closePath', but the currentRing is empty. Full command:", commandInteger) + }else{ + currentRing.push([...currentRing[0]]) + } i++ } } @@ -434,7 +438,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature } this._features.setData(features) } catch (e) { - console.error("Could not download MVT tile due to", e) + console.error("Could not download MVT "+this._url+" tile due to", e) } } diff --git a/src/Logic/ImageProviders/AllImageProviders.ts b/src/Logic/ImageProviders/AllImageProviders.ts index 1e5dcd2f5..8957c945d 100644 --- a/src/Logic/ImageProviders/AllImageProviders.ts +++ b/src/Logic/ImageProviders/AllImageProviders.ts @@ -12,19 +12,23 @@ import { WikidataImageProvider } from "./WikidataImageProvider" export default class AllImageProviders { private static dontLoadFromPrefixes = ["https://photos.app.goo.gl/"] - public static ImageAttributionSource: ImageProvider[] = [ + /** + * The 'genericImageProvider' is a fallback that scans various other tags for tags, unless the URL starts with one of the given prefixes + */ + public static genericImageProvider = new GenericImageProvider([ + ...Imgur.defaultValuePrefix, + ...WikimediaImageProvider.commonsPrefixes, + ...Mapillary.valuePrefixes, + ...AllImageProviders.dontLoadFromPrefixes, + "Category:", + ]) + + private static ImageAttributionSource: ImageProvider[] = [ Imgur.singleton, Mapillary.singleton, WikidataImageProvider.singleton, WikimediaImageProvider.singleton, - // The 'genericImageProvider' is a fallback that scans various other tags for tags, unless the URL starts with one of the given prefixes - new GenericImageProvider([ - ...Imgur.defaultValuePrefix, - ...WikimediaImageProvider.commonsPrefixes, - ...Mapillary.valuePrefixes, - ...AllImageProviders.dontLoadFromPrefixes, - "Category:", - ]), + AllImageProviders.genericImageProvider ] public static apiUrls: string[] = [].concat( ...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()) @@ -47,6 +51,23 @@ export default class AllImageProviders { return AllImageProviders.providersByName[name.toLowerCase()] } + public static async selectBestProvider(key: string, value: string): Promise { + + for (const imageProvider of AllImageProviders.ImageAttributionSource) { + try{ + + const extracted = await Promise.all(await imageProvider.ExtractUrls(key, value)) + if(extracted?.length > 0){ + return imageProvider + } + }catch (e) { + console.warn("Provider gave an error while trying to determine a match:", e) + } + } + + return AllImageProviders.genericImageProvider + } + public static LoadImagesFor( tags: Store>, tagKey?: string[] @@ -63,7 +84,7 @@ export default class AllImageProviders { const source = new UIEventSource([]) this._cache.set(cacheKey, source) - const allSources = [] + const allSources: Store[] = [] for (const imageProvider of AllImageProviders.ImageAttributionSource) { let prefixes = imageProvider.defaultKeyPrefixes if (tagKey !== undefined) { diff --git a/src/Logic/ImageProviders/GenericImageProvider.ts b/src/Logic/ImageProviders/GenericImageProvider.ts index c5c1ccc1f..f3b02b8d4 100644 --- a/src/Logic/ImageProviders/GenericImageProvider.ts +++ b/src/Logic/ImageProviders/GenericImageProvider.ts @@ -2,6 +2,7 @@ import ImageProvider, { ProvidedImage } from "./ImageProvider" export default class GenericImageProvider extends ImageProvider { public defaultKeyPrefixes: string[] = ["image"] + public readonly name = "Generic" public apiUrls(): string[] { return [] diff --git a/src/Logic/ImageProviders/ImageProvider.ts b/src/Logic/ImageProviders/ImageProvider.ts index 613eda941..ebc600af6 100644 --- a/src/Logic/ImageProviders/ImageProvider.ts +++ b/src/Logic/ImageProviders/ImageProvider.ts @@ -15,6 +15,8 @@ export interface ProvidedImage { export default abstract class ImageProvider { public abstract readonly defaultKeyPrefixes: string[] + public abstract readonly name: string + public abstract SourceIcon(id?: string, location?: { lon: number; lat: number }): BaseUIElement /** diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index dc8ae1369..0a0c2abfb 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -8,6 +8,7 @@ import { ImageUploader } from "./ImageUploader" export class Imgur extends ImageProvider implements ImageUploader { public static readonly defaultValuePrefix = ["https://i.imgur.com"] public static readonly singleton = new Imgur() + public readonly name = "Imgur" public readonly defaultKeyPrefixes: string[] = ["image"] public readonly maxFileSizeInMegabytes = 10 public static readonly apiUrl = "https://api.imgur.com/3/image" diff --git a/src/Logic/ImageProviders/Mapillary.ts b/src/Logic/ImageProviders/Mapillary.ts index 62a6c465d..d753a88f6 100644 --- a/src/Logic/ImageProviders/Mapillary.ts +++ b/src/Logic/ImageProviders/Mapillary.ts @@ -8,6 +8,8 @@ import MapillaryIcon from "./MapillaryIcon.svelte" export class Mapillary extends ImageProvider { public static readonly singleton = new Mapillary() + public readonly name = "Mapillary" + private static readonly valuePrefix = "https://a.mapillary.com" public static readonly valuePrefixes = [ Mapillary.valuePrefix, diff --git a/src/Logic/ImageProviders/WikidataImageProvider.ts b/src/Logic/ImageProviders/WikidataImageProvider.ts index 27b3ca934..ab5017c73 100644 --- a/src/Logic/ImageProviders/WikidataImageProvider.ts +++ b/src/Logic/ImageProviders/WikidataImageProvider.ts @@ -8,6 +8,7 @@ import * as Wikidata_icon from "../../assets/svg/Wikidata.svelte" export class WikidataImageProvider extends ImageProvider { public static readonly singleton = new WikidataImageProvider() public readonly defaultKeyPrefixes = ["wikidata"] + public readonly name = "Wikidata" private constructor() { super() diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index 5c969dbce..8c403f42e 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -18,6 +18,7 @@ export class WikimediaImageProvider extends ImageProvider { public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"] private readonly commons_key = "wikimedia_commons" public readonly defaultKeyPrefixes = [this.commons_key, "image"] + public readonly name = "Wikimedia" private constructor() { super() @@ -133,9 +134,9 @@ export class WikimediaImageProvider extends ImageProvider { "titles=" + filename + "&format=json&origin=*" - const data = await Utils.downloadJsonCached(url, 365 * 24 * 60 * 60) + const data = await Utils.downloadJsonCached<{query: {pages: {title: string, imageinfo: { extmetadata} []}[]}}>(url, 365 * 24 * 60 * 60) const licenseInfo = new LicenseInfo() - const pageInfo = data.query.pages[-1] + const pageInfo = data.query.pages.at(-1) if (pageInfo === undefined) { return undefined } diff --git a/src/Logic/Web/NearbyImagesSearch.ts b/src/Logic/Web/NearbyImagesSearch.ts index c8b58cb1f..077299a83 100644 --- a/src/Logic/Web/NearbyImagesSearch.ts +++ b/src/Logic/Web/NearbyImagesSearch.ts @@ -7,6 +7,9 @@ import { BBox } from "../BBox" import Constants from "../../Models/Constants" import { Utils } from "../../Utils" import { Point } from "geojson" +import MvtSource from "../FeatureSource/Sources/MvtSource" +import AllImageProviders from "../ImageProviders/AllImageProviders" +import { contains } from "@rgossiaux/svelte-headlessui/internal/dom-containers" interface ImageFetcher { /** @@ -101,9 +104,9 @@ class P4CImageFetcher implements ImageFetcher { searchRadius, { mindate: new Date().getTime() - maxAgeSeconds, - towardscenter: false, + towardscenter: false - }, + } ) } catch (e) { console.log("P4C image fetcher failed with", e) @@ -114,7 +117,7 @@ class P4CImageFetcher implements ImageFetcher { } /** - * Extracts pictures from currently loaded features + * Extracts pictures from features which are currently loaded on the local machine, probably features of the same layer */ class ImagesInLoadedDataFetcher implements ImageFetcher { private indexedFeatures: IndexedFeatureSource @@ -154,9 +157,9 @@ class ImagesInLoadedDataFetcher implements ImageFetcher { coordinates: { lng: centerpoint[0], lat: centerpoint[1] }, provider: "OpenStreetMap", details: { - isSpherical: false, + isSpherical: false }, - osmTags: { image }, + osmTags: { image } }) } }) @@ -165,6 +168,92 @@ class ImagesInLoadedDataFetcher implements ImageFetcher { } } +class ImagesFromCacheServerFetcher implements ImageFetcher { + private readonly _searchRadius: number + public readonly name = "fromCacheServer" + private readonly _serverUrl: string + + constructor(searchRadius: number = 500, serverUrl: string = Constants.VectorTileServer) { + this._searchRadius = searchRadius + this._serverUrl = serverUrl + } + + async fetchImages(lat: number, lon: number): Promise { + return (await Promise.all([ + this.fetchImagesForType(lat, lon, "lines"), + this.fetchImagesForType(lat, lon, "pois"), + this.fetchImagesForType(lat, lon, "polygons") + + ])).flatMap(x => x) + } + + async fetchImagesForType(targetlat: number, targetlon: number, type: "lines" | "pois" | "polygons"): Promise { + const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14) + + const url = this._serverUrl + + async function getFeatures(x: number, y: number) { + const src = new MvtSource(Utils.SubstituteKeys(url, { + type, x, y, z, layer: "item_with_image" + }), x, y, z) + await src.updateAsync() + return src.features.data + } + + const features = (await Promise.all([ + getFeatures(x, y), + getFeatures(x, y + 1), + getFeatures(x, y - 1), + + getFeatures(x + 1, y + 1), + getFeatures(x + 1, y), + getFeatures(x + 1, y - 1), + + getFeatures(x - 1, y - 1), + getFeatures(x - 1, y), + getFeatures(x - 1, y + 1) + ])).flatMap(x => x) + + const pics: P4CPicture[] = [] + for (const f of features) { + + const [lng, lat] = GeoOperations.centerpointCoordinates(f) + if (GeoOperations.distanceBetween([targetlon, targetlat], [lng, lat]) > this._searchRadius) { + return [] + } + for (let i = -1; i < 50; i++) { + let key = "image" + if (i >= 0) { + key += ":" + i + } + const v = f.properties[key] + console.log(v) + if (!v) { + continue + } + let provider = "unkown" + try { + provider = (await AllImageProviders.selectBestProvider("image", v))?.name + } catch (e) { + console.error("Could not detect provider for", "image", v) + } + pics.push({ + pictureUrl: v, + coordinates: { lat, lng }, + details: { + isSpherical: false + }, + osmTags: { + image: v + }, + thumbUrl: v, + provider + }) + } + } + return pics + } +} class MapillaryFetcher implements ImageFetcher { @@ -201,21 +290,29 @@ class MapillaryFetcher implements ImageFetcher { url += "&is_pano=true" } if (this.start_captured_at) { - url += "&start_captured_at="+ this.start_captured_at?.toISOString() + url += "&start_captured_at=" + this.start_captured_at?.toISOString() } if (this.end_captured_at) { - url += "&end_captured_at="+ this.end_captured_at?.toISOString() + 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 }[] + 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 - if(img.thumb_original_url === undefined){ + if (img.thumb_original_url === undefined) { continue } pics.push({ @@ -224,11 +321,11 @@ class MapillaryFetcher implements ImageFetcher { coordinates: { lng: c[0], lat: c[1] }, thumbUrl: img.thumb_256_url, osmTags: { - "mapillary":img.id + "mapillary": img.id }, details: { - isSpherical: img.is_pano, - }, + isSpherical: img.is_pano + } }) } return pics @@ -244,58 +341,62 @@ export class CombinedFetcher { 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"), + // new ImagesInLoadedDataFetcher(indexedFeatures, radius), + new ImagesFromCacheServerFetcher(radius) + /* new MapillaryFetcher({ + panoramas: "no", + max_images: 25, + start_captured_at : maxage + }), + new P4CImageFetcher("mapillary"), + new P4CImageFetcher("wikicommons"), //*/ ].map(f => new CachedFetcher(f)) } + private async fetchImage(source: CachedFetcher, lat: number, lon: number, state: UIEventSource>, sink: UIEventSource): Promise { + try { + + const pics = await source.fetchImages(lat, lon) + console.log(source.name, "==>>", pics) + state.data[source.name] = "done" + state.ping() + + + if (sink.data === undefined) { + sink.setData(pics) + } else { + const newList = [] + const seenIds = new Set() + 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() + } + } + public getImagesAround(lon: number, lat: number): { images: Store, state: Store> } { - const src = new UIEventSource([]) + const sink = new UIEventSource([]) const state = new UIEventSource>({}) 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 = [] - const seenIds = new Set() - for (const p4CPicture of [...src.data, ...pics]) { - const id = p4CPicture.pictureUrl - if(seenIds.has(id)){ - continue - } - newList.push(p4CPicture) - seenIds.add(id) - if(id === undefined){ - - console.log("Img:", p4CPicture) - } - } - 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() - }) + this.fetchImage(source, lat, lon, state, sink) } - return { images: src, state } + return { images: sink, state } } } diff --git a/src/UI/BigComponents/ThemeButton.svelte b/src/UI/BigComponents/ThemeButton.svelte index 1dead5e3b..6ff150d41 100644 --- a/src/UI/BigComponents/ThemeButton.svelte +++ b/src/UI/BigComponents/ThemeButton.svelte @@ -28,11 +28,11 @@ return true }) - $: title = new Translation( + $: title = Translations.T( theme.title, !isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined ) - $: description = new Translation(theme.shortDescription) + $: description = Translations.T(theme.shortDescription) // TODO: Improve this function function createUrl(