diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index da9073c..2ccb475 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -839,6 +839,12 @@ export default function App() { return next; }); }, []); + // Global band-map-tab options (apply to every map in the tab): hide FTx/digital + // spots, and fit each map to its full band span. + const [bandMapHideFt, setBandMapHideFt] = useState(() => localStorage.getItem('opslog.bandMapHideFt') === '1'); + const [bandMapFit, setBandMapFit] = useState(() => localStorage.getItem('opslog.bandMapFit') === '1'); + const toggleBandMapHideFt = useCallback(() => setBandMapHideFt((v) => { const n = !v; writeUiPref('opslog.bandMapHideFt', n ? '1' : '0'); return n; }), []); + const toggleBandMapFit = useCallback(() => setBandMapFit((v) => { const n = !v; writeUiPref('opslog.bandMapFit', n ? '1' : '0'); return n; }), []); // Single band map docked beside the table (toggled by the toolbar button, // visible across tabs). Independent of the multi-band "Band Map" tab. const [showBandMap, setShowBandMap] = useState(() => localStorage.getItem('bandmap.show') === '1'); @@ -4122,7 +4128,7 @@ export default function App() {
- Bands: + {t('bmp.bandsLabel')} {bands.map((b) => { const on = bandMapBands.includes(b); return ( @@ -4133,6 +4139,18 @@ export default function App() { ); })} + + + +
{bandMapBands.length === 0 ? ( @@ -4148,6 +4166,8 @@ export default function App() { currentFreqHz={band === b && freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0} onSpotClick={handleSpotClick} onClose={() => toggleBandMapBand(b)} + hideDigital={bandMapHideFt} + fitToBand={bandMapFit} />
))} diff --git a/frontend/src/components/BandMap.tsx b/frontend/src/components/BandMap.tsx index fa23637..9dc5488 100644 --- a/frontend/src/components/BandMap.tsx +++ b/frontend/src/components/BandMap.tsx @@ -35,6 +35,12 @@ interface Props { onClose?: () => void; side?: 'left' | 'right'; onToggleSide?: () => void; + // hideDigital drops every DATA-class (FT8/FT4/JS8/…) spot so the crowded + // watering holes don't hog the map. fitToBand overrides zoom so the whole + // band edge-to-edge fits the visible height (no scrolling). Both are driven + // globally from the band-map tab toolbar. + hideDigital?: boolean; + fitToBand?: boolean; } const BAND_RANGES: Record = { @@ -147,7 +153,7 @@ const BOT_PAD = 14; // the top-most freq label isn't clipped at y=0 // last; ties broken by closeness to the rig freq). const MAX_VISIBLE_SPOTS = 30; -export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide }: Props) { +export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false }: Props) { const { t } = useI18n(); const range = BAND_RANGES[band]; const segments = SEGMENT_COLORS[band] ?? []; @@ -167,7 +173,12 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o const fallback: [number, number] = range ?? [0, 1]; const [lo, hi] = fallback; const span = hi - lo; - const pxPerKHz = PX_PER_KHZ[zoomIdx]; + // Fit-to-band computes the exact px/kHz so the whole band edge-to-edge fills + // the visible height; otherwise use the discrete zoom step. + const fitPxPerKHz = fitToBand && containerH > 0 && span > 0 + ? Math.max(0.05, (containerH - TOP_PAD - BOT_PAD) / span) + : 0; + const pxPerKHz = fitPxPerKHz || PX_PER_KHZ[zoomIdx]; // Anti-overlap layout: each label wants to sit at its true freq, but // never closer than PILL_H from the previous one. Sorted top-to-bottom @@ -199,7 +210,8 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o // spots carry no mode word and the band-plan fallback labels them the // generic "DATA" rather than "FT8". CW and SSB are always shown in full. const isFlood = (s: Spot) => spotModeCategory(inferSpotMode(s.comment ?? '', s.freq_hz)) === 'DATA'; - const ftSpots = inBand.filter(isFlood); + // hideDigital removes them entirely; otherwise they're capped below. + const ftSpots = hideDigital ? [] : inBand.filter(isFlood); const otherSpots = inBand.filter((s) => !isFlood(s)); // Rank a DATA spot by usefulness (new entity → unworked → worked); ties @@ -277,7 +289,7 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o totalH: Math.max(innerH + TOP_PAD + BOT_PAD, lastLabelBottom + BOT_PAD), hidden: hiddenCount, }; - }, [spots, range, lo, hi, span, pxPerKHz, containerH, spotStatus, currentFreqHz]); + }, [spots, range, lo, hi, span, pxPerKHz, containerH, spotStatus, currentFreqHz, hideDigital]); // freqToY for elements rendered outside the memo (ticks, rig pointer). // Must mirror the same offset so the rig triangle sits on the right kHz. @@ -350,13 +362,13 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
{t('bmp.map')} · {band} - - {pxPerKHz}px/kHz -