Load nearby image and image:* images from server for the nearby images
This commit is contained in:
parent
9a4572cdfa
commit
caa2e18a03
10 changed files with 206 additions and 72 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ImageProvider> {
|
||||
|
||||
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<Record<string, string>>,
|
||||
tagKey?: string[]
|
||||
|
@ -63,7 +84,7 @@ export default class AllImageProviders {
|
|||
|
||||
const source = new UIEventSource([])
|
||||
this._cache.set(cacheKey, source)
|
||||
const allSources = []
|
||||
const allSources: Store<ProvidedImage[]>[] = []
|
||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||
let prefixes = imageProvider.defaultKeyPrefixes
|
||||
if (tagKey !== undefined) {
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<P4CPicture[]> {
|
||||
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<P4CPicture[]> {
|
||||
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<Record<string, "loading" | "done" | "error">>, sink: UIEventSource<P4CPicture[]>): Promise<void> {
|
||||
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<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()
|
||||
}
|
||||
}
|
||||
|
||||
public getImagesAround(lon: number, lat: number): {
|
||||
images: Store<P4CPicture[]>,
|
||||
state: Store<Record<string, "loading" | "done" | "error">>
|
||||
} {
|
||||
const src = new UIEventSource<P4CPicture[]>([])
|
||||
const sink = 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 = []
|
||||
const seenIds = new Set<string>()
|
||||
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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue