forked from MapComplete/MapComplete
		
	Refactor OsmObject to use eventsources, add first version of the delete button
This commit is contained in:
		
							parent
							
								
									ec7833b2ee
								
							
						
					
					
						commit
						bbfcee686f
					
				
					 15 changed files with 553 additions and 229 deletions
				
			
		|  | @ -154,10 +154,7 @@ export class InitUiElements { | |||
|         } | ||||
| 
 | ||||
|         State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home) | ||||
|             .addCallbackAndRun(home => { | ||||
|                 if (home === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|             .addCallbackAndRunD(home => { | ||||
|                 const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color") | ||||
|                 const icon = L.icon({ | ||||
|                     iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), | ||||
|  | @ -286,10 +283,8 @@ export class InitUiElements { | |||
|             isOpened.setData(false); | ||||
|         }) | ||||
| 
 | ||||
|         State.state.selectedElement.addCallbackAndRun(selected => { | ||||
|             if (selected !== undefined) { | ||||
|         State.state.selectedElement.addCallbackAndRunD(_ => { | ||||
|                 isOpened.setData(false); | ||||
|             } | ||||
|         }) | ||||
|         isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome") | ||||
|     } | ||||
|  | @ -337,11 +332,9 @@ export class InitUiElements { | |||
|                 copyrightButton.isEnabled.setData(false); | ||||
|             }); | ||||
| 
 | ||||
|         State.state.selectedElement.addCallbackAndRun(feature => { | ||||
|             if (feature !== undefined) { | ||||
|         State.state.selectedElement.addCallbackAndRunD(_ => { | ||||
|                 layerControlButton.isEnabled.setData(false); | ||||
|                 copyrightButton.isEnabled.setData(false); | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     } | ||||
|  |  | |||
|  | @ -61,7 +61,7 @@ export default class SelectedFeatureHandler { | |||
|             return; // No valid feature selected
 | ||||
|         } | ||||
|         // We should have a valid osm-ID and zoom to it
 | ||||
|         OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => { | ||||
|         OsmObject.DownloadObject(hash).addCallbackAndRunD(element => { | ||||
|             const centerpoint = element.centerpoint(); | ||||
|             console.log("Zooming to location for select point: ", centerpoint) | ||||
|             location.data.lat = centerpoint[0] | ||||
|  |  | |||
|  | @ -228,9 +228,9 @@ export class Changes implements FeatureSource{ | |||
|         } | ||||
| 
 | ||||
|         neededIds = Utils.Dedup(neededIds); | ||||
|         OsmObject.DownloadAll(neededIds, {}, (knownElements) => { | ||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||
|             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) | ||||
|         }); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -7,6 +7,7 @@ import State from "../../State"; | |||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| 
 | ||||
| export class ChangesetHandler { | ||||
| 
 | ||||
|  | @ -47,11 +48,20 @@ export class ChangesetHandler { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The full logic to upload a change to one or more elements. | ||||
|      * | ||||
|      * This method will attempt to reuse an existing, open changeset for this theme (or open one if none available). | ||||
|      * Then, it will upload a changes-xml within this changeset (and leave the changeset open) | ||||
|      * When upload is successfull, eventual id-rewriting will be handled (aka: don't worry about that) | ||||
|      * | ||||
|      * If 'dryrun' is specified, the changeset XML will be printed to console instead of being uploaded | ||||
|      * | ||||
|      */ | ||||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         continuation: () => void) { | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
| 
 | ||||
|         if (this.userDetails.data.csCount == 0) { | ||||
|             // The user became a contributor!
 | ||||
|  | @ -62,7 +72,6 @@ export class ChangesetHandler { | |||
|         if (this._dryRun) { | ||||
|             const changesetXML = generateChangeXML("123456"); | ||||
|             console.log(changesetXML); | ||||
|             continuation(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -97,7 +106,7 @@ export class ChangesetHandler { | |||
|                     // Mark the CS as closed...
 | ||||
|                     this.currentChangeset.setData(""); | ||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist  
 | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML, continuation); | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML); | ||||
| 
 | ||||
|                 } | ||||
|             ) | ||||
|  | @ -105,7 +114,60 @@ export class ChangesetHandler { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes the element with the given ID from the OSM database. | ||||
|      * DOES NOT PERFORM ANY SAFETY CHECKS! | ||||
|      * | ||||
|      * For the deletion of an element, a new, seperate changeset is created with a slightly changed comment and some extra flags set. | ||||
|      * The CS will be closed afterwards. | ||||
|      * | ||||
|      * If dryrun is specified, will not actually delete the point but print the CS-XML to console instead | ||||
|      * | ||||
|      */ | ||||
|     public DeleteElement(object: OsmObject, | ||||
|                          layout: LayoutConfig, | ||||
|                          reason: string, | ||||
|                          allElements: ElementStorage, | ||||
|                          continuation: () => void) { | ||||
| 
 | ||||
|         function generateChangeXML(csId: string) { | ||||
|             let [lat, lon] = object.centerpoint(); | ||||
| 
 | ||||
|             let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||
|             changes += | ||||
|                 `<delete><${object.type} id="${object.id}" version="${object.version}" changeset="${csId}" lat="${lat}" lon="${lon}" /></delete>`; | ||||
|             changes += "</osmChange>"; | ||||
| 
 | ||||
|             return changes; | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (this._dryRun) { | ||||
|             const changesetXML = generateChangeXML("123456"); | ||||
|             console.log(changesetXML); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const self = this; | ||||
|         this.OpenChangeset(layout, (csId: string) => { | ||||
|              | ||||
|             // The cs is open - let us actually upload!
 | ||||
|             const changes = generateChangeXML(csId) | ||||
|              | ||||
|             self.AddChange(csId, changes, allElements, (csId) => { | ||||
|                 console.log("Successfully deleted ", object.id) | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }, (csId) => { | ||||
|                 alert("Deletion failed... Should not happend") | ||||
|                 // FAILED
 | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }) | ||||
|         }, true, reason) | ||||
|     } | ||||
| 
 | ||||
|     private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { | ||||
|     }) { | ||||
|         if (changesetId === undefined) { | ||||
|             changesetId = this.currentChangeset.data; | ||||
|  | @ -133,15 +195,25 @@ export class ChangesetHandler { | |||
| 
 | ||||
|     private OpenChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         continuation: (changesetId: string) => void) { | ||||
|         continuation: (changesetId: string) => void, | ||||
|         isDeletionCS: boolean = false, | ||||
|         deletionReason: string = undefined) { | ||||
| 
 | ||||
|         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; | ||||
|         let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|         if (isDeletionCS) { | ||||
|             comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|             if(deletionReason){ | ||||
|                 comment += ": "+deletionReason; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let path = window.location.pathname; | ||||
|         path = path.substr(1, path.lastIndexOf("/")); | ||||
|         const metadata = [ | ||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||
|             ["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`], | ||||
|             ["comment", comment], | ||||
|             ["deletion", isDeletionCS ? "yes" : undefined], | ||||
|             ["theme", layout.id], | ||||
|             ["language", Locale.language.data], | ||||
|             ["host", window.location.host], | ||||
|  | @ -172,11 +244,21 @@ export class ChangesetHandler { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload a changesetXML | ||||
|      * @param changesetId | ||||
|      * @param changesetXML | ||||
|      * @param allElements | ||||
|      * @param continuation | ||||
|      * @param onFail | ||||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     private AddChange(changesetId: string, | ||||
|                       changesetXML: string, | ||||
|                       allElements: ElementStorage, | ||||
|                       continuation: ((changesetId: string, idMapping: any) => void), | ||||
|                       onFail: ((changesetId: string) => void) = undefined) { | ||||
|                       onFail: ((changesetId: string, reason: string) => void) = undefined) { | ||||
|         this.auth.xhr({ | ||||
|             method: 'POST', | ||||
|             options: {header: {'Content-Type': 'text/xml'}}, | ||||
|  | @ -186,7 +268,7 @@ export class ChangesetHandler { | |||
|             if (response == null) { | ||||
|                 console.log("err", err); | ||||
|                 if (onFail) { | ||||
|                     onFail(changesetId); | ||||
|                     onFail(changesetId, err); | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|  |  | |||
							
								
								
									
										207
									
								
								Logic/Osm/DeleteAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								Logic/Osm/DeleteAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,207 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Translation} from "../../UI/i18n/Translation"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| import State from "../../State"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| 
 | ||||
| export default class DeleteAction { | ||||
| 
 | ||||
|     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; | ||||
|     private readonly _id: string; | ||||
| 
 | ||||
|     constructor(id: string) { | ||||
|         this._id = id; | ||||
| 
 | ||||
|         this.canBeDeleted = new UIEventSource<{canBeDeleted?: boolean; reason: Translation}>({ | ||||
|             canBeDeleted : false, | ||||
|             reason: Translations.t.delete.loading | ||||
|         }) | ||||
|          | ||||
|         this.CheckDeleteability() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public DoDelete(reason: string): UIEventSource<boolean> { | ||||
|         const isDeleted = new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|         const self = this; | ||||
|         let deletionStarted = false; | ||||
|         this.canBeDeleted.addCallbackAndRun( | ||||
|             canBeDeleted => { | ||||
|                 if (!canBeDeleted) { | ||||
|                     // We are not allowed to delete (yet), this might change in the future though
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (isDeleted.data) { | ||||
|                     // Already deleted...
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (deletionStarted) { | ||||
|                     // Deletion is already running...
 | ||||
|                     return; | ||||
|                 } | ||||
|                 deletionStarted = true; | ||||
|                 OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => { | ||||
|                     if(obj === undefined){ | ||||
|                         return; | ||||
|                     } | ||||
|                     State.state.osmConnection.changesetHandler.DeleteElement( | ||||
|                         obj, | ||||
|                         State.state.layoutToUse.data, | ||||
|                         reason, | ||||
|                         State.state.allElements, | ||||
|                         () => { | ||||
|                             isDeleted.setData(true) | ||||
|                         } | ||||
|                     ) | ||||
|                 }) | ||||
|                | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         return isDeleted; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if the currently logged in user can delete the current point. | ||||
|      * State is written into this._canBeDeleted | ||||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     private CheckDeleteability(): void { | ||||
|         const t = Translations.t.delete; | ||||
|         const id = this._id; | ||||
|         const state = this.canBeDeleted | ||||
|         if (!id.startsWith("node")) { | ||||
|             this.canBeDeleted.setData({ | ||||
|                 canBeDeleted: false, | ||||
|                 reason: t.isntAPoint | ||||
|             }) | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Does the currently logged in user have enough experience to delete this point?
 | ||||
| 
 | ||||
|         const deletingPointsOfOtherAllowed = State.state.osmConnection.userDetails.map(ud => { | ||||
|             if (ud === undefined) { | ||||
|                 return undefined; | ||||
|             } | ||||
|             if(!ud.loggedIn){ | ||||
|                 return false; | ||||
|             } | ||||
|             return ud.csCount >= Constants.userJourney.deletePointsOfOthersUnlock; | ||||
|         }) | ||||
| 
 | ||||
|         const previousEditors = new UIEventSource<number[]>(undefined) | ||||
| 
 | ||||
|         const allByMyself = previousEditors.map(previous => { | ||||
|             if (previous === null || previous === undefined) { | ||||
|                 // Not yet downloaded
 | ||||
|                 return null; | ||||
|             } | ||||
|             const userId = State.state.osmConnection.userDetails.data.uid; | ||||
|             return !previous.some(editor => editor !== userId) | ||||
|         }, [State.state.osmConnection.userDetails]) | ||||
| 
 | ||||
| 
 | ||||
|         // User allowed OR only edited by self?
 | ||||
|         const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => { | ||||
|             if (isAllowed === undefined) { | ||||
|                 // No logged in user => definitively not allowed to delete!
 | ||||
|                 return false; | ||||
|             } | ||||
|             if (isAllowed === true) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             // At this point, the logged in user is not allowed to delete points created/edited by _others_
 | ||||
|             // however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
 | ||||
| 
 | ||||
|             if (allByMyself.data === null) { | ||||
|                 // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
 | ||||
|                 OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors) | ||||
|             } | ||||
|             if (allByMyself.data === true) { | ||||
|                 // Yay! We can download!
 | ||||
|                 return true; | ||||
|             } | ||||
|             if (allByMyself.data === false) { | ||||
|                 // Nope, downloading not allowed...
 | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             // At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
 | ||||
|             return undefined; | ||||
|         }, [allByMyself]) | ||||
| 
 | ||||
| 
 | ||||
|         const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null) | ||||
|         const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null) | ||||
|         deletetionAllowed.addCallbackAndRunD(deletetionAllowed => { | ||||
|              | ||||
|             if (deletetionAllowed === false) { | ||||
|                 // Nope, we are not allowed to delete
 | ||||
|                 state.setData({ | ||||
|                     canBeDeleted: false, | ||||
|                     reason: t.notEnoughExperience | ||||
|                 }) | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             // All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
 | ||||
|             OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => { | ||||
|                 hasRelations.setData(rels.length > 0) | ||||
|             }) | ||||
| 
 | ||||
|             OsmObject.DownloadReferencingWays(id).addCallbackAndRunD(ways => { | ||||
|                 hasWays.setData(ways.length > 0) | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const hasWaysOrRelations = hasRelations.map(hasRelationsData => { | ||||
|             if (hasRelationsData === true) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (hasWays.data === true) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (hasWays.data === false && hasRelationsData === false) { | ||||
|                 return false; | ||||
|             } | ||||
|             return null; | ||||
|         }, [hasWays]) | ||||
| 
 | ||||
|         hasWaysOrRelations.addCallbackAndRun( | ||||
|             waysOrRelations => { | ||||
|                 if (waysOrRelations == null) { | ||||
|                     // Not yet loaded - we still wait a little bit
 | ||||
|                     return; | ||||
|                 } | ||||
|                 if (waysOrRelations) { | ||||
|                     // not deleteble by mapcomplete
 | ||||
|                     state.setData({ | ||||
|                         canBeDeleted: false, | ||||
|                         reason: t.partOfOthers | ||||
|                     }) | ||||
|                 } | ||||
| 
 | ||||
|                 // alright, this point can be safely deleted!
 | ||||
|                 state.setData({ | ||||
|                     canBeDeleted: true, | ||||
|                     reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete | ||||
|                 }) | ||||
| 
 | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -8,6 +8,7 @@ import Svg from "../../Svg"; | |||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| 
 | ||||
| export default class UserDetails { | ||||
| 
 | ||||
|  | @ -20,6 +21,11 @@ export default class UserDetails { | |||
|     public totalMessages = 0; | ||||
|     public dryRun: boolean; | ||||
|     home: { lon: number; lat: number }; | ||||
|     public backend: string; | ||||
|      | ||||
|     constructor(backend: string) { | ||||
|         this.backend = backend; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class OsmConnection { | ||||
|  | @ -62,9 +68,10 @@ export class OsmConnection { | |||
|         this._singlePage = singlePage; | ||||
|         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
| 
 | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails"); | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); | ||||
|         this.userDetails.data.dryRun = dryRun; | ||||
|         const self =this; | ||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { | ||||
|  | @ -103,10 +110,8 @@ export class OsmConnection { | |||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         continuation: () => void = () => { | ||||
|         }) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation); | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); | ||||
|     } | ||||
| 
 | ||||
|     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|  |  | |||
|  | @ -1,10 +1,17 @@ | |||
| import * as $ from "jquery" | ||||
| import {Utils} from "../../Utils"; | ||||
| import * as polygon_features from "../../assets/polygon-features.json"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| 
 | ||||
| 
 | ||||
| export abstract class OsmObject { | ||||
| 
 | ||||
|     protected static backendURL = "https://www.openstreetmap.org/" | ||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); | ||||
|     private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); | ||||
|     private static referencingRelationsCache = new Map<string, UIEventSource<OsmRelation[]>>(); | ||||
|     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>(); | ||||
|     type: string; | ||||
|     id: number; | ||||
|     tags: {} = {}; | ||||
|  | @ -12,9 +19,6 @@ export abstract class OsmObject { | |||
|     public changed: boolean = false; | ||||
|     timestamp: Date; | ||||
| 
 | ||||
| 
 | ||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||
| 
 | ||||
|     protected constructor(type: string, id: number) { | ||||
|         this.id = id; | ||||
|         this.type = type; | ||||
|  | @ -23,67 +27,99 @@ export abstract class OsmObject { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { | ||||
|     public static SetBackendUrl(url: string) { | ||||
|         if (!url.endsWith("/")) { | ||||
|             throw "Backend URL must end with a '/'" | ||||
|         } | ||||
|         if (!url.startsWith("http")) { | ||||
|             throw "Backend URL must begin with http" | ||||
|         } | ||||
|         this.backendURL = url; | ||||
|     } | ||||
| 
 | ||||
|     static DownloadObject(id): UIEventSource<OsmObject> { | ||||
|         if (OsmObject.objectCache.has(id)) { | ||||
|             return OsmObject.objectCache.get(id) | ||||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = splitted[1]; | ||||
| 
 | ||||
|         const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => { | ||||
|             continuation(element, meta); | ||||
|         const src = new UIEventSource<OsmObject>(undefined) | ||||
|         OsmObject.objectCache.set(id, src); | ||||
|         const newContinuation = (element: OsmObject) => { | ||||
|             src.setData(element) | ||||
|         } | ||||
| 
 | ||||
|         switch (type) { | ||||
|             case("node"): | ||||
|                 return new OsmNode(idN).Download(newContinuation); | ||||
|                 new OsmNode(idN).Download(newContinuation); | ||||
|                 break; | ||||
|             case("way"): | ||||
|                 return new OsmWay(idN).Download(newContinuation); | ||||
|                 new OsmWay(idN).Download(newContinuation); | ||||
|                 break; | ||||
|             case("relation"): | ||||
|                 return new OsmRelation(idN).Download(newContinuation); | ||||
|                 new OsmRelation(idN).Download(newContinuation); | ||||
|                 break; | ||||
| 
 | ||||
|         } | ||||
|         return src; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads the ways that are using this node. | ||||
|      * Beware: their geometry will be incomplete! | ||||
|      * @param id | ||||
|      * @param continuation | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static DownloadReferencingWays(id: string, continuation: (referencingWays: OsmWay[]) => void){ | ||||
|         Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/ways`) | ||||
|     public static DownloadReferencingWays(id: string): UIEventSource<OsmWay[]> { | ||||
|         if (OsmObject.referencingWaysCache.has(id)) { | ||||
|             return OsmObject.referencingWaysCache.get(id); | ||||
|         } | ||||
|         const waysSrc = new UIEventSource<OsmWay[]>([]) | ||||
|         OsmObject.referencingWaysCache.set(id, waysSrc); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`) | ||||
|             .then(data => { | ||||
|                 const ways = data.elements.map(wayInfo => { | ||||
|                     const way = new OsmWay(wayInfo.id) | ||||
|                     way.LoadData(wayInfo) | ||||
|                     return way | ||||
|                 }) | ||||
|                 continuation(ways) | ||||
|                 waysSrc.setData(ways) | ||||
|             }) | ||||
|         return waysSrc; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads the relations that are using this feature. | ||||
|      * Beware: their geometry will be incomplete! | ||||
|      * @param id | ||||
|      * @param continuation | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static DownloadReferencingRelations(id: string, continuation: (referencingRelations: OsmRelation[]) => void){ | ||||
|         Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/relations`) | ||||
|     public static DownloadReferencingRelations(id: string): UIEventSource<OsmRelation[]> { | ||||
|         if (OsmObject.referencingRelationsCache.has(id)) { | ||||
|             return OsmObject.referencingRelationsCache.get(id); | ||||
|         } | ||||
|         const relsSrc = new UIEventSource<OsmRelation[]>([]) | ||||
|         OsmObject.referencingRelationsCache.set(id, relsSrc); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`) | ||||
|             .then(data => { | ||||
|                 const rels = data.elements.map(wayInfo => { | ||||
|                     const rel = new OsmRelation(wayInfo.id) | ||||
|                     rel.LoadData(wayInfo) | ||||
|                     return rel | ||||
|                 }) | ||||
|                 continuation(rels) | ||||
|                 relsSrc.setData(rels) | ||||
|             }) | ||||
|         return relsSrc; | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadHistory(id: string): UIEventSource<OsmObject []> { | ||||
|         if (OsmObject.historyCache.has(id)) { | ||||
|             return OsmObject.historyCache.get(id) | ||||
|         } | ||||
|     public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void{ | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = splitted[1]; | ||||
|         $.getJSON("https://www.openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => { | ||||
|         const src = new UIEventSource<OsmObject[]>([]); | ||||
|         OsmObject.historyCache.set(id, src); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { | ||||
|             const elements: any[] = data.elements; | ||||
|             const osmObjects: OsmObject[] = [] | ||||
|             for (const element of elements) { | ||||
|  | @ -103,30 +139,42 @@ export abstract class OsmObject { | |||
|                 osmObject?.SaveExtraData(element, []); | ||||
|                 osmObjects.push(osmObject) | ||||
|             } | ||||
|             continuation(osmObjects) | ||||
|             src.setData(osmObjects) | ||||
|         }) | ||||
|         return src; | ||||
|     } | ||||
| 
 | ||||
|     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 | ||||
|     public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) { | ||||
|         const minlon = bounds[0][1] | ||||
|         const maxlon = bounds[1][1] | ||||
|         const minlat = bounds[1][0] | ||||
|         const maxlat = bounds[0][0]; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` | ||||
|         $.getJSON(url, data => { | ||||
|             const elements: any[] = data.elements; | ||||
|             const objects = OsmObject.ParseObjects(elements) | ||||
|             callback(objects); | ||||
| 
 | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> { | ||||
|         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); | ||||
|     public static DownloadAll(neededIds): UIEventSource<OsmObject[]> { | ||||
|         // local function which downloads all the objects one by one
 | ||||
|         // this is one big loop, running one download, then rerunning the entire function
 | ||||
| 
 | ||||
|         for (const polygonFeature of polygon_features) { | ||||
|             const key = polygonFeature.key; | ||||
| 
 | ||||
|             if (polygonFeature.polygon === "all") { | ||||
|                 result.set(key, {values: null, blacklist: false}) | ||||
|                 continue | ||||
|         const allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id)) | ||||
|         const allCompleted = new UIEventSource(undefined).map(_ => { | ||||
|             return !allSources.some(uiEventSource => uiEventSource.data === undefined) | ||||
|         }, allSources) | ||||
|         return allCompleted.map(completed => { | ||||
|             if (completed) { | ||||
|                 return allSources.map(src => src.data) | ||||
|             } | ||||
| 
 | ||||
|             const blacklist = polygonFeature.polygon === "blacklist" | ||||
|             result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist}) | ||||
| 
 | ||||
|             return [] | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     protected static isPolygon(tags: any): boolean { | ||||
|         for (const tagsKey in tags) { | ||||
|             if (!tags.hasOwnProperty(tagsKey)) { | ||||
|  | @ -145,43 +193,23 @@ export abstract class OsmObject { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 | ||||
|     public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) { | ||||
|         const minlon = bounds[0][1] | ||||
|         const maxlon = bounds[1][1] | ||||
|         const minlat = bounds[1][0] | ||||
|         const maxlat = bounds[0][0]; | ||||
|         const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` | ||||
|         $.getJSON(url, data => { | ||||
|             const elements: any[] = data.elements; | ||||
|             const objects = OsmObject.ParseObjects(elements) | ||||
|             callback(objects); | ||||
|     private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> { | ||||
|         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); | ||||
| 
 | ||||
|         }) | ||||
|         for (const polygonFeature of polygon_features) { | ||||
|             const key = polygonFeature.key; | ||||
| 
 | ||||
|             if (polygonFeature.polygon === "all") { | ||||
|                 result.set(key, {values: null, blacklist: false}) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|     //Loads an area from the OSM-api.
 | ||||
|             const blacklist = polygonFeature.polygon === "blacklist" | ||||
|             result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist}) | ||||
| 
 | ||||
|     public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) { | ||||
|         // local function which downloads all the objects one by one
 | ||||
|         // this is one big loop, running one download, then rerunning the entire function
 | ||||
|         if (neededIds.length == 0) { | ||||
|             continuation(knownElements); | ||||
|             return; | ||||
|         } | ||||
|         const neededId = neededIds.pop(); | ||||
| 
 | ||||
|         if (neededId in knownElements) { | ||||
|             OsmObject.DownloadAll(neededIds, knownElements, continuation); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         OsmObject.DownloadObject(neededId, | ||||
|             function (element) { | ||||
|                 knownElements[neededId] = element; // assign the element for later, continue downloading the next element
 | ||||
|                 OsmObject.DownloadAll(neededIds, knownElements, continuation); | ||||
|             } | ||||
|         ); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private static ParseObjects(elements: any[]): OsmObject[] { | ||||
|  | @ -245,7 +273,7 @@ export abstract class OsmObject { | |||
|     Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { | ||||
|         const self = this; | ||||
|         const full = this.type !== "way" ? "" : "/full"; | ||||
|         const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/${this.type}/${this.id}${full}`; | ||||
|         $.getJSON(url, function (data) { | ||||
| 
 | ||||
|                 const element = data.elements.pop(); | ||||
|  |  | |||
|  | @ -56,7 +56,7 @@ export default class UserBadge extends Toggle { | |||
|                 let messageSpan = | ||||
|                     new Link( | ||||
|                         new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), | ||||
|                         'https://www.openstreetmap.org/messages/inbox', | ||||
|                         `${user.backend}/messages/inbox`, | ||||
|                         true | ||||
|                     ) | ||||
| 
 | ||||
|  | @ -64,14 +64,14 @@ export default class UserBadge extends Toggle { | |||
|                 const csCount = | ||||
|                     new Link( | ||||
|                         new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), | ||||
|                         `https://www.openstreetmap.org/user/${user.name}/history`, | ||||
|                         `${user.backend}/user/${user.name}/history`, | ||||
|                         true); | ||||
| 
 | ||||
| 
 | ||||
|                 if (user.unreadMessages > 0) { | ||||
|                     messageSpan = new Link( | ||||
|                         new Combine([Svg.envelope, "" + user.unreadMessages]), | ||||
|                         'https://www.openstreetmap.org/messages/inbox', | ||||
|                         '${user.backend}/messages/inbox', | ||||
|                         true | ||||
|                     ).SetClass("alert") | ||||
|                 } | ||||
|  | @ -83,22 +83,22 @@ export default class UserBadge extends Toggle { | |||
| 
 | ||||
|                 const settings = | ||||
|                     new Link(Svg.gear_svg(), | ||||
|                         `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`, | ||||
|                         `${user.backend}/user/${encodeURIComponent(user.name)}/account`, | ||||
|                         true) | ||||
| 
 | ||||
| 
 | ||||
|                 const userIcon = new Link( | ||||
|                     new Img(user.img) | ||||
|                     user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img) | ||||
|                         .SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 h16 float-left") | ||||
|                     , | ||||
|                     `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}`, | ||||
|                     `${user.backend}/user/${encodeURIComponent(user.name)}`, | ||||
|                     true | ||||
|                 ); | ||||
| 
 | ||||
| 
 | ||||
|                 const userName = new Link( | ||||
|                     new FixedUiElement(user.name), | ||||
|                     `https://www.openstreetmap.org/user/${user.name}`, | ||||
|                     `${user.backend}/user/${user.name}`, | ||||
|                     true); | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,61 +26,13 @@ export default class PublicHolidayInput extends InputElement<string> { | |||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) { | ||||
| 
 | ||||
|         const value = this._value; | ||||
|         value.addCallbackAndRun(ph => { | ||||
|             if (ph === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             const parsed = OH.ParsePHRule(ph); | ||||
|             if (parsed === null) { | ||||
|                 return; | ||||
|             } | ||||
|             mode.setData(parsed.mode) | ||||
|             startTime.setData(parsed.start) | ||||
|             endTime.setData(parsed.end) | ||||
|         }) | ||||
| 
 | ||||
|         // We use this as a 'addCallbackAndRun'
 | ||||
|         mode.map(mode => { | ||||
|                 if (mode === undefined || mode === "") { | ||||
|                     // not known
 | ||||
|                     value.setData(undefined) | ||||
|                     return | ||||
|                 } | ||||
|                 if (mode === "off") { | ||||
|                     value.setData("PH off"); | ||||
|                     return; | ||||
|                 } | ||||
|                 if (mode === "open") { | ||||
|                     value.setData("PH open"); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 // Open during PH with special hours
 | ||||
|                 if (startTime.data === undefined || endTime.data === undefined) { | ||||
|                     // hours not filled in - not saveable
 | ||||
|                     value.setData(undefined) | ||||
|                     return | ||||
|                 } | ||||
|                 const oh = `PH ${startTime.data}-${endTime.data}` | ||||
|                 value.setData(oh) | ||||
| 
 | ||||
| 
 | ||||
|             }, [startTime, endTime] | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|      | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         const dropdown = new DropDown( | ||||
|             Translations.t.general.opening_hours.open_during_ph.Clone(), | ||||
|             [ | ||||
|                 {shown: Translations.t.general.opening_hours.ph_not_known.Clone(), value: ""}, | ||||
|                 {shown: Translations.t.general.opening_hours.ph_closed.Clone(), value: "off"}, | ||||
|                 {shown:Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"}, | ||||
|                 {shown: Translations.t.general.opening_hours.ph_open_as_usual.Clone(), value: "open"}, | ||||
|                 {shown: Translations.t.general.opening_hours.ph_open.Clone(), value: " "}, | ||||
|             ] | ||||
|         ).SetClass("inline-block"); | ||||
|  | @ -118,4 +70,45 @@ export default class PublicHolidayInput extends InputElement<string> { | |||
|         ]).ConstructElement() | ||||
|     } | ||||
| 
 | ||||
|     private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) { | ||||
| 
 | ||||
|         const value = this._value; | ||||
|         value.map(ph => OH.ParsePHRule(ph)) | ||||
|             .addCallbackAndRunD(parsed => { | ||||
|             mode.setData(parsed.mode) | ||||
|             startTime.setData(parsed.start) | ||||
|             endTime.setData(parsed.end) | ||||
|         }) | ||||
| 
 | ||||
|         // We use this as a 'addCallbackAndRun'
 | ||||
|         mode.map(mode => { | ||||
|                 if (mode === undefined || mode === "") { | ||||
|                     // not known
 | ||||
|                     value.setData(undefined) | ||||
|                     return | ||||
|                 } | ||||
|                 if (mode === "off") { | ||||
|                     value.setData("PH off"); | ||||
|                     return; | ||||
|                 } | ||||
|                 if (mode === "open") { | ||||
|                     value.setData("PH open"); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 // Open during PH with special hours
 | ||||
|                 if (startTime.data === undefined || endTime.data === undefined) { | ||||
|                     // hours not filled in - not saveable
 | ||||
|                     value.setData(undefined) | ||||
|                     return | ||||
|                 } | ||||
|                 const oh = `PH ${startTime.data}-${endTime.data}` | ||||
|                 value.setData(oh) | ||||
| 
 | ||||
| 
 | ||||
|             }, [startTime, endTime] | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,72 +1,72 @@ | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {OsmObject} from "../../Logic/Osm/OsmObject"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Translation} from "../i18n/Translation"; | ||||
| import State from "../../State"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import UserDetails from "../../Logic/Osm/OsmConnection"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {SubtleButton} from "../Base/SubtleButton"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import DeleteAction from "../../Logic/Osm/DeleteAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import {RadioButton} from "../Input/RadioButton"; | ||||
| import {FixedInputElement} from "../Input/FixedInputElement"; | ||||
| import {TextField} from "../Input/TextField"; | ||||
| 
 | ||||
| 
 | ||||
| export default class DeleteButton extends Toggle { | ||||
|     constructor(id: string) { | ||||
| export default class DeleteWizard extends Toggle { | ||||
|     /** | ||||
|      * The UI-element which triggers 'deletion' (either soft or hard). | ||||
|      *  | ||||
|      * - A 'hard deletion' is if the point is actually deleted from the OSM database | ||||
|      * - A 'soft deletion' is if the point is not deleted, but the tagging is modified which will result in the point not being picked up by the filters anymore. | ||||
|      *    Apart having needing theme-specific tags added (which must be supplied by the theme creator), fixme='marked for deletion' will be added too  | ||||
|      *  | ||||
|      * A deletion is only possible if the user is logged in. | ||||
|      * A soft deletion is only possible if tags are provided | ||||
|      * A hard deletion is only possible if the user has sufficient rigts | ||||
|      *  | ||||
|      * If no deletion is possible at all, the delete button will not be shown - but a reason will be shown instead. | ||||
|      *  | ||||
|      * @param id: The id of the element to remove | ||||
|      * @param softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted  | ||||
|      */ | ||||
|     constructor(id: string, softDeletionTags? : Tag[]) { | ||||
|         const t = Translations.t.delete | ||||
| 
 | ||||
|         const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null) | ||||
|         OsmObject.DownloadReferencingRelations(id, (rels) => { | ||||
|             hasRelations.setData(rels.length > 0) | ||||
|         }) | ||||
|         const deleteAction = new DeleteAction(id); | ||||
|          | ||||
|         const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null) | ||||
|         OsmObject.DownloadReferencingWays(id, (ways) => { | ||||
|             hasWays.setData(ways.length > 0) | ||||
|         }) | ||||
|         const deleteReasons = new RadioButton<string>( | ||||
|             [new FixedInputElement( | ||||
|                 t.reasons.test, "test" | ||||
|             ), | ||||
|             new FixedInputElement(t.reasons.disused, "disused"), | ||||
|             new FixedInputElement(t.reasons.notFound, "not found"), | ||||
|             new TextField()] | ||||
|              | ||||
|         const previousEditors = new UIEventSource<number[]>(null) | ||||
|         OsmObject.DownloadHistory(id, versions => { | ||||
|             const uids = versions.map(version => version.tags["_last_edit:contributor:uid"]) | ||||
|             previousEditors.setData(uids) | ||||
|         }) | ||||
|         const allByMyself = previousEditors.map(previous => { | ||||
|             if (previous === null) { | ||||
|                 return null; | ||||
|             } | ||||
|             const userId = State.state.osmConnection.userDetails.data.uid; | ||||
|             return !previous.some(editor => editor !== userId) | ||||
|         }, [State.state.osmConnection.userDetails]) | ||||
| 
 | ||||
|         const t = Translations.t.deleteButton | ||||
| 
 | ||||
|         super( | ||||
|             new Toggle( | ||||
|                 new VariableUiElement( | ||||
|                     hasRelations.map(hasRelations => { | ||||
|                         if (hasRelations === null || hasWays.data === null) { | ||||
|                             return new Loading() | ||||
|                         } | ||||
|                         if (hasWays.data || hasRelations) { | ||||
|                             return t.partOfOthers.Clone() | ||||
|                         } | ||||
| 
 | ||||
|                         return new Toggle( | ||||
|                             new SubtleButton(Svg.delete_icon_svg(), t.delete.Clone()), | ||||
|                             t.notEnoughExperience.Clone(), | ||||
|                             State.state.osmConnection.userDetails.map(userinfo => | ||||
|                                 allByMyself.data || | ||||
|                                 userinfo.csCount >= Constants.userJourney.deletePointsOfOthersUnlock, | ||||
|                                 [allByMyself]) | ||||
|         ) | ||||
| 
 | ||||
|                     }, [hasWays]) | ||||
|         const deleteButton = new SubtleButton( | ||||
|             Svg.delete_icon_svg(), | ||||
|             t.delete.Clone() | ||||
|         ).onClick(() => deleteAction.DoDelete(deleteReasons.GetValue().data)) | ||||
| 
 | ||||
|          | ||||
|          | ||||
|          | ||||
|         super( | ||||
|              | ||||
|              | ||||
|              | ||||
|             new Toggle( | ||||
|                 deleteButton, | ||||
|                 new VariableUiElement(deleteAction.canBeDeleted.map(cbd => cbd.reason.Clone())), | ||||
|                 deleteAction.canBeDeleted.map(cbd => cbd.canBeDeleted) | ||||
|             ), | ||||
|                 t.onlyEditedByLoggedInUser.Clone().onClick(State.state.osmConnection.AttemptLogin), | ||||
|                 State.state.osmConnection.isLoggedIn), | ||||
|             t.isntAPoint, | ||||
|             new UIEventSource<boolean>(id.startsWith("node")) | ||||
|         ); | ||||
|              | ||||
|              | ||||
|              | ||||
|             t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), | ||||
|             State.state.osmConnection.isLoggedIn | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -35,7 +35,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|                 configuration: TagRenderingConfig, | ||||
|                 units: Unit[], | ||||
|                 afterSave?: () => void, | ||||
|                 cancelButton?: BaseUIElement | ||||
|                 cancelButton?: BaseUIElement, | ||||
|     ) { | ||||
|         if (configuration === undefined) { | ||||
|             throw "A question is needed for a question visualization" | ||||
|  |  | |||
|  | @ -62,6 +62,9 @@ export default class ShowDataLayer { | |||
|             const allFeats = features.data.map(ff => ff.feature); | ||||
|             geoLayer = self.CreateGeojsonLayer(); | ||||
|             for (const feat of allFeats) { | ||||
|                 if(feat === undefined){ | ||||
|                     continue | ||||
|                 } | ||||
|                 // @ts-ignore
 | ||||
|                 geoLayer.addData(feat); | ||||
|             } | ||||
|  | @ -76,7 +79,13 @@ export default class ShowDataLayer { | |||
|             } | ||||
| 
 | ||||
|             if (zoomToFeatures) { | ||||
|                 try{ | ||||
|                      | ||||
|                 mp.fitBounds(geoLayer.getBounds()) | ||||
| 
 | ||||
|                 }catch(e){ | ||||
|                     console.error(e) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -169,8 +178,8 @@ export default class ShowDataLayer { | |||
|             infobox.Activate(); | ||||
|         }); | ||||
|         const self = this; | ||||
|         State.state.selectedElement.addCallbackAndRun(selected => { | ||||
|             if (selected === undefined || self._leafletMap.data === undefined) { | ||||
|         State.state.selectedElement.addCallbackAndRunD(selected => { | ||||
|             if ( self._leafletMap.data === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|             if (leafletLayer.getPopup().isOpen()) { | ||||
|  |  | |||
							
								
								
									
										3
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -45,7 +45,8 @@ if (location.href.indexOf("buurtnatuur.be") >= 0) { | |||
| 
 | ||||
| 
 | ||||
| let testing: UIEventSource<string>; | ||||
| if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { | ||||
| if (QueryParameters.GetQueryParameter("backend", undefined).data !== "osm-test" && | ||||
|     (location.hostname === "localhost" || location.hostname === "127.0.0.1")) { | ||||
|     testing = QueryParameters.GetQueryParameter("test", "true"); | ||||
|     // Set to true if testing and changes should NOT be saved
 | ||||
|     testing.setData(testing.data ?? "true") | ||||
|  |  | |||
|  | @ -27,14 +27,20 @@ | |||
|     "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", | ||||
|     "pickTheme": "Pick a theme below to get started." | ||||
|   }, | ||||
|   "deleteButton": { | ||||
|   "delete": { | ||||
|     "delete": "Delete", | ||||
|     "loginToDelete": "You must be logged in to delete a point", | ||||
|     "checkingDeletability": "Inspecting properties to check if this feature can be deleted", | ||||
|     "safeDelete": "This point can be safely deleted", | ||||
|     "isntAPoint": "Only points can be deleted", | ||||
|     "onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it", | ||||
|     "notEnoughExperience": "You don't have enough experience to delete points made by other people. Make more edits to improve your skills", | ||||
|     "partOfOthers": "This point is part of some way or relation, so you can not delete it" | ||||
|     "partOfOthers": "This point is part of some way or relation, so you can not delete it", | ||||
|     "loading": "Inspecting properties to check if this feature can be deleted", | ||||
|     "reasons": { | ||||
|       "test": "This was a testing point - the feature was never actually there", | ||||
|       "disused": "This feature is disused or removed", | ||||
|       "notFound": "This feature couldn't be found" | ||||
|     } | ||||
|   }, | ||||
|   "general": { | ||||
|     "loginWithOpenStreetMap": "Login with OpenStreetMap", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue