mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
4c0583845a
Selecting a place no longer spiderfies a city-sized cluster: the reveal only fans open bubbles of up to 12 stops, or stacks whose stops all share one coordinate, so a zoomed-out trip with hundreds of places stays usable (#2990). The selected pin now carries a white border inside a pulsing blue ring on both renderers (static under reduced motion), and the map measures the place card instead of assuming a fixed 320px, so the selection pans the pin above even a tall card on portrait tablets and eases it back out when the card grows over it later (#2991). Closes #2990 Closes #2991
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
import L from 'leaflet'
|
|
import { STACK_RADIUS_PX } from './coincidentPlaces'
|
|
import { clusterBubbleSize } from './placeClusterLayers'
|
|
|
|
/** Up to here the map is an overview, where stops merge to keep the world readable. */
|
|
export const CLUSTER_UNTIL_ZOOM = 9
|
|
/** The overview radius, in pixels at the current zoom. */
|
|
export const CLUSTER_RADIUS_PX = 20
|
|
|
|
/**
|
|
* The cluster group the planner and the public share page both draw places with,
|
|
* so a shared link clusters a trip the way the planner does (#2343).
|
|
*/
|
|
export const CLUSTER_OPTIONS = {
|
|
chunkedLoading: true,
|
|
chunkInterval: 30,
|
|
chunkDelay: 0,
|
|
/**
|
|
* Pixels at the current zoom, so ordinary stops still come apart by zooming in.
|
|
* From the day zooms upwards only the width of a pin counts: stops sharing one
|
|
* coordinate cannot be separated by any zoom, and a cluster is the only thing
|
|
* that can fan them apart again. There is deliberately no `disableClusteringAtZoom`
|
|
* here — it redefines the group's own maximum zoom, which is what the spiderfy
|
|
* branch is measured against.
|
|
*
|
|
* A whole pin's third is affordable precisely because the fold is reversible here:
|
|
* what comes out of it is a counted bubble the user can click open. The GL renderer
|
|
* has no fan to offer and folds far tighter — see `COINCIDENT_RADIUS_PX`.
|
|
*/
|
|
maxClusterRadius: (zoom: number) => (zoom < CLUSTER_UNTIL_ZOOM ? CLUSTER_RADIUS_PX : STACK_RADIUS_PX),
|
|
spiderfyOnMaxZoom: true,
|
|
// The default legs put four 36px pins barely a pin apart and six of them back on
|
|
// top of each other, which is the stack the fan is there to undo.
|
|
spiderfyDistanceMultiplier: 1.6,
|
|
showCoverageOnHover: false,
|
|
zoomToBoundsOnClick: true,
|
|
animate: false,
|
|
}
|
|
|
|
/** The part of a cluster group that `revealInCluster` reads. */
|
|
export interface ClusterGroupLike {
|
|
getVisibleParent?: (marker: unknown) => unknown
|
|
}
|
|
|
|
/** The part of a cluster bubble that `revealInCluster` reads. */
|
|
interface RevealableCluster {
|
|
spiderfy: () => void
|
|
getChildCount: () => number
|
|
getBounds?: () => { getNorthEast: () => L.LatLng; getSouthWest: () => L.LatLng }
|
|
}
|
|
|
|
/**
|
|
* The most stops a selection fans open around itself. Up to here the fan stays a small
|
|
* ring around the bubble; a city-sized bubble from the overview would throw hundreds of
|
|
* legs across the whole map instead (#2990).
|
|
*/
|
|
export const REVEAL_FAN_MAX_CHILDREN = 12
|
|
|
|
/**
|
|
* Whether opening this bubble shows the selection or buries the map.
|
|
*
|
|
* A small bubble always opens. A bigger one only when every stop in it sits on the same
|
|
* coordinate, the stack no zoom can take apart, which is the case the fan is there for.
|
|
* Anything else is a pile of ordinary neighbours that zooming in separates, so it stays
|
|
* closed and the selection is shown by the pan alone.
|
|
*/
|
|
function fansOpenOnSelect(cluster: RevealableCluster): boolean {
|
|
if (cluster.getChildCount() <= REVEAL_FAN_MAX_CHILDREN) return true
|
|
const bounds = cluster.getBounds?.()
|
|
return !!bounds && bounds.getNorthEast().equals(bounds.getSouthWest())
|
|
}
|
|
|
|
function isRevealableCluster(layer: unknown): layer is RevealableCluster {
|
|
const candidate = layer as Partial<RevealableCluster> | null
|
|
return !!candidate && typeof candidate.spiderfy === 'function' && typeof candidate.getChildCount === 'function'
|
|
}
|
|
|
|
/**
|
|
* Fan open the bubble a stop is hidden inside, if it is in one and small enough.
|
|
*
|
|
* Picking a stop from the places rail used to raise its pin out of the stack with a
|
|
* z-index. A stop sharing its coordinates now sits in a cluster instead and has no pin
|
|
* of its own to raise, so the bubble has to open for the selection to be visible at all.
|
|
* Deliberately not `zoomToShowLayer`: the zoom belongs to the day fit, and the selection
|
|
* only ever pans. See `fansOpenOnSelect` for which bubbles are opened.
|
|
*/
|
|
export function revealInCluster(group: ClusterGroupLike | null | undefined, marker: unknown): boolean {
|
|
if (!group || !marker || typeof group.getVisibleParent !== 'function') return false
|
|
const parent = group.getVisibleParent(marker)
|
|
if (parent === marker || !isRevealableCluster(parent) || !fansOpenOnSelect(parent)) return false
|
|
parent.spiderfy()
|
|
return true
|
|
}
|
|
|
|
/** The part of a Leaflet cluster this factory reads. */
|
|
export interface ClusterLike {
|
|
getChildCount: () => number
|
|
}
|
|
|
|
/** A count bubble sized by its count, styled by `.marker-cluster-custom` in index.css. */
|
|
export function createClusterIcon(cluster: ClusterLike) {
|
|
const count = cluster.getChildCount()
|
|
const size = clusterBubbleSize(count)
|
|
return L.divIcon({
|
|
html: `<div class="marker-cluster-custom" style="width:${size}px;height:${size}px;"><span>${count}</span></div>`,
|
|
className: 'marker-cluster-wrapper',
|
|
iconSize: L.point(size, size),
|
|
})
|
|
}
|