Commit Graph
2635 Commits
Author SHA1 Message Date
Denis Groshev 01a74fceec fix(cockpit): an application subject's eye eases off the floor instead of jumping: its anchor is no longer clamped to the floor, and the eye's clearance above it changes at most 1.5 m/s (easeCockpitLift) 2026-11-12 10:50:27 +01:00
Denis Groshev f75179eea0 feat(cockpit): an application can add its own cockpit subjects; the Rail and Transit layers expose the read and hold APIs such a subject needs
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.
2026-11-12 10:50:26 +01:00
Sameh Khamis a105e819e0 fix(news): an application's picture cards stay at every height
Zoomed out, a hotspot's reports give way to its beacon, and the cards an application draws through pictureCards went with them; they came back only once the camera had stopped after zooming in. The cards now come from every report the close-up view would mark, at any height; the layer's own markers and beacons are unchanged.
2026-11-11 19:43:06 -08:00
Sameh Khamis 2b1784d7ed Merge remote-tracking branch 'upstream/main' into denis/news-world-clock 2026-11-11 15:17:44 -08:00
Sameh Khamis b7015f8819 Merge remote-tracking branch 'upstream/main' into denis/news-world-clock
# Conflicts:
#	docs/CURRENT-STATE.md
#	src/app/constructCatalog.js
2026-11-11 15:03:45 -08:00
Sameh Khamis 283890cee0 fix(rail): trip-update delays are as old as the server's read
The browser stamped every trip-updates answer as just read. While an
operator's feed fails, the rail proxy replays its last good copy for up to
ten minutes, and a cached copy can be a TTL old, so those delays never
reached RAIL_UPDATES_MAX_AGE_MS and a selected train's card showed them as
current. Delays now take their age from the snapshot's fetchedAt (capped
at now), as positions do in ingestion.js, so they stop applying 75 s after
the server last read the operator.
2026-11-11 14:57:02 -08:00
Sameh Khamis ace2160002 fix(rail): Montpellier tram speeds are converted from km/h
TaM publishes km/h where GTFS-Realtime asks m/s, and the Transit layer's
entry for the same feed already sets speedUnit: 'km/h'. The Rail entry did
not, so the proxy passed the readings through and the card multiplied them
by 3.6 again: a tram at 30 km/h read about 108 km/h. A registry test now
requires a Rail feed that Transit also reads to declare the same unit.
2026-11-11 14:54:01 -08:00
Denis Groshev 9b3ca481ce Merge upstream main into denis/rail-layer 2026-11-11 23:23:58 +01:00
Sameh Khamis 99a1e06b61 feat(news): an application may draw the News picture cards itself
createNewsLayer takes a pictureCards(cards, { selectedId }) function; createApplicationCatalog passes it through newsOptions. With it, the layer paints no picture cards of its own and hands the function one card per pictured event on the map, most severe then newest first, with the selected report's id; it hands no cards when the layer turns off.
2026-11-11 13:53:10 -08:00
Kevin Rajan ca58e113c0 test(routes): cover HTML 502 outage in MCP plan_route (#1509)
Preserve quanticsoul4772's routing-outage fix and add a focused voice-facing regression for a non-JSON gateway response.
2026-11-11 09:44:24 -08:00
German Khokhlov 82e2b716bc docs(routes): tighten the outage comments and notes
Shorter comments, changelog and CURRENT-STATE text for the 502 outage
path; the CURRENT-STATE paragraph now also names the 429 exception and
which budget expiry answers busy and which answers 502. Text only.
2026-11-11 21:21:07 +04:00
German Khokhlov a29611ca17 Merge branch 'main' into upstream/route-outage-unavailable 2026-11-11 21:16:50 +04:00
Denis Groshev 13818d0b9f Merge upstream main into denis/rail-layer 2026-11-11 18:02:52 +01:00
Denis Groshev 44d2c0695c feat(news): the News layer reads its time from the world clock
Core's catalog passes worldClock.timeFor('news') as the layer's now, so a host that follows the clock for News can show a past time's reports with their ages, window and counts as they were then. Core does not follow the clock for the layer, so it keeps reading the wall clock.
2026-11-11 18:00:24 +01:00
German Khokhlov 1c08482fa8 Merge branch 'main' into upstream/firms-age-rounding 2026-11-11 20:14:57 +04:00
Denis Groshev 830c080803 feat(rail): add a Rail layer with keyless live trains
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.
2026-11-11 16:06:22 +01:00
German Khokhlov 71797427d6 Street Level: a posted view whose camera was taken stands down whole again
Applying the rest of a posted view after the camera was taken during its
terrain wait still let it change the camera: stop_tracking dropped a follow
the user had just started, and annotate_map framed the view's marks. A
camera taken during the wait now ends the view again, as before. The 2 s
terrain timeout and the per-view catch on the queue stay.
2026-11-11 19:01:58 +04:00
German Khokhlov 0ebc09079d Street Level: polar look-at, a bounded terrain wait, takeovers during the flight
- The look-at longitude wraps fully, so a tilted camera at a pole gives a
  point on the globe. The strict off-globe check on street_level is undone:
  points are clamped again, as before.
- A posted view that fails is answered, and the views behind it still apply.
- A posted view waits at most 2 s for the terrain under its camera, then
  keeps its own estimate.
- A camera taken during that wait stays where it is: the rest of the view
  applies, and only its photos stand down.
- show_street_level keeps a search or a press on the globe made during its
  own flight: it adopts only its own navigation stamps on landing, and a
  gesture is never forgiven.
2026-11-11 18:02:15 +04:00
Said Turksever ae66d07575 i18n(sv): the PR's 27 Street Level strings
Main added a Swedish catalog in #1630, so the catalog test failed on the
strings this PR adds. They use the catalog's terms (Gatunivå, nyckel,
bilder på gatunivå, vyns mitt).
2026-11-11 12:16:58 +00:00
Said Turksever 43893697aa Merge upstream main into pr/street-level-show-action 2026-11-11 12:15:18 +00:00
German Khokhlov 4fd36c44e0 Street Level: a replacement keeps its own state, and a posted view stands down
- Releasing a held viewer frees the old source's adapter without resetting
  the panel, so a replacement's error, outcome and armed OPEN survive; a
  cross-source replacement drops the old photo's details and marker.
- The action holds the open photo only for its own switch-off: a source
  switched by hand during the load stops it and reports cancelled.
- A posted view waiting on terrain watches the camera and stands down after a
  navigation or a globe press; a flat or straight-down camera asks no terrain.
- An arming link opens the panel without persisting the layout, and a posted
  view's street-level step runs as programmatic.
- An off-globe street_level point throws TypeError.
- zh-Hant uses the file's wording for the new strings; README and
  CURRENT-STATE count 35 tools.
2026-11-11 16:05:26 +04:00
German Khokhlov 45d9ff1d86 Merge branch 'main' into sv-locale
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.
2026-11-11 13:59:40 +04:00
German Khokhlov f612fc2180 Street Level: a posted view without a photo point grounds its look-at too
show_in_gods_eye_view measured a view's altitude from the terrain before
taking the point the camera looks at; a view posted straight into the
embedded app used the ellipsoid altitude, so a camera over high ground
opened photos kilometres ahead. groundedLookAt moves to the view module,
and the embedded app uses it with the app's terrain heights for a posted
street_level that names no point.
2026-11-11 13:46:10 +04:00
German Khokhlov a8970c1f18 Street Level: a geocoder that throws fails show_street_level at its location stage
A provider that threw while geocoding (offline, an upstream down) escaped
the action as a bare error. show_street_level now reports it as its
location stage ("Could not arrive at …", the cause in detail), and nothing
is switched or looked up; fly_to_location itself is unchanged.
2026-11-11 13:46:10 +04:00
German Khokhlov 8334262109 Street Level: the action's failure cards in the interface language
The voice card showed English for show_street_level failures the catalog
did not know: a missing key, a map or source that could not be switched on,
the layer, invalid coordinates, an imagery fallback and the filter clause.
They now translate like the other action errors (exact messages and
patterns in speech.js), in all twelve languages; the spoken line stays
English, and the filter clause names the filters as their chips do.
2026-11-11 13:46:10 +04:00
German Khokhlov e91e5a9da9 i18n(sv): apply the second translation review
- The cloud brightness key is a filter, not a brightness scale.
- Counted lines without a plural form read "label: count", so 1 agrees.
- Fire confidence is «tillförlitlighet», not «säkerhet».
- Agreement with «flöde» and «luftfartyg»; course on the tracking line is
  «FÄRDRIKTNING», which fits both aircraft and ships.
