Refactoring: fix 'delete' and 'move'-buttons as special elements

This commit is contained in:
Pieter Vander Vennet 2023-04-14 02:42:57 +02:00
parent 466dd16568
commit 8a1f0599d9
19 changed files with 317 additions and 296 deletions

View file

@ -20,10 +20,10 @@ import { RadioButton } from "../Input/RadioButton"
import { FixedInputElement } from "../Input/FixedInputElement"
import Title from "../Base/Title"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import TagRenderingQuestion from "./TagRenderingQuestion"
import { OsmId } from "../../Models/OsmFeature"
import { OsmId, OsmTags } from "../../Models/OsmFeature"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
export default class DeleteWizard extends Toggle {
/**
@ -41,13 +41,18 @@ export default class DeleteWizard extends Toggle {
* Ideal for the case of "THIS PATH IS ON MY GROUND AND SHOULD BE DELETED IMMEDIATELY OR I WILL GET MY LAWYER" but to mark it as private instead.
* (Note that _delete_reason is used as trigger to do actual deletion - setting such a tag WILL delete from the database with that as changeset comment)
*
* @param id: The id of the element to remove
* @param state: the state of the application
* @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted
*/
constructor(id: OsmId, state: FeaturePipelineState, options: DeleteConfig) {
const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets)
const tagsSource = state.allElements.getEventSourceById(id)
constructor(
id: OsmId,
tagsSource: UIEventSource<OsmTags>,
state: SpecialVisualizationState,
options: DeleteConfig
) {
const deleteAbility = new DeleteabilityChecker(
id,
state.osmConnection,
options.neededChangesets
)
const isDeleted = new UIEventSource(false)
const allowSoftDeletion = !!options.softDeletionTags
@ -62,7 +67,7 @@ export default class DeleteWizard extends Toggle {
if (selected["retagTo"] !== undefined) {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
actionToTake = new ChangeTagAction(id, selected["retagTo"], tagsSource.data, {
theme: state?.layoutToUse?.id ?? "unkown",
theme: state?.layout?.id ?? "unkown",
changeType: "special-delete",
})
} else {
@ -70,7 +75,7 @@ export default class DeleteWizard extends Toggle {
id,
options.softDeletionTags,
{
theme: state?.layoutToUse?.id ?? "unkown",
theme: state?.layout?.id ?? "unkown",
specialMotivation: selected["deleteReason"],
},
deleteAbility.canBeDeleted.data.canBeDeleted
@ -250,7 +255,7 @@ export default class DeleteWizard extends Toggle {
private static constructMultipleChoice(
config: DeleteConfig,
tagsSource: UIEventSource<Record<string, string>>,
state: FeaturePipelineState
state: SpecialVisualizationState
): InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[] = []
@ -282,19 +287,13 @@ export default class DeleteWizard extends Toggle {
class DeleteabilityChecker {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>
private readonly _id: string
private readonly _id: OsmId
private readonly _allowDeletionAtChangesetCount: number
private readonly _state: {
osmConnection: OsmConnection
}
private readonly _osmConnection: OsmConnection
constructor(
id: string,
state: { osmConnection: OsmConnection },
allowDeletionAtChangesetCount?: number
) {
constructor(id: OsmId, osmConnection: OsmConnection, allowDeletionAtChangesetCount?: number) {
this._id = id
this._state = state
this._osmConnection = osmConnection
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
@ -324,7 +323,7 @@ class DeleteabilityChecker {
}
// Does the currently logged in user have enough experience to delete this point?
const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map((ud) => {
const deletingPointsOfOtherAllowed = this._osmConnection.userDetails.map((ud) => {
if (ud === undefined) {
return undefined
}
@ -347,10 +346,10 @@ class DeleteabilityChecker {
// Not yet downloaded
return null
}
const userId = self._state.osmConnection.userDetails.data.uid
const userId = self._osmConnection.userDetails.data.uid
return !previous.some((editor) => editor !== userId)
},
[self._state.osmConnection.userDetails]
[self._osmConnection.userDetails]
)
// User allowed OR only edited by self?

View file

@ -17,7 +17,6 @@ import MoveWizard from "./MoveWizard"
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { Tag } from "../../Logic/Tags/Tag"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
@ -32,9 +31,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
setHash?: true | boolean
}
) {
if (state === undefined) {
throw "State is undefined!"
}
const showAllQuestions = state.featureSwitchShowAllQuestions.map(
(fsShow) => fsShow || state.showAllQuestionsAtOnce.data,
[state.showAllQuestionsAtOnce]
@ -98,27 +94,11 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateMainContent(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState,
showAllQuestions?: Store<boolean>
state: FeaturePipelineState
): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>()
const t = Translations.t.general
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map((tr) => tr.group))
if (state?.featureSwitchUserbadge?.data ?? true) {
const questionSpecs = layerConfig.tagRenderings.filter((tr) => tr.id === "questions")
for (const groupName of allGroupNames) {
const questions = layerConfig.tagRenderings.filter((tr) => tr.group === groupName)
const questionSpec = questionSpecs.filter((tr) => tr.group === groupName)[0]
const questionBox = new QuestionBox(state, {
tagsSource: tags,
tagRenderings: questions,
units: layerConfig.units,
showAllQuestionsAtOnce:
questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? showAllQuestions,
})
questionBoxes.set(groupName, questionBox)
}
}
const withQuestion = layerConfig.tagRenderings.filter(
(tr) => tr.question !== undefined
@ -243,40 +223,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
editElements.push(questionBox)
})
if (layerConfig.allowMove) {
editElements.push(
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => {
const feature = state.allElements.ContainingFeatures.get(id)
if (feature === undefined) {
return "This feature is not register in the state.allElements and cannot be moved"
}
return new MoveWizard(feature, state, layerConfig.allowMove)
})
).SetClass("text-base")
)
}
if (layerConfig.deletion) {
editElements.push(
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => new DeleteWizard(id, state, layerConfig.deletion))
).SetClass("text-base")
)
}
if (layerConfig.allowSplit) {
editElements.push(
new VariableUiElement(
tags.map((tags) => tags.id).map((id) => new SplitRoadWizard(id, state))
).SetClass("text-base")
)
}
editElements.push(
new VariableUiElement(
state.osmConnection.userDetails
@ -302,30 +248,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
)
)
editElements.push(
Toggle.If(state.featureSwitchIsDebugging, () => {
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
{ render: "{all_tags()}" },
""
)
const config_download: TagRenderingConfig = new TagRenderingConfig(
{ render: "{export_as_geojson()}" },
""
)
const config_id: TagRenderingConfig = new TagRenderingConfig(
{ render: "{open_in_iD()}" },
""
)
return new Combine([
new TagRenderingAnswer(tags, config_all_tags, state),
new TagRenderingAnswer(tags, config_download, state),
new TagRenderingAnswer(tags, config_id, state),
"This is layer " + layerConfig.id,
])
})
)
return new Combine(editElements).SetClass("flex flex-col")
}
}

View file

@ -1,29 +1,28 @@
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
import LocationInput from "../Input/LocationInput"
import Loc from "../../Models/Loc"
import { GeoOperations } from "../../Logic/GeoOperations"
import { OsmObject } from "../../Logic/Osm/OsmObject"
import { Changes } from "../../Logic/Osm/Changes"
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import MoveConfig from "../../Models/ThemeConfig/MoveConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import BaseLayer from "../../Models/BaseLayer"
import SearchAndGo from "../BigComponents/SearchAndGo"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature, Point } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
import SvelteUIElement from "../Base/SvelteUIElement"
import { MapProperties } from "../../Models/MapProperties"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import Geosearch from "../BigComponents/Geosearch.svelte"
import Constants from "../../Models/Constants"
interface MoveReason {
text: Translation | string
@ -43,14 +42,9 @@ export default class MoveWizard extends Toggle {
* The UI-element which helps moving a point
*/
constructor(
featureToMove: any,
state: {
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
},
featureToMove: Feature<Point>,
tags: UIEventSource<OsmTags>,
state: SpecialVisualizationState,
options: MoveConfig
) {
const t = Translations.t.move
@ -130,56 +124,38 @@ export default class MoveWizard extends Toggle {
if (reason === undefined) {
return undefined
}
const loc = new UIEventSource<Loc>({
lon: lon,
lat: lat,
zoom: reason?.startZoom ?? 16,
})
let background: string[]
if (typeof reason.background == "string") {
background = [reason.background]
} else {
background = reason.background
const mapProperties: Partial<MapProperties> = {
minzoom: new UIEventSource(reason.minZoom),
zoom: new UIEventSource(reason?.startZoom ?? 16),
location: new UIEventSource({ lon, lat }),
bounds: new UIEventSource(undefined),
}
const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(
loc,
new UIEventSource(background)
).data
const locationInput = new LocationInput({
minZoom: reason.minZoom,
centerLocation: loc,
mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer
state: <any>state,
const value = new UIEventSource<{ lon: number; lat: number }>(undefined)
const locationInput = new SvelteUIElement(LocationInput, {
mapProperties,
value,
})
if (reason.lockBounds) {
locationInput.installBounds(0.05, true)
}
let searchPanel: BaseUIElement = undefined
if (reason.includeSearch) {
searchPanel = new SearchAndGo({
leafletMap: locationInput.leafletMap,
})
searchPanel = new SvelteUIElement(Geosearch, { bounds: mapProperties.bounds })
}
locationInput.SetStyle("height: 17.5rem")
const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove)
confirmMove.onClick(async () => {
const loc = locationInput.GetValue().data
const loc = value.data
await state.changes.applyAction(
new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], {
reason: reason.changesetCommentValue,
theme: state.layoutToUse.id,
theme: state.layout.id,
})
)
featureToMove.properties._lat = loc.lat
featureToMove.properties._lon = loc.lon
featureToMove.geometry.coordinates = [loc.lon, loc.lat]
if (reason.eraseAddressFields) {
await state.changes.applyAction(
new ChangeTagAction(
@ -193,13 +169,13 @@ export default class MoveWizard extends Toggle {
featureToMove.properties,
{
changeType: "relocated",
theme: state.layoutToUse.id,
theme: state.layout.id,
}
)
)
}
state.allElements.getEventSourceById(id).ping()
state.featureProperties.getStore(id).ping()
currentStep.setData("moved")
})
const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6")
@ -209,7 +185,7 @@ export default class MoveWizard extends Toggle {
new Toggle(
confirmMove,
zoomInFurhter,
locationInput.GetValue().map((l) => l.zoom >= 19)
mapProperties.zoom.map((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint)
),
]).SetClass("flex flex-col")
})

View file

@ -2,33 +2,27 @@ import Toggle from "../Input/Toggle"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LeafletMouseEvent } from "leaflet"
import Combine from "../Base/Combine"
import { Button } from "../Base/Button"
import Translations from "../i18n/Translations"
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Title from "../Base/Title"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox"
import split_point from "../../assets/layers/split_point/split_point.json"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualizationState } from "../SpecialVisualization"
export default class SplitRoadWizard extends Combine {
// @ts-ignore
private static splitLayerStyling = new LayerConfig(
split_point,
"(BUILTIN) SplitRoadWizard.ts",
@ -43,22 +37,7 @@ export default class SplitRoadWizard extends Combine {
* @param id: The id of the road to remove
* @param state: the state of the application
*/
constructor(
id: string,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>
backgroundLayer: UIEventSource<BaseLayer>
featureSwitchIsTesting: UIEventSource<boolean>
featureSwitchIsDebugging: UIEventSource<boolean>
featureSwitchShowAllQuestions: UIEventSource<boolean>
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
selectedElement: UIEventSource<any>
}
) {
constructor(id: string, state: SpecialVisualizationState) {
const t = Translations.t.split
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring

View file

@ -26,18 +26,27 @@
}));
let htmlElem: HTMLElement;
const _htmlElement = new UIEventSource<HTMLElement>(undefined);
$: _htmlElement.setData(htmlElem);
function setHighlighting() {
if (highlightedRendering === undefined) {
return;
}
if (htmlElem === undefined) {
return;
}
const highlighted = highlightedRendering.data;
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow");
} else {
htmlElem.classList.remove("glowing-shadow");
}
}
if (highlightedRendering) {
$: onDestroy(highlightedRendering.addCallbackAndRun(highlighted => {
console.log("Highlighted rendering is", highlighted)
if(htmlElem === undefined){
return
}
if (config.id === highlighted) {
htmlElem.classList.add("glowing-shadow");
} else {
htmlElem.classList.remove("glowing-shadow");
}
}));
onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting()))
onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting()))
}