mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(maps): project the journey map like the planner on a GCJ-02 basemap
The journey map read the same tile setting as the planner but built its Leaflet map without the shifted CRS, so an Amap basemap put every entry a few hundred metres off. A change of datum rebuilds the map, since Leaflet cannot swap a CRS after construction; every other template change still retiles in place.
This commit is contained in:
@@ -62,6 +62,7 @@ import { render, act, fireEvent, waitFor } from '../../../tests/helpers/render';
|
|||||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||||
import { useSettingsStore } from '../../store/settingsStore';
|
import { useSettingsStore } from '../../store/settingsStore';
|
||||||
import { buildSettings } from '../../../tests/helpers/factories';
|
import { buildSettings } from '../../../tests/helpers/factories';
|
||||||
|
import { AMAP_ROAD } from '../../constants/mapDefaults';
|
||||||
import type { Mock } from 'vitest';
|
import type { Mock } from 'vitest';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import JourneyMap from './JourneyMap';
|
import JourneyMap from './JourneyMap';
|
||||||
@@ -89,6 +90,11 @@ vi.mock('../Map/engines/maplibre', () => ({ default: {} }));
|
|||||||
// jsdom refuses a WebGL context, and the basemap now believes it (#2288). These
|
// jsdom refuses a WebGL context, and the basemap now believes it (#2288). These
|
||||||
// cases are about the GL layer, so the probe says yes here.
|
// cases are about the GL layer, so the probe says yes here.
|
||||||
vi.mock('../../utils/webgl', () => ({ hasWebGL: () => true, resetWebGLProbe: () => {} }));
|
vi.mock('../../utils/webgl', () => ({ hasWebGL: () => true, resetWebGLProbe: () => {} }));
|
||||||
|
// The real CRS is built from Leaflet's projection, which the mock above does not
|
||||||
|
// carry. What matters here is only that the map is handed one for Amap tiles.
|
||||||
|
vi.mock('../Map/gcj02Crs', () => ({
|
||||||
|
crsForBasemap: (gcj02: boolean) => (gcj02 ? { code: 'TREK:GCJ02' } : undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
const entriesWithCoords = [
|
const entriesWithCoords = [
|
||||||
{ id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Paris', mood: null, entry_date: '2025-06-01' },
|
{ id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Paris', mood: null, entry_date: '2025-06-01' },
|
||||||
@@ -420,6 +426,18 @@ describe('JourneyMap', () => {
|
|||||||
seedStore(useSettingsStore, { settings: buildSettings({ map_tile_url: 'https://tiles.test/{z}/{x}/{y}.png' }) });
|
seedStore(useSettingsStore, { settings: buildSettings({ map_tile_url: 'https://tiles.test/{z}/{x}/{y}.png' }) });
|
||||||
render(<JourneyMap checkins={[]} entries={entriesWithCoords} />);
|
render(<JourneyMap checkins={[]} entries={entriesWithCoords} />);
|
||||||
expect(vi.mocked(L.tileLayer).mock.calls[0][0]).toBe('https://tiles.test/{z}/{x}/{y}.png');
|
expect(vi.mocked(L.tileLayer).mock.calls[0][0]).toBe('https://tiles.test/{z}/{x}/{y}.png');
|
||||||
|
// A WGS-84 template leaves the projection alone: passing the default CRS
|
||||||
|
// explicitly would be the same value, but omitting it keeps the map as it was.
|
||||||
|
expect(vi.mocked(L.map).mock.calls[0][1]).not.toHaveProperty('crs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('FE-COMP-JOURNEYMAP-027b: an Amap preset builds the map on the GCJ-02 projection', () => {
|
||||||
|
// The same shift the planner map applies. Miss it here and every entry on
|
||||||
|
// the journey page sits a few hundred metres from where the planner drew it.
|
||||||
|
seedStore(useSettingsStore, { settings: buildSettings({ map_tile_url: AMAP_ROAD }) });
|
||||||
|
render(<JourneyMap checkins={[]} entries={entriesWithCoords} />);
|
||||||
|
expect(vi.mocked(L.tileLayer).mock.calls[0][0]).toBe(AMAP_ROAD);
|
||||||
|
expect(vi.mocked(L.map).mock.calls[0][1]).toMatchObject({ crs: { code: 'TREK:GCJ02' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('FE-COMP-JOURNEYMAP-041: a basemap change restyles in place instead of rebuilding the map (#2097)', async () => {
|
it('FE-COMP-JOURNEYMAP-041: a basemap change restyles in place instead of rebuilding the map (#2097)', async () => {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useEffect, useRef, useImperativeHandle, useCallback, type Ref } from 'r
|
|||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
import { useSettingsStore } from '../../store/settingsStore'
|
import { useSettingsStore } from '../../store/settingsStore'
|
||||||
import { useCartoApiKey } from '../../hooks/useTileUrl'
|
import { useCartoApiKey } from '../../hooks/useTileUrl'
|
||||||
import { isVectorStyle, resolveTileUrl } from '../../utils/tileUrl'
|
import { isGcj02Basemap, isVectorStyle, resolveTileUrl } from '../../utils/tileUrl'
|
||||||
import { OFM_DARK, OFM_POSITRON, attributionForTile } from '../../constants/mapDefaults'
|
import { OFM_DARK, OFM_POSITRON, attributionForTile } from '../../constants/mapDefaults'
|
||||||
import { attachVectorBasemap, detachBasemapLayer, restyleBasemap, type BasemapLayer } from '../Map/VectorBasemap'
|
import { attachVectorBasemap, detachBasemapLayer, restyleBasemap, type BasemapLayer } from '../Map/VectorBasemap'
|
||||||
|
import { crsForBasemap } from '../Map/gcj02Crs'
|
||||||
import { escapeHtml, type JourneyTrack } from '@trek/shared'
|
import { escapeHtml, type JourneyTrack } from '@trek/shared'
|
||||||
|
|
||||||
export interface MapMarkerItem {
|
export interface MapMarkerItem {
|
||||||
@@ -156,6 +157,10 @@ function JourneyMap(
|
|||||||
const storedCartoKey = useCartoApiKey()
|
const storedCartoKey = useCartoApiKey()
|
||||||
const cartoKey = cartoApiKey || storedCartoKey
|
const cartoKey = cartoApiKey || storedCartoKey
|
||||||
const tileUrl = resolveTileUrl(mapTileUrl, dark ? OFM_DARK : OFM_POSITRON, cartoKey)
|
const tileUrl = resolveTileUrl(mapTileUrl, dark ? OFM_DARK : OFM_POSITRON, cartoKey)
|
||||||
|
// Amap's tiles are GCJ-02 (see gcj02Crs.ts), the same shift the planner map
|
||||||
|
// applies. Leaflet fixes a map's CRS at construction, so this one value is
|
||||||
|
// allowed to rebuild the map where a template change only retiles it.
|
||||||
|
const isGcjBasemap = !isVectorStyle(tileUrl) && isGcj02Basemap(tileUrl)
|
||||||
// Read through a ref by the map effect, retiled in place by its own effect below:
|
// Read through a ref by the map effect, retiled in place by its own effect below:
|
||||||
// the CARTO key reaches the store after the first render, and rebuilding the map
|
// the CARTO key reaches the store after the first render, and rebuilding the map
|
||||||
// for that raced with the markers and layers already on it (#2097).
|
// for that raced with the markers and layers already on it (#2097).
|
||||||
@@ -235,7 +240,9 @@ function JourneyMap(
|
|||||||
|
|
||||||
markersRef.current.clear()
|
markersRef.current.clear()
|
||||||
|
|
||||||
|
const crs = crsForBasemap(isGcjBasemap)
|
||||||
const map = L.map(containerRef.current, {
|
const map = L.map(containerRef.current, {
|
||||||
|
...(crs ? { crs } : {}),
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
scrollWheelZoom: fullScreen ? true : false,
|
scrollWheelZoom: fullScreen ? true : false,
|
||||||
@@ -384,7 +391,7 @@ function JourneyMap(
|
|||||||
glLayerRef.current = null
|
glLayerRef.current = null
|
||||||
markersRef.current.clear()
|
markersRef.current.clear()
|
||||||
}
|
}
|
||||||
}, [entries, stableTrail, stableTracks, dark, fullScreen, paddingBottom])
|
}, [entries, stableTrail, stableTracks, dark, fullScreen, paddingBottom, isGcjBasemap])
|
||||||
|
|
||||||
// Retile in place rather than through the effect above, which would drop every
|
// Retile in place rather than through the effect above, which would drop every
|
||||||
// marker and track it just drew. A vector basemap restyles instead, which also
|
// marker and track it just drew. A vector basemap restyles instead, which also
|
||||||
|
|||||||
Reference in New Issue
Block a user