orbit-studio / src /modules /util /entityInfo.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
4.93 kB
import { JulianDate } from "@cesium/engine";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import type { OrbitClass } from "../../config/orbitClass";
import { satcatLabel, SATCAT_LAUNCH_SITE, SATCAT_OPS_STATUS, SATCAT_ORBIT_TYPE, SATCAT_OWNER } from "../../config/satcatCodes";
import { swathExtentsOf, type SatelliteMetadata } from "../../config/satelliteMetadata";
import type Orbit from "../Orbit";
import { recordTleLines } from "./gp";
import { derivedOrbitRows, orbitRegimeLabel } from "./orbitFacts";
dayjs.extend(utc);
export type ElementsInfo = { kind: "tle"; epoch: string; lines: string } | { kind: "omm"; epoch: string; rows: [string, string][] };
/**
* Label/value rows describing the satellite itself, as opposed to its orbit
* elements or current position: what follows from the element set, plus whatever
* static facts its record carries.
*
* Two provenances, in that order. The derived rows come first: orbit regime, then
* the apsides and node times from ./orbitFacts.ts. They are true of every satellite,
* and are what the served fields are read against. Every served row
* appears only when the record actually carries the field: a satellite absent from
* the satellite table shows nothing rather than the fallback values the renderer
* happens to use, which would read as data about the satellite when it is really a
* default.
*
* Takes the class rather than reading it off the metadata bag, so the caller
* resolves the one cache miss (`CatalogEntry.orbitClass`) instead of this
* function needing a second opinion about how to derive it. Takes the orbit as
* well, because the apsides and the node times need the satrec the class does not.
*/
export function getSatelliteInfo(orbit: Orbit, orbitClass: OrbitClass, metadata: SatelliteMetadata): [string, string][] {
const rows: [string, string][] = [["Orbit", orbitRegimeLabel(orbitClass, orbit)], ...derivedOrbitRows(orbit)];
const { coneFovDeg, operator, missionType, owner, launchDate, launchSite, opsStatus, orbitType, decayDate } = metadata;
const extents = swathExtentsOf(metadata);
if (extents !== undefined) {
const { starboardKm, portKm } = extents;
const total = starboardKm + portKm;
// Spell out the sides only when they differ — otherwise the total says it all.
rows.push(["Swath", starboardKm === portKm ? `${total} km` : `${total} km (${starboardKm} stbd / ${portKm} port)`]);
}
if (coneFovDeg !== undefined) {
rows.push(["Sensor FOV", `${coneFovDeg}°`]);
}
if (operator !== undefined) {
rows.push(["Operator", operator]);
}
if (missionType !== undefined) {
rows.push(["Mission", missionType]);
}
if (owner !== undefined) {
rows.push(["Owner", satcatLabel(SATCAT_OWNER, owner)]);
}
if (launchDate !== undefined) {
// One row, not two: the site alone reads as a fact about a place rather than
// about this satellite, and the pair is how anyone actually reads it.
rows.push(["Launched", launchSite === undefined ? launchDate : `${launchDate} · ${satcatLabel(SATCAT_LAUNCH_SITE, launchSite)}`]);
}
if (opsStatus !== undefined) {
rows.push(["Status", satcatLabel(SATCAT_OPS_STATUS, opsStatus)]);
}
// "Orbiting" is true of all but a handful of the satellites served, so stating
// it would cost a row on every panel to say nothing. Docked, impacted and
// landed are the cases worth a line.
if (orbitType !== undefined && orbitType !== "ORB") {
rows.push(["Orbit type", satcatLabel(SATCAT_ORBIT_TYPE, orbitType)]);
}
if (decayDate !== undefined) {
rows.push(["Decayed", decayDate]);
}
return rows;
}
export function getElementsInfo(orbit: Orbit): ElementsInfo {
const epoch = formatEpoch(orbit.julianDate);
if (orbit.record.kind === "tle") {
// TLE-sourced: the two element-set lines.
const tle = orbit.tle ?? recordTleLines(orbit.record)!;
return { kind: "tle", epoch, lines: tle.slice(1, 3).join("\n") };
}
// OMM-sourced: a compact element table.
const { omm } = orbit.record;
const rows: [string, unknown][] = [
["OBJECT_ID", omm.OBJECT_ID],
["NORAD_CAT_ID", omm.NORAD_CAT_ID],
["INCLINATION", omm.INCLINATION],
["RA_OF_ASC_NODE", omm.RA_OF_ASC_NODE],
["ECCENTRICITY", omm.ECCENTRICITY],
["ARG_OF_PERICENTER", omm.ARG_OF_PERICENTER],
["MEAN_ANOMALY", omm.MEAN_ANOMALY],
["MEAN_MOTION", omm.MEAN_MOTION],
["BSTAR", omm.BSTAR],
];
return {
kind: "omm",
epoch,
rows: rows.filter(([, value]) => value !== undefined && value !== null).map(([label, value]) => [label, String(value)]),
};
}
export function formatEpoch(julianDate: number): string {
const julianDayNumber = Math.floor(julianDate);
const secondsOfDay = (julianDate - julianDayNumber) * 60 * 60 * 24;
const epochDate = new JulianDate(julianDayNumber, secondsOfDay);
return dayjs.utc(epochDate as unknown as Date).format("YYYY-MM-DD HH:mm:ss");
}