forked from MapComplete/MapComplete
		
	Add propagation of metadata in changedescriptions, aggregate metadata in changeset tags
This commit is contained in:
		
							parent
							
								
									81f3ec385f
								
							
						
					
					
						commit
						21fd148f38
					
				
					 19 changed files with 545 additions and 403 deletions
				
			
		|  | @ -1,225 +1,62 @@ | |||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Translation} from "../../../UI/i18n/Translation"; | ||||
| import State from "../../../State"; | ||||
| import {OsmObject} from "../OsmObject"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| import Constants from "../../../Models/Constants"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import ChangeTagAction from "./ChangeTagAction"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {And} from "../../Tags/And"; | ||||
| import {Tag} from "../../Tags/Tag"; | ||||
| 
 | ||||
| export default class DeleteAction { | ||||
| export default class DeleteAction extends OsmChangeAction { | ||||
| 
 | ||||
|     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; | ||||
|     public readonly isDeleted = new UIEventSource<boolean>(false); | ||||
|     private readonly _softDeletionTags: TagsFilter; | ||||
|     private readonly meta: { | ||||
|         theme: string, | ||||
|         specialMotivation: string, | ||||
|         changeType: "deletion" | ||||
|     }; | ||||
|     private readonly _id: string; | ||||
|     private readonly _allowDeletionAtChangesetCount: number; | ||||
|     private _hardDelete: boolean; | ||||
| 
 | ||||
| 
 | ||||
|     constructor(id: string, allowDeletionAtChangesetCount?: number) { | ||||
|     constructor(id: string, | ||||
|                 softDeletionTags: TagsFilter, | ||||
|                 meta: { | ||||
|                     theme: string, | ||||
|                     specialMotivation: string | ||||
|                 }, | ||||
|                 hardDelete: boolean) { | ||||
|         super() | ||||
|         this._id = id; | ||||
|         this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE; | ||||
|         this._hardDelete = hardDelete; | ||||
|         this.meta = {...meta, changeType: "deletion"}; | ||||
|         this._softDeletionTags = new And([softDeletionTags, | ||||
|             new Tag("fixme", `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`) | ||||
|         ]); | ||||
| 
 | ||||
|         this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({ | ||||
|             canBeDeleted: undefined, | ||||
|             reason: Translations.t.delete.loading | ||||
|         }) | ||||
| 
 | ||||
|         this.CheckDeleteability(false) | ||||
|     } | ||||
| 
 | ||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
| 
 | ||||
|     /** | ||||
|      * Does actually delete the feature; returns the event source 'this.isDeleted' | ||||
|      * If deletion is not allowed, triggers the callback instead | ||||
|      */ | ||||
|     public DoDelete(reason: string, onNotAllowed: () => void): void { | ||||
|         const isDeleted = this.isDeleted | ||||
|         const self = this; | ||||
|         let deletionStarted = false; | ||||
|         this.canBeDeleted.addCallbackAndRun( | ||||
|             canBeDeleted => { | ||||
|                 if (isDeleted.data || deletionStarted) { | ||||
|                     // Already deleted...
 | ||||
|                     return; | ||||
|         const osmObject = await OsmObject.DownloadObjectAsync(this._id) | ||||
| 
 | ||||
|         if (this._hardDelete) { | ||||
|             return [{ | ||||
|                 meta: this.meta, | ||||
|                 doDelete: true, | ||||
|                 type: osmObject.type, | ||||
|                 id: osmObject.id, | ||||
|             }] | ||||
|         } else { | ||||
|             return await new ChangeTagAction( | ||||
|                 this._id, this._softDeletionTags, osmObject.tags, | ||||
|                 { | ||||
|                     theme: State.state?.layoutToUse?.id ?? "unkown", | ||||
|                     changeType: "soft-delete" | ||||
|                 } | ||||
| 
 | ||||
|                 if (canBeDeleted.canBeDeleted === false) { | ||||
|                     // We aren't allowed to delete
 | ||||
|                     deletionStarted = true; | ||||
|                     onNotAllowed(); | ||||
|                     isDeleted.setData(true); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!canBeDeleted) { | ||||
|                     // We are not allowed to delete (yet), this might change in the future though
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 deletionStarted = true; | ||||
|                 OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => { | ||||
|                     if (obj === undefined) { | ||||
|                         return; | ||||
|                     } | ||||
|                     State.state.osmConnection.changesetHandler.DeleteElement( | ||||
|                         obj, | ||||
|                         State.state.layoutToUse, | ||||
|                         reason, | ||||
|                         State.state.allElements, | ||||
|                         () => { | ||||
|                             isDeleted.setData(true) | ||||
|                         } | ||||
|                     ) | ||||
|                 }) | ||||
| 
 | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if the currently logged in user can delete the current point. | ||||
|      * State is written into this._canBeDeleted | ||||
|      * @constructor | ||||
|      * @private | ||||
|      */ | ||||
|     public CheckDeleteability(useTheInternet: boolean): 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; | ||||
|             ).CreateChangeDescriptions(changes) | ||||
|         } | ||||
| 
 | ||||
|         // 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 >= Math.min(Constants.userJourney.deletePointsOfOthersUnlock, this._allowDeletionAtChangesetCount); | ||||
|         }) | ||||
| 
 | ||||
|         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 && useTheInternet) { | ||||
|                 // 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 true; // unregister this caller!
 | ||||
|             } | ||||
| 
 | ||||
|             if (!useTheInternet) { | ||||
|                 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).then(rels => { | ||||
|                 hasRelations.setData(rels.length > 0) | ||||
|             }) | ||||
| 
 | ||||
|             OsmObject.DownloadReferencingWays(id).then(ways => { | ||||
|                 hasWays.setData(ways.length > 0) | ||||
|             }) | ||||
|             return true; // unregister to only run once
 | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const hasWaysOrRelations = hasRelations.map(hasRelationsData => { | ||||
|             if (hasRelationsData === true) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (hasWays.data === true) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (hasWays.data === null || hasRelationsData === null) { | ||||
|                 return null; | ||||
|             } | ||||
|             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 | ||||
|                     }) | ||||
|                 } else { | ||||
|                     // alright, this point can be safely deleted!
 | ||||
|                     state.setData({ | ||||
|                         canBeDeleted: true, | ||||
|                         reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete | ||||
|                     }) | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue