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) |         State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home) | ||||||
|             .addCallbackAndRun(home => { |             .addCallbackAndRunD(home => { | ||||||
|                 if (home === undefined) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|                 const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color") |                 const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color") | ||||||
|                 const icon = L.icon({ |                 const icon = L.icon({ | ||||||
|                     iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), |                     iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), | ||||||
|  | @ -286,10 +283,8 @@ export class InitUiElements { | ||||||
|             isOpened.setData(false); |             isOpened.setData(false); | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         State.state.selectedElement.addCallbackAndRun(selected => { |         State.state.selectedElement.addCallbackAndRunD(_ => { | ||||||
|             if (selected !== undefined) { |  | ||||||
|                 isOpened.setData(false); |                 isOpened.setData(false); | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
|         isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome") |         isOpened.setData(Hash.hash.data === undefined || Hash.hash.data === "" || Hash.hash.data == "welcome") | ||||||
|     } |     } | ||||||
|  | @ -337,11 +332,9 @@ export class InitUiElements { | ||||||
|                 copyrightButton.isEnabled.setData(false); |                 copyrightButton.isEnabled.setData(false); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|         State.state.selectedElement.addCallbackAndRun(feature => { |         State.state.selectedElement.addCallbackAndRunD(_ => { | ||||||
|             if (feature !== undefined) { |  | ||||||
|                 layerControlButton.isEnabled.setData(false); |                 layerControlButton.isEnabled.setData(false); | ||||||
|                 copyrightButton.isEnabled.setData(false); |                 copyrightButton.isEnabled.setData(false); | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -61,7 +61,7 @@ export default class SelectedFeatureHandler { | ||||||
|             return; // No valid feature selected
 |             return; // No valid feature selected
 | ||||||
|         } |         } | ||||||
|         // We should have a valid osm-ID and zoom to it
 |         // 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(); |             const centerpoint = element.centerpoint(); | ||||||
|             console.log("Zooming to location for select point: ", centerpoint) |             console.log("Zooming to location for select point: ", centerpoint) | ||||||
|             location.data.lat = centerpoint[0] |             location.data.lat = centerpoint[0] | ||||||
|  |  | ||||||
|  | @ -228,9 +228,9 @@ export class Changes implements FeatureSource{ | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         neededIds = Utils.Dedup(neededIds); |         neededIds = Utils.Dedup(neededIds); | ||||||
|         OsmObject.DownloadAll(neededIds, {}, (knownElements) => { |         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||||
|             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) |             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) | ||||||
|         }); |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -7,6 +7,7 @@ import State from "../../State"; | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale"; | ||||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
|  | import {OsmObject} from "./OsmObject"; | ||||||
| 
 | 
 | ||||||
| export class ChangesetHandler { | 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( |     public UploadChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage, | ||||||
|         generateChangeXML: (csid: string) => string, |         generateChangeXML: (csid: string) => string) { | ||||||
|         continuation: () => void) { |  | ||||||
| 
 | 
 | ||||||
|         if (this.userDetails.data.csCount == 0) { |         if (this.userDetails.data.csCount == 0) { | ||||||
|             // The user became a contributor!
 |             // The user became a contributor!
 | ||||||
|  | @ -62,7 +72,6 @@ export class ChangesetHandler { | ||||||
|         if (this._dryRun) { |         if (this._dryRun) { | ||||||
|             const changesetXML = generateChangeXML("123456"); |             const changesetXML = generateChangeXML("123456"); | ||||||
|             console.log(changesetXML); |             console.log(changesetXML); | ||||||
|             continuation(); |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -97,7 +106,7 @@ export class ChangesetHandler { | ||||||
|                     // Mark the CS as closed...
 |                     // Mark the CS as closed...
 | ||||||
|                     this.currentChangeset.setData(""); |                     this.currentChangeset.setData(""); | ||||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist  
 |                     // ... 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) { |         if (changesetId === undefined) { | ||||||
|             changesetId = this.currentChangeset.data; |             changesetId = this.currentChangeset.data; | ||||||
|  | @ -133,15 +195,25 @@ export class ChangesetHandler { | ||||||
| 
 | 
 | ||||||
|     private OpenChangeset( |     private OpenChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         continuation: (changesetId: string) => void) { |         continuation: (changesetId: string) => void, | ||||||
|  |         isDeletionCS: boolean = false, | ||||||
|  |         deletionReason: string = undefined) { | ||||||
| 
 | 
 | ||||||
|         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; |         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; |         let path = window.location.pathname; | ||||||
|         path = path.substr(1, path.lastIndexOf("/")); |         path = path.substr(1, path.lastIndexOf("/")); | ||||||
|         const metadata = [ |         const metadata = [ | ||||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], |             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||||
|             ["comment", `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`], |             ["comment", comment], | ||||||
|  |             ["deletion", isDeletionCS ? "yes" : undefined], | ||||||
|             ["theme", layout.id], |             ["theme", layout.id], | ||||||
|             ["language", Locale.language.data], |             ["language", Locale.language.data], | ||||||
|             ["host", window.location.host], |             ["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, |     private AddChange(changesetId: string, | ||||||
|                       changesetXML: string, |                       changesetXML: string, | ||||||
|                       allElements: ElementStorage, |                       allElements: ElementStorage, | ||||||
|                       continuation: ((changesetId: string, idMapping: any) => void), |                       continuation: ((changesetId: string, idMapping: any) => void), | ||||||
|                       onFail: ((changesetId: string) => void) = undefined) { |                       onFail: ((changesetId: string, reason: string) => void) = undefined) { | ||||||
|         this.auth.xhr({ |         this.auth.xhr({ | ||||||
|             method: 'POST', |             method: 'POST', | ||||||
|             options: {header: {'Content-Type': 'text/xml'}}, |             options: {header: {'Content-Type': 'text/xml'}}, | ||||||
|  | @ -186,7 +268,7 @@ export class ChangesetHandler { | ||||||
|             if (response == null) { |             if (response == null) { | ||||||
|                 console.log("err", err); |                 console.log("err", err); | ||||||
|                 if (onFail) { |                 if (onFail) { | ||||||
|                     onFail(changesetId); |                     onFail(changesetId, err); | ||||||
|                 } |                 } | ||||||
|                 return; |                 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 LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
| import Img from "../../UI/Base/Img"; | import Img from "../../UI/Base/Img"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | import {OsmObject} from "./OsmObject"; | ||||||
| 
 | 
 | ||||||
| export default class UserDetails { | export default class UserDetails { | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +21,11 @@ export default class UserDetails { | ||||||
|     public totalMessages = 0; |     public totalMessages = 0; | ||||||
|     public dryRun: boolean; |     public dryRun: boolean; | ||||||
|     home: { lon: number; lat: number }; |     home: { lon: number; lat: number }; | ||||||
|  |     public backend: string; | ||||||
|  |      | ||||||
|  |     constructor(backend: string) { | ||||||
|  |         this.backend = backend; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class OsmConnection { | export class OsmConnection { | ||||||
|  | @ -62,9 +68,10 @@ export class OsmConnection { | ||||||
|         this._singlePage = singlePage; |         this._singlePage = singlePage; | ||||||
|         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; |         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; | ||||||
|         console.debug("Using backend", this._oauth_config.url) |         console.debug("Using backend", this._oauth_config.url) | ||||||
|  |         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; |         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; |         this.userDetails.data.dryRun = dryRun; | ||||||
|         const self =this; |         const self =this; | ||||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { |         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { | ||||||
|  | @ -103,10 +110,8 @@ export class OsmConnection { | ||||||
|     public UploadChangeset( |     public UploadChangeset( | ||||||
|         layout: LayoutConfig, |         layout: LayoutConfig, | ||||||
|         allElements: ElementStorage, |         allElements: ElementStorage, | ||||||
|         generateChangeXML: (csid: string) => string, |         generateChangeXML: (csid: string) => string) { | ||||||
|         continuation: () => void = () => { |         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); | ||||||
|         }) { |  | ||||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { |     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||||
|  |  | ||||||
|  | @ -1,10 +1,17 @@ | ||||||
| import * as $ from "jquery" | import * as $ from "jquery" | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import * as polygon_features from "../../assets/polygon-features.json"; | import * as polygon_features from "../../assets/polygon-features.json"; | ||||||
|  | import {UIEventSource} from "../UIEventSource"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export abstract class OsmObject { | 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; |     type: string; | ||||||
|     id: number; |     id: number; | ||||||
|     tags: {} = {}; |     tags: {} = {}; | ||||||
|  | @ -12,9 +19,6 @@ export abstract class OsmObject { | ||||||
|     public changed: boolean = false; |     public changed: boolean = false; | ||||||
|     timestamp: Date; |     timestamp: Date; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() |  | ||||||
| 
 |  | ||||||
|     protected constructor(type: string, id: number) { |     protected constructor(type: string, id: number) { | ||||||
|         this.id = id; |         this.id = id; | ||||||
|         this.type = type; |         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 splitted = id.split("/"); | ||||||
|         const type = splitted[0]; |         const type = splitted[0]; | ||||||
|         const idN = splitted[1]; |         const idN = splitted[1]; | ||||||
| 
 | 
 | ||||||
|         const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => { |         const src = new UIEventSource<OsmObject>(undefined) | ||||||
|             continuation(element, meta); |         OsmObject.objectCache.set(id, src); | ||||||
|  |         const newContinuation = (element: OsmObject) => { | ||||||
|  |             src.setData(element) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         switch (type) { |         switch (type) { | ||||||
|             case("node"): |             case("node"): | ||||||
|                 return new OsmNode(idN).Download(newContinuation); |                 new OsmNode(idN).Download(newContinuation); | ||||||
|  |                 break; | ||||||
|             case("way"): |             case("way"): | ||||||
|                 return new OsmWay(idN).Download(newContinuation); |                 new OsmWay(idN).Download(newContinuation); | ||||||
|  |                 break; | ||||||
|             case("relation"): |             case("relation"): | ||||||
|                 return new OsmRelation(idN).Download(newContinuation); |                 new OsmRelation(idN).Download(newContinuation); | ||||||
|  |                 break; | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|  |         return src; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Downloads the ways that are using this node. |      * Downloads the ways that are using this node. | ||||||
|      * Beware: their geometry will be incomplete! |      * Beware: their geometry will be incomplete! | ||||||
|      * @param id |  | ||||||
|      * @param continuation |  | ||||||
|      * @constructor |  | ||||||
|      */ |      */ | ||||||
|     public static DownloadReferencingWays(id: string, continuation: (referencingWays: OsmWay[]) => void){ |     public static DownloadReferencingWays(id: string): UIEventSource<OsmWay[]> { | ||||||
|         Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/ways`) |         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 => { |             .then(data => { | ||||||
|                const ways = data.elements.map(wayInfo => { |                 const ways = data.elements.map(wayInfo => { | ||||||
|                     const way = new OsmWay(wayInfo.id) |                     const way = new OsmWay(wayInfo.id) | ||||||
|                     way.LoadData(wayInfo) |                     way.LoadData(wayInfo) | ||||||
|                     return way |                     return way | ||||||
|                 }) |                 }) | ||||||
|                 continuation(ways) |                 waysSrc.setData(ways) | ||||||
|             }) |             }) | ||||||
|  |         return waysSrc; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Downloads the relations that are using this feature. |      * Downloads the relations that are using this feature. | ||||||
|      * Beware: their geometry will be incomplete! |      * Beware: their geometry will be incomplete! | ||||||
|      * @param id |  | ||||||
|      * @param continuation |  | ||||||
|      * @constructor |  | ||||||
|      */ |      */ | ||||||
|     public static DownloadReferencingRelations(id: string, continuation: (referencingRelations: OsmRelation[]) => void){ |     public static DownloadReferencingRelations(id: string): UIEventSource<OsmRelation[]> { | ||||||
|         Utils.downloadJson(`https://www.openStreetMap.org/api/0.6/${id}/relations`) |         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 => { |             .then(data => { | ||||||
|                 const rels = data.elements.map(wayInfo => { |                 const rels = data.elements.map(wayInfo => { | ||||||
|                     const rel = new OsmRelation(wayInfo.id) |                     const rel = new OsmRelation(wayInfo.id) | ||||||
|                     rel.LoadData(wayInfo) |                     rel.LoadData(wayInfo) | ||||||
|                     return rel |                     return rel | ||||||
|                 }) |                 }) | ||||||
|                 continuation(rels) |                 relsSrc.setData(rels) | ||||||
|             }) |             }) | ||||||
|  |         return relsSrc; | ||||||
|     } |     } | ||||||
|     public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void{ | 
 | ||||||
|  |     public static DownloadHistory(id: string): UIEventSource<OsmObject []> { | ||||||
|  |         if (OsmObject.historyCache.has(id)) { | ||||||
|  |             return OsmObject.historyCache.get(id) | ||||||
|  |         } | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/"); | ||||||
|         const type = splitted[0]; |         const type = splitted[0]; | ||||||
|         const idN = splitted[1]; |         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 elements: any[] = data.elements; | ||||||
|             const osmObjects: OsmObject[] = [] |             const osmObjects: OsmObject[] = [] | ||||||
|             for (const element of elements) { |             for (const element of elements) { | ||||||
|  | @ -103,30 +139,42 @@ export abstract class OsmObject { | ||||||
|                 osmObject?.SaveExtraData(element, []); |                 osmObject?.SaveExtraData(element, []); | ||||||
|                 osmObjects.push(osmObject) |                 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 }> { |     public static DownloadAll(neededIds): UIEventSource<OsmObject[]> { | ||||||
|         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); |         // 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 allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id)) | ||||||
|             const key = polygonFeature.key; |         const allCompleted = new UIEventSource(undefined).map(_ => { | ||||||
| 
 |             return !allSources.some(uiEventSource => uiEventSource.data === undefined) | ||||||
|             if (polygonFeature.polygon === "all") { |         }, allSources) | ||||||
|                 result.set(key, {values: null, blacklist: false}) |         return allCompleted.map(completed => { | ||||||
|                 continue |             if (completed) { | ||||||
|  |                 return allSources.map(src => src.data) | ||||||
|             } |             } | ||||||
| 
 |             return [] | ||||||
|             const blacklist = polygonFeature.polygon === "blacklist" |         }); | ||||||
|             result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist}) |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return result; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     protected static isPolygon(tags: any): boolean { |     protected static isPolygon(tags: any): boolean { | ||||||
|         for (const tagsKey in tags) { |         for (const tagsKey in tags) { | ||||||
|             if (!tags.hasOwnProperty(tagsKey)) { |             if (!tags.hasOwnProperty(tagsKey)) { | ||||||
|  | @ -145,43 +193,23 @@ export abstract class OsmObject { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
 |     private static constructPolygonFeatures(): Map<string, { values: Set<string>, blacklist: boolean }> { | ||||||
|     public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) { |         const result = new Map<string, { values: Set<string>, blacklist: boolean }>(); | ||||||
|         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); |  | ||||||
| 
 | 
 | ||||||
|         }) |         for (const polygonFeature of polygon_features) { | ||||||
|     } |             const key = polygonFeature.key; | ||||||
| 
 | 
 | ||||||
|     //Loads an area from the OSM-api.
 |             if (polygonFeature.polygon === "all") { | ||||||
| 
 |                 result.set(key, {values: null, blacklist: false}) | ||||||
|     public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) { |                 continue | ||||||
|         // 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); |  | ||||||
|             } |             } | ||||||
|         ); | 
 | ||||||
|  |             const blacklist = polygonFeature.polygon === "blacklist" | ||||||
|  |             result.set(key, {values: new Set<string>(polygonFeature.values), blacklist: blacklist}) | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static ParseObjects(elements: any[]): OsmObject[] { |     private static ParseObjects(elements: any[]): OsmObject[] { | ||||||
|  | @ -245,7 +273,7 @@ export abstract class OsmObject { | ||||||
|     Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { |     Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { | ||||||
|         const self = this; |         const self = this; | ||||||
|         const full = this.type !== "way" ? "" : "/full"; |         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) { |         $.getJSON(url, function (data) { | ||||||
| 
 | 
 | ||||||
|                 const element = data.elements.pop(); |                 const element = data.elements.pop(); | ||||||
|  |  | ||||||
|  | @ -56,7 +56,7 @@ export default class UserBadge extends Toggle { | ||||||
|                 let messageSpan = |                 let messageSpan = | ||||||
|                     new Link( |                     new Link( | ||||||
|                         new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), |                         new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle), | ||||||
|                         'https://www.openstreetmap.org/messages/inbox', |                         `${user.backend}/messages/inbox`, | ||||||
|                         true |                         true | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|  | @ -64,14 +64,14 @@ export default class UserBadge extends Toggle { | ||||||
|                 const csCount = |                 const csCount = | ||||||
|                     new Link( |                     new Link( | ||||||
|                         new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), |                         new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle), | ||||||
|                         `https://www.openstreetmap.org/user/${user.name}/history`, |                         `${user.backend}/user/${user.name}/history`, | ||||||
|                         true); |                         true); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|                 if (user.unreadMessages > 0) { |                 if (user.unreadMessages > 0) { | ||||||
|                     messageSpan = new Link( |                     messageSpan = new Link( | ||||||
|                         new Combine([Svg.envelope, "" + user.unreadMessages]), |                         new Combine([Svg.envelope, "" + user.unreadMessages]), | ||||||
|                         'https://www.openstreetmap.org/messages/inbox', |                         '${user.backend}/messages/inbox', | ||||||
|                         true |                         true | ||||||
|                     ).SetClass("alert") |                     ).SetClass("alert") | ||||||
|                 } |                 } | ||||||
|  | @ -83,22 +83,22 @@ export default class UserBadge extends Toggle { | ||||||
| 
 | 
 | ||||||
|                 const settings = |                 const settings = | ||||||
|                     new Link(Svg.gear_svg(), |                     new Link(Svg.gear_svg(), | ||||||
|                         `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`, |                         `${user.backend}/user/${encodeURIComponent(user.name)}/account`, | ||||||
|                         true) |                         true) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|                 const userIcon = new Link( |                 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") |                         .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 |                     true | ||||||
|                 ); |                 ); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|                 const userName = new Link( |                 const userName = new Link( | ||||||
|                     new FixedUiElement(user.name), |                     new FixedUiElement(user.name), | ||||||
|                     `https://www.openstreetmap.org/user/${user.name}`, |                     `${user.backend}/user/${user.name}`, | ||||||
|                     true); |                     true); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ export default class PublicHolidayInput extends InputElement<string> { | ||||||
|         this._value = value; |         this._value = value; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     | 
 | ||||||
|     GetValue(): UIEventSource<string> { |     GetValue(): UIEventSource<string> { | ||||||
|         return this._value; |         return this._value; | ||||||
|     } |     } | ||||||
|  | @ -25,18 +25,56 @@ export default class PublicHolidayInput extends InputElement<string> { | ||||||
|     IsValid(t: string): boolean { |     IsValid(t: string): boolean { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|  |     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.Clone(), value: " "}, | ||||||
|  |             ] | ||||||
|  |         ).SetClass("inline-block"); | ||||||
|  |         /* | ||||||
|  |         * Either "" (unknown), " " (opened) or "off" (closed) | ||||||
|  |         * */ | ||||||
|  |         const mode = dropdown.GetValue(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const start = new TextField({ | ||||||
|  |             placeholder: "starthour", | ||||||
|  |             htmlType: "time" | ||||||
|  |         }).SetClass("inline-block"); | ||||||
|  |         const end = new TextField({ | ||||||
|  |             placeholder: "starthour", | ||||||
|  |             htmlType: "time" | ||||||
|  |         }).SetClass("inline-block"); | ||||||
|  | 
 | ||||||
|  |         const askHours = new Toggle( | ||||||
|  |             new Combine([ | ||||||
|  |                 Translations.t.general.opening_hours.opensAt.Clone(), | ||||||
|  |                 start, | ||||||
|  |                 Translations.t.general.opening_hours.openTill.Clone(), | ||||||
|  |                 end | ||||||
|  |             ]), | ||||||
|  |             undefined, | ||||||
|  |             mode.map(mode => mode === " ") | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.SetupDataSync(mode, start.GetValue(), end.GetValue()) | ||||||
|  | 
 | ||||||
|  |         return new Combine([ | ||||||
|  |             dropdown, | ||||||
|  |             askHours | ||||||
|  |         ]).ConstructElement() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) { |     private SetupDataSync(mode: UIEventSource<string>, startTime: UIEventSource<string>, endTime: UIEventSource<string>) { | ||||||
| 
 | 
 | ||||||
|         const value = this._value; |         const value = this._value; | ||||||
|         value.addCallbackAndRun(ph => { |         value.map(ph => OH.ParsePHRule(ph)) | ||||||
|             if (ph === undefined) { |             .addCallbackAndRunD(parsed => { | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             const parsed = OH.ParsePHRule(ph); |  | ||||||
|             if (parsed === null) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             mode.setData(parsed.mode) |             mode.setData(parsed.mode) | ||||||
|             startTime.setData(parsed.start) |             startTime.setData(parsed.start) | ||||||
|             endTime.setData(parsed.end) |             endTime.setData(parsed.end) | ||||||
|  | @ -72,50 +110,5 @@ export default class PublicHolidayInput extends InputElement<string> { | ||||||
|             }, [startTime, endTime] |             }, [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.Clone(), value: " "}, |  | ||||||
|             ] |  | ||||||
|         ).SetClass("inline-block"); |  | ||||||
|         /* |  | ||||||
|         * Either "" (unknown), " " (opened) or "off" (closed) |  | ||||||
|         * */ |  | ||||||
|         const mode = dropdown.GetValue(); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const start = new TextField({ |  | ||||||
|             placeholder: "starthour", |  | ||||||
|             htmlType: "time" |  | ||||||
|         }).SetClass("inline-block"); |  | ||||||
|         const end = new TextField({ |  | ||||||
|             placeholder: "starthour", |  | ||||||
|             htmlType: "time" |  | ||||||
|         }).SetClass("inline-block"); |  | ||||||
|          |  | ||||||
|         const askHours = new Toggle( |  | ||||||
|             new Combine([ |  | ||||||
|                 Translations.t.general.opening_hours.opensAt.Clone(), |  | ||||||
|                 start, |  | ||||||
|                 Translations.t.general.opening_hours.openTill.Clone(), |  | ||||||
|                 end |  | ||||||
|             ]), |  | ||||||
|             undefined, |  | ||||||
|             mode.map(mode => mode === " ") |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         this.SetupDataSync(mode, start.GetValue(), end.GetValue()) |  | ||||||
|          |  | ||||||
|         return new Combine([ |  | ||||||
|             dropdown, |  | ||||||
|             askHours |  | ||||||
|         ]).ConstructElement() |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -1,72 +1,72 @@ | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | 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 State from "../../State"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import Translations from "../i18n/Translations"; | 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 {SubtleButton} from "../Base/SubtleButton"; | ||||||
| import Svg from "../../Svg"; | 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 { | export default class DeleteWizard extends Toggle { | ||||||
|     constructor(id: string) { |     /** | ||||||
|  |      * 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) |         const deleteAction = new DeleteAction(id); | ||||||
|         OsmObject.DownloadReferencingRelations(id, (rels) => { |          | ||||||
|             hasRelations.setData(rels.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 hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null) |         const deleteButton = new SubtleButton( | ||||||
|         OsmObject.DownloadReferencingWays(id, (ways) => { |             Svg.delete_icon_svg(), | ||||||
|             hasWays.setData(ways.length > 0) |             t.delete.Clone() | ||||||
|         }) |         ).onClick(() => deleteAction.DoDelete(deleteReasons.GetValue().data)) | ||||||
| 
 |  | ||||||
|         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( |         super( | ||||||
|  |              | ||||||
|  |              | ||||||
|  |              | ||||||
|             new Toggle( |             new Toggle( | ||||||
|                 new VariableUiElement( |                 deleteButton, | ||||||
|                     hasRelations.map(hasRelations => { |                 new VariableUiElement(deleteAction.canBeDeleted.map(cbd => cbd.reason.Clone())), | ||||||
|                         if (hasRelations === null || hasWays.data === null) { |                 deleteAction.canBeDeleted.map(cbd => cbd.canBeDeleted) | ||||||
|                             return new Loading() |             ), | ||||||
|                         } |              | ||||||
|                         if (hasWays.data || hasRelations) { |              | ||||||
|                             return t.partOfOthers.Clone() |              | ||||||
|                         } |             t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), | ||||||
|  |             State.state.osmConnection.isLoggedIn | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|                         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]) |  | ||||||
|                 ), |  | ||||||
|                 t.onlyEditedByLoggedInUser.Clone().onClick(State.state.osmConnection.AttemptLogin), |  | ||||||
|                 State.state.osmConnection.isLoggedIn), |  | ||||||
|             t.isntAPoint, |  | ||||||
|             new UIEventSource<boolean>(id.startsWith("node")) |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -69,7 +69,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | ||||||
|         if (!hasMinimap) { |         if (!hasMinimap) { | ||||||
|             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) |             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) | ||||||
|         } |         } | ||||||
| 
 |          | ||||||
|         renderings.push( |         renderings.push( | ||||||
|             new VariableUiElement( |             new VariableUiElement( | ||||||
|                 State.state.osmConnection.userDetails.map(userdetails => { |                 State.state.osmConnection.userDetails.map(userdetails => { | ||||||
|  |  | ||||||
|  | @ -35,7 +35,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|                 configuration: TagRenderingConfig, |                 configuration: TagRenderingConfig, | ||||||
|                 units: Unit[], |                 units: Unit[], | ||||||
|                 afterSave?: () => void, |                 afterSave?: () => void, | ||||||
|                 cancelButton?: BaseUIElement |                 cancelButton?: BaseUIElement, | ||||||
|     ) { |     ) { | ||||||
|         if (configuration === undefined) { |         if (configuration === undefined) { | ||||||
|             throw "A question is needed for a question visualization" |             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); |             const allFeats = features.data.map(ff => ff.feature); | ||||||
|             geoLayer = self.CreateGeojsonLayer(); |             geoLayer = self.CreateGeojsonLayer(); | ||||||
|             for (const feat of allFeats) { |             for (const feat of allFeats) { | ||||||
|  |                 if(feat === undefined){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|                 // @ts-ignore
 |                 // @ts-ignore
 | ||||||
|                 geoLayer.addData(feat); |                 geoLayer.addData(feat); | ||||||
|             } |             } | ||||||
|  | @ -76,7 +79,13 @@ export default class ShowDataLayer { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (zoomToFeatures) { |             if (zoomToFeatures) { | ||||||
|  |                 try{ | ||||||
|  |                      | ||||||
|                 mp.fitBounds(geoLayer.getBounds()) |                 mp.fitBounds(geoLayer.getBounds()) | ||||||
|  | 
 | ||||||
|  |                 }catch(e){ | ||||||
|  |                     console.error(e) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -169,8 +178,8 @@ export default class ShowDataLayer { | ||||||
|             infobox.Activate(); |             infobox.Activate(); | ||||||
|         }); |         }); | ||||||
|         const self = this; |         const self = this; | ||||||
|         State.state.selectedElement.addCallbackAndRun(selected => { |         State.state.selectedElement.addCallbackAndRunD(selected => { | ||||||
|             if (selected === undefined || self._leafletMap.data === undefined) { |             if ( self._leafletMap.data === undefined) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (leafletLayer.getPopup().isOpen()) { |             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>; | 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"); |     testing = QueryParameters.GetQueryParameter("test", "true"); | ||||||
|     // Set to true if testing and changes should NOT be saved
 |     // Set to true if testing and changes should NOT be saved
 | ||||||
|     testing.setData(testing.data ?? "true") |     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.", |     "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", | ||||||
|     "pickTheme": "Pick a theme below to get started." |     "pickTheme": "Pick a theme below to get started." | ||||||
|   }, |   }, | ||||||
|   "deleteButton": { |   "delete": { | ||||||
|     "delete": "Delete", |     "delete": "Delete", | ||||||
|     "loginToDelete": "You must be logged in to delete a point", |     "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", |     "isntAPoint": "Only points can be deleted", | ||||||
|     "onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it", |     "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", |     "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": { |   "general": { | ||||||
|     "loginWithOpenStreetMap": "Login with OpenStreetMap", |     "loginWithOpenStreetMap": "Login with OpenStreetMap", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue