UI: don't allow cylindrical images for now, see #2424

This commit is contained in:
Pieter Vander Vennet 2025-06-02 16:08:55 +02:00
parent 10e0262a0d
commit b9293dc2c9
5 changed files with 80 additions and 75 deletions

View file

@ -13,6 +13,7 @@ import ImageUploadQueue, { ImageUploadArguments } from "./ImageUploadQueue"
import { GeoOperations } from "../GeoOperations"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader"
/**
* The ImageUploadManager has a
@ -81,7 +82,7 @@ export class ImageUploadManager {
this._reportError = reportError
}
public canBeUploaded(file: File): true | { error: Translation } {
public async canBeUploaded(file: File): Promise<true | { error: Translation }> {
const sizeInBytes = file.size
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
const error = Translations.t.image.toBig.Subs({
@ -94,12 +95,19 @@ export class ImageUploadManager {
if (ext !== "jpg" && ext !== "jpeg") {
return { error: new Translation({ en: "Only JPG-files are allowed" }) }
}
const tags = await ExifReader.load(file)
if (tags.ProjectionType.value === "cylindrical") {
return { error: new Translation({ en: "Cylindrical images (typically created by a Panorama-app) are not supported" }) }
}
return true
}
/**
* 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 automatically, based on the ID of the feature.
* Does _not_ check 'canBeUploaded'
* Note: the image will actually be added to the queue. If the image-upload fails, this will be attempted when visiting MC again
* @param file a jpg file to upload
* @param tagsStore The tags of the feature
@ -117,10 +125,6 @@ export class ImageUploadManager {
ignoreGPS: boolean | false
}
): void {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
throw canBeUploaded.error
}
const tags: OsmTags = tagsStore.data
const featureId = <OsmId | NoteId>tags.id
@ -286,7 +290,7 @@ export class ImageUploadManager {
let absoluteUrl: string
try {
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,

View file

@ -51,7 +51,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
new SvelteUIElement(Panoramax_bw),
p.createViewLink({
imageId: img?.id,
location,
location
}),
true
)
@ -65,14 +65,14 @@ export default class PanoramaxImageProvider extends ImageProvider {
const p = new Panoramax(host)
return p.createViewLink({
imageId: img?.id,
location,
location
})
}
public addKnownMeta(meta: ImageData, url?: string) {
PanoramaxImageProvider.knownMeta[meta.id] = {
data: Promise.resolve({ data: meta, url }),
time: new Date(),
time: new Date()
}
}
@ -125,7 +125,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
status: meta.properties["geovisio:status"],
rotation: Number(meta.properties["view:azimuth"]),
isSpherical: meta.properties.exif["Xmp.GPano.ProjectionType"] === "equirectangular",
date: new Date(meta.properties.datetime),
date: new Date(meta.properties.datetime)
}
}
@ -156,7 +156,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
const promise: Promise<{ data: ImageData; url: string }> = this.getInfoForUncached(id)
PanoramaxImageProvider.knownMeta[id] = {
time: new Date(),
data: promise,
data: promise
}
return await promise
}
@ -215,7 +215,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
return {
artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader
date: new Date(meta.data.properties["datetime"]),
licenseShortName: meta.data.properties["geovisio:license"],
licenseShortName: meta.data.properties["geovisio:license"]
}
}
@ -247,8 +247,8 @@ export default class PanoramaxImageProvider extends ImageProvider {
properties: {
url,
northOffset,
pitchOffset,
},
pitchOffset
}
}
}
}
@ -263,6 +263,7 @@ export class PanoramaxUploader implements ImageUploader {
this.panoramax = new AuthorizedPanoramax(url, token)
}
async uploadImage(
blob: File,
currentGps: [number, number],
@ -282,53 +283,63 @@ export class PanoramaxUploader implements ImageUploader {
datetime ??= new Date().toISOString()
try {
const tags = await ExifReader.load(blob)
const [[latD], [latM], [latS, latSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLatitude?.value
const [[lonD], [lonM], [lonS, lonSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLongitude?.value
if (tags.ProjectionType.value === "cylindrical") {
throw "Unsupported image format: cylindrical images (panorama images) are currently not supported"
}
if (tags?.GPSLatitude?.value && tags?.GPSLongitude?.value) {
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
if (
typeof exifLat === "number" &&
!isNaN(exifLat) &&
typeof exifLon === "number" &&
!isNaN(exifLon) &&
!(exifLat === 0 && exifLon === 0)
) {
lat = exifLat
lon = exifLon
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
lat *= -1
}
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
lon *= -1
const [[latD], [latM], [latS, latSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLatitude?.value
const [[lonD], [lonM], [lonS, lonSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLongitude?.value
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
if (
typeof exifLat === "number" &&
!isNaN(exifLat) &&
typeof exifLon === "number" &&
!isNaN(exifLon) &&
!(exifLat === 0 && exifLon === 0)
) {
lat = exifLat
lon = exifLon
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
lat *= -1
}
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
lon *= -1
}
}
}
const [date, time] = (
const dateTime = (
tags.DateTime.value[0] ??
tags.DateTimeOriginal.value[0] ??
tags.GPSDateStamp ??
tags.CreateDate ??
tags["Date Created"]
).split(" ")
const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
if (exifDatetime.getFullYear() === 1970) {
// The data probably got reset to the epoch
// we don't use the value
console.log(
"Datetime from picture is probably invalid:",
exifDatetime,
"using 'now' instead"
)
} else {
datetime = exifDatetime.toISOString()
)?.split(" ")
if (dateTime) {
const [date, time] = dateTime
const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
if (exifDatetime.getFullYear() === 1970) {
// The data probably got reset to the epoch
// we don't use the value
console.log(
"Datetime from picture is probably invalid:",
exifDatetime,
"using 'now' instead"
)
} else {
datetime = exifDatetime.toISOString()
}
}
console.log("Tags are", tags)
} catch (e) {
console.warn("Could not read EXIF-tags")
console.warn("Could not read EXIF-tags due to", e)
}
const p = this.panoramax
@ -345,7 +356,7 @@ export class PanoramaxUploader implements ImageUploader {
indexInSequence: sequence["stats:items"].count + 1, // stats:items is '1'-indexed, so .count is also the last index
exifOverride: {
Artist: author,
},
}
}
if (progress) {
options.onProgress = (e: ProgressEvent) => {
@ -362,7 +373,7 @@ export class PanoramaxUploader implements ImageUploader {
return {
key: "panoramax",
value: img.id,
absoluteUrl: img.assets.hd.href,
absoluteUrl: img.assets.hd.href
}
}
}

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher, onDestroy } from "svelte"
import { twMerge } from "tailwind-merge"
export let accept: string | undefined
export let capture: string | undefined = undefined

View file

@ -42,7 +42,7 @@
const file = files.item(i)
console.log("Got file", file.name)
try {
const canBeUploaded = state?.imageUploadManager?.canBeUploaded(file)
const canBeUploaded = await state?.imageUploadManager?.canBeUploaded(file)
if (canBeUploaded !== true) {
errs.push(canBeUploaded.error)
continue

View file

@ -1,28 +1,19 @@
<script lang="ts">
import { OsmConnection } from "../Logic/Osm/OsmConnection"
const conn = new OsmConnection()
const ud = conn.userDetails.mapD(ud => ud.name)
import FileSelector from "./Base/FileSelector.svelte"
import ExifReader from "exifreader"
import { UIEventSource } from "../Logic/UIEventSource"
const pref = conn.getPreference("test")
const enigma = "De Schlüsselmaschine E, ook wel bekend als de Cypher Machine E, is vooral bekend als de Enigma.\n" +
"\n" +
"De Enigma is een soortnaam van elektromechanische codeermachines van het type rotormachine. Hiermee kunnen berichten gecodeerd worden in andere lettercombinaties dan het origineel, die vervolgens weer terugvertaald kunnen worden door een identieke machine. Enigma is Grieks voor raadsel.\n" +
"\n" +
"Het Enigma-toestel werd in de jaren twintig op de markt gebracht door Chiffriermaschinen AG en gebruikt door verscheidene Europese bedrijven, diplomatieke diensten en legers, maar werd vooral bekend als codeermachine van de Wehrmacht vóór en tijdens de Tweede Wereldoorlog in nazi-Duitsland.\n" +
"\n" +
"Mede dankzij de Poolse inlichtingendienst, slaagde de Pool Marian Adam Rejewski er tijdens de Tweede Wereldoorlog in de Enigmacodes te breken, in tegenstelling tot de bewering dat de Britse inlichtingendienst hiervoor verantwoordelijk zou zijn. Het breken van de Enigmacodes bleek een goudmijn aan informatie te zijn. Deze informatie, verkregen door ontcijfering van de geheime Duitse berichten, kreeg de codenaam Ultra en speelde een uiterst belangrijke rol in het verloop van de Tweede Wereldoorlog, vooral in de U-bootoorlog in de Atlantische Oceaan, de veldslagen in Afrika en de Landing in Normandië.\n" +
"\n" +
"De Enigma-machine had een zeer degelijk ontwerp waarvan de code onbreekbaar leek vanwege een ongeëvenaard cryptografisch veiligheidsniveau. Het waren buitgemaakte codeboeken, fouten door operators en onveilige procedures bij de versleuteling van berichten die het breken van de Enigmacode mogelijk maakten. "
let txt = new UIEventSource("")
async function accept(fileList: FileList) {
const tags = await ExifReader.load(fileList.item(0))
console.log("All tags:", tags)
txt.set(tags.ProjectionType.value)
}
</script>
<h3>Settings test</h3>
Logged in as <b>{$ud}</b>
<FileSelector on:submit={fileList => accept(fileList.detail)} accept="image/jpg">Select file</FileSelector>
Current value of pref is {$pref}
<button on:click={() => {pref.set(undefined)}}>Clear</button>
<button on:click={() => {pref.set("Short text")}}>Short</button>
<button on:click={() => {pref.set(enigma)}}>Long</button>
<b>{$txt}</b>