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:
- it fits the SDK's existing
HonuaClient.queryFeatures()surface - it only needs one bounded polyline query plus optional terrain
- it demonstrates the difference between "Cesium can consume Honua-served data" and "Honua already exposes a first-class 3D scene contract"
What The Example Includes
index.html: a plain browser page that loads local Cesium assetsapp.mjs: viewer wiring, diagnostics, playback controls, and optional terraindata-path.mjs: explicit query and normalization helpersfixtures/route-query-response.json: deterministic Honua query fixture for CIfixtures/source-manifest.json: query shape, field mapping, and preprocessing notes
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.
mode:fixtureby default. Useliveto query a Honua server from the browser.baseUrl: required in live mode. Trailing slashes are trimmed beforeHonuaClientis created.serviceId: defaults toroute-playback-demo.layerId: defaults to0.routeId: optional explicit route identifier used only for client-side feature selection when a live query can still return multiple polylines. It does not narrow the server request by itself, so prefer pairing it with a tighterwhereorobjectIdsfilter when possible. If you rely onrouteIdto select among multiple returned routes, widenresultRecordCountbeyond the default1so the target feature can actually be returned. The matcher compares normalized string values, including numeric ids coerced to strings, across the configured route-id field when one is known, otherwise it falls back to the example's common route-id aliases. If no returned polyline matches, the example throws instead of falling back to another feature. The normalized result summary also prefers that configured field when reportingrouteId, and leavesrouteIdasnullwhen the selected feature has no route-id attribute at all.routeIdField: optional live-mode route-id attribute name. Set this when the live layer stores route ids outside the example's built-in aliases likeroute_idorrouteId. When provided, live route matching and normalizedrouteIdoutput treat that configured field as authoritative instead of falling back to alias fields first.where: defaults to1=1.objectIds: optional live-mode filter passed through to the query request.resultRecordCount: defaults to1so the live query stays intentionally bounded.speed: playback speed in meters per second. Default:18.fixtureUrl: fixture payload URL. Default:./fixtures/route-query-response.json.manifestUrl: fixture-mode manifest URL. Default:./fixtures/source-manifest.json. Live mode ignores this and synthesizes a minimal manifest from the live query parameters instead.terrainUrl: optional Cesium terrain endpoint. Tried beforeionToken.ionToken: optional Cesium ion token used only whenterrainUrlis absent or fails.
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:
terrainUrl=https://terrain.example/tiles- or
ionToken=<token>to use Cesium World Terrain
Notes:
- live mode calls
checkCompatibility()before querying features - the query is intentionally bounded and always uses
outFields=["*"],outSr=4326,returnGeometry=true, andextraParams={ outSr: 4326, returnZ: true } - the live
queryRequestechoed in diagnostics andwindow.__cesiumRoutePlaybackResultis that exact boundedHonuaClient.queryFeatures()input:serviceId,layerId,where, optionalobjectIds,outFields,outSr,returnGeometry,resultRecordCount, andextraParams={ outSr: 4326, returnZ: true } - the checked-in fixture manifest mirrors that same bounded query shape and
adds manifest-only
baseUrlandrouteIdValuenotes for reproducible local review - the echoed live request intentionally omits
routeIdandrouteIdField; those stay in the synthetic manifest as post-query selectors only - if the live layer can still return multiple polyline features, pass
routeId, widenresultRecordCountenough to return that feature, setrouteIdFieldwhen the identifier lives in a custom attribute, or narrowwhere/objectIds; the example errors instead of choosing one heuristically - the request duration shown in diagnostics comes from a temporary
HonuaClientinterceptor attached by the example - CORS still has to allow the browser request when
baseUrlis cross-origin
Response Contract And Normalization
The example keeps the conversion path explicit:
HonuaClient.checkCompatibility()gates live mode through the existing SDK compatibility contract.HonuaClient.queryFeatures()reads one bounded polyline route withreturnGeometry=true,outSr=4326, andreturnZ=true.- The query response must expose
features[]entries with polylinegeometry.paths. Non-polyline features are ignored. - In live mode,
data-path.mjssynthesizes a minimal manifest from the bounded query request and URL parameters. It copiesrouteIdintomanifest.query.routeIdValueand 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,routeIdFieldseedsfieldMapping.routeIdwhen the layer uses a nonstandard attribute name, and matching treats that configured field as authoritative; withoutrouteIdField, 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. - Multipart polylines are reduced to the physically longest path by measured segment distance, then normalized into Cesium-friendly WGS84 coordinates.
- Route labels fall back through
route_name,routeName,name, andName. Route ids first honormanifest.fieldMapping.routeIdwhen it is set, then fall back throughroute_id,routeId,ROUTE_ID, andName. If none of those attributes are present on the selected feature, the normalized summary leavesrouteIdasnullinstead of inventing a placeholder. - If source Z values exist, the example preserves them for display when terrain is disabled.
- If terrain is configured, the example samples external terrain and uses that as the display height while keeping source Z for diagnostics only.
- Playback timestamps are derived from cumulative segment distance at the configured speed. The example does not currently consume route time fields.
- Playback starts from
manifest.playback.startTimestampwhen the manifest provides one, otherwise it falls back to2026-01-01T00:00:00Z. - 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:
- query in WGS84 degrees because Cesium expects longitude/latitude input
- provide
routeIdor another query bound whenever a live route query can still return multiple polyline features, and raiseresultRecordCountwhen the target route might not be the first returned feature - provide
routeIdFieldwhen the live route identifier is not exposed through the example's built-in route-id aliases - normalize Esri polyline paths into one playback track by selecting the physically longest path
- decide how to treat source Z values when their units are not confirmed
- derive timestamps client-side instead of consuming route time attributes
- supply an external terrain source if topographic context matters
Terrain And Height Handling
Height handling is ordered:
terrainUrlionToken- ellipsoid fallback
When terrain is disabled:
- source Z values produce
heightMode=source-z-unverified - missing Z values produce
heightMode=ellipsoid-zero
When terrain sampling succeeds:
- display heights come from sampled terrain
- source Z values are kept for diagnostics only
When terrain sampling fails:
- the example falls back to the non-terrain height mode
- the failure is surfaced in the warnings list and result summary
Gap Analysis
What worked directly:
- current
HonuaClientquery and compatibility surfaces were enough for a small browser prototype - Cesium can render a believable moving-route workflow from a Honua-shaped polyline response without changing SDK exports
- the repo's existing browser smoke-test lane can validate both fixture and live code paths
What is still missing:
- no documented Honua terrain, 3D Tiles, or I3S contract in this repo
- no approved stable public route dataset is referenced here, so fixture mode is the canonical reproducible path
- source Z units are not validated by contract, so the example must label them as unverified display heights
- Cesium setup still requires local asset serving and optional terrain configuration outside the SDK runtime surface
Diagnostics And Verification
The example surfaces its runtime state in two places:
- the page diagnostics panel shows source mode, selected query, geometry type, feature and vertex counts, source-Z presence, height mode, terrain mode, request duration, playback distance, and playback duration
- the warnings panel shows terrain fallback and load failures directly
For browser smoke tests and local inspection, the example also exposes:
window.__cesiumRoutePlaybackDonewindow.__cesiumRoutePlaybackErrorwindow.__cesiumRoutePlaybackResult
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:
sourceMode,routeName, nullablerouteId,featureCount,vertexCount, andhasZterrainEnabled,terrainMode, andheightModecompatibilitySupportedandrequestDurationMs, bothnullin fixture modequeryRequest(fixture manifest query in fixture mode, live request in live mode),warnings,preprocessingSteps, andentityCount
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:
- it proves a credible consumer-side 3D story for one workflow today
- it does not yet justify claiming first-class 3D parity for Honua itself
- the strongest missing pieces are platform contracts and demo-data governance, not SDK syntax
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
honua-sdk-js: extract the route-normalization helpers from this example only if another maintained sample needs the same conversion pathhonua-server: publish one documented, stable, Z-aware demo route layer for browser sampleshonua-server: define a terrain or 3D scene contract before any portfolio claim of near-parity 3D support