TALOS v0.5 -- Visualization and Standards¶
Date: April 2026 Scope: CesiumJS integration, HTMX for non-realtime pages, CCSDS OMM and TDM standards adoption Author: Engineering (automated review)
1. CesiumJS 3D Visualization¶
The current TALOS dashboard uses Leaflet.js for a 2D map view of satellite positions and ground station locations. While functional, a 2D projection cannot effectively convey orbital mechanics -- ground track wrap-around, satellite altitude, coverage footprints, and the spatial relationship between stations and satellites are all lost in the flat projection.
CesiumJS provides a WebGL-powered 3D globe that natively supports satellite visualization with orbit trails, ground tracks, coverage cones, and time-dynamic animation.
1.1 CesiumJS Overview¶
| Property | Value |
|---|---|
| Package | cesium (npm) |
| Version | 1.140 (latest stable) |
| License | Apache 2.0 |
| Size | ~30 MB (including assets) |
| Browser support | Chrome, Firefox, Edge, Safari (WebGL 2.0) |
| Key feature | CZML format for time-dynamic data |
1.2 Deployment Strategy¶
CesiumJS will be offered as an opt-in 3D view alongside the existing Leaflet 2D map. The user selects their preferred view via a toggle control. This avoids forcing a heavy WebGL dependency on users with constrained hardware.
Dashboard
|
+-- [2D] Leaflet map (default, existing)
| |-- Satellite markers (lat/lon)
| |-- Ground station markers
| |-- Ground track polylines
|
+-- [3D] CesiumJS globe (opt-in)
|-- Satellite entities with orbit trails
|-- Ground station entities with coverage cones
|-- CZML-driven time animation
|-- Click-to-select satellite info panel
1.3 CZML Format¶
CZML (Cesium Language) is a JSON-based format for describing time-dynamic 3D scenes. The Director generates CZML documents that CesiumJS consumes directly.
CZML document structure:
[
{
"id": "document",
"name": "TALOS Tracking",
"version": "1.0",
"clock": {
"interval": "2026-04-03T00:00:00Z/2026-04-04T00:00:00Z",
"currentTime": "2026-04-03T12:00:00Z",
"multiplier": 1,
"range": "LOOP_STOP",
"step": "SYSTEM_CLOCK_MULTIPLIER"
}
},
{
"id": "satellite/25544",
"name": "ISS (ZARYA)",
"description": "NORAD 25544 -- Active campaign: ISS Tracking",
"availability": "2026-04-03T00:00:00Z/2026-04-04T00:00:00Z",
"position": {
"interpolationAlgorithm": "LAGRANGE",
"interpolationDegree": 5,
"referenceFrame": "INERTIAL",
"epoch": "2026-04-03T00:00:00Z",
"cartographicDegrees": [
0, -33.8688, 151.2093, 420000,
60, -30.0000, 155.0000, 420500,
120, -26.0000, 159.0000, 421000
]
},
"point": {
"pixelSize": 8,
"color": { "rgba": [0, 255, 128, 255] }
},
"path": {
"material": {
"solidColor": { "color": { "rgba": [0, 255, 128, 128] } }
},
"width": 2,
"leadTime": 2700,
"trailTime": 2700,
"resolution": 60
}
},
{
"id": "station/gs-001",
"name": "Athens Ground Station",
"position": {
"cartographicDegrees": [23.7275, 37.9838, 150]
},
"point": {
"pixelSize": 10,
"color": { "rgba": [65, 105, 225, 255] }
},
"ellipsoid": {
"radii": { "cartesian": [500000, 500000, 500000] },
"material": {
"solidColor": { "color": { "rgba": [65, 105, 225, 30] } }
}
}
}
]
1.4 CZML Generation in Python¶
Two libraries enable CZML generation from Python:
czml3 -- Pure Python CZML writer:
from czml3 import Document, Packet, Preamble
from czml3.properties import Position, Point, Path, Clock, Color
from czml3.types import IntervalValue, Sequence
def generate_czml(satellites: list, stations: list,
start_time: datetime, end_time: datetime) -> str:
"""Generate a CZML document for CesiumJS consumption."""
packets = [
Preamble(
name="TALOS Tracking",
clock=Clock(
interval=f"{start_time.isoformat()}Z/{end_time.isoformat()}Z",
currentTime=f"{datetime.utcnow().isoformat()}Z",
multiplier=1,
),
),
]
for sat in satellites:
positions = propagate_orbit(sat.tle, start_time, end_time, step_seconds=60)
packets.append(
Packet(
id=f"satellite/{sat.norad_id}",
name=sat.name,
position=Position(
interpolationAlgorithm="LAGRANGE",
interpolationDegree=5,
epoch=start_time.isoformat() + "Z",
cartographicDegrees=flatten_positions(positions),
),
point=Point(pixelSize=8, color=Color(rgba=[0, 255, 128, 255])),
path=Path(
width=2,
leadTime=2700,
trailTime=2700,
resolution=60,
),
)
)
for station in stations:
packets.append(
Packet(
id=f"station/{station.id}",
name=station.name,
position=Position(
cartographicDegrees=[
station.longitude, station.latitude, station.altitude
],
),
point=Point(pixelSize=10, color=Color(rgba=[65, 105, 225, 255])),
)
)
doc = Document(packets)
return doc.dumps()
poliastro -- Full astrodynamics library with CZML export:
poliastro provides orbit propagation and CZML export in a single package. However, it is a heavy dependency (~100 MB with SciPy). For TALOS, czml3 (pure Python, ~50 KB) is preferred for CZML generation, keeping poliastro as an optional dependency for advanced analysis.
1.5 API Endpoint¶
GET /api/v1/org/{slug}/czml
Query params:
- hours: prediction horizon (default: 2)
- satellites: comma-separated NORAD IDs (default: all active)
Response: CZML JSON document
Content-Type: application/json
The endpoint generates CZML on demand. Caching with a 30-second TTL prevents redundant computation.
1.6 Frontend Integration¶
<!-- CesiumJS 3D view (opt-in) -->
<div id="cesium-container" style="display: none;">
<div id="cesiumWidget"></div>
</div>
<script>
async function loadCesiumView(orgSlug) {
const response = await fetch(`/api/v1/org/${orgSlug}/czml`);
const czml = await response.json();
const viewer = new Cesium.Viewer("cesiumWidget", {
shouldAnimate: true,
terrainProvider: Cesium.createWorldTerrain(),
});
const dataSource = new Cesium.CzmlDataSource();
await dataSource.load(czml);
viewer.dataSources.add(dataSource);
viewer.zoomTo(dataSource);
}
</script>
2. HTMX for Non-Realtime Pages¶
The real-time dashboard requires WebSocket or SSE updates, but most TALOS pages (station list, campaign management, member management, settings) are standard CRUD interfaces that do not need real-time updates. These pages can be simplified with HTMX.
2.1 HTMX Overview¶
| Property | Value |
|---|---|
| Package | htmx.org (npm/CDN) |
| Version | 2.0 |
| Size | ~14 KB (gzipped) |
| License | BSD 2-Clause |
| Key feature | HTML-over-the-wire with attribute-driven AJAX |
HTMX replaces client-side JavaScript with HTML attributes that trigger server-side rendering. The server returns HTML fragments instead of JSON, and HTMX swaps them into the DOM.
2.2 FastAPI Partial Response Pattern¶
from fastapi import Request
from fastapi.responses import HTMLResponse
@router.get("/org/{slug}/stations", response_class=HTMLResponse)
async def stations_page(request: Request, slug: str):
stations = await get_stations(slug)
if request.headers.get("HX-Request"):
# HTMX request: return only the station list fragment
return templates.TemplateResponse(
"fragments/station_list.html",
{"request": request, "stations": stations},
)
else:
# Full page request: return complete page with layout
return templates.TemplateResponse(
"pages/stations.html",
{"request": request, "stations": stations},
)
2.3 Template Example¶
Full page (pages/stations.html):
{% extends "layout.html" %}
{% block content %}
<h1>Stations</h1>
<div id="station-list">
{% include "fragments/station_list.html" %}
</div>
<button hx-post="/org/{{ slug }}/stations/provision"
hx-target="#station-list"
hx-swap="innerHTML">
Provision Station
</button>
{% endblock %}
Fragment (fragments/station_list.html):
<table class="rux-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Last Heartbeat</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for station in stations %}
<tr>
<td>{{ station.name }}</td>
<td>{{ station.status }}</td>
<td>{{ station.last_heartbeat }}</td>
<td>
<button hx-delete="/org/{{ slug }}/stations/{{ station.id }}"
hx-target="closest tr"
hx-swap="outerHTML"
hx-confirm="Delete station {{ station.name }}?">
Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
2.4 Pages Suitable for HTMX¶
| Page | Current Approach | HTMX Benefit |
|---|---|---|
| Station list | Full page reload on action | Inline add/remove without reload |
| Campaign management | Full page reload | Inline status changes, assignment edits |
| Member management | Full page reload | Inline role changes, member removal |
| Organization settings | Full page reload | Inline save with feedback |
| Admin panel | Full page reload | Inline network/station/campaign actions |
2.5 Pages NOT Suitable for HTMX¶
| Page | Reason |
|---|---|
| Real-time dashboard | Needs WebSocket/SSE for 2 Hz updates |
| Public satellite tracker | Needs continuous map updates |
| CesiumJS 3D view | WebGL rendering, not HTML |
3. CCSDS 502.0 -- Orbit Mean-Elements Message (OMM)¶
The Consultative Committee for Space Data Systems (CCSDS) defines standard formats for space data interchange. CCSDS 502.0 (OMM) standardizes the representation of satellite orbital elements -- the same data currently stored as raw TLE strings.
3.1 Why OMM¶
| Concern | Raw TLE | OMM |
|---|---|---|
| Catalog number overflow | 5-digit limit (breaks July 2026) | Integer field (no limit) |
| Machine readability | Fixed-width text parsing required | JSON or XML structured data |
| Metadata | None (just two lines of numbers) | Object name, ID, originator, message epoch |
| Industry alignment | Legacy format from 1960s | Current CCSDS standard (2009, updated 2022) |
| CelesTrak compatibility | Requires format conversion | Native GP API format |
3.2 OMM Data Fields¶
The OMM message contains all information present in a TLE plus additional metadata:
| Field | TLE Equivalent | Description |
|---|---|---|
OBJECT_NAME |
Line 0 | Satellite name |
OBJECT_ID |
International designator | Launch year, number, piece |
NORAD_CAT_ID |
Line 1, cols 3-7 | Catalog number (integer) |
EPOCH |
Line 1, cols 19-32 | Element set epoch |
MEAN_MOTION |
Line 2, cols 53-63 | Revolutions per day |
ECCENTRICITY |
Line 2, cols 27-33 | Orbital eccentricity |
INCLINATION |
Line 2, cols 9-16 | Orbital inclination (degrees) |
RA_OF_ASC_NODE |
Line 2, cols 18-25 | Right ascension of ascending node |
ARG_OF_PERICENTER |
Line 2, cols 35-42 | Argument of perigee |
MEAN_ANOMALY |
Line 2, cols 44-51 | Mean anomaly |
BSTAR |
Line 1, cols 54-61 | Drag term |
ELEMENT_SET_NO |
Line 1, cols 65-68 | Element set number |
REV_AT_EPOCH |
Line 2, cols 64-68 | Revolution number at epoch |
3.3 Migration Path¶
- Phase 1 (v0.5): Add OMM JSON support alongside existing TLE format. CelesTrak fallback uses OMM natively. Internal storage remains TLE strings.
- Phase 2 (v0.6): Migrate internal TLE storage to OMM JSON. Update
TLEManagerto work with OMM objects. Update Skyfield and dSGP4 backends to accept OMM input. - Phase 3 (pre-July 2026): Remove raw TLE string storage. All orbital data flows through OMM format.
3.4 OMM Pydantic Model¶
from pydantic import BaseModel, Field
from datetime import datetime
class OMMMessage(BaseModel):
"""CCSDS 502.0 Orbit Mean-Elements Message."""
object_name: str = Field(..., alias="OBJECT_NAME")
object_id: str = Field(..., alias="OBJECT_ID")
norad_cat_id: int = Field(..., alias="NORAD_CAT_ID")
epoch: datetime = Field(..., alias="EPOCH")
mean_motion: float = Field(..., alias="MEAN_MOTION")
eccentricity: float = Field(..., alias="ECCENTRICITY")
inclination: float = Field(..., alias="INCLINATION")
ra_of_asc_node: float = Field(..., alias="RA_OF_ASC_NODE")
arg_of_pericenter: float = Field(..., alias="ARG_OF_PERICENTER")
mean_anomaly: float = Field(..., alias="MEAN_ANOMALY")
bstar: float = Field(..., alias="BSTAR")
element_set_no: int = Field(..., alias="ELEMENT_SET_NO")
rev_at_epoch: int = Field(..., alias="REV_AT_EPOCH")
mean_motion_dot: float = Field(0.0, alias="MEAN_MOTION_DOT")
mean_motion_ddot: float = Field(0.0, alias="MEAN_MOTION_DDOT")
class Config:
populate_by_name = True
def to_tle_lines(self) -> tuple[str, str]:
"""Convert OMM back to TLE format for legacy consumers."""
# Only valid for catalog numbers <= 99999
if self.norad_cat_id > 99999:
raise ValueError(
f"Catalog number {self.norad_cat_id} exceeds TLE 5-digit limit"
)
# ... TLE formatting logic ...
4. CCSDS 503.0 -- Tracking Data Message (TDM)¶
CCSDS 503.0 defines a standard format for tracking measurement data -- azimuth, elevation, range, Doppler, and signal metrics. This is the natural export format for TALOS telemetry data.
4.1 TDM Overview¶
A TDM document contains:
- Header: Metadata about the tracking session.
- Data segments: Time-tagged measurement records.
4.2 TDM Structure (Keyword-Value Notation)¶
CCSDS_TDM_VERS = 2.0
CREATION_DATE = 2026-04-03T12:00:00.000
ORIGINATOR = TALOS
META_START
PARTICIPANT_1 = ISS (ZARYA)
PARTICIPANT_2 = ATHENS-GS-001
START_TIME = 2026-04-03T10:15:00.000
STOP_TIME = 2026-04-03T10:25:00.000
TIME_SYSTEM = UTC
ANGLE_TYPE = AZEL
META_STOP
DATA_START
ANGLE_1 = 2026-04-03T10:15:00.000 45.123
ANGLE_2 = 2026-04-03T10:15:00.000 12.456
DOPPLER_INSTANTANEOUS = 2026-04-03T10:15:00.000 -1234.5
ANGLE_1 = 2026-04-03T10:15:00.500 45.234
ANGLE_2 = 2026-04-03T10:15:00.500 12.789
DOPPLER_INSTANTANEOUS = 2026-04-03T10:15:00.500 -1230.1
DATA_STOP
4.3 TDM Export Endpoint¶
GET /api/v1/org/{slug}/campaigns/{campaign_id}/tdm
Query params:
- station_id: specific station (optional)
- start: ISO 8601 start time
- end: ISO 8601 end time
- format: "kvn" (keyword-value, default) or "xml"
Response: TDM document
Content-Type: text/plain (KVN) or application/xml (XML)
4.4 Python TDM Generator¶
from datetime import datetime
from io import StringIO
def generate_tdm_kvn(campaign_name: str, station_name: str,
satellite_name: str,
measurements: list[TrackingMeasurement]) -> str:
"""Generate a CCSDS 503.0 TDM document in Keyword-Value Notation."""
buf = StringIO()
# Header
buf.write("CCSDS_TDM_VERS = 2.0\n")
buf.write(f"CREATION_DATE = {datetime.utcnow().isoformat()}\n")
buf.write("ORIGINATOR = TALOS\n\n")
# Metadata
buf.write("META_START\n")
buf.write(f"PARTICIPANT_1 = {satellite_name}\n")
buf.write(f"PARTICIPANT_2 = {station_name}\n")
if measurements:
buf.write(f"START_TIME = {measurements[0].timestamp.isoformat()}\n")
buf.write(f"STOP_TIME = {measurements[-1].timestamp.isoformat()}\n")
buf.write("TIME_SYSTEM = UTC\n")
buf.write("ANGLE_TYPE = AZEL\n")
buf.write("META_STOP\n\n")
# Data
buf.write("DATA_START\n")
for m in measurements:
ts = m.timestamp.isoformat()
buf.write(f"ANGLE_1 = {ts} {m.azimuth:.3f}\n")
buf.write(f"ANGLE_2 = {ts} {m.elevation:.3f}\n")
if m.doppler_shift_hz is not None:
buf.write(f"DOPPLER_INSTANTANEOUS = {ts} {m.doppler_shift_hz:.1f}\n")
buf.write("DATA_STOP\n")
return buf.getvalue()
5. Implementation Priority¶
| Feature | Version | Effort | Dependency |
|---|---|---|---|
| CelesTrak OMM JSON support | v0.5 | 2-3 days | None |
| OMM Pydantic model | v0.5 | 1 day | None |
| TDM export endpoint | v0.5 | 2-3 days | TimescaleDB telemetry |
| HTMX station list | v0.6 | 1-2 days | None |
| HTMX campaign management | v0.6 | 2-3 days | None |
| CesiumJS integration | v0.6 | 5-7 days | CZML generation |
| CZML generation endpoint | v0.6 | 3-4 days | None |
| OMM internal storage migration | v0.6 | 3-5 days | OMM model |
| Full OMM migration (remove TLE strings) | pre-July 2026 | 2-3 days | OMM internal storage |
6. Dependencies¶
| Package | Version | Purpose | Install size |
|---|---|---|---|
cesium |
>= 1.140 | 3D globe visualization | ~30 MB (frontend asset) |
czml3 |
>= 1.0 | CZML document generation | ~50 KB |
htmx.org |
>= 2.0 | HTML-over-the-wire AJAX | ~14 KB (gzipped) |
poliastro |
>= 0.18 | Advanced astrodynamics (optional) | ~100 MB (with SciPy) |
CesiumJS is a frontend-only dependency served as a static asset. czml3 and htmx.org are lightweight. poliastro is optional and only needed for advanced orbit analysis beyond what Skyfield provides.
Summary¶
The visualization and standards roadmap for v0.5-v0.6 addresses three distinct gaps: richer orbital visualization (CesiumJS), simpler CRUD page interactions (HTMX), and industry-standard data interchange (CCSDS OMM/TDM). CesiumJS is the highest-value addition for operator experience, but the OMM migration is the most time-critical due to the July 2026 catalog number overflow. HTMX is low-risk incremental improvement that can be applied page by page without architectural changes.