feat(ftmap): the live decode feed as a world map — feature branch
A new FT Map tab (Tools menu, beside FT decodes): every station decoded in the last half hour drawn as a great-circle arc from the operator's own square, coloured by band the way PSK Reporter taught everyone to read it, fading with age, one arc per call (its freshest sighting), a tooltip with call/grid/SNR, the MainMap basemap picker and a legend of only the bands on screen. Performance was the open question and it is answered structurally: the panel mounts only while its tab is ACTIVE (zero cost otherwise), the map renders canvas (one element, not one node per arc), arcs are capped at 300, and redraws follow the decode list — every period, not every frame.
This commit is contained in:
@@ -75,6 +75,7 @@ import { SendEQSLModal } from '@/components/qsl/SendEQSLModal';
|
||||
import { AutoEQSL } from '@/components/qsl/AutoEQSL';
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||
import { SettingsModal } from '@/components/SettingsModal';
|
||||
import { FTMapPanel } from '@/components/FTMapPanel';
|
||||
import { FirstRunModal } from '@/components/FirstRunModal';
|
||||
import { QSOEditModal } from '@/components/QSOEditModal';
|
||||
import { BandMap } from '@/components/BandMap';
|
||||
@@ -1313,6 +1314,17 @@ export default function App() {
|
||||
writeUiPref('opslog.gridsTab', '0');
|
||||
setActiveTab((t) => (t === 'grids' ? 'recent' : t));
|
||||
}
|
||||
const [ftmapTabOpen, setFtmapTabOpen] = useState(() => localStorage.getItem('opslog.ftmapTab') === '1');
|
||||
function openFtmapTab() {
|
||||
setFtmapTabOpen(true);
|
||||
writeUiPref('opslog.ftmapTab', '1');
|
||||
setActiveTab('ftmap');
|
||||
}
|
||||
function closeFtmapTab() {
|
||||
setFtmapTabOpen(false);
|
||||
writeUiPref('opslog.ftmapTab', '0');
|
||||
setActiveTab((t) => (t === 'ftmap' ? 'recent' : t));
|
||||
}
|
||||
function openDecodesTab() {
|
||||
setDecodesTabOpen(true);
|
||||
writeUiPref('opslog.decodesTab', '1');
|
||||
@@ -5110,6 +5122,7 @@ export default function App() {
|
||||
{ type: 'item', label: t('stats.tab'), action: 'tools.stats' },
|
||||
{ type: 'item', label: t('station.title'), action: 'tools.station' },
|
||||
{ type: 'item', label: t('dec.tab'), action: 'tools.decodes' },
|
||||
{ type: 'item', label: t('ftmap.tab'), action: 'tools.ftmap' },
|
||||
{ type: 'item', label: t('gsm.title'), action: 'tools.grids' },
|
||||
{ type: 'item', label: t('tools.qslDesigner'), action: 'tools.qsldesigner' },
|
||||
{ type: 'separator' },
|
||||
@@ -5164,6 +5177,7 @@ export default function App() {
|
||||
case 'tools.stats': setStatsTabOpen(true); setActiveTab('stats'); break;
|
||||
case 'tools.station': setStationTabOpen(true); setActiveTab('station'); break;
|
||||
case 'tools.decodes': openDecodesTab(); break;
|
||||
case 'tools.ftmap': openFtmapTab(); break;
|
||||
case 'tools.grids': openGridsTab(); break;
|
||||
case 'tools.qsldesigner': setQslDesignerOpen(true); break;
|
||||
case 'tools.winkeyer': wkSetEnabled(!wkEnabled); break;
|
||||
@@ -7795,6 +7809,21 @@ export default function App() {
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{ftmapTabOpen && (
|
||||
<TabsTrigger value="ftmap" className="gap-1.5">
|
||||
{t('ftmap.tab')}
|
||||
<span
|
||||
role="button"
|
||||
aria-label="Close FT map"
|
||||
title="Close"
|
||||
className="inline-flex items-center justify-center size-4 rounded hover:bg-foreground/10 text-muted-foreground hover:text-foreground"
|
||||
onPointerDown={(e) => { e.stopPropagation(); }}
|
||||
onClick={(e) => { e.stopPropagation(); closeFtmapTab(); }}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{stationTabOpen && (
|
||||
<TabsTrigger value="station" className="gap-1.5">
|
||||
{t('station.title')}
|
||||
@@ -8393,6 +8422,15 @@ export default function App() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{ftmapTabOpen && (
|
||||
<TabsContent value="ftmap" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||
{activeTab === 'ftmap' && (
|
||||
<div className="h-full w-full min-h-0 bg-card border border-border rounded-lg overflow-hidden">
|
||||
<FTMapPanel decodes={decodes as any} myGrid={station.my_grid} />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
)}
|
||||
{stationTabOpen && (
|
||||
<TabsContent value="station" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||
<StationControlPanel
|
||||
|
||||
Reference in New Issue
Block a user