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 { GeoOperations } from "../GeoOperations"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement" import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader"
/** /**
* The ImageUploadManager has a * The ImageUploadManager has a
@ -81,7 +82,7 @@ export class ImageUploadManager {
this._reportError = reportError this._reportError = reportError
} }
public canBeUploaded(file: File): true | { error: Translation } { public async canBeUploaded(file: File): Promise<true | { error: Translation }> {
const sizeInBytes = file.size const sizeInBytes = file.size
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
const error = Translations.t.image.toBig.Subs({ const error = Translations.t.image.toBig.Subs({
@ -94,12 +95,19 @@ export class ImageUploadManager {
if (ext !== "jpg" && ext !== "jpeg") { if (ext !== "jpg" && ext !== "jpeg") {
return { error: new Translation({ en: "Only JPG-files are allowed" }) } 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 return true
} }
/** /**
* Uploads the given image, applies the correct title and license for the known user. * 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. * 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 * 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 file a jpg file to upload
* @param tagsStore The tags of the feature * @param tagsStore The tags of the feature
@ -117,10 +125,6 @@ export class ImageUploadManager {
ignoreGPS: boolean | false ignoreGPS: boolean | false
} }
): void { ): void {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
throw canBeUploaded.error
}
const tags: OsmTags = tagsStore.data const tags: OsmTags = tagsStore.data
const featureId = <OsmId | NoteId>tags.id const featureId = <OsmId | NoteId>tags.id
@ -286,7 +290,7 @@ export class ImageUploadManager {
let absoluteUrl: string let absoluteUrl: string
try { try {
;({ key, value, absoluteUrl } = await this._uploader.uploadImage( ({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob, blob,
location, location,
author, author,

View file

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

View file

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

View file

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

View file

@ -1,28 +1,19 @@
<script lang="ts"> <script lang="ts">
import { OsmConnection } from "../Logic/Osm/OsmConnection"
const conn = new OsmConnection() import FileSelector from "./Base/FileSelector.svelte"
const ud = conn.userDetails.mapD(ud => ud.name) import ExifReader from "exifreader"
import { UIEventSource } from "../Logic/UIEventSource"
const pref = conn.getPreference("test") let txt = new UIEventSource("")
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. "
async function accept(fileList: FileList) {
const tags = await ExifReader.load(fileList.item(0))
console.log("All tags:", tags)
txt.set(tags.ProjectionType.value)
}
</script> </script>
<h3>Settings test</h3> <FileSelector on:submit={fileList => accept(fileList.detail)} accept="image/jpg">Select file</FileSelector>
Logged in as <b>{$ud}</b>
Current value of pref is {$pref} <b>{$txt}</b>
<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>