2024-09-28 02:44:03 +02:00
|
|
|
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"
|
2024-10-17 04:06:03 +02:00
|
|
|
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"
|
2024-05-28 01:25:43 +02:00
|
|
|
import { Translation } from "../../UI/i18n/Translation"
|
2024-09-26 19:15:20 +02:00
|
|
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
|
|
|
import { GeoOperations } from "../GeoOperations"
|
2024-10-29 23:53:58 +01:00
|
|
|
import { Feature } from "geojson"
|
2023-09-25 02:13:24 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The ImageUploadManager has a
|
|
|
|
*/
|
|
|
|
export class ImageUploadManager {
|
2023-09-28 23:50:27 +02:00
|
|
|
private readonly _uploader: ImageUploader
|
|
|
|
private readonly _featureProperties: FeaturePropertiesStore
|
2024-10-17 04:06:03 +02:00
|
|
|
private readonly _theme: ThemeConfig
|
2024-09-26 19:15:20 +02:00
|
|
|
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
|
2024-03-28 15:55:12 +01:00
|
|
|
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(
|
2024-10-17 04:06:03 +02:00
|
|
|
layout: ThemeConfig,
|
2023-09-28 23:50:27 +02:00
|
|
|
uploader: ImageUploader,
|
|
|
|
featureProperties: FeaturePropertiesStore,
|
|
|
|
osmConnection: OsmConnection,
|
2024-09-26 19:15:20 +02:00
|
|
|
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
|
2024-10-17 04:06:03 +02:00
|
|
|
this._theme = layout
|
2023-09-28 23:50:27 +02:00
|
|
|
this._osmConnection = osmConnection
|
|
|
|
this._changes = changes
|
2024-09-26 19:15:20 +02:00
|
|
|
this._indexedFeatures = allFeatures
|
|
|
|
this._gps = gpsLocation
|
2024-10-10 23:09:31 +02:00
|
|
|
this._reportError = reportError
|
2024-03-28 15:55:12 +01:00
|
|
|
|
|
|
|
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-25 02:55:43 +02:00
|
|
|
}
|
2023-09-25 02:13:24 +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
|
2024-09-27 03:26:17 +02:00
|
|
|
* @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),
|
2024-12-11 02:45:44 +01:00
|
|
|
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId),
|
2023-09-28 23:50:27 +02:00
|
|
|
}
|
2023-09-25 02:55:43 +02:00
|
|
|
}
|
2023-09-25 02:13:24 +02:00
|
|
|
|
2024-06-16 16:06:26 +02:00
|
|
|
public canBeUploaded(file: File): true | { error: Translation } {
|
2024-05-28 01:25:43 +02:00
|
|
|
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({
|
2024-05-28 01:25:43 +02:00
|
|
|
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
2024-12-11 02:45:44 +01:00
|
|
|
max_size: this._uploader.maxFileSizeInMegabytes + "MB",
|
2024-05-28 01:25:43 +02:00
|
|
|
})
|
2024-06-16 16:06:26 +02:00
|
|
|
return { error }
|
2024-05-28 01:25:43 +02:00
|
|
|
}
|
|
|
|
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
|
2023-10-22 00:51:43 +02:00
|
|
|
* @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'
|
2024-12-13 13:47:47 +01:00
|
|
|
* @param noblur if true, then the api call will indicate that the image is already blurred. The server won't apply blurring in this case
|
|
|
|
* @param feature the feature this image is about. Will be used as fallback to get the GPS-coordinates
|
2023-09-28 23:50:27 +02:00
|
|
|
*/
|
2023-10-30 13:44:27 +01:00
|
|
|
public async uploadImageAndApply(
|
|
|
|
file: File,
|
|
|
|
tagsStore: UIEventSource<OsmTags>,
|
2024-10-12 13:36:10 +02:00
|
|
|
targetKey: string,
|
2024-12-13 13:47:47 +01:00
|
|
|
noblur: boolean,
|
|
|
|
feature: Feature
|
2023-10-30 13:44:27 +01:00
|
|
|
): Promise<void> {
|
2024-05-28 01:25:43 +02:00
|
|
|
const canBeUploaded = this.canBeUploaded(file)
|
2024-06-16 16:06:26 +02:00
|
|
|
if (canBeUploaded !== true) {
|
2024-05-28 01:25:43 +02:00
|
|
|
throw canBeUploaded.error
|
2023-09-28 23:50:27 +02:00
|
|
|
}
|
|
|
|
|
2024-05-28 01:25:43 +02:00
|
|
|
const tags = tagsStore.data
|
2024-09-28 02:44:03 +02:00
|
|
|
|
2024-05-28 01:25:43 +02:00
|
|
|
const featureId = <OsmId>tags.id
|
2023-09-28 23:50:27 +02:00
|
|
|
|
2024-09-26 19:15:20 +02:00
|
|
|
const author = this._osmConnection.userDetails.data.name
|
2023-09-28 23:50:27 +02:00
|
|
|
|
2024-09-28 02:44:03 +02:00
|
|
|
const uploadResult = await this.uploadImageWithLicense(
|
2023-10-30 13:44:27 +01:00
|
|
|
featureId,
|
2024-09-26 19:15:20 +02:00
|
|
|
author,
|
2023-10-30 13:44:27 +01:00
|
|
|
file,
|
2023-12-03 04:49:28 +01:00
|
|
|
targetKey,
|
2024-12-13 13:47:47 +01:00
|
|
|
noblur,
|
|
|
|
feature
|
2023-10-30 13:44:27 +01:00
|
|
|
)
|
2024-09-28 02:44:03 +02:00
|
|
|
if (!uploadResult) {
|
2023-09-28 23:50:27 +02:00
|
|
|
return
|
|
|
|
}
|
2024-09-28 02:44:03 +02:00
|
|
|
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,
|
2024-12-11 02:45:44 +01:00
|
|
|
changeType: "add-image",
|
2024-10-19 14:44:55 +02:00
|
|
|
}
|
|
|
|
)
|
2024-09-28 02:44:03 +02:00
|
|
|
|
2023-09-28 23:50:27 +02:00
|
|
|
await this._changes.applyAction(action)
|
2023-09-25 02:13:24 +02:00
|
|
|
}
|
|
|
|
|
2024-09-28 02:44:03 +02:00
|
|
|
public async uploadImageWithLicense(
|
2024-09-28 12:01:10 +02:00
|
|
|
featureId: string,
|
2024-09-26 19:15:20 +02:00
|
|
|
author: string,
|
2023-10-22 00:51:43 +02:00
|
|
|
blob: File,
|
2023-12-03 04:49:28 +01:00
|
|
|
targetKey: string | undefined,
|
2024-10-29 23:53:58 +01:00
|
|
|
noblur: boolean,
|
2024-12-13 13:47:47 +01:00
|
|
|
feature: Feature,
|
2024-11-25 18:00:30 +01:00
|
|
|
ignoreGps: boolean = false
|
2024-09-28 02:44:03 +02:00
|
|
|
): Promise<UploadResult> {
|
2023-09-28 23:50:27 +02:00
|
|
|
this.increaseCountFor(this._uploadStarted, featureId)
|
|
|
|
let key: string
|
|
|
|
let value: string
|
2024-09-28 02:44:03 +02:00
|
|
|
let absoluteUrl: string
|
2024-09-26 19:15:20 +02:00
|
|
|
let location: [number, number] = undefined
|
2024-11-25 18:00:30 +01:00
|
|
|
if (this._gps.data && !ignoreGps) {
|
2024-09-26 19:15:20 +02:00
|
|
|
location = [this._gps.data.longitude, this._gps.data.latitude]
|
|
|
|
}
|
2024-12-10 02:45:30 +01:00
|
|
|
{
|
2024-10-29 23:53:58 +01:00
|
|
|
feature ??= this._indexedFeatures.featuresById.data.get(featureId)
|
2024-12-10 02:45:30 +01:00
|
|
|
if (feature === undefined) {
|
2024-12-06 01:20:41 +01:00
|
|
|
throw "ImageUploadManager: no feature given and no feature found in the indexedFeature. Cannot upload this image"
|
|
|
|
}
|
2024-12-10 02:45:30 +01:00
|
|
|
const featureCenterpoint = GeoOperations.centerpointCoordinates(feature)
|
2024-12-11 02:45:44 +01:00
|
|
|
if (
|
|
|
|
location === undefined ||
|
|
|
|
location?.some((l) => l === undefined) ||
|
|
|
|
GeoOperations.distanceBetween(location, featureCenterpoint) > 150
|
|
|
|
) {
|
2024-12-10 02:45:30 +01:00
|
|
|
/* GPS location is either unknown or very far away from the photographed location.
|
2024-12-11 02:45:44 +01:00
|
|
|
* Default to the centerpoint
|
|
|
|
*/
|
2024-12-10 02:45:30 +01:00
|
|
|
location = featureCenterpoint
|
|
|
|
}
|
2024-09-26 19:15:20 +02:00
|
|
|
}
|
2023-09-28 23:50:27 +02:00
|
|
|
try {
|
2025-04-05 22:36:25 +02:00
|
|
|
({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
2024-10-19 14:44:55 +02:00
|
|
|
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 {
|
2025-04-05 22:36:25 +02:00
|
|
|
({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
2024-10-19 14:44:55 +02:00
|
|
|
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,
|
2024-12-11 02:45:44 +01:00
|
|
|
targetKey,
|
2024-10-19 14:44:55 +02:00
|
|
|
})
|
|
|
|
)
|
2024-03-11 16:35:15 +01:00
|
|
|
return undefined
|
2023-09-28 23:50:27 +02:00
|
|
|
}
|
|
|
|
}
|
2025-04-05 22:36:25 +02:00
|
|
|
console.log("Uploading image done, creating action for", featureId, " targetkey is", targetKey, "key is", key)
|
2023-10-22 00:51:43 +02:00
|
|
|
key = targetKey ?? key
|
2025-04-05 22:36:25 +02:00
|
|
|
if (targetKey && targetKey.indexOf(key) < 0) {
|
2024-09-27 03:26:17 +02:00
|
|
|
// This is a non-standard key, so we use the image link directly
|
|
|
|
value = absoluteUrl
|
|
|
|
}
|
2024-07-17 18:42:39 +02:00
|
|
|
this.increaseCountFor(this._uploadFinished, featureId)
|
2024-10-19 14:44:55 +02:00
|
|
|
return { key, absoluteUrl, value }
|
2023-09-25 02:13:24 +02:00
|
|
|
}
|
|
|
|
|
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-25 02:13:24 +02:00
|
|
|
|
2023-09-28 23:50:27 +02:00
|
|
|
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
2023-10-16 13:17:30 +02:00
|
|
|
{
|
|
|
|
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
|
|
|
}
|
2023-09-25 02:13:24 +02:00
|
|
|
}
|