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.
About 20 sites called scene.sampleHeight, globe.getHeight,
scene.clampToHeight or the MostDetailed variants directly, each with its own
idea of a miss (undefined, NaN, a throw). They now ask heightsFor(scene) in
src/queries/heights.js:
- now(point, { against, exclude }) answers from what is loaded, or null;
against is 'surface' (what is drawn) or 'terrain' (loaded terrain only).
- clampNow(position) moves a position onto the drawn surface, or null.
- batch() and clampBatch() wait for the most detailed tiles: null per
missed point, a rejection when the whole query fails.
- supported() reports { surface, clamp } without allocating.
Cesium still answers now() synchronously, so nothing changes on screen and
nothing is slower: in the live app 1,000 calls cost the same through the
service as direct (terrain 0.5-0.7 ms, surface 2-6 ms a call, run-to-run
noise larger than the difference). Every site already treated a miss as
"hold the last value or fall back"; a renderer that can only answer from a
cache or asynchronously can now plug in at one place.
- A Bhote Koshi evidence point whose terrain sample misses stays at its
fallback elevation instead of reading the miss as sea level.
- The mesh floor sampler skips a scene without depth-texture support
instead of spending its budget on probes that throw.
- The world annotation probe's trace reads hit/missed.
- A source scan forbids direct height calls outside the service.
The service is exported as ./queries/heights and documented in
docs/APPLICATION.md.
Every layer and tool bound its own ScreenSpaceEventHandler and picked the
same click again: with the usual layers on, one click ran about 26
scene.pick and 2 drillPick passes (120-215 ms in NYC with 18 layers). Pointer
input now comes in through src/input/dispatcher.js, one handler per scene.
A click is picked lazily and at most once per kind of query, and every
subscriber reads the same ClickEvent (pick, hit, id, ownerId, gesture,
drill, ground). The same click now costs 3 picks and 2 drills, 17-70 ms.
- Ambient onClick subscribers run only while no tool holds the pointer,
checked once per event; tools subscribe with their lease. Moves and
releases always reach ambient subscribers, so hover and drags end cleanly.
- Camera input and the viewer's stock select/track clicks are ref-counted
holds (suspendCamera, suppressStockClicks). The dispatcher owns the
controller's enableInputs, so a Cesium flight that ends no longer turns
input back on under a drag or the cockpit.
- The draw tool, imagery box, CCTV gizmo, cockpit, directions and every
selecting layer use it; bindTrackingClickGesture is gone (the gesture is
on the event). A source scan forbids new handlers, enableInputs writes and
stock-click removal outside the dispatcher.
- The sensor-style pick correction transit had now applies to every layer.
- Radio's 8-pick near-miss ring is one 17x17 windowed drill.
- A missed pick passed to pickContact is no longer picked again, and a
touch tap's contact search shares the click's drill.
- A gizmo drag whose release Cesium never reported ends on the next press
instead of leaving the camera held.
The dispatcher is exported as ./input with the pointer-lease functions, and
documented in docs/APPLICATION.md.
Aircraft draw from GPU icon sets, so the billboard mirror, the billboard
treatment and the aircraft visual-state module had no callers left. 3D models
are now records the layer holds under the contact's id (modelRecord.js):
show and placement are written through showModel/placeModel and read back
from the record; renderer-measured values stay on record.model.
The tracked-model click check only matches when a tracked model exists, so
an id-only pick is never mistaken for a click on the tracked plane.
The flights icon planning and presentation move into
`createAircraftIcons` (src/data/aircraftIconLayer.js), and the motion
regime both layers dead-reckon from into src/data/aircraftMotionRegime.js.
Flights keeps only its grounded-floor pass on top.
Military now draws every contact as a row of that icon set, like
flights: the billboards, the cull positions and the rotation-pass
signature go. Tracking hides the row and untracking re-plans it from
the tracked course. Queries read the drawn row position and judge the
horizon as the shader does. Snapshots that arrive before init or after
destroy are ignored, and per-contact lookups find no contact then
instead of throwing.
With about 120 live military contacts, preRender p95 drops from
0.7-0.8 ms to 0.2 ms on the same views. Render time and frame gaps are
unchanged or slightly lower.
Every flight is now a row in the layer's icon set. Its motion segment
follows the same regime `_deadReckon` picks (interpolate between fixes,
warm up backward from the oldest, coast forward from the newest), and
one shared `_motionRegime` makes that choice for both. The shader moves
every untracked contact between polls, so the 12 Hz fleet tick no
longer touches each billboard. It now only re-plans expired rows,
floors grounded contacts, advances focus while needed, and runs the 3D
model regime.
Grounded contacts the floor lifts are pinned to a hold at the floored
position. Their nose glides on the GPU. Contacts within 50 km re-floor
every tick, and farther ones take turns about once a second. Low
contacts are judged for the horizon at no lower than 12 m above the
ellipsoid, as before. Below hover speed a contact's nose holds. A row
whose fleet model is drawn keeps its focus updated, because the model
reads it. Untracking re-plans and re-floors the icon from the tracked
course.
On the same views with about 6,700 OpenSky flights, frame gaps at p95
drop from 28-40 ms to 17 ms, and the fleet tick from 16-37 ms to about
2 ms at most.
Icon rows live in typed-array columns: one packed motion segment (hold,
lerp or arc, with a course glide), style, colour and pick ids per row.
The primitive draws every row in one instanced call, dead-reckons in the
vertex shader, matches the billboard treatment the fleet layers use, and
picks and drill-picks like a billboard. Cyber sonar covers icon sets.
Earthquake analyst records come from the events the layer drew, gated on
its own enabled flag, instead of reading each entity's position and
property bag back; the discs no longer carry the bag. CCTV coverage rays
and projection planes record their camera id in a layer pick store
instead of a cctvCameraId entity property, with the same ownership checks.
The tracked flight, military and local ADS-B billboards record their size
and distance scaling as the entity record's sprite, which the close-range
camera guard and the screen annotations' tracked footprint now read in
place of the billboard's property getters.
Flights, military and local ADS-B now write their models' show and world
matrix through setModel in src/data/modelMirror.js, and read them back
through modelState: the fleet and tracked handoffs, the tracked visual and
trail anchors, detection anchors and modelAnchorWorld. A matrix is copied
into Core's record and into the model's own matrix, so the shared scratch
can be passed directly and models still never share one matrix object.
What Cesium measures (ready, computedScale) is still read from the model,
and colour stays live as on billboards, because cyber sonar writes it too.
Test seams mirror the models they install, as they do billboards.
The three layers now add their billboards through addBillboard and write
the mirrored fields through setBillboard, so selection, cards, queries,
focus, view thinning and the FIRMS horizon cull read what was pushed from
Core instead of from Cesium. Vessels and FIRMS state their sprites' own
sizes (32 px ship artwork, the glow canvas at twice the core size), which
the mirror needs. A vessel helper, iconPosition, replaces the repeated
mirror-or-visual position lookups, and the vessel visibility pass writes
show only when it changes, as CCTV and FIRMS already did.
A billboard never given a position now has none in the mirror rather than
the origin.
Flights and military now add their billboards through addBillboard and
write show, position, scale, rotation, size and distance scaling through
setBillboard, which keeps Core's copy in src/data/billboardMirror.js.
Their reads go to billboardState instead of the Cesium billboard, so the
layers no longer read back what they pushed to the renderer.
A mirrored billboard needs an explicit width and height, because unset
Cesium reports the loaded image's size. Colour is not mirrored yet: the
cyber-sonar theme writes it too and tells the writers apart by the live
value, so it moves with the effects port.
Submarine cables wrote each pick's info both to a per-load WeakMap and to a
__gevTeleGeography expando, always together; one cablePicks store replaces
both and resolves the same objects. The voice view-target prewarm marks its
viewer in a WeakSet instead of a __gevViewTargetPrewarmInstalled property.
The aircraft layers' model spec key, IR boost and belly offset and the
billboards' large-icon flag move into aircraftVisuals, one store shared by
flights, military and local ADS-B. Installation markers' last drawn height
and selection move into installationMarkers. The CCTV viewshed QA tag is now
the viewshedCameraId record field, which qa-cctv-v2 reads through
entityRecords. Test fixtures drop a placement flag nothing reads.
The ALPR layer's private per-entity fields (record, clamp, canvas and display
anchors, wedge, native appearance, pick position, shown, surface check) move
from gevAlpr* expandos into alprMarkers, a store made by the new
createEntityStore(). The shared entity records are built on the same factory.
Flights, military, satellites, installations, local ADS-B and ALPR hung
gevTrackedId, gevDisplayPosition, gevVisualPosition, gevSelectionOrigin and
gevLabelModel on the Cesium entities they draw, and the tracked readout,
follow camera, cockpit, share links and awareness read them back. They now
live in src/data/entityRecords.js, a Core-owned record per entity: layers
write entityRecord(entity), readers use readEntityRecord(entity). Nothing
changes on screen. This is the first slice of the renderer plan's state
mirror (P1).
The module is exported as ./data/entity-records so hosted code can read the
same records, and window.__godsEyeView.entityRecords gives the QA scripts
the same access from the page.
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
Main moved on since this PR opened; the overlay layer is ported to the
current contracts:
- Layer-state token 6 (j belongs to transit; new single-character
tokens are digits) with a reservation-ledger row, and the option owner
renamed to the layer id so the compact share codec can encode it.
- Constructed per catalog by createOsmOverlaysLayer() in
constructCatalog.js; src/standalone/data.js no longer registers layers.
- Drapes through maps/imageryHost.js, so the overlays show on Google 3D
(tileset imagery) as well as the globe stacks, and follow map-stack
switches. Imagery layers are created on enable and destroyed on
disable/destroy.
- Translated chip labels and layer name in every locale, LAYER_NAME_KEYS,
Infrastructure panel group, voice manifest entry.
- Attribution: OpenSeaMap tiles CC BY-SA 2.0, OpenSnowMap credit as
required, OSM ODbL credit for both; bulk-download note in DATA_SOURCES.
- CURRENT-STATE row, changelog fragment, share-codec absent defaults,
updated token-ledger fixtures and voice schema pins.
Directions, the Contacts marker and panel, placed annotation labels, the
drawn-line measure, the Cockpit and formatRouteDistance each chose the unit
and precision on their own, and most chose before rounding: #1450, #1433 and
formatDistanceText or kilometreDigits from src/data/distanceFormat.js, which
cut after rounding. Each keeps its own metre step and rendering.
That also fixes the 10 km cut the Contacts marker and panel, the drawn-line
measure and the Cockpit still had (9,960 m read 10.0 km). The helper returns
kilometres already rounded, so the plain marker and the localized panel no
longer disagree on half-tenths such as 1,450 m.
A negative annotation distance now leaves the label without a distance, and
a drawn line with a coordinate that is not a number shows no measure, rather
than printing 0 m or NaN km. The HUD altitude tag and the spoken view height
keep their own unit choice.
The transit, ALPR, vessel, CCTV and flight/military context fixes each
wrapped 359.5° to north with an inline Math.round(x) % 360. They now call
roundHeadingDegrees in src/data/headingDegrees.js, which also wraps negative
and multi-turn bearings. normalizeHeading moves there with it, and
cockpitMath re-exports it so existing imports keep working. It now adds a
turn only to a negative remainder, so a bearing just under half a degree
keeps its precision.
The awareness, Local ADS-B, cockpit wind, cockpit heading and Street Level
readouts use the same helper. The Cockpit's TRACK ACQUIRED signal rounded
without wrapping and could read COURSE 360°; it now uses
formatHeadingReadout, which shows --- for a track that is not finite.
The application package boundary owns src/i18n/locales/ar.json beside the
other catalogs, and the style.css shim test checks that the shim holds only
import statements rather than counting its lines, so adding rtl.css to it
does not read as rules.
Selects pad the chevron's side with padding-inline-end, as do the Radio
filter, the SDR gain select and the voice timeout select, which set their
own padding; rtl.css moves the chevron to the left, where an Arabic field's
text ends. A rail legend's range keeps left-to-right order under its colour
ramp, which always runs low to high from left to right.
Main moved the UI to Vue views and Pinia stores, so the Arabic changes to
the old DOM controllers now live in their views: the rail legend range
comes from formatScaleRange (keeping units a source supplies as given),
the voice timeout options are worded by Intl unit formatting in
VoiceControl, and the scene editor's JSON fields stay left to right in
SceneDialog. Main's CCTV status line, launch panel and calibration values
keep the Arabic isolates and Western-digit formatting.
isolate() and keepCodesWithValues() read the locale through getLocale(),
which follows the page's vue-i18n instance, and the Intl formatters take
intlLocale(getLocale()). A language switch now applies in place, and
applyDocumentLanguage sets <html dir> on every switch, so the page mirrors
without a reload.
The icon rails and their tips are placed with logical properties, so they
stay on the outer side of the mirrored stacks; Cyber's new stack offsets,
padding and edge lines are logical too, and rtl.css moves Cyber's right
stack to its new rail clearance. Main removed the Context mini radio, so its
right-to-left rules and test go.
The Arabic catalog gains main's new strings and drops the ones main no
longer uses; the other catalogs keep main's new keys and the branch's
landing labels.
The application views install one vue-i18n instance that holds the active
locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale()
keeps its locale in the instance's ref, so t(), tn() and the Intl formatters
read it from there and a view that translates while rendering renders again
when the locale changes. A tag written straight to the ref is normalized.
Message formatting, catalogs and call sites are unchanged, and switching
language still reloads the page.
The instance outlives the views: unmounting one no longer disposes it. The
browser build defines vue-i18n's feature flags and drops its message
compiler, which Core does not use.
VisualSettings publishes the active style's parameter rows to the display
store, and the Display panel renders them with the shared row component;
a moved slider goes back through the store's setStyleParameter action.
The readout snaps a value to its step as the browser does, half-steps up
and an off-grid maximum down. A preset that resets the shown style's
uniforms republishes its rows, input for another style's parameters is
ignored, and a stopped owner takes its rows with it unless a newer owner
has published since. createStyleParameters is deprecated and builds its
rows the same way.
RecordingControls publishes the safe frame a recording composes in, 16:9
or 9:16 (null while none records), to the new recording store, and the
scene chrome renders the overlay's active and ratio classes from it
instead of the controls writing them. Any frame but 9:16, even none named,
records in 16:9 as before.
The camera orientation controls publish whether the map is tilted and the
camera's whole-degree heading instead of writing the buttons' attributes,
and answer tilt and north-up through the globe actions store. The new
GlobeCompass renders the heading, so a rotating camera re-renders only the
compass. The store drops an unchanged camera state and keeps each clear
owner's busy state, so the buttons show the shell that answers them: a
shell replaced by a newer one, or disposed mid-clear, leaves the live
shell's state alone, and an older shell that answers again shows its own.
createOwners gains owns() and ownerOf().
The top-center bar is now GlobeActions.vue. The shell publishes whether a
clear is under way and answers the clear and share buttons through the
globe actions store, instead of binding listeners and writing attributes on
the buttons. A shell disposed mid-clear resets the busy state, so the next
shell's bar is not left busy. bindClearLayersControl stays exported and is
deprecated.
UiSlider merges a caller's own input, change and blur handlers with its
own, leaves the input alone on a null model, and gains unstyled for a
range drawn as the browser's own. The rail card setting, observation
timeline, imagery opacity, Street Level date and Bhote Koshi split and
progress ranges now render through it. The timeline and the setting
ranges put the handle back once their owner has answered, since a drag
that ends where the slider last rendered leaves its model unchanged.
The radio tuner keeps its own input: its owner moves the handle back to
the selected station without changing the slot.
UiBadge takes a tone and carries Core's gev-status-badge class, with
the density variants and colours left to its callers; the CCTV source and
calibration badges and the rail card badges render through it. UiEmpty takes
a title and hint, with the title as the line itself, since Core's
empty states are one line; the Scene shot list, mission roster and imagery
strip use it.
UiSelect renders the bare native select with v-model, options and
disabled. Options that carry their own attributes come through the slot, a
caller's own change handler still reaches the select, and hold never
rewrites a focused select and takes the model back on blur. Without hold, a
select takes its model back once its change has rendered, so a pick its
owner refuses goes back, as the raw value binding did.
The HUD layout, draw colour, RTL-SDR gain, radio station tag, voice
provider, voice timeout, scene and CCTV camera selects now render through
it.
UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
UiSlider takes v-model, min, max, step and disabled
over Core's native range input, emits numbers, and with `hold` never
rewrites a focused handle and takes the model back on blur. It is a
functional render function, so the portable style parameter rows use it
too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style
parameter sliders render with it; server-rendered markup keeps their
default values.
SdrCard calls LocalSdrControls through radio store actions instead of
DOM listeners, and renders its gain options from the receiver's own
R820T gain steps with page-language labels. The display and radio
stores share one owner list (createOwners): the newest connected owner
with an action answers it.