feat(maps): remembered views for the FT and grid maps, and no wasted first paint

The world map was the only one that remembered anything. Panning and
zooming a map is the operator saying which part of the world they work,
and on the FT decodes map and the grid-square map that was thrown away on
every tab switch. Both now keep centre and zoom, through one shared
helper (lib/mapView) that the world map uses too, and the keys are
portable so a copied data folder brings the views with it.

The world map also waits for the station's square before it paints. It
drew the world at 0 degrees and then moved to the operator's longitude —
a screenful of Esri tiles fetched and discarded on every first run. It is
now built once, knowing where it is looking; a profile with no locator
gets the default view after two seconds rather than a blank panel.
This commit is contained in:
2026-09-06 14:23:43 +02:00
parent 50e97a8e0d
commit f56630d7d6
6 changed files with 91 additions and 27 deletions
+12 -3
View File
@@ -8,6 +8,7 @@ import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { BASEMAPS, addBasemap, loadBasemap, type BasemapKey } from '@/components/MainMap';
import { writeUiPref } from '@/lib/uiPref';
import { loadMapView, saveMapView, MAP_VIEW_GRIDS } from '@/lib/mapView';
// GridSquareMap — every Maidenhead square in the log, drawn on a world map.
//
@@ -131,8 +132,16 @@ export function GridSquareMap({ myGrid, className }: { myGrid?: string; classNam
}).setView([20, 0], 2);
// The whole world, once, whatever the window is shaped like — computed by
// Leaflet from the container rather than guessed with a zoom number.
m.fitWorld({ animate: false });
// Leaflet from the container rather than guessed with a zoom number. Unless
// the operator has already chosen a view: theirs is the answer, and fitting
// the world over it would undo the choice on every tab switch.
const saved = loadMapView(MAP_VIEW_GRIDS);
if (saved) m.setView([saved.lat, saved.lon], saved.zoom);
else m.fitWorld({ animate: false });
m.on('moveend', () => {
const c = m.getCenter();
saveMapView(MAP_VIEW_GRIDS, c.lat, c.lng, m.getZoom());
});
mapRef.current = m;
layerRef.current = L.layerGroup().addTo(m);
// Leaflet measures its container ONCE, when the map is created, and never
@@ -144,7 +153,7 @@ export function GridSquareMap({ myGrid, className }: { myGrid?: string; classNam
// fitWorld above ran against a container that may still have had no size —
// the tab is mounted hidden. Fit ONCE more the first time it really has one,
// and never again: after that the view belongs to the operator.
let fitted = false;
let fitted = !!saved; // a remembered view is already the right size
const ro = new ResizeObserver(() => {
m.invalidateSize({ animate: false });
const el = hostRef.current;