UX: fix #2257 , clearer UI for splitting a road

This commit is contained in:
Pieter Vander Vennet 2024-11-19 16:42:53 +01:00
parent 8be5ecb85a
commit c667f384c7
8 changed files with 135 additions and 40 deletions

View file

@ -16,7 +16,13 @@
"marker": [ "marker": [
{ {
"icon": "circle", "icon": "circle",
"color": "white" "color": {
"render": "white",
"mappings": [{
"if": "reuse=yes",
"then": "#cccccc"
}]
}
}, },
{ {
"icon": "./assets/svg/scissors.svg" "icon": "./assets/svg/scissors.svg"

View file

@ -17,13 +17,33 @@
"icon": "bug" "icon": "bug"
} }
] ]
},
{
"location": [
"waypoints"
],
"iconSize": "4,4",
"anchor": "center",
"marker": [
{
"icon": {
"render": "circle",
"id": "circle"
},
"color": "#888888"
}
]
} }
], ],
"lineRendering": [ "lineRendering": [ {
"width": "13",
"color": "black"
},
{ {
"width": "8", "width": "8",
"color": "black" "color": "white"
} }
], ],
"allowMove": false "allowMove": false
} }

View file

@ -829,7 +829,7 @@ export class GeoOperations {
} }
return undefined return undefined
default: default:
throw "Unkown location type: " + location + " for feature " + feature.properties.id throw "Unknown location type: " + location + " for feature " + feature.properties.id
} }
} }

View file

@ -29,7 +29,7 @@ export default interface PointRenderingConfigJson {
/** /**
* question: At what location should this icon be shown? * question: At what location should this icon be shown?
* multianswer: true * multianswer: true
* suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line. Does not show an item on polygons"}, {if: "value=polygon_centroid",then: "Show an icon at a polygon centroid (but not if it is a way)"}] * suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line. Does not show an item on polygons"}, {if: "value=polygon_centroid",then: "Show an icon at a polygon centroid (but not if it is a way)"}, {if: "value=waypoints", then: "Show an icon on every intermediate point of a way"}]
*/ */
location: ( location: (
| "point" | "point"
@ -38,6 +38,7 @@ export default interface PointRenderingConfigJson {
| "end" | "end"
| "projected_centerpoint" | "projected_centerpoint"
| "polygon_centroid" | "polygon_centroid"
| "waypoints"
| string | string
)[] )[]

View file

@ -41,6 +41,7 @@ export default class PointRenderingConfig extends WithContextLoader {
"end", "end",
"projected_centerpoint", "projected_centerpoint",
"polygon_centroid", "polygon_centroid",
"waypoints"
]) ])
public readonly location: Set< public readonly location: Set<
| "point" | "point"
@ -49,6 +50,7 @@ export default class PointRenderingConfig extends WithContextLoader {
| "end" | "end"
| "projected_centerpoint" | "projected_centerpoint"
| "polygon_centroid" | "polygon_centroid"
| "waypoints"
| string | string
> >

View file

@ -53,10 +53,15 @@
*/ */
export let mapProperties: undefined | Partial<MapProperties> = undefined export let mapProperties: undefined | Partial<MapProperties> = undefined
/**
* Reuse a point if the clicked location is within this amount of meter
*/
export let snapTolerance: number = 5
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let adaptor = new MapLibreAdaptor(map, mapProperties) let adaptor = new MapLibreAdaptor(map, mapProperties)
const wayGeojson: Feature<LineString> = GeoOperations.forceLineString(osmWay.asGeoJson()) let wayGeojson: Feature<LineString> = GeoOperations.forceLineString(osmWay.asGeoJson())
adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson)) adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson))
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2)) adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2)) adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
@ -64,7 +69,7 @@
state?.showCurrentLocationOn(map) state?.showCurrentLocationOn(map)
new ShowDataLayer(map, { new ShowDataLayer(map, {
features: new StaticFeatureSource([wayGeojson]), features: new StaticFeatureSource([wayGeojson]),
drawMarkers: false, drawMarkers: true,
layer: layer, layer: layer,
}) })
@ -85,8 +90,8 @@
layer: splitpoint_style, layer: splitpoint_style,
features: splitPointsFS, features: splitPointsFS,
onClick: (clickedFeature: Feature) => { onClick: (clickedFeature: Feature) => {
console.log("Clicked feature is", clickedFeature, splitPoints.data) // A 'splitpoint' was clicked, so we remove it again
const i = splitPoints.data.findIndex((f) => f === clickedFeature) const i = splitPoints.data.findIndex((f) => f.properties.id === clickedFeature.properties.id)
if (i < 0) { if (i < 0) {
return return
} }
@ -96,7 +101,47 @@
}) })
let id = 0 let id = 0
adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => { adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => {
const projected = GeoOperations.nearestPoint(wayGeojson, [lon, lat]) let projected: Feature<Point, {index:number, id?: number, reuse?: string}> = GeoOperations.nearestPoint(wayGeojson, [lon, lat])
console.log("Added splitpoint", projected, id)
// We check the next and the previous point. If those are closer then the tolerance, we reuse those instead
const i = projected.properties.index
const p = projected.geometry.coordinates
const way = wayGeojson.geometry.coordinates
const nextPoint = <[number,number]> way[i + 1]
const nextDistance = GeoOperations.distanceBetween(nextPoint, p)
const previousPoint = <[number,number]> way[i]
const previousDistance = GeoOperations.distanceBetween(previousPoint, p)
console.log("ND", nextDistance, "PD", previousDistance)
if(nextDistance <= snapTolerance && previousDistance >= nextDistance){
projected = {
type:"Feature",
geometry: {
type:"Point",
coordinates: nextPoint
},
properties: {
index: i+1,
reuse: "yes"
}
}
}
if (previousDistance <= snapTolerance && previousDistance < nextDistance){
projected = {
type:"Feature",
geometry: {
type:"Point",
coordinates: previousPoint
},
properties: {
index: i,
reuse: "yes"
}
}
}
projected.properties["id"] = id projected.properties["id"] = id
id++ id++

View file

@ -16,6 +16,7 @@ import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFea
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { TagsFilter } from "../../Logic/Tags/TagsFilter" import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { featureEach } from "@turf/turf"
class PointRenderingLayer { class PointRenderingLayer {
private readonly _config: PointRenderingConfig private readonly _config: PointRenderingConfig
@ -38,7 +39,7 @@ class PointRenderingLayer {
visibility?: Store<boolean>, visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>, fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void, onClick?: (feature: Feature) => void,
selectedElement?: Store<{ properties: { id?: string } }> selectedElement?: Store<{ properties: { id?: string } }>,
) { ) {
this._visibility = visibility this._visibility = visibility
this._config = config this._config = config
@ -97,15 +98,31 @@ class PointRenderingLayer {
" while rendering", " while rendering",
location, location,
"of", "of",
this._config this._config,
) )
} }
const id = feature.properties.id + "-" + location const id = feature.properties.id + "-" + location
unseenKeys.delete(id) unseenKeys.delete(id)
if (location === "waypoints") {
if (feature.geometry.type === "LineString") {
for (const loc of feature.geometry.coordinates) {
this.addPoint(feature, <[number, number]>loc)
}
}
if (feature.geometry.type === "MultiLineString" || feature.geometry.type === "Polygon") {
for (const coors of feature.geometry.coordinates) {
for (const loc of coors) {
this.addPoint(feature, <[number, number]>loc)
}
}
}
continue
}
const loc = GeoOperations.featureToCoordinateWithRenderingType( const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>feature, <any>feature,
location location,
) )
if (loc === undefined) { if (loc === undefined) {
continue continue
@ -234,7 +251,7 @@ class LineRenderingLayer {
config: LineRenderingConfig, config: LineRenderingConfig,
visibility?: Store<boolean>, visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>, fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void onClick?: (feature: Feature) => void,
) { ) {
this._layername = layername this._layername = layername
this._map = map this._map = map
@ -254,7 +271,7 @@ class LineRenderingLayer {
private async addSymbolLayer( private async addSymbolLayer(
sourceId: string, sourceId: string,
imageAlongWay: { if?: TagsFilter; then: string }[] imageAlongWay: { if?: TagsFilter; then: string }[],
) { ) {
const map = this._map const map = this._map
await Promise.allSettled( await Promise.allSettled(
@ -284,7 +301,7 @@ class LineRenderingLayer {
spec.filter = filter spec.filter = filter
} }
map.addLayer(spec) map.addLayer(spec)
}) }),
) )
} }
@ -294,7 +311,7 @@ class LineRenderingLayer {
* @private * @private
*/ */
private calculatePropsFor( private calculatePropsFor(
properties: Record<string, string> properties: Record<string, string>,
): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> { ): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> {
const config = this._config const config = this._config
@ -376,7 +393,7 @@ class LineRenderingLayer {
} catch (e) { } catch (e) {
console.error( console.error(
`Invalid dasharray in layer ${this._layername}:`, `Invalid dasharray in layer ${this._layername}:`,
this._config.dashArray this._config.dashArray,
) )
} }
} }
@ -393,15 +410,17 @@ class LineRenderingLayer {
} }
map.setFeatureState( map.setFeatureState(
{ source: this._layername, id: feature.properties.id }, { source: this._layername, id: feature.properties.id },
this.calculatePropsFor(feature.properties) this.calculatePropsFor(feature.properties),
) )
} }
map.on("click", linelayer, (e) => { if(this._onClick){
// line-layer-listener map.on("click", linelayer, (e) => {
e.originalEvent["consumed"] = true // line-layer-listener
this._onClick(e.features[0]) e.originalEvent["consumed"] = true
}) this._onClick(e.features[0])
})
}
const polylayer = this._layername + "_polygon" const polylayer = this._layername + "_polygon"
map.addLayer({ map.addLayer({
@ -436,7 +455,7 @@ class LineRenderingLayer {
"Error while setting visibility of layers ", "Error while setting visibility of layers ",
linelayer, linelayer,
polylayer, polylayer,
e e,
) )
} }
}) })
@ -457,7 +476,7 @@ class LineRenderingLayer {
console.trace( console.trace(
"Got a feature without ID; this causes rendering bugs:", "Got a feature without ID; this causes rendering bugs:",
feature, feature,
"from" "from",
) )
LineRenderingLayer.missingIdTriggered = true LineRenderingLayer.missingIdTriggered = true
} }
@ -469,7 +488,7 @@ class LineRenderingLayer {
if (this._fetchStore === undefined) { if (this._fetchStore === undefined) {
map.setFeatureState( map.setFeatureState(
{ source: this._layername, id }, { source: this._layername, id },
this.calculatePropsFor(feature.properties) this.calculatePropsFor(feature.properties),
) )
} else { } else {
const tags = this._fetchStore(id) const tags = this._fetchStore(id)
@ -486,7 +505,7 @@ class LineRenderingLayer {
} }
map.setFeatureState( map.setFeatureState(
{ source: this._layername, id }, { source: this._layername, id },
this.calculatePropsFor(properties) this.calculatePropsFor(properties),
) )
}) })
} }
@ -510,7 +529,7 @@ export default class ShowDataLayer {
layer: LayerConfig layer: LayerConfig
drawMarkers?: true | boolean drawMarkers?: true | boolean
drawLines?: true | boolean drawLines?: true | boolean
} },
) { ) {
this._options = options this._options = options
this.onDestroy.push(map.addCallbackAndRunD((map) => this.initDrawFeatures(map))) this.onDestroy.push(map.addCallbackAndRunD((map) => this.initDrawFeatures(map)))
@ -520,7 +539,7 @@ export default class ShowDataLayer {
mlmap: UIEventSource<MlMap>, mlmap: UIEventSource<MlMap>,
features: FeatureSource, features: FeatureSource,
layers: LayerConfig[], layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions> options?: Partial<ShowDataLayerOptions>,
) { ) {
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> = const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
new PerLayerFeatureSourceSplitter( new PerLayerFeatureSourceSplitter(
@ -528,7 +547,7 @@ export default class ShowDataLayer {
features, features,
{ {
constructStore: (features, layer) => new SimpleFeatureSource(layer, features), constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
} },
) )
if (options?.zoomToFeatures) { if (options?.zoomToFeatures) {
options.zoomToFeatures = false options.zoomToFeatures = false
@ -552,7 +571,7 @@ export default class ShowDataLayer {
public static showRange( public static showRange(
map: Store<MlMap>, map: Store<MlMap>,
features: FeatureSource, features: FeatureSource,
doShowLayer?: Store<boolean> doShowLayer?: Store<boolean>,
): ShowDataLayer { ): ShowDataLayer {
return new ShowDataLayer(map, { return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer, layer: ShowDataLayer.rangeLayer,
@ -561,7 +580,8 @@ export default class ShowDataLayer {
}) })
} }
public destruct() {} public destruct() {
}
private static zoomToCurrentFeatures(map: MlMap, features: Feature[]) { private static zoomToCurrentFeatures(map: MlMap, features: Feature[]) {
if (!features || !map || features.length == 0) { if (!features || !map || features.length == 0) {
@ -585,8 +605,8 @@ export default class ShowDataLayer {
this._options.layer.title === undefined this._options.layer.title === undefined
? undefined ? undefined
: (feature: Feature) => { : (feature: Feature) => {
selectedElement?.setData(feature) selectedElement?.setData(feature)
} }
} }
if (this._options.drawLines !== false) { if (this._options.drawLines !== false) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) { for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
@ -598,7 +618,7 @@ export default class ShowDataLayer {
lineRenderingConfig, lineRenderingConfig,
doShowLayer, doShowLayer,
fetchStore, fetchStore,
onClick onClick,
) )
this.onDestroy.push(l.destruct) this.onDestroy.push(l.destruct)
} }
@ -614,13 +634,13 @@ export default class ShowDataLayer {
doShowLayer, doShowLayer,
fetchStore, fetchStore,
onClick, onClick,
selectedElement selectedElement,
) )
} }
} }
if (this._options.zoomToFeatures) { if (this._options.zoomToFeatures) {
features.features.addCallbackAndRunD((features) => features.features.addCallbackAndRunD((features) =>
ShowDataLayer.zoomToCurrentFeatures(map, features) ShowDataLayer.zoomToCurrentFeatures(map, features),
) )
} }
} }

View file

@ -17,6 +17,7 @@
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let id: WayId export let id: WayId
const t = Translations.t.split const t = Translations.t.split
let snapTolerance = 5 // meter
let step: let step:
| "initial" | "initial"
| "loading_way" | "loading_way"
@ -60,7 +61,7 @@
{ {
theme: state?.theme?.id, theme: state?.theme?.id,
}, },
5 snapTolerance
) )
await state.changes?.applyAction(splitAction) await state.changes?.applyAction(splitAction)
// We throw away the old map and splitpoints, and create a new map from scratch // We throw away the old map and splitpoints, and create a new map from scratch
@ -87,7 +88,7 @@
{:else if step === "splitting"} {:else if step === "splitting"}
<div class="interactive border-interactive flex flex-col p-2"> <div class="interactive border-interactive flex flex-col p-2">
<div class="h-80 w-full"> <div class="h-80 w-full">
<WaySplitMap {state} {splitPoints} {osmWay} /> <WaySplitMap {state} {splitPoints} {osmWay} {snapTolerance} mapProperties={{rasterLayer: state.mapProperties.rasterLayer}}/>
</div> </div>
<div class="flex w-full flex-wrap-reverse md:flex-nowrap"> <div class="flex w-full flex-wrap-reverse md:flex-nowrap">
<BackButton <BackButton