UX: small maps now follow the rotation of the main map, fix #2433, add compass indicators to most of the minimaps

This commit is contained in:
Pieter Vander Vennet 2025-08-26 02:48:08 +02:00
parent aed6defa16
commit 376ed60e6e
15 changed files with 71 additions and 33 deletions

View file

@ -87,6 +87,8 @@ export class Stores {
}) })
return newStore return newStore
} }
} }
export abstract class Store<T> implements Readable<T> { export abstract class Store<T> implements Readable<T> {
@ -347,6 +349,16 @@ export abstract class Store<T> implements Readable<T> {
} }
}) })
} }
/**
* Create a new UIEVentSource. Whenever 'this.data' changes, the returned UIEventSource will get this value as well.
* However, this value can be overridden without affecting source
*/
public followingClone(): UIEventSource<T> {
const src = new UIEventSource(this.data)
this.addCallback((t) => src.setData(t))
return src
}
} }
export class ImmutableStore<T> extends Store<T> { export class ImmutableStore<T> extends Store<T> {
@ -814,17 +826,6 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
(b) => JSON.stringify(b) ?? "" (b) => JSON.stringify(b) ?? ""
) )
} }
/**
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
* However, this value can be overriden without affecting source
*/
static feedFrom<T>(store: Store<T>): UIEventSource<T> {
const src = new UIEventSource(store.data)
store.addCallback((t) => src.setData(t))
return src
}
/** /**
* Adds a callback * Adds a callback
* *

View file

@ -18,6 +18,9 @@ export interface MapProperties {
readonly maxbounds: UIEventSource<undefined | BBox> readonly maxbounds: UIEventSource<undefined | BBox>
readonly allowMoving: UIEventSource<true | boolean> readonly allowMoving: UIEventSource<true | boolean>
readonly allowRotating: UIEventSource<true | boolean> readonly allowRotating: UIEventSource<true | boolean>
/**
* Current rotation of the map, ccw in degrees
*/
readonly rotation: UIEventSource<number> readonly rotation: UIEventSource<number>
readonly pitch: UIEventSource<number> readonly pitch: UIEventSource<number>
readonly lastClickLocation: Store<{ readonly lastClickLocation: Store<{

View file

@ -22,7 +22,7 @@
let compassLoaded = Orientation.singleton.gotMeasurement let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating let allowRotation = mapProperties.allowRotating
function clicked(e: Event) { function clicked() {
if (mapProperties.rotation.data === 0) { if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) { if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data) mapProperties.rotation.set(orientation.data)
@ -35,9 +35,8 @@
{#if $allowRotation || $gotNonZero} {#if $allowRotation || $gotNonZero}
<button <button
style="z-index: -1"
class={"as-link pointer-events-auto relative " + size} class={"as-link pointer-events-auto relative " + size}
on:click={(e) => clicked(e)} on:click={() => clicked()}
> >
{#if $allowRotation && !$compassLoaded && !$gotNonZero} {#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div <div

View file

@ -76,7 +76,10 @@
allowMoving: new UIEventSource<boolean>(true), allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true), allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18), minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer), rasterLayer: state.mapProperties.rasterLayer.followingClone(),
allowRotating: state.mapProperties.allowRotating,
rotation: state.mapProperties.rotation.followingClone()
} }
state?.showCurrentLocationOn(map) state?.showCurrentLocationOn(map)

View file

@ -25,6 +25,7 @@
import type { Feature, LineString, Point } from "geojson" import type { Feature, LineString, Point } from "geojson"
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte" import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
import CompassWidget from "./CompassWidget.svelte"
const splitpoint_style = new LayerConfig( const splitpoint_style = new LayerConfig(
<LayerConfigJson>split_point, <LayerConfigJson>split_point,
@ -51,7 +52,7 @@
/** /**
* Optional: use these properties to set e.g. background layer * Optional: use these properties to set e.g. background layer
*/ */
export let mapProperties: undefined | Partial<MapProperties> = undefined export let mapProperties: undefined | Partial<Omit<MapProperties, "lastClickLocation">> = undefined
/** /**
* Reuse a point if the clicked location is within this amount of meter * Reuse a point if the clicked location is within this amount of meter
@ -156,4 +157,7 @@
<div class="relative h-full w-full"> <div class="relative h-full w-full">
<MaplibreMap {map} mapProperties={adaptor} /> <MaplibreMap {map} mapProperties={adaptor} />
<SmallZoomButtons {adaptor} /> <SmallZoomButtons {adaptor} />
<div class="absolute top-0 left-0">
<CompassWidget mapProperties={adaptor} />
</div>
</div> </div>

View file

@ -51,10 +51,12 @@
state?.mapProperties.rasterLayer.addCallbackD((layer) => rasterLayer.set(layer)) state?.mapProperties.rasterLayer.addCallbackD((layer) => rasterLayer.set(layer))
} }
} }
let mapProperties: Partial<MapProperties> = { let mapProperties: Partial<MapProperties> & { location } = {
rasterLayer: rasterLayer, rasterLayer: rasterLayer,
location: mapLocation, location: mapLocation,
zoom: new UIEventSource(args?.zoom ?? 18), zoom: new UIEventSource(args?.zoom ?? 18),
rotation: state.mapProperties.rotation.followingClone(),
lastClickLocation: new UIEventSource(undefined)
} }
let start: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined) let start: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
@ -98,6 +100,13 @@
layer: new LayerConfig(conflation), layer: new LayerConfig(conflation),
features: new StaticFeatureSource(lengthFeature), features: new StaticFeatureSource(lengthFeature),
}) })
mapProperties.lastClickLocation.addCallbackAndRunD(lonlat => {
if (start.data === undefined) {
start.set(lonlat)
}
mapProperties.location.set(lonlat)
}, onDestroy)
const t = Translations.t.input_helpers.distance const t = Translations.t.input_helpers.distance
</script> </script>

View file

@ -14,6 +14,7 @@
import { createEventDispatcher, onDestroy } from "svelte" import { createEventDispatcher, onDestroy } from "svelte"
import Move_arrows from "../../../assets/svg/Move_arrows.svelte" import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte" import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
import CompassWidget from "../../BigComponents/CompassWidget.svelte"
/** /**
* A visualisation to pick a location on a map background * A visualisation to pick a location on a map background
@ -92,7 +93,6 @@
<div class="relative h-full min-h-32 cursor-pointer overflow-hidden"> <div class="relative h-full min-h-32 cursor-pointer overflow-hidden">
<div class="absolute left-0 top-0 h-full w-full cursor-pointer"> <div class="absolute left-0 top-0 h-full w-full cursor-pointer">
<MaplibreMap <MaplibreMap
center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }}
{map} {map}
mapProperties={mla} mapProperties={mla}
/> />
@ -108,4 +108,8 @@
<DragInvitation hideSignal={mla.location} /> <DragInvitation hideSignal={mla.location} />
<SmallZoomButtons adaptor={mla} /> <SmallZoomButtons adaptor={mla} />
<div class="absolute top-0 left-0 ">
<CompassWidget mapProperties={mla} />
</div>
</div> </div>

View file

@ -70,7 +70,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
constructor( constructor(
maplibreMap: Store<MLMap>, maplibreMap: Store<MLMap>,
state?: Partial<MapProperties>, state?: Partial<MapProperties & {
lastClickLocation: UIEventSource<{
lon: number
lat: number
mode: "left" | "right" | "middle"
nearestFeature?: Feature
}>
}>,
options?: { options?: {
correctClick?: number correctClick?: number
} }
@ -114,7 +121,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
this.showScale = state?.showScale ?? new UIEventSource<boolean>(false) this.showScale = state?.showScale ?? new UIEventSource<boolean>(false)
const lastClickLocation = new UIEventSource<{ const lastClickLocation = state?.lastClickLocation ?? new UIEventSource<{
lat: number lat: number
lon: number lon: number
mode: "left" | "right" | "middle" mode: "left" | "right" | "middle"
@ -156,7 +163,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const way = <Feature<LineString>>feature const way = <Feature<LineString>>feature
const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat] const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat]
const p = GeoOperations.nearestPoint(way, lngLat) const p = GeoOperations.nearestPoint(way, lngLat)
console.log(">>>", p, way, lngLat)
if (!p) { if (!p) {
continue continue
} }
@ -182,7 +188,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.setMinzoom(this.minzoom.data) this.setMinzoom(this.minzoom.data)
this.setMaxzoom(this.maxzoom.data) this.setMaxzoom(this.maxzoom.data)
this.setBounds(this.bounds.data) this.setBounds(this.bounds.data)
this.setRotation(this.rotation.data) this.setRotation(Math.floor(this.rotation.data))
this.setScale(this.showScale.data) this.setScale(this.showScale.data)
this.updateStores(true) this.updateStores(true)
} }
@ -562,6 +568,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!map || bearing === undefined) { if (!map || bearing === undefined) {
return return
} }
if (Math.abs(map.getBearing() - bearing) < 0.1) {
// We don't bother to actually rotate
// Aborting small changes helps to dampen changes
return
}
map.rotateTo(bearing, { duration: 500 }) map.rotateTo(bearing, { duration: 500 })
} }
@ -769,8 +780,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
public installQuicklocation(ratelimitMs = 50) { public installQuicklocation(ratelimitMs = 50) {
this._maplibreMap.addCallbackAndRunD((map) => { this._maplibreMap.addCallbackAndRunD((map) => {
let lastUpdate = new Date().getTime() let lastUpdate = new Date().getTime()
map.on("drag", (e) => { map.on("drag", () => {
let now = new Date().getTime() const now = new Date().getTime()
if (now - lastUpdate < ratelimitMs) { if (now - lastUpdate < ratelimitMs) {
return return
} }

View file

@ -56,6 +56,7 @@
maxZoom: 24, maxZoom: 24,
interactive: true, interactive: true,
attributionControl: false, attributionControl: false,
bearing: mapProperties?.rotation?.data ?? 0
} }
_map = new maplibre.Map(options) _map = new maplibre.Map(options)
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {

View file

@ -24,9 +24,9 @@
let altmap: UIEventSource<MlMap> = new UIEventSource(undefined) let altmap: UIEventSource<MlMap> = new UIEventSource(undefined)
let altproperties = new MapLibreAdaptor(altmap, { let altproperties = new MapLibreAdaptor(altmap, {
rasterLayer, rasterLayer,
zoom: UIEventSource.feedFrom(placedOverMapProperties.zoom), zoom: placedOverMapProperties.zoom.followingClone(),
rotation: UIEventSource.feedFrom(placedOverMapProperties.rotation), rotation: placedOverMapProperties.rotation.followingClone(),
pitch: UIEventSource.feedFrom(placedOverMapProperties.pitch), pitch: placedOverMapProperties.pitch.followingClone()
}) })
altproperties.allowMoving.setData(false) altproperties.allowMoving.setData(false)
altproperties.allowZooming.setData(false) altproperties.allowZooming.setData(false)

View file

@ -45,7 +45,7 @@
) )
} }
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer) let rasterLayerOnMap = rasterLayer.followingClone()
if (shown) { if (shown) {
onDestroy( onDestroy(

View file

@ -21,8 +21,8 @@
const map = new UIEventSource<MlMap>(undefined) const map = new UIEventSource<MlMap>(undefined)
const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature) const [lon, lat] = GeoOperations.centerpointCoordinates(importFlow.originalFeature)
const mla = new MapLibreAdaptor(map, { const mla = new MapLibreAdaptor(map, {
allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting), allowMoving: state.featureSwitchIsTesting.followingClone(),
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting), allowZooming: state.featureSwitchIsTesting.followingClone(),
rasterLayer: state.mapProperties.rasterLayer, rasterLayer: state.mapProperties.rasterLayer,
location: new UIEventSource<{ lon: number; lat: number }>({ lon, lat }), location: new UIEventSource<{ lon: number; lat: number }>({ lon, lat }),
zoom: new UIEventSource<number>(18), zoom: new UIEventSource<number>(18),

View file

@ -60,6 +60,8 @@
rasterLayer: state.mapProperties.rasterLayer, rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource<number>(17), zoom: new UIEventSource<number>(17),
maxzoom: new UIEventSource<number>(17), maxzoom: new UIEventSource<number>(17),
rotation: state.mapProperties.rotation.followingClone(),
allowRotating: state.mapProperties.allowRotating.followingClone()
}) })
mla.allowMoving.setData(false) mla.allowMoving.setData(false)

View file

@ -47,13 +47,14 @@
function initMapProperties(reason: MoveReason): Partial<MapProperties> & { location } { function initMapProperties(reason: MoveReason): Partial<MapProperties> & { location } {
return { return {
allowMoving: new UIEventSource(true), allowMoving: new UIEventSource(true),
allowRotating: new UIEventSource(false), allowRotating: state.mapProperties.allowRotating.followingClone(),
allowZooming: new UIEventSource(true), allowZooming: new UIEventSource(true),
bounds: new UIEventSource(undefined), bounds: new UIEventSource(undefined),
location: new UIEventSource({ lon, lat }), location: new UIEventSource({ lon, lat }),
minzoom: new UIEventSource(reason.minZoom), minzoom: new UIEventSource(reason.minZoom),
rasterLayer: state.mapProperties.rasterLayer, rasterLayer: state.mapProperties.rasterLayer,
zoom: new UIEventSource(reason?.startZoom ?? 16), zoom: new UIEventSource(reason?.startZoom ?? 16),
rotation: state.mapProperties.rotation.followingClone()
} }
} }
@ -72,7 +73,7 @@
let isSearching = new UIEventSource<boolean>(false) let isSearching = new UIEventSource<boolean>(false)
let zoomedInEnough = currentMapProperties let zoomedInEnough = currentMapProperties
.bindD((properties) => properties.zoom, onDestroy) .bindD((properties) => properties.zoom, onDestroy)
.mapD((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint, onDestroy) .mapD((zoom: number) => zoom >= Constants.minZoomLevelToAddNewPoint, onDestroy)
const searcher = new NominatimGeocoding(1) const searcher = new NominatimGeocoding(1)
async function searchPressed() { async function searchPressed() {

View file

@ -94,7 +94,7 @@
{splitPoints} {splitPoints}
{osmWay} {osmWay}
{snapTolerance} {snapTolerance}
mapProperties={{ rasterLayer: state.mapProperties.rasterLayer }} mapProperties={{ rasterLayer: state.mapProperties.rasterLayer, rotation: state.mapProperties.rotation.followingClone() }}
/> />
</div> </div>
<div class="flex w-full flex-wrap-reverse md:flex-nowrap"> <div class="flex w-full flex-wrap-reverse md:flex-nowrap">