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
				
			
		|  | @ -37,7 +37,10 @@ export default class ImportButton extends Toggle { | |||
|             } | ||||
|             originalTags.data["_imported"] = "yes" | ||||
|             originalTags.ping() // will set isImported as per its definition
 | ||||
|             const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon) | ||||
|             const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, { | ||||
|                 theme: State.state.layoutToUse.id, | ||||
|                 changeType: "import" | ||||
|             }) | ||||
|             await State.state.changes.applyAction(newElementAction) | ||||
|             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|  |  | |||
|  | @ -56,7 +56,10 @@ export default class SimpleAddUI extends Toggle { | |||
| 
 | ||||
| 
 | ||||
|        async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) { | ||||
|             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {snapOnto: snapOntoWay}) | ||||
|             const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { | ||||
|                 theme: State.state?.layoutToUse?.id ?? "unkown", | ||||
|                 changeType: "create", | ||||
|                 snapOnto: snapOntoWay}) | ||||
|             await State.state.changes.applyAction(newElementAction) | ||||
|             selectedPreset.setData(undefined) | ||||
|             isShown.setData(false) | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import Toggle from "../Input/Toggle"; | |||
| import Translations from "../i18n/Translations"; | ||||
| import Svg from "../../Svg"; | ||||
| import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||
| import TagRenderingQuestion from "./TagRenderingQuestion"; | ||||
|  | @ -13,13 +12,11 @@ import {SubtleButton} from "../Base/SubtleButton"; | |||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import {Translation} from "../i18n/Translation"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {Changes} from "../../Logic/Osm/Changes"; | ||||
| import {And} from "../../Logic/Tags/And"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; | ||||
| import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"; | ||||
| import {OsmObject} from "../../Logic/Osm/OsmObject"; | ||||
| 
 | ||||
