Skip to content

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

  1. Phase 1 (v0.5): Add OMM JSON support alongside existing TLE format. CelesTrak fallback uses OMM natively. Internal storage remains TLE strings.
  2. Phase 2 (v0.6): Migrate internal TLE storage to OMM JSON. Update TLEManager to work with OMM objects. Update Skyfield and dSGP4 backends to accept OMM input.
  3. 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.