feat: New fit to band and hide FTx option in bandmap tab
This commit is contained in:
+21
-1
@@ -839,6 +839,12 @@ export default function App() {
|
|||||||
return next;
|
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,
|
// Single band map docked beside the table (toggled by the toolbar button,
|
||||||
// visible across tabs). Independent of the multi-band "Band Map" tab.
|
// visible across tabs). Independent of the multi-band "Band Map" tab.
|
||||||
const [showBandMap, setShowBandMap] = useState(() => localStorage.getItem('bandmap.show') === '1');
|
const [showBandMap, setShowBandMap] = useState(() => localStorage.getItem('bandmap.show') === '1');
|
||||||
@@ -4122,7 +4128,7 @@ export default function App() {
|
|||||||
|
|
||||||
<TabsContent value="bandmap" className="mt-0 flex flex-col min-h-0 flex-1">
|
<TabsContent value="bandmap" className="mt-0 flex flex-col min-h-0 flex-1">
|
||||||
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-border/60 shrink-0 flex-wrap">
|
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-border/60 shrink-0 flex-wrap">
|
||||||
<span className="text-xs text-muted-foreground mr-1">Bands:</span>
|
<span className="text-xs text-muted-foreground mr-1">{t('bmp.bandsLabel')}</span>
|
||||||
{bands.map((b) => {
|
{bands.map((b) => {
|
||||||
const on = bandMapBands.includes(b);
|
const on = bandMapBands.includes(b);
|
||||||
return (
|
return (
|
||||||
@@ -4133,6 +4139,18 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<span className="ml-auto flex items-center gap-1">
|
||||||
|
<button type="button" onClick={toggleBandMapHideFt} title={t('bmp.hideFtTitle')}
|
||||||
|
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||||
|
bandMapHideFt ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||||
|
{t('bmp.hideFt')}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={toggleBandMapFit} title={t('bmp.fitTitle')}
|
||||||
|
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||||
|
bandMapFit ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||||
|
{t('bmp.fitBand')}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-0 flex gap-2 p-2 overflow-x-auto">
|
<div className="flex-1 min-h-0 flex gap-2 p-2 overflow-x-auto">
|
||||||
{bandMapBands.length === 0 ? (
|
{bandMapBands.length === 0 ? (
|
||||||
@@ -4148,6 +4166,8 @@ export default function App() {
|
|||||||
currentFreqHz={band === b && freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0}
|
currentFreqHz={band === b && freqMhz ? Math.round(parseFloat(freqMhz) * 1_000_000) : 0}
|
||||||
onSpotClick={handleSpotClick}
|
onSpotClick={handleSpotClick}
|
||||||
onClose={() => toggleBandMapBand(b)}
|
onClose={() => toggleBandMapBand(b)}
|
||||||
|
hideDigital={bandMapHideFt}
|
||||||
|
fitToBand={bandMapFit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ interface Props {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
side?: 'left' | 'right';
|
side?: 'left' | 'right';
|
||||||
onToggleSide?: () => void;
|
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<string, [number, number]> = {
|
const BAND_RANGES: Record<string, [number, number]> = {
|
||||||
@@ -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).
|
// last; ties broken by closeness to the rig freq).
|
||||||
const MAX_VISIBLE_SPOTS = 30;
|
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 { t } = useI18n();
|
||||||
const range = BAND_RANGES[band];
|
const range = BAND_RANGES[band];
|
||||||
const segments = SEGMENT_COLORS[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 fallback: [number, number] = range ?? [0, 1];
|
||||||
const [lo, hi] = fallback;
|
const [lo, hi] = fallback;
|
||||||
const span = hi - lo;
|
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
|
// 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
|
// 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
|
// 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.
|
// 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 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));
|
const otherSpots = inBand.filter((s) => !isFlood(s));
|
||||||
|
|
||||||
// Rank a DATA spot by usefulness (new entity → unworked → worked); ties
|
// 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),
|
totalH: Math.max(innerH + TOP_PAD + BOT_PAD, lastLabelBottom + BOT_PAD),
|
||||||
hidden: hiddenCount,
|
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).
|
// freqToY for elements rendered outside the memo (ticks, rig pointer).
|
||||||
// Must mirror the same offset so the rig triangle sits on the right kHz.
|
// 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
|
|||||||
<div className="h-full w-full flex flex-col min-h-0 bg-card">
|
<div className="h-full w-full flex flex-col min-h-0 bg-card">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground bg-muted/40 border-b border-border flex flex-nowrap items-center gap-0.5 shrink-0">
|
<div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground bg-muted/40 border-b border-border flex flex-nowrap items-center gap-0.5 shrink-0">
|
||||||
<span className="flex-1 min-w-0 truncate">{t('bmp.map')} · {band}</span>
|
<span className="flex-1 min-w-0 truncate">{t('bmp.map')} · {band}</span>
|
||||||
<button type="button" onClick={() => setZoomIdx((z) => Math.max(0, z - 1))} disabled={zoomIdx === 0}
|
<button type="button" onClick={() => setZoomIdx((z) => Math.max(0, z - 1))} disabled={fitToBand || zoomIdx === 0}
|
||||||
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
||||||
title={t('bmp.zoomOut')}>
|
title={t('bmp.zoomOut')}>
|
||||||
<Minus className="size-3" />
|
<Minus className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
<span className="shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-0.5">{pxPerKHz}px/kHz</span>
|
<span className="shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-0.5">{fitToBand ? t('bmp.fit') : `${pxPerKHz}px/kHz`}</span>
|
||||||
<button type="button" onClick={() => setZoomIdx((z) => Math.min(PX_PER_KHZ.length - 1, z + 1))} disabled={zoomIdx === PX_PER_KHZ.length - 1}
|
<button type="button" onClick={() => setZoomIdx((z) => Math.min(PX_PER_KHZ.length - 1, z + 1))} disabled={fitToBand || zoomIdx === PX_PER_KHZ.length - 1}
|
||||||
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
className="size-5 shrink-0 inline-flex items-center justify-center rounded hover:bg-muted disabled:opacity-30"
|
||||||
title={t('bmp.zoomIn')}>
|
title={t('bmp.zoomIn')}>
|
||||||
<Plus className="size-3" />
|
<Plus className="size-3" />
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ const en: Dict = {
|
|||||||
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)',
|
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)',
|
||||||
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
||||||
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
||||||
|
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
||||||
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.legendWorked': 'Worked', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
|
'bmp.legendNewDxcc': 'New DXCC', 'bmp.legendNewBand': 'New band', 'bmp.legendNewSlot': 'New slot (mode)', 'bmp.legendWorked': 'Worked', 'bmp.footerHint': 'scroll · ctrl+wheel = zoom · ◎ = jump to rig', 'bmp.spotsHidden': '{n} FT8/FT4 spots hidden — top {max} kept (CW/SSB all shown)',
|
||||||
'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).',
|
'frm.welcome': 'Welcome to OpsLog', 'frm.intro': 'Set up your station to start logging. These fields stamp every QSO and can be changed later in Preferences → Station Information (and per profile).',
|
||||||
'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name',
|
'frm.callsign': 'Callsign', 'frm.locator': 'Locator', 'frm.operator': 'Operator', 'frm.operatorPh': 'same as callsign', 'frm.owner': 'Owner', 'frm.ownerPh': 'station owner callsign', 'frm.name': 'Name', 'frm.namePh': 'your first name',
|
||||||
@@ -345,6 +346,7 @@ const fr: Dict = {
|
|||||||
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)',
|
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)',
|
||||||
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
||||||
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
||||||
|
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
||||||
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.legendWorked': 'Contacté', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
|
'bmp.legendNewDxcc': 'Nouveau DXCC', 'bmp.legendNewBand': 'Nouvelle bande', 'bmp.legendNewSlot': 'Nouveau mode', 'bmp.legendWorked': 'Contacté', 'bmp.footerHint': 'défiler · ctrl+molette = zoom · ◎ = aller au poste', 'bmp.spotsHidden': '{n} spots FT8/FT4 masqués — {max} meilleurs conservés (CW/SSB tous affichés)',
|
||||||
'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).',
|
'frm.welcome': 'Bienvenue dans OpsLog', 'frm.intro': 'Configure ta station pour commencer à logger. Ces champs sont inscrits sur chaque QSO et peuvent être modifiés plus tard dans Préférences → Informations station (et par profil).',
|
||||||
'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom',
|
'frm.callsign': 'Indicatif', 'frm.locator': 'Locator', 'frm.operator': 'Opérateur', 'frm.operatorPh': "identique à l'indicatif", 'frm.owner': 'Propriétaire', 'frm.ownerPh': 'indicatif du propriétaire de la station', 'frm.name': 'Nom', 'frm.namePh': 'ton prénom',
|
||||||
|
|||||||
Reference in New Issue
Block a user