feat(sat): set it up in Settings, work the pass in the tab
Two things belong in different places, and they were in one. Settings → Satellites now holds the setup: which satellites to follow — the same two-column shape as the awards, for the same reason, since a feed carries two hundred birds and an operator works six — and the orbital elements, their age, the fetch, and pasting your own. Following none still means every satellite with both elements and a frequency plan, so somebody who has not chosen yet is not handed an empty tab. The panel keeps only what a pass needs. A countdown to AOS, or to LOS once it is up, because that is the number that decides whether you sit down; a bar for where in the pass you are, since mid-pass the useful question is not the clock but whether you are past the peak; rise, peak and set with compass directions, because "rises SW" is a direction to look in and 213° is arithmetic. Distance, altitude and footprint. And approaching or receding, which is the sign of the whole Doppler correction and the only thing that explains why the frequencies are moving the way they are. The countdowns run in the browser from two timestamps. Predicting a pass steps the orbit across a day thirty seconds at a time, which is not something to do once a second for a clock the page can keep itself.
This commit is contained in:
+76
@@ -136,6 +136,37 @@ type SatTuning struct {
|
||||
RangeRate float64 `json:"range_rate"`
|
||||
Visible bool `json:"visible"`
|
||||
At time.Time `json:"at"`
|
||||
|
||||
// Where the satellite is over the earth. Carried with the tuning because
|
||||
// they are read together and change together — the panel would otherwise ask
|
||||
// twice a second for two halves of one instant.
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
AltKm float64 `json:"alt_km"`
|
||||
Footprint float64 `json:"footprint_km"`
|
||||
}
|
||||
|
||||
// SatPassInfo is the pass in progress, or the next one.
|
||||
//
|
||||
// Separate from the tuning and polled far more slowly: predicting a pass steps
|
||||
// the orbit thirty seconds at a time across hours, which is not something to do
|
||||
// once a second for a countdown a browser can run itself from two timestamps.
|
||||
type SatPassInfo struct {
|
||||
Name string `json:"name"`
|
||||
HasPass bool `json:"has_pass"`
|
||||
// InPass distinguishes "it is up now" from "it rises at". The pass in
|
||||
// progress is reported whatever its maximum elevation: an operator watching
|
||||
// a satellite go over does not want it hidden because it fell below the
|
||||
// threshold that filters the TABLE of what is worth waiting for.
|
||||
InPass bool `json:"in_pass"`
|
||||
AOS time.Time `json:"aos"`
|
||||
LOS time.Time `json:"los"`
|
||||
AOSAz float64 `json:"aos_az"`
|
||||
LOSAz float64 `json:"los_az"`
|
||||
MaxEl float64 `json:"max_el"`
|
||||
MaxElAz float64 `json:"max_el_az"`
|
||||
MaxElAt time.Time `json:"max_el_at"`
|
||||
Duration float64 `json:"duration_s"`
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
||||
@@ -708,6 +739,50 @@ func (a *App) GetSatellitePasses(names []string, hours int) ([]sat.Pass, error)
|
||||
return passes, nil
|
||||
}
|
||||
|
||||
// GetSatelliteNextPass is the pass in progress, or the next one to come.
|
||||
//
|
||||
// The one question that decides whether an operator sits down at the radio, and
|
||||
// the reason a satellite tab is worth having at all: how long have I got, and
|
||||
// how high does it get.
|
||||
func (a *App) GetSatelliteNextPass(name string) (SatPassInfo, error) {
|
||||
out := SatPassInfo{Name: name}
|
||||
obs, err := a.satObserver()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
real, ok := a.satResolve(name)
|
||||
if !ok {
|
||||
return out, fmt.Errorf("%s is not in the element set", name)
|
||||
}
|
||||
store, _, _ := a.satParts()
|
||||
now := time.Now().UTC()
|
||||
// From a little before now: a pass that started two minutes ago is the one
|
||||
// the operator is in, and asking from this instant would skip it and report
|
||||
// the next orbit instead — an hour and a half away, while the satellite is
|
||||
// overhead.
|
||||
from := now.Add(-30 * time.Minute)
|
||||
// Elevation zero, not the operator's minimum. That threshold filters the
|
||||
// table of passes worth waiting for; it must not hide the pass they are
|
||||
// actually working.
|
||||
passes, err := store.Passes(real, obs, from, now.Add(26*time.Hour), 0)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
for _, p := range passes {
|
||||
if p.LOS.Before(now) {
|
||||
continue // already over
|
||||
}
|
||||
out.HasPass = true
|
||||
out.InPass = !p.AOS.After(now)
|
||||
out.AOS, out.LOS = p.AOS, p.LOS
|
||||
out.AOSAz, out.LOSAz = p.AOSAz, p.LOSAz
|
||||
out.MaxEl, out.MaxElAz, out.MaxElAt = p.MaxEl, p.MaxElAz, p.MaxElAt
|
||||
out.Duration = p.Duration
|
||||
return out, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetSatelliteTuning is the working answer: where to listen, where to transmit,
|
||||
// and where the bird is, for one satellite and one transponder.
|
||||
//
|
||||
@@ -777,6 +852,7 @@ func (a *App) GetSatelliteTuning(name string, transponder int, downHz int64) (Sa
|
||||
sh := sat.Doppler(p, out.NominalDown, out.NominalUp)
|
||||
out.DownHz, out.UpHz = sh.DownHz, sh.UpHz
|
||||
out.Az, out.El, out.RangeKm, out.RangeRate = p.Az, p.El, p.RangeKm, p.RangeRate
|
||||
out.Lat, out.Lon, out.AltKm, out.Footprint = p.Lat, p.Lon, p.AltKm, p.Footprint
|
||||
out.Visible = p.Visible()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user