MapComplete/src/Logic/ImageProviders/ImageUploadManager.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

236 lines
8.6 KiB
TypeScript
Raw Normal View History

import { ImageUploader, UploadResult } from "./ImageUploader"
2023-09-28 23:50:27 +02:00
import LinkImageAction from "../Osm/Actions/LinkImageAction"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { OsmId, OsmTags } from "../../Models/OsmFeature"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
2023-09-28 23:50:27 +02:00
import { Store, UIEventSource } from "../UIEventSource"
import { OsmConnection } from "../Osm/OsmConnection"
import { Changes } from "../Osm/Changes"
import Translations from "../../UI/i18n/Translations"
import { Translation } from "../../UI/i18n/Translation"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoOperations } from "../GeoOperations"
/**
* The ImageUploadManager has a
*/
export class ImageUploadManager {
2023-09-28 23:50:27 +02:00
private readonly _uploader: ImageUploader
private readonly _featureProperties: FeaturePropertiesStore
private readonly _theme: ThemeConfig
private readonly _indexedFeatures: IndexedFeatureSource
private readonly _gps: Store<GeolocationCoordinates | undefined>
2023-09-28 23:50:27 +02:00
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map()
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map()
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map()
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map()
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map()
private readonly _osmConnection: OsmConnection
private readonly _changes: Changes
public readonly isUploading: Store<boolean>
2024-10-19 14:44:55 +02:00
private readonly _reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
2023-09-28 23:50:27 +02:00
constructor(
layout: ThemeConfig,
2023-09-28 23:50:27 +02:00
uploader: ImageUploader,
featureProperties: FeaturePropertiesStore,
osmConnection: OsmConnection,
changes: Changes,
gpsLocation: Store<GeolocationCoordinates | undefined>,
allFeatures: IndexedFeatureSource,
2024-10-19 14:44:55 +02:00
reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
2023-09-28 23:50:27 +02:00
) {
this._uploader = uploader
this._featureProperties = featureProperties
this._theme = layout
2023-09-28 23:50:27 +02:00
this._osmConnection = osmConnection
this._changes = changes
this._indexedFeatures = allFeatures
this._gps = gpsLocation
this._reportError = reportError
const failed = this.getCounterFor(this._uploadFailed, "*")
const done = this.getCounterFor(this._uploadFinished, "*")
2024-04-13 02:40:21 +02:00
this.isUploading = this.getCounterFor(this._uploadStarted, "*").map(
(startedCount) => {
return startedCount > failed.data + done.data
},
2024-10-19 14:44:55 +02:00
[failed, done]
2024-04-13 02:40:21 +02:00
)
}
2023-09-28 23:50:27 +02:00
/**
* Gets various counters.
* Note that counters can only increase
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
* @param featureId the id of the feature you want information for. '*' has a global counter
2023-09-28 23:50:27 +02:00
*/
public getCountsFor(featureId: string | "*"): {
retried: Store<number>
uploadStarted: Store<number>
retrySuccess: Store<number>
failed: Store<number>
uploadFinished: Store<number>
} {
return {
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
retried: this.getCounterFor(this._uploadRetried, featureId),
failed: this.getCounterFor(this._uploadFailed, featureId),
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId),
}
}
2024-06-16 16:06:26 +02:00
public canBeUploaded(file: File): true | { error: Translation } {
const sizeInBytes = file.size
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
2024-06-16 16:06:26 +02:00
const error = Translations.t.image.toBig.Subs({
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
max_size: this._uploader.maxFileSizeInMegabytes + "MB",
})
2024-06-16 16:06:26 +02:00
return { error }
}
return true
}
2023-09-28 23:50:27 +02:00
/**
* Uploads the given image, applies the correct title and license for the known user.
* Will then add this image to the OSM-feature or the OSM-note
* @param file a jpg file to upload
* @param tagsStore The tags of the feature
* @param targetKey Use this key to save the attribute under. Default: 'image'
2023-09-28 23:50:27 +02:00
*/
2023-10-30 13:44:27 +01:00
public async uploadImageAndApply(
file: File,
tagsStore: UIEventSource<OsmTags>,
targetKey: string,
2024-10-19 14:44:55 +02:00
noblur: boolean
2023-10-30 13:44:27 +01:00
): Promise<void> {
const canBeUploaded = this.canBeUploaded(file)
2024-06-16 16:06:26 +02:00
if (canBeUploaded !== true) {
throw canBeUploaded.error
2023-09-28 23:50:27 +02:00
}
const tags = tagsStore.data
const featureId = <OsmId>tags.id
2023-09-28 23:50:27 +02:00
const author = this._osmConnection.userDetails.data.name
2023-09-28 23:50:27 +02:00
const uploadResult = await this.uploadImageWithLicense(
2023-10-30 13:44:27 +01:00
featureId,
author,
2023-10-30 13:44:27 +01:00
file,
2023-12-03 04:49:28 +01:00
targetKey,
noblur
2023-10-30 13:44:27 +01:00
)
if (!uploadResult) {
2023-09-28 23:50:27 +02:00
return
}
const properties = this._featureProperties.getStore(featureId)
2024-10-19 14:44:55 +02:00
const action = new LinkImageAction(
featureId,
uploadResult.key,
uploadResult.value,
properties,
{
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
changeType: "add-image",
}
)
2023-09-28 23:50:27 +02:00
await this._changes.applyAction(action)
}
public async uploadImageWithLicense(
2024-09-28 12:01:10 +02:00
featureId: string,
author: string,
blob: File,
2023-12-03 04:49:28 +01:00
targetKey: string | undefined,
noblur: boolean
): Promise<UploadResult> {
2023-09-28 23:50:27 +02:00
this.increaseCountFor(this._uploadStarted, featureId)
let key: string
let value: string
let absoluteUrl: string
let location: [number, number] = undefined
if (this._gps.data) {
location = [this._gps.data.longitude, this._gps.data.latitude]
}
2024-10-19 14:44:55 +02:00
if (location === undefined || location?.some((l) => l === undefined)) {
const feature = this._indexedFeatures.featuresById.data.get(featureId)
location = GeoOperations.centerpointCoordinates(feature)
}
2023-09-28 23:50:27 +02:00
try {
2024-10-19 14:44:55 +02:00
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
2023-09-28 23:50:27 +02:00
} catch (e) {
this.increaseCountFor(this._uploadRetried, featureId)
console.error("Could not upload image, trying again:", e)
try {
2024-10-19 14:44:55 +02:00
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
2023-09-28 23:50:27 +02:00
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
} catch (e) {
console.error("Could again not upload image due to", e)
this.increaseCountFor(this._uploadFailed, featureId)
2024-10-19 14:44:55 +02:00
await this._reportError(
e,
JSON.stringify({
ctx: "While uploading an image in the Image Upload Manager",
featureId,
author,
targetKey,
})
)
return undefined
2023-09-28 23:50:27 +02:00
}
}
console.log("Uploading image done, creating action for", featureId)
key = targetKey ?? key
2024-10-19 14:44:55 +02:00
if (targetKey) {
// This is a non-standard key, so we use the image link directly
value = absoluteUrl
}
this.increaseCountFor(this._uploadFinished, featureId)
2024-10-19 14:44:55 +02:00
return { key, absoluteUrl, value }
}
2023-09-28 23:50:27 +02:00
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
if (this._featureProperties.aliases.has(key)) {
key = this._featureProperties.aliases.get(key)
}
if (!collection.has(key)) {
collection.set(key, new UIEventSource<number>(0))
}
return collection.get(key)
}
2023-09-28 23:50:27 +02:00
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
{
const counter = this.getCounterFor(collection, key)
counter.setData(counter.data + 1)
}
{
const global = this.getCounterFor(collection, "*")
global.setData(global.data + 1)
}
2023-09-28 23:50:27 +02:00
}
}