forked from MapComplete/MapComplete
UX: when clicking on the map, all (way) features within 20px are inspected and the closest one is inspected. Fixes #2261
This commit is contained in:
parent
c34300fae1
commit
8680fce4e7
5 changed files with 76 additions and 22 deletions
|
@ -1,6 +1,7 @@
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
import { RasterLayerPolygon } from "./RasterLayers"
|
import { RasterLayerPolygon } from "./RasterLayers"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export interface KeyNavigationEvent {
|
export interface KeyNavigationEvent {
|
||||||
date: Date
|
date: Date
|
||||||
|
@ -19,7 +20,10 @@ export interface MapProperties {
|
||||||
readonly allowRotating: UIEventSource<true | boolean>
|
readonly allowRotating: UIEventSource<true | boolean>
|
||||||
readonly rotation: UIEventSource<number>
|
readonly rotation: UIEventSource<number>
|
||||||
readonly pitch: UIEventSource<number>
|
readonly pitch: UIEventSource<number>
|
||||||
readonly lastClickLocation: Store<{ lon: number; lat: number }>
|
readonly lastClickLocation: Store<{ lon: number; lat: number ; /**
|
||||||
|
* The nearest feature from a MapComplete layer
|
||||||
|
*/
|
||||||
|
nearestFeature?: Feature}>
|
||||||
readonly allowZooming: UIEventSource<true | boolean>
|
readonly allowZooming: UIEventSource<true | boolean>
|
||||||
readonly useTerrain: Store<boolean>
|
readonly useTerrain: Store<boolean>
|
||||||
readonly showScale: UIEventSource<boolean>
|
readonly showScale: UIEventSource<boolean>
|
||||||
|
|
|
@ -178,7 +178,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.map = new UIEventSource<MlMap>(undefined)
|
this.map = new UIEventSource<MlMap>(undefined)
|
||||||
const geolocationState = new GeoLocationState()
|
const geolocationState = new GeoLocationState()
|
||||||
const initial = new InitialMapPositioning(layout, geolocationState)
|
const initial = new InitialMapPositioning(layout, geolocationState)
|
||||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
this.mapProperties = new MapLibreAdaptor(this.map, initial, {correctClick: 20})
|
||||||
|
|
||||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||||
|
@ -554,6 +554,16 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setSelectedElement(feature: Feature){
|
||||||
|
const current = this.selectedElement.data
|
||||||
|
if(current?.properties?.id !== undefined && current.properties.id === feature.properties.id ){
|
||||||
|
console.log("Not setting selected, same id", current, feature)
|
||||||
|
return // already set
|
||||||
|
}
|
||||||
|
// this.selectedElement.setData(undefined)
|
||||||
|
this.selectedElement.setData(feature)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the feature that is 'i' closest to the map center
|
* Selects the feature that is 'i' closest to the map center
|
||||||
*/
|
*/
|
||||||
|
@ -570,13 +580,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
if (!toSelect) {
|
if (!toSelect) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.selectedElement.setData(undefined)
|
this.setSelectedElement(toSelect)
|
||||||
this.selectedElement.setData(toSelect)
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.selectedElement.setData(undefined)
|
this.setSelectedElement(toSelect)
|
||||||
this.selectedElement.setData(toSelect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initHotkeys() {
|
private initHotkeys() {
|
||||||
|
@ -992,6 +1000,15 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.userRelatedState.recentlyVisitedSearch.add(r)
|
this.userRelatedState.recentlyVisitedSearch.add(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.mapProperties.lastClickLocation.addCallbackD(lastClick => {
|
||||||
|
if(lastClick.mode !== "left" || !lastClick.nearestFeature){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const f = lastClick.nearestFeature
|
||||||
|
this.setSelectedElement(f)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
||||||
this.mapProperties.showScale.set(showScale)
|
this.mapProperties.showScale.set(showScale)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import maplibregl, {
|
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
|
||||||
Map as MLMap,
|
|
||||||
Map as MlMap,
|
|
||||||
ScaleControl,
|
|
||||||
SourceSpecification,
|
|
||||||
} from "maplibre-gl"
|
|
||||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
|
@ -16,6 +11,8 @@ import * as htmltoimage from "html-to-image"
|
||||||
import RasterLayerHandler from "./RasterLayerHandler"
|
import RasterLayerHandler from "./RasterLayerHandler"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { Protocol } from "pmtiles"
|
import { Protocol } from "pmtiles"
|
||||||
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import { Feature, LineString } from "geojson"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||||
|
@ -46,7 +43,10 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
readonly allowRotating: UIEventSource<true | boolean | undefined>
|
readonly allowRotating: UIEventSource<true | boolean | undefined>
|
||||||
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
||||||
readonly lastClickLocation: Store<
|
readonly lastClickLocation: Store<
|
||||||
undefined | { lon: number; lat: number; mode: "left" | "right" | "middle" }
|
undefined | { lon: number; lat: number; mode: "left" | "right" | "middle" , /**
|
||||||
|
* The nearest feature from a MapComplete layer
|
||||||
|
*/
|
||||||
|
nearestFeature?: Feature }
|
||||||
>
|
>
|
||||||
readonly minzoom: UIEventSource<number>
|
readonly minzoom: UIEventSource<number>
|
||||||
readonly maxzoom: UIEventSource<number>
|
readonly maxzoom: UIEventSource<number>
|
||||||
|
@ -64,7 +64,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private readonly _maplibreMap: Store<MLMap>
|
private readonly _maplibreMap: Store<MLMap>
|
||||||
|
|
||||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>, options?:{
|
||||||
|
correctClick?: number
|
||||||
|
}) {
|
||||||
if (!MapLibreAdaptor.pmtilesInited) {
|
if (!MapLibreAdaptor.pmtilesInited) {
|
||||||
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
||||||
MapLibreAdaptor.pmtilesInited = true
|
MapLibreAdaptor.pmtilesInited = true
|
||||||
|
@ -104,7 +106,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
const lastClickLocation = new UIEventSource<{
|
const lastClickLocation = new UIEventSource<{
|
||||||
lat: number
|
lat: number
|
||||||
lon: number
|
lon: number
|
||||||
mode: "left" | "right" | "middle"
|
mode: "left" | "right" | "middle",
|
||||||
|
nearestFeature?: Feature
|
||||||
}>(undefined)
|
}>(undefined)
|
||||||
this.lastClickLocation = lastClickLocation
|
this.lastClickLocation = lastClickLocation
|
||||||
const self = this
|
const self = this
|
||||||
|
@ -122,8 +125,40 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
const lat = e.lngLat.lat
|
const lat = e.lngLat.lat
|
||||||
const mouseEvent: MouseEvent = e.originalEvent
|
const mouseEvent: MouseEvent = e.originalEvent
|
||||||
mode = mode ?? clickmodes[mouseEvent.button]
|
mode = mode ?? clickmodes[mouseEvent.button]
|
||||||
|
let nearestFeature: Feature = undefined
|
||||||
|
if(options?.correctClick && maplibreMap.data){
|
||||||
|
const map = maplibreMap.data
|
||||||
|
const point = e.point
|
||||||
|
const buffer = options?.correctClick
|
||||||
|
const features = map.queryRenderedFeatures([
|
||||||
|
[point.x - buffer, point.y - buffer],
|
||||||
|
[point.x + buffer, point.y + buffer]
|
||||||
|
]).filter(f => f.source.startsWith("mapcomplete_"))
|
||||||
|
if(features.length === 1){
|
||||||
|
nearestFeature = features[0]
|
||||||
|
}else{
|
||||||
|
let nearestD: number = undefined
|
||||||
|
for (const feature of features) {
|
||||||
|
let d: number // in meter
|
||||||
|
if(feature.geometry.type === "LineString"){
|
||||||
|
const way = <Feature<LineString>> feature
|
||||||
|
const lngLat:[number,number] = [e.lngLat.lng, e.lngLat.lat]
|
||||||
|
const p = GeoOperations.nearestPoint(way, lngLat)
|
||||||
|
console.log(">>>",p, way, lngLat)
|
||||||
|
if(!p){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d = p.properties.dist * 1000
|
||||||
|
if(nearestFeature === undefined || d < nearestD){
|
||||||
|
nearestFeature = way
|
||||||
|
nearestD = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastClickLocation.setData({ lon, lat, mode, nearestFeature })
|
||||||
|
|
||||||
lastClickLocation.setData({ lon, lat, mode })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maplibreMap.addCallbackAndRunD((map) => {
|
maplibreMap.addCallbackAndRunD((map) => {
|
||||||
|
|
|
@ -568,7 +568,6 @@ export default class ShowDataLayer {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
||||||
console.debug("Zooming to features", bbox.asGeoJson())
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
map.resize()
|
map.resize()
|
||||||
map.fitBounds(bbox.toLngLat(), {
|
map.fitBounds(bbox.toLngLat(), {
|
||||||
|
|
|
@ -54,7 +54,6 @@
|
||||||
|
|
||||||
let theme = state.theme
|
let theme = state.theme
|
||||||
let maplibremap: UIEventSource<MlMap> = state.map
|
let maplibremap: UIEventSource<MlMap> = state.map
|
||||||
let state_selectedElement = state.selectedElement
|
|
||||||
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
|
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
|
||||||
let compass = Orientation.singleton.alpha
|
let compass = Orientation.singleton.alpha
|
||||||
let compassLoaded = Orientation.singleton.gotMeasurement
|
let compassLoaded = Orientation.singleton.gotMeasurement
|
||||||
|
@ -99,7 +98,7 @@
|
||||||
|
|
||||||
state.mapProperties.installCustomKeyboardHandler(viewport)
|
state.mapProperties.installCustomKeyboardHandler(viewport)
|
||||||
|
|
||||||
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
|
let selectedLayer: Store<LayerConfig> = selectedElement.mapD((element) => {
|
||||||
if (element.properties.id.startsWith("current_view")) {
|
if (element.properties.id.startsWith("current_view")) {
|
||||||
return currentViewLayer
|
return currentViewLayer
|
||||||
}
|
}
|
||||||
|
@ -458,7 +457,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div slot="close-button" />
|
<div slot="close-button" />
|
||||||
<SelectedElementPanel {state} selected={$state_selectedElement} />
|
<SelectedElementPanel {state} selected={$selectedElement} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -472,7 +471,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span slot="close-button" />
|
<span slot="close-button" />
|
||||||
<SelectedElementPanel absolute={false} {state} selected={$state_selectedElement} />
|
<SelectedElementPanel absolute={false} {state} selected={$selectedElement} />
|
||||||
</FloatOver>
|
</FloatOver>
|
||||||
{:else}
|
{:else}
|
||||||
<FloatOver
|
<FloatOver
|
||||||
|
@ -483,7 +482,7 @@
|
||||||
<SelectedElementView
|
<SelectedElementView
|
||||||
{state}
|
{state}
|
||||||
layer={$selectedLayer}
|
layer={$selectedLayer}
|
||||||
selectedElement={$state_selectedElement}
|
selectedElement={$selectedElement}
|
||||||
/>
|
/>
|
||||||
</FloatOver>
|
</FloatOver>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue