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

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

157
UI/Popup/MultiApply.ts Normal file
View file

@ -0,0 +1,157 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import {Changes} from "../../Logic/Osm/Changes";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {Tag} from "../../Logic/Tags/Tag";
import {ElementStorage} from "../../Logic/ElementStorage";
import {And} from "../../Logic/Tags/And";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import Toggle from "../Input/Toggle";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
export interface MultiApplyParams {
featureIds: UIEventSource<string[]>,
keysToApply: string[],
text: string,
autoapply: boolean,
overwrite: boolean,
tagsSource: UIEventSource<any>,
state: {
changes: Changes,
allElements: ElementStorage,
layoutToUse: LayoutConfig,
osmConnection: OsmConnection
}
}
class MultiApplyExecutor {
private readonly originalValues = new Map<string, string>()
private readonly params: MultiApplyParams;
private constructor(params: MultiApplyParams) {
this.params = params;
const p = params
for (const key of p.keysToApply) {
this.originalValues.set(key, p.tagsSource.data[key])
}
if (p.autoapply) {
const self = this;
const relevantValues = p.tagsSource.map(tags => {
const currentValues = p.keysToApply.map(key => tags[key])
// By stringifying, we have a very clear ping when they changec
return JSON.stringify(currentValues);
})
relevantValues.addCallbackD(_ => {
self.applyTaggingOnOtherFeatures()
})
}
}
public applyTaggingOnOtherFeatures() {
console.log("Multi-applying changes...")
const featuresToChange = this.params.featureIds.data
const changes = this.params.state.changes
const allElements = this.params.state.allElements
const keysToChange = this.params.keysToApply
const overwrite = this.params.overwrite
const selfTags = this.params.tagsSource.data;
const theme = this.params.state.layoutToUse.id
for (const id of featuresToChange) {
const tagsToApply: Tag[] = []
const otherFeatureTags = allElements.getEventSourceById(id).data
for (const key of keysToChange) {
const newValue = selfTags[key]
if (newValue === undefined) {
continue
}
const otherValue = otherFeatureTags[key]
if (newValue === otherValue) {
continue;// No changes to be made
}
if (overwrite) {
tagsToApply.push(new Tag(key, newValue))
continue;
}
if (otherValue === undefined || otherValue === "" || otherValue === this.originalValues.get(key)) {
tagsToApply.push(new Tag(key, newValue))
}
}
if (tagsToApply.length == 0) {
continue;
}
changes.applyAction(
new ChangeTagAction(id, new And(tagsToApply), otherFeatureTags, {
theme,
changeType: "answer"
}))
}
}
private static executorCache = new Map<string, MultiApplyExecutor>()
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
if (MultiApplyExecutor.executorCache.has(id)) {
return MultiApplyExecutor.executorCache.get(id)
}
const applicator = new MultiApplyExecutor(params)
MultiApplyExecutor.executorCache.set(id, applicator)
return applicator
}
}
export default class MultiApply extends Toggle {
constructor(params: MultiApplyParams) {
const p = params
const t = Translations.t.multi_apply
const featureId = p.tagsSource.data.id
if (featureId === undefined) {
throw "MultiApply needs a feature id"
}
const applicator = MultiApplyExecutor.GetApplicator(featureId, params)
const elems: (string | BaseUIElement)[] = []
if (p.autoapply) {
elems.push(new FixedUiElement(p.text).SetClass("block"))
elems.push(new VariableUiElement(p.featureIds.map(featureIds =>
t.autoApply.Subs({
attr_names: p.keysToApply.join(", "),
count: "" + featureIds.length
}))).SetClass("block subtle text-sm"))
} else {
elems.push(
new SubtleButton(undefined, p.text).onClick(() => applicator.applyTaggingOnOtherFeatures())
)
}
const isShown: UIEventSource<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => {
return loggedIn && p.featureIds.data.length > 0
}, [p.featureIds])
super(new Combine(elems), undefined, isShown);
}
}

View file

@ -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");

View file

@ -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")
})
@ -133,7 +136,7 @@ export default class TagRenderingQuestion extends Combine {
options.cancelButton,
saveButton,
bottomTags])
this.SetClass("question")
this.SetClass("question disable-links")
}