| export default class DeleteWizard extends Toggle { | ||||
|     /** | ||||
|  | @ -43,44 +40,32 @@ export default class DeleteWizard extends Toggle { | |||
|     constructor(id: string, | ||||
|                 options: DeleteConfig) { | ||||
| 
 | ||||
|         const deleteAction = new DeleteAction(id, options.neededChangesets); | ||||
|         const deleteAbility = new DeleteabilityChecker(id, options.neededChangesets) | ||||
|         const tagsSource = State.state.allElements.getEventSourceById(id) | ||||
| 
 | ||||
|         const isDeleted = new UIEventSource(false) | ||||
|         const allowSoftDeletion = !!options.softDeletionTags | ||||
| 
 | ||||
|         const confirm = new UIEventSource<boolean>(false) | ||||
| 
 | ||||
| 
 | ||||
|         async function softDelete(reason: string, tagsToApply: { k: string, v: string }[]) { | ||||
|             if (reason !== undefined) { | ||||
|                 tagsToApply.splice(0, 0, { | ||||
|                     k: "fixme", | ||||
|                     v: `A mapcomplete user marked this feature to be deleted (${reason})` | ||||
|                 }) | ||||
|             } | ||||
|             await (State.state?.changes ?? new Changes()) | ||||
|                 .applyAction(new ChangeTagAction( | ||||
|                     id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data | ||||
|                 )) | ||||
|         } | ||||
| 
 | ||||
|         function doDelete(selected: TagsFilter) { | ||||
|             // Selected == the reasons, not the tags of the object
 | ||||
|             const tgs = selected.asChange(tagsSource.data) | ||||
|             const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason") | ||||
|             if (deleteReasonMatch.length > 0) { | ||||
|                 // We should actually delete!
 | ||||
|                 const deleteReason = deleteReasonMatch[0].v | ||||
|                 deleteAction.DoDelete(deleteReason, () => { | ||||
|                     // The user doesn't have sufficient permissions to _actually_ delete the feature
 | ||||
|                     // We 'soft delete' instead (and add a fixme)
 | ||||
|                     softDelete(deleteReason, tgs.filter(kv => kv.k !== "_delete_reason")) | ||||
| 
 | ||||
|                 }); | ||||
|                 return | ||||
|             } else { | ||||
|                 // This is a 'non-delete'-option that was selected
 | ||||
|                 softDelete(undefined, tgs) | ||||
|             if (deleteReasonMatch.length === 0) { | ||||
|                 return; | ||||
|             } | ||||
|             const deleteAction = new DeleteAction(id, | ||||
|                 options.softDeletionTags, | ||||
|                 { | ||||
|                     theme: State.state?.layoutToUse?.id ?? "unkown", | ||||
|                     specialMotivation: deleteReasonMatch[0]?.v | ||||
|                 }, | ||||
|                 deleteAbility.canBeDeleted.data.canBeDeleted | ||||
|             ) | ||||
|             State.state.changes.applyAction(deleteAction) | ||||
|             isDeleted.setData(true) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|  | @ -98,7 +83,7 @@ export default class DeleteWizard extends Toggle { | |||
|                     saveButtonConstr: (v) => DeleteWizard.constructConfirmButton(v).onClick(() => { | ||||
|                         doDelete(v.data) | ||||
|                     }), | ||||
|                     bottomText: (v) => DeleteWizard.constructExplanation(v, deleteAction) | ||||
|                     bottomText: (v) => DeleteWizard.constructExplanation(v, deleteAbility) | ||||
|                 } | ||||
|             ) | ||||
|         })) | ||||
|  | @ -110,7 +95,7 @@ export default class DeleteWizard extends Toggle { | |||
|         const deleteButton = new SubtleButton( | ||||
|             Svg.delete_icon_ui().SetStyle("width: 2rem; height: 2rem;"), t.delete.Clone()).onClick( | ||||
|             () => { | ||||
|                 deleteAction.CheckDeleteability(true) | ||||
|                 deleteAbility.CheckDeleteability(true) | ||||
|                 confirm.setData(true); | ||||
|             } | ||||
|         ).SetClass("w-1/2 float-right"); | ||||
|  | @ -132,13 +117,13 @@ export default class DeleteWizard extends Toggle { | |||
| 
 | ||||
|                             deleteButton, | ||||
|                             confirm), | ||||
|                         new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))), | ||||
|                         deleteAction.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), | ||||
|                         new VariableUiElement(deleteAbility.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))), | ||||
|                         deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), | ||||
| 
 | ||||
|                     t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), | ||||
|                     State.state.osmConnection.isLoggedIn | ||||
|                 ), | ||||
|                 deleteAction.isDeleted), | ||||
|                 isDeleted), | ||||
|             undefined, | ||||
|             isShown) | ||||
| 
 | ||||
|  | @ -167,7 +152,7 @@ export default class DeleteWizard extends Toggle { | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static constructExplanation(tags: UIEventSource<TagsFilter>, deleteAction: DeleteAction) { | ||||
|     private static constructExplanation(tags: UIEventSource<TagsFilter>, deleteAction: DeleteabilityChecker) { | ||||
|         const t = Translations.t.delete; | ||||
|         return new VariableUiElement(tags.map( | ||||
|             currentTags => { | ||||
|  | @ -263,4 +248,172 @@ export default class DeleteWizard extends Toggle { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class DeleteabilityChecker { | ||||
| 
 | ||||
|     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; | ||||
|     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) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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 | ||||
|                     }) | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -136,7 +136,9 @@ export default class SplitRoadWizard extends Toggle { | |||
|         // Save button
 | ||||
|         const saveButton = new Button(t.split.Clone(), () => { | ||||
|             hasBeenSplit.setData(true) | ||||
|             State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates))) | ||||
|             State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), { | ||||
|                 theme: State.state?.layoutToUse?.id | ||||
|             })) | ||||
|         }) | ||||
| 
 | ||||
|         saveButton.SetClass("btn btn-primary mr-3"); | ||||
|  |  | |||
|  | @ -86,7 +86,10 @@ export default class TagRenderingQuestion extends Combine { | |||
|             if (selection) { | ||||
|                 (State.state?.changes ?? new Changes()) | ||||
|                     .applyAction(new ChangeTagAction( | ||||
|                         tags.data.id, selection, tags.data | ||||
|                         tags.data.id, selection, tags.data, { | ||||
|                             theme: State.state?.layoutToUse?.id ?? "unkown", | ||||
|                             changeType: "answer", | ||||
|                         } | ||||
|                     )).then(_ => { | ||||
|                     console.log("Tagchanges applied") | ||||
|                 }) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue