createApplicationCatalog takes an optional placement provider. Its
layer(kind) hands Flights, Military Flights, AIS Vessels, Transit and Rail
a place(id, wallMs), and optionally observe(id, fix) and forget(id). While
live, each layer tells it every fix it accepts and draws a contact where it
places it: position, height (the layer's own when none is given), course
and speed. A null answer, a provider that throws, a kind it leaves out, or
no provider at all keeps the layer's own motion unchanged, and following
the world clock the layers never ask it. A transit vehicle or train it
holds still lets the render loop idle and is asked again each second;
vessels ask for frames only while it places one, and stop when the layer
turns off.
Tests show the layers draw and reconcile exactly as before without a
provider and with one that places nothing, draw where a provider places
them, and idle while it holds them still.
A buoy, beacon, RACON, light or IALA mark in the vessel feed was drawn
as a ship. A vessel record whose type names one, or whose MMSI is an
aid-to-navigation station's (99MIDxxxx), is now isAidToNavigation and
vesselModelSpec draws it as the fleet set's buoy of its mark (cardinal
N/E/S/W, port or starboard hand, safe water, isolated danger, special
mark; a red can when the type names no mark), at its built size and
flagged fixed so its model does not turn to a heading. Its icon is a
disc instead of a chevron.
AISStream now subscribes to AidsToNavigationReport by default, and the
server names each report's aid type code (0 to 31) as its kind instead
of reading it as a ship type.
Feeds carry many airport ground vehicles (type ZVEH) and ground stations
(GRND) that were drawn as the generic plane, some at the 10 km airborne
default.
- types.json gains aircraft_callsign rules: a ZVEH callsign starting FIRE is
a fire truck, PUSH/TUG a pushback tug, DEICER/SQUID/SQB a van, else a car.
GRND draws a new 11.8 m radio mast (other/radio_mast.glb).
- resolveAircraft and aircraftModelSpec take the callsign; a matched rule
names the model outright, ahead of the type's class model.
- The layer spec cache keys such types by the rule matched, so it holds at
most one entry per rule.
- These models (catalog category `other`) draw at their built size, as such
models already do, and a
contact with no altitude whose model is one now stands on the ground; the
feed's own type is kept for such contacts, as no airframe lookup knows it.
- Local ADS-B passes the callsign too.
Vessels, Transit and Rail turn nearby icons into fleet models at the
aircraft layers' distances. A vessel is drawn at its AIS length and beam
(static dimensions now reach vessel rows), standing at the sea surface; a
transit vehicle or train stands on its road or rail, raised by its type's
elevation, and is painted in its mode or category colour where the model
marks it.
startPageLocale, followPageLocale and switchLocale start, follow and switch
the page's language as the standalone application and the globe picker do,
for an application that offers the language elsewhere and passes
localePicker: false. The standalone application uses them.
Selecting a vessel asks Open Vessel Photos (a free, keyless API) for its
photo by MMSI, once per MMSI. The CONTACTS view shows it under the vessel's
name and MMSI with the photographer, the photo's open licence and a link to
the vessel's page; only images on Open Vessel Photos' own host are shown, and
an answer about another MMSI is no photo. The vessel source exposes the lookup
as getEnrichment({kind: 'photo'}) and fetchOpenVesselPhoto for applications
with their own vessel source.
The core layer lifecycle imported the whole Data Library manifest
contract only to check the lib- prefix, so the layer-lifecycle package
owned a feature module. The namespace rule (prefix, entry id pattern,
the two id helpers) now lives in src/data/libraryLayerIds.js, which the
lifecycle owns; the manifest contract imports and re-exports it, so
library callers are unchanged.
The dialog is now DataLibraryDialog.vue (with DataLibraryCard.vue for each
entry), placed with the Browse button by DataLibraryBrowse.vue in
LayerPanelsView and teleported to the body. It renders what the
gev-data-library store holds and sends clicks and typing to the owner.
src/ui/libraryBrowser.js is now the framework-free owner: it presents the
open state, filters, the rows they show, the stat summary and Remove all
through `publish`, and its controls answer the view. Its text helpers are
what the view renders with t(), so every label follows the locale without
rebuilding anything. Only showModal/close and focus moves stay imperative;
focus returns to Browse also when the browser closes the dialog on Escape
itself.
Component tests mount the view over the real owner (open, filters, Add to
Remove keeping focus, Zoom, Remove all, a language switch, clean view).
A link check of every manifest found 404 licence or provider pages for
OpenSeaMap seamarks, EMODnet bathymetry and the PDOK, basemap.at, Geneva
SITG, ICGC Catalunya, Luxembourg and Vlaanderen aerial photos, and no
answer from the SEDAC GPW, Minnesota and Bangor hosts. Drop those 11
entries, renumber the unpublished share-token ledger, point the tests at
remaining entries and regenerate DATA_SOURCES.md.
The City of Amsterdam's verkeersinformatiesystemen register lists where
its ~500 traffic and ANPR cameras stand, with type and asset number, but
publishes no pictures. That fits a Data Library dataset better than a
CCTV pack: the register answers GeoJSON in WGS84 with CORS headers, so
the entry fetches it browser-direct with no proxy code.
The register names no licence ("public unless stated otherwise"), so the
entry stays operator opt-in, as #251 had it. The dataset and the
position-only camera idea come from @Maher-Reven's #251, which this
replaces; the entry lists them as maintainer and in its attribution.
Co-authored-by: Maher Kurdi <[email protected]>
DATA LAYERS keeps its curated core layers; regional and niche sources
live in the Data Library, opened from Browse data library at the foot
of the panel. Each entry is a JSON manifest in src/library/entries/
pointing at a live upstream (query API, provider-hosted file or tile
endpoint), so nothing is bundled and a self-hosted instance hosts no
data. Sources without CORS go through one allowlisted /api/library
route. Kinds: dataset (GeoJSON, CSV, JSON records, ArcGIS, partitioned
GeoParquet), tiles (XYZ, WMS/ArcGIS bbox export, terrarium relief) and
pack. Selections persist and travel in share links as lib=, separate
from the core layer tokens. Library rows get Zoom and Remove icon
chips that work while the layer is off, rendered through RailChips.
Strings are translated in every locale.
GEV itself registers no new subject: with no provider, the Cockpit offers
aircraft, satellites and vessels as before, except that the most recently
selected of them now wins entry.
- services.cockpitSubjects: providers ({kind, layerId, entryFace, priority,
isEntryAllowed, read, hold, release, hud}) are checked once by the
coordinator; frames and HUD views on every update; a throwing provider
ends its view or keeps the last HUD.
- Entry: the most recently selected subject wins, across all kinds.
- Camera: a frame may turn the ground clamp off, anchor exactly on the
subject and update every frame.
- A generic HUD: kicker, title, cards, always-visible source rows, banner,
a veil with light streaks (off under reduced motion) and a bottom
progress line whose close ticks merge.
- Text comes from the provider, already in the page's language
(./i18n/locale: getLocale, onLocaleChange).
- Rail and Transit read and hold APIs (getSelectedTrain/Vehicle,
getCockpitSubject, setCockpitSubject, focusTrain/Vehicle); a held
train's marker, card and route line are hidden, and its feed keeps
polling while held.
- ./layers/rail/paths is exported.
Live trains from 30 keyless operator feeds: GPS positions from 29 and
Irish Rail's station reports, shown as the operators publish them. 28
work out of the box; Renfe long and medium distance and KTMB appear once
their pack is built, because their vehicles name no route.
Packs are opt-in (scripts/build-rail-paths.mjs): the operator's own
route lines, stop names, next stops and trip-update delays for live
trains. Nothing downloads when the app starts, no train is placed from a
timetable, and no request carries a deployer's key.
Translate the 148 strings main added since (mostly the News layer) and
prune the 5 it no longer uses; keep both Hebrew and Swedish in the
language lists.
A live source may now declare a timeline: it can answer a snapshot at a
given time (source.timeline = { coverage(), stepMs, subscribe? }, read by
readSnapshotTimeline). While such a layer is on it follows the world clock
and registers the coverage. At the live edge it polls exactly as before;
away from it the layer asks for getSnapshot({ ...query, atMs }) at the step
at or before the clock time and shows that snapshot instead of polling.
src/time/snapshotTimeline.js owns the following: a seek, time going
backwards or leaving live replaces every contact with the snapshot at the
new time (play waits on it, for at most 8 s); playing on keeps one
snapshot ahead and gates play on it, applying those snapshots as polls so
contacts move smoothly; outside coverage the layer is empty; back at live
it is cleared and polls at once. One load at a time, and a load the
source drops for a newer one is not a failure.
While following, aircraft are drawn at the clock time with no render
delay, records and absences are judged at the clock, track requests carry
atMs, and vessels move between fixes along the great circle. The world
clock counts jumps (seeks and returns to live) in getState().
A source with no timeline never follows the clock, and every layer stays
live, unchanged.
The detection overlay only hid a contact behind the Earth's curve, so a
street vehicle behind or under a building, a 3D tile or a hill still had
its bracket and "VEH-nnnn" callout painted over that geometry, while its
own dot was depth-tested and correctly hidden.
A contact a layer marks `depthTested: true` (only Street Traffic does) is
now tested against the depth the scene drew and left out of the overlay
while a drawn surface stands in front of it by more than max(3 m, 2% of
its range). Other layers are untouched: their markers are drawn over the
scene on purpose.
The test runs on the GPU and never waits for it. Each tested contact is
one point drawn inside an occlusion query; the vertex shader looks up the
frame's packed pick depth at the contact's pixel, decodes it and drops the
point when the surface is nearer than the contact's threshold. Query
results are collected on later frames. Reading depth back was measured
and rejected: `scene.pickPosition` per contact costs 0.2 to 1.4 ms each,
and Chromium answers `getBufferSubData` with a round trip to its GPU
process even behind a signalled sync object (1.5 ms on a quiet machine,
15 to 25 ms on a busy one).
- Contacts are retested every 100 to 200 ms, spread across frames, at
most 256 a frame; with more in view the pace stretches so none starves.
- Hiding is immediate. A hidden contact returns 600 ms after its last
"hidden" answer, so one on a silhouette edge does not blink, and a
contact new to the view is held back until its first answer (400 ms at
most), so orbiting does not flash brackets behind buildings.
- A callout whose contact goes behind something is dropped at once, not
carried to the next solve and not faded out on the facade.
- These times run on painted frames, so a hidden tab does not expire them.
- A control point in every batch must draw; if it does not, the batch is
ignored and after three the probe stands down for ten seconds. With no
pick depth (2D, Columbus view, depth picking off) nothing is hidden.
A WebGL 1 context reads depth back synchronously about 8 times a second.
The renderer internals this depends on are read in one function,
`readSceneDepthSource`, with the engine version they were checked against.
Main added the News layer at the same places: both layers are kept in the
layer names, panel order (News stays last of Events), standalone sources,
voice manifest, credits, DATA_SOURCES and the CURRENT-STATE table. News took
share-link token 00 first, so Shipwrecks moves to 01 (ledger, registry,
CURRENT-STATE and the token tests). The catalog now has 40 layers and the
share-link registry 38. Shipwreck strings are added to the new Hebrew
catalog; the other catalogs keep both sides' strings.
They were listed as owned by the ghostmaps and ghostmaps-source packages,
which they are not part of; ghostmaps-source is meant to stay free of
rendering code, and shipwrecks/index.js imports Cesium. The app groups that
own every layer keep them.
The trail no longer joins a day of history into one line. It lifts the pen
across a gap of more than 48 minutes, an implied speed over 1,500 kt, or a
report without altitude (a parked aircraft within 0.1 nm for under 3 h stays
joined), and every history point is kept as its own segment. History never
gets an invented altitude. The live end is the path the aircraft was drawn
along, thinned to its shape, so it no longer cuts corners through turns; a
late report trims what was drawn after it. Old segments are frozen in chunks,
so a 2,000-point trail costs about 0.1 ms a frame.
A tracked aircraft that misses an update shows STALE and is dimmed. It keeps
flying on its last speed and heading for five minutes (at most 1,500 kt),
then stops, dimmed, labelled SIGNAL LOST with the time since its last report,
and is kept for ten minutes after that report so the camera keeps its target.
Aircraft on or near known ground hold at their last report instead of
coasting. Military flights follow the same rules.
- A pasted or start-up link opens its photos only when its restore's camera
landed: a drag or search during the restore flight keeps the camera.
- A posted view whose photo step was cancelled or superseded no longer closes
the viewer or flies; one that found a photo it could not load keeps the error.
- openNearest gains `replace` (keep the open photo while it looks, close it
only when nothing opens, so EXPAND stays) and `proceed()` (asked before a
found photo loads); the action uses both, so a navigation, a gesture or a
hiding mode during the lookup loads no billed panorama.
- Mapillary's timeout message is a msg() string with all eleven translations.
Every camera find_alpr_cameras returns comes from the tile source, which builds
it through normalizeAlprNode, so the tags are already one line and capped.
Drop the second pass in the tool, its duplicate test and the boundary entry,
and the function check normalizeDisplayText never sees.
Conflicts:
- docs/CURRENT-STATE.md: keep main's hidden-tab polling paragraph and the
display-text normalization paragraph.
- scripts/package-boundaries.json: keep main's voice panel and text chat
modules and add src/data/displayText.js.
- src/voice/gevActions.test.mjs: keep main's set_panel_open and partial
analyst card tests and the display-text tests.
Tracking a civil flight asks Planespotters for that aircraft's photo, once
per aircraft, straight from the browser. The CONTEXT panel's CONTACTS view
shows it under the flight's name with the photographer's credit, as a plain
link to the photo's page. Only images on plnspttrs.net and pages on
www.planespotters.net are ever shown or linked. Military aircraft and a
converted TR-3B are never looked up.
Flight lookups (type, route, photo) whose source failed temporarily are
asked again after a 60-second cool-down; an answer, found or not, and a
permanent refusal stay final for the session. A route belongs to the
callsign it was looked up for, so a callsign change drops the old route.
Browser QA scripts that track a flight answer the photo lookup themselves,
so they never call the third party.
- Add the full catalog (2940 entries keyed by the English text; 75 plural
entries carry one/two/other per Intl.PluralRules('he')), reviewed for
meaning, terms, separators and label length.
- Register he: he-IL and he map to it; it joins RTL_LOCALES so <html
dir=rtl> mirrors the chrome through logical properties and rtl.css.
- rtl.css: Noto Sans Hebrew (loaded with the other Google fonts) leads the
mono and sans stacks, tracking released like Arabic, HUDs keep JetBrains
Mono for Latin telemetry.
- same-as-english: instrument codes and HUD readouts stay as printed
(HDG/BRG/CRS/FOV/CRT/NVG/FLIR/CAL/SRC, MW units, Sentinel-2, km/h...).
- docs/translations/he.md: style, plurals and glossary.
- docs: TRANSLATORS.md and CURRENT-STATE.md updated; normalizeLocale test
covers he-IL.
The orbital Context card is gone and the Contact card stays hidden in orbit
again. Live Signals page 1 had nothing in orbit but TRACK ACQUIRED, because
orbit feeds no Contacts snapshot; it now lists what is around the
satellite, built the way the aircraft rows are, five rows with stable keys
rebuilt once a second. Rows already shown keep their places while they
still qualify, so a site passing under a low orbit is not pulled from under
the pointer.
Site rows come from the Mapped Installations layer (getNearby, asked only
as far as the reach extends) and focus their site, or release it on the
focused site's row, which reads as the subject does. A site the layer drops
during its frequent reloads stays listed for 10 s, since sites do not move.
With the layer off nothing is listed and a SITES STANDBY row says why.
Satellite rows come from the satellites layer's bounded nearest query,
measured from the satellite itself, and ride the satellite; RETURN goes
back to the one before, skipping one that has left the catalogue. While
the camera hops to a new satellite only TRACK ACQUIRED and RETURN show, so
nothing is measured from the old position. An arrow pressed while a focus
eases back takes the arrows' own clock. Site and satellite rows name their
own actions.