forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
448468c928
97 changed files with 5039 additions and 1139 deletions
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue