forked from MapComplete/MapComplete
		
	Chore: rework image uploading, should work better now
This commit is contained in:
		
							parent
							
								
									6f5b0622a5
								
							
						
					
					
						commit
						94ba18785d
					
				
					 17 changed files with 548 additions and 238 deletions
				
			
		|  | @ -344,8 +344,8 @@ | ||||||
|         }, |         }, | ||||||
|         "useSearch": "Use the search above to see presets", |         "useSearch": "Use the search above to see presets", | ||||||
|         "useSearchForMore": "Use the search function to search within {total} more values…", |         "useSearchForMore": "Use the search function to search within {total} more values…", | ||||||
|         "waitingForGeopermission": "Waiting for your permission to use the geolocation...", |         "waitingForGeopermission": "Waiting for your permission to use the geolocation…", | ||||||
|         "waitingForLocation": "Searching your current location...", |         "waitingForLocation": "Searching your current location…", | ||||||
|         "weekdays": { |         "weekdays": { | ||||||
|             "abbreviations": { |             "abbreviations": { | ||||||
|                 "friday": "Fri", |                 "friday": "Fri", | ||||||
|  | @ -416,6 +416,22 @@ | ||||||
|         "pleaseLogin": "Please log in to add a picture", |         "pleaseLogin": "Please log in to add a picture", | ||||||
|         "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", |         "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", | ||||||
|         "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", |         "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", | ||||||
|  |         "upload": { | ||||||
|  |             "failReasons": "You might have lost connection to the internet", | ||||||
|  |             "failReasonsAdvanced": "Alternatively, make sure your browser and extensions do not block third-party API's.", | ||||||
|  |             "multiple": { | ||||||
|  |                 "done": "{count} images are successfully uploaded. Thank you!", | ||||||
|  |                 "partiallyDone": "{count} images are getting uploaded, {done} images are done…", | ||||||
|  |                 "someFailed": "Sorry, we could not upload {count} images", | ||||||
|  |                 "uploading": "{count} images are getting uploaded…" | ||||||
|  |             }, | ||||||
|  |             "one": { | ||||||
|  |                 "done": "Your image was successfully uploaded. Thank you!", | ||||||
|  |                 "failed": "Sorry, we could not upload your image", | ||||||
|  |                 "retrying": "Your image is getting uploaded again…", | ||||||
|  |                 "uploading": "Your image is getting uploaded…" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "uploadDone": "Your picture has been added. Thanks for helping out!", |         "uploadDone": "Your picture has been added. Thanks for helping out!", | ||||||
|         "uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.", |         "uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.", | ||||||
|         "uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!", |         "uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!", | ||||||
|  |  | ||||||
|  | @ -2682,26 +2682,6 @@ a.link-underline { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (prefers-reduced-motion: reduce) { |  | ||||||
|   @-webkit-keyframes spin { |  | ||||||
|     to { |  | ||||||
|       -webkit-transform: rotate(360deg); |  | ||||||
|               transform: rotate(360deg); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   @keyframes spin { |  | ||||||
|     to { |  | ||||||
|       -webkit-transform: rotate(360deg); |  | ||||||
|               transform: rotate(360deg); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .motion-reduce\:animate-spin { |  | ||||||
|     -webkit-animation: spin 1s linear infinite; |  | ||||||
|             animation: spin 1s linear infinite; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media (max-width: 480px) { | @media (max-width: 480px) { | ||||||
|   .max-\[480px\]\:w-full { |   .max-\[480px\]\:w-full { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { OsmTags } from "../../../Models/OsmFeature" | ||||||
|  */ |  */ | ||||||
| export default class FeaturePropertiesStore { | export default class FeaturePropertiesStore { | ||||||
|     private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>() |     private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>() | ||||||
| 
 |     public readonly aliases = new Map<string, string>() | ||||||
|     constructor(...sources: FeatureSource[]) { |     constructor(...sources: FeatureSource[]) { | ||||||
|         for (const source of sources) { |         for (const source of sources) { | ||||||
|             this.trackFeatureSource(source) |             this.trackFeatureSource(source) | ||||||
|  | @ -92,7 +92,6 @@ export default class FeaturePropertiesStore { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // noinspection JSUnusedGlobalSymbols
 |  | ||||||
|     public addAlias(oldId: string, newId: string): void { |     public addAlias(oldId: string, newId: string): void { | ||||||
|         if (newId === undefined) { |         if (newId === undefined) { | ||||||
|             // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 |             // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 | ||||||
|  | @ -112,6 +111,7 @@ export default class FeaturePropertiesStore { | ||||||
|         } |         } | ||||||
|         element.data.id = newId |         element.data.id = newId | ||||||
|         this._elements.set(newId, element) |         this._elements.set(newId, element) | ||||||
|  |         this.aliases.set(newId, oldId) | ||||||
|         element.ping() |         element.ping() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,150 @@ | ||||||
|  | import { ImageUploader } from "./ImageUploader"; | ||||||
|  | import LinkImageAction from "../Osm/Actions/LinkImageAction"; | ||||||
|  | import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"; | ||||||
|  | import { OsmId, OsmTags } from "../../Models/OsmFeature"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import { Store, UIEventSource } from "../UIEventSource"; | ||||||
|  | import { OsmConnection } from "../Osm/OsmConnection"; | ||||||
|  | import { Changes } from "../Osm/Changes"; | ||||||
|  | import Translations from "../../UI/i18n/Translations"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The ImageUploadManager has a | ||||||
|  |  */ | ||||||
|  | export class ImageUploadManager { | ||||||
|  | 
 | ||||||
|  |   private readonly _uploader: ImageUploader; | ||||||
|  |   private readonly _featureProperties: FeaturePropertiesStore; | ||||||
|  |   private readonly _layout: LayoutConfig; | ||||||
|  | 
 | ||||||
|  |   private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map(); | ||||||
|  |   private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map(); | ||||||
|  |   private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map(); | ||||||
|  |   private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map(); | ||||||
|  |   private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map(); | ||||||
|  |   private readonly _osmConnection: OsmConnection; | ||||||
|  |   private readonly _changes: Changes; | ||||||
|  | 
 | ||||||
|  |   constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) { | ||||||
|  |     this._uploader = uploader; | ||||||
|  |     this._featureProperties = featureProperties; | ||||||
|  |     this._layout = layout; | ||||||
|  |     this._osmConnection = osmConnection; | ||||||
|  |     this._changes = changes; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Gets various counters. | ||||||
|  |    * Note that counters can only increase | ||||||
|  |    * If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased | ||||||
|  |    * @param featureId: the id of the feature you want information for. '*' has a global counter | ||||||
|  |    */ | ||||||
|  |   public getCountsFor(featureId: string | "*"): { | ||||||
|  |     retried: Store<number>; | ||||||
|  |     uploadStarted: Store<number>; | ||||||
|  |     retrySuccess: Store<number>; | ||||||
|  |     failed: Store<number>; | ||||||
|  |     uploadFinished: Store<number> | ||||||
|  |   } { | ||||||
|  |     return { | ||||||
|  |       uploadStarted: this.getCounterFor(this._uploadStarted, featureId), | ||||||
|  |       uploadFinished: this.getCounterFor(this._uploadFinished, featureId), | ||||||
|  |       retried: this.getCounterFor(this._uploadRetried, featureId), | ||||||
|  |       failed: this.getCounterFor(this._uploadFailed, featureId), | ||||||
|  |       retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId) | ||||||
|  | 
 | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Uploads the given image, applies the correct title and license for the known user | ||||||
|  |    */ | ||||||
|  |   public async uploadImageAndApply(file: File, tags: OsmTags) { | ||||||
|  | 
 | ||||||
|  |       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 | ||||||
|  |       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"); | ||||||
|  |     const license = licenseStore?.data ?? "CC0"; | ||||||
|  | 
 | ||||||
|  |     const matchingLayer = this._layout?.getMatchingLayer(tags); | ||||||
|  | 
 | ||||||
|  |     const title = | ||||||
|  |       matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ?? | ||||||
|  |       tags.name ?? | ||||||
|  |       "https//osm.org/" + tags.id; | ||||||
|  |     const description = [ | ||||||
|  |       "author:" + this._osmConnection.userDetails.data.name, | ||||||
|  |       "license:" + license, | ||||||
|  |       "osmid:" + tags.id | ||||||
|  |     ].join("\n"); | ||||||
|  | 
 | ||||||
|  |     console.log("Upload done, creating ") | ||||||
|  |     const action = await this.uploadImageWithLicense(featureId, title, description, file); | ||||||
|  |     await this._changes.applyAction(action); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async uploadImageWithLicense( | ||||||
|  |     featureId: OsmId, | ||||||
|  |     title: string, description: string, blob: File | ||||||
|  |   ): Promise<LinkImageAction> { | ||||||
|  |     this.increaseCountFor(this._uploadStarted, featureId); | ||||||
|  |     const properties = this._featureProperties.getStore(featureId); | ||||||
|  |     let key: string; | ||||||
|  |     let value: string; | ||||||
|  |     try { | ||||||
|  |       ({ key, value } = await this._uploader.uploadImage(title, description, blob)); | ||||||
|  |     } catch (e) { | ||||||
|  |       this.increaseCountFor(this._uploadRetried, featureId); | ||||||
|  |       console.error("Could not upload image, trying again:", e); | ||||||
|  |       try { | ||||||
|  | 
 | ||||||
|  |         ({ key, value } = await this._uploader.uploadImage(title, description, blob)); | ||||||
|  |         this.increaseCountFor(this._uploadRetriedSuccess, featureId); | ||||||
|  |       } catch (e) { | ||||||
|  |         console.error("Could again not upload image due to", e); | ||||||
|  |         this.increaseCountFor(this._uploadFailed, featureId); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |     console.log("Uploading done, creating action for", featureId) | ||||||
|  |     const action = new LinkImageAction(featureId, key, value, properties, { | ||||||
|  |       theme: this._layout.id, | ||||||
|  |       changeType: "add-image" | ||||||
|  |     }); | ||||||
|  |     this.increaseCountFor(this._uploadFinished, featureId); | ||||||
|  |     return action; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") { | ||||||
|  |     if (this._featureProperties.aliases.has(key)) { | ||||||
|  |       key = this._featureProperties.aliases.get(key); | ||||||
|  |     } | ||||||
|  |     if (!collection.has(key)) { | ||||||
|  |       collection.set(key, new UIEventSource<number>(0)); | ||||||
|  |     } | ||||||
|  |     return collection.get(key); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") { | ||||||
|  |     const counter = this.getCounterFor(collection, key); | ||||||
|  |     counter.setData(counter.data + 1); | ||||||
|  |     const global = this.getCounterFor(collection, "*"); | ||||||
|  |     global.setData(counter.data + 1); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | export interface ImageUploader { | ||||||
|  |   maxFileSizeInMegabytes?: number; | ||||||
|  |     /** | ||||||
|  |      * Uploads the 'blob' as image, with some metadata. | ||||||
|  |      * Returns the URL to be linked + the appropriate key to add this to OSM | ||||||
|  |      * @param title | ||||||
|  |      * @param description | ||||||
|  |      * @param blob | ||||||
|  |      */ | ||||||
|  |     uploadImage( | ||||||
|  |         title: string, | ||||||
|  |         description: string, | ||||||
|  |         blob: File | ||||||
|  |     ): Promise<{ key: string, value: string }>; | ||||||
|  | } | ||||||
|  | @ -1,60 +1,30 @@ | ||||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | import ImageProvider, { ProvidedImage } from "./ImageProvider"; | ||||||
| import BaseUIElement from "../../UI/BaseUIElement" | import BaseUIElement from "../../UI/BaseUIElement"; | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils"; | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants"; | ||||||
| import { LicenseInfo } from "./LicenseInfo" | import { LicenseInfo } from "./LicenseInfo"; | ||||||
|  | import { ImageUploader } from "./ImageUploader"; | ||||||
| 
 | 
 | ||||||
| export class Imgur extends ImageProvider { | export class Imgur extends ImageProvider implements ImageUploader{ | ||||||
|     public static readonly defaultValuePrefix = ["https://i.imgur.com"] |     public static readonly defaultValuePrefix = ["https://i.imgur.com"] | ||||||
|     public static readonly singleton = new Imgur() |     public static readonly singleton = new Imgur() | ||||||
|     public readonly defaultKeyPrefixes: string[] = ["image"] |     public readonly defaultKeyPrefixes: string[] = ["image"] | ||||||
| 
 |     public readonly  maxFileSizeInMegabytes = 10 | ||||||
|     private constructor() { |     private constructor() { | ||||||
|         super() |         super() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static uploadMultiple( |     /** | ||||||
|  |      * Uploads an image, returns the URL where to find the image | ||||||
|  |      * @param title | ||||||
|  |      * @param description | ||||||
|  |      * @param blob | ||||||
|  |      */ | ||||||
|  |     public async uploadImage( | ||||||
|         title: string, |         title: string, | ||||||
|         description: string, |         description: string, | ||||||
|         blobs: FileList, |         blob: File | ||||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, |     ): Promise<{ key: string, value: string }> { | ||||||
|         allDone: () => void, |  | ||||||
|         onFail: (reason: string) => void, |  | ||||||
|         offset: number = 0 |  | ||||||
|     ) { |  | ||||||
|         if (blobs.length == offset) { |  | ||||||
|             allDone() |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
|         const blob = blobs.item(offset) |  | ||||||
|         const self = this |  | ||||||
|         this.uploadImage( |  | ||||||
|             title, |  | ||||||
|             description, |  | ||||||
|             blob, |  | ||||||
|             async (imageUrl) => { |  | ||||||
|                 await handleSuccessfullUpload(imageUrl) |  | ||||||
|                 self.uploadMultiple( |  | ||||||
|                     title, |  | ||||||
|                     description, |  | ||||||
|                     blobs, |  | ||||||
|                     handleSuccessfullUpload, |  | ||||||
|                     allDone, |  | ||||||
|                     onFail, |  | ||||||
|                     offset + 1 |  | ||||||
|                 ) |  | ||||||
|             }, |  | ||||||
|             onFail |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     static uploadImage( |  | ||||||
|         title: string, |  | ||||||
|         description: string, |  | ||||||
|         blob: File, |  | ||||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, |  | ||||||
|         onFail: (reason: string) => void |  | ||||||
|     ) { |  | ||||||
|         const apiUrl = "https://api.imgur.com/3/image" |         const apiUrl = "https://api.imgur.com/3/image" | ||||||
|         const apiKey = Constants.ImgurApiKey |         const apiKey = Constants.ImgurApiKey | ||||||
| 
 | 
 | ||||||
|  | @ -63,6 +33,7 @@ export class Imgur extends ImageProvider { | ||||||
|         formData.append("title", title) |         formData.append("title", title) | ||||||
|         formData.append("description", description) |         formData.append("description", description) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         const settings: RequestInit = { |         const settings: RequestInit = { | ||||||
|             method: "POST", |             method: "POST", | ||||||
|             body: formData, |             body: formData, | ||||||
|  | @ -74,17 +45,9 @@ export class Imgur extends ImageProvider { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Response contains stringified JSON
 |         // Response contains stringified JSON
 | ||||||
|         // Image URL available at response.data.link
 |         const response = await fetch(apiUrl, settings) | ||||||
|         fetch(apiUrl, settings) |         const content = await response.json() | ||||||
|             .then(async function (response) { |         return { key: "image", value: content.data.link } | ||||||
|                 const content = await response.json() |  | ||||||
|                 await handleSuccessfullUpload(content.data.link) |  | ||||||
|             }) |  | ||||||
|             .catch((reason) => { |  | ||||||
|                 console.log("Uploading to IMGUR failed", reason) |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 onFail(reason) |  | ||||||
|             }) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SourceIcon(): BaseUIElement { |     SourceIcon(): BaseUIElement { | ||||||
|  |  | ||||||
|  | @ -1,11 +1,20 @@ | ||||||
| import ChangeTagAction from "./ChangeTagAction" | import ChangeTagAction from "./ChangeTagAction"; | ||||||
| import { Tag } from "../../Tags/Tag" | import { Tag } from "../../Tags/Tag"; | ||||||
|  | import OsmChangeAction from "./OsmChangeAction"; | ||||||
|  | import { Changes } from "../Changes"; | ||||||
|  | import { ChangeDescription } from "./ChangeDescription"; | ||||||
|  | import { Store } from "../../UIEventSource"; | ||||||
|  | 
 | ||||||
|  | export default class LinkImageAction extends OsmChangeAction { | ||||||
|  |     private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string; | ||||||
|  |     private readonly _url: string; | ||||||
|  |     private readonly _currentTags: Store<Record<string, string>>; | ||||||
|  |     private readonly _meta: { theme: string; changeType: "add-image" | "link-image" }; | ||||||
| 
 | 
 | ||||||
| export default class LinkPicture extends ChangeTagAction { |  | ||||||
|     /** |     /** | ||||||
|      * Adds a link to an image |      * Adds an image-link to a feature | ||||||
|      * @param elementId |      * @param elementId | ||||||
|      * @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed) |      * @param proposedKey a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed) | ||||||
|      * @param url |      * @param url | ||||||
|      * @param currentTags |      * @param currentTags | ||||||
|      * @param meta |      * @param meta | ||||||
|  | @ -15,18 +24,31 @@ export default class LinkPicture extends ChangeTagAction { | ||||||
|         elementId: string, |         elementId: string, | ||||||
|         proposedKey: "image" | "mapillary" | "wiki_commons" | string, |         proposedKey: "image" | "mapillary" | "wiki_commons" | string, | ||||||
|         url: string, |         url: string, | ||||||
|         currentTags: Record<string, string>, |         currentTags: Store<Record<string, string>>, | ||||||
|         meta: { |         meta: { | ||||||
|             theme: string |             theme: string | ||||||
|             changeType: "add-image" | "link-image" |             changeType: "add-image" | "link-image" | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         let key = proposedKey |         super(elementId, true) | ||||||
|  |         this._proposedKey = proposedKey; | ||||||
|  |         this._url = url; | ||||||
|  |         this._currentTags = currentTags; | ||||||
|  |         this._meta = meta; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected CreateChangeDescriptions(): Promise<ChangeDescription[]> { | ||||||
|  |         let key = this._proposedKey | ||||||
|         let i = 0 |         let i = 0 | ||||||
|  |         const currentTags = this._currentTags.data | ||||||
|  |         const url = this._url | ||||||
|         while (currentTags[key] !== undefined && currentTags[key] !== url) { |         while (currentTags[key] !== undefined && currentTags[key] !== url) { | ||||||
|             key = proposedKey + ":" + i |             key = this._proposedKey + ":" + i | ||||||
|             i++ |             i++ | ||||||
|         } |         } | ||||||
|         super(elementId, new Tag(key, url), currentTags, meta) |         const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta) | ||||||
|  |         return tagChangeAction.CreateChangeDescriptions() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,6 +19,9 @@ export default abstract class OsmChangeAction { | ||||||
|     constructor(mainObjectId: string, trackStatistics: boolean = true) { |     constructor(mainObjectId: string, trackStatistics: boolean = true) { | ||||||
|         this.trackStatistics = trackStatistics |         this.trackStatistics = trackStatistics | ||||||
|         this.mainObjectId = mainObjectId |         this.mainObjectId = mainObjectId | ||||||
|  |         if(mainObjectId === undefined || mainObjectId === null){ | ||||||
|  |             throw "OsmObject received '"+mainObjectId+"' as mainObjectId" | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Perform(changes: Changes) { |     public async Perform(changes: Changes) { | ||||||
|  |  | ||||||
|  | @ -1,22 +1,22 @@ | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection"; | ||||||
| import { MangroveIdentity } from "../Web/MangroveReviews" | import { MangroveIdentity } from "../Web/MangroveReviews"; | ||||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | import { Store, Stores, UIEventSource } from "../UIEventSource"; | ||||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" | import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"; | ||||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | import { FeatureSource } from "../FeatureSource/FeatureSource"; | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson"; | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils"; | ||||||
| import translators from "../../assets/translators.json" | import translators from "../../assets/translators.json"; | ||||||
| import codeContributors from "../../assets/contributors.json" | import codeContributors from "../../assets/contributors.json"; | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
| import usersettings from "../../../src/assets/generated/layers/usersettings.json" | import usersettings from "../../../src/assets/generated/layers/usersettings.json"; | ||||||
| import Locale from "../../UI/i18n/Locale" | import Locale from "../../UI/i18n/Locale"; | ||||||
| import LinkToWeblate from "../../UI/Base/LinkToWeblate" | import LinkToWeblate from "../../UI/Base/LinkToWeblate"; | ||||||
| import FeatureSwitchState from "./FeatureSwitchState" | import FeatureSwitchState from "./FeatureSwitchState"; | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants"; | ||||||
| import { QueryParameters } from "../Web/QueryParameters" | import { QueryParameters } from "../Web/QueryParameters"; | ||||||
| import { ThemeMetaTagging } from "./UserSettingsMetaTagging" | import { ThemeMetaTagging } from "./UserSettingsMetaTagging"; | ||||||
| import { MapProperties } from "../../Models/MapProperties"; | import { MapProperties } from "../../Models/MapProperties"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -43,7 +43,7 @@ export default class UserRelatedState { | ||||||
|     public readonly homeLocation: FeatureSource |     public readonly homeLocation: FeatureSource | ||||||
|     public readonly language: UIEventSource<string> |     public readonly language: UIEventSource<string> | ||||||
|     public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined> |     public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined> | ||||||
|     public readonly preferredBackgroundLayerForTheme: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined> |     public readonly imageLicense : UIEventSource<string> | ||||||
|     /** |     /** | ||||||
|      * The number of seconds that the GPS-locations are stored in memory. |      * The number of seconds that the GPS-locations are stored in memory. | ||||||
|      * Time in seconds |      * Time in seconds | ||||||
|  | @ -108,6 +108,9 @@ export default class UserRelatedState { | ||||||
|             documentation: "The ID of a layer or layer category that MapComplete uses by default" |             documentation: "The ID of a layer or layer category that MapComplete uses by default" | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |         this.imageLicense =  this.osmConnection.GetPreference("pictures-license", "CC0", { | ||||||
|  |             documentation: "The license under which new images are uploaded" | ||||||
|  |         }) | ||||||
|         this.installedUserThemes = this.InitInstalledUserThemes() |         this.installedUserThemes = this.InitInstalledUserThemes() | ||||||
| 
 | 
 | ||||||
|         this.homeLocation = this.initHomeLocation() |         this.homeLocation = this.initHomeLocation() | ||||||
|  |  | ||||||
|  | @ -51,6 +51,8 @@ import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; | ||||||
| import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; | import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; | ||||||
| import FilteredLayer from "./FilteredLayer"; | import FilteredLayer from "./FilteredLayer"; | ||||||
| import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; | import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; | ||||||
|  | import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; | ||||||
|  | import { Imgur } from "../Logic/ImageProviders/Imgur"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  | @ -99,6 +101,8 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     readonly userRelatedState: UserRelatedState; |     readonly userRelatedState: UserRelatedState; | ||||||
|     readonly geolocation: GeoLocationHandler; |     readonly geolocation: GeoLocationHandler; | ||||||
| 
 | 
 | ||||||
|  |     readonly imageUploadManager: ImageUploadManager | ||||||
|  | 
 | ||||||
|     readonly lastClickObject: WritableFeatureSource; |     readonly lastClickObject: WritableFeatureSource; | ||||||
|     readonly overlayLayerStates: ReadonlyMap< |     readonly overlayLayerStates: ReadonlyMap< | ||||||
|         string, |         string, | ||||||
|  | @ -168,6 +172,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
| 
 | 
 | ||||||
|         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); |         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); |         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); | ||||||
| 
 | 
 | ||||||
|  | @ -323,6 +328,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.perLayerFiltered = this.showNormalDataOn(this.map); |         this.perLayerFiltered = this.showNormalDataOn(this.map); | ||||||
| 
 | 
 | ||||||
|         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; |         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; | ||||||
|  |         this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes) | ||||||
| 
 | 
 | ||||||
|         this.initActors(); |         this.initActors(); | ||||||
|         this.addLastClick(lastClick); |         this.addLastClick(lastClick); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   import { createEventDispatcher } from "svelte"; | ||||||
|  |   import { twMerge } from "tailwind-merge"; | ||||||
|  | 
 | ||||||
|  |   export let accept: string; | ||||||
|  |   export let multiple: boolean = true; | ||||||
|  | 
 | ||||||
|  |   const dispatcher = createEventDispatcher<{ submit: FileList }>(); | ||||||
|  |   export let cls: string = ""; | ||||||
|  |   let drawAttention = false; | ||||||
|  |   let inputElement: HTMLInputElement; | ||||||
|  |   let id = Math.random() * 1000000000 + ""; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <form> | ||||||
|  |   <label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}> | ||||||
|  |     <slot /> | ||||||
|  | 
 | ||||||
|  |   </label> | ||||||
|  |   <input {accept} bind:this={inputElement} class="hidden" id={"fileinput"  + id} {multiple} name="file-input" | ||||||
|  |          on:change|preventDefault={() => { | ||||||
|  |   drawAttention = false;  | ||||||
|  |   dispatcher("submit", inputElement.files)}} | ||||||
|  |           | ||||||
|  |          on:dragend={ () => {drawAttention = false}} | ||||||
|  |          on:dragover|preventDefault|stopPropagation={(e) => { | ||||||
|  |            console.log("Dragging over!") | ||||||
|  |              drawAttention = true | ||||||
|  |              e.dataTransfer.drop = "copy" | ||||||
|  |            }} | ||||||
|  |          on:dragstart={ () => {drawAttention = false}} | ||||||
|  |          on:drop|preventDefault|stopPropagation={(e) => { | ||||||
|  |            console.log("Got a 'drop'") | ||||||
|  |           drawAttention = false | ||||||
|  |           dispatcher("submit", e.dataTransfer.files) | ||||||
|  |         }} | ||||||
|  |          type="file" | ||||||
|  |   > | ||||||
|  | </form> | ||||||
|  | @ -1,9 +1,12 @@ | ||||||
| <script> | <script lang="ts"> | ||||||
|   import ToSvelte from "./ToSvelte.svelte" |   import ToSvelte from "./ToSvelte.svelte"; | ||||||
|   import Svg from "../../Svg" |   import Svg from "../../Svg"; | ||||||
|  |   import { twMerge } from "tailwind-merge"; | ||||||
|  | 
 | ||||||
|  |   export let cls : string = undefined | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="flex p-1 pl-2"> | <div class={twMerge( "flex p-1 pl-2", cls)}> | ||||||
|   <div class="min-w-6 h-6 w-6 animate-spin self-center"> |   <div class="min-w-6 h-6 w-6 animate-spin self-center"> | ||||||
|     <ToSvelte construct={Svg.loading_svg()} /> |     <ToSvelte construct={Svg.loading_svg()} /> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|  | @ -15,8 +15,9 @@ import Loading from "../Base/Loading" | ||||||
| import { LoginToggle } from "../Popup/LoginButton" | import { LoginToggle } from "../Popup/LoginButton" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import exp from "constants"; | ||||||
| 
 | 
 | ||||||
| export class ImageUploadFlow extends Toggle { | export class ImageUploadFlow extends Combine { | ||||||
|     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() |     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -129,7 +130,7 @@ export class ImageUploadFlow extends Toggle { | ||||||
|             uploader.uploadMany(title, description, filelist) |             uploader.uploadMany(title, description, filelist) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const uploadFlow: BaseUIElement = new Combine([ |         super([ | ||||||
|             new VariableUiElement( |             new VariableUiElement( | ||||||
|                 uploader.queue |                 uploader.queue | ||||||
|                     .map((q) => q.length) |                     .map((q) => q.length) | ||||||
|  | @ -183,17 +184,9 @@ export class ImageUploadFlow extends Toggle { | ||||||
|                     }) |                     }) | ||||||
|                     .SetClass("underline"), |                     .SetClass("underline"), | ||||||
|             ]).SetStyle("font-size:small;"), |             ]).SetStyle("font-size:small;"), | ||||||
|         ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none") |         ]) | ||||||
|  |         this.SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none") | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|         super( |  | ||||||
|             new LoginToggle( |  | ||||||
|                 /*We can show the actual upload button!*/ |  | ||||||
|                 uploadFlow, |  | ||||||
|                 /* User not logged in*/ t.pleaseLogin.Clone(), |  | ||||||
|                 state |  | ||||||
|             ), |  | ||||||
|             undefined /* Nothing as the user badge is disabled*/, |  | ||||||
|             state?.featureSwitchUserbadge |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,73 @@ | ||||||
|  | <script lang="ts">/** | ||||||
|  |  * Shows an 'upload'-button which will start the upload for this feature | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|  | import { Store } from "../../Logic/UIEventSource"; | ||||||
|  | import type { OsmTags } from "../../Models/OsmFeature"; | ||||||
|  | import LoginToggle from "../Base/LoginToggle.svelte"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import Tr from "../Base/Tr.svelte"; | ||||||
|  | import UploadingImageCounter from "./UploadingImageCounter.svelte"; | ||||||
|  | import FileSelector from "../Base/FileSelector.svelte"; | ||||||
|  | import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  | import Svg from "../../Svg"; | ||||||
|  | 
 | ||||||
|  | export let state: SpecialVisualizationState; | ||||||
|  | 
 | ||||||
|  | export let tags: Store<OsmTags>; | ||||||
|  | export let image: string = undefined; | ||||||
|  | if (image === "") { | ||||||
|  |   image = undefined; | ||||||
|  | } | ||||||
|  | export let labelText: string = undefined; | ||||||
|  | const t = Translations.t.image; | ||||||
|  | 
 | ||||||
|  | let licenseStore = state.userRelatedState.imageLicense; | ||||||
|  | 
 | ||||||
|  | function handleFiles(files: FileList) { | ||||||
|  |   for (let i = 0; i < files.length; i++) { | ||||||
|  |     const file = files.item(i); | ||||||
|  |     console.log("Got file", file.name) | ||||||
|  |     try { | ||||||
|  |       state.imageUploadManager.uploadImageAndApply(file, tags.data); | ||||||
|  |     } catch (e) { | ||||||
|  |       alert(e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <LoginToggle {state}> | ||||||
|  | 
 | ||||||
|  |   <Tr slot="not-logged-in" t={t.pleaseLogin} /> | ||||||
|  |   <div class="flex flex-col"> | ||||||
|  | 
 | ||||||
|  |     <UploadingImageCounter {state} {tags} /> | ||||||
|  |     <FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true} | ||||||
|  |                   on:submit={e => handleFiles(e.detail)}> | ||||||
|  |       <div class="flex items-center"> | ||||||
|  | 
 | ||||||
|  |         {#if image !== undefined} | ||||||
|  |           <img src={image} /> | ||||||
|  |         {:else} | ||||||
|  |           <ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} /> | ||||||
|  |         {/if} | ||||||
|  |         {#if labelText} | ||||||
|  |           {labelText} | ||||||
|  |         {:else} | ||||||
|  |           <Tr t={t.addPicture} /> | ||||||
|  |         {/if} | ||||||
|  |       </div> | ||||||
|  |     </FileSelector> | ||||||
|  |     <div class="text-sm"> | ||||||
|  |       <Tr t={t.respectPrivacy} /> | ||||||
|  |       <a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}> | ||||||
|  |         <Tr t={t.currentLicense.Subs({license: $licenseStore})} /> | ||||||
|  |       </a> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | </LoginToggle> | ||||||
|  | @ -1,31 +1,67 @@ | ||||||
| <script lang="ts">/** | <script lang="ts">/** | ||||||
|  * Shows an 'upload'-button which will start the upload for this feature |  * Shows information about how much images are uploaded for the given feature | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import type { SpecialVisualizationState } from "../SpecialVisualization"; | import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
| import type { Feature } from "geojson"; |  | ||||||
| import { Store } from "../../Logic/UIEventSource"; | import { Store } from "../../Logic/UIEventSource"; | ||||||
| import type { OsmTags } from "../../Models/OsmFeature"; | import type { OsmTags } from "../../Models/OsmFeature"; | ||||||
| import { ImageUploader } from "../../Logic/ImageProviders/ImageUploader"; |  | ||||||
| import LoginToggle from "../Base/LoginToggle.svelte"; |  | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import Tr from "../Base/Tr.svelte"; | import Tr from "../Base/Tr.svelte"; | ||||||
| import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager"; | import Loading from "../Base/Loading.svelte"; | ||||||
| 
 | 
 | ||||||
| export let state: SpecialVisualizationState; | export let state: SpecialVisualizationState; | ||||||
| export let feature: Feature; |  | ||||||
| 
 |  | ||||||
| export let tags: Store<OsmTags>; | export let tags: Store<OsmTags>; | ||||||
| export let state: SpecialVisualizationState; | const featureId = tags.data.id; | ||||||
| export let lon: number; | const { | ||||||
| export let lat: number; |   uploadStarted, | ||||||
| const t = Translations.t.image |   uploadFinished, | ||||||
|  |   retried, | ||||||
|  |   failed | ||||||
|  | } = state.imageUploadManager.getCountsFor(featureId); | ||||||
|  | const t = Translations.t.image; | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | {#if $uploadStarted == 1} | ||||||
|  |   {#if $uploadFinished == 1 } | ||||||
|  |     <Tr cls="thanks" t={t.upload.one.done} /> | ||||||
|  |   {:else if $failed == 1} | ||||||
|  |     <div class="flex flex-col alert"> | ||||||
|  |       <Tr cls="self-center" t={t.upload.one.failed} /> | ||||||
|  |       <Tr t={t.upload.failReasons} /> | ||||||
|  |       <Tr t={t.upload.failReasonsAdvanced} /> | ||||||
|  |     </div> | ||||||
|  |   {:else if $retried == 1} | ||||||
|  |     <Loading cls="alert"> | ||||||
|  |       <Tr t={t.upload.one.retrying} /> | ||||||
|  |     </Loading> | ||||||
|  |   {:else } | ||||||
|  |     <Loading cls="alert"> | ||||||
|  |       <Tr t={t.upload.one.uploading} /> | ||||||
|  |     </Loading> | ||||||
|  |   {/if} | ||||||
|  | {:else if $uploadStarted > 1} | ||||||
|  |   {#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0} | ||||||
|  |     <Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} /> | ||||||
|  |   {:else if $uploadFinished == 0} | ||||||
|  |     <Loading cls="alert"> | ||||||
|  |       <Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} /> | ||||||
|  |     </Loading> | ||||||
|  |   {:else if $uploadFinished > 0} | ||||||
|  |     <Loading cls="alert"> | ||||||
|  |       <Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted -  $uploadFinished, done: $uploadFinished})} /> | ||||||
|  |     </Loading> | ||||||
|  |   {/if} | ||||||
|  |   {#if $failed > 0} | ||||||
|  |     <div class="flex flex-col alert"> | ||||||
|  |       {#if failed === 1} | ||||||
|  |         <Tr cls="self-center" t={t.upload.one.failed} /> | ||||||
|  |       {:else} | ||||||
|  |         <Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({count: $failed})} /> | ||||||
| 
 | 
 | ||||||
| <LoginToggle> |       {/if} | ||||||
|    |       <Tr t={t.upload.failReasons} /> | ||||||
|   <Tr slot="not-logged-in" t={t.pleaseLogin}/> |       <Tr t={t.upload.failReasonsAdvanced} /> | ||||||
|    |     </div> | ||||||
| </LoginToggle> |   {/if} | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -1,113 +1,117 @@ | ||||||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||||
| import BaseUIElement from "./BaseUIElement" | import BaseUIElement from "./BaseUIElement"; | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" | import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; | ||||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | import { OsmConnection } from "../Logic/Osm/OsmConnection"; | ||||||
| import { Changes } from "../Logic/Osm/Changes" | import { Changes } from "../Logic/Osm/Changes"; | ||||||
| import { ExportableMap, MapProperties } from "../Models/MapProperties" | import { ExportableMap, MapProperties } from "../Models/MapProperties"; | ||||||
| import LayerState from "../Logic/State/LayerState" | import LayerState from "../Logic/State/LayerState"; | ||||||
| import { Feature, Geometry, Point } from "geojson" | import { Feature, Geometry, Point } from "geojson"; | ||||||
| import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" | import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; | ||||||
| import { MangroveIdentity } from "../Logic/Web/MangroveReviews" | import { MangroveIdentity } from "../Logic/Web/MangroveReviews"; | ||||||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
| import FeatureSwitchState from "../Logic/State/FeatureSwitchState" | import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||||
| import { MenuState } from "../Models/MenuState" | import { MenuState } from "../Models/MenuState"; | ||||||
| import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" | import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; | ||||||
| import { RasterLayerPolygon } from "../Models/RasterLayers" | import { RasterLayerPolygon } from "../Models/RasterLayers"; | ||||||
|  | import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The state needed to render a special Visualisation. |  * The state needed to render a special Visualisation. | ||||||
|  */ |  */ | ||||||
| export interface SpecialVisualizationState { | export interface SpecialVisualizationState { | ||||||
|     readonly guistate: MenuState |   readonly guistate: MenuState; | ||||||
|     readonly layout: LayoutConfig |   readonly layout: LayoutConfig; | ||||||
|     readonly featureSwitches: FeatureSwitchState |   readonly featureSwitches: FeatureSwitchState; | ||||||
| 
 | 
 | ||||||
|     readonly layerState: LayerState |   readonly layerState: LayerState; | ||||||
|     readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> } |   readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }; | ||||||
| 
 | 
 | ||||||
|     readonly indexedFeatures: IndexedFeatureSource |   readonly indexedFeatures: IndexedFeatureSource; | ||||||
| 
 | 
 | ||||||
|     /** |   /** | ||||||
|      * Some features will create a new element that should be displayed. |    * Some features will create a new element that should be displayed. | ||||||
|      * These can be injected by appending them to this featuresource (and pinging it) |    * These can be injected by appending them to this featuresource (and pinging it) | ||||||
|      */ |    */ | ||||||
|     readonly newFeatures: WritableFeatureSource |   readonly newFeatures: WritableFeatureSource; | ||||||
| 
 | 
 | ||||||
|     readonly historicalUserLocations: WritableFeatureSource<Feature<Point>> |   readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>; | ||||||
| 
 | 
 | ||||||
|     readonly osmConnection: OsmConnection |   readonly osmConnection: OsmConnection; | ||||||
|     readonly featureSwitchUserbadge: Store<boolean> |   readonly featureSwitchUserbadge: Store<boolean>; | ||||||
|     readonly featureSwitchIsTesting: Store<boolean> |   readonly featureSwitchIsTesting: Store<boolean>; | ||||||
|     readonly changes: Changes |   readonly changes: Changes; | ||||||
|     readonly osmObjectDownloader: OsmObjectDownloader |   readonly osmObjectDownloader: OsmObjectDownloader; | ||||||
|     /** |   /** | ||||||
|      * State of the main map |    * State of the main map | ||||||
|      */ |    */ | ||||||
|     readonly mapProperties: MapProperties & ExportableMap |   readonly mapProperties: MapProperties & ExportableMap; | ||||||
| 
 | 
 | ||||||
|     readonly selectedElement: UIEventSource<Feature> |   readonly selectedElement: UIEventSource<Feature>; | ||||||
|     /** |   /** | ||||||
|      * Works together with 'selectedElement' to indicate what properties should be displayed |    * Works together with 'selectedElement' to indicate what properties should be displayed | ||||||
|      */ |    */ | ||||||
|     readonly selectedLayer: UIEventSource<LayerConfig> |   readonly selectedLayer: UIEventSource<LayerConfig>; | ||||||
|     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> |   readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; | ||||||
| 
 | 
 | ||||||
|     /** |   /** | ||||||
|      * If data is currently being fetched from external sources |    * If data is currently being fetched from external sources | ||||||
|      */ |    */ | ||||||
|     readonly dataIsLoading: Store<boolean> |   readonly dataIsLoading: Store<boolean>; | ||||||
|     /** |   /** | ||||||
|      * Only needed for 'ReplaceGeometryAction' |    * Only needed for 'ReplaceGeometryAction' | ||||||
|      */ |    */ | ||||||
|     readonly fullNodeDatabase?: FullNodeDatabaseSource |   readonly fullNodeDatabase?: FullNodeDatabaseSource; | ||||||
| 
 | 
 | ||||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> |   readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>; | ||||||
|     readonly userRelatedState: { |   readonly userRelatedState: { | ||||||
|         readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> |     readonly imageLicense: UIEventSource<string>; | ||||||
|         readonly mangroveIdentity: MangroveIdentity |     readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> |     readonly mangroveIdentity: MangroveIdentity | ||||||
|         readonly preferencesAsTags: Store<Record<string, string>> |     readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||||
|         readonly language: UIEventSource<string> |     readonly preferencesAsTags: Store<Record<string, string>> | ||||||
|     } |     readonly language: UIEventSource<string> | ||||||
|     readonly lastClickObject: WritableFeatureSource |   }; | ||||||
|  |   readonly lastClickObject: WritableFeatureSource; | ||||||
| 
 | 
 | ||||||
|     readonly availableLayers: Store<RasterLayerPolygon[]> |   readonly availableLayers: Store<RasterLayerPolygon[]>; | ||||||
|  | 
 | ||||||
|  |   readonly imageUploadManager: ImageUploadManager; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface SpecialVisualization { | export interface SpecialVisualization { | ||||||
|     readonly funcName: string |   readonly funcName: string; | ||||||
|     readonly docs: string | BaseUIElement |   readonly docs: string | BaseUIElement; | ||||||
|     readonly example?: string |   readonly example?: string; | ||||||
| 
 | 
 | ||||||
|     /** |   /** | ||||||
|      * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included |    * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included | ||||||
|      */ |    */ | ||||||
|     readonly needsNodeDatabase?: boolean |   readonly needsNodeDatabase?: boolean; | ||||||
|     readonly args: { |   readonly args: { | ||||||
|         name: string |     name: string | ||||||
|         defaultValue?: string |     defaultValue?: string | ||||||
|         doc: string |     doc: string | ||||||
|         required?: false | boolean |     required?: false | boolean | ||||||
|     }[] |   }[]; | ||||||
|     readonly getLayerDependencies?: (argument: string[]) => string[] |   readonly getLayerDependencies?: (argument: string[]) => string[]; | ||||||
| 
 | 
 | ||||||
|     structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[] |   structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]; | ||||||
| 
 | 
 | ||||||
|     constr( |   constr( | ||||||
|         state: SpecialVisualizationState, |     state: SpecialVisualizationState, | ||||||
|         tagSource: UIEventSource<Record<string, string>>, |     tagSource: UIEventSource<Record<string, string>>, | ||||||
|         argument: string[], |     argument: string[], | ||||||
|         feature: Feature, |     feature: Feature, | ||||||
|         layer: LayerConfig |     layer: LayerConfig | ||||||
|     ): BaseUIElement |   ): BaseUIElement; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RenderingSpecification = | export type RenderingSpecification = | ||||||
|     | string |   | string | ||||||
|     | { |   | { | ||||||
|           func: SpecialVisualization |   func: SpecialVisualization | ||||||
|           args: string[] |   args: string[] | ||||||
|           style: string |   style: string | ||||||
|       } | } | ||||||
|  |  | ||||||
|  | @ -35,7 +35,6 @@ import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" | ||||||
| import { SubtleButton } from "./Base/SubtleButton" | import { SubtleButton } from "./Base/SubtleButton" | ||||||
| import Svg from "../Svg" | import Svg from "../Svg" | ||||||
| import NoteCommentElement from "./Popup/NoteCommentElement" | import NoteCommentElement from "./Popup/NoteCommentElement" | ||||||
| import ImgurUploader from "../Logic/ImageProviders/ImgurUploader" |  | ||||||
| import FileSelectorButton from "./Input/FileSelectorButton" | import FileSelectorButton from "./Input/FileSelectorButton" | ||||||
| import { LoginToggle } from "./Popup/LoginButton" | import { LoginToggle } from "./Popup/LoginButton" | ||||||
| import Toggle from "./Input/Toggle" | import Toggle from "./Input/Toggle" | ||||||
|  | @ -74,6 +73,7 @@ import FediverseValidator from "./InputElement/Validators/FediverseValidator" | ||||||
| import SendEmail from "./Popup/SendEmail.svelte" | import SendEmail from "./Popup/SendEmail.svelte" | ||||||
| import NearbyImages from "./Popup/NearbyImages.svelte" | import NearbyImages from "./Popup/NearbyImages.svelte" | ||||||
| import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" | import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" | ||||||
|  | import UploadImage from "./Image/UploadImage.svelte"; | ||||||
| 
 | 
 | ||||||
| class NearbyImageVis implements SpecialVisualization { | class NearbyImageVis implements SpecialVisualization { | ||||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 |     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||||
|  | @ -616,16 +616,19 @@ export default class SpecialVisualizations { | ||||||
|                     { |                     { | ||||||
|                         name: "image-key", |                         name: "image-key", | ||||||
|                         doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", |                         doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", | ||||||
|                         defaultValue: "image", |                         required: false | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         name: "label", |                         name: "label", | ||||||
|                         doc: "The text to show on the button", |                         doc: "The text to show on the button", | ||||||
|                         defaultValue: "Add image", |                         required: false | ||||||
|                     }, |                     }, | ||||||
|                 ], |                 ], | ||||||
|                 constr: (state, tags, args) => { |                 constr: (state, tags, args) => { | ||||||
|                     return new ImageUploadFlow(tags, state, args[0], args[1]) |                     return new SvelteUIElement(UploadImage, { | ||||||
|  |                         state,tags, labelText: args[1], image: args[0] | ||||||
|  |                     }) | ||||||
|  |                    // return new ImageUploadFlow(tags, state, args[0], args[1])
 | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue