forked from MapComplete/MapComplete
		
	Refactoring: port add-image-to-note to new element as well, remove obsolete classes, fix note creation
This commit is contained in:
		
							parent
							
								
									94ba18785d
								
							
						
					
					
						commit
						9a5a2e9924
					
				
					 10 changed files with 617 additions and 1001 deletions
				
			
		|  | @ -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,23 +59,24 @@ 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<OsmTags>) : Promise<void>{ | ||||
| 
 | ||||
|       const sizeInBytes = file.size | ||||
|     const featureId = <OsmId> tags.id | ||||
|       console.log(file.name + " has a size of " + sizeInBytes + " Bytes, attaching to", tags.id) | ||||
|       const self = this | ||||
|     const sizeInBytes = file.size; | ||||
|     const tags= tagsStore.data | ||||
|     const featureId = <OsmId>tags.id; | ||||
|     const self = this; | ||||
|     if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { | ||||
|           this.increaseCountFor(this._uploadStarted, featureId) | ||||
|           this.increaseCountFor(this._uploadFailed, featureId) | ||||
|       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", | ||||
|           max_size: self._uploader.maxFileSizeInMegabytes + "MB" | ||||
|         }).txt | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -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, <UIEventSource<any>> 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" | ||||
|  |  | |||
|  | @ -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<Record<string, string>>; | ||||
|     private readonly _meta: { theme: string; changeType: "add-image" | "link-image" }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,62 +1,63 @@ | |||
| // @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" | ||||
| 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 | ||||
|   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 | ||||
|     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<string, AuthConfig> = | ||||
|         config.config.oauth_credentials | ||||
|     public auth | ||||
|     public userDetails: UIEventSource<UserDetails> | ||||
|     public isLoggedIn: Store<boolean> | ||||
|     config.config.oauth_credentials; | ||||
|   public auth; | ||||
|   public userDetails: UIEventSource<UserDetails>; | ||||
|   public isLoggedIn: Store<boolean>; | ||||
|   public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>( | ||||
|     "unknown" | ||||
|     ) | ||||
|   ); | ||||
|   public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>( | ||||
|     "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<boolean> | ||||
|     private fakeUser: boolean | ||||
|     private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] | ||||
|     private readonly _iframeMode: Boolean | boolean | ||||
|     private readonly _singlePage: boolean | ||||
|     private isChecking = false | ||||
|   ); | ||||
|   public preferencesHandler: OsmPreferences; | ||||
|   public readonly _oauth_config: AuthConfig; | ||||
|   private readonly _dryRun: Store<boolean>; | ||||
|   private fakeUser: boolean; | ||||
|   private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []; | ||||
|   private readonly _iframeMode: Boolean | boolean; | ||||
|   private readonly _singlePage: boolean; | ||||
|   private isChecking = false; | ||||
| 
 | ||||
|   constructor(options?: { | ||||
|     dryRun?: Store<boolean> | ||||
|  | @ -67,83 +68,83 @@ export class OsmConnection { | |||
|     osmConfiguration?: "osm" | "osm-test" | ||||
|     attemptLogin?: true | boolean | ||||
|   }) { | ||||
|         options = options ?? {} | ||||
|         this.fakeUser = options.fakeUser ?? false | ||||
|         this._singlePage = options.singlePage ?? true | ||||
|     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 | ||||
|       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") | ||||
|       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", | ||||
|             } | ||||
|         url: "https://api.openstreetmap.org" | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     this.userDetails = new UIEventSource<UserDetails>( | ||||
|       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 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() | ||||
|     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() | ||||
|         self.AttemptLogin(); | ||||
|       } | ||||
|         }) | ||||
|     }); | ||||
| 
 | ||||
|         this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false) | ||||
|     this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|         this.updateAuthObject() | ||||
|     this.updateAuthObject(); | ||||
| 
 | ||||
