chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2024-10-19 14:44:55 +02:00
parent c9ce29f206
commit 40e894df8b
294 changed files with 14209 additions and 4192 deletions

View file

@ -34,10 +34,10 @@ export default class AllImageProviders {
AllImageProviders.genericImageProvider,
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()),
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
)
public static defaultKeys = [].concat(
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes),
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
)
private static providersByName = {
imgur: Imgur.singleton,
@ -71,13 +71,12 @@ export default class AllImageProviders {
*/
public static LoadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[],
tagKey?: string[]
): Store<ProvidedImage[]> {
if (tags?.data?.id === undefined) {
return undefined
}
const source = new UIEventSource([])
const allSources: Store<ProvidedImage[]>[] = []
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
@ -86,11 +85,11 @@ export default class AllImageProviders {
However, we override them if a custom image tag is set, e.g. 'image:menu'
*/
const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes
const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes))
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
allSources.push(singleSource)
singleSource.addCallbackAndRunD((_) => {
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url)
const dedup = Utils.DedupOnId(all, (i) => i?.id ?? i?.url)
source.set(dedup)
})
}
@ -103,7 +102,7 @@ export default class AllImageProviders {
*/
public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> {
const tags = {
id:"na"
id: "na",
}
for (let i = 0; i < urls.length; i++) {
const url = urls[i]

View file

@ -27,12 +27,14 @@ export default class GenericImageProvider extends ImageProvider {
return undefined
}
return [{
key: key,
url: value,
provider: this,
id: value,
}]
return [
{
key: key,
url: value,
provider: this,
id: value,
},
]
}
SourceIcon() {

View file

@ -9,15 +9,15 @@ export interface ProvidedImage {
key: string
provider: ImageProvider
id: string
date?: Date,
date?: Date
status?: string | "ready"
/**
* Compass angle of the taken image
* 0 = north, 90° = East
*/
rotation?: number
lat?: number,
lon?: number,
lat?: number
lon?: number
host?: string
}
@ -26,8 +26,10 @@ export default abstract class ImageProvider {
public abstract readonly name: string
public abstract SourceIcon(img?: {id: string, url: string, host?: string}, location?: { lon: number; lat: number }): BaseUIElement
public abstract SourceIcon(
img?: { id: string; url: string; host?: string },
location?: { lon: number; lat: number }
): BaseUIElement
/**
* Gets all the relevant URLS for the given tags and for the given prefixes;
@ -35,12 +37,19 @@ export default abstract class ImageProvider {
* @param tags
* @param prefixes
*/
public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> {
public async getRelevantUrlsFor(
tags: Record<string, string>,
prefixes: string[]
): Promise<ProvidedImage[]> {
const relevantUrls: ProvidedImage[] = []
const seenValues = new Set<string>()
for (const key in tags) {
if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) {
if (
!prefixes.some(
(prefix) => key === prefix || key.match(new RegExp(prefix + ":[0-9]+"))
)
) {
continue
}
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
@ -50,10 +59,10 @@ export default abstract class ImageProvider {
}
seenValues.add(value)
let images = this.ExtractUrls(key, value)
if(!Array.isArray(images)){
images = await images
if (!Array.isArray(images)) {
images = await images
}
if(images){
if (images) {
relevantUrls.push(...images)
}
}
@ -61,12 +70,17 @@ export default abstract class ImageProvider {
return relevantUrls
}
public getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
public getRelevantUrls(
tags: Record<string, string>,
prefixes: string[]
): Store<ProvidedImage[]> {
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
}
public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
public abstract ExtractUrls(
key: string,
value: string
): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
public abstract DownloadAttribution(providedImage: {
url: string

View file

@ -28,7 +28,10 @@ export class ImageUploadManager {
private readonly _osmConnection: OsmConnection
private readonly _changes: Changes
public readonly isUploading: Store<boolean>
private readonly _reportError: (message: (string | Error | XMLHttpRequest), extramessage?: string) => Promise<void>
private readonly _reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
constructor(
layout: ThemeConfig,
@ -38,7 +41,10 @@ export class ImageUploadManager {
changes: Changes,
gpsLocation: Store<GeolocationCoordinates | undefined>,
allFeatures: IndexedFeatureSource,
reportError: (message: string | Error | XMLHttpRequest, extramessage?: string ) => Promise<void>
reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
) {
this._uploader = uploader
this._featureProperties = featureProperties
@ -56,7 +62,7 @@ export class ImageUploadManager {
(startedCount) => {
return startedCount > failed.data + done.data
},
[failed, done],
[failed, done]
)
}
@ -105,7 +111,7 @@ export class ImageUploadManager {
file: File,
tagsStore: UIEventSource<OsmTags>,
targetKey: string,
noblur: boolean,
noblur: boolean
): Promise<void> {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
@ -130,10 +136,16 @@ export class ImageUploadManager {
}
const properties = this._featureProperties.getStore(featureId)
const action = new LinkImageAction(featureId, uploadResult. key, uploadResult . value, properties, {
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
changeType: "add-image",
})
const action = new LinkImageAction(
featureId,
uploadResult.key,
uploadResult.value,
properties,
{
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
changeType: "add-image",
}
)
await this._changes.applyAction(action)
}
@ -153,34 +165,51 @@ export class ImageUploadManager {
if (this._gps.data) {
location = [this._gps.data.longitude, this._gps.data.latitude]
}
if (location === undefined || location?.some(l => l === undefined)) {
if (location === undefined || location?.some((l) => l === undefined)) {
const feature = this._indexedFeatures.featuresById.data.get(featureId)
location = GeoOperations.centerpointCoordinates(feature)
}
try {
({ key, value, absoluteUrl } = await this._uploader.uploadImage(blob, location, author, noblur))
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
} catch (e) {
this.increaseCountFor(this._uploadRetried, featureId)
console.error("Could not upload image, trying again:", e)
try {
({ key, value , absoluteUrl} = await this._uploader.uploadImage(blob, location, author, noblur))
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
} catch (e) {
console.error("Could again not upload image due to", e)
this.increaseCountFor(this._uploadFailed, featureId)
await this._reportError(e, JSON.stringify({ctx:"While uploading an image in the Image Upload Manager", featureId, author, targetKey}))
await this._reportError(
e,
JSON.stringify({
ctx: "While uploading an image in the Image Upload Manager",
featureId,
author,
targetKey,
})
)
return undefined
}
}
console.log("Uploading image done, creating action for", featureId)
key = targetKey ?? key
if(targetKey){
if (targetKey) {
// This is a non-standard key, so we use the image link directly
value = absoluteUrl
}
this.increaseCountFor(this._uploadFinished, featureId)
return {key, absoluteUrl, value}
return { key, absoluteUrl, value }
}
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {

View file

@ -6,10 +6,14 @@ export interface ImageUploader {
*/
uploadImage(
blob: File,
currentGps: [number,number],
currentGps: [number, number],
author: string,
noblur: boolean
): Promise<UploadResult>
}
export interface UploadResult{ key: string; value: string, absoluteUrl: string }
export interface UploadResult {
key: string
value: string
absoluteUrl: string
}

View file

@ -19,7 +19,6 @@ export class Imgur extends ImageProvider {
return [Imgur.apiUrl]
}
SourceIcon(): BaseUIElement {
return undefined
}
@ -32,7 +31,7 @@ export class Imgur extends ImageProvider {
key: key,
provider: this,
id: value,
}
},
]
}
return undefined

View file

@ -118,7 +118,7 @@ export class Mapillary extends ImageProvider {
}
SourceIcon(
img: {id: string, url: string},
img: { id: string; url: string },
location?: {
lon: number
lat: number
@ -182,7 +182,7 @@ export class Mapillary extends ImageProvider {
key,
rotation,
lat: geometry.coordinates[1],
lon: geometry.coordinates[0]
lon: geometry.coordinates[0],
}
}
}

View file

@ -11,27 +11,35 @@ import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import Link from "../../UI/Base/Link"
export default class PanoramaxImageProvider extends ImageProvider {
public static readonly singleton = new PanoramaxImageProvider()
private static readonly xyz = new PanoramaxXYZ()
private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token)
private static defaultPanoramax = new AuthorizedPanoramax(
Constants.panoramax.url,
Constants.panoramax.token
)
public defaultKeyPrefixes: string[] = ["panoramax"]
public readonly name: string = "panoramax"
private static knownMeta: Record<string, { data: ImageData, time: Date }> = {}
private static knownMeta: Record<string, { data: ImageData; time: Date }> = {}
public SourceIcon(img?: { id: string, url: string, host?: string }, location?: {
lon: number;
lat: number;
}): BaseUIElement {
public SourceIcon(
img?: { id: string; url: string; host?: string },
location?: {
lon: number
lat: number
}
): BaseUIElement {
const p = new Panoramax(img.host)
return new Link(new SvelteUIElement(Panoramax_bw), p.createViewLink({
imageId: img?.id,
location,
}), true)
return new Link(
new SvelteUIElement(Panoramax_bw),
p.createViewLink({
imageId: img?.id,
location,
}),
true
)
}
public addKnownMeta(meta: ImageData) {
@ -43,25 +51,24 @@ export default class PanoramaxImageProvider extends ImageProvider {
* @param id
* @private
*/
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData; url: string }> {
const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
const url = `https://panoramax.mapcomplete.org/`
const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id, sequence)
return { url, data }
}
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData; url: string }> {
const data = await PanoramaxImageProvider.xyz.imageInfo(imageId)
return { data, url: "https://api.panoramax.xyz/" }
}
/**
* Reads a geovisio-somewhat-looking-like-geojson object and converts it to a provided image
* @param meta
* @private
*/
private featureToImage(info: { data: ImageData, url: string }) {
private featureToImage(info: { data: ImageData; url: string }) {
const meta = info?.data
if (!meta) {
return undefined
@ -82,8 +89,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
id: meta.id,
url: makeAbsolute(meta.assets.sd.href),
url_hd: makeAbsolute(meta.assets.hd.href),
host: meta["links"].find(l => l.rel === "root")?.href,
lon, lat,
host: meta["links"].find((l) => l.rel === "root")?.href,
lon,
lat,
key: "panoramax",
provider: this,
status: meta.properties["geovisio:status"],
@ -92,14 +100,13 @@ export default class PanoramaxImageProvider extends ImageProvider {
}
}
private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFor(id: string): Promise<{ data: ImageData; url: string }> {
if (!id.match(/^[a-zA-Z0-9-]+$/)) {
return undefined
}
const cached = PanoramaxImageProvider.knownMeta[id]
if (cached) {
if (new Date().getTime() - cached.time.getTime() < 1000) {
return { data: cached.data, url: undefined }
}
}
@ -120,10 +127,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
if (!Panoramax.isId(value)) {
return undefined
}
return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))]
return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))]
}
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
@ -131,14 +137,15 @@ export default class PanoramaxImageProvider extends ImageProvider {
if (data === undefined) {
return true
}
return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken")
return data?.some(
(img) =>
img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken"
)
}
Stores.Chronic(1500, () =>
hasLoading(source.data),
).addCallback(_ => {
Stores.Chronic(1500, () => hasLoading(source.data)).addCallback((_) => {
console.log("UPdating... ")
super.getRelevantUrlsFor(tags, prefixes).then(data => {
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
console.log("New panoramax data is", data, hasLoading(data))
source.set(data)
return !hasLoading(data)
@ -148,7 +155,10 @@ export default class PanoramaxImageProvider extends ImageProvider {
return source
}
public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> {
public async DownloadAttribution(providedImage: {
url: string
id: string
}): Promise<LicenseInfo> {
const meta = await this.getInfoFor(providedImage.id)
return {
@ -171,9 +181,14 @@ export class PanoramaxUploader implements ImageUploader {
this._panoramax = new AuthorizedPanoramax(url, token)
}
async uploadImage(blob: File, currentGps: [number, number], author: string, noblur: boolean = false): Promise<{
key: string;
value: string;
async uploadImage(
blob: File,
currentGps: [number, number],
author: string,
noblur: boolean = false
): Promise<{
key: string
value: string
absoluteUrl: string
}> {
// https://panoramax.openstreetmap.fr/api/docs/swagger#/
@ -183,7 +198,7 @@ export class PanoramaxUploader implements ImageUploader {
let hasGPS = false
try {
const tags = await ExifReader.load(blob)
hasDate = tags?.DateTime !== undefined
hasDate = tags?.DateTime !== undefined
hasGPS = tags?.GPSLatitude !== undefined && tags?.GPSLongitude !== undefined
} catch (e) {
console.error("Could not read EXIF-tags")
@ -203,7 +218,6 @@ export class PanoramaxUploader implements ImageUploader {
exifOverride: {
Artist: author,
},
})
PanoramaxImageProvider.singleton.addKnownMeta(img)
return {
@ -212,5 +226,4 @@ export class PanoramaxUploader implements ImageUploader {
absoluteUrl: img.assets.hd.href,
}
}
}

View file

@ -11,8 +11,10 @@ export class WikidataImageProvider extends ImageProvider {
public static readonly singleton = new WikidataImageProvider()
public readonly defaultKeyPrefixes = ["wikidata"]
public readonly name = "Wikidata"
private static readonly keyBlacklist: ReadonlySet<string> = new Set(
["mapillary", ...Utils.Times(i => "mapillary:" + i, 10)])
private static readonly keyBlacklist: ReadonlySet<string> = new Set([
"mapillary",
...Utils.Times((i) => "mapillary:" + i, 10),
])
private constructor() {
super()
@ -26,7 +28,7 @@ export class WikidataImageProvider extends ImageProvider {
return new SvelteUIElement(Wikidata_icon)
}
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
if (WikidataImageProvider.keyBlacklist.has(key)) {
return undefined
}

View file

@ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider {
return value
}
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
value,
value
)}`
if (useHd) {
return baseUrl
@ -106,7 +106,8 @@ export class WikimediaImageProvider extends ImageProvider {
value = WikimediaImageProvider.removeCommonsPrefix(value)
if (value.startsWith("Category:")) {
const urls = await Wikimedia.GetCategoryContents(value)
return urls.filter((url) => url.startsWith("File:"))
return urls
.filter((url) => url.startsWith("File:"))
.map((image) => this.UrlForImage(image))
}
if (value.startsWith("File:")) {
@ -117,7 +118,7 @@ export class WikimediaImageProvider extends ImageProvider {
return undefined
}
// We do a last effort and assume this is a file
return [(this.UrlForImage("File:" + value))]
return [this.UrlForImage("File:" + value)]
}
public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> {
@ -147,7 +148,7 @@ export class WikimediaImageProvider extends ImageProvider {
console.warn(
"The file",
filename,
"has no usable metedata or license attached... Please fix the license info file yourself!",
"has no usable metedata or license attached... Please fix the license info file yourself!"
)
return undefined
}