The arrival ground guard replaced camera.flyTo, flyToBoundingSphere,
setView, lookAt and cancelFlight with wrappers so a direct caller would stop
it. It now remembers the pose it started on and, before each retry, yields
if a flight is under way or the camera has moved across the ground, down or
around. A straight rise is not a move: Cesium's terrain collision lifts a
buried eye that way as tiles load, which is the case the guard exists for.
This also catches owners the wrappers missed (a pose written directly,
lookAtTransform), and no camera method is replaced.
Recent imagery and weather overwrote provider.requestImage (weather also
provider.credit) to ration tiles. They now state that policy as a hook with
withTileRequestHook(provider, { requestImage, credit }) from
src/render/cesium/tileRequests.js, which hands Cesium a provider delegating
to the original and leaves the original untouched.
Every private Cesium field Core reaches goes through a named helper in
src/render/cesium/internals.js: the main and offscreen pick views and their
command extents, a billboard's collection, a cluster's point collection,
the widget's render-loop latch, the WebGL context and the current camera
flight. internals.test.mjs fails when the installed Cesium stops declaring
one of them, and a source scan fails on private access anywhere else. The
cyber sonar's primitives.update wrap stays until the effects work.
Live (Austin, keyless globe): a buried eye is lifted to 253 m over a 133 m
surface; a setView or flyTo by another caller makes the guard yield; radar
loads its tiles through the hook. Documented in docs/APPLICATION.md and
docs/CURRENT-STATE.md.
18 KiB
Application construction
gods-eye-view/application exports createApplication. Importing it does not
create a viewer, discover configuration, start requests, or attach browser
listeners. Construction is also inactive until the caller invokes start().
The caller supplies four constructors, in this order:
| Constructor | Receives | Standalone implementation |
|---|---|---|
createScene |
signal, defer |
Viewer, attribution and initial map |
createControls |
scene, signal, defer |
Style manager and camera presentation |
createData |
scene, controls, signal, defer |
Layer catalog, registration and restoration |
createTools |
scene, controls, data, signal, defer |
Scenes, annotations, voice and page listeners |
Each constructor returns its component object, or a promise for that object. Those objects are passed unchanged to later constructors. Configuration and service instances belong in the caller's closures; the application does not interpret provider names, environment variables, endpoints, or module paths. There is no module discovery or automatic import mechanism.
import { createApplication } from 'gods-eye-view/application';
import { createApplicationViewer } from 'gods-eye-view/application/viewer';
const app = createApplication({
createScene({ defer }) {
const viewer = createApplicationViewer({ container, creditContainer });
defer(() => viewer.destroy());
return { viewer };
},
createControls,
createData,
createTools,
});
const unsubscribe = app.subscribe(({ status, phase }) => {
// Update the caller's startup presentation.
});
await app.start();
// When the application is no longer needed:
unsubscribe();
await app.destroy();
The example's containers and three remaining constructors are supplied by the consumer. The viewer helper preserves the standalone viewer's render settings; it neither selects map sources nor reads keys. Supply a visible credit container and retain all attribution required by the chosen sources. Cesium is external to this export: the consuming build must provide the same instance used elsewhere in its application.
Lifecycle and ownership
Register defer(cleanup) immediately after acquiring each resource, before any
await. Cleanup registration remains open until that constructor settles. Each
callback may return a promise. Within a phase, callbacks run in reverse order.
Across phases, teardown runs tools, controls, data, scene: controls must cancel
restoration while their data manager and viewer are still alive.
start() returns the same promise on repeated calls. destroy() is terminal and
also returns the same promise on repeated calls. It aborts the shared signal
immediately, waits for an in-flight constructor to settle, and runs all registered
cleanup callbacks. Constructors must forward the signal to cancellable operations
and check it after awaits. If a provider cannot cancel construction, register
cleanup for its late result before returning. Destruction waits for that result;
it does not pretend a pending resource has already been released.
A constructor failure triggers the same cleanup before rejecting startup.
Cleanup failures do not stop the remaining callbacks; they are reported through
an AggregateError. Constructors own cleanup for resources created internally,
including anything allocated before their own construction throws.
getState() returns a frozen { status, phase } snapshot. Status is created,
starting, ready, destroying, destroyed, or failed. phase identifies the
constructor during startup. subscribe(listener) immediately reports the current
snapshot and returns an unsubscribe function. Install observers before start()
to receive every startup phase. Observer failures cannot interrupt startup or
teardown. getComponents() returns a frozen, shallow snapshot of the currently
constructed component objects; the component instances themselves remain mutable.
ready means all constructors have returned. Background feed activity and the
standalone share-restoration result keep their existing separate contracts.
Pointer input
gods-eye-view/input exports inputFor(viewer), the scene's one input
dispatcher. Every layer and tool takes its pointer input from it, and so can
the caller: one ScreenSpaceEventHandler per scene, and a click picked once
and handed to every subscriber.
import { inputFor } from 'gods-eye-view/input';
const input = inputFor(viewer);
const off = input.onClick((event) => {
if (event.ownerId === 'flights') showFlight(event.id);
});
// Later:
off();
onClick runs while no tool holds the pointer (the draw tool, the imagery
box), so a vertex placed on an aircraft is not also a click on it. Its event
reads lazily; a subscriber that reads nothing costs no pick:
| Member | What it is |
|---|---|
screenPosition |
Where the user clicked, in CSS pixels; for DOM and overlay hit-tests |
position |
Where that lands in the pick buffer (corrected under sensor styles) |
gesture |
{ travelPx, durationMs } of the press, to tell a click from a drag |
pick |
scene.pick at position |
hit |
pick, or on a coarse pointer the contact under a surface pick |
id, ownerId |
The hit's pick id and the layer that registered it, or null |
meta |
The hit's own metadata, when its pick carries any |
surface |
Whether the hit is the map itself |
drill(limit) |
scene.drillPick, shared by every subscriber of the click |
pickWindow(w, h) |
scene.pick over a w×h window around position |
ground() |
The world position under the click, or null over the sky |
onPointer(type, fn, { lease }) subscribes to down, move, up, click
or dblclick; with a lease it runs only while that lease is current. A tool
takes its lease with claimPointer(ownerId) from the same export (and gives
it back with releasePointer(lease)), which also keeps every ambient
subscriber off its clicks; isPointerFree() and pointerOwner() report who
holds it. suspendCamera(reason) and suppressStockClicks() return their
own release: camera input and the viewer's stock select and double-click-track
come back when the last hold is released, so two holders cannot undo each
other. The dispatcher owns the camera controller's enableInputs: a direct
write (Cesium's camera flights make one when they start and end) is kept, but
reads true only while no hold is out. A caller-supplied createScene that creates its own viewer registers
defer(() => inputFor(viewer).destroy()) right after it; the standalone scene
does this and returns the dispatcher as scene.input.
Height queries
gods-eye-view/queries/heights exports heightsFor(scene), the one place
height queries go. Layers, tools and the camera ask it instead of calling
scene.sampleHeight, globe.getHeight, scene.clampToHeight or the
*MostDetailed variants themselves, so a renderer that answers heights another
way plugs in without touching them.
import { heightsFor } from 'gods-eye-view/queries/heights';
const heights = heightsFor(viewer.scene);
const ground = heights.now({ lon, lat }, { exclude: [ownMarkers] });
if (ground !== null) place(ground);
| Member | What it answers |
|---|---|
now(point, opts) |
Height in metres from what is loaded, or null |
clampNow(position, opts) |
The Cartesian3 moved onto the drawn surface, or null |
batch(points, opts) |
A promise of heights once the most detailed tiles stream; null per miss |
clampBatch(positions, opts) |
A promise of clamped copies of the positions; null per miss |
supported() |
{ surface, clamp }: whether the drawn surface can be queried at all |
A point is { lon, lat } in degrees or a Cesium.Cartographic in radians.
against: 'surface' (the default) reads what is drawn, 3D tiles and globe;
against: 'terrain' reads the loaded terrain tiles alone. exclude lists the
primitives and entities a query sees through. Every miss is null, including
a Cesium throw for missing depth-texture support, and every caller treats it
as one. Cesium answers now() synchronously from the scene, so it costs what
the direct call did.
Cesium internals
Core reaches private Cesium fields only through
src/render/cesium/internals.js, one named helper per field (the main and
offscreen pick views, a billboard's collection, a cluster's point collection,
the widget's render-loop latch, the WebGL context and whether a camera flight
is under way). internals.test.mjs fails when the installed Cesium stops
declaring one of them, and when a private field is reached anywhere else.
Imagery that rations its tile requests states the policy as a hook with
withTileRequestHook(provider, { requestImage, credit }) from
src/render/cesium/tileRequests.js, which leaves the provider untouched.
Standalone wiring
src/main.js reads the existing browser configuration and starts
src/standalone/application.js. That module selects the four implementations
in its directory. Scene setup, controls, layer registration, tools and loading
chrome have separate owners. The existing window.__godsEyeView debugging shape
is preserved while the app is running.
The standalone controls and layer modules still contain page-scoped state. Only one standalone application may be constructed per page. Shutdown releases its runtime resources and debugging handles; reload the page to start again. It is not an embeddable, removable HTML shell or a multiple-viewer implementation. Consumers of the small application export supply their own component ownership; the lifecycle controller itself has no shared instance state.
Later component extractions can replace a constructor's internals without changing the startup contract or moving standalone imports into the package. Run the unit suite, package boundary gate, build, tracking and first-run browser checks when changing this wiring.
Geospatial services
src/search owns forward/reverse geocoding, place search and route provider
interfaces. createStandaloneApplication({ geospatial }) accepts endpoints
and providers. Default forward search tries decimal coordinates and bundled
place names (presets) first, without a network request, then Google when a key
is configured, Photon, and the local /api/geocode Nominatim route. The
defaults also keep Google place context and the local OSRM route proxy.
Location, annotations, HUD labels and voice use the composed service.
Provider-specific credentials belong in the selected transport; server secrets
never belong in browser configuration.
For a compatible protocol, configure geocode, photon, nominatim, reverse,
textSearch, nearby or route endpoints. These are developer-selected
configuration, not URLs accepted from page queries or model arguments. A different protocol supplies
an adapter function instead. The exported createDefaultPlaceSearch constructor
supports the same options for direct composition:
const services = createDefaultPlaceSearch({
resolveApiKey,
signal,
endpoints: { photon: 'https://search.example/api/', route: '/api/routes' },
providers: {
route: routeProvider,
routeProfiles: ['foot', 'car'],
},
});
Forward providers.geocode is the existing ordered geocoder array. Other
operations are independent functions: reverseGeocode(latitude, longitude, { signal }), textSearch(query, point, { signal }), nearby(point, { signal })
and route(coordinates, profile, { signal }). Points use latitude, longitude
and radiusM; route coordinates use [longitude, latitude] in WGS84. Routes
return { geometry, distanceM, durationS } with metres and seconds. Unavailable
routes return null, retaining the explicitly labelled direct-line fallback.
Place search returns normalized place arrays; reverse lookup returns address,
locality, region, country and label fields used by scene context. Unsupported
operations are reported in capabilities; adapters can provide attribution and
supported route profiles. A provider's lifetime and caller cancellation both
invalidate late response bodies.
Node middleware is configured separately. googlePlacesContextProxy accepts
endpoints: { nearby, textSearch }, resolveApiKey and fetchImpl.
overpassProxy({ routing: { endpoints: { foot, car, bike }, fetchImpl } })
selects compatible OSRM base URLs while preserving coordinate/span limits.
regionalBriefProxy({ placeProvider }) accepts the regional lookup function;
createRegionalPlaceProvider({ endpoint, requestJson }) supplies the existing
serialized Nominatim implementation. Configuration is per instance, including
routing and regional caches. Request parameters cannot override these endpoints.
Voice connections and controls
Voice controls accept an action runner and a controller factory through
createVoiceCommands (./voice/commands). The default Realtime controller owns
microphone tracks, playback, push-to-talk, tool cancellation and radio handoff.
Its backend supplies requestToken({ tier, authMode, signal }) and
negotiate({ offerSdp, credential, signal }). The Realtime-compatible adapter
(./voice/realtime-backend) accepts separate token and connection transports
and endpoints. Only the short-lived client secret reaches the SDP endpoint.
Stopping or ending the application lifetime aborts connection requests and
rejects delayed responses; a new start requests a fresh secret. Secret expiry
limits connection creation and does not describe the connected session lifetime.
createStandaloneApplication({ voice }) passes these construction options to
the controls. An incompatible protocol needs a separate controller adapter;
changing an endpoint alone does not translate protocol messages. No alternate
model is bundled by this extraction. The Node Realtime provider accepts
realtime: { endpoint, models: { standard, mini }, resolveApiKey, fetchImpl };
existing environment variables remain the default configuration. Model choices
come from server configuration. Keep secret keys in the server adapter.
Tool schemas, model defaults and cost estimates are unchanged. Unknown model
IDs retain the existing conservative estimate until their rates are registered.
Composing the existing application components
application/scene, application/controls, application/data and
application/tools expose the existing globe, controls, catalog and tools as
separate constructors. Pass these to createApplication; use its signal and
defer for ownership. The compatibility catalog is still page-scoped: construct
one application per page. Controls, actions and renderers use the same layer
instances. Configure sources with application/sources before registration or
state restoration, and release them after the consumers stop. The catalog
constructor (application/layers) also requires wind, weather and
cyclones sources with getSnapshot for the Weather layers, and owns the
observed-history clock those layers share for the catalog's lifetime.
application/services accepts boundary, terrain, regional-context, weather and
summary services; its weather service supplies cockpit local conditions, not the
Weather layers. application/requests supplies the existing HTTP protocols
with configurable endpoints and a scoped transport. Changing an endpoint works
only for a compatible protocol; another protocol supplies a service adapter.
Cancellation discards late response bodies, and replacing a source invalidates
its pending results. Source disposal does not silently reinstate a default.
ui/composition supplies the default engines to the shell. Applications can
supply a HUD implementation or request policy through control services. The
shared chrome owns the welcome/loading transition; standalone composition adds
Provider Settings. A page places each Vue view in src/ui/views with a host,
<div class="gev-view" data-gev-view="NAME"></div>, and writes its own loading
screen (#loading-screen with a .loader-status line) as static markup so it
paints before any script runs. createApplication() renders the views before
anything reads the page; omitViews names views the application leaves out,
such as provider-settings, and their hosts are removed. Leaving out
layer-panels also leaves out the scene sharing dialogs it draws, and leaving
out scene-chrome the before/after imagery dividers (the imagery still splits,
with no line or handle to move it), the status chips, the toast, the ORBIT
indicator, the FPS readout, the Scene actions panel, the data-pack attribution
cards, the wind inspection marker, the Space Missions replay vehicle, the
full-globe celestial ring, the screen-space map annotations, the visible
map target list and the Global Context compass;
leaving out command-dock the voice control and its pointer reticle;
leaving out context the Bhote Koshi reconstruction panel; and leaving out
intel-hud the Intel HUD overlay (#intel-hud), whose readouts the HUD
implementation publishes to the gev-hud store. An unknown
view name is an error. A host that does not initialize Core's locale passes
localePicker: false to createApplicationControls instead of editing the
markup. Node tests that import application modules register the component
loader with node --import=<core package>/build/register-vue.