|     this.preferencesHandler = new OsmPreferences( | ||||
|       this.auth, | ||||
|       <any /*This is needed to make the tests work*/>this | ||||
|         ) | ||||
|     ); | ||||
| 
 | ||||
|     if (options.oauth_token?.data !== undefined) { | ||||
|             console.log(options.oauth_token.data) | ||||
|             const self = this | ||||
|       console.log(options.oauth_token.data); | ||||
|       const self = this; | ||||
|       this.auth.bootstrapToken( | ||||
|         options.oauth_token.data, | ||||
|         (x) => { | ||||
|                     console.log("Called back: ", x) | ||||
|                     self.AttemptLogin() | ||||
|           console.log("Called back: ", x); | ||||
|           self.AttemptLogin(); | ||||
|         }, | ||||
|         this.auth | ||||
|             ) | ||||
|       ); | ||||
| 
 | ||||
|             options.oauth_token.setData(undefined) | ||||
|       options.oauth_token.setData(undefined); | ||||
|     } | ||||
|     if (this.auth.authenticated() && options.attemptLogin !== false) { | ||||
|             this.AttemptLogin() // Also updates the user badge
 | ||||
|       this.AttemptLogin(); // Also updates the user badge
 | ||||
|     } else { | ||||
|             console.log("Not authenticated") | ||||
|       console.log("Not authenticated"); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -155,25 +156,25 @@ export class OsmConnection { | |||
|       prefix?: string | ||||
|     } | ||||
|   ): UIEventSource<string> { | ||||
|         return this.preferencesHandler.GetPreference(key, defaultValue, options) | ||||
|     return this.preferencesHandler.GetPreference(key, defaultValue, options); | ||||
|   } | ||||
| 
 | ||||
|   public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|         return this.preferencesHandler.GetLongPreference(key, prefix) | ||||
|     return this.preferencesHandler.GetLongPreference(key, prefix); | ||||
|   } | ||||
| 
 | ||||
