mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Signed-off-by: Rudy Celekli <[email protected]>
248 lines
9.3 KiB
JavaScript
248 lines
9.3 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import { msg } from '../../i18n/index.js';
|
|
import { WINDOW_DAYS } from './policy.js';
|
|
|
|
/**
|
|
* Fallback when Launch Library sends no status. English in the record
|
|
* (launchStatusAllowsOrbit, analyst records); the mission panel translates it.
|
|
*/
|
|
export const UNKNOWN_LAUNCH_STATUS = msg('Unknown');
|
|
|
|
export function createModel({ state: layerState, services, parts, source }) {
|
|
function finiteCoordinate(value) {
|
|
// Number(null), Number('') and Number(false) are 0: absent is not zero.
|
|
if (value == null || typeof value === 'boolean') return null;
|
|
if (typeof value === 'string' && value.trim() === '') return null;
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
function normalizePayloadFlights(launch) {
|
|
const flights =
|
|
launch.rocket?.payloads ||
|
|
launch.payloads ||
|
|
launch.mission?.payloads ||
|
|
[];
|
|
if (!Array.isArray(flights)) return [];
|
|
return flights.map((flight, index) => {
|
|
const payload = flight.payload || flight;
|
|
return {
|
|
id: String(flight.id || payload.id || `payload-${index}`),
|
|
name: payload.name || flight.name || 'Unnamed payload',
|
|
type: payload.type?.name || flight.type?.name || null,
|
|
manufacturer: payload.manufacturer?.name || null,
|
|
operator: payload.operator?.name || null,
|
|
destination: flight.destination || payload.destination || null,
|
|
amount: Number.isFinite(Number(flight.amount))
|
|
? Number(flight.amount)
|
|
: 1,
|
|
massKg:
|
|
(typeof payload.mass === 'number' ||
|
|
(typeof payload.mass === 'string' && payload.mass.trim() !== '')) &&
|
|
Number.isFinite(Number(payload.mass)) &&
|
|
Number(payload.mass) >= 0
|
|
? Number(payload.mass)
|
|
: null,
|
|
};
|
|
});
|
|
}
|
|
|
|
function normalizeLanding(stage, fallbackName, category, index) {
|
|
const landing = stage?.landing;
|
|
const location = landing?.landing_location || {};
|
|
const launcher = stage?.launcher || stage?.spacecraft || {};
|
|
const serial = launcher.serial_number || stage?.serial_number || null;
|
|
const stageType = stage?.type?.name || stage?.type || category;
|
|
const attempted = landing?.attempt === true;
|
|
const success = landing?.success;
|
|
const recoveryType = landing?.type?.name || null;
|
|
const launcherStatus = launcher.status?.name || null;
|
|
const status =
|
|
success === true
|
|
? 'RECOVERED'
|
|
: success === false
|
|
? 'LOST'
|
|
: attempted
|
|
? 'RECOVERY ATTEMPT'
|
|
: recoveryType
|
|
? recoveryType.toUpperCase()
|
|
: launcherStatus
|
|
? launcherStatus.toUpperCase()
|
|
: 'NO RECOVERY DATA';
|
|
return {
|
|
id: String(stage?.id || landing?.id || `${category}-${index}`),
|
|
category,
|
|
name: [stageType, serial].filter(Boolean).join(' · ') || fallbackName,
|
|
serial,
|
|
reused: stage?.reused === true,
|
|
flightNumber: finiteCoordinate(stage?.launcher_flight_number),
|
|
status,
|
|
attempted,
|
|
success: success === true ? true : success === false ? false : null,
|
|
recoveryType,
|
|
destination:
|
|
location.name || landing?.destination || landing?.type?.name || null,
|
|
description: landing?.description || null,
|
|
downrangeKm: finiteCoordinate(landing?.downrange_distance),
|
|
lat: finiteCoordinate(location.latitude ?? landing?.latitude),
|
|
lon: finiteCoordinate(location.longitude ?? landing?.longitude),
|
|
};
|
|
}
|
|
|
|
function normalizeRecoveryStages(launch, payloads) {
|
|
const rocket = launch.rocket || {};
|
|
const launcherStages = Array.isArray(rocket.launcher_stage)
|
|
? rocket.launcher_stage
|
|
: [];
|
|
const spacecraftStages = Array.isArray(rocket.spacecraft_stage)
|
|
? rocket.spacecraft_stage
|
|
: [];
|
|
const stages = [
|
|
...launcherStages.map((stage, index) =>
|
|
normalizeLanding(
|
|
stage,
|
|
`Launcher stage ${index + 1}`,
|
|
'LAUNCHER',
|
|
index,
|
|
),
|
|
),
|
|
...spacecraftStages.map((stage, index) =>
|
|
normalizeLanding(
|
|
stage,
|
|
`Spacecraft stage ${index + 1}`,
|
|
'SPACECRAFT',
|
|
index,
|
|
),
|
|
),
|
|
];
|
|
const payloadFlights = rocket.payloads || launch.payloads || [];
|
|
if (Array.isArray(payloadFlights)) {
|
|
payloadFlights.forEach((flight, index) => {
|
|
if (!flight?.landing) return;
|
|
stages.push(
|
|
normalizeLanding(
|
|
{
|
|
...flight,
|
|
type: payloads[index]?.type || 'Payload',
|
|
serial_number: payloads[index]?.name,
|
|
},
|
|
payloads[index]?.name || `Payload ${index + 1}`,
|
|
'PAYLOAD',
|
|
index,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
return stages;
|
|
}
|
|
|
|
/**
|
|
* Select a stable marker color from the mission operator and payload name.
|
|
* Labels stay cyan so the layer remains visually coherent.
|
|
* @param {object} launch Normalized launch record.
|
|
* @returns {Cesium.Color}
|
|
*/
|
|
|
|
function missionMarkerColor(launch) {
|
|
const identity =
|
|
`${launch.provider || ''} ${launch.name || ''} ${launch.missionName || ''}`.toLowerCase();
|
|
if (/nasa|national aeronautics/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#ff9f43');
|
|
if (/starlink|spacex|space exploration/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#4cc9f0');
|
|
if (/rocket lab/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#7bed9f');
|
|
if (/isro|indian space/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#ff66c4');
|
|
if (/cnsa|china national|long march/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#ffd166');
|
|
if (/blue origin/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#a78bfa');
|
|
if (/ula|united launch alliance/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#f97316');
|
|
if (/arianespace|esa|european space/.test(identity))
|
|
return Cesium.Color.fromCssColorString('#60a5fa');
|
|
if (launch.provider) return Cesium.Color.fromCssColorString('#c084fc');
|
|
return Cesium.Color.fromCssColorString('#22e6e6');
|
|
}
|
|
|
|
/**
|
|
* Normalize a Launch Library 2 response into records suitable for the layer.
|
|
* Trajectory points are retained only when the upstream explicitly supplies
|
|
* them; orbital tracks must not be reconstructed from launch metadata.
|
|
* @param {object} payload Launch Library 2-compatible response.
|
|
* @param {Date} [now] Reference time used for the rolling window.
|
|
* @returns {Array<object>}
|
|
*/
|
|
|
|
function normalizeRocketLaunches(payload, now = new Date()) {
|
|
const launches = Array.isArray(payload) ? payload : payload?.results;
|
|
if (!Array.isArray(launches)) return [];
|
|
const cutoff = now.getTime() - WINDOW_DAYS * 86400000;
|
|
return launches
|
|
.map((launch) => {
|
|
const launchTime =
|
|
launch.net || launch.window_start || launch.pad?.location?.name;
|
|
const date = Date.parse(launchTime);
|
|
const pad = launch.pad || {};
|
|
const location = pad.location || {};
|
|
const coordinates = location.coordinates || '';
|
|
const [coordinateLon, coordinateLat] = String(coordinates).split(',');
|
|
const lat =
|
|
finiteCoordinate(pad.latitude) ?? finiteCoordinate(coordinateLat);
|
|
const lon =
|
|
finiteCoordinate(pad.longitude) ?? finiteCoordinate(coordinateLon);
|
|
const payloads = normalizePayloadFlights(launch);
|
|
return {
|
|
id: String(
|
|
launch.id || launch.slug || launch.name || `launch-${date}`,
|
|
),
|
|
name: launch.name || 'Unnamed launch',
|
|
status: launch.status?.name || UNKNOWN_LAUNCH_STATUS,
|
|
launchTime: Number.isFinite(date)
|
|
? new Date(date).toISOString()
|
|
: null,
|
|
launchSite: pad.name || location.name || 'Unknown launch site',
|
|
lat: Number.isFinite(lat) ? lat : null,
|
|
lon: Number.isFinite(lon) ? lon : null,
|
|
provider: launch.launch_service_provider?.name || null,
|
|
mission: launch.mission?.description || null,
|
|
missionName: launch.mission?.name || null,
|
|
satelliteQuery: launch.mission?.name || launch.name || null,
|
|
payloads,
|
|
recoveryStages: normalizeRecoveryStages(launch, payloads),
|
|
trajectory: Array.isArray(launch.trajectory) ? launch.trajectory : [],
|
|
timeline: Array.isArray(launch.timeline)
|
|
? launch.timeline.map((event) => ({
|
|
name:
|
|
event.type?.abbrev ||
|
|
event.type?.name ||
|
|
event.name ||
|
|
'Mission event',
|
|
relativeTime: event.relative_time || event.relativeTime || null,
|
|
offsetSeconds: parts.policyHelpers.parseMissionDurationSeconds(
|
|
event.relative_time || event.relativeTime,
|
|
),
|
|
}))
|
|
: [],
|
|
orbit: launch.mission?.orbit || launch.orbit || null,
|
|
source: 'Launch Library 2',
|
|
inWindow:
|
|
Number.isFinite(date) && date >= cutoff && date <= now.getTime(),
|
|
};
|
|
})
|
|
.filter(
|
|
(launch) =>
|
|
launch.inWindow && launch.lat !== null && launch.lon !== null,
|
|
);
|
|
}
|
|
return {
|
|
finiteCoordinate,
|
|
normalizePayloadFlights,
|
|
normalizeLanding,
|
|
normalizeRecoveryStages,
|
|
missionMarkerColor,
|
|
normalizeRocketLaunches,
|
|
};
|
|
}
|