- Wildlife is «Artobservationer», since the layer also shows plants and fungi.
- Bridge readouts are nouns (BLICK, ÖGONHÖJD), cockpit key hints share one
  verb form, tool names in pointer messages are quoted, and «jfr» replaces
  «jmf.»; the guide's example follows.
- Smaller wording and consistency fixes across the catalog.
2026-11-11 13:37:26 +04:00
Said Turksever e0fedc13dd Street Level: a link whose source has no working key says why 2026-11-11 09:21:06 +00:00
Said Turksever 5b7e917f94 Street Level: Hebrew strings for the show_street_level action 2026-11-11 08:55:55 +00:00
Said Turksever f878efe22d Merge commit '0ceee8df40b97c6c693e86a4443794cd4ed1ff35' into HEAD 2026-11-11 08:54:28 +00:00
Sameh Khamis e18353f769 feat(weather): a host can supply decoded frames to the weather layers
A weather feed may also be a frame source. With an optional
getFrameImage({ product, time, size, bbox, signal }) the layers take
whole-extent images, detail windows and the global mosaic from it; with an
optional getFrameTile({ product, time, z, x, y, size, signal }) regional
tiles and their prefetch come from it, null being a transparent tile. Pixels
may be an ImageBitmap, ImageData, canvas or VideoFrame of exactly the
requested size; the layer draws them once and closes them. Cancellation
reaches the source, and a feed without these methods reads the image and
tile routes as before.
2026-11-10 21:54:08 -08:00
Sameh Khamis b226fa820a test(news): briefing stories share one published time, so their order never depends on a clock tick 2026-11-10 21:19:33 -08:00
Sameh Khamis c45d9cfb03 Merge branch 'main' into sameh/contacts-follow-world-clock 2026-11-10 18:26:22 -08:00
Sameh Khamis cbbf55cacd feat(vessels): key vessels by their source's stable id, and share a selected vessel
AIS Vessels are keyed by the id their live source gives, with the MMSI as an
attribute: an MMSI can be shared, spoofed or reused, so two vessels with one
MMSI are now two vessels. Looking a vessel up or selecting it by MMSI still
finds it.

