forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			225 lines
		
	
	
		
			No EOL
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			No EOL
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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";
 | |
| 
 | |
| export default class DeleteAction {
 | |
| 
 | |
|     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
 | |
|     public readonly isDeleted = new UIEventSource<boolean>(false);
 | |
|     private readonly _id: string;
 | |
|     private readonly _allowDeletionAtChangesetCount: number;
 | |
| 
 | |
| 
 | |
|     constructor(id: string, allowDeletionAtChangesetCount?: number) {
 | |
|         this._id = id;
 | |
|         this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE;
 | |
| 
 | |
|         this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
 | |
|             canBeDeleted: undefined,
 | |
|             reason: Translations.t.delete.loading
 | |
|         })
 | |
| 
 | |
|         this.CheckDeleteability(false)
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * 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;
 | |
|                 }
 | |
| 
 | |
|                 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;
 | |
|         }
 | |
| 
 | |
|         // 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
 | |
|                     })
 | |
|                 }
 | |
| 
 | |
| 
 | |
|             }
 | |
|         )
 | |
| 
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
| } |