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