Cesium Route Playback Spike

This example is an exploratory 3D consumer workflow for honua-sdk-js. It does not change the public SDK runtime surface. The goal is narrower: prove that a current Honua FeatureServer/query response can drive one believable Cesium workflow end to end, while documenting the manual conversion steps and the gaps that still block a stronger platform-level 3D claim.

Scenario

The spike uses route playback with elevation context.

Why this scenario:

What The Example Includes

Run It

From the repo root:

# Node.js 20.19.0+; `.nvmrc` pins the reviewed repo floor
npm install
npm run build
python3 -m http.server 8080

Open:

http://127.0.0.1:8080/docs/examples/cesium-route-playback/

Fixture mode is the default. It renders the checked-in Honua query payload and keeps local review and CI deterministic.

Runtime Defaults And URL Parameters

The browser page reads its configuration from the query string.

Live Mode

Live mode uses the same example, but switches the source from the checked-in fixture to a user-supplied Honua server and feature layer:

http://127.0.0.1:8080/docs/examples/cesium-route-playback/?mode=live&baseUrl=https%3A%2F%2Fyour-honua-server.example&serviceId=transport&layerId=0&routeId=route-playback-demo&where=route_id%20%3D%20'route-playback-demo'

Optional terrain:

Notes:

Response Contract And Normalization

The example keeps the conversion path explicit:

  1. HonuaClient.checkCompatibility() gates live mode through the existing SDK compatibility contract.
  2. HonuaClient.queryFeatures() reads one bounded polyline route with returnGeometry=true, outSr=4326, and returnZ=true.
  3. The query response must expose features[] entries with polyline geometry.paths. Non-polyline features are ignored.
  4. In live mode, data-path.mjs synthesizes a minimal manifest from the bounded query request and URL parameters. It copies routeId into manifest.query.routeIdValue and uses it only for post-query client-side feature selection. It selects the polyline feature whose configured route id matches after string normalization, including numeric route ids that need to be coerced to strings. In live mode, routeIdField seeds fieldMapping.routeId when the layer uses a nonstandard attribute name, and matching treats that configured field as authoritative; without routeIdField, the selector falls back through the example's route-id aliases. If no returned polyline matches the configured route id, or if multiple polyline features remain without a configured route id, the example throws instead of guessing.
  5. Multipart polylines are reduced to the physically longest path by measured segment distance, then normalized into Cesium-friendly WGS84 coordinates.
  6. Route labels fall back through route_name, routeName, name, and Name. Route ids first honor manifest.fieldMapping.routeId when it is set, then fall back through route_id, routeId, ROUTE_ID, and Name. If none of those attributes are present on the selected feature, the normalized summary leaves routeId as null instead of inventing a placeholder.
  7. If source Z values exist, the example preserves them for display when terrain is disabled.
  8. If terrain is configured, the example samples external terrain and uses that as the display height while keeping source Z for diagnostics only.
  9. Playback timestamps are derived from cumulative segment distance at the configured speed. The example does not currently consume route time fields.
  10. Playback starts from manifest.playback.startTimestamp when the manifest provides one, otherwise it falls back to 2026-01-01T00:00:00Z.
  11. Cesium entities render the route line, start/end markers, and a moving asset driven by SampledPositionProperty.

Required Preprocessing Today

Nothing is hidden behind SDK internals. The example still needs consumer-side preprocessing:

Terrain And Height Handling

Height handling is ordered:

  1. terrainUrl
  2. ionToken
  3. ellipsoid fallback

When terrain is disabled:

When terrain sampling succeeds:

When terrain sampling fails:

Gap Analysis

What worked directly:

What is still missing:

Diagnostics And Verification

The example surfaces its runtime state in two places:

For browser smoke tests and local inspection, the example also exposes:

window.__cesiumRoutePlaybackDone flips to true on both success and failure so smoke tests can wait on completion before inspecting the error or result.

window.__cesiumRoutePlaybackError stays null on success and carries the terminal failure message when the example cannot finish loading.

window.__cesiumRoutePlaybackResult is populated only on success and remains null on failure. On success it includes:

The diagnostics panel uses the same queryRequest source: fixture mode echoes fixtures/source-manifest.json#query, which mirrors the bounded live request shape with outFields=["*"], outSr=4326, returnGeometry=true, and extraParams={ outSr: 4326, returnZ: true } plus manifest-only baseUrl and routeIdValue; live mode echoes the bounded queryFeatures() request sent through HonuaClient. In live mode that echoed request does not include routeId or routeIdField; those stay in the synthetic manifest for post-query selection only. The same success summary split applies to the browser globals: completion always lands on window.__cesiumRoutePlaybackDone, failures populate only window.__cesiumRoutePlaybackError, and successes populate only window.__cesiumRoutePlaybackResult.

Verification commands:

npm run build
npx vitest run test/cesium-route-playback.test.ts
npx playwright test test/playwright/cesium-route-playback.spec.mjs

The Playwright lane validates both the default fixture path and the live-query path against a local mock Honua server, including the compatibility request and the returnZ=true query contract.

Recommendation

Keep this example exploratory.

Reasoning:

Promotion to a portfolio demo should wait for a stable route dataset and a documented terrain or 3D scene contract.

Bounded Follow-On Child Tickets