Rework split-road flow, fix #1166, fix #1148

This commit is contained in:
Pieter Vander Vennet 2022-12-24 01:58:52 +01:00
parent c8971a1cbe
commit 8f51dd8d64
4 changed files with 123 additions and 70 deletions

View file

@ -16,6 +16,7 @@ export default class SplitAction extends OsmChangeAction {
private readonly _splitPointsCoordinates: [number, number][] // lon, lat private readonly _splitPointsCoordinates: [number, number][] // lon, lat
private _meta: { theme: string; changeType: "split" } private _meta: { theme: string; changeType: "split" }
private _toleranceInMeters: number private _toleranceInMeters: number
private _withNewCoordinates: (coordinates: [number, number][]) => void
/** /**
* Create a changedescription for splitting a point. * Create a changedescription for splitting a point.
@ -24,17 +25,20 @@ export default class SplitAction extends OsmChangeAction {
* @param splitPointCoordinates: lon, lat * @param splitPointCoordinates: lon, lat
* @param meta * @param meta
* @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point * @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point
* @param withNewCoordinates: an optional callback which will leak the new coordinates of the original way
*/ */
constructor( constructor(
wayId: string, wayId: string,
splitPointCoordinates: [number, number][], splitPointCoordinates: [number, number][],
meta: { theme: string }, meta: { theme: string },
toleranceInMeters = 5 toleranceInMeters = 5,
withNewCoordinates?: (coordinates: [number, number][]) => void
) { ) {
super(wayId, true) super(wayId, true)
this.wayId = wayId this.wayId = wayId
this._splitPointsCoordinates = splitPointCoordinates this._splitPointsCoordinates = splitPointCoordinates
this._toleranceInMeters = toleranceInMeters this._toleranceInMeters = toleranceInMeters
this._withNewCoordinates = withNewCoordinates
this._meta = { ...meta, changeType: "split" } this._meta = { ...meta, changeType: "split" }
} }
@ -59,7 +63,7 @@ export default class SplitAction extends OsmChangeAction {
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
const originalNodes = originalElement.nodes const originalNodes = originalElement.nodes
// First, calculate splitpoints and remove points close to one another // First, calculate the splitpoints and remove points close to one another
const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters)
// Now we have a list with e.g. // Now we have a list with e.g.
// [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}] // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
@ -90,7 +94,7 @@ export default class SplitAction extends OsmChangeAction {
} }
const changeDescription: ChangeDescription[] = [] const changeDescription: ChangeDescription[] = []
// Let's create the new points as needed // Let's create the new nodes as needed
for (const element of splitInfo) { for (const element of splitInfo) {
if (element.originalIndex >= 0) { if (element.originalIndex >= 0) {
continue continue
@ -114,17 +118,21 @@ export default class SplitAction extends OsmChangeAction {
for (const wayPart of wayParts) { for (const wayPart of wayParts) {
let isOriginal = wayPart === longest let isOriginal = wayPart === longest
if (isOriginal) { if (isOriginal) {
// We change the actual element! // We change the existing way
const nodeIds = wayPart.map((p) => p.originalIndex) const nodeIds = wayPart.map((p) => p.originalIndex)
const newCoordinates = wayPart.map((p) => p.lngLat)
changeDescription.push({ changeDescription.push({
type: "way", type: "way",
id: originalElement.id, id: originalElement.id,
changes: { changes: {
coordinates: wayPart.map((p) => p.lngLat), coordinates: newCoordinates,
nodes: nodeIds, nodes: nodeIds,
}, },
meta: this._meta, meta: this._meta,
}) })
if (this._withNewCoordinates) {
this._withNewCoordinates(newCoordinates)
}
allWayIdsInOrder.push(originalElement.id) allWayIdsInOrder.push(originalElement.id)
allWaysNodesInOrder.push(nodeIds) allWaysNodesInOrder.push(nodeIds)
} else { } else {
@ -141,6 +149,10 @@ export default class SplitAction extends OsmChangeAction {
kv.push({ k: k, v: originalElement.tags[k] }) kv.push({ k: k, v: originalElement.tags[k] })
} }
const nodeIds = wayPart.map((p) => p.originalIndex) const nodeIds = wayPart.map((p) => p.originalIndex)
if (nodeIds.length <= 1) {
console.error("Got a segment with only one node - skipping")
continue
}
changeDescription.push({ changeDescription.push({
type: "way", type: "way",
id: id, id: id,

View file

@ -22,8 +22,10 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../../Logic/ElementStorage" import { ElementStorage } from "../../Logic/ElementStorage"
import BaseLayer from "../../Models/BaseLayer" import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
export default class SplitRoadWizard extends Toggle { export default class SplitRoadWizard extends Combine {
// @ts-ignore // @ts-ignore
private static splitLayerStyling = new LayerConfig( private static splitLayerStyling = new LayerConfig(
split_point, split_point,
@ -63,6 +65,106 @@ export default class SplitRoadWizard extends Toggle {
// Toggle variable between show split button and map // Toggle variable between show split button and map
const splitClicked = new UIEventSource<boolean>(false) const splitClicked = new UIEventSource<boolean>(false)
const leafletMap = new UIEventSource<BaseUIElement>(
SplitRoadWizard.setupMapComponent(id, splitPoints, state)
)
// Toggle between splitmap
const splitButton = new SubtleButton(
Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"),
new Toggle(
t.splitAgain.Clone().SetClass("text-lg font-bold"),
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),
hasBeenSplit
)
)
splitButton.onClick(() => {
splitClicked.setData(true)
})
// Only show the splitButton if logged in, else show login prompt
const loginBtn = t.loginToSplit
.Clone()
.onClick(() => state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly")
const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn)
// Save button
const saveButton = new Button(t.split.Clone(), async () => {
hasBeenSplit.setData(true)
splitClicked.setData(false)
const splitAction = new SplitAction(
id,
splitPoints.data.map((ff) => ff.feature.geometry.coordinates),
{
theme: state?.layoutToUse?.id,
},
5,
(coordinates) => {
state.allElements.ContainingFeatures.get(id).geometry["coordinates"] =
coordinates
}
)
await state.changes.applyAction(splitAction)
// We throw away the old map and splitpoints, and create a new map from scratch
splitPoints.setData([])
leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state))
})
saveButton.SetClass("btn btn-primary mr-3")
const disabledSaveButton = new Button("Split", undefined)
disabledSaveButton.SetClass("btn btn-disabled mr-3")
// Only show the save button if there are split points defined
const saveToggle = new Toggle(
disabledSaveButton,
saveButton,
splitPoints.map((data) => data.length === 0)
)
const cancelButton = Translations.t.general.cancel
.Clone() // Not using Button() element to prevent full width button
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
splitPoints.setData([])
splitClicked.setData(false)
})
cancelButton.SetClass("btn btn-secondary block")
const splitTitle = new Title(t.splitTitle)
const mapView = new Combine([
splitTitle,
new VariableUiElement(leafletMap),
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
])
mapView.SetClass("question")
super([
Toggle.If(hasBeenSplit, () =>
t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")
),
new Toggle(mapView, splitToggle, splitClicked),
])
this.dialogIsOpened = splitClicked
}
private static setupMapComponent(
id: string,
splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>,
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
}
): BaseUIElement {
// Load the road with given id on the minimap // Load the road with given id on the minimap
const roadElement = state.allElements.ContainingFeatures.get(id) const roadElement = state.allElements.ContainingFeatures.get(id)
@ -96,7 +198,6 @@ export default class SplitRoadWizard extends Toggle {
layerToShow: SplitRoadWizard.splitLayerStyling, layerToShow: SplitRoadWizard.splitLayerStyling,
state, state,
}) })
/** /**
* Handles a click on the overleaf map. * Handles a click on the overleaf map.
* Finds the closest intersection with the road and adds a point there, ready to confirm the cut. * Finds the closest intersection with the road and adds a point there, ready to confirm the cut.
@ -137,67 +238,6 @@ export default class SplitRoadWizard extends Toggle {
onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat])
}) })
) )
return miniMap
// Toggle between splitmap
const splitButton = new SubtleButton(
Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"),
t.inviteToSplit.Clone().SetClass("text-lg font-bold")
)
splitButton.onClick(() => {
splitClicked.setData(true)
})
// Only show the splitButton if logged in, else show login prompt
const loginBtn = t.loginToSplit
.Clone()
.onClick(() => state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly")
const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn)
// Save button
const saveButton = new Button(t.split.Clone(), () => {
hasBeenSplit.setData(true)
state.changes.applyAction(
new SplitAction(
id,
splitPoints.data.map((ff) => ff.feature.geometry.coordinates),
{
theme: state?.layoutToUse?.id,
}
)
)
})
saveButton.SetClass("btn btn-primary mr-3")
const disabledSaveButton = new Button("Split", undefined)
disabledSaveButton.SetClass("btn btn-disabled mr-3")
// Only show the save button if there are split points defined
const saveToggle = new Toggle(
disabledSaveButton,
saveButton,
splitPoints.map((data) => data.length === 0)
)
const cancelButton = Translations.t.general.cancel
.Clone() // Not using Button() element to prevent full width button
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
splitPoints.setData([])
splitClicked.setData(false)
})
cancelButton.SetClass("btn btn-secondary block")
const splitTitle = new Title(t.splitTitle)
const mapView = new Combine([
splitTitle,
miniMap,
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
])
mapView.SetClass("question")
const confirm = new Toggle(mapView, splitToggle, splitClicked)
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
this.dialogIsOpened = splitClicked
} }
} }

View file

@ -913,6 +913,7 @@
"inviteToSplit": "Split this road in smaller segments. This allows to give different properties to parts of the road.", "inviteToSplit": "Split this road in smaller segments. This allows to give different properties to parts of the road.",
"loginToSplit": "You must be logged in to split a road", "loginToSplit": "You must be logged in to split a road",
"split": "Split", "split": "Split",
"splitAgain": "Split this road again",
"splitTitle": "Choose on the map where the properties of this road change" "splitTitle": "Choose on the map where the properties of this road change"
}, },
"translations": { "translations": {

View file

@ -7594,7 +7594,7 @@ describe("GenerateCache", () => {
} }
mkdirSync(dir + "np-cache") mkdirSync(dir + "np-cache")
initDownloads( initDownloads(
"(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22selected%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*foot.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*hiking.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*bycicle.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*horse.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22selected%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*foot.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*hiking.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*bycicle.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*horse.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B"
) )
await main([ await main([
"natuurpunt", "natuurpunt",