feat(maps): satellite on the GL maps too, not just Leaflet

The imagery toggle has been there for a while and only ever worked on Leaflet, so
anyone on MapLibre or Mapbox — which is the default since the vector basemap landed —
had a switch they could not see and a view they could not reach.

Leaflet swaps its whole tile layer for the imagery. A GL map cannot: its basemap is a
style with dozens of layers in it. So the same tokenless ESRI source goes on as a
raster layer of its own, inserted before the first layer TREK draws, which leaves the
route, the pins and the tracks on top and legible. Switching back hides it rather than
tearing it down, so going to and fro does not re-fetch a tile.

Built on demand rather than once at load, because a basemap change rebuilds the style
from scratch and takes every source with it; the styledata event is subscribed for the
same reason. The switch reads the setting the Leaflet map already stores, so the choice
survives an instance changing renderer, and it sits in the same corner with the same
offsets.
This commit is contained in:
Maurice
2026-09-09 15:56:37 +02:00
committed by Maurice
parent 756e0fb908
commit 00cac02665
2 changed files with 152 additions and 1 deletions
@@ -1955,4 +1955,68 @@ describe('MapViewGL', () => {
// in the wrong place.
expect(layer.firstElementChild).not.toBe(handle)
})
// ── Satellite ───────────────────────────────────────────────────────────────
//
// Leaflet has had the imagery for a while and swaps its whole tile layer for it.
// A GL map cannot do that, because its basemap is a style with dozens of layers,
// so the imagery goes on as a raster layer under everything TREK draws.
it('FE-COMP-MAPVIEWGL-074: satellite adds the imagery under the first TREK layer', async () => {
loadOnAttach()
useSettingsStore.setState({
settings: { ...useSettingsStore.getState().settings, map_base_layer: 'satellite' },
} as never)
glMap.getStyle.mockReturnValue({
layers: [{ id: 'background' }, { id: 'road' }, { id: 'trip-route' }, { id: 'trip-gpx-hit' }],
})
render(<MapViewGL places={[]} fitKey={1} />)
await flushFrames()
expect(glMap.addSource).toHaveBeenCalledWith('trip-satellite', expect.objectContaining({
type: 'raster',
tiles: [expect.stringContaining('arcgisonline.com')],
}))
// Anchored before the route, or the imagery would be painted over it.
expect(glMap.addLayer).toHaveBeenCalledWith(
expect.objectContaining({ id: 'trip-satellite-raster', type: 'raster' }),
'trip-route',
)
})
it('FE-COMP-MAPVIEWGL-075: with the default basemap no imagery is fetched at all', async () => {
loadOnAttach()
useSettingsStore.setState({
settings: { ...useSettingsStore.getState().settings, map_base_layer: 'default' },
} as never)
render(<MapViewGL places={[]} fitKey={1} />)
await flushFrames()
expect(glMap.addSource).not.toHaveBeenCalledWith('trip-satellite', expect.anything())
})
it('FE-COMP-MAPVIEWGL-076: switching back hides the layer instead of tearing it down', async () => {
loadOnAttach()
useSettingsStore.setState({
settings: { ...useSettingsStore.getState().settings, map_base_layer: 'satellite' },
} as never)
glMap.getStyle.mockReturnValue({ layers: [{ id: 'background' }, { id: 'trip-route' }] })
// Present from the first pass on, the way the real map reports it afterwards.
glMap.getSource.mockImplementation((id: string) => (id === 'trip-satellite' ? {} : null))
glMap.getLayer.mockImplementation((id: string) => (id === 'trip-satellite-raster' ? {} : null))
const { rerender } = render(<MapViewGL places={[]} fitKey={1} />)
await flushFrames()
act(() => {
useSettingsStore.setState({
settings: { ...useSettingsStore.getState().settings, map_base_layer: 'default' },
} as never)
})
rerender(<MapViewGL places={[]} fitKey={1} />)
await flushFrames()
expect(glMap.setLayoutProperty).toHaveBeenCalledWith('trip-satellite-raster', 'visibility', 'none')
})
})