Merge develop

This commit is contained in:
Pieter Vander Vennet 2021-10-13 17:23:51 +02:00
commit 448468c928
97 changed files with 5039 additions and 1139 deletions

View file

@ -5,6 +5,24 @@ import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
*/
export interface ChangeDescription {
/**
* Metadata to be included in the changeset
*/
meta: {
/*
* The theme with which this changeset was made
*/
theme: string,
/**
* The type of the change
*/
changeType: "answer" | "create" | "split" | "delete" | string
/**
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
*/
specialMotivation?: string
},
/**
* Identifier of the object
*/

View file

@ -7,12 +7,17 @@ export default class ChangeTagAction extends OsmChangeAction {
private readonly _elementId: string;
private readonly _tagsFilter: TagsFilter;
private readonly _currentTags: any;
private readonly _meta: {theme: string, changeType: string};
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any) {
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
theme: string,
changeType: "answer" | "soft-delete" | "add-image"
}) {
super();
this._elementId = elementId;
this._tagsFilter = tagsFilter;
this._currentTags = currentTags;
this._meta = meta;
}
/**
@ -43,10 +48,10 @@ export default class ChangeTagAction extends OsmChangeAction {
const type = typeId[0]
const id = Number(typeId [1])
return [{
// @ts-ignore
type: type,
type: <"node"|"way"|"relation"> type,
id: id,
tags: changedTags
tags: changedTags,
meta: this._meta
}]
}
}

View file

@ -14,8 +14,14 @@ export default class CreateNewNodeAction extends OsmChangeAction {
private readonly _lon: number;
private readonly _snapOnto: OsmWay;
private readonly _reusePointDistance: number;
private meta: { changeType: "create" | "import"; theme: string };
constructor(basicTags: Tag[], lat: number, lon: number, options?: { snapOnto: OsmWay, reusePointWithinMeters?: number }) {
constructor(basicTags: Tag[],
lat: number, lon: number,
options: {
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" }) {
super()
this._basicTags = basicTags;
this._lat = lat;
@ -25,6 +31,10 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
this._snapOnto = options?.snapOnto;
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
this.meta = {
theme: options.theme,
changeType: options.changeType
}
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
@ -47,7 +57,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: {
lat: this._lat,
lon: this._lon
}
},
meta: this.meta
}
if (this._snapOnto === undefined) {
return [newPointChange]
@ -78,7 +89,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
return [{
tags: new And(this._basicTags).asChange(properties),
type: "node",
id: reusedPointId
id: reusedPointId,
meta: this.meta
}]
}
@ -99,7 +111,8 @@ export default class CreateNewNodeAction extends OsmChangeAction {
changes: {
coordinates: locations,
nodes: ids
}
},
meta:this.meta
}
]
}

View file

@ -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
})
}
}
)
}
}

View file

@ -16,14 +16,16 @@ export interface RelationSplitInput {
*/
export default class RelationSplitHandler extends OsmChangeAction {
private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) {
constructor(input: RelationSplitInput, theme: string) {
super()
this._input = input;
this._theme = theme;
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
return new InPlaceReplacedmentRTSH(this._input).CreateChangeDescriptions(changes)
return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions(changes)
}
@ -39,10 +41,12 @@ export default class RelationSplitHandler extends OsmChangeAction {
*/
export class InPlaceReplacedmentRTSH extends OsmChangeAction {
private readonly _input: RelationSplitInput;
private readonly _theme: string;
constructor(input: RelationSplitInput) {
constructor(input: RelationSplitInput, theme: string) {
super();
this._input = input;
this._theme = theme;
}
/**
@ -137,7 +141,11 @@ export class InPlaceReplacedmentRTSH extends OsmChangeAction {
return [{
id: relation.id,
type: "relation",
changes: {members: newMembers}
changes: {members: newMembers},
meta:{
changeType: "relation-fix",
theme: this._theme
}
}];
}

View file

@ -14,16 +14,19 @@ interface SplitInfo {
export default class SplitAction extends OsmChangeAction {
private readonly wayId: string;
private readonly _splitPointsCoordinates: [number, number] []// lon, lat
private _meta: { theme: string, changeType: "split" };
/**
*
* @param wayId
* @param splitPointCoordinates: lon, lat
* @param meta
*/
constructor(wayId: string, splitPointCoordinates: [number, number][]) {
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: {theme: string}) {
super()
this.wayId = wayId;
this._splitPointsCoordinates = splitPointCoordinates
this._meta = {...meta, changeType: "split"};
}
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
@ -89,7 +92,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
lon: element.lngLat[0],
lat: element.lngLat[1]
}
},
meta: this._meta
})
}
@ -110,7 +114,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds
}
},
meta: this._meta
})
allWayIdsInOrder.push(originalElement.id)
allWaysNodesInOrder.push(nodeIds)
@ -135,7 +140,8 @@ export default class SplitAction extends OsmChangeAction {
changes: {
coordinates: wayPart.map(p => p.lngLat),
nodes: nodeIds
}
},
meta: this._meta
})
allWayIdsInOrder.push(id)
@ -152,8 +158,8 @@ export default class SplitAction extends OsmChangeAction {
allWayIdsInOrder: allWayIdsInOrder,
originalNodes: originalNodes,
allWaysNodesInOrder: allWaysNodesInOrder,
originalWayId: originalElement.id
}).CreateChangeDescriptions(changes)
originalWayId: originalElement.id,
}, this._meta.theme).CreateChangeDescriptions(changes)
changeDescription.push(...changDescrs)
}
@ -240,7 +246,6 @@ export default class SplitAction extends OsmChangeAction {
closest = prevPoint
}
// Ok, we have a closest point!
if(closest.originalIndex === 0 || closest.originalIndex === originalPoints.length){
// We can not split on the first or last points...
continue