diff --git a/client/src/components/Journey/JourneyMap.test.tsx b/client/src/components/Journey/JourneyMap.test.tsx
index d73e8fd52..6e704a377 100644
--- a/client/src/components/Journey/JourneyMap.test.tsx
+++ b/client/src/components/Journey/JourneyMap.test.tsx
@@ -37,7 +37,7 @@ vi.mock('leaflet', () => {
return {
default: {
map: vi.fn(() => mockMap),
- tileLayer: vi.fn(() => ({ addTo: vi.fn() })),
+ tileLayer: vi.fn(() => ({ addTo: vi.fn(), setUrl: vi.fn() })),
marker: vi.fn(() => mockMarker),
polyline: vi.fn(() => { const line: any = { addTo: vi.fn(() => line), bindTooltip: vi.fn(() => line) }; return line }),
divIcon: vi.fn(() => ({})),
@@ -45,7 +45,7 @@ vi.mock('leaflet', () => {
layerGroup: vi.fn(() => ({ addLayer: vi.fn(), addTo: vi.fn(), remove: vi.fn() })),
},
map: vi.fn(() => mockMap),
- tileLayer: vi.fn(() => ({ addTo: vi.fn() })),
+ tileLayer: vi.fn(() => ({ addTo: vi.fn(), setUrl: vi.fn() })),
marker: vi.fn(() => mockMarker),
polyline: vi.fn(() => { const line: any = { addTo: vi.fn(() => line), bindTooltip: vi.fn(() => line) }; return line }),
divIcon: vi.fn(() => ({})),
@@ -388,6 +388,22 @@ describe('JourneyMap', () => {
expect(vi.mocked(L.tileLayer).mock.calls[0][0]).toBe('https://tiles.test/{z}/{x}/{y}.png');
});
+ it('FE-COMP-JOURNEYMAP-041: a CARTO key arriving after mount retiles instead of rebuilding the map (#2097)', () => {
+ render();
+ const tiles = vi.mocked(L.tileLayer).mock.results[0].value as { setUrl: ReturnType };
+ const mapsBefore = vi.mocked(L.map).mock.calls.length;
+
+ act(() => {
+ seedStore(useSettingsStore, { settings: buildSettings({ carto_api_key: 'k1' }) });
+ });
+
+ expect(tiles.setUrl).toHaveBeenCalledWith(expect.stringContaining('key=k1'));
+ // The markers and tracks the map already carries are drawn by the effect that
+ // builds it, so a rebuild here would tear them down mid-flight.
+ expect(vi.mocked(L.map).mock.calls.length).toBe(mapsBefore);
+ expect(mockedMap().remove).not.toHaveBeenCalled();
+ });
+
it('FE-COMP-JOURNEYMAP-028: the activeMarkerId prop flies to that marker after the settle delay', () => {
vi.useFakeTimers();
try {
diff --git a/client/src/components/Journey/JourneyMap.tsx b/client/src/components/Journey/JourneyMap.tsx
index 0147a0ed5..5539e98f1 100644
--- a/client/src/components/Journey/JourneyMap.tsx
+++ b/client/src/components/Journey/JourneyMap.tsx
@@ -154,6 +154,13 @@ function JourneyMap(
const mapTileUrl = useSettingsStore(s => s.settings.map_tile_url)
const storedCartoKey = useCartoApiKey()
const cartoKey = cartoApiKey || storedCartoKey
+ const tileUrl = resolveTileUrl(mapTileUrl, dark ? CARTO_DARK : CARTO_VOYAGER, cartoKey)
+ // 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
+ // for that raced with the markers and layers already on it (#2097).
+ const tileUrlRef = useRef(tileUrl)
+ tileUrlRef.current = tileUrl
+ const tileLayerRef = useRef(null)
const containerRef = useRef(null)
const mapRef = useRef(null)
const markersRef = useRef