feat(sat): a sky plot — the pass seen from underneath it
The map answers "where is the satellite over the earth". This answers "where do I look", which during a pass is the question that matters. The projection is the one every tracker uses and every operator already reads: the centre is the zenith, the rim is the horizon, north is up. So the radius is (90 − elevation), not the elevation — a bird overhead is a dot in the middle, and a pass that hugs the rim is one that never rises. Whether it comes over the roof or along the treeline is something no amount of azimuth and elevation digits conveys, and one glance settles. The whole pass is drawn: a dashed track with arrowheads for the direction of travel, a hollow circle where it rises, a filled one where it sets, and a cross where the satellite is now — green above the horizon, grey below, because the numbers are still right down there and nothing can be worked through the earth. The track is fetched once a minute, not once a second: the SHAPE of a pass does not change while it happens. Only the marker moves, and that rides on the tuning poll that was already running.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { Satellite as SatIcon, Radio, ArrowUp, ArrowDown, PanelRightClose, PanelRightOpen } from 'lucide-react';
|
||||
import { Satellite as SatIcon, Radio, ArrowUp, ArrowDown, PanelRightClose, PanelRightOpen, Radar } from 'lucide-react';
|
||||
import {
|
||||
GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning,
|
||||
GetSatelliteGroundTrack, GetSatelliteTLEInfo, GetSatelliteNextPass,
|
||||
GetSatelliteGroundTrack, GetSatelliteTLEInfo, GetSatelliteNextPass, GetSatelliteSkyTrack,
|
||||
StartSatelliteTracking, StopSatelliteTracking, GetSatelliteTracking,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
@@ -14,6 +14,7 @@ import { BASEMAPS, type BasemapKey } from '@/components/MainMap';
|
||||
import { loadMapView, saveMapView } from '@/lib/mapView';
|
||||
import { loadMapBase, saveMapBase, MAP_BASE_SAT } from '@/lib/mapBase';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { SkyPlot, type SkyPoint } from '@/components/SkyPlot';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -71,6 +72,7 @@ const MAP_VIEW_SAT = 'opslog.satMapView';
|
||||
const SIDE_W_KEY = 'opslog.satSideWidth';
|
||||
const SIDE_SHOWN_KEY = 'opslog.satSideShown';
|
||||
const SIDE_W_DEFAULT = 336, SIDE_W_MIN = 240, SIDE_W_MAX = 720;
|
||||
const SKY_SHOWN_KEY = 'opslog.satSkyShown';
|
||||
|
||||
const fmtHz = (hz: number) => {
|
||||
if (!hz) return '—';
|
||||
@@ -149,6 +151,8 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
const [pass, setPass] = useState<PassInfo | null>(null);
|
||||
const [tle, setTle] = useState<{ count: number; age_h: number; stale: boolean; custom: number } | null>(null);
|
||||
const [tracking, setTracking] = useState<Track | null>(null);
|
||||
const [sky, setSky] = useState<SkyPoint[]>([]);
|
||||
const [skyShown, setSkyShown] = useState(() => localStorage.getItem(SKY_SHOWN_KEY) !== '0');
|
||||
const [err, setErr] = useState('');
|
||||
// A clock of its own, so every countdown on the panel ticks from one instant
|
||||
// and none of them needs a round trip to Go to lose a second.
|
||||
@@ -255,6 +259,24 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
return () => { live = false; window.clearInterval(id); };
|
||||
}, [sel]);
|
||||
|
||||
// The pass drawn across the sky. Once a minute is plenty: the SHAPE of a
|
||||
// pass does not change while it happens — only the marker on it moves, and
|
||||
// that comes from the tuning poll a second at a time.
|
||||
useEffect(() => {
|
||||
if (!sel || !skyShown) { setSky([]); return; }
|
||||
let live = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const pts: SkyPoint[] = ((await GetSatelliteSkyTrack(sel, 120)) as any) ?? [];
|
||||
if (live) setSky(pts);
|
||||
} catch { if (live) setSky([]); }
|
||||
};
|
||||
load();
|
||||
const id = window.setInterval(load, 60_000);
|
||||
return () => { live = false; window.clearInterval(id); };
|
||||
}, [sel, skyShown]);
|
||||
useEffect(() => { writeUiPref(SKY_SHOWN_KEY, skyShown ? '1' : '0'); }, [skyShown]);
|
||||
|
||||
// Passes are cheap but not free, and they change slowly.
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(loadPasses, 5 * 60_000);
|
||||
@@ -493,6 +515,14 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
>
|
||||
{Object.entries(BASEMAPS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
className={cn('h-7 px-1.5', skyShown && 'text-success')}
|
||||
onClick={() => setSkyShown((v) => !v)}
|
||||
title={skyShown ? t('sat.hideSky') : t('sat.showSky')}
|
||||
>
|
||||
<Radar className="size-3.5" />
|
||||
</Button>
|
||||
{/* Put the whole window on the map. On a laptop the readout takes a
|
||||
third of the screen, and there are moments — watching a footprint
|
||||
cross an ocean — when the map IS the answer. */}
|
||||
@@ -578,6 +608,21 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* The sky, seen from underneath it: the centre is straight up, the
|
||||
rim is the horizon, north is at the top. One glance says whether
|
||||
the pass comes over the roof or along the treeline. */}
|
||||
{skyShown && (
|
||||
<div className="rounded-lg border border-border bg-card p-2">
|
||||
<SkyPlot
|
||||
track={sky}
|
||||
az={tuning?.az ?? null}
|
||||
el={tuning?.el ?? null}
|
||||
name={bird?.name}
|
||||
visible={!!tuning?.visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Where it is, right now. */}
|
||||
<div className="rounded-lg border border-border bg-card p-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
||||
Reference in New Issue
Block a user