A share link made while a vessel is selected opens that vessel. A UUID vessel
id travels as 16 bytes in a new thing field (13). A shared thing that is not
reporting now says so in a sentence per kind, with its id (ICAO, NORAD, MMSI
or vessel UUID) on a second line.
2026-11-10 18:12:53 -08:00
Sameh Khamis 57ff81f723 feat(live): flights, military and vessels follow the world clock when their source declares a timeline
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.
2026-11-10 18:12:53 -08:00
Sameh Khamis 874f83ba48 fix(voice): messages worded before reaching the status also follow a language change (#1661)
The radio and push-to-talk helpers shared by both voice providers now pass
English catalog text, and the OpenAI controller words its detail when shown,
like the Gemini session. A token failure without a server message carries
its status as sourceParams, so it is worded again with the number. The docs
say sourceDetail is the message as given, catalog text when it is the app's
own.
2026-11-10 18:11:07 -08:00
Sameh Khamis b1c5de0ed1 fix(voice): Gemini voice messages follow a language change
The Gemini session translated each status and error message once, when it
happened, so after a language change the voice control kept showing it in
the old language. The state event now also carries the English catalog text
as sourceDetail, and the control words it when shown. The local server's
known token errors stay catalog text until then instead of being translated
on arrival.
2026-11-10 18:04:52 -08:00
Sameh Khamis fdbff75e1e Format the keyless Gemini voice tests (#1660) 2026-11-10 17:10:45 -08:00
Sameh Khamis b89f81b729 fix(voice): keyless Gemini voice drops the microphone hint
The Gemini token route now answers a missing key with code API_KEY_MISSING,
the session carries it to the error state as missingKey, and the voice
control hides "Check microphone permission" for that error, as it already
does for OpenAI. The OpenAI controller sends the same flag, so the shared
control and the controller agree whichever updates last.
2026-11-10 17:06:50 -08:00
Sameh Khamis f86b8954f6 fix(voice): a missing-key error keeps its wording and hides the microphone hint after a language change (#1599)
The voice error was matched against the translated key message to decide
whether to show the microphone hint, so switching language brought the hint
back and left the message in the old language. The error now carries a
missing-key flag into the status, which words the message at render time.
A controller test covers the language switch, and CURRENT-STATE.md documents
the keyless voice response.
2026-11-10 16:11:30 -08:00
Said Turksever c03a54de40 Tests: pin the API_KEY_MISSING code on the keyless voice token response 2026-11-10 16:11:17 -08:00
Said Turksever 09d02b6e7a Keyless voice: say where to add the OpenAI key instead of blaming the microphone 2026-11-10 16:11:17 -08:00
Sameh Khamis 49edc31879 Merge branch 'main' into feat/viewport-context-decision-upstream 2026-11-10 15:54:49 -08:00
Sameh Khamis 100369f9af Merge remote-tracking branch 'origin/main' into feat/viewport-context-decision-upstream 2026-11-10 15:44:17 -08:00
Sameh Khamis 9234c1b3d2 fix(voice): drop viewport decision codes the caller can never log, name the crop decision eligible (#835)
The decision no longer reports not-entity-context or channel-closed, which
its only caller rules out first, nor pointer-scale-not-useful, which the
crop path already checks and logs. A closed channel returns before any
record, as on main. The crop decision is pointer-crop-eligible, since the
crop checks after it decide whether the pointer is on ground, and a sender
called on a closed channel logs channel-lost.
2026-11-10 15:44:17 -08:00
Sameh Khamis 076d264982 Hide street vehicles behind low walls and trees seen from above
In a dense downtown view from about 400 m up, the occlusion margin of
max(3 m, 2% of the range) came to 14 to 20 m, so a wall, awning, tree or
lower facade standing 10 to 20 m in front of a street was read as the
road itself: the vehicle's dot was hidden by the scene, but its bracket and
callout were still drawn. Checked against scene.pickPosition in one such
view, 18 of 470 vehicles in frame fell in that band.

The margin now depends on how steeply the camera looks down on the
contact. A point on the line of sight s metres in front of a vehicle
stands s * sin(elevation) above it, so a surface there hides the vehicle
once that rise passes 2 m. The margin stays at least 3 m and at most 2%
of the range, so grazing and street-level views are unchanged. In the
same views no vehicle with the drawn surface within 3 m in front of it
(its own road) is newly hidden.

From the review of the first change:

- A contact back in view after longer than a retest interval is held back
  and retested like a new one, instead of being drawn from an answer
  given before it left the view. It gets a fresh record, so a test still
  in flight from before cannot end the hold-back either.
- The hold-back for a contact's first answer stretches with the retest
  pace when a crowded view slows it.
- A batch whose control point failed no longer counts as a first answer.
- An unknown log-depth flag reads as "no depth" rather than as standard
  depth, which would have put every surface in front of every contact.
- A shader that fails to build leaves no shader behind and is not built
  again on the same context.
2026-11-10 15:08:26 -08:00
Sameh Khamis 630e1b1998 Hide street-vehicle brackets and labels behind buildings and terrain
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.
2026-11-10 15:08:26 -08:00
Sameh Khamis c068dc2290 Merge branch 'main' into sameh/photo-without-credit 2026-11-10 14:23:06 -08:00
Sameh Khamis 259bc1a024 feat(flights): show a Planespotters photo even when it names no photographer
A photo whose answer gave no photographer was dropped as no photo. It is
now shown, still linked to its Planespotters page, with no credit line.
2026-11-10 14:12:42 -08:00
Sameh Khamis 2992232fcc fix(shipwrecks): tidy after merging main: the retired-token test reuses the retired token, natural Hebrew, voice comment beside its entry
- layerStateTokenLedger.test: the retired-token reuse case now offers the
  retired token (02) to the newcomer, not News's 00.
- he.json: the catalog's singular imperative, {count} first in the plural,
  Wikidata read as one source, and smoother legend and info text.
- layerManifest: News after Shipwrecks, so the prompt-budget comment stays
  next to the entries it explains.
- CURRENT-STATE: Prettier-formatted (table column, blank line before a heading).
2026-11-10 14:07:07 -08:00
Sameh Khamis 8ff22f1a12 Merge main into feat/shipwrecks
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.
2026-11-10 14:03:39 -08:00