feat(roadtrip): share trip preferences and improve service stops

This commit is contained in:
Maurice
2026-09-12 00:24:45 +02:00
parent dd6ad31ab9
commit b773faff70
72 changed files with 1136 additions and 375 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ export default function ClusteredPois({ pois, enabled, onPoiClick, children }: {
map.on('zoomend', update)
return () => { map.off('zoomend', update) }
}, [map])
const groups = useMemo(() => enabled ? clusterPois(pois, poi => map.latLngToContainerPoint([poi.lat, poi.lng])) : [], [pois, enabled, map, zoom])
const groups = useMemo(() => enabled ? clusterPois(pois, poi => map.latLngToContainerPoint([poi.lat, poi.lng]), zoom) : [], [pois, enabled, map, zoom])
const markers = new Map(pois.map((poi, index) => [poi.osm_id, children[index]]))
if (!enabled) return <>{children}</>
return <>{groups.map(group => {
@@ -1391,6 +1391,7 @@ describe('MapViewGL', () => {
it('clusters roadtrip stations and zooms before offering overlapping stations individually', async () => {
loadOnAttach()
vi.mocked(glMap.getZoom).mockReturnValue(8)
const poi: Poi = {
osm_id: 'n1', name: 'Station A', lat: 48.854, lng: 2.332, category: 'charging_station',
poi_type: 'charging_station', address: null, website: null, phone: null,
@@ -1411,6 +1412,7 @@ describe('MapViewGL', () => {
const list = calls[calls.length - 1][0]
act(() => { list.querySelectorAll('button')[1].click() })
expect(onPoiClick).toHaveBeenCalledWith(other)
vi.mocked(glMap.getZoom).mockReturnValue(10)
})
it('FE-COMP-MAPVIEWGL-043: plugin markers render as tone dots with a text-only popup', async () => {
+1 -1
View File
@@ -1840,7 +1840,7 @@ export function MapViewGL({
poiMarkersRef.current = []
poiCleanupRef.current.forEach(off => off())
poiCleanupRef.current = []
const groups = clusterLoosely ? clusterPois(pois, poi => map.project([poi.lng, poi.lat])) : pois.map(poi => ({ pois: [poi], lat: poi.lat, lng: poi.lng }))
const groups = clusterLoosely ? clusterPois(pois, poi => map.project([poi.lng, poi.lat]), map.getZoom()) : pois.map(poi => ({ pois: [poi], lat: poi.lat, lng: poi.lng }))
for (const group of groups) {
if (group.pois.length > 1) {
const el = document.createElement('button')
@@ -10,6 +10,12 @@ const station = (id: string, lng: number): Poi => ({
const project = (poi: Poi) => ({ x: poi.lng, y: poi.lat })
describe('station clusters', () => {
it('only clusters in the distant overview, even for coincident stations', () => {
const pois = [station('a', 1), station('b', 1)]
expect(clusterPois(pois, project, 8)).toHaveLength(1)
expect(clusterPois(pois, project, 9)).toHaveLength(2)
expect(clusterPois(pois, project, 14)).toHaveLength(2)
})
it('groups across grid edges, counts unique stations and preserves the originals', () => {
const a = station('a', 47), b = station('b', 49), c = station('c', 150)
const groups = clusterPois([c, b, a, a], project)
@@ -41,3 +47,9 @@ describe('station clusters', () => {
expect(select).toHaveBeenCalledWith(poi)
})
})
it('combines nearby small groups into one overview cluster', () => {
const pois = [station('a', 0), station('b', 3), station('c', 20), station('d', 23), station('e', 40), station('f', 43)]
expect(clusterPois(pois, project, 8).map(group => group.pois.length)).toEqual([6])
expect(clusterPois(pois, project, 9)).toHaveLength(6)
})
+3 -1
View File
@@ -1,13 +1,15 @@
import type { Poi } from './poiCategories'
export const POI_CLUSTER_RADIUS = 48
export const POI_CLUSTER_MAX_ZOOM = 8
export const POI_CLUSTER_DETAIL_ZOOM = 18
export interface PoiCluster { pois: Poi[]; lat: number; lng: number }
export function clusterPois(pois: Poi[], project: (poi: Poi) => { x: number; y: number }): PoiCluster[] {
export function clusterPois(pois: Poi[], project: (poi: Poi) => { x: number; y: number }, zoom = 0): PoiCluster[] {
const groups: (PoiCluster & { x: number; y: number })[] = []
const cells = new Map<string, number[]>()
const unique = new Map(pois.map(poi => [poi.osm_id, poi]))
if (zoom > POI_CLUSTER_MAX_ZOOM) return [...unique.values()].map(poi => ({ pois: [poi], lat: poi.lat, lng: poi.lng }))
for (const poi of [...unique.values()].sort((a, b) => a.osm_id.localeCompare(b.osm_id))) {
const point = project(poi)
const cx = Math.floor(point.x / POI_CLUSTER_RADIUS), cy = Math.floor(point.y / POI_CLUSTER_RADIUS)