|   public OnLoggedIn(action: (userDetails: UserDetails) => void) { | ||||
|         this._onLoggedIn.push(action) | ||||
|     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") | ||||
|     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"); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -182,95 +183,95 @@ export class OsmConnection { | |||
|    * new OsmConnection().Backend() // => "https://www.openstreetmap.org"
 | ||||
|    */ | ||||
|   public Backend(): string { | ||||
|         return this._oauth_config.url | ||||
|     return this._oauth_config.url; | ||||
|   } | ||||
| 
 | ||||
|   public AttemptLogin() { | ||||
|         this.UpdateCapabilities() | ||||
|         this.loadingStatus.setData("loading") | ||||
|     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 | ||||
|       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() | ||||
|     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", | ||||
|         path: "/api/0.6/user/details" | ||||
|       }, | ||||
|       function(err, details) { | ||||
|         if (err != null) { | ||||
|                     console.log(err) | ||||
|                     self.loadingStatus.setData("error") | ||||
|           console.log(err); | ||||
|           self.loadingStatus.setData("error"); | ||||
|           if (err.status == 401) { | ||||
|                         console.log("Clearing tokens...") | ||||
|             console.log("Clearing tokens..."); | ||||
|             // Not authorized - our token probably got revoked
 | ||||
|                         self.auth.logout() | ||||
|                         self.LogOut() | ||||
|             self.auth.logout(); | ||||
|             self.LogOut(); | ||||
|           } | ||||
|                     return | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         if (details == null) { | ||||
|                     self.loadingStatus.setData("error") | ||||
|                     return | ||||
|           self.loadingStatus.setData("error"); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|                 self.CheckForMessagesContinuously() | ||||
|         self.CheckForMessagesContinuously(); | ||||
| 
 | ||||
|         // details is an XML DOM of user details
 | ||||
|                 let userInfo = details.getElementsByTagName("user")[0] | ||||
|         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")) | ||||
|         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") | ||||
|         data.img = undefined; | ||||
|         const imgEl = userInfo.getElementsByTagName("img"); | ||||
|         if (imgEl !== undefined && imgEl[0] !== undefined) { | ||||
|                     data.img = imgEl[0].getAttribute("href") | ||||
|           data.img = imgEl[0].getAttribute("href"); | ||||
|         } | ||||
| 
 | ||||
|                 const description = userInfo.getElementsByTagName("description") | ||||
|         const description = userInfo.getElementsByTagName("description"); | ||||
|         if (description !== undefined && description[0] !== undefined) { | ||||
|                     data.description = description[0]?.innerHTML | ||||
|           data.description = description[0]?.innerHTML; | ||||
|         } | ||||
|                 const homeEl = userInfo.getElementsByTagName("home") | ||||
|         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 } | ||||
|           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") | ||||
|         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")) | ||||
|           .getElementsByTagName("received")[0]; | ||||
|         data.unreadMessages = parseInt(messages.getAttribute("unread")); | ||||
|         data.totalMessages = parseInt(messages.getAttribute("count")); | ||||
| 
 | ||||
|                 self.userDetails.ping() | ||||
|         self.userDetails.ping(); | ||||
|         for (const action of self._onLoggedIn) { | ||||
|                     action(self.userDetails.data) | ||||
|           action(self.userDetails.data); | ||||
|         } | ||||
|                 self._onLoggedIn = [] | ||||
|         self._onLoggedIn = []; | ||||
|       } | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -289,20 +290,20 @@ export class OsmConnection { | |||
|         { | ||||
|           method, | ||||
|           options: { | ||||
|                         header, | ||||
|             header | ||||
|           }, | ||||
|           content, | ||||
|                     path: `/api/0.6/${path}`, | ||||
|           path: `/api/0.6/${path}` | ||||
|         }, | ||||
|         function(err, response) { | ||||
|           if (err !== null) { | ||||
|                         error(err) | ||||
|             error(err); | ||||
|           } else { | ||||
|                         ok(response) | ||||
|             ok(response); | ||||
|           } | ||||
|         } | ||||
|             ) | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async post( | ||||
|  | @ -310,7 +311,7 @@ export class OsmConnection { | |||
|     content?: string, | ||||
|     header?: Record<string, string | number> | ||||
|   ): Promise<any> { | ||||
|         return await this.interact(path, "POST", header, content) | ||||
|     return await this.interact(path, "POST", header, content); | ||||
|   } | ||||
| 
 | ||||
|   public async put( | ||||
|  | @ -318,59 +319,60 @@ export class OsmConnection { | |||
|     content?: string, | ||||
|     header?: Record<string, string | number> | ||||
|   ): Promise<any> { | ||||
|         return await this.interact(path, "PUT", header, content) | ||||
|     return await this.interact(path, "PUT", header, content); | ||||
|   } | ||||
| 
 | ||||
|   public async get(path: string, header?: Record<string, string | number>): Promise<any> { | ||||
|         return await this.interact(path, "GET", header) | ||||
|     return await this.interact(path, "GET", header); | ||||
|   } | ||||
| 
 | ||||
|   public closeNote(id: number | string, text?: string): Promise<void> { | ||||
|         let textSuffix = "" | ||||
|     let textSuffix = ""; | ||||
|     if ((text ?? "") !== "") { | ||||
|             textSuffix = "?text=" + encodeURIComponent(text) | ||||
|       textSuffix = "?text=" + encodeURIComponent(text); | ||||
|     } | ||||
|     if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) | ||||
|       console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text); | ||||
|       return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }) | ||||
|         ok(); | ||||
|       }); | ||||
|     } | ||||
|         return this.post(`notes/${id}/close${textSuffix}`) | ||||
|     return this.post(`notes/${id}/close${textSuffix}`); | ||||
|   } | ||||
| 
 | ||||
