From 9a5a2e9924f668af8666bc156a4788850dda429a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 25 Sep 2023 02:55:43 +0200 Subject: [PATCH] Refactoring: port add-image-to-note to new element as well, remove obsolete classes, fix note creation --- .../ImageProviders/ImageUploadManager.ts | 45 +- src/Logic/Osm/Actions/LinkImageAction.ts | 2 +- src/Logic/Osm/OsmConnection.ts | 1008 +++++++++-------- src/UI/Image/ImageUploadFlow.ts | 192 ---- src/UI/Image/UploadImage.svelte | 6 +- src/UI/Input/FileSelectorButton.ts | 111 -- src/UI/Input/Slider.ts | 62 - src/UI/Popup/CreateNewNote.svelte | 5 + src/UI/SpecialVisualization.ts | 3 +- src/UI/SpecialVisualizations.ts | 184 ++- 10 files changed, 617 insertions(+), 1001 deletions(-) delete mode 100644 src/UI/Image/ImageUploadFlow.ts delete mode 100644 src/UI/Input/FileSelectorButton.ts delete mode 100644 src/UI/Input/Slider.ts diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 9bb2f9535..85964a507 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -7,6 +7,7 @@ import { Store, UIEventSource } from "../UIEventSource"; import { OsmConnection } from "../Osm/OsmConnection"; import { Changes } from "../Osm/Changes"; import Translations from "../../UI/i18n/Translations"; +import NoteCommentElement from "../../UI/Popup/NoteCommentElement"; /** @@ -58,24 +59,25 @@ export class ImageUploadManager { } /** - * 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 */ - public async uploadImageAndApply(file: File, tags: OsmTags) { + public async uploadImageAndApply(file: File, tagsStore: UIEventSource) : Promise{ - const sizeInBytes = file.size - const featureId = tags.id - console.log(file.name + " has a size of " + sizeInBytes + " Bytes, attaching to", tags.id) - const self = this - if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { - this.increaseCountFor(this._uploadStarted, featureId) - this.increaseCountFor(this._uploadFailed, featureId) - throw( - Translations.t.image.toBig.Subs({ - actual_size: Math.floor(sizeInBytes / 1000000) + "MB", - max_size: self._uploader.maxFileSizeInMegabytes + "MB", - }).txt - ) - } + const sizeInBytes = file.size; + const tags= tagsStore.data + const featureId = tags.id; + const self = this; + if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { + this.increaseCountFor(this._uploadStarted, featureId); + this.increaseCountFor(this._uploadFailed, featureId); + throw ( + Translations.t.image.toBig.Subs({ + actual_size: Math.floor(sizeInBytes / 1000000) + "MB", + max_size: self._uploader.maxFileSizeInMegabytes + "MB" + }).txt + ); + } const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0"); @@ -93,8 +95,15 @@ export class ImageUploadManager { "osmid:" + tags.id ].join("\n"); - console.log("Upload done, creating ") + console.log("Upload done, creating "); const action = await this.uploadImageWithLicense(featureId, title, description, file); + if(!isNaN(Number( featureId))){ + // THis is a map note + const url = action._url + await this._osmConnection.addCommentToNote(featureId, url) + NoteCommentElement.addCommentTo(url, > tagsStore, {osmConnection: this._osmConnection}) + return + } await this._changes.applyAction(action); } @@ -121,7 +130,7 @@ export class ImageUploadManager { } } - console.log("Uploading done, creating action for", featureId) + console.log("Uploading done, creating action for", featureId); const action = new LinkImageAction(featureId, key, value, properties, { theme: this._layout.id, changeType: "add-image" diff --git a/src/Logic/Osm/Actions/LinkImageAction.ts b/src/Logic/Osm/Actions/LinkImageAction.ts index 1b2b90d19..7d4ec23c8 100644 --- a/src/Logic/Osm/Actions/LinkImageAction.ts +++ b/src/Logic/Osm/Actions/LinkImageAction.ts @@ -7,7 +7,7 @@ import { Store } from "../../UIEventSource"; export default class LinkImageAction extends OsmChangeAction { private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string; - private readonly _url: string; + public readonly _url: string; private readonly _currentTags: Store>; private readonly _meta: { theme: string; changeType: "add-image" | "link-image" }; diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index e650250a9..07028c35a 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -1,551 +1,553 @@ // @ts-ignore -import { osmAuth } from "osm-auth" -import { Store, Stores, UIEventSource } from "../UIEventSource" -import { OsmPreferences } from "./OsmPreferences" -import { Utils } from "../../Utils" -import { LocalStorageSource } from "../Web/LocalStorageSource" -import * as config from "../../../package.json" -export default class UserDetails { - public loggedIn = false - public name = "Not logged in" - public uid: number - public csCount = 0 - public img?: string - public unreadMessages = 0 - public totalMessages: number = 0 - public home: { lon: number; lat: number } - public backend: string - public account_created: string - public tracesCount: number = 0 - public description: string +import { osmAuth } from "osm-auth"; +import { Store, Stores, UIEventSource } from "../UIEventSource"; +import { OsmPreferences } from "./OsmPreferences"; +import { Utils } from "../../Utils"; +import { LocalStorageSource } from "../Web/LocalStorageSource"; +import * as config from "../../../package.json"; - constructor(backend: string) { - this.backend = backend - } +export default class UserDetails { + public loggedIn = false; + public name = "Not logged in"; + public uid: number; + public csCount = 0; + public img?: string; + public unreadMessages = 0; + public totalMessages: number = 0; + public home: { lon: number; lat: number }; + public backend: string; + public account_created: string; + public tracesCount: number = 0; + public description: string; + + constructor(backend: string) { + this.backend = backend; + } } export interface AuthConfig { - "#"?: string // optional comment - oauth_client_id: string - oauth_secret: string - url: string + "#"?: string; // optional comment + oauth_client_id: string; + oauth_secret: string; + url: string; } export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" export class OsmConnection { - public static readonly oauth_configs: Record = - config.config.oauth_credentials - public auth - public userDetails: UIEventSource - public isLoggedIn: Store - public gpxServiceIsOnline: UIEventSource = new UIEventSource( - "unknown" - ) - public apiIsOnline: UIEventSource = new UIEventSource( - "unknown" - ) + public static readonly oauth_configs: Record = + config.config.oauth_credentials; + public auth; + public userDetails: UIEventSource; + public isLoggedIn: Store; + public gpxServiceIsOnline: UIEventSource = new UIEventSource( + "unknown" + ); + public apiIsOnline: UIEventSource = new UIEventSource( + "unknown" + ); - public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( - "not-attempted" - ) - public preferencesHandler: OsmPreferences - public readonly _oauth_config: AuthConfig - private readonly _dryRun: Store - private fakeUser: boolean - private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] - private readonly _iframeMode: Boolean | boolean - private readonly _singlePage: boolean - private isChecking = false + public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">( + "not-attempted" + ); + public preferencesHandler: OsmPreferences; + public readonly _oauth_config: AuthConfig; + private readonly _dryRun: Store; + private fakeUser: boolean; + private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; + private readonly _iframeMode: Boolean | boolean; + private readonly _singlePage: boolean; + private isChecking = false; - constructor(options?: { - dryRun?: Store - fakeUser?: false | boolean - oauth_token?: UIEventSource - // Used to keep multiple changesets open and to write to the correct changeset - singlePage?: boolean - osmConfiguration?: "osm" | "osm-test" - attemptLogin?: true | boolean - }) { - options = options ?? {} - this.fakeUser = options.fakeUser ?? false - this._singlePage = options.singlePage ?? true - this._oauth_config = - OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? - OsmConnection.oauth_configs.osm - console.debug("Using backend", this._oauth_config.url) - this._iframeMode = Utils.runningFromConsole ? false : window !== window.top + constructor(options?: { + dryRun?: Store + fakeUser?: false | boolean + oauth_token?: UIEventSource + // Used to keep multiple changesets open and to write to the correct changeset + singlePage?: boolean + osmConfiguration?: "osm" | "osm-test" + attemptLogin?: true | boolean + }) { + options = options ?? {}; + this.fakeUser = options.fakeUser ?? false; + this._singlePage = options.singlePage ?? true; + this._oauth_config = + OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? + OsmConnection.oauth_configs.osm; + console.debug("Using backend", this._oauth_config.url); + this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; - // Check if there are settings available in environment variables, and if so, use those - if ( - import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined && - import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined - ) { - console.debug("Using environment variables for oauth config") - this._oauth_config = { - oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID, - oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET, - url: "https://api.openstreetmap.org", - } - } - - this.userDetails = new UIEventSource( - new UserDetails(this._oauth_config.url), - "userDetails" - ) - if (options.fakeUser) { - const ud = this.userDetails.data - ud.csCount = 5678 - ud.loggedIn = true - ud.unreadMessages = 0 - ud.name = "Fake user" - ud.totalMessages = 42 - } - const self = this - this.UpdateCapabilities() - this.isLoggedIn = this.userDetails.map( - (user) => - user.loggedIn && - (self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), - [this.apiIsOnline] - ) - this.isLoggedIn.addCallback((isLoggedIn) => { - if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { - // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do - // This means someone attempted to toggle this; so we attempt to login! - self.AttemptLogin() - } - }) - - this._dryRun = options.dryRun ?? new UIEventSource(false) - - this.updateAuthObject() - - this.preferencesHandler = new OsmPreferences( - this.auth, - this - ) - - if (options.oauth_token?.data !== undefined) { - console.log(options.oauth_token.data) - const self = this - this.auth.bootstrapToken( - options.oauth_token.data, - (x) => { - console.log("Called back: ", x) - self.AttemptLogin() - }, - this.auth - ) - - options.oauth_token.setData(undefined) - } - if (this.auth.authenticated() && options.attemptLogin !== false) { - this.AttemptLogin() // Also updates the user badge - } else { - console.log("Not authenticated") - } + // Check if there are settings available in environment variables, and if so, use those + if ( + import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined && + import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined + ) { + console.debug("Using environment variables for oauth config"); + this._oauth_config = { + oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID, + oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET, + url: "https://api.openstreetmap.org" + }; } - public GetPreference( - key: string, - defaultValue: string = undefined, - options?: { - documentation?: string - prefix?: string - } - ): UIEventSource { - return this.preferencesHandler.GetPreference(key, defaultValue, options) + this.userDetails = new UIEventSource( + new UserDetails(this._oauth_config.url), + "userDetails" + ); + if (options.fakeUser) { + const ud = this.userDetails.data; + ud.csCount = 5678; + ud.loggedIn = true; + ud.unreadMessages = 0; + ud.name = "Fake user"; + ud.totalMessages = 42; } + const self = this; + this.UpdateCapabilities(); + this.isLoggedIn = this.userDetails.map( + (user) => + user.loggedIn && + (self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), + [this.apiIsOnline] + ); + this.isLoggedIn.addCallback((isLoggedIn) => { + if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { + // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do + // This means someone attempted to toggle this; so we attempt to login! + self.AttemptLogin(); + } + }); - public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { - return this.preferencesHandler.GetLongPreference(key, prefix) + this._dryRun = options.dryRun ?? new UIEventSource(false); + + this.updateAuthObject(); + + this.preferencesHandler = new OsmPreferences( + this.auth, + this + ); + + if (options.oauth_token?.data !== undefined) { + console.log(options.oauth_token.data); + const self = this; + this.auth.bootstrapToken( + options.oauth_token.data, + (x) => { + console.log("Called back: ", x); + self.AttemptLogin(); + }, + this.auth + ); + + options.oauth_token.setData(undefined); } - - public OnLoggedIn(action: (userDetails: UserDetails) => void) { - this._onLoggedIn.push(action) + if (this.auth.authenticated() && options.attemptLogin !== false) { + this.AttemptLogin(); // Also updates the user badge + } else { + console.log("Not authenticated"); } + } - public LogOut() { - this.auth.logout() - this.userDetails.data.loggedIn = false - this.userDetails.data.csCount = 0 - this.userDetails.data.name = "" - this.userDetails.ping() - console.log("Logged out") - this.loadingStatus.setData("not-attempted") + public GetPreference( + key: string, + defaultValue: string = undefined, + options?: { + documentation?: string + prefix?: string } + ): UIEventSource { + return this.preferencesHandler.GetPreference(key, defaultValue, options); + } - /** - * The backend host, without path or trailing '/' - * - * new OsmConnection().Backend() // => "https://www.openstreetmap.org" - */ - public Backend(): string { - return this._oauth_config.url + public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { + return this.preferencesHandler.GetLongPreference(key, prefix); + } + + public OnLoggedIn(action: (userDetails: UserDetails) => void) { + this._onLoggedIn.push(action); + } + + public LogOut() { + this.auth.logout(); + this.userDetails.data.loggedIn = false; + this.userDetails.data.csCount = 0; + this.userDetails.data.name = ""; + this.userDetails.ping(); + console.log("Logged out"); + this.loadingStatus.setData("not-attempted"); + } + + /** + * The backend host, without path or trailing '/' + * + * new OsmConnection().Backend() // => "https://www.openstreetmap.org" + */ + public Backend(): string { + return this._oauth_config.url; + } + + public AttemptLogin() { + this.UpdateCapabilities(); + this.loadingStatus.setData("loading"); + if (this.fakeUser) { + this.loadingStatus.setData("logged-in"); + console.log("AttemptLogin called, but ignored as fakeUser is set"); + return; } - - public AttemptLogin() { - this.UpdateCapabilities() - this.loadingStatus.setData("loading") - if (this.fakeUser) { - this.loadingStatus.setData("logged-in") - console.log("AttemptLogin called, but ignored as fakeUser is set") - return - } - const self = this - console.log("Trying to log in...") - this.updateAuthObject() - LocalStorageSource.Get("location_before_login").setData( - Utils.runningFromConsole ? undefined : window.location.href - ) - this.auth.xhr( - { - method: "GET", - path: "/api/0.6/user/details", - }, - function (err, details) { - if (err != null) { - console.log(err) - self.loadingStatus.setData("error") - if (err.status == 401) { - console.log("Clearing tokens...") - // Not authorized - our token probably got revoked - self.auth.logout() - self.LogOut() - } - return - } - - if (details == null) { - self.loadingStatus.setData("error") - return - } - - self.CheckForMessagesContinuously() - - // details is an XML DOM of user details - let userInfo = details.getElementsByTagName("user")[0] - - let data = self.userDetails.data - data.loggedIn = true - console.log("Login completed, userinfo is ", userInfo) - data.name = userInfo.getAttribute("display_name") - data.account_created = userInfo.getAttribute("account_created") - data.uid = Number(userInfo.getAttribute("id")) - data.csCount = Number.parseInt( - userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0 - ) - data.tracesCount = Number.parseInt( - userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0 - ) - - data.img = undefined - const imgEl = userInfo.getElementsByTagName("img") - if (imgEl !== undefined && imgEl[0] !== undefined) { - data.img = imgEl[0].getAttribute("href") - } - - const description = userInfo.getElementsByTagName("description") - if (description !== undefined && description[0] !== undefined) { - data.description = description[0]?.innerHTML - } - const homeEl = userInfo.getElementsByTagName("home") - if (homeEl !== undefined && homeEl[0] !== undefined) { - const lat = parseFloat(homeEl[0].getAttribute("lat")) - const lon = parseFloat(homeEl[0].getAttribute("lon")) - data.home = { lat: lat, lon: lon } - } - - self.loadingStatus.setData("logged-in") - const messages = userInfo - .getElementsByTagName("messages")[0] - .getElementsByTagName("received")[0] - data.unreadMessages = parseInt(messages.getAttribute("unread")) - data.totalMessages = parseInt(messages.getAttribute("count")) - - self.userDetails.ping() - for (const action of self._onLoggedIn) { - action(self.userDetails.data) - } - self._onLoggedIn = [] - } - ) - } - - /** - * Interact with the API. - * - * @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close' - */ - public async interact( - path: string, - method: "GET" | "POST" | "PUT" | "DELETE", - header?: Record, - content?: string - ): Promise { - return new Promise((ok, error) => { - this.auth.xhr( - { - method, - options: { - header, - }, - content, - path: `/api/0.6/${path}`, - }, - function (err, response) { - if (err !== null) { - error(err) - } else { - ok(response) - } - } - ) - }) - } - - public async post( - path: string, - content?: string, - header?: Record - ): Promise { - return await this.interact(path, "POST", header, content) - } - - public async put( - path: string, - content?: string, - header?: Record - ): Promise { - return await this.interact(path, "PUT", header, content) - } - - public async get(path: string, header?: Record): Promise { - return await this.interact(path, "GET", header) - } - - public closeNote(id: number | string, text?: string): Promise { - let textSuffix = "" - if ((text ?? "") !== "") { - textSuffix = "?text=" + encodeURIComponent(text) - } - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) - return new Promise((ok) => { - ok() - }) - } - return this.post(`notes/${id}/close${textSuffix}`) - } - - public reopenNote(id: number | string, text?: string): Promise { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) - return new Promise((ok) => { - ok() - }) - } - let textSuffix = "" - if ((text ?? "") !== "") { - textSuffix = "?text=" + encodeURIComponent(text) - } - return this.post(`notes/${id}/reopen${textSuffix}`) - } - - public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually opening note with text ", text) - return new Promise<{ id: number }>((ok) => { - window.setTimeout( - () => ok({ id: Math.floor(Math.random() * 1000) }), - Math.random() * 5000 - ) - }) - } - const content = { lat, lon, text } - const response = await this.post("notes.json", JSON.stringify(content), { - "Content-Type": "application/json", - }) - const parsed = JSON.parse(response) - const id = parsed.properties - console.log("OPENED NOTE", id) - return id - } - - public async uploadGpxTrack( - gpx: string, - options: { - description: string - visibility: "private" | "public" | "trackable" | "identifiable" - filename?: string - /** - * Some words to give some properties; - * - * Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words. - */ - labels: string[] - } - ): Promise<{ id: number }> { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually uploading GPX ", gpx) - return new Promise<{ id: number }>((ok, error) => { - window.setTimeout( - () => ok({ id: Math.floor(Math.random() * 1000) }), - Math.random() * 5000 - ) - }) + const self = this; + console.log("Trying to log in..."); + this.updateAuthObject(); + LocalStorageSource.Get("location_before_login").setData( + Utils.runningFromConsole ? undefined : window.location.href + ); + this.auth.xhr( + { + method: "GET", + path: "/api/0.6/user/details" + }, + function(err, details) { + if (err != null) { + console.log(err); + self.loadingStatus.setData("error"); + if (err.status == 401) { + console.log("Clearing tokens..."); + // Not authorized - our token probably got revoked + self.auth.logout(); + self.LogOut(); + } + return; } - const contents = { - file: gpx, - description: options.description ?? "", - tags: options.labels?.join(",") ?? "", - visibility: options.visibility, + if (details == null) { + self.loadingStatus.setData("error"); + return; } - const extras = { - file: - '; filename="' + - (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + - '"\r\nContent-Type: application/gpx+xml', + self.CheckForMessagesContinuously(); + + // details is an XML DOM of user details + let userInfo = details.getElementsByTagName("user")[0]; + + let data = self.userDetails.data; + data.loggedIn = true; + console.log("Login completed, userinfo is ", userInfo); + data.name = userInfo.getAttribute("display_name"); + data.account_created = userInfo.getAttribute("account_created"); + data.uid = Number(userInfo.getAttribute("id")); + data.csCount = Number.parseInt( + userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? 0 + ); + data.tracesCount = Number.parseInt( + userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? 0 + ); + + data.img = undefined; + const imgEl = userInfo.getElementsByTagName("img"); + if (imgEl !== undefined && imgEl[0] !== undefined) { + data.img = imgEl[0].getAttribute("href"); } - const boundary = "987654" - - let body = "" - for (const key in contents) { - body += "--" + boundary + "\r\n" - body += 'Content-Disposition: form-data; name="' + key + '"' - if (extras[key] !== undefined) { - body += extras[key] - } - body += "\r\n\r\n" - body += contents[key] + "\r\n" + const description = userInfo.getElementsByTagName("description"); + if (description !== undefined && description[0] !== undefined) { + data.description = description[0]?.innerHTML; + } + const homeEl = userInfo.getElementsByTagName("home"); + if (homeEl !== undefined && homeEl[0] !== undefined) { + const lat = parseFloat(homeEl[0].getAttribute("lat")); + const lon = parseFloat(homeEl[0].getAttribute("lon")); + data.home = { lat: lat, lon: lon }; } - body += "--" + boundary + "--\r\n" - const response = await this.post("gpx/create", body, { - "Content-Type": "multipart/form-data; boundary=" + boundary, - "Content-Length": body.length, - }) - const parsed = JSON.parse(response) - console.log("Uploaded GPX track", parsed) - return { id: parsed } + self.loadingStatus.setData("logged-in"); + const messages = userInfo + .getElementsByTagName("messages")[0] + .getElementsByTagName("received")[0]; + data.unreadMessages = parseInt(messages.getAttribute("unread")); + data.totalMessages = parseInt(messages.getAttribute("count")); + + self.userDetails.ping(); + for (const action of self._onLoggedIn) { + action(self.userDetails.data); + } + self._onLoggedIn = []; + } + ); + } + + /** + * Interact with the API. + * + * @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close' + */ + public async interact( + path: string, + method: "GET" | "POST" | "PUT" | "DELETE", + header?: Record, + content?: string + ): Promise { + return new Promise((ok, error) => { + this.auth.xhr( + { + method, + options: { + header + }, + content, + path: `/api/0.6/${path}` + }, + function(err, response) { + if (err !== null) { + error(err); + } else { + ok(response); + } + } + ); + }); + } + + public async post( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "POST", header, content); + } + + public async put( + path: string, + content?: string, + header?: Record + ): Promise { + return await this.interact(path, "PUT", header, content); + } + + public async get(path: string, header?: Record): Promise { + return await this.interact(path, "GET", header); + } + + public closeNote(id: number | string, text?: string): Promise { + let textSuffix = ""; + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text); + } + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text); + return new Promise((ok) => { + ok(); + }); + } + return this.post(`notes/${id}/close${textSuffix}`); + } + + public reopenNote(id: number | string, text?: string): Promise { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text); + return new Promise((ok) => { + ok(); + }); + } + let textSuffix = ""; + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text); + } + return this.post(`notes/${id}/reopen${textSuffix}`); + } + + public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually opening note with text ", text); + return new Promise<{ id: number }>((ok) => { + window.setTimeout( + () => ok({ id: Math.floor(Math.random() * 1000) }), + Math.random() * 5000 + ); + }); + } + // Lat and lon must be strings for the API to accept it + const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}` + const response = await this.post("notes.json", content, { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + }); + const parsed = JSON.parse(response); + const id = parsed.properties; + console.log("OPENED NOTE", id); + return id; + } + + public async uploadGpxTrack( + gpx: string, + options: { + description: string + visibility: "private" | "public" | "trackable" | "identifiable" + filename?: string + /** + * Some words to give some properties; + * + * Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words. + */ + labels: string[] + } + ): Promise<{ id: number }> { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually uploading GPX ", gpx); + return new Promise<{ id: number }>((ok, error) => { + window.setTimeout( + () => ok({ id: Math.floor(Math.random() * 1000) }), + Math.random() * 5000 + ); + }); } - public addCommentToNote(id: number | string, text: string): Promise { - if (this._dryRun.data) { - console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id) - return new Promise((ok) => { - ok() - }) - } - if ((text ?? "") === "") { - throw "Invalid text!" - } + const contents = { + file: gpx, + description: options.description ?? "", + tags: options.labels?.join(",") ?? "", + visibility: options.visibility + }; - return new Promise((ok, error) => { - this.auth.xhr( - { - method: "POST", + const extras = { + file: + "; filename=\"" + + (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + + "\"\r\nContent-Type: application/gpx+xml" + }; - path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, - }, - function (err, _) { - if (err !== null) { - error(err) - } else { - ok() - } - } - ) - }) + const boundary = "987654"; + + let body = ""; + for (const key in contents) { + body += "--" + boundary + "\r\n"; + body += "Content-Disposition: form-data; name=\"" + key + "\""; + if (extras[key] !== undefined) { + body += extras[key]; + } + body += "\r\n\r\n"; + body += contents[key] + "\r\n"; + } + body += "--" + boundary + "--\r\n"; + + const response = await this.post("gpx/create", body, { + "Content-Type": "multipart/form-data; boundary=" + boundary, + "Content-Length": body.length + }); + const parsed = JSON.parse(response); + console.log("Uploaded GPX track", parsed); + return { id: parsed }; + } + + public addCommentToNote(id: number | string, text: string): Promise { + if (this._dryRun.data) { + console.warn("Dryrun enabled - not actually adding comment ", text, "to note ", id); + return new Promise((ok) => { + ok(); + }); + } + if ((text ?? "") === "") { + throw "Invalid text!"; } - private updateAuthObject() { - let pwaStandAloneMode = false - try { - if (Utils.runningFromConsole) { - pwaStandAloneMode = true - } else if ( - window.matchMedia("(display-mode: standalone)").matches || - window.matchMedia("(display-mode: fullscreen)").matches - ) { - pwaStandAloneMode = true - } - } catch (e) { - console.warn( - "Detecting standalone mode failed", - e, - ". Assuming in browser and not worrying furhter" - ) + return new Promise((ok, error) => { + this.auth.xhr( + { + method: "POST", + + path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` + }, + function(err, _) { + if (err !== null) { + error(err); + } else { + ok(); + } } - const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage + ); + }); + } - // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... - // Same for an iframe... + /** + * To be called by land.html + */ + public finishLogin(callback: (previousURL: string) => void) { + this.auth.authenticate(function() { + // Fully authed at this point + console.log("Authentication successful!"); + const previousLocation = LocalStorageSource.Get("location_before_login"); + callback(previousLocation.data); + }); + } - this.auth = new osmAuth({ - client_id: this._oauth_config.oauth_client_id, - url: this._oauth_config.url, - scope: "read_prefs write_prefs write_api write_gpx write_notes", - redirect_uri: Utils.runningFromConsole - ? "https://mapcomplete.org/land.html" - : window.location.protocol + "//" + window.location.host + "/land.html", - singlepage: !standalone, - auto: true, - }) + private updateAuthObject() { + let pwaStandAloneMode = false; + try { + if (Utils.runningFromConsole) { + pwaStandAloneMode = true; + } else if ( + window.matchMedia("(display-mode: standalone)").matches || + window.matchMedia("(display-mode: fullscreen)").matches + ) { + pwaStandAloneMode = true; + } + } catch (e) { + console.warn( + "Detecting standalone mode failed", + e, + ". Assuming in browser and not worrying furhter" + ); } + const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage; - /** - * To be called by land.html - */ - public finishLogin(callback: (previousURL: string) => void) { - this.auth.authenticate(function () { - // Fully authed at this point - console.log("Authentication successful!") - const previousLocation = LocalStorageSource.Get("location_before_login") - callback(previousLocation.data) - }) - } + // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... + // Same for an iframe... - private CheckForMessagesContinuously() { - const self = this - if (this.isChecking) { - return - } - this.isChecking = true - Stores.Chronic(5 * 60 * 1000).addCallback((_) => { - if (self.isLoggedIn.data) { - console.log("Checking for messages") - self.AttemptLogin() - } - }) - } + this.auth = new osmAuth({ + client_id: this._oauth_config.oauth_client_id, + url: this._oauth_config.url, + scope: "read_prefs write_prefs write_api write_gpx write_notes", + redirect_uri: Utils.runningFromConsole + ? "https://mapcomplete.org/land.html" + : window.location.protocol + "//" + window.location.host + "/land.html", + singlepage: !standalone, + auto: true + }); + } - private UpdateCapabilities(): void { - const self = this - this.FetchCapabilities().then(({ api, gpx }) => { - self.apiIsOnline.setData(api) - self.gpxServiceIsOnline.setData(gpx) - }) + private CheckForMessagesContinuously() { + const self = this; + if (this.isChecking) { + return; } + this.isChecking = true; + Stores.Chronic(5 * 60 * 1000).addCallback((_) => { + if (self.isLoggedIn.data) { + console.log("Checking for messages"); + self.AttemptLogin(); + } + }); + } - private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> { - if (Utils.runningFromConsole) { - return { api: "online", gpx: "online" } - } - const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities") - if (result["content"] === undefined) { - console.log("Something went wrong:", result) - return { api: "unreachable", gpx: "unreachable" } - } - const xmlRaw = result["content"] - const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml") - const statusEl = parsed.getElementsByTagName("status")[0] - const api = statusEl.getAttribute("api") - const gpx = statusEl.getAttribute("gpx") - return { api, gpx } + private UpdateCapabilities(): void { + const self = this; + this.FetchCapabilities().then(({ api, gpx }) => { + self.apiIsOnline.setData(api); + self.gpxServiceIsOnline.setData(gpx); + }); + } + + private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> { + if (Utils.runningFromConsole) { + return { api: "online", gpx: "online" }; } + const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities"); + if (result["content"] === undefined) { + console.log("Something went wrong:", result); + return { api: "unreachable", gpx: "unreachable" }; + } + const xmlRaw = result["content"]; + const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml"); + const statusEl = parsed.getElementsByTagName("status")[0]; + const api = statusEl.getAttribute("api"); + const gpx = statusEl.getAttribute("gpx"); + return { api, gpx }; + } } diff --git a/src/UI/Image/ImageUploadFlow.ts b/src/UI/Image/ImageUploadFlow.ts deleted file mode 100644 index 2a90ab66a..000000000 --- a/src/UI/Image/ImageUploadFlow.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import Svg from "../../Svg" -import { Tag } from "../../Logic/Tags/Tag" -import BaseUIElement from "../BaseUIElement" -import Toggle from "../Input/Toggle" -import FileSelectorButton from "../Input/FileSelectorButton" -import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader" -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { FixedUiElement } from "../Base/FixedUiElement" -import { VariableUiElement } from "../Base/VariableUIElement" -import Loading from "../Base/Loading" -import { LoginToggle } from "../Popup/LoginButton" -import Constants from "../../Models/Constants" -import { SpecialVisualizationState } from "../SpecialVisualization" -import exp from "constants"; - -export class ImageUploadFlow extends Combine { - private static readonly uploadCountsPerId = new Map>() - - constructor( - tagsSource: Store, - state: SpecialVisualizationState, - imagePrefix: string = "image", - text: string = undefined - ) { - const perId = ImageUploadFlow.uploadCountsPerId - const id = tagsSource.data.id - if (!perId.has(id)) { - perId.set(id, new UIEventSource(0)) - } - const uploadedCount = perId.get(id) - const uploader = new ImgurUploader(async (url) => { - // A file was uploaded - we add it to the tags of the object - - const tags = tagsSource.data - let key = imagePrefix - if (tags[imagePrefix] !== undefined) { - let freeIndex = 0 - while (tags[imagePrefix + ":" + freeIndex] !== undefined) { - freeIndex++ - } - key = imagePrefix + ":" + freeIndex - } - - await state.changes.applyAction( - new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { - changeType: "add-image", - theme: state.layout.id, - }) - ) - console.log("Adding image:" + key, url) - uploadedCount.data++ - uploadedCount.ping() - }) - - const t = Translations.t.image - - let labelContent: BaseUIElement - if (text === undefined) { - labelContent = Translations.t.image.addPicture - .Clone() - .SetClass("block align-middle mt-1 ml-3 text-4xl ") - } else { - labelContent = new FixedUiElement(text).SetClass( - "block align-middle mt-1 ml-3 text-2xl " - ) - } - const label = new Combine([ - Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl "), - labelContent, - ]).SetClass("w-full flex justify-center items-center") - - const licenseStore = state?.osmConnection?.GetPreference("pictures-license", "CC0") - - const fileSelector = new FileSelectorButton(label, { - acceptType: "image/*", - allowMultiple: true, - labelClasses: "rounded-full border-2 border-black font-bold", - }) - /* fileSelector.SetClass( - "p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center" - ) - .SetStyle(" border-color: var(--foreground-color);")*/ - fileSelector.GetValue().addCallback((filelist) => { - if (filelist === undefined || filelist.length === 0) { - return - } - - for (var i = 0; i < filelist.length; i++) { - const sizeInBytes = filelist[i].size - console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes") - if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) { - alert( - Translations.t.image.toBig.Subs({ - actual_size: Math.floor(sizeInBytes / 1000000) + "MB", - max_size: uploader.maxFileSizeInMegabytes + "MB", - }).txt - ) - return - } - } - - const license = licenseStore?.data ?? "CC0" - - const tags = tagsSource.data - - const layout = state?.layout - let matchingLayer: LayerConfig = undefined - for (const layer of layout?.layers ?? []) { - if (layer.source.osmTags.matchesProperties(tags)) { - matchingLayer = layer - break - } - } - - const title = - matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.ConstructElement() - ?.textContent ?? - tags.name ?? - "https//osm.org/" + tags.id - const description = [ - "author:" + state.osmConnection.userDetails.data.name, - "license:" + license, - "osmid:" + tags.id, - ].join("\n") - - uploader.uploadMany(title, description, filelist) - }) - - super([ - new VariableUiElement( - uploader.queue - .map((q) => q.length) - .map((l) => { - if (l == 0) { - return undefined - } - if (l == 1) { - return new Loading(t.uploadingPicture).SetClass("alert") - } else { - return new Loading( - t.uploadingMultiple.Subs({ count: "" + l }) - ).SetClass("alert") - } - }) - ), - new VariableUiElement( - uploader.failed - .map((q) => q.length) - .map((l) => { - if (l == 0) { - return undefined - } - console.log(l) - return t.uploadFailed.SetClass("block alert") - }) - ), - new VariableUiElement( - uploadedCount.map((l) => { - if (l == 0) { - return undefined - } - if (l == 1) { - return t.uploadDone.Clone().SetClass("thanks block") - } - return t.uploadMultipleDone.Subs({ count: l }).SetClass("thanks block") - }) - ), - - fileSelector, - new Combine([ - Translations.t.image.respectPrivacy, - new VariableUiElement( - licenseStore.map((license) => - Translations.t.image.currentLicense.Subs({ license }) - ) - ) - .onClick(() => { - console.log("Opening the license settings... ") - state.guistate.openUsersettings("picture-license") - }) - .SetClass("underline"), - ]).SetStyle("font-size:small;"), - ]) - this.SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none") - - - } -} diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index fa82ee34c..23408e778 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -16,6 +16,10 @@ import Svg from "../../Svg"; export let state: SpecialVisualizationState; export let tags: Store; +/** + * Image to show in the button + * NOT the image to upload! + */ export let image: string = undefined; if (image === "") { image = undefined; @@ -30,7 +34,7 @@ function handleFiles(files: FileList) { const file = files.item(i); console.log("Got file", file.name) try { - state.imageUploadManager.uploadImageAndApply(file, tags.data); + state.imageUploadManager.uploadImageAndApply(file, tags); } catch (e) { alert(e); } diff --git a/src/UI/Input/FileSelectorButton.ts b/src/UI/Input/FileSelectorButton.ts deleted file mode 100644 index c3f56d297..000000000 --- a/src/UI/Input/FileSelectorButton.ts +++ /dev/null @@ -1,111 +0,0 @@ -import BaseUIElement from "../BaseUIElement" -import { InputElement } from "./InputElement" -import { UIEventSource } from "../../Logic/UIEventSource" - -/** - * @deprecated - */ -export default class FileSelectorButton extends InputElement { - private static _nextid = 0 - private readonly _value = new UIEventSource(undefined) - private readonly _label: BaseUIElement - private readonly _acceptType: string - private readonly allowMultiple: boolean - private readonly _labelClasses: string - - constructor( - label: BaseUIElement, - options?: { - acceptType: "image/*" | string - allowMultiple: true | boolean - labelClasses?: string - } - ) { - super() - this._label = label - this._acceptType = options?.acceptType ?? "image/*" - this._labelClasses = options?.labelClasses ?? "" - this.SetClass("block cursor-pointer") - label.SetClass("cursor-pointer") - this.allowMultiple = options?.allowMultiple ?? true - } - - GetValue(): UIEventSource { - return this._value - } - - IsValid(t: FileList): boolean { - return true - } - - protected InnerConstructElement(): HTMLElement { - const self = this - const el = document.createElement("form") - const label = document.createElement("label") - label.appendChild(this._label.ConstructElement()) - label.classList.add(...this._labelClasses.split(" ").filter((t) => t !== "")) - el.appendChild(label) - - const actualInputElement = document.createElement("input") - actualInputElement.style.cssText = "display:none" - actualInputElement.type = "file" - actualInputElement.accept = this._acceptType - actualInputElement.name = "picField" - actualInputElement.multiple = this.allowMultiple - actualInputElement.id = "fileselector" + FileSelectorButton._nextid - FileSelectorButton._nextid++ - - label.htmlFor = actualInputElement.id - - actualInputElement.onchange = () => { - if (actualInputElement.files !== null) { - self._value.setData(actualInputElement.files) - } - } - - el.addEventListener("submit", (e) => { - if (actualInputElement.files !== null) { - self._value.setData(actualInputElement.files) - } - actualInputElement.classList.remove("glowing-shadow") - - e.preventDefault() - }) - - el.appendChild(actualInputElement) - - function setDrawAttention(isOn: boolean) { - if (isOn) { - label.classList.add("glowing-shadow") - } else { - label.classList.remove("glowing-shadow") - } - } - - el.addEventListener("dragover", (event) => { - event.stopPropagation() - event.preventDefault() - setDrawAttention(true) - // Style the drag-and-drop as a "copy file" operation. - event.dataTransfer.dropEffect = "copy" - }) - - window.document.addEventListener("dragenter", () => { - setDrawAttention(true) - }) - - window.document.addEventListener("dragend", () => { - setDrawAttention(false) - }) - - el.addEventListener("drop", (event) => { - event.stopPropagation() - event.preventDefault() - label.classList.remove("glowing-shadow") - const fileList = event.dataTransfer.files - this._value.setData(fileList) - }) - - return el - } -} diff --git a/src/UI/Input/Slider.ts b/src/UI/Input/Slider.ts deleted file mode 100644 index 9fce626a7..000000000 --- a/src/UI/Input/Slider.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { InputElement } from "./InputElement" -import { UIEventSource } from "../../Logic/UIEventSource" - -/** - * @deprecated - */ -export default class Slider extends InputElement { - private readonly _value: UIEventSource - private readonly min: number - private readonly max: number - private readonly step: number - private readonly vertical: boolean - - /** - * Constructs a slider input element for natural numbers - * @param min: the minimum value that is allowed, inclusive - * @param max: the max value that is allowed, inclusive - * @param options: value: injectable value; step: the step size of the slider - */ - constructor( - min: number, - max: number, - options?: { - value?: UIEventSource - step?: 1 | number - vertical?: false | boolean - } - ) { - super() - this.max = max - this.min = min - this._value = options?.value ?? new UIEventSource(min) - this.step = options?.step ?? 1 - this.vertical = options?.vertical ?? false - } - - GetValue(): UIEventSource { - return this._value - } - - protected InnerConstructElement(): HTMLElement { - const el = document.createElement("input") - el.type = "range" - el.min = "" + this.min - el.max = "" + this.max - el.step = "" + this.step - const valuestore = this._value - el.oninput = () => { - valuestore.setData(Number(el.value)) - } - if (this.vertical) { - el.classList.add("vertical") - el.setAttribute("orient", "vertical") // firefox only workaround... - } - valuestore.addCallbackAndRunD((v) => (el.value = "" + valuestore.data)) - return el - } - - IsValid(t: number): boolean { - return Math.round(t) == t && t >= this.min && t <= this.max - } -} diff --git a/src/UI/Popup/CreateNewNote.svelte b/src/UI/Popup/CreateNewNote.svelte index 4bdb92b23..10eda741a 100644 --- a/src/UI/Popup/CreateNewNote.svelte +++ b/src/UI/Popup/CreateNewNote.svelte @@ -62,8 +62,13 @@ state.newFeatures.features.data.push(feature) state.newFeatures.features.ping() state.selectedElement?.setData(feature) + if(state.featureProperties.trackFeature){ + state.featureProperties.trackFeature(feature) + } comment.setData("") created = true + state.selectedElement.setData(feature) + state.selectedLayer.setData(state.layerState.filteredLayers.get("note")) } diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 1d3575b42..a4e00100b 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -16,6 +16,7 @@ import { MenuState } from "../Models/MenuState"; import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; import { RasterLayerPolygon } from "../Models/RasterLayers"; import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; +import { OsmTags } from "../Models/OsmFeature"; /** * The state needed to render a special Visualisation. @@ -26,7 +27,7 @@ export interface SpecialVisualizationState { readonly featureSwitches: FeatureSwitchState; readonly layerState: LayerState; - readonly featureProperties: { getStore(id: string): UIEventSource> }; + readonly featureProperties: { getStore(id: string): UIEventSource>, trackFeature?(feature: { properties: OsmTags }) }; readonly indexedFeatures: IndexedFeatureSource; diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 24b712da2..23d1b57c8 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -1,78 +1,70 @@ -import Combine from "./Base/Combine" -import { FixedUiElement } from "./Base/FixedUiElement" -import BaseUIElement from "./BaseUIElement" -import Title from "./Base/Title" -import Table from "./Base/Table" -import { - RenderingSpecification, - SpecialVisualization, - SpecialVisualizationState, -} from "./SpecialVisualization" -import { HistogramViz } from "./Popup/HistogramViz" -import { MinimapViz } from "./Popup/MinimapViz" -import { ShareLinkViz } from "./Popup/ShareLinkViz" -import { UploadToOsmViz } from "./Popup/UploadToOsmViz" -import { MultiApplyViz } from "./Popup/MultiApplyViz" -import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz" -import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" -import TagApplyButton from "./Popup/TagApplyButton" -import { CloseNoteButton } from "./Popup/CloseNoteButton" -import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" -import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" -import AllTagsPanel from "./Popup/AllTagsPanel.svelte" -import AllImageProviders from "../Logic/ImageProviders/AllImageProviders" -import { ImageCarousel } from "./Image/ImageCarousel" -import { ImageUploadFlow } from "./Image/ImageUploadFlow" -import { VariableUiElement } from "./Base/VariableUIElement" -import { Utils } from "../Utils" -import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" -import { Translation } from "./i18n/Translation" -import Translations from "./i18n/Translations" -import ReviewForm from "./Reviews/ReviewForm" -import ReviewElement from "./Reviews/ReviewElement" -import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" -import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" -import { SubtleButton } from "./Base/SubtleButton" -import Svg from "../Svg" -import NoteCommentElement from "./Popup/NoteCommentElement" -import FileSelectorButton from "./Input/FileSelectorButton" -import { LoginToggle } from "./Popup/LoginButton" -import Toggle from "./Input/Toggle" -import { SubstitutedTranslation } from "./SubstitutedTranslation" -import List from "./Base/List" -import StatisticsPanel from "./BigComponents/StatisticsPanel" -import AutoApplyButton from "./Popup/AutoApplyButton" -import { LanguageElement } from "./Popup/LanguageElement" -import FeatureReviews from "../Logic/Web/MangroveReviews" -import Maproulette from "../Logic/Maproulette" -import SvelteUIElement from "./Base/SvelteUIElement" -import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" -import QuestionViz from "./Popup/QuestionViz" -import { Feature, Point } from "geojson" -import { GeoOperations } from "../Logic/GeoOperations" -import CreateNewNote from "./Popup/CreateNewNote.svelte" -import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" -import UserProfile from "./BigComponents/UserProfile.svelte" -import LanguagePicker from "./LanguagePicker" -import Link from "./Base/Link" -import LayerConfig from "../Models/ThemeConfig/LayerConfig" -import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" -import { OsmTags, WayId } from "../Models/OsmFeature" -import MoveWizard from "./Popup/MoveWizard" -import SplitRoadWizard from "./Popup/SplitRoadWizard" -import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" -import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" -import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" -import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz" -import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz" -import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz" -import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte" -import { OpenJosm } from "./BigComponents/OpenJosm" -import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" -import FediverseValidator from "./InputElement/Validators/FediverseValidator" -import SendEmail from "./Popup/SendEmail.svelte" -import NearbyImages from "./Popup/NearbyImages.svelte" -import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" +import Combine from "./Base/Combine"; +import { FixedUiElement } from "./Base/FixedUiElement"; +import BaseUIElement from "./BaseUIElement"; +import Title from "./Base/Title"; +import Table from "./Base/Table"; +import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"; +import { HistogramViz } from "./Popup/HistogramViz"; +import { MinimapViz } from "./Popup/MinimapViz"; +import { ShareLinkViz } from "./Popup/ShareLinkViz"; +import { UploadToOsmViz } from "./Popup/UploadToOsmViz"; +import { MultiApplyViz } from "./Popup/MultiApplyViz"; +import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"; +import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"; +import TagApplyButton from "./Popup/TagApplyButton"; +import { CloseNoteButton } from "./Popup/CloseNoteButton"; +import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"; +import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"; +import AllTagsPanel from "./Popup/AllTagsPanel.svelte"; +import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; +import { ImageCarousel } from "./Image/ImageCarousel"; +import { VariableUiElement } from "./Base/VariableUIElement"; +import { Utils } from "../Utils"; +import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"; +import { Translation } from "./i18n/Translation"; +import Translations from "./i18n/Translations"; +import ReviewForm from "./Reviews/ReviewForm"; +import ReviewElement from "./Reviews/ReviewElement"; +import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; +import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; +import { SubtleButton } from "./Base/SubtleButton"; +import Svg from "../Svg"; +import NoteCommentElement from "./Popup/NoteCommentElement"; +import { SubstitutedTranslation } from "./SubstitutedTranslation"; +import List from "./Base/List"; +import StatisticsPanel from "./BigComponents/StatisticsPanel"; +import AutoApplyButton from "./Popup/AutoApplyButton"; +import { LanguageElement } from "./Popup/LanguageElement"; +import FeatureReviews from "../Logic/Web/MangroveReviews"; +import Maproulette from "../Logic/Maproulette"; +import SvelteUIElement from "./Base/SvelteUIElement"; +import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; +import QuestionViz from "./Popup/QuestionViz"; +import { Feature, Point } from "geojson"; +import { GeoOperations } from "../Logic/GeoOperations"; +import CreateNewNote from "./Popup/CreateNewNote.svelte"; +import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"; +import UserProfile from "./BigComponents/UserProfile.svelte"; +import LanguagePicker from "./LanguagePicker"; +import Link from "./Base/Link"; +import LayerConfig from "../Models/ThemeConfig/LayerConfig"; +import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; +import { OsmTags, WayId } from "../Models/OsmFeature"; +import MoveWizard from "./Popup/MoveWizard"; +import SplitRoadWizard from "./Popup/SplitRoadWizard"; +import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"; +import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"; +import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"; +import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"; +import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"; +import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"; +import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"; +import { OpenJosm } from "./BigComponents/OpenJosm"; +import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"; +import FediverseValidator from "./InputElement/Validators/FediverseValidator"; +import SendEmail from "./Popup/SendEmail.svelte"; +import NearbyImages from "./Popup/NearbyImages.svelte"; +import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"; import UploadImage from "./Image/UploadImage.svelte"; class NearbyImageVis implements SpecialVisualization { @@ -272,6 +264,7 @@ export default class SpecialVisualizations { SpecialVisualizations.specialVisualizations .map((sp) => sp.funcName + "()") .join(", ") + } } @@ -628,7 +621,6 @@ export default class SpecialVisualizations { return new SvelteUIElement(UploadImage, { state,tags, labelText: args[1], image: args[0] }) - // return new ImageUploadFlow(tags, state, args[0], args[1]) }, }, { @@ -867,43 +859,11 @@ export default class SpecialVisualizations { }, ], constr: (state, tags, args) => { - const isUploading = new UIEventSource(false) - const t = Translations.t.notes const id = tags.data[args[0] ?? "id"] - - const uploader = new ImgurUploader(async (url) => { - isUploading.setData(false) - await state.osmConnection.addCommentToNote(id, url) - NoteCommentElement.addCommentTo(url, tags, state) - }) - - const label = new Combine([ - Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl "), - Translations.t.image.addPicture, - ]).SetClass( - "p-2 border-4 border-black rounded-full font-bold h-full align-center w-full flex justify-center" - ) - - const fileSelector = new FileSelectorButton(label) - fileSelector.GetValue().addCallback((filelist) => { - isUploading.setData(true) - uploader.uploadMany("Image for osm.org/note/" + id, "CC0", filelist) - }) - const ti = Translations.t.image - const uploadPanel = new Combine([ - fileSelector, - ti.respectPrivacy.SetClass("text-sm"), - ]).SetClass("flex flex-col") - return new LoginToggle( - new Toggle( - Translations.t.image.uploadingPicture.SetClass("alert"), - uploadPanel, - isUploading - ), - t.loginToAddPicture, - state - ) - }, + tags = state.featureProperties.getStore(id) + console.log("Id is", id) + return new SvelteUIElement(UploadImage, {state, tags}) + } }, { funcName: "title",