feat(map): the world map opens on the operator's own longitude

Centred on 0 degrees it left an Australian looking at their own country
in the bottom-right corner, with the paths to everywhere they work
running off both edges. Centred on their own longitude the same map reads
the way their antenna does: the Americas to the east, Europe and Africa
to the west. Leaflet repeats the world horizontally, so this is a choice
of viewpoint and costs nothing — the tiles either side are the same
world, and unwrapLon already draws the arcs across the seam.

The latitude is damped rather than followed (a station at 69 north
centred on itself would spend half the map on the Arctic), the station's
square arrives after the map is built so it re-centres once when it
lands, and a view the operator has panned to themselves still wins.
This commit is contained in:
2026-09-06 14:14:36 +02:00
parent e0bba21ecc
commit 50e97a8e0d
2 changed files with 38 additions and 3 deletions
+34 -1
View File
@@ -28,6 +28,26 @@ function saveMapView(m: L.Map) {
// unwrapLon makes a lat/lon ring continuous in longitude (each point within
// 180° of the previous) so a polygon crossing the antimeridian doesn't snap
// across the whole map. Coords may exceed ±180; Leaflet (worldCopyJump) is fine.
// homeView is where a world map should open for THIS station.
//
// Centred on 0° a world map puts Europe in the middle and leaves an Australian
// looking at their own country in the bottom-right corner, with the paths to
// everywhere they work running off both edges. Centred on their own longitude
// the same map reads the way their antenna does: the Americas to the east,
// Europe and Africa to the west. Leaflet repeats the world horizontally
// (worldCopyJump), so this is a choice of viewpoint and costs nothing — the
// tiles either side are the same world, and the great-circle code already draws
// across the seam (see unwrapLon).
//
// The latitude is damped, not followed: a station at 69° north centred on itself
// would spend half the map on the Arctic. It leans towards the operator's
// hemisphere and stops well short of the pole.
function homeView(grid: string): [number, number] {
const home = gridToLatLon(grid);
if (!home) return [20, 0];
return [Math.max(-35, Math.min(45, home.lat * 0.6 + 8)), home.lon];
}
function unwrapLon(ring: [number, number][]): [number, number][] {
const out: [number, number][] = [];
let prev = NaN;
@@ -178,6 +198,18 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
const autoZoomRef = useRef(autoZoom);
useEffect(() => { autoZoomRef.current = autoZoom; }, [autoZoom]);
// The station's own square arrives from the profile a moment AFTER this map is
// built, so the first view is drawn without it. Re-centred once when it lands —
// and never again, nor over a view the operator has panned to themselves.
const homeCentred = useRef(false);
useEffect(() => {
const m = worldMap.current;
if (!m || homeCentred.current || !gridToLatLon(fromGrid)) return;
homeCentred.current = true;
if (loadMapView()) return; // their own view wins
m.setView(homeView(fromGrid), m.getZoom());
}, [fromGrid]);
// One-time map creation.
useEffect(() => {
if (worldRef.current && !worldMap.current) {
@@ -187,7 +219,7 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
// enough to blow WebView2's raster budget and leave the window painting in
// patches. On canvas it is a single layer.
const m = L.map(worldRef.current, { zoomControl: true, attributionControl: true, worldCopyJump: true, preferCanvas: true })
.setView([20, 0], 1);
.setView(homeView(fromGrid), 1);
addBasemap(m, basemap, baseLayer, labelsLayer);
worldOverlay.current = L.layerGroup().addTo(m);
worldMap.current = m;
@@ -196,6 +228,7 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
m.on('moveend', () => { if (!autoZoomRef.current) saveMapView(m); });
}
const t = window.setTimeout(() => { worldMap.current?.invalidateSize(); }, 80);
// Resizing the pane is the ONLY thing that needs invalidateSize. Calling it on
// every overlay redraw (as before) forced a full repaint of the map each time
// the rotor moved a degree.