fix(maps): one imagery choice per map, not one for two of them
The world map and the grid-square map shared a single key, so picking satellite imagery to look at grids repainted the main map too, and there was no way to have terrain on one and streets on the other. They are different maps answering different questions, and the imagery that suits one is not the imagery that suits the next. Four keys now, one per map, in lib/mapBase beside the remembered views — named in one place so a rename cannot silently orphan somebody's choice — and portable, so a copied data folder brings them along. The grid map inherits whatever was set under the old shared key rather than being reset to the default: an operator who chose imagery there keeps it.
This commit is contained in:
@@ -6,6 +6,7 @@ import { BASEMAPS, type BasemapKey } from '@/components/MainMap';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { loadMapView, saveMapView, MAP_VIEW_FT } from '@/lib/mapView';
|
||||
import { loadMapBase, saveMapBase, MAP_BASE_FT } from '@/lib/mapBase';
|
||||
|
||||
// FT Map — the live decode feed as geography: every station decoded in the
|
||||
// last half hour, an arc from the operator's own square to theirs, coloured by
|
||||
@@ -128,7 +129,7 @@ export function FTMapPanel({ decodes, myGrid, onSelect, onCall }: {
|
||||
};
|
||||
baseRef.current = L.tileLayer(bm.url, { ...opts, attribution: bm.attr, subdomains: bm.subdomains ?? 'abc' }).addTo(m);
|
||||
if (bm.labelsUrl) labelsRef.current = L.tileLayer(bm.labelsUrl, opts).addTo(m);
|
||||
localStorage.setItem('opslog.ftmapBase', basemap);
|
||||
saveMapBase(MAP_BASE_FT, basemap);
|
||||
}, [basemap]);
|
||||
|
||||
// The arcs, redrawn when the decode list changes. Newest last so they paint
|
||||
|
||||
@@ -6,7 +6,8 @@ import { GridSquares } from '../../wailsjs/go/main/App';
|
||||
import { gridSquareBounds, gridToLatLon } from '@/lib/maidenhead';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BASEMAPS, addBasemap, loadBasemap, type BasemapKey } from '@/components/MainMap';
|
||||
import { BASEMAPS, addBasemap, type BasemapKey } from '@/components/MainMap';
|
||||
import { loadMapBase, saveMapBase, MAP_BASE_GRIDS, MAP_BASE_WORLD } from '@/lib/mapBase';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { loadMapView, saveMapView, MAP_VIEW_GRIDS } from '@/lib/mapView';
|
||||
|
||||
@@ -86,7 +87,9 @@ export function GridSquareMap({ myGrid, className }: { myGrid?: string; classNam
|
||||
() => (SCOPES.some((s) => s.key === localStorage.getItem(SCOPE_KEY))
|
||||
? (localStorage.getItem(SCOPE_KEY) as ScopeKey) : 'DIGI'));
|
||||
|
||||
const [basemap, setBasemap] = useState<BasemapKey>(loadBasemap);
|
||||
// This map's own imagery. It shared the world map's key until they were
|
||||
// separated, so a choice made back then is inherited rather than reset.
|
||||
const [basemap, setBasemap] = useState<BasemapKey>(() => loadMapBase(MAP_BASE_GRIDS, 'light', MAP_BASE_WORLD));
|
||||
const [confColour, setConfColour] = useState(() => localStorage.getItem(COL_CONFIRMED_KEY) ?? '');
|
||||
const [workedColour, setWorkedColour] = useState(() => localStorage.getItem(COL_WORKED_KEY) ?? '');
|
||||
// Repaint the squares when the THEME changes, not the basemap: the fills come
|
||||
@@ -253,7 +256,7 @@ export function GridSquareMap({ myGrid, className }: { myGrid?: string; classNam
|
||||
</span>
|
||||
<select
|
||||
value={basemap}
|
||||
onChange={(e) => { const v = e.target.value as BasemapKey; setBasemap(v); writeUiPref('opslog.mapBasemap', v); }}
|
||||
onChange={(e) => { const v = e.target.value as BasemapKey; setBasemap(v); saveMapBase(MAP_BASE_GRIDS, v); }}
|
||||
title={t('gsm.basemap')}
|
||||
className="h-6 rounded border border-border bg-background px-1 text-[11px]"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { nightPolygon } from '../lib/greyline';
|
||||
import { gridToLatLon, gridSquareBounds, greatCirclePoints, pathBetween, destinationPoint } from '@/lib/maidenhead';
|
||||
import { loadMapBase, saveMapBase, MAP_BASE_WORLD } from '@/lib/mapBase';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { formatDistance } from '@/lib/units';
|
||||
import { loadMapView, saveMapView, MAP_VIEW_WORLD } from '@/lib/mapView';
|
||||
@@ -116,9 +117,10 @@ export const BASEMAPS: Record<BasemapKey, { label: string; url: string; attr: st
|
||||
attr: 'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics',
|
||||
labelsUrl: 'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}' },
|
||||
};
|
||||
// loadBasemap is the WORLD map's imagery. Each map keeps its own — see
|
||||
// lib/mapBase, which is where the keys live.
|
||||
export function loadBasemap(): BasemapKey {
|
||||
const v = localStorage.getItem('opslog.mapBasemap');
|
||||
return v === 'voyager' || v === 'street' || v === 'satellite' ? v : 'light';
|
||||
return loadMapBase(MAP_BASE_WORLD, 'light');
|
||||
}
|
||||
|
||||
// addBasemap (re)installs the imagery layer and, for satellite, its transparent
|
||||
@@ -445,7 +447,7 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
onClick={() => { setBasemap(k); writeUiPref('opslog.mapBasemap', k); }}
|
||||
onClick={() => { setBasemap(k); saveMapBase(MAP_BASE_WORLD, k); }}
|
||||
title={`Basemap: ${BASEMAPS[k].label}`}
|
||||
className={`px-2 py-1 text-[11px] font-medium transition-colors ${
|
||||
basemap === k ? 'bg-primary text-primary-foreground' : 'bg-card/90 text-muted-foreground hover:bg-card'
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { gridToLatLon, splitAtAntimeridian } from '@/lib/maidenhead';
|
||||
import { BASEMAPS, type BasemapKey } from '@/components/MainMap';
|
||||
import { loadMapView, saveMapView } from '@/lib/mapView';
|
||||
import { loadMapBase, saveMapBase, MAP_BASE_SAT } from '@/lib/mapBase';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -246,8 +247,7 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
const layerRef = useRef<L.LayerGroup | null>(null);
|
||||
const baseRef = useRef<L.TileLayer | null>(null);
|
||||
const labelsRef = useRef<L.TileLayer | null>(null);
|
||||
const [basemap, setBasemap] = useState<BasemapKey>(() =>
|
||||
(localStorage.getItem('opslog.satMapBase') as BasemapKey) || 'light');
|
||||
const [basemap, setBasemap] = useState<BasemapKey>(() => loadMapBase(MAP_BASE_SAT, 'light'));
|
||||
const saved = useRef(loadMapView(MAP_VIEW_SAT));
|
||||
const [track, setTrack] = useState<Position[]>([]);
|
||||
|
||||
@@ -293,7 +293,7 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
};
|
||||
baseRef.current = L.tileLayer(bm.url, { ...opts, attribution: bm.attr, subdomains: bm.subdomains ?? 'abc' }).addTo(m);
|
||||
if (bm.labelsUrl) labelsRef.current = L.tileLayer(bm.labelsUrl, opts).addTo(m);
|
||||
localStorage.setItem('opslog.satMapBase', basemap);
|
||||
saveMapBase(MAP_BASE_SAT, basemap);
|
||||
}, [basemap]);
|
||||
|
||||
// The selected bird's path over the ground, redrawn when the selection
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Which imagery each map draws on — one choice per map.
|
||||
//
|
||||
// The world map and the grid-square map used to share a single key, so picking
|
||||
// satellite imagery to look at grids also repainted the main map, and there was
|
||||
// no way to have terrain on one and plain streets on the other. They are
|
||||
// different maps answering different questions, and the imagery that suits one
|
||||
// is not the imagery that suits the next.
|
||||
//
|
||||
// Portable (see lib/uiPref) like the remembered views in lib/mapView: a copied
|
||||
// data folder brings the choices with it.
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import type { BasemapKey } from '@/components/MainMap';
|
||||
|
||||
// The keys in use. Named here rather than typed at each call site so a rename
|
||||
// cannot silently orphan somebody's choice.
|
||||
export const MAP_BASE_WORLD = 'opslog.mapBasemap';
|
||||
export const MAP_BASE_GRIDS = 'opslog.gridMapBase';
|
||||
export const MAP_BASE_FT = 'opslog.ftmapBase';
|
||||
export const MAP_BASE_SAT = 'opslog.satMapBase';
|
||||
|
||||
const VALID = ['light', 'voyager', 'street', 'satellite'];
|
||||
|
||||
// loadMapBase reads one map's choice.
|
||||
//
|
||||
// inheritFrom exists for the split: the grid map's choice lived under the world
|
||||
// map's key until they were separated, so an operator who had chosen imagery
|
||||
// there keeps it instead of being silently reset to the default.
|
||||
export function loadMapBase(key: string, fallback: BasemapKey, inheritFrom?: string): BasemapKey {
|
||||
const read = (k: string) => {
|
||||
const v = localStorage.getItem(k);
|
||||
return v && VALID.includes(v) ? (v as BasemapKey) : null;
|
||||
};
|
||||
return read(key) ?? (inheritFrom ? read(inheritFrom) : null) ?? fallback;
|
||||
}
|
||||
|
||||
export function saveMapBase(key: string, v: BasemapKey): void {
|
||||
writeUiPref(key, v);
|
||||
}
|
||||
@@ -35,7 +35,8 @@ const PORTABLE_KEYS = [
|
||||
'opslog.lookupOnBlur', // run the callsign lookup on blur instead of while typing
|
||||
'opslog.groupDigitalSlots', // matrix + cluster: all digital modes count as ONE (DXCC-style) instead of per-mode slots
|
||||
'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane)
|
||||
'opslog.mapBasemap', // world map basemap (light / street / satellite)
|
||||
// One imagery choice per map — world, grid squares, FT map, satellites.
|
||||
'opslog.mapBasemap', 'opslog.gridMapBase', 'opslog.ftmapBase', 'opslog.satMapBase',
|
||||
'opslog.dateFormat', // how dates are DISPLAYED (iso / fr / us); storage stays ISO
|
||||
'opslog.mapGreyline', // world map: grey line (day/night terminator) shown
|
||||
'opslog.awardRefSort', 'opslog.awardRefSortDir', // award reference table: sort column and direction
|
||||
|
||||
Reference in New Issue
Block a user