mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(maps): search the view in road trip mode too
The category pill was switched off whenever road trip mode was on, which left the corridor search as the only way to find anything. The two answer different questions though — 'what is along this drive' against 'what is in view right now' — and looking for a hotel at tonight's stop is exactly the second one. Furkot shows accommodation at a stop for the same reason (disc #1797 thread). Both searches now draw together. A place found by both appears once, as the corridor knows it, because only that copy carries how far along the drive it sits, and the merged list keeps its reference while nothing changes so the map does not rebuild every pin on each render.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useMergedMapPois } from './useMergedMapPois'
|
||||
import type { Poi } from './poiCategories'
|
||||
|
||||
/**
|
||||
* FE-MAP-MERGEPOI-001..006 — the two searches that can run at once in road trip mode.
|
||||
*
|
||||
* The corridor answers "what is along the drive", the category pill answers "what is in
|
||||
* view". Both are drawn, the corridor's copy of a shared hit wins, and the reference is
|
||||
* only new when the content really is — the map rebuilds every pin off it.
|
||||
*/
|
||||
|
||||
const poi = (osm_id: string, name = osm_id): Poi => ({
|
||||
osm_id, name, lat: 48, lng: 2, category: 'hotel', source: 'openstreetmap',
|
||||
} as Poi)
|
||||
|
||||
describe('useMergedMapPois', () => {
|
||||
it('FE-MAP-MERGEPOI-001: outside road trip mode only the pill hits are drawn', () => {
|
||||
const explore = [poi('n1')]
|
||||
const { result } = renderHook(() => useMergedMapPois(null, explore))
|
||||
expect(result.current).toBe(explore)
|
||||
})
|
||||
|
||||
it('FE-MAP-MERGEPOI-002: with nothing found in view the corridor list passes through as-is', () => {
|
||||
const corridor = [poi('n1')]
|
||||
const { result } = renderHook(() => useMergedMapPois(corridor, []))
|
||||
expect(result.current).toBe(corridor)
|
||||
})
|
||||
|
||||
it('FE-MAP-MERGEPOI-003: before a corridor search the pill hits pass through as-is', () => {
|
||||
const explore = [poi('n2')]
|
||||
const { result } = renderHook(() => useMergedMapPois([], explore))
|
||||
expect(result.current).toBe(explore)
|
||||
})
|
||||
|
||||
it('FE-MAP-MERGEPOI-004: both searches draw together, corridor first', () => {
|
||||
const { result } = renderHook(() => useMergedMapPois([poi('n1')], [poi('n2')]))
|
||||
expect(result.current.map(p => p.osm_id)).toEqual(['n1', 'n2'])
|
||||
})
|
||||
|
||||
it('FE-MAP-MERGEPOI-005: a place both searches found is drawn once, as the corridor knows it', () => {
|
||||
// The corridor copy carries how far along the drive it sits, which the pill's does not.
|
||||
const corridor = [poi('n1', 'Hotel am Weg')]
|
||||
const { result } = renderHook(() => useMergedMapPois(corridor, [poi('n1', 'Hotel am Weg (Sicht)')]))
|
||||
expect(result.current).toBe(corridor)
|
||||
expect(result.current).toHaveLength(1)
|
||||
expect(result.current[0].name).toBe('Hotel am Weg')
|
||||
})
|
||||
|
||||
it('FE-MAP-MERGEPOI-006: an unchanged pair keeps its reference, so no pin is rebuilt', () => {
|
||||
const corridor = [poi('n1')]
|
||||
const explore = [poi('n2')]
|
||||
const { result, rerender } = renderHook(() => useMergedMapPois(corridor, explore))
|
||||
const first = result.current
|
||||
rerender()
|
||||
expect(result.current).toBe(first)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Poi } from './poiCategories'
|
||||
|
||||
/**
|
||||
* The findings the map should draw, when two searches can be running at once.
|
||||
*
|
||||
* In road trip mode the corridor search answers "what is along this drive". The category
|
||||
* pill answers a different question — "what is in view right now" — and a hotel at tonight's
|
||||
* stop is exactly the case where someone wants the second one without giving up the first,
|
||||
* so the two lists are drawn together rather than one replacing the other.
|
||||
*
|
||||
* Identity is `osm_id`, the same key the corridor search dedupes its own overlapping tiles
|
||||
* on: a hotel found both along the route and in the viewport is one pin, and the corridor's
|
||||
* copy wins because it carries how far along the drive it sits.
|
||||
*
|
||||
* Returns the input array itself whenever there is nothing to merge. The map redraws off
|
||||
* this reference, so handing it a fresh array on every render would rebuild every pin.
|
||||
*/
|
||||
export function useMergedMapPois(corridor: Poi[] | null, explore: Poi[]): Poi[] {
|
||||
return useMemo(() => {
|
||||
if (!corridor) return explore
|
||||
if (!explore.length) return corridor
|
||||
if (!corridor.length) return explore
|
||||
const seen = new Set(corridor.map(p => p.osm_id))
|
||||
const extra = explore.filter(p => !seen.has(p.osm_id))
|
||||
return extra.length ? [...corridor, ...extra] : corridor
|
||||
}, [corridor, explore])
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import type { Accommodation, TripMember, Day, Place, Reservation, PackingItem, T
|
||||
import { ListTodo, Download, Plus, Trash2, FolderPlus } from 'lucide-react'
|
||||
import { useTripPlanner } from './tripPlanner/useTripPlanner'
|
||||
import { usePoiExplore } from '../components/Map/usePoiExplore'
|
||||
import { useMergedMapPois } from '../components/Map/useMergedMapPois'
|
||||
import PoiCategoryPill from '../components/Map/PoiCategoryPill'
|
||||
import { useTouchDragBridge } from '../hooks/useTouchDragBridge'
|
||||
|
||||
@@ -320,7 +321,11 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
const [glMap, setGlMap] = useState<CompassMap | null>(null)
|
||||
// The corridor search draws into the same map channel and answers the same question for
|
||||
// a drive, so the explore pill stands down while road trip mode is on.
|
||||
const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false && !roadtripActive
|
||||
// Also in road trip mode: searching the view is a different question from searching the
|
||||
// drive ("is there a hotel at tonight's stop" versus "what is along the way"), and the
|
||||
// two answers are drawn side by side rather than one hiding the other.
|
||||
const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false
|
||||
const mapPois = useMergedMapPois(roadtripActive ? roadtripCorridor.visible : null, poi.pois)
|
||||
|
||||
// Costs expense editor opened from a booking modal (save-then-open). Lives at the
|
||||
// page level so it has tripMembers / base currency / current user available.
|
||||
@@ -425,12 +430,11 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
const r = reservations.find(x => x.id === rid)
|
||||
if (r) setMapTransportDetail(r)
|
||||
}}
|
||||
pois={roadtripActive
|
||||
/* `visible`, not `search.results`: the map is the picture of this very
|
||||
list, and filtering the list while seventy pins stay on the map no
|
||||
longer answers "which of these". */
|
||||
? roadtripCorridor.visible
|
||||
: poi.pois}
|
||||
/* In road trip mode the corridor's `visible` (not `search.results`: the map is
|
||||
the picture of that very list, and filtering the list while seventy pins stay
|
||||
on the map no longer answers "which of these") plus whatever the category
|
||||
pill found in view. Outside it, only the pill's hits. */
|
||||
pois={mapPois}
|
||||
onPoiClick={handlePoiClick}
|
||||
// Only while road trip mode is on: outside it there is no drive to drop onto.
|
||||
onPoiDropOnRoute={roadtripActive ? dropPoiOnRoute : undefined}
|
||||
|
||||
Reference in New Issue
Block a user