|   public reopenNote(id: number | string, text?: string): Promise<void> { | ||||
|     if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) | ||||
|       console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text); | ||||
|       return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }) | ||||
|         ok(); | ||||
|       }); | ||||
|     } | ||||
|         let textSuffix = "" | ||||
|     let textSuffix = ""; | ||||
|     if ((text ?? "") !== "") { | ||||
|             textSuffix = "?text=" + encodeURIComponent(text) | ||||
|       textSuffix = "?text=" + encodeURIComponent(text); | ||||
|     } | ||||
|         return this.post(`notes/${id}/reopen${textSuffix}`) | ||||
|     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) | ||||
|       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 | ||||
|     // 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( | ||||
|  | @ -388,61 +390,61 @@ export class OsmConnection { | |||
|     } | ||||
|   ): Promise<{ id: number }> { | ||||
|     if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually uploading GPX ", gpx) | ||||
|       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 contents = { | ||||
|       file: gpx, | ||||
|       description: options.description ?? "", | ||||
|       tags: options.labels?.join(",") ?? "", | ||||
|             visibility: options.visibility, | ||||
|         } | ||||
|       visibility: options.visibility | ||||
|     }; | ||||
| 
 | ||||
|     const extras = { | ||||
|       file: | ||||
|                 '; filename="' + | ||||
|         "; filename=\"" + | ||||
|         (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + | ||||
|                 '"\r\nContent-Type: application/gpx+xml', | ||||
|         } | ||||
|         "\"\r\nContent-Type: application/gpx+xml" | ||||
|     }; | ||||
| 
 | ||||
|         const boundary = "987654" | ||||
|     const boundary = "987654"; | ||||
| 
 | ||||
|         let body = "" | ||||
|     let body = ""; | ||||
|     for (const key in contents) { | ||||
|             body += "--" + boundary + "\r\n" | ||||
|             body += 'Content-Disposition: form-data; name="' + key + '"' | ||||
|       body += "--" + boundary + "\r\n"; | ||||
|       body += "Content-Disposition: form-data; name=\"" + key + "\""; | ||||
|       if (extras[key] !== undefined) { | ||||
|                 body += extras[key] | ||||
|         body += extras[key]; | ||||
|       } | ||||
|             body += "\r\n\r\n" | ||||
|             body += contents[key] + "\r\n" | ||||
|       body += "\r\n\r\n"; | ||||
|       body += contents[key] + "\r\n"; | ||||
|     } | ||||
|         body += "--" + boundary + "--\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 } | ||||
|       "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<void> { | ||||
|     if (this._dryRun.data) { | ||||
|             console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id) | ||||
|       console.warn("Dryrun enabled - not actually adding comment ", text, "to  note ", id); | ||||
|       return new Promise((ok) => { | ||||
|                 ok() | ||||
|             }) | ||||
|         ok(); | ||||
|       }); | ||||
|     } | ||||
|     if ((text ?? "") === "") { | ||||
|             throw "Invalid text!" | ||||
|       throw "Invalid text!"; | ||||
|     } | ||||
| 
 | ||||
|     return new Promise((ok, error) => { | ||||
|  | @ -450,38 +452,50 @@ export class OsmConnection { | |||
|         { | ||||
|           method: "POST", | ||||
| 
 | ||||
|                     path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, | ||||
|           path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}` | ||||
|         }, | ||||
|         function(err, _) { | ||||
|           if (err !== null) { | ||||
|                         error(err) | ||||
|             error(err); | ||||
|           } else { | ||||
|                         ok() | ||||
|             ok(); | ||||
|           } | ||||
|         } | ||||
|             ) | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * 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); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private updateAuthObject() { | ||||
|         let pwaStandAloneMode = false | ||||
|     let pwaStandAloneMode = false; | ||||
|     try { | ||||
|       if (Utils.runningFromConsole) { | ||||
|                 pwaStandAloneMode = true | ||||
|         pwaStandAloneMode = true; | ||||
|       } else if ( | ||||
|         window.matchMedia("(display-mode: standalone)").matches || | ||||
|         window.matchMedia("(display-mode: fullscreen)").matches | ||||
|       ) { | ||||
|                 pwaStandAloneMode = true | ||||
|         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 | ||||
|     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...
 | ||||
|  | @ -494,58 +508,46 @@ export class OsmConnection { | |||
|         ? "https://mapcomplete.org/land.html" | ||||
|         : window.location.protocol + "//" + window.location.host + "/land.html", | ||||
|       singlepage: !standalone, | ||||
|             auto: true, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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) | ||||
|         }) | ||||
|       auto: true | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private CheckForMessagesContinuously() { | ||||
|         const self = this | ||||
|     const self = this; | ||||
|     if (this.isChecking) { | ||||
|             return | ||||
|       return; | ||||
|     } | ||||
|         this.isChecking = true | ||||
|     this.isChecking = true; | ||||
|     Stores.Chronic(5 * 60 * 1000).addCallback((_) => { | ||||
|       if (self.isLoggedIn.data) { | ||||
|                 console.log("Checking for messages") | ||||
|                 self.AttemptLogin() | ||||
|         console.log("Checking for messages"); | ||||
|         self.AttemptLogin(); | ||||
|       } | ||||
|         }) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private UpdateCapabilities(): void { | ||||
|         const self = this | ||||
|     const self = this; | ||||
|     this.FetchCapabilities().then(({ api, gpx }) => { | ||||
|             self.apiIsOnline.setData(api) | ||||
|             self.gpxServiceIsOnline.setData(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" } | ||||
|       return { api: "online", gpx: "online" }; | ||||
|     } | ||||
|         const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities") | ||||
|     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" } | ||||
|       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 = <OsmServiceState>statusEl.getAttribute("api") | ||||
|         const gpx = <OsmServiceState>statusEl.getAttribute("gpx") | ||||
|         return { api, gpx } | ||||
|     const xmlRaw = result["content"]; | ||||
|     const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml"); | ||||
|     const statusEl = parsed.getElementsByTagName("status")[0]; | ||||
|     const api = <OsmServiceState>statusEl.getAttribute("api"); | ||||
|     const gpx = <OsmServiceState>statusEl.getAttribute("gpx"); | ||||
|     return { api, gpx }; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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<string, UIEventSource<number>>() | ||||
| 
 | ||||
|     constructor( | ||||
|         tagsSource: Store<any>, | ||||
|         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<number>(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") | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -16,6 +16,10 @@ import Svg from "../../Svg"; | |||
| export let state: SpecialVisualizationState; | ||||
| 
 | ||||
| export let tags: Store<OsmTags>; | ||||
| /** | ||||
|  * 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); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,111 +0,0 @@ | |||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { InputElement } from "./InputElement" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated | ||||
|  */ | ||||
| export default class FileSelectorButton extends InputElement<FileList> { | ||||
|     private static _nextid = 0 | ||||
|     private readonly _value = new UIEventSource<FileList>(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<FileList> { | ||||
|         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 | ||||
|     } | ||||
| } | ||||
|  | @ -1,62 +0,0 @@ | |||
| import { InputElement } from "./InputElement" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated | ||||
|  */ | ||||
| export default class Slider extends InputElement<number> { | ||||
|     private readonly _value: UIEventSource<number> | ||||
|     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<number> | ||||
|             step?: 1 | number | ||||
|             vertical?: false | boolean | ||||
|         } | ||||
|     ) { | ||||
|         super() | ||||
|         this.max = max | ||||
|         this.min = min | ||||
|         this._value = options?.value ?? new UIEventSource<number>(min) | ||||
|         this.step = options?.step ?? 1 | ||||
|         this.vertical = options?.vertical ?? false | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<number> { | ||||
|         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 | ||||
|     } | ||||
| } | ||||
|  | @ -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")) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<Record<string, string>> }; | ||||
|   readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>>, trackFeature?(feature: { properties: OsmTags }) }; | ||||
| 
 | ||||
|   readonly indexedFeatures: IndexedFeatureSource; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue