Compare commits

..
Author SHA1 Message Date
German Khokhlov a9b703f3ea Raise the main-chunk budget to main's growth
Main's own main chunk grew 232 KB since the baseline (the News layer and
the Swedish catalog), measured on keyless builds of main; this branch
still adds 72 KB on top of it.
2026-11-11 16:27:28 +04:00
German Khokhlov a757beba77 Merge origin/main into german/navara
Brings in the Swedish locale. Swedish gains the 14 messages this branch
adds and drops the 7 it no longer uses.
2026-11-11 16:14:44 +04:00
German Khokhlov ab5c593ab6 Launch QA browsers on SwiftShader where there is no Metal
The shared launcher always asked for ANGLE's Metal backend, which exists
only on macOS; on CI's Linux runners WebGL failed and the Street Level gate
never saw the app start. The default is now Metal on macOS and SwiftShader
elsewhere; QA_ANGLE still overrides it.
2026-11-11 15:55:12 +04:00
German Khokhlov 02ad2d045a Compare the 3D Tiles fixture's numbers within floating-point tolerance
The tileset's transform and probe points come from trigonometry whose last
bits differ between arm64 and x64, so a fixture written on one failed the
byte comparison on the other. The numbers now match to 1e-12 of their size;
the meshes and the served files are still compared byte for byte.
2026-11-11 15:55:12 +04:00
German Khokhlov ed47aeedbb Show Navara imagery at full detail and discard Esri's empty tiles
The tile budget left 768 MiB for tiles over the screen buffers, while an
oblique view needs about 0.9 GiB at 960x640 and 1.2 GiB at 1440x900; over
budget Navara coarsens the view and stops loading, so the start view stayed
at imagery level 15 where Cesium shows 17. The room for tiles is now
Navara's device default (a quarter of device memory, at most 2 GiB).

Terrain tiles also split past the keyless mesh's level 14, so imagery can
refine beyond level 15, and OSM stops at its level 19.

Esri answers a tile it has no imagery for with a grey "Map data not yet
available" image (over open ocean from level 14). Cesium discards it and
keeps the parent level; on Navara that tile now fails the same way.
2026-11-11 14:54:50 +04:00
German Khokhlov 6a7fed6cb7 Let waitForEngine take any engine picker value
The check added for a missing engine kind rejected navara-hd, a picker
value the HD scenario waits for.
2026-11-11 14:54:50 +04:00
German Khokhlov 14db73f201 Merge pull request #1630 from Kajjan7610/sv-locale
i18n: add Swedish (sv)
2026-11-11 14:37:04 +04:00
German Khokhlov 2d0aa25e35 Fix the trail and decoded-weather probes on both engines
Both probes called waitForEngine without the engine kind, which can never
match and timed out after 120 s; waitForEngine now rejects a missing kind.
The trail probe passed no result vectors to the geo helpers, and drew its
trail below the ground, where only the dimmed occluded pass showed it; it
now draws halfway down to the ground and both engines count the same
pixels.
2026-11-11 14:01:29 +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 1856477e0a Merge origin/main into german/navara
Brings in 190 commits from main. Integration on the engine contract:

- News and Shipwrecks draw, pick and keep their selection through the
  contract and shared map markers, so they follow engine switches.
- Aircraft tracking uses main's segmented trails with contract frame
  numbers.
- Decoded weather tiles move into the Cesium adapter and are injected
  through the renderer registry; Navara hides those sources on globe
  surfaces (docs/KNOWN-ISSUES.md).
- Street-vehicle GPU depth occlusion stays Cesium-only; Navara keeps
  the geometric visibility checks (docs/KNOWN-ISSUES.md).
- Transit includes the heading-neutral atlas icons.
2026-11-11 13:41:30 +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
Sameh Khamis 0ceee8df40 Merge pull request #1666 from bilawalsidhu/sameh/gdelt-tie-order
fix(news): Conflicts reports of the same time come in a fixed order
2026-11-10 22:49:55 -08:00
Sameh Khamis bc786f8c55 fix(news): Conflicts reports of the same time come in a fixed order
GDELT exports are read in parallel, so two reports with the same time
arrived in whichever order their exports finished, and the feed test that
pages through them failed now and then. Same-time reports now order by
most mentions, then by id.
2026-11-10 22:40:15 -08:00
Sameh Khamis 9bca228340 Merge pull request #1665 from bilawalsidhu/sameh/weather-frame-source
feat(weather): a host can supply decoded frames to the weather layers
2026-11-10 22:04:58 -08:00
Sameh Khamis aa5c0fb5e3 docs(changelog): weather frame source (#1665) 2026-11-10 21:54:27 -08: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 aff5ce7f17 Merge pull request #1663 from bilawalsidhu/sameh/news-test-shared-time
test(news): briefing stories share one published time
2026-11-10 21:29:18 -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 1fb0fdad71 Merge pull request #1662 from bilawalsidhu/sameh/contacts-follow-world-clock
feat(live): flights, military and vessels follow the world clock; vessels keyed by source id; share a selected vessel
2026-11-10 18:35:44 -08:00
Sameh Khamis c45d9cfb03 Merge branch 'main' into sameh/contacts-follow-world-clock 2026-11-10 18:26:22 -08:00
Sameh Khamis cb414a1671 Merge pull request #1661 from bilawalsidhu/sameh/gemini-error-locale
fix(voice): Gemini voice messages follow a language change
2026-11-10 18:20:57 -08:00
Sameh Khamis 5826420a27 chore(changelog): name the contacts fragment for #1662 2026-11-10 18:17:52 -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 44dd6d48a8 Changelog fragment for #1661 2026-11-10 18:05:04 -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 962f7293c6 Merge pull request #1660 from bilawalsidhu/sameh/gemini-keyless-mic-hint
fix(voice): keyless Gemini voice drops the microphone hint
2026-11-10 17:20:20 -08:00
Sameh Khamis fdbff75e1e Format the keyless Gemini voice tests (#1660) 2026-11-10 17:10:45 -08:00
Sameh Khamis 0118e17ea6 Changelog fragment for #1660 2026-11-10 17:07:01 -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 9560a6dcd1 Merge pull request #1599 from bilawalsidhu/fix/keyless-voice-key-hint
fix(voice): keyless mic says where to add the OpenAI key
2026-11-10 16:22:58 -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 0687e0e3da Changelog: 1599 fragment 2026-11-10 16:11:17 -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 775e9df120 Merge pull request #835 from kvnloo/feat/viewport-context-decision-upstream
feat(voice): reason-code viewport context escalation
2026-11-10 16:04:54 -08:00
Sameh Khamis 49edc31879 Merge branch 'main' into feat/viewport-context-decision-upstream 2026-11-10 15:54:49 -08:00
Sameh Khamis 4442243749 Merge pull request #1659 from bilawalsidhu/sameh/detection-occlusion
Hide street-vehicle brackets and labels behind buildings and terrain
2026-11-10 15:45:56 -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 85f42f6cd8 Changelog fragment for #1659 2026-11-10 15:28:44 -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 126616ac6c Merge pull request #1657 from bilawalsidhu/sameh/photo-without-credit
feat(flights): show a Planespotters photo even when it names no photographer
2026-11-10 14:32:17 -08:00
German Khokhlov af0dc2e2ec Wait for the traffic state the layer reaches in an oblique view
Roads the view cannot sample may keep their pending surface heights, as
before the refactoring; the oblique wait now asks for current coverage,
drawn traffic, finished loading and no scheduled refinement, and prints
every loading field when it times out.
2026-11-11 02:27:40 +04:00
German Khokhlov ed68be0610 Share the traffic refinement deadline across passes
Each refinement pass renewed the ten-second deadline, so changing
surface readiness could keep the layer refining, and loading, without
end. The passes now share one deadline, and the scheduled surface work is
reported as surfaceWaiting.
2026-11-11 02:27:40 +04:00
Sameh Khamis c068dc2290 Merge branch 'main' into sameh/photo-without-credit 2026-11-10 14:23:06 -08:00
Sameh Khamis e5bf072f6c Merge pull request #1583 from IMBrandao/feat/shipwrecks
feat(layers): add shipwrecks layer (EMODnet + Wikidata)
2026-11-10 14:19:54 -08:00
German Khokhlov b870f446c6 Let the cold-start comparison excuse a field within a bound
Field entries in the ledger allowlist now apply to every difference row,
in the ledger and in the probe alike, and may carry maxDelta. The traffic
fill gets one with a bound of three: its simulated vehicles enter and
leave every frame, so two pages snapshotted moments apart differ by a
vehicle or two.
2026-11-11 02:15:52 +04:00
Sameh Khamis f1b843aaa1 docs(changelog): uncredited Planespotters photos are shown 2026-11-10 14:12:58 -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
German Khokhlov 49b2849528 Wait for the directions route flight instead of three seconds
The FLY check slept a fixed 3 s and then expected a route flight, which
passed or failed on the same commit. It now waits, up to 15 s, for the
layer to report the route motion and the camera to move.
2026-11-11 02:07:36 +04:00
German Khokhlov 00cb44ad2c Stop refining traffic surfaces that make no progress
An oblique view kept roads whose samples could not resolve from it, and
the refinement retried them without end, leaving the layer loading with
work pending. A refinement pass that resolves nothing new stops, and work
still pending at the existing deadline is dropped.
2026-11-11 02:07:36 +04:00
German Khokhlov ed6b6114cf Enable the Bhote Koshi event on Cesium while 3D tiles are shown
Under photoreal tiles Cesium hides the globe, so its imagery layers never
request tiles and the event's imagery install waited for a frame that
never came: turning the story on with 3D tiles on never finished. The
decoded images are installed hidden and shown afterwards by the same rule
as the presentation mode.
2026-11-11 02:07:36 +04: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
German Khokhlov c1795c7dc3 Sample the released-tileset check over open ocean
Under the camera and at 20°E 10°N other contract checks leave meshes and
models; Cesium's scene sample hits them, so the check failed in the full
suite and passed alone. It now samples two places none of them draws at.
2026-11-11 01:51:34 +04:00
Mazeyar Moeini Feizabadi 558a2f8854 Merge pull request #1593 from ZloyS/denis/gev-news-layer
feat(news): News layer with keyless GDELT conflicts and local news
2026-11-10 22:49:35 +01:00
Sameh Khamis f9b7a77140 test(shipwrecks): the page deadline subtracts the newest available source's reported age from the limit 2026-11-10 13:42:08 -08:00
Sameh Khamis 41641cd91a fix(shipwrecks): the page clears shown wrecks as a whole, once even the newest source is too old
Removing one source's wrecks on the page split merged records: a wreck both
sources list is one Wikidata record carrying EMODnet facts, so expiring
Wikidata dropped wrecks EMODnet still had, and expiring EMODnet kept its
facts. The page now keeps every shown wreck, marked stale, until the newest
source passes the limit, then clears them all. Adds a source test for the
reported ages and limit.
2026-11-10 13:40:40 -08:00
Mazeyar Moeini Feizabadi 8f489d203f Merge branch 'main' into denis/gev-news-layer 2026-11-10 22:38:56 +01:00
Sameh Khamis 7704de5075 fix(shipwrecks): the page expires each source's wrecks by its own clock, from the age the proxy reports
The page compared the proxy's fetchedAt with its own clock, so a skewed
browser clock cleared or kept wrecks wrongly; a snapshot without a timestamp
never expired; and one old source cleared the other's fresh wrecks. The proxy
now reports each source's age on its own clock and its seven-day limit; the
page turns them into per-source deadlines when the snapshot arrives (seven
days from arrival when no age is given) and, on a failed refresh, removes only
the sources past theirs. A refused short refresh is logged.
2026-11-10 13:38:31 -08:00
Sameh Khamis 6e4dd6299e docs(shipwrecks): refresh timing and the personal-text filter as they now work, rewrapped
CURRENT-STATE said each source is asked at most once a day; a failed source
is retried after 15 minutes. It now also covers refused short refreshes and
the page clearing expired wrecks, and its two shipwreck paragraphs are
rewrapped. The card's comment no longer says the records carry no names.
2026-11-10 13:36:18 -08:00
Sameh Khamis c885825f2b fix(shipwrecks): rebuild unless the snapshot has a version string; a failed page refresh marks the wrecks stale and clears them after seven days
- A source without a version (undefined, not null) compared as unchanged, so
  an injected source's new snapshots were never drawn.
- When the page could not refresh (the proxy answers 502 once both sources
  are past seven days), it kept showing the old wrecks as fresh. They are now
  marked stale, and removed once older than the proxy's own seven-day limit.
2026-11-10 13:36:18 -08:00
Sameh Khamis 66fba3da47 fix(shipwrecks): a source refresh with less than half of its last good wrecks is refused until the old set expires
A 200 answer that was valid but cut short (30 Wikidata wrecks instead of
about 1,450) replaced the last good set for a day. It now counts as a failed
refresh: the old records are served as stale and the source is retried after
15 minutes; once the old set is past seven days, the new one is taken.
2026-11-10 13:36:18 -08:00
Sameh Khamis 0c159186f9 fix(shipwrecks): small row and query fixes: era counts in one pass, pickers list only years with a wreck, no Portuguese default
- The legend counted each era twice, scanning every record eight times per
  row refresh; it now counts once.
- Each year picker listed every year from the oldest dated wreck (well over
  a thousand entries); it lists the years that have a wreck, plus the chosen
  ends.
- Labels fell back to Portuguese and links to Portuguese Wikipedia while the
  app ships 11 locales: labels now fall back through the app's languages
  after English, and a wreck with no English article links its Wikidata
  item, which lists the article in every language.
2026-11-10 13:29:25 -08:00
Sameh Khamis 38cf1e2be5 test(shipwrecks): an upstream body over its size cap counts as a failed source and is cancelled at the cap 2026-11-10 13:28:10 -08:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 16b1bc7b74 feat(news): larger markers, and a country's own news as one badge with a count
A report placed no closer than a country was a marker at the country's
centre, so a country's news piled there. They are now one disc with the
number of reports, named, and a click opens the Local list over the
country. Markers are 24 px at least: the category icon was unreadable at 16.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 22:28:08 +01:00
Sameh Khamis 6c4c5dd041 docs(shipwrecks): say the personal-text filter is a keyword filter, not a guarantee that no name is shown
DATA_SOURCES and CURRENT-STATE said text naming people is dropped. The filter
drops text with a title or role word (Mr, captain, owner, crew, reported by);
a bare name with none of them still passes. The docs and the code comment
now say so.
2026-11-10 13:27:59 -08:00
Sameh Khamis dd731a5da9 docs(shipwrecks): describe the layer on its own, not by a layer that is not on main
Air accidents (#1584) is not merged, so comments, the changelog fragment and
CURRENT-STATE no longer explain the shipwrecks markers, card and picker by
it; the row-range test fixture is the shipwrecks row. CURRENT-STATE now says
the markers are one billboard collection and the period is stored swapped.
2026-11-10 13:27:26 -08:00
Sameh Khamis 32e5768954 fix(shipwrecks): take the shipwreck modules out of the GhostMaps package lists
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.
2026-11-10 13:26:23 -08:00
Sameh Khamis 0ac06dad2d fix(shipwrecks): a reversed period is stored in the order the picker shows it, so changing one end keeps the other
A period set as 1950 to 1900 (a link or the voice tool) showed as 1900–1950
but stayed reversed, so changing "From" to 1920 gave 1920 to 1900, shown as
1900–1920: the end the reader did not touch moved.
2026-11-10 13:25:25 -08:00
Sameh Khamis 38ba8a3b59 perf(shipwrecks): draw the wreck markers from one billboard collection, on the wreck, and rebuild them only on new data
- The ~8,000 markers were one Cesium entity each; they are now one
  BillboardCollection (as FIRMS, vessels and wildlife sightings do), ordered
  at the bottom of the shared sprite stack so moving traffic draws over it.
- eyeOffset.z was the death toll in metres, so a 1,500-death wreck's marker
  sat 1.5 km toward the camera and clipped or drifted when close. It is gone;
  markers are added by rising toll, so a bigger toll still draws on top.
- The page asks every 6 h but the proxy changes data at most daily: the proxy
  now sends a version that changes with either source's data, and the layer
  rebuilds only when it does.
- Adds a controller test: enable/disable, teardown, draw order, unchanged
  snapshots, a late response after turning off, and the period closing an
  open card.
2026-11-10 13:25:03 -08:00
Sameh Khamis 3b52e24140 fix(shipwrecks): a failed or empty source refresh keeps its last good wrecks, and waits 15 minutes before the next try
A refresh with one source down (or Wikidata answering 200 with no results)
was cached as the day's snapshot, replacing the full set; and once the
snapshot was a day old, every request after a failure fetched both sources
again. Each source now keeps its own last good records, served as stale for
up to seven days, and a source that failed is not asked again for 15 minutes,
even when there is no data yet. A Wikidata answer without results, or with no
wreck in it, counts as a failure.
2026-11-10 13:20:49 -08:00
Sameh Khamis a1200efba6 fix(shipwrecks): an end-of-service date (P576) keeps its precision, so a year-only one is not shown as 1 January
The query read P576 through wdt:, which carries no precision, and a missing
precision counted as a day. It now reads the best-rank statement's value with
wikibase:timePrecision, and a value without a precision is trusted to the
year only. Against the live query service the new query takes 23 s and dates
16 more wrecks while every date the old one gave is unchanged.
2026-11-10 13:19:23 -08:00
Sameh Khamis 0da3a28d76 fix(shipwrecks): a Wikidata date before AD 100 reads as ancient text (300 BC), not as the AD year
parseYear took the minus sign of -0299 as a separator, so a wreck from 300 BC
showed as year 300, sat at AD 300 in the period picker and stretched its
range. Years before AD 100 now keep their year as text ("300 BC", "AD 50")
and count as ancient, like EMODnet's period-dated wrecks; the query service's
XSD 1.1 years (0000 is 1 BC) are converted. Merging records now carries that
text with the date, and coarser-than-year precisions give no date.
2026-11-10 13:17:50 -08:00
Sameh Khamis 32e7a1a913 fix(shipwrecks): skip every wrecksite.eu row, even when the name comes after the 80-character cut or only in the link
The exclusion tested the provenance after it was cut to 80 characters, so a
long source list ending in wrecksite.eu kept the row, and a row whose only
trace was a wrecksite.eu page link was kept too. Both raw values are now
checked before anything is cut.
2026-11-10 13:16:42 -08:00
Sameh Khamis 85b1694bad Merge pull request #1656 from bilawalsidhu/sameh/signal-lost-followups
fix(flights): a stalled feed dims the tracked aircraft as STALE
2026-11-10 13:13:14 -08:00
Sameh Khamis 12116f0ed8 docs(changelog): stalled feed dims the tracked aircraft 2026-11-10 13:03:25 -08:00
Sameh Khamis 447cc08783 fix(flights): a stalled feed dims the tracked aircraft as STALE
When the whole feed backs off, no poll counts a miss, so the tracked
aircraft read STALE at full brightness while no newer position came in.
It now takes the same stale fade as a missed update, in the flights and
military layers.
2026-11-10 13:03:15 -08:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 bd1b16e4ee fix(news): on phones the News panel keeps its height in the Context sheet
It shrank under the Context panel, leaving the feed a strip one card high.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:51:34 +01:00
German Khokhlov 69617af7ef Count only the placer's samples against its budget in the tiles scenario
The scenario counted every sampleHeight call in a task as the placer's
pass, so a single sample from elsewhere in the app that landed in the
same task pushed a full pass to 17. Calls that are neither a fixture
point nor the one camera estimate are now counted apart.
2026-11-11 00:45:36 +04:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 caa6f6f812 fix(news): the browser keeps Local news an hour at most; docs say what the fixes do
Local kept World News API's rows listed after the server had deleted them.
README names News among the routes refused behind a proxy; a timed-out
request is said to count; the cockpit cached state is near the same place.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:41:29 +01:00
German Khokhlov 9e2c424030 Share the surface sample budget between placements in one task
Two triggers firing together (tiles loaded and a camera move end) ran two
passes in the same task, each within 16 samples but 17 or more together.
Placements in one task now share the budget; a placement that finds it
spent continues in the next task, and that continuation starts a fresh
budget.
2026-11-11 00:40:45 +04:00
Sameh Khamis c87510fd2e Merge pull request #1574 from IMBrandao/feat/transit-icon-heading
feat(transit): give vehicle icons a readable front, and none without a course
2026-11-10 12:36:09 -08:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 671a256a6a fix(news): follow-up review of the fixes
Deleted World News API news is not restored by a refresh that fails; a
refund goes only to the day the read was counted in, and not to a request
that timed out; a cockpit brief from another place is dropped, not kept for
a language change; a feed list started over lets scrolling ask again.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:34:58 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 bb7e7ecad6 fix(news): Hebrew strings for the News layer and the gate modules in the news boundary
main added the he locale and the news provider now imports the same-site gate.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:33:01 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 9c0ad0a730 fix(cockpit): a failed refresh after a move never shows the last location's brief
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:26:48 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 369b8e924a fix(news): show the News panel in the phone Context sheet only
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:26:48 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 fd9373c09c fix(news): keep the feed cursor in step with the list and drop Local reads for an area left
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:26:48 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 7cd2af46e4 fix(news): one-hour World News API retention, refunded reads, pins, disputed areas, counts
Keep World News API results an hour at most and say caching needs its
permission; refund a read that never reached it; match Local by any pin;
assign a disputed area by a point inside it; untitled GDELT stories are
minor; hotspot counts leave out minor reports.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:26:48 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 49ac289d00 fix(news): gate /api/news same-site, answer 400 on a bad request line, relay key states
A malformed request line crashed the server, any web page could spend the
news keys, and the forwarding proxy turned the news server's 503 key and
configuration states into a generic 502.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:26:48 +01:00
German Khokhlov 982d197111 Check the surface pass's time budget right before each tile sample
The 4 ms budget was checked at the top of each point, but projecting the
point and reading the terrain came before its sample, so on Cesium a pick
render could start after the deadline. The time is checked again just
before the sample once the pass has taken one.
2026-11-11 00:25:31 +04:00
Sameh Khamis 05daaf267a Merge branch 'main' into feat/transit-icon-heading 2026-11-10 12:24:46 -08:00
German Khokhlov 3ab0bbf7c1 Hide a selected radio station beyond the horizon, also after a switch
Re-adding the selected station's marker reset it to visible, and the
horizon check skips a still camera, so a station selected on the far side
of the Earth showed through the globe until the camera moved. The marker
now starts with the station's current horizon visibility, and the layer
recomputes it once the view is restored after an engine switch.
2026-11-11 00:21:45 +04:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 3342d7a07a Merge origin/main into review/pr-1593
Conflicts: docs/CURRENT-STATE.md layer table, src/ui/layerPanel.js imports,
src/data/dataCredits.test.mjs imports; both sides kept.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-10 21:20:15 +01:00
Sameh Khamis 3da4204e97 Merge pull request #1655 from bilawalsidhu/sameh/route-corridor
fix(flights): a long flight keeps its route up to 15% of its length off the great circle
2026-11-10 11:53:33 -08:00
Sameh Khamis d1cd7f63ef Merge branch 'main' into sameh/route-corridor 2026-11-10 11:45:29 -08:00
Sameh Khamis 9947c6b2e5 Merge pull request #1654 from bilawalsidhu/sameh/flight-trails
feat(flights): trails split at gaps and follow the aircraft; a lost aircraft coasts, then reads SIGNAL LOST
2026-11-10 11:43:22 -08:00
German Khokhlov 697dc91e81 Check a released tileset by height, not by the source label
Cesium labels every scene hit, the globe included, as tiles, so asking
that no answer is labelled tiles after leaving photoreal failed on the
reference engine. The check now asks that no tile geometry answers above
the terrain: a tile-labelled height must match the terrain height.
2026-11-10 23:39:36 +04:00
Sameh Khamis b97b49cf1d docs(changelog): long-route corridor (#1655) 2026-11-10 11:37:14 -08:00
Sameh Khamis 486a78ad8e fix(flights): a long flight keeps its route when it is up to 15% of the route's length off the great circle
The route check allowed 200 km off the great circle between the airports, so a
coast-to-coast flight routed a few hundred kilometres around weather lost its
origin and destination (JFK–SFO over Kansas, 384 km south). The tolerance is
now 200 km or 15% of the route's length, whichever is larger: 623 km for
JFK–SFO, and still 200 km for a short hop, so a wrong leg stays hidden.
2026-11-10 11:37:04 -08:00
Sameh Khamis 8e25105a6c docs(changelog): flight trails and SIGNAL LOST (#1654) 2026-11-10 11:33:14 -08:00
Sameh Khamis 8fec5a76bf feat(flights): a tracked flight's trail splits at gaps, follows the drawn aircraft, and a lost aircraft coasts then reads SIGNAL LOST
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.
2026-11-10 11:30:59 -08:00
Sameh Khamis 66a8ff06cb Merge pull request #1650 from bilawalsidhu/sameh/firms-window-label
fix(firms): label a snapshot that ends at a set time with that time, not LIVE
2026-11-10 10:44:53 -08:00
Sameh Khamis 6eb0729832 fix(firms): translate the snapshot-end label for Hebrew 2026-11-10 10:35:16 -08:00
German Khokhlov e98bf9c485 Merge branch 'german/refactor-traffic-surface-pending' into german/navara-refactor 2026-11-10 22:31:21 +04:00
German Khokhlov be395f0f52 Finish traffic surface sampling with the camera still
Road samples that were not ready, or left over when a pass ran out of its
slice, waited for the next camera move; with the camera at rest they
stayed pending and part of the network never settled on the ground. The
flow now requests the frame it needs and continues the unresolved samples
within the same 12 ms slice and station budget.
2026-11-10 22:31:20 +04:00
German Khokhlov d83f06755f Merge branch 'german/refactor-cold-start-fixes-2' into german/navara-refactor 2026-11-10 22:31:14 +04:00
German Khokhlov 34b4fdce57 Release hidden CCTV geometry and excuse level-of-detail path counts
CCTV kept the coverage and projection geometry of every camera the
opening view passed on its way to the restored one; after each visibility
update it now releases the coverage of cameras out of view and the planes
of inactive cameras, keeping decoded media for reselection.

The cold-start comparison accepts a field entry in the ledger allowlist
that excuses one field of one set with a reason, and uses it for the
infrastructure footprint counts: that level of detail keeps features
chosen on the way to a view so they do not flicker while panning, which a
cold start cannot reproduce. Presence checks ignore field entries. The
fixtures give recent imagery a matching scene and date and traffic its
deterministic starts, and traffic counts as drawn without every loaded
vehicle visible.
2026-11-10 22:31:13 +04:00
German Khokhlov ed47de6a2c Let the tiles scenario allow the page's web fonts
The check for requests to Google and Cesium ion caught the Google Fonts
stylesheet the page loads on every run, which the map policy allows; it
now looks only for tiles, credits and ion.
2026-11-10 22:28:15 +04:00
German Khokhlov ee87482f07 Count the camera ground estimate within the surface sample budget
A pass checked its budget of 16 tile samples before each point, but a
point that needed the estimate of the ground under the camera took two
samples, so a pass could reach 17. The point is now left for the next
pass when the estimate used the last sample.
2026-11-10 22:28:14 +04:00
Sameh Khamis bb45aacb24 Merge branch 'main' into sameh/firms-window-label 2026-11-10 10:27:01 -08:00
German Khokhlov 1d10ba242d Merge branch 'german/refactor-navara-photoreal-release' into german/navara-refactor 2026-11-10 22:15:50 +04:00
German Khokhlov cd571dfb32 Remove a released 3D tileset from Navara's scene and surface queries
With its cache budget on, Navara 0.2.1 deletes a source without emitting
the model-removal events, so the meshes of a released photoreal tileset
stayed in the scene: after switching to another map the buildings were
still drawn, and sampleHeight, clampToHeight, the shared surface
placement and ground drapes still answered from them. The adapter now
disposes the deleted layer's meshes, registry entries and retained
geometry itself, also for tiles that finish decoding afterwards, and
WORKAROUNDS.md records it. A contract check fails when a surface query
still reports tiles after leaving photoreal, with a mutant.
2026-11-10 22:15:50 +04:00
German Khokhlov ab41c765cc Merge branch 'german/refactor-e12-script-regressions-2' into german/navara-refactor 2026-11-10 22:05:58 +04:00
German Khokhlov 6b0d3f4675 Fix the camera picker and oblique traffic waits in two QA scripts
The CCTV camera check lost camera B from the picker when the flight to
camera A changed the cameras in view; the picker now keeps focus while A
is held. The oblique traffic wait compared exact seven-decimal view keys
that a settled camera never reproduced; it compares the coverage with the
current view within small tolerances and prints both keys on timeout.
2026-11-10 21:51:54 +04:00
German Khokhlov c733e4d2ad Merge pull request #1594 from meirm/feat/hebrew-i18n
i18n(he): complete and ship Hebrew
2026-11-10 21:47:12 +04:00
German Khokhlov 59ac26ed8c Merge branch 'main' into feat/hebrew-i18n 2026-11-10 21:37:45 +04:00
German Khokhlov 4bbf9949d5 Merge branch 'german/refactor-navara-tiles-drape-2' into german/navara-refactor 2026-11-10 21:36:39 +04:00
German Khokhlov 88f434ded7 Merge branch 'german/refactor-cold-start-fixes' into german/navara-refactor 2026-11-10 21:36:39 +04:00
German Khokhlov 7c03d68a2f Report the depths behind each drape sample in the tiles scenario
With --report the scenario prints, on Navara, the scene, globe and
combined depths at the sample pixel with the drape hidden, shown and
hidden again, the buffer the ground pass classifies it as, and whether a
shown tile lies on the ray, so a drape on the wrong surface explains
itself.
2026-11-10 21:36:38 +04:00
German Khokhlov 705b7a5314 Make the cold-start comparison wait for the same data and attribute it
Both pages now get the same deterministic SATCAT, NASA image and wildlife
data and advance animation clocks alike; the scenario waits until each
display has published and the registries stay unchanged for two seconds,
and late-created content is attributed to its registered owner. Known
allocation counters are normalised; every slot, count and visibility is
still compared.

Infrastructure drew on attach before the camera was restored, seeding its
level of detail at the incoming engine's default view; it now draws when
the view is restored.
2026-11-10 21:36:38 +04:00
Mazeyar Moeini Feizabadi 9c0c85fd83 Merge pull request #1651 from bilawalsidhu/codex/discord-merged-pr-announcements
Announce merged PRs in Discord #contributions
2026-11-10 18:35:11 +01:00
German Khokhlov b6c2ba379b i18n(he): translate the photo credit added on main 2026-11-10 21:26:15 +04:00
German Khokhlov 97e192ebce Request frames again while waiting for tiles and transitions
A request-mode globe updates its tile state only on drawn frames, so tile
and UI readiness that never asked for one waited until something else
drew: imagery, weather and ground scenarios took several times longer.
The idle checks measure through their own passive observer and are not
affected.
2026-11-10 21:25:08 +04:00
German Khokhlov 2919e90c63 Merge branch 'main' into feat/hebrew-i18n 2026-11-10 21:24:21 +04:00
German Khokhlov 7571e42556 i18n(he): apply the second translation review
- RECORD is «רשומה», a record rather than a recording.
- Time abbreviations end in the geresh again (דק׳, שנ׳, שע׳, ימ׳), and the
  counted forms agree with 1.
- Numbers after a Latin name, ordinals and units keep their order in RTL.
- Meaning, agreement and domain terms fixed across the catalog: cyclone
  advisories and storm centre, sensor swath, track-up, cinematic, tropical
  depression, pilot and sailing vessels, chips, metered billing, radio static,
  airspace, NOT AN ALL-CLEAR, and the remaining review items.
- LOOK and OFF-NADIR are translated; {value} KM stays Latin as an instrument
  readout, like KM/S.
- rtl.css shares the Arabic and Hebrew letter-spacing reset and drops the
  duplicated Arabic rules; he.md and its glossary follow the catalog.
2026-11-10 21:23:28 +04:00
German Khokhlov 555906da6b Merge branch 'german/refactor-e12-script-regressions' into german/navara-refactor 2026-11-10 20:58:16 +04:00
Mazeyar Moeini Feizabadi c9559955f0 ci: announce merged PRs in Discord 2026-11-10 17:57:37 +01:00
Sameh Khamis 4ef0869a87 fix(firms): drop LIVE from the row's source and the voice answer for a snapshot that ends at a set time
The catalog source 'NASA FIRMS · LIVE' still led the row; getStats() now
names the source 'NASA FIRMS' for such a snapshot. get_active_fires says
"in the 24 hours to" its end and reports window_end. The end is checked
to be a printable time, cleared under KEY REQUIRED, the test moves next
to the layer, and the changelog and CURRENT-STATE describe it.
2026-11-10 08:19:40 -08:00
German Khokhlov b5cc4feacb Restore the conditions eight converted QA scripts were waiting for
The harness conversion replaced pauses that stood in for real conditions
with camera readiness: the cables and infrastructure idle checks started
measuring while the overlay was still fading in after moveEnd, directions
sampled before its maneuver dots had ground cells, three scripts clicked
before the loading cover was gone, the credits check hit-tested during UI
transitions, and the oblique traffic flight lost its height cap. Each now
waits for its condition; idle checks observe without requesting frames
and record the reasons of the frames they count; readiness waits for the
startup cover and no longer requests frames while polling.
2026-11-10 20:14:50 +04:00
Sameh Khamis 43706b8dd0 fix(firms): label a snapshot that ends at a set time with that time, not LIVE
A FIRMS source may answer the 24 hours to a set time, naming its end as
`at` (epoch ms). The layer row then reads "24 h to MM-DD HH:MM UTC"
instead of "LIVE · updated ...", and getStats() reports `at`. A
snapshot without `at` (the trailing 24 hours to now, as the dev proxy
serves) keeps the LIVE label. Translated in every catalog.
2026-11-10 08:11:58 -08:00
German Khokhlov 2c62e1e378 Merge branch 'german/refactor-navara-tiles-drape' into german/navara-refactor 2026-11-10 19:12:25 +04:00
German Khokhlov b3daed4a9b Drape a 'tiles' ground set only on 3D tiles on Navara
Navara's ground pass took a pixel for a tile whenever its depth lay in
front of the saved globe depth, without checking which buffer the visible
depth came from; the scene buffer can be cleared apart from the globe
depth, so a set draped on 'tiles' also drew on bare terrain, which Cesium
never does. The pass now matches the visible depth to the globe or the
tiles before it drapes. A contract check compares a 'tiles' set hidden,
shown and hidden again over terrain, with a mutant.
2026-11-10 19:12:25 +04:00
German Khokhlov 087adc6dc9 Count a running mission replay as the camera's owner in readiness
A Space Missions replay flies the camera until it ends, so waiting for a
still camera after a switch let the replay finish before the scenario
looked at it. While a replay runs, readiness no longer asks for enabled
inputs or a still pose, as for the cockpit and a followed target.
2026-11-10 19:11:08 +04:00
German Khokhlov d3433f66c9 Merge branch 'german/refactor-e8-replay-switch' into german/navara-refactor 2026-11-10 19:11:07 +04:00
German Khokhlov 2dcec2195e Check that a running mission replay survives an engine switch
The session test now switches engines in both directions with the replay
running, paused and resumed, and checks it stays active, unpaused and
advancing.
2026-11-10 19:11:07 +04:00
German Khokhlov facc9541ab Merge branch 'german/refactor-e1-readiness-fixes' into german/navara-refactor 2026-11-10 18:41:42 +04:00
German Khokhlov f288b6ba1e Merge branch 'german/refactor-e8-switch-regressions' into german/navara-refactor 2026-11-10 18:41:42 +04:00
German Khokhlov b73fe82e27 Let readiness waits know when the app owns the camera
While the cockpit holds camera inputs or the camera follows a target,
whenSettled no longer waits for enabled inputs or a still pose; it still
waits for the engine, the restored view, drawn frames and no flight, and
its timeout now prints what it saw (inputs, following, cockpit, last pose
change). Scenarios that waited for the camera where they needed the
picture wait for it instead: weather and Nepal imagery for their tiles
and frames, imagery capture while a box drag holds inputs, and pixel and
pick polling for drawn frames. The vessel click retries at the vessel's
rendered position.
2026-11-10 18:31:45 +04:00
German Khokhlov 39206ef4c3 Keep Space Missions and the cockpit through an engine switch
Retaining a disabled layer that was never initialised replayed its
parameters, and the change notice it sent restored the Context snapshot,
which ended the Space Missions mode on every switch. Such layers are
retained as they are again.

The tracked aircraft's handle is rebuilt only once the view is restored,
so the share restore's earlier trackById found nothing to follow and the
cockpit did not come back after a failed switch rolled back. The scene
switcher now takes the aircraft up again after the camera owners resume
and then restores the cockpit and its vision mode. Both are covered by
tests on the real session modules.
2026-11-10 18:29:03 +04:00
German Khokhlov 417890b862 Merge branch 'german/refactor-e8-cold-start' into german/navara-refactor 2026-11-10 17:47:30 +04:00
German Khokhlov 8caff876cb Merge branch 'german/refactor-e12b-fake-engine-tests' into german/navara-refactor 2026-11-10 17:47:29 +04:00
German Khokhlov 560b72c3f7 Merge branch 'german/refactor-e6-tiles-heights' into german/navara-refactor 2026-11-10 17:47:29 +04:00
German Khokhlov 00153acbeb Keep Navara's 3D Tiles where the tileset puts them
Navara's 3D Tiles source defaults to model.height 1, which moves a
transformed tileset by a metre along Earth-fixed X: on the synthetic set
flat roofs read 0.12 m high and a sloping roof 0.68 m off, and the drawn
meshes move with them. The adapter sets the height to 0 and records the
package behaviour in WORKAROUNDS.md, with a test that loads the real
package. The downward surface ray now takes its direction from the
place's own normal, so it no longer drifts sideways.

qa-tiles-fixture gains --report (heights and drape pixels per engine,
without asserting), starts on Cesium, and checks each drape hidden, shown
and hidden again; qa-surface-heights uses the plan's 0.5 m tolerance under
tiles and names the point that misses.
2026-11-10 17:16:23 +04:00
German Khokhlov 7c5ebf4814 Check that an engine switch draws what a cold start from the share link draws
qa-switch-equals-cold-start switches engines in both directions with each
layer on deterministic fixtures, then opens the share link of the same
state in a new page on the target engine, and compares the draw ledgers
and the probe's view of the layer. It runs as 19 acceptance groups
covering all 38 layers.

State a layer keeps through a switch but the share link does not carry is
declared as sessionState (the directions route, Radio's selected station
and audio state, the wind reading, local ADS-B's and OSM Places' local
toggles and results); a test checks each declaration against what the
layer really retains, so a declaration cannot hide a regression.
2026-11-10 17:08:05 +04:00
German Khokhlov 225c1c25c7 Build test engines from the shared fake instead of literals
Tests that made their own engine object now use createFakeEngine() with
its scripting hooks or an override of the one member under test, so they
follow the contract when it changes. 125 such literals in 74 test files
go down to two that are not globe engines on purpose; report-qa-harness
counts them, and the baseline keeps the number from growing.
2026-11-10 17:07:27 +04:00
German Khokhlov 3c304502a0 Merge branch 'german/refactor-e12-qa-harness-2' into german/navara-refactor
# Conflicts:
#	scripts/qa-harness-baseline.json
#	src/layers/satellites/cockpitSubject.test.mjs
2026-11-10 16:45:38 +04:00
German Khokhlov df02033465 Share copied test fixtures through src/testSupport
Environment overrides, manual timers, a cancellable streaming response,
GhostMaps KML/KMZ fixtures and the satellite service doubles move into
five shared helpers; 21 tests drop 418 lines of copies.
2026-11-10 16:43:58 +04:00
German Khokhlov 6c416f3fe6 Move QA scripts to the shared harness, batch 4 of 4
As in the previous batches. Scripts launching Chrome themselves go from
99 to 8 (all outside this conversion), fixed pauses from 86 to 69 and
Cesium-only reads from 75 to 46; the QA harness baseline records them.
2026-11-10 16:43:58 +04:00
German Khokhlov 53b07ba91f Move QA scripts to the shared harness, batch 3 of 4
Each script launches and closes Chrome through scripts/qa/browser.mjs,
waits on the application through scripts/qa/ready.mjs instead of the
loading screen and fixed pauses, and reads the contract instead of
Cesium where the check is not about Cesium itself.
2026-11-10 16:43:58 +04:00
German Khokhlov 373edf2989 Move QA scripts to the shared harness, batch 2 of 4
Each script launches and closes Chrome through scripts/qa/browser.mjs,
waits on the application through scripts/qa/ready.mjs instead of the
loading screen and fixed pauses, and reads the contract instead of
Cesium where the check is not about Cesium itself.
2026-11-10 16:43:58 +04:00
German Khokhlov ee697422c9 Move QA scripts to the shared harness, batch 1 of 4
Each script launches and closes Chrome through scripts/qa/browser.mjs,
waits on the application through scripts/qa/ready.mjs instead of the
loading screen and fixed pauses, and reads the contract instead of
Cesium where the check is not about Cesium itself.
2026-11-10 16:43:58 +04:00
German Khokhlov 731eadc34a Give QA scripts one launch, readiness and geometry helper
scripts/qa/browser.mjs launches headless Chrome with QA_ANGLE and the
shared Chrome lookup; scripts/qa/ready.mjs adds frame, tile and transition
readiness and bounded polling; scripts/qa/geometry.mjs hands page
evaluations the engine-neutral geometry without dev-only source URLs.
CODE-BOUNDARIES gains the rule for new QA scripts.
2026-11-10 16:43:58 +04:00
German Khokhlov 08d5c86e80 Merge branch 'german/refactor-e7-height-datum' into german/navara-refactor 2026-11-10 16:42:50 +04:00
German Khokhlov dd56cba2f3 Measure the heights the surface-bound layers place their points at
qa-surface-heights loads deterministic fixtures for import pins,
infrastructure bases, ALPR markers, directions A/B, bikeshare stations
and Bhote Koshi story points, waits for the app to settle on each engine
and records every placed point through the mover contract. --compare
fails when a point moved by more than 0.1 m on terrain; --tiles checks
the points land on the synthetic tileset's 50 m block.
2026-11-10 16:42:50 +04:00
German Khokhlov 59ff0b38a2 Merge branch 'german/refactor-e8-switch-protocol' into german/navara-refactor
# Conflicts:
#	scripts/engine-coupling-baseline.json
#	src/layers/wind/index.test.mjs
2026-11-10 16:35:39 +04:00
German Khokhlov a2905eb700 Move the engine switch protocol from the layers into the scene binding
attachRenderer takes one context { engine, signal, viewRestored, ... } and
bindCatalog no longer takes a viewer; no layer module receives Cesium's
viewer any more, and a rehearsal check fails if one reads it.

The handoff between engines is one typed object (layers, camera owners,
selection, director) created when the old scene leaves, replacing the
session's handoffLayers, restoringView and selectionContext. viewRestored
resolves where the camera is restored, and the nine layers that kept
their own restoring-view flag wait on it instead. After attach the
binding applies a disabled layer's visibility, registers the picks each
module declares, and resumes the camera owners, so engineSwitch.js no
longer names directions or satellites.
2026-11-10 16:33:29 +04:00
German Khokhlov 9cfac35e38 Merge pull request #1280 from bilawalsidhu/codex/fix-tool-display-text
fix(tools): bound untrusted display text at output boundaries
2026-11-10 16:29:25 +04:00
German Khokhlov d51e4140b7 Merge branch 'german/refactor-e10-layers-without-viewer' into german/navara-refactor
# Conflicts:
#	scripts/package-boundaries.json
2026-11-10 16:16:32 +04:00
German Khokhlov bcff542b16 Merge branch 'german/refactor-e7-surface-placement' into german/navara-refactor
# Conflicts:
#	scripts/package-boundaries.json
2026-11-10 16:15:09 +04:00
Said Turksever 9bd2b7ca37 Merge branch 'main' into feat/viewport-context-decision-upstream 2026-11-10 12:14:56 +00:00
German Khokhlov aac65c21c4 Open the orbital cockpit on Navara
The orbital view, sensor zoom, ground focus and satellite switching run on
the contract: camera.setFovy sets the vertical lens on both engines and the
fake, including sub-degree angles on Navara through the guarded core
bridge, with a contract check and mutants. Cesium's imagery look-ahead
stays inside its adapter. KNOWN-ISSUES keeps the two parts Navara still
lacks: imagery prefetch for the orbital view and finest-detail meshes for
the ship bridge waterline.
2026-11-10 16:14:14 +04:00
German Khokhlov 6727aaadcd Fly, follow and draw the celestial ring through the contract
The Bhote Koshi event flies with engine.camera instead of the viewer's
camera, tools listens to camera.followChanged instead of the viewer's
tracked entity, and the celestial ring uses the engine's host, frames,
pose and render requests. The embedded story media no longer imports
Cesium, which removes one of the three import exceptions.
2026-11-10 16:14:14 +04:00
German Khokhlov 4f654df3c0 Inspect the wind field through the engine contract
The inspection marker and the centre probe use the engine's host,
projection, frame events, ellipsoid pick and render requests instead of
Cesium's scene, so they behave the same on Navara.
2026-11-10 16:14:14 +04:00
German Khokhlov e0d3e93a28 Move the wind's Cesium rendering into the Cesium adapter
The particle canvas, its GPU path and the relief shading move unchanged
from src/layers/wind/ to src/globe/cesium/; the adapter reads the Cesium
viewer itself, so the layer no longer hands one in.
2026-11-10 16:14:14 +04:00
German Khokhlov 82ed68f443 Place every surface-bound layer through the shared placement
Import pins, infrastructure, ALPR, directions markers, bikeshare and the
Bhote Koshi story use createSurfacePoints instead of their own sampling,
budgets and partial triggers; ground snap, the mesh floor sampler,
traffic, transit, Street Level, the camera ground guard, wildlife and the
cockpit host read heights through readSurface or surfaceTriggers.
Per-layer offsets and floor policies stay as options. The import no
longer re-places pins on every camera move from the UI.

Calls to engine.surface outside src/globe go from 41 sampling calls to
10, each a raycast, a one-shot fly-to height or an asynchronous detailed
sample.
2026-11-10 16:13:06 +04:00
German Khokhlov ef104aaac3 Move surface placement into the globe and give it the fourth trigger
src/globe/surfacePlacement.js takes over the shared surface placer and
adds readSurface (tiles through sampleHeight, otherwise terrain, with a
plausibility range), surfaceTriggers (one subscription to camera moves,
tile loads, the photoreal switch and terrain revisions) and a forced full
pass. Points now re-place when the terrain provider changes, which they
did not before. GhostMaps, cables, OSM Places and Street Level move to the
new module unchanged.
2026-11-10 16:13:06 +04:00
German Khokhlov 50e486ceec Merge branch 'german/refactor-e11-boundary-catalogs' into german/navara-refactor
# Conflicts:
#	scripts/package-boundaries.json
2026-11-10 16:08:08 +04:00
German Khokhlov 75cf0aeaf9 Merge branch 'german/refactor-e4-model-retry' into german/navara-refactor 2026-11-10 16:04:06 +04:00
German Khokhlov a07d9f5d85 Retry a failed 3D model inside the engine instead of in the layers
A mover set whose glTF or atlas fails to load retries by itself after
retryAfterMs (1,500 ms by default), at most three attempts, on Cesium,
Navara and the fake engine alike: failed() is true while the latest
attempt failed and stays true after the last, ready() turns true when a
retry loads, and an instance given another pickId loads again. Healthy
models of the same class no longer load again when one fails.

The layers drop what they did instead: moverBank's rebuilds, the loops
that dropped failed handles in flights, military and local ADS-B, and the
tracked-model backoff in the aircraft display. The contract suite checks
the one behaviour on both engines, with a mutant that never retries.
2026-11-10 16:04:01 +04:00
German Khokhlov 27939a2d84 Own shared foundations by directory and check the manifest both ways
src/i18n/, src/geo/ and the engine-neutral part of src/globe/ are owned by
directory entries, and aggregate groups use a directory wherever their
list covered it, so a new shared module no longer needs a manifest edit.
Small groups keep explicit lists, now checked as a snapshot: the check
fails on a reached module that is not declared and on a declared one no
longer reached, and prints both per group. --write keeps directory
entries as they are.

The manifest goes from 4,737 entries to 3,494; src/i18n accounts for 87
of them instead of 666.
2026-11-10 15:59:43 +04:00
German Khokhlov 1ac9f28c4a Merge branch 'german/refactor-e6-tiles-fixture' into german/navara-refactor
# Conflicts:
#	scripts/qa-harness-baseline.json
2026-11-10 15:54:13 +04:00
German Khokhlov e9ba5e9859 Check the photoreal paths in the browser on a synthetic 3D Tiles set
A generated tileset (a plate at ellipsoid level, 50 m and 200 m blocks and
a pitched roof, 27 KB) replaces Google's tiles when a QA page passes
?qaTileset=/path; only a root-relative path on the page origin is
accepted, and the Google credit is not shown for it. The strict harness
serves the set from disk, so nothing reaches Google.

qa-tiles-fixture checks on both engines that sampleHeight reads the block
at 50 m, clampToHeight lands on the roof, tiles and terrain drapes draw
where they should, tilesLoaded turns true, surface points land on the
roof within their budget, and pick renders per pass stay within it.
2026-11-10 15:49:52 +04:00
German Khokhlov 37263a3e2b Give the draw ledger test a drawn mover instance
A mover set lists as shown only once one of its drawn instances is.
2026-11-10 15:48:49 +04:00
German Khokhlov 18da76e8d8 Merge branch 'german/refactor-e1-qa-readiness' into german/navara-refactor
# Conflicts:
#	scripts/qa-harness-baseline.json
#	scripts/qa-strict-harness.mjs
2026-11-10 15:47:21 +04:00
German Khokhlov 60f9de8511 Wait for the application, not the clock, in the strict QA set
The probe exposes settled() and the first-frame outcome; scripts/qa/ready.mjs
waits for drawn frames with the camera at rest and for a pick at the click
position, with a deadline and its reason in the error. The strict scenarios
use these instead of fixed pauses and loading-screen polling, and the
hidden-switch scenario checks how the loading screen was lifted instead of
how long it took.

Each switch scenario now records a draw ledger per engine and runs a
contract monitor; both write to the scenario's evidence file only, with an
empty allowlist until browser runs show what differs on purpose.
2026-11-10 15:46:26 +04:00
German Khokhlov 5b27f26e14 Merge branch 'german/refactor-e5-navara-mesh-colour' into german/navara-refactor 2026-11-10 15:45:37 +04:00
German Khokhlov 28f3030c31 Draw Navara world meshes in the overlay pass after tone mapping
Meshes now go through the same overlay as sprites and lines, so their
colours are the ones asked for without undoing the view's exposure. The
contract suite no longer carries the mesh colour check as a known
failure on Navara.
2026-11-10 15:45:26 +04:00
German Khokhlov 353eebf224 refactor(tools): normalize ALPR text once, in the records
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.
2026-11-10 15:37:16 +04:00
German Khokhlov 12a5630991 docs: tighten the display-text notes
Name routeId among the canonical analyst IDs in CURRENT-STATE, say what the
changelog entry changes for users, and drop the comment that repeated the
canonical-field doc.
2026-11-10 15:34:32 +04:00
German Khokhlov a90407d547 Count the Navara canary script in the QA harness baseline
It drives a bare Navara view outside the app to test the package itself, so it launches Chrome and paces its fixture on its own.
2026-11-10 15:30:09 +04:00
German Khokhlov 33ccef9662 Merge branch 'german/refactor-e3-contract-behaviour' into german/navara-refactor 2026-11-10 15:27:12 +04:00
German Khokhlov 49b180f1fd Merge branch 'german/refactor-e9-import-plan' into german/navara-refactor 2026-11-10 15:27:12 +04:00
German Khokhlov 233c4def5b Merge branch 'german/refactor-e2-layer-lifecycle' into german/navara-refactor 2026-11-10 15:27:12 +04:00
German Khokhlov 0953287c3d Merge branch 'german/refactor-e13-navara-workarounds' into german/navara-refactor 2026-11-10 15:27:12 +04:00
German Khokhlov b03edaaf2b Check the globe contract by behaviour on every engine
The contract suite is split into one module per area and grows from 87
to 114 checks, each named after the member it exercises: a gesture during
a flight does not cut it, a render request inside a frame draws the next
one, a pure rotation raises changed, ground corridors are as wide in CSS
pixels at device scale 2, imagery replans on resize, drapes draw only on
their surface, sampleTerrainMostDetailed keeps order and passes on a
rejection, terrainRevision changes on a new terrain and on fallback,
follows raise followChanged on retarget, and a mesh, a ground fill and an
imagery pixel draw the colour asked for. In the browser Cesium passes all
113 it runs at device scale 1 and 2; Navara passes 110 of 111 and reports
its single-colour mesh, drawn before tone mapping, as a known failure.

scripts/contract-mutants.test.mjs breaks one promise of the fake engine
at a time, 30 mutants in all, and names the check that catches each; the
list of contract members the suite never calls is down to five.

maps.errors reports tile and terrain errors where an engine has them
(the tileErrors capability: Cesium yes, Navara no), and the map
controller falls back to another map only where it does; movers.list()
rows tell whether a set is shown, as the other families do; FollowHandle
.name and PickResult.primitive are marked deprecated. qa-contract-suite
takes --dpr and the ANGLE backend from QA_ANGLE.
2026-11-10 15:26:55 +04:00
German Khokhlov 9ae7dea918 Plan KML and KMZ drawing in one pure function, checked against Cesium
src/data/geoImportPlan.js decides what an imported file draws and with
what: raised or clamped geometry, rings and holes, rotation near the
poles and across 360 degrees, absolute and relative altitudes, draw
order, canvas budgets and the vertex and ring limits, with no engine and
no DOM. drawImportedFile draws that plan through the globe contract and
keeps pin placement, yielding and cancellation. Every limit of the import
lives in src/data/geoImportLimits.js with what it protects.

scripts/fixtures/geo-import holds 13 files covering those cases, and
reference.json what Cesium's own KML and GeoJSON data sources make of
them; scripts/qa-geo-import-reference.mjs records it and compares the
plan with it. Property tests on seeded random polygons check area,
bounds, ring closure and winding. Writing the plan fixed three cases:
overlays below the ellipsoid kept a negative height, raised plain-colour
overlays now draw, and the vertex limit no longer depends on whether a
ring repeats its first point.
2026-11-10 15:26:10 +04:00
German Khokhlov ffbceb977c Register the Navara package workarounds and check them against the package
src/globe/navara/WORKAROUNDS.md lists every place the adapter works
around @navaramap/three 0.2.1: what the package does wrong or lacks, how
it is detected and what to delete once it is fixed.
scripts/qa-navara-canaries.mjs confirms three of the bugs on the real
package in the browser (batch ids never released, events of a deleted
vector layer kept queued, idle reported before a raster tile arrives) and
names the workaround to delete when one is gone.

The private members the adapter reads are probed when the engine starts,
with an error naming the member and the pinned version instead of a
silent fallback, and the test that only internals.js reaches private
members now scans the adapter's subfolders too.
2026-11-10 15:22:43 +04:00
German Khokhlov 20ad9b41b3 Rehearse every catalog layer across two engines in the unit tests
lifecycleRehearsal.test.mjs takes each of the 38 catalog layers from
attach on one fake engine through enable, detach and that engine's
destruction to attach on a second engine, resume and disable. It checks
that the engine left keeps no drawn sets, listeners, credits, input
suspensions, continuous renders or follows; that nothing writes to it
afterwards; that a layer waiting for the camera restore reads no source
before resumeViewport; that a disabled layer stays hidden after attach;
and which layers still read the viewer argument, a list that may only
shrink.

The catalog takes the displays and scene services a test supplies, and
the pick registry lists its owners. Two defects it found are fixed:
Active Fires loaded its heatmap before the camera restore it was told to
wait for, and the wind inspection marker asked Cesium's scene for a
render on any engine instead of the engine's render request.
2026-11-10 15:01:59 +04:00
German Khokhlov bd4f9eaee4 Keep the strict scenarios in step with main's photos and satellite groups
Main's flight photos ask Planespotters for every tracked aircraft, which
the strict network policy counted as a violation; the harness now answers
those lookups with no photos, as the other QA scripts already do. Main
also reads five more TLE groups (resource, weather, planet, SAR and
science): the satellite fixture serves them, and the space switch expects
all twelve groups and derives its catalog sizes from the fixture.
2026-11-10 15:01:39 +04:00
German Khokhlov 3521c96172 Merge remote-tracking branch 'origin/main' into codex/fix-tool-display-text 2026-11-10 14:32:55 +04:00
German Khokhlov 1b010fe328 fix(voice): keep transit route IDs out of display-text normalization
A follow-up analyst_query filters on the routeId it was handed back, so a
route ID with repeated spaces or over 180 characters must stay byte-for-byte
raw like the other canonical fields.
2026-11-10 14:31:28 +04:00
German Khokhlov 5874773ad9 Merge pull request #1577 from bilawalsidhu/german/vessel-callout-under-card
Hide a vessel's detection callout while its own card paints
2026-11-10 14:23:33 +04:00
German Khokhlov 9c2a2d6951 Measure engine coupling, QA harness use and contract coverage
Counters that may only fall, each with a committed baseline and a test
that fails on growth:

- report-engine-coupling.mjs gains layer modules that take the viewer
  in attachRenderer (33), layer folders with their own "view being
  restored" flag (9), engine.surface calls outside src/globe (41) and
  layer ids named in the engine switch (3).
- report-qa-harness.mjs counts QA scripts that launch Chrome themselves
  (98), wait with fixed sleeps (117), take the hidden loading screen as
  readiness (9), read Cesium-only objects (75) or define their own
  waitFor helpers (19).
- withCallLog records every contract member the contract suite calls on
  the fake engine; contractCoverage.test.mjs fails when a member outside
  contract-uncalled-baseline.json goes uncalled, and when a listed one
  is called now, so the list of 12 only shrinks.
2026-11-10 14:23:29 +04:00
German Khokhlov ba04ceaa32 Describe the strict page clock as the nearest 18:30 UTC 2026-11-10 14:23:29 +04:00
German Khokhlov ed6ae54ef6 Stop the strict acceptance from waiting for itself
A scenario whose body ran past about 19 s idled until 45 s after its
start, and twice one never exited and hit the runner's 300 s timeout
after it had passed: something of Chrome outlived Browser.close and kept
its stdout and stderr open.

- scripts/qa/browser.mjs closes Chrome within 5 s, kills its process
  group and closes its pipes; the harness then finishes and exits with
  the scenario's code. QA_EXIT_DIAGNOSTICS=1 keeps the old ending and
  prints the open handles and Chrome's process tree 5 s after finish.
- Every strict page starts with its clock at the 18:30 UTC nearest to
  the real time, so Navara's sun is up over the test cities whenever the
  acceptance runs; a scenario can ask for its own instant or the real
  clock (Wildlife does, for its freshness fixtures).
- The runner records a scenario that passed but did not exit as hung,
  kills the whole process group on a timeout, runs a failed scenario
  once more and reports one that passes then as flaky, and appends each
  run to --history=FILE; scripts/report-acceptance-flake.mjs reads it.
2026-11-10 14:10:46 +04:00
German Khokhlov 20d58cf888 Report zero queued alpha for an entry the yield pass hid
getQueuedOverlayEntryAlpha now returns 0 for a yielding entry that
declutterYieldingEntries marked collisionHidden this frame. That pass runs
before custom lanes, so they no longer read full opacity for an entry that
will not paint.
2026-11-10 14:06:24 +04:00
German Khokhlov a913e5466e Fill raised KML polygons up to 32,768 vertices
A raised polygon past 4,096 vertices lost its fill, which main drew.
Triangulating 32,768 vertices takes about 58 ms on a fast laptop, so the
fill limit moves there; 128 rings stay the hole limit, since holes make
earcut quadratic. The polygon loop yields to the event loop after 8 ms of
work, and the import cancels a draw whose globe went away while it
yielded, without reporting the file as unreadable.

KNOWN-ISSUES names the fill limit and two Navara limits: an expired
terrain token is not refreshed while the page is open, and a failing map
source does not fall back to another map.
2026-11-10 14:03:35 +04:00
German Khokhlov 0af33707da fix(voice): restore the globe-rotation import a main merge dropped
The earlier main merge replaced `import { interruptGlobeRotation } from
'../cameraVerbs.js'` with the displayText import while keeping the
`interruptGlobeRotation('voice-zoom-intent')` call, so voice zoom actions
threw ReferenceError. The same merge replaced main's Overpass reservation
paragraph in docs/CURRENT-STATE.md; restore it next to the new text.
2026-11-10 13:58:48 +04:00
German Khokhlov 39246adaf7 Merge remote-tracking branch 'origin/main' into codex/fix-tool-display-text
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.
2026-11-10 13:58:41 +04:00
German Khokhlov 2a761aa5eb Merge origin/main into german/navara
Main's work since the last merge runs on the globe contract where this
branch draws through it:

- Satellites take main's classes, sensors, ground-track bearings and
  world time through contract points and rings; launches take the world
  timeline. The satellite's cockpit subject dims, copies positions and
  refocuses through the contract.
- Flights, military and vessels keep main's photos, retry cool-downs,
  world-clock motion and the ship bridge. Trail seeding in the shared
  aircraft tracking reads each layer's clock.
- Traffic's flow lines and single build draw as ground sets; transit
  keeps dated credits and the new cities; CCTV opens the nearest camera,
  and the Quebec overlay projects and tests the horizon through the
  contract. Bikeshare's manager cleanup moved into its current lifecycle.
- Draw keeps main's mark actions across engine switches.
- The world clock in src/time is plain numbers again: Julian dates come
  from src/geo, the frame loop is injected, and the binding to Cesium's
  viewer clock lives in src/globe/cesium/worldTime.js. Navara's sky and
  sun follow the world time; live feeds keep the real time.
- The cockpit's orbital view, with its sensor zoom, ground focus and
  satellite switching, stays Cesium-only for now: Navara hides its entry.
  Its Cesium calls live in src/globe/cesium/orbital.js. The ship bridge's
  finest-detail waterline sampling is Cesium-only too; Navara samples
  loaded geometry and detailed terrain. Both are in KNOWN-ISSUES.
- Main's tests that built a Cesium scene by hand run on the fake engine.
- The boundary manifest is regenerated, the coupling baseline records
  the orbital and bridge code, and the main-chunk baseline is raised to
  the merged size: main itself grew its main chunk by 93,959 bytes
  (gzip -9) since the previous merge base.
2026-11-10 13:52:52 +04:00
German Khokhlov f437c615a0 Add a --write mode to the package boundary check
`node scripts/check-package-boundaries.mjs --write` regenerates each
group's module list in scripts/package-boundaries.json from what the
group's build reaches: entries that own nothing reached are removed, newly
reached modules are appended in sorted order, and the order of kept
entries stays. The check without the flag is unchanged.
2026-11-10 12:56:36 +04:00
German Khokhlov 8eb0173940 Merge branch 'main' into german/vessel-callout-under-card 2026-11-10 12:47:09 +04:00
manjunath22466 e7325685e5 docs: restore GitHub handles in the #835 changelog credit
Recent fragments credit as name, handle and PR number; keep that form.
2026-11-10 13:30:41 +05:30
manjunath22466 fd2200effa docs: credit viewport diagnostics contributors (#835) 2026-11-10 12:36:22 +05:30
manjunath22466 26b268a06c chore: merge main into viewport diagnostics pr (#835) 2026-11-10 12:36:12 +05:30
Sameh Khamis bb40ac5e54 Merge pull request #1647 from bilawalsidhu/sameh/flight-photo
Show the tracked aircraft's Planespotters photo in Contacts
2026-11-09 21:21:22 -08:00
manjunath22466 1cd21d784a Merge main into viewport diagnostics PR (#835) 2026-11-10 10:45:23 +05:30
Sameh Khamis 9fe27c2d75 Merge branch 'main' into sameh/flight-photo 2026-11-09 21:13:16 -08:00
Sameh Khamis c620fac9dc Merge pull request #1645 from bilawalsidhu/sameh/traffic-lines-one-build
perf(traffic): build road lines one set at a time
2026-11-09 20:41:55 -08:00
Sameh Khamis a904562f0f Changelog fragment for #1647 2026-11-09 20:36:34 -08:00
Sameh Khamis 1a8defbfc8 feat(flights): show the tracked aircraft's Planespotters photo in Contacts
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.
2026-11-09 20:36:03 -08:00
Sameh Khamis b996e35b0c docs(changelog): traffic lines build one set at a time 2026-11-09 20:31:53 -08:00
Sameh Khamis 4dc51927bc perf(traffic): build road lines one set at a time
With Lines on, every progressive paint of a loading city rebuilt all road
lines: up to three ground-line primitives for as many as 1,500 roads, built
asynchronously. Builds started faster than they finished, and each kept the
set it replaced in the scene until it was ready, so sets piled up.

One set now builds at a time. A rebuild asked for while lines are building
replaces the one waiting, and the newest starts when the build finishes.
Lines replaced by a finished build are removed before the next one starts.
Turning Lines off removes the lines at once and drops the waiting rebuild.
2026-11-09 20:31:28 -08:00
Sameh Khamis 78c5b6c245 Merge pull request #1641 from bilawalsidhu/sameh/draw-shape
Applications can take the pointer for their own tool
2026-11-09 17:38:26 -08:00
Sameh Khamis 28907caf7b Merge branch 'main' into sameh/draw-shape 2026-11-09 17:30:15 -08:00
Sameh Khamis a7f96313e9 feat(pointer): an application's own tool can take the pointer
createApplicationTools returns pointer: claim(owner) takes the pointer as
Draw does and returns its lease, or null while another tool holds it;
release(lease) gives it back; isFree() lets the application's own click
handlers yield, as the layers' do. This replaces drawShape, which this
branch added: Draw is as it was.
2026-11-09 16:56:59 -08:00
Sameh Khamis da134328ef Merge pull request #1642 from bilawalsidhu/sameh/transit-floor-fix
Keep vehicles off rooftops and bridge decks by reading the 3D-tile surface where each vehicle is
2026-11-09 15:12:25 -08:00
Sameh Khamis 09520bc34c Changelog fragment for #1642 2026-11-09 14:54:51 -08:00
Sameh Khamis 9bce41d3c6 Read the mesh floor where transit vehicles and grounded flights actually are
With readings kept per point, the floor a vehicle gets depends on which point
is probed and where it is read. Transit offered the sampler the first point of
each vehicle's path in a cell, usually its oldest fix there, so a vehicle that
stayed in one cell never had its current spot probed. It now offers the latest
point of the path in each cell, and the newest fix in the vehicle's own cell
(the selected vehicle's history is walked newest first).

The grounded-flight display clamp read the floor at its sticky cell's centre,
which now answers from the reading nearest the centre, not the contact. It
reads at the contact's own position, held inside the sticky cell so boundary
hysteresis still applies.
2026-11-09 14:51:35 -08:00
Sameh Khamis 04b8c856ad Keep vehicles off rooftops and bridge decks: probe the mesh where the vehicle is
On the photoreal stack the rendered-mesh floor took one probe at each coarse
cell's centre (0.001 degrees) and kept it for the session. A centre on a roof
passed the +80 m DEM window and lifted every vehicle in the cell to roof height
(a Haymarket bus drew 23.5 m above the street, on the garage roof); a centre in
the river put Red Line trains on the Longfellow Bridge 6-9 m under the deck.

The sampler now probes at the caller's point. A cell with a reading is probed
again only at spots about 17 m apart from earlier ones, eight at most, and a
spot refused only because the cell's DEM prior was cold is tried again later.
The floor keeps each reading that passes the gate at the point it was taken
(two within about 5 m keep the lower; sixteen at most per cell) and answers a
read from the reading nearest the asked point, so a viaduct and the street
beside it in one cell keep their own heights.
2026-11-09 14:51:35 -08:00
German Khokhlov 60fb8fee07 Fix the follow-up review findings in import, layers, startup and engines
- KML/KMZ import: a per-file canvas budget and cache for ground overlays,
  limits on rings and vertices of raised polygons, overlays across 360° and
  near the poles, degenerate holes, polar and single-latitude fills,
  absolute overlays at their altitude, overlay order by drawOrder as on
  Cesium, and pins on tiles keep their measured height.
- Points on the surface: OSM Places and Street Level cones share the
  surface sampler and sit on the terrain before they are measured, within
  a sampling budget per pass; Cockpit's contact-dot bank keeps the dot
  atlas, and a failed fleet model loads again after a pause.
- Startup: the loading screen lifts after the share-link view is restored,
  the retry screen names the failed engine and takes focus, and the
  fallback toast stays for 6 s.
- Engines: an engine switch lands a running camera flight at its
  destination (camera.completeFlight on both engines), the sighting card
  leaves focus on the engine picker after a switch, and a Cesium model
  error stays with the instance that failed.
- The MCP panel builds again: its wind-mesh alias resolves to the stub.
- QA: the wildlife switch waits for the camera to rest, GBFS --w2 says it
  needs the dev server, and test fixtures are shared instead of copied.
2026-11-10 01:57:11 +04:00
Meir Michanie f79ceb690f i18n(he): apply review (#1594)
Before merging: singular 'one' forms from the review table (44 entries),
counters keep their order across the slash (מתוך, as Arabic المن), the
lightning layer says ברקים (the legend now counts ברק strikes, the help
text keeps 110°E Latin), configured-elsewhere keys are promised
untouched, Punk is פאנק רוק, dock/panel side labels follow the mirrored
layout, and the HUD-summary English guard covers he (NON_LATIN_LOCALES,
tested) — documented in Adding a language along with format-scope.

Suggestions and nits: restricted is מוגבל, stage recovery is החבה not
שחזור, voice-inactivity names the behavior, lane directions use
attributes (השמאלי) and around is לאחור, Ironbow and KM/S stay Latin in
same-as-english, {age} no longer doubles לפני, typos and garbled words
fixed (תנאים, ידנית, ודאות, מקלט, זמניים, ערוץ 14, מעונן חלקית, אין אות
קריאה, Tab לתצוגה מקדימה, השאר ריק, היוועץ), counted units shortened
(שנ' דק' שע' ימ'), terminology unified (briefing תדריך, forecast תחזית,
course מהלך, אונייה, שריפה), rtl.css shares the mono redirect under
:is(:lang(ar), :lang(he)) and its comments match the rules,
worldOverlayTokens adds Noto Sans Hebrew to the canvas card fonts, he.md
gets the fraction-aware plural table, iw/iw-IL map to he (tested).
2026-11-09 14:41:36 -06:00
Meir Michanie 23609b94b6 i18n(he): follow main's place names 2026-11-09 14:41:36 -06:00
Meir Michanie 3dd89af354 i18n(he): follow main's orbital site focus
- Translate the 12 new strings: site standby and Live-Signal pings,
  releasing and keeping the camera on a subject, riding a satellite,
  the focus and return chips, unnamed mapped sites.
- OFF-NADIR stays as printed via same-as-english.
2026-11-09 14:41:36 -06:00
Meir Michanie e35f38df8c i18n(he): follow main's satellite classes, orbital and bridge views
- Rebase on origin/main and translate the 130 new strings: event-age
  filter, satellite class groups (STATIONS/IMAGING/TELECOM/STARLINK/
  PROBES/DEBRIS) with CelesTrak SATCAT context, TomTom traffic limits,
  the ship bridge view (look/hold/radar/SOG/COG), the orbital view
  (swath, off-nadir, TLE, sub-satellite point, nadir/track-up) and the
  world-time play speeds.
- Instrument readouts keep the Latin codes (LOOK/STBD/PORT, NM, NORAD,
  EPOCH, TLE, OFF-NADIR, STARLINK, EYE, SWATH variants) via
  same-as-english.
- Prune 3 entries whose English main replaced.
2026-11-09 14:41:36 -06:00
Meir Michanie 25229a8e4e changelog: Hebrew (#1594) 2026-11-09 14:41:36 -06:00
Meir Michanie 584732c3ec i18n(he): complete and ship Hebrew
- 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.
2026-11-09 14:41:36 -06:00
Mazeyar Moeini Feizabadi e5da073d33 Merge pull request #1638 from avaldini/fix/realtime-voice-unicode-patterns
fix(voice): send OpenAI Realtime tool schemas the API accepts
2026-11-09 20:32:38 +01:00
Mazeyar Moeini Feizabadi ddd07f13f0 Merge branch 'main' into fix/realtime-voice-unicode-patterns 2026-11-09 19:44:29 +01:00
Kaj ObstandClaude Opus 5.5 cdccf88bbb i18n(sv): address review (export buttons, singular, consistent terms)
- EXP. {slot} and EXPORT so the Recent Imagery export buttons fit and A
  and B read apart; EXPORT listed as same-as-English.
- {count} HÖRT / hört for one aircraft; the plural rule in sv.md no
  longer lists HÖRDA.
- INTE FARA ÖVER throughout; VÄ/HÖ for left/right (V is west on the
  compass); STYRD KURS vs KURS Ö. GRUND; MIK in the first-run tip.
- Label-first counts for sightings and standby cameras; neuter audio
  states; OSÄKER on chip and status line; KN for knots; the ellipsis
  character; Ladda ned and VÄNTLÄGE throughout; Olagligt ingripande;
  FÖRHANDSVY for the preview flag and authoring heading.
- sv.json in the format scope; Swedish in language-code order in
  TRANSLATORS.md; glossary row reduced to incident → händelse; 29
  letterless entries dropped from same-as-english.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:25:24 +01:00
German Khokhlov 10784fa9c9 Merge pull request #1552 from IMBrandao/fix/firms-drop-snpp
fix(firms): stop requesting the retired Suomi NPP source
2026-11-09 21:52:09 +04:00
German Khokhlov fce5ee6381 docs(firms): tighten the Suomi NPP notes 2026-11-09 21:44:09 +04:00
German Khokhlov d90da158ea Merge branch 'main' into fix/firms-drop-snpp
# Conflicts:
#	docs/CURRENT-STATE.md
2026-11-09 21:41:01 +04:00
German Khokhlov b04fcf191c docs(firms): count two VIIRS sources in the layer table and test note 2026-11-09 21:38:07 +04:00
German Khokhlov 0ef29781e5 Fix the review findings across the engines, layers, data and QA
Navara now matches Cesium where the review found it diverging: a flight
holds the camera inputs, camera.changed counts turns as Cesium does,
render requests made during a frame survive it, follow pinches zoom,
ground widths follow the pixel ratio and Cyber sonar reaches glyph caches
and models. On Cesium, a staged imagery swap keeps loading tiles, setView
replaces a running flight, and a cleared weather detail goes away.

The contract gains MoverSet.failed, maps.terrainRevision,
render.maxTextureSize and surface.sampleTerrainMostDetailed on every
engine, so layers retry failed models, key road heights by terrain,
size weather textures to the GPU and place Bhote Koshi cards on detailed
terrain.

Engine switches keep the live view, follows, focus, layer options and
cards: a pick hands over the exact pose, applies no address options and
pans no carried-over card; a history visit still applies its entry.

Layers put GhostMaps pins, cable landings, OSM Places and directions on
the measured surface without per-frame sampling, draw the Cockpit contact
dot, hide disabled CCTV on attach, and keep ALPR running outside secure
contexts. KML imports draw raised polygon fills, rotated and coloured
ground overlays and north-referenced icon headings, and report files
with nothing to draw.

QA scripts read the contract instead of Cesium internals (test:track is
109/109 again), wait for the camera to rest before clicking, and give
cold switches and parked engines realistic time. Docs follow the code.
2026-11-09 21:34:42 +04:00
German Khokhlov 581ef7eef4 Merge pull request #766 from BreezeDelegate/fix/node26-allocation-gates
fix(ci): run allocation gates on Node 26
2026-11-09 20:35:28 +04:00
German Khokhlov 015e3ff7fc Merge branch 'main' into fix/node26-allocation-gates 2026-11-09 20:19:29 +04:00
German Khokhlov 72a240ba6f Merge pull request #762 from monwiss/refactor/tile-budget
refactor(providers): move the tile budget helpers out of the TomTom module
2026-11-09 20:00:55 +04:00
German Khokhlov 77df600329 Merge branch 'main' into refactor/tile-budget 2026-11-09 19:52:40 +04:00
Said Turksever b4c03a1c0c Merge branch 'main' into german/vessel-callout-under-card 2026-11-09 15:50:25 +00:00
Said Turksever 0dbc657bf9 Merge pull request #1576 from bilawalsidhu/german/transit-bikeshare-empty-state
Say why Transit and Bike Share rows are empty outside coverage
2026-11-09 15:48:34 +00:00
German Khokhlov e22348a8e1 Merge branch 'main' into refactor/tile-budget 2026-11-09 19:43:58 +04:00
German Khokhlov 75e8e7ddbd refactor(data): describe tile budgets per period, drop a duplicate test, add the changelog entry 2026-11-09 19:41:51 +04:00
Said Turksever 9787d9c4cc Merge branch 'main' into german/transit-bikeshare-empty-state 2026-11-09 15:32:48 +00:00
Said Turksever 2725f307a6 Merge pull request #1628 from IMBrandao/feat/transit-more-cities
feat(transit): add 34 keyless cities across Europe, Asia, Oceania and North America
2026-11-09 15:30:57 +00:00
Said Turksever cf05a29b7f Merge branch 'main' into feat/transit-more-cities 2026-11-09 15:20:12 +00:00
German Khokhlov 45108a0b0b Merge pull request #794 from daikaginza/fix/api-method-guard
fix(server): declare the method contract on the three read-only query proxies
2026-11-09 19:17:49 +04:00
Sameh Khamis 0600d86584 docs(changelog): Draw hands one shape back (#1641) 2026-11-09 07:16:52 -08:00
Sameh Khamis 4da477a654 feat(draw): hand one drawn area or line back to the application
createApplicationTools returns drawShape({ shape, signal }): it opens Draw
for one area or line and resolves with its Polygon or LineString without
placing a mark, or null when draw mode is left, the signal aborts, a newer
request replaces it, the pointer is held elsewhere or the tool goes. The
shape controls cannot change the asked-for shape, and a shape across the
antimeridian is refused as a row's actions refuse it and drawn again. It
also returns pointerFree, so an application's own click handlers yield to a
tool holding the pointer, as the layers' do.
2026-11-09 07:16:14 -08:00
Said Turksever c1ca0f489c Merge branch 'main' into feat/transit-more-cities 2026-11-09 15:13:18 +00:00
German Khokhlov cdf52ab290 test(server): drop unused fields from the /track hand-off stub 2026-11-09 19:07:14 +04:00
German Khokhlov c3738c0fdb docs(server): tighten the method-guard comments and changelog 2026-11-09 19:07:14 +04:00
Said Turksever cbb0a391f2 Merge branch 'main' into feat/transit-icon-heading 2026-11-09 15:06:36 +00:00
German Khokhlov ace88172d3 Merge branch 'main' into fix/api-method-guard 2026-11-09 18:51:02 +04:00
German Khokhlov 8b0b975e18 Merge branch 'main' into sv-locale 2026-11-09 18:45:01 +04:00
German Khokhlov 6a7a09372a Merge pull request #795 from daikaginza/feat/opensky-compress-cached-body
feat(server): compress the cached OpenSky snapshot for clients that accept gzip
2026-11-09 18:40:19 +04:00
Denis Groshev 0fdde159cb Merge branch 'main' into denis/gev-news-layer
# Conflicts:
#	DATA_SOURCES.md
#	docs/CURRENT-STATE.md
#	src/data/local_data/natural_earth/README.md
#	src/i18n/same-as-english.json
#	style.css
2026-11-09 15:35:56 +01:00
Said Turksever 3e630cf34f fix(transit): mark route line names as kept for i18n
Métro, Calypso, LD and the metro letters are line names as signed, not UI
copy; the i18n catalog check flagged them.
2026-11-09 14:34:33 +00:00
German Khokhlov 9d7fe8fcaa Merge branch 'main' into feat/opensky-compress-cached-body 2026-11-09 18:30:32 +04:00
German Khokhlov a45d6af5f7 Merge pull request #1611 from abiyeenzo/fr-fixes
i18n(fr): typography, terminology and wording fixes
2026-11-09 18:22:18 +04:00
German Khokhlov caad795fa2 refactor(server): drop the null and String paths from gzipBody 2026-11-09 18:15:13 +04:00
German Khokhlov 09bf01630b docs(server): tighten compression comments and docs 2026-11-09 18:14:30 +04:00
German Khokhlov 2ea4a78e75 Fix the fr review follow-ups
- "destination refresh" is the refresh at the place the camera flew to,
  so restore "son actualisation à destination".
- Radio note: "que vous la demandez par la voix", "Votre adresse IP".
- "{count} / 30D": "30 J", like "{days} j".
- Starlink: "la couche complète", as in the active-state tooltip.
- "Show {name} sighting": colon form, so names starting with a vowel
  do not break the elision after "de".
- Wildlife note: Acartia is the source, not a kind of marine mammal.
- Straight apostrophes in the five strings added on main since.
2026-11-09 18:12:48 +04:00
German Khokhlov 61e9cf87f9 Merge branch 'main' into fr-fixes
# Conflicts:
#	src/i18n/locales/fr.json
2026-11-09 18:12:09 +04:00
Said Turksever 2c1b2c278d fix(transit): read Rouen and Le Havre from the operators' own feeds
Rouen now reads Métropole Rouen Normandie's TCAR vehicle feed
(api.mrn.cityway.fr) and Le Havre LiA's own GTFS-RT
(opendata.transports-lia.fr), both listed in their transport.data.gouv.fr
records, instead of the unofficial bus-tracker.fr relay. LiA ids carry an
8- prefix, so a new liaRouteLabel strips it and liaRouteMode reads the line.
2026-11-09 13:58:20 +00:00
Said Turksever 7a053c640d fix(transit): real route-id modes for Prague, Bordeaux and Rouen; dated Warsaw/Poznań credits; data.gov.my refresh
- Prague PID: map route-id ranges from PID's GTFS (metro L991-993, S/R trains
  L1000s/L1100s, P ferries L1800s, trolleybuses 50-59, X lines, funicular LD)
  instead of 'below 100 is a tram, rest bus'.
- Bordeaux TBM: trams ride numbered ids 59-62/163/164 (A-F), boats 951-953;
  the old letter match never fired.
- Rouen Astuce: TCAR:90 Metro as tram, TCAR:99 Calypso as ferry; Le Havre FUNI.
- Warsaw and Poznan: credit carries the data date (and Warsaw links its API),
  as their terms require.
- data.gov.my feeds: CC BY 4.0, refreshMs 30 s to stay under 4 requests/min;
  proxy honours per-feed refreshMs.
- Denver: quote the RTD agreement; tests use the feeds' real route ids.
2026-11-09 13:50:14 +00:00
German Khokhlov a5b40dfc3d Merge pull request #1569 from rudycelekli/fix/perimeters-sub-acre-size
fix(perimeters): show a 0.25-acre fire as 0.25 ac, not 0 ac
2026-11-09 17:39:29 +04:00
German Khokhlov be9c37bee2 Merge remote-tracking branch 'origin/main' into fix/perimeters-sub-acre-size 2026-11-09 17:07:05 +04:00
German Khokhlov 394f3216f8 Drop the sub-acre rounding comment 2026-11-09 17:06:56 +04:00
German Khokhlov c62f7ef00e Merge pull request #1524 from quanticsoul4772/upstream/tools-heading-wrap
fix(tools): wrap whole-degree headings at 360 to 0
2026-11-09 17:00:58 +04:00
quanticsoul4772 dbb082355a Merge branch 'main' into upstream/tools-heading-wrap 2026-11-09 04:47:58 -08:00
German Khokhlov da6b63b6fd docs(tools): tighten the heading-wrap changelog, comments and overview 2026-11-09 16:45:11 +04:00
German Khokhlov 3150a460aa Merge pull request #1547 from quanticsoul4772/upstream/nominatim-cache-mode-windows
test(nominatim): check cache file modes only where POSIX modes exist
2026-11-09 16:44:00 +04:00
Kaj ObstandClaude Opus 5.5 315627534a i18n(sv): ORBIT as OMLOPP, which also fits the satellite orbit entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:36:47 +01:00
German Khokhlov c2bff536a5 Merge branch 'main' into upstream/nominatim-cache-mode-windows 2026-11-09 16:07:00 +04:00
Kaj ObstandClaude Opus 5.5 dd02f8093d i18n(sv): translate the strings added since the Swedish catalog
Brings the Swedish catalog up to date with main: 122 new strings from the
satellite classes, the ship bridge and orbital cockpit views, the world
timeline and the place-name layer, translated per
docs/translations/sv.md. Drops the three entries main no longer uses
(DENSE ···, DENSE ✕, C TOGGLE). Brand names, codes and units that read the
same in Swedish (STARLINK, NADIR, TLE, NORAD, NM, Live, …) are listed in
same-as-english.json.

`npm run i18n -- --check` reports sv complete; the i18n tests pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 12:56:16 +01:00
Kaj Obst 6a6cee4c21 Merge remote-tracking branch 'origin/main' into sv-locale 2026-11-09 12:53:37 +01:00
German Khokhlov 69fbf5b38d Check hidden lines behind the Earth and open the picker panel by keyboard
Cesium culls a line the globe hides whole and does not depth-test lines
against the near side of a keyless globe, so the contract check runs the
hidden line round the back of the Earth to a visible end. The wildlife
switch scenario opens VISUAL PRESETS from the keyboard before tabbing to
the engine picker and reports the page state when no switch follows.
2026-11-09 13:03:13 +04:00
German Khokhlov 68845a4977 Start Navara's camera model from a placed pose only
Navara's camera sits at the Earth's centre until it is first placed. The
model took that as the pose, so camera.height() threw while a layer
attached to a starting Navara (wildlife sightings asks for it), and
switching to Navara with that layer on rolled back to Cesium.
2026-11-09 13:03:13 +04:00
Andrés Valdez d52c719ef6 docs(changelog): OpenAI Realtime voice tool patterns (#1638) 2026-11-09 07:52:24 +00:00
Andrés Valdez 9f2dfdf820 fix(voice): send OpenAI Realtime tool schemas the API accepts
OpenAI Realtime now answers 400 invalid_function_parameters for a whole
session whose tool schema uses Unicode property classes, as resolve_area's
area-id pattern does, so voice never connects ("Failed to create Realtime
token"). The session sends its tools through chatSchema, the filter typed
chat adopted in #1616, which leaves out only \p{…} patterns; area ids are
still checked against the action's own contract before the tool runs.

Fixes #1618
2026-11-09 07:34:44 +00:00
Sameh Khamis 027c9616a5 Merge pull request #1637 from bilawalsidhu/sameh/orbital-followups
Orbital cockpit follow-ups: sensor strip through zoom, one owner-flag sheet, place names in the loading status
2026-11-08 20:21:29 -08:00
Sameh Khamis c1c71d1987 docs(changelog): orbital cockpit follow-ups (#1637) 2026-11-08 20:12:00 -08:00
Sameh Khamis 26718edc0b test(place-names): stand-in packs, the whole status run, and a failure within a load that stays failed 2026-11-08 20:07:04 -08:00
Sameh Khamis 2f31828d28 fix(place-names): a load starts with no failures, so packs that failed earlier do not mark a retry that lands as failed 2026-11-08 20:04:40 -08:00
Sameh Khamis c5e1951f51 fix(cockpit): one place lookup that loads several packs reads as one load, not a LOAD COMPLETE between packs 2026-11-08 19:57:28 -08:00
Sameh Khamis f6dc89b4fa fix(cockpit): a place pack retried after a failure counts as failed only if the retry fails 2026-11-08 19:49:05 -08:00
Sameh Khamis e954734379 fix(cockpit): a place pack that will not load ends the loading batch failed; flags keep their neighbours out from under the border and out of a scaled flag's edge; a new ride asks again for a sheet that failed 2026-11-08 19:45:01 -08:00
Sameh Khamis 5a6cf22be1 feat(cockpit): the owner flags load at startup from one generated 15 KB sheet; the loading status names the place packs while they load on first lookup 2026-11-08 19:35:30 -08:00
Sameh Khamis 63aa45c520 fix(cockpit): the sensor strip follows the view through a zoom step; the pre-load level test checks a worked number; the boundary gate's dependency-asset exemption is tested 2026-11-08 19:08:51 -08:00
Sameh Khamis 1453794573 Merge pull request #1636 from bilawalsidhu/feat/draw-mark-actions
Draw rows take an application's own actions
2026-11-08 18:27:04 -08:00
Sameh Khamis eb1969e874 Merge branch 'main' into feat/draw-mark-actions 2026-11-08 18:20:47 -08:00
Sameh Khamis 034acb06a8 Changelog: Draw rows take an application's own actions 2026-11-08 18:12:48 -08:00
Sameh Khamis 27847bd063 Draw rows take an application's own actions
createApplicationTools({ drawMarkActions }) adds an application's buttons to
the rows of drawn areas and lines. An action gets the shape's id, kind, name,
row title and [lon, lat] geometry, and what it returns or resolves to is a
note under the row. The standalone shell passes none, so the open-source
build is unchanged.
2026-11-08 18:12:32 -08:00
Sameh Khamis 4ee617cf42 Merge pull request #1580 from ZloyS/denis/gev-orbital-focus
feat(cockpit): keep the orbital camera on a ground target
2026-11-08 17:21:27 -08:00
Sameh Khamis 577170ee8e fix(cockpit): the hop's circle runs through both ends, so it moves smoothly as a nearly opposite destination does and lands on it 2026-11-08 17:12:04 -08:00
Sameh Khamis 6be8af17f1 fix(cockpit): a new ride's TRACK ACQUIRED keeps its own time; an arrow during the ease back tilts on from the view; a site beyond the reach ends a focus; the horizon uses the WGS84 surface normal; a hop to the opposite side goes round the Earth; drop the unused in-frame listing 2026-11-08 17:12:04 -08:00
Denis Groshev 7d6314b981 docs(changelog): add the #1580 fragment 2026-11-09 01:56:30 +01:00
Denis Groshev 7b844792be docs(cockpit): describe orbit's Live Signals rows and site focus 2026-11-09 01:56:30 +01:00
Denis Groshev dd02068ce9 feat(cockpit): list orbit's sites and satellites in Live Signals
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.
2026-11-09 01:56:30 +01:00
Denis Groshev fad62a4ba8 feat(satellites): find the satellites nearest a position
getNearestPositions makes one bounded pass over the shown points, keeps the
nearest few in a short sorted list and reads latitude and longitude for
those alone, leaving out dense extras, asked-for ids and classes such as
debris. A caller that wants the nearest handful no longer builds and sorts
an entry for every satellite.
2026-11-09 01:56:30 +01:00
Denis Groshev 569c7cfb1b feat(installations): look a mapped site up by id or scene pick
getById returns a loaded installation with its drawn position, as getNearby
does, without selecting it; findByPick resolves a scene pick (a named
marker's installationId or a site entity) the way the layer's own click
selection reads it. Both return null while the layer is off.
2026-11-09 01:56:30 +01:00
Denis Groshev b18336d3d4 fix(cockpit): keep the orbital view on a double-click at a ground point
In the orbital view a double-click on a mapped ground point went on to
Cesium's own double-click, which tracks the picked entity and so ended the
view. The Cockpit's double-click routing now takes it in orbit: it keeps or
starts the focus on that point (a point beyond the reach is left unfocused
but still not tracked). A double-click anywhere else, one a tool owns, and
every one outside orbit go on to Cesium's as before, and stopping the
Cockpit still hands Cesium's handler back.
2026-11-09 01:56:30 +01:00
Denis Groshev dd063b0e83 docs(cockpit): describe the orbital view's ground focus 2026-11-09 01:56:30 +01:00
Denis Groshev 567c833582 feat(cockpit): keep the orbital camera on a ground target
In the satellite orbital view a single click on a mapped ground point (a
ground point of a layer that is on, such as a Mapped Installations site, a
fire or a local data pack feature) keeps the camera on it. Each frame the
look is aimed from the satellite's position to the point's Earth-fixed
position in the track-up nadir frame the arrows tilt, so the point stays
at the centre as the satellite moves. The swing onto the point, or between
points, uses the arrows' easing on a slower clock and then holds the point
exactly; reduced motion cuts.

A click elsewhere, FOCUS on the focused point, NADIR, Home or 0 ends the
focus and the view eases back to nadir; an arrow ends it and tilts on from
where the view is. The focus also ends, easing back, when the point leaves
the ±45° reach or the satellite's horizon, when its layer is turned off or
its record is dropped, and with the view. While focused the readout gives
the single off-nadir angle to the point (OFF-NADIR 23.4°), names it in a
polite live region, and the swath reads approximate as for a tilt. Clicks
still reach Cesium's own handler and never untrack the satellite; a click
a tool owns is ignored.

Under the readout, IN REACH lists the mapped points facing the satellite
that are within the reach or in the frame, nearest to nadir first and at
most 12, rebuilt once a second. A row opens the point's position, source
and OpenStreetMap tags (or its layer's properties); FOCUS focuses it,
switches to it, or ends the focus held on it, and is unavailable for a
point in frame but beyond the reach.
2026-11-09 01:56:29 +01:00
Sameh Khamis 745c11969b Merge pull request #1560 from ZloyS/denis/gev-sensor-zoom
feat(cockpit): zoom the orbital view to the imaging sensor's swath
2026-11-08 16:53:41 -08:00
Sameh Khamis 4396ba4506 fix(cockpit): the pre-load serves every lead's centre line before the frame edges, starts a fresh memory and cancels the old map's requests on a new base map, and retries only what the scheduler dropped; the sensor waits for the eased view to reach the Earth 2026-11-08 16:43:59 -08:00
Sameh Khamis b953c3f1dc fix(cockpit): no sensor swath past the limb, no strip for a GEO view or a SAR; the TLE age reads from the world-clock time shown; pre-loads cancel on exit, retry when they fail, start over on a new base map, wait while the clock is away from now and reach the frame edges 2026-11-08 16:35:23 -08:00
Sameh Khamis a274d10038 Merge pull request #1559 from ZloyS/denis/gev-orbital-view
feat(cockpit): ride a tracked satellite in an orbital view
2026-11-08 16:27:41 -08:00
Sameh Khamis 9a16fb8cbc Merge Core main into denis/gev-sensor-zoom
# Conflicts:
#	src/cockpitOrbital.test.mjs
2026-11-08 16:20:37 -08:00
Sameh Khamis e944f75275 Merge pull request #1558 from ZloyS/denis/gev-satellite-classes
feat(satellites): file satellites under six classes with a toggle each
2026-11-08 16:17:12 -08:00
Sameh Khamis 83a24b8d27 Merge Core main into denis/gev-orbital-view
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
#	src/layers/satellites/state.js
2026-11-08 16:09:30 -08:00
Sameh Khamis 4ab23733e9 test(satellites): the class test fakes carry the world-clock coverage hook main added 2026-11-08 16:07:09 -08:00
Sameh Khamis f1ed762e84 Merge Core main into denis/gev-satellite-classes
# Conflicts:
#	scripts/package-boundaries.json
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
#	src/layers/satellites/ingestion.test.mjs
#	src/layers/satellites/lifecycle.js
#	src/layers/satellites/state.js
2026-11-08 16:04:31 -08:00
Sameh Khamis 30fbfe238f Merge pull request #1557 from ZloyS/denis/gev-ship-bridge
feat(cockpit): ride the selected vessel from its bridge
2026-11-08 16:03:54 -08:00
Sameh Khamis 573479d7ab fix(satellites): the UCS pack build drops rows whose NORAD number is not positive 2026-11-08 15:55:13 -08:00
Sameh Khamis 3bf7e9edb7 fix(cockpit): the limb check reads the exact look angles; the lease note names both ways back to Recent Imagery 2026-11-08 15:55:02 -08:00
Sameh Khamis e968233a52 fix(cockpit): the swath waits for the eased view to reach the Earth, not only the look target 2026-11-08 15:55:02 -08:00
Sameh Khamis 85d0f84066 docs(cockpit): orbit's imagery lease keeps Recent Imagery out until exit 2026-11-08 15:55:02 -08:00
Sameh Khamis bf29408d00 chore(cockpit): drop a stale C-key comment and a dead fixture field, and keep the orbital stylesheet in the format scope 2026-11-08 15:55:02 -08:00
Sameh Khamis b72f453fea refactor(satellites): the UCS pack keeps only the operator, owner country and purpose the layer reads 2026-11-08 15:55:02 -08:00
Sameh Khamis 6ca53febad fix(cockpit): no swath readout while the orbital view looks past the Earth's limb 2026-11-08 15:55:02 -08:00
Sameh Khamis 5c25bc54e6 fix(cockpit): voice entry in an orbital view is refused before it touches tracking 2026-11-08 15:55:02 -08:00
Sameh Khamis a5df391d6c fix(cockpit): a catalog refresh in orbit keeps the subject and its companions hidden, and releasing the ISS leaves its ring to its class 2026-11-08 15:55:02 -08:00
Sameh Khamis af8dc37df5 Merge Core main into denis/gev-ship-bridge
# Conflicts:
#	index.html
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-08 15:51:34 -08:00
Denis Groshev 19d7148b06 docs(changelog): add the #1560 fragment 2026-11-09 00:25:08 +01:00
Denis Groshev 59c0997636 docs(cockpit): describe the orbital view's sensor-aware zoom
The Orbital view section said the zoom stops at 8x. It now covers the
16x to 128x steps for satellites with a confirmed sensor swath, capped
from live altitude, the mouse wheel, the SENSOR button, the to-scale
swath strip and readout (approximate off nadir), the imagery pre-load
ahead of the ground track, the TLE age on the identifier line, and that
the sensor values cite operator and agency pages per platform.
2026-11-09 00:25:07 +01:00
Denis Groshev fe5470a852 feat(cockpit): zoom the orbital view to the imaging sensor's swath
An imaging satellite with a known sensor can now zoom past 8x, up to 128x,
as far as the view stays at least as wide as the sensor's narrowest
standard swath at its live altitude; only swaths confirmed on a cited page
open those steps. The top step follows the altitude and window shape and
steps back when it falls below the zoom shown.

The view draws the sensor's swath to scale along the ground track with a
SENSOR SWATH readout (approximate off nadir), and a SENSOR button zooms to
exactly that width when it lies in range at nadir. From 16x the imagery
tiles the view will need 5 to 20 s ahead along the ground track are
pre-loaded at the matching level. The mouse wheel steps the zoom one notch
at a time, and the identity line shows how old the TLE is. The sensor
figures come from a new bundled pack of 46 Earth-observation platforms,
each value cited to its operator or agency, with its own data credit.
2026-11-09 00:25:06 +01:00
Denis Groshev d0ef5250ac docs(changelog): add the #1559 fragment 2026-11-09 00:24:22 +01:00
Denis Groshev 048293fc2a feat(cockpit): ride a tracked satellite in an orbital view
With a satellite tracked and no aircraft tracked, Cockpit can ride the
satellite. Entry needs only the Satellites layer; debris can be tracked but
not entered, and voice stays aircraft-only. Double-clicking the tracked
satellite or the entry buttons (reading ORBIT) enter it.

The camera sits on the satellite's per-frame SGP4 position and looks
straight down, turned to the ground track (north-up for a satellite with
none). The optical zoom steps 1x to 8x with a focus pull, the arrows tilt
the view up to 45 degrees off nadir, and Home or 0 returns to nadir. Off
nadir the tilt reads its total angle first and the swath readout becomes an
approximation marked with ≈. While in orbit the map leases flat Esri
imagery, the cloud pass stays off and the view opens in the optical style.

The orbital HUD names the satellite with its owner's flag (from the
flag-icons package, loaded per country) or an organisation badge, its
operator, class, NORAD and COSPAR ids and TLE epoch with the element lines
on request, altitude, orbital speed and the sub-satellite point. CURRENTLY
OVER names the country and its first-level division, or the sea, and from
8x the most prominent populated place nearby, looked up offline from the
bundled Natural Earth packs (populated places are a new pack). The
satellites layer hides the ridden satellite's own marks, dims the other
orbit rings and ignores scene clicks and Escape while held. Exit restores
the camera's field of view and the satellite's follow camera.

C no longer enters or leaves Cockpit, bridge or orbit: it is left to the
app-wide CCTV shortcut, and Escape or the exit button leave first person.
2026-11-09 00:24:22 +01:00
Denis Groshev a648c9b665 feat(satellites): owner, operator and COSPAR groundwork for the orbital view
The orbital view reads a satellite's owner country, operator, COSPAR id,
TLE epoch and whether its class can be ridden. #1558 now carries only
what the class toggles read, so this brings the rest back here, where it
is first used: the classes.js helpers and the UCS pack's operator and
owner fields.
2026-11-09 00:22:12 +01:00
Sameh Khamis eaf2b8f5b4 fix(satellites): the ISS ring follows its class through re-enable and rebuilds and stays while tracked; a mission group outranks geo; an Unknown UCS purpose is no evidence; the UNK grace is stated in calendar years 2026-11-08 15:16:01 -08:00
Sameh Khamis 83dd1af22e test(satellites): the card check reads the real record, the group checks cover every ingested group, and fixtures use NORAD ids UCS does not know 2026-11-08 15:16:01 -08:00
Sameh Khamis 608ab4adea fix(satellites): an unknown object from a launch under two years old is not debris yet; Earth/Space Observation is imaging; the data-sources table names SATCAT 2026-11-08 15:16:01 -08:00
Sameh Khamis cc24da165b fix(satellites): a current SATCAT read is kept six hours instead of re-reading eleven groups every update; the tracking regression serves SATCAT locally 2026-11-08 15:16:01 -08:00
Sameh Khamis 3bb742ae7e refactor(satellites): remove the unused owner, operator, COSPAR and orbital-view groundwork; the UCS pack keeps only the purpose the classes read 2026-11-08 15:16:01 -08:00
Sameh Khamis 7dcf671b2e fix(satellites): a hidden class stays out of analyst and voice answers; the tracked satellite still answers 2026-11-08 15:16:01 -08:00
Sameh Khamis b59eb8b18c fix(satellites): a hidden class takes the ISS ring, label and docked names
Class toggles only hid points: with Stations off the ISS's orbit ring and
its label still drew, and each catalog refresh showed the ring again. The
ring and label now follow the ISS's class, and the tracked card no longer
names a docked companion whose class is hidden.

A failed Starlink load reverted to the core catalog without applying class
visibility again, so the core catalog's own Starlink points stayed on while
the chip, legend and lists said Starlink was off, and classes.starlink kept
reading true. Both are put right on failure.
2026-11-08 15:16:01 -08:00
Sameh Khamis 8bd16b0be9 fix(satellites): classes read every group a TLE came in, and current SATCAT
Deduplication kept only the first group's tag, so a recent imaging
satellite in both visual and resource, with no UCS record or known name,
fell to Probes. The catalog entry now keeps every group, and the
last-resort class is the first of them that names one.

SATCAT groups merged in group order, so a stale cached copy read later
overwrote a current record of the same object: a satellite SATCAT now
calls dead stayed in Imaging. A stale copy no longer overrules a current
read, from the same merge or an earlier one.
2026-11-08 15:16:01 -08:00
Sameh Khamis c84cfcadeb fix(satellites): class choices survive a reload and travel in share links
Only catalog and the tracked satellite were saved for the layer, so a class
chip lasted until the next reload and no share link carried it. The choice
is now the satellites option `classes`: one letter per class shown
(`sitpd` by default, `0` for none), with Starlink left to `catalog`, and
the layer takes the letters back through setParams. A partial request
(`{debris: false}`) hides only what it names.
2026-11-08 15:16:01 -08:00
Sameh Khamis 8ae56587c3 fix(satellites): a SATCAT failure no longer stops TLE refreshes
Both CelesTrak routes shared one cooldown, and any failure set it: one bad
SATCAT answer made /api/celestrak return 502 "cooling down" for a minute,
failing the Starlink load and stalling every TLE group past its cache.
Throttling (403/429) still holds both routes off, since CelesTrak throttles
the client; any other failure holds off only the route that failed.
2026-11-08 15:16:01 -08:00
Sameh Khamis d404445da3 fix(satellites): every class but Starlink shows by default
Showing only Stations and Imaging by default hid GPS, GEO comsats, Hubble
and the rest of what the layer showed before classes, for every existing
user and saved view. Every class now starts on except Starlink, which stays
off as the DENSE switch was.
2026-11-08 15:16:01 -08:00
Denis Groshev 63e6338c19 docs(changelog): add the #1558 fragment 2026-11-08 23:35:10 +01:00
Denis Groshev c56f098d07 feat(satellites): file satellites under six classes with a toggle each
The satellites layer now sorts every object into Stations, Imaging,
Telecom, Starlink, Probes or Debris, each with a chip and a live count on
the layer row. Stations and Imaging show by default; the Starlink chip is
the former DENSE catalog switch. Hidden classes keep their points, so a
toggle needs no reload, and they leave the detection overlay and the voice
position lists.

A class comes, strongest evidence first, from CelesTrak SATCAT (object type
and status, served by a new /api/celestrak-satcat/<group> proxy route and
cached for a day), the bundled UCS Satellite Database pack (operator, owner
country and purpose), name patterns for later launches, and the CelesTrak
group. The core catalog also reads the resource, weather, planet, sar and
science groups. Payloads with no evidence of life launched 20 or more years
ago are filed as debris. The UCS pack, its build script and its data credit
are added.
2026-11-08 23:34:55 +01:00
Sameh Khamis 922712121c Merge pull request #1634 from bilawalsidhu/fix/layer-state-registered-only
Layer state keeps only layers the application registered
2026-11-08 14:28:48 -08:00
Sameh Khamis f62d947a82 Changelog: layer state keeps only registered layers 2026-11-08 14:21:09 -08:00
Sameh Khamis e9346cef19 Layer state keeps only layers the application registered
An application built with fewer layers no longer restores, stores or shares a
layer it does not have when a link or saved state from a build that has it
names that layer. LayerStateCoordinator drops unregistered enabled-layer ids
before normalizing, so dependent options (a tracked selection) go with them.
An application with every layer registered is unchanged.
2026-11-08 14:19:56 -08:00
Sameh Khamis 1b9bb844e6 revert(cockpit): no water read timeout, and loaded reads not raise-only
Both reviews of the timeout found that Cesium cannot cancel a full-detail
read: each one given up kept its nine picks traversing every frame, so
stalled tiles piled up a new set every 30 s, while slow but valid answers
were dropped. Raise-only loaded reads then locked in a wrong-high height.
Back to one full-detail read at a time whose answer always applies, and
loaded reads that may adjust a river height but never take it to the sea;
a coarse probe can again lower it partway until the full read lands.

Keeps the test for another ship selected inside the double-click memory,
and corrects CURRENT-STATE: a failed read keeps the height it had.
2026-11-08 14:09:18 -08:00
Sameh Khamis 1bc4f980e0 fix(cockpit): a stalled water read is given up, and coarse reads only raise
Before the first full-detail read lands, a loaded-tile read could still
lower a river height partway (27 m to a 15 m bank). Loaded-tile reads now
only raise it; lowering is the full-detail read's to say.

That makes a full read that never lands (stalled tile requests) hold a
wrong height for the whole bridge, so one still running after 30 s is given
up for a fresh one; its late answer is dropped.

Also tests the other half of the double-click memory fix: another ship
selected within the memory is not entered through the first ship's spot.
2026-11-08 14:09:18 -08:00
Sameh Khamis 80a33ba52c fix(cockpit): a coarse read never takes a river bridge back to the sea
Before the first full-detail water read lands (it can take many seconds
from cold tiles), a refresh's loaded-tile read with one low probe still
dropped a river height to the geoid. Before that landing, loaded-tile reads
may now find raised water but only a full-detail read lowers it.

CURRENT-STATE also says what the earlier fixes changed: C stays the CCTV
shortcut when the bridge cannot be entered, voice entry is refused on a
bridge, and the ridden ship's trail is hidden with its sprite.
2026-11-08 14:09:18 -08:00
Sameh Khamis 5c937e67e1 fix(cockpit): a ship's double-click memory belongs to that ship
The first press's picture of the ship survived a change of followed
subject: following an aircraft (or another ship) within 700 ms let a
double-click on the old ship's spot enter the new subject. The memory now
carries the ship's MMSI and is used only while that ship is still the one
followed.
2026-11-08 14:09:17 -08:00
Sameh Khamis f96a89a9ef fix(cockpit): radar contacts stay where the vessel layer draws them
The vessel layer draws other ships at their last AIS fix and does not
advance them, so dead-reckoning radar contacts moved the mismatch from own
ship to everyone else: a 15 kn ferry reported 90 s ago showed ~700 m ahead
of the ship out of the window. Own ship stays centred where the camera
rides; contacts go back to their last fixes, and the query drops the extra
reach and candidates (which could also cut the nearest ship in a crowd).
2026-11-08 14:09:17 -08:00
Sameh Khamis 0ba7ba6512 test(cockpit): the real vessel layer keeps the ridden ship on a water click
The bridge tests drive a hand-written layer, so nothing exercised the
vessel layer's own scene-click gate. A wire test now clicks open water with
the bridge holding the ship, then again after release.

The Contacts test that adopts Live AIS Vessels directly is renamed to say
so: it never rides a bridge.
2026-11-08 14:09:17 -08:00
Sameh Khamis 9abc105266 refactor(cockpit): the bridge styles get their own stylesheet
The ship bridge rules were appended to cockpit.css, taking it to 2705
lines; they move unchanged to cockpitBridge.css, imported right after it so
the cascade is the same. cockpitBridge.css and cockpitRadar.css join the
format scope and the styles package.
2026-11-08 14:09:17 -08:00
Sameh Khamis 0faa29dfbd style(cockpit): format the lastSubjectInfo rename 2026-11-08 14:09:17 -08:00
Sameh Khamis af21219911 refactor(cockpit): lastAircraftInfo is lastSubjectInfo
It holds the ridden vessel on a bridge, and the radar range button reads a
vessel from it.
2026-11-08 14:09:17 -08:00
Sameh Khamis 620a18608e fix(cockpit): only a ship's double-click remembers where it began
The first press's picture of the track is there because a click on a
selected ship re-centres the camera. It applied to followed aircraft too,
widening their double-click hit area to wherever the aircraft was drawn
as the gesture began. Aircraft are back to the hit where they are drawn.
2026-11-08 14:09:17 -08:00
Sameh Khamis bbe33b48c7 fix(vessels): hide the ridden ship's trail while its bridge is held
The sprite and card were hidden from the bridge but the selected-vessel
trail was not: at sea it ran astern from under the eye, and on a raised
river it lay buried under the water. It is now hidden while the bridge
holds its ship, keeping its history, and shown again on release.
2026-11-08 14:09:17 -08:00
Sameh Khamis 3ce3fce46f fix(cockpit): the bridge reads its floor where the ship is now
The ground clamp read the floor at the ship's last AIS fix while the camera
rode that fix dead-reckoned up to 180 s ahead, so a quay or bridge deck at
the old position could lift the eye. Vessels now read the floor at the
dead-reckoned target, as the water read already does; aircraft are
unchanged.
2026-11-08 14:09:17 -08:00
Sameh Khamis 052731202d fix(cockpit): the bridge radar matches the view out of the window
The radar centred on the ship's last AIS fix while the camera rode that fix
dead-reckoned up to 180 s ahead: at 20 kn about a nautical mile apart, so a
ship already passed still showed ahead. Own ship now sits where the camera
rides and each contact is dead-reckoned to the same moment; the query reaches
past the range for ships that have since moved into it.

Ships with no MMSI each get a key of their own instead of all sharing an
empty one on the scope.
2026-11-08 14:09:17 -08:00
Sameh Khamis 18c15d0352 fix(cockpit): a river bridge keeps its water height through empty and coarse reads
A full-detail water read that sampled nothing (tiles not loaded) fell back
to the geoid, and while a full read ran, a loaded-tile read with one coarse
low probe did the same: on the Seine either dropped the eye ~27 m under the
water until the next read. An empty read is now no answer, and once a
full-detail read has answered only full-detail reads change the height.

The read sequence numbers go: only one full-detail read runs at a time,
so a late read can never be older than one already applied.
2026-11-08 14:09:17 -08:00
Sameh Khamis 36a2216528 fix(cockpit): voice refuses entry while on a vessel bridge
Voice entry tracked an aircraft first, which cleared the vessel selection
and ended the bridge, and then failed because Cockpit was still active:
no bridge, no cockpit, no selected ship, and voice said entry was
unavailable. It now refuses before it touches tracking and says to exit
the bridge first.
2026-11-08 14:09:17 -08:00
Sameh Khamis b98bc4b0dc fix(cockpit): C stays the CCTV shortcut when the bridge cannot be entered
With a vessel selected and no aircraft tracked, Cockpit took C and stopped
it even when the bridge gate was shut, so the key did nothing and CCTV no
longer toggled. C is now taken only when the bridge can be entered.

The arrow keys also stay with comboboxes, spinbuttons, trees, grids,
menubars, scrollbars and text boxes on a bridge, which move with them.
2026-11-08 14:09:17 -08:00
Sameh Khamis 7ee7dc26b8 Merge pull request #1513 from bilawalsidhu/sameh/world-timeline-launches
Move launches with the timeline, choose its speed, and wait for data while it plays
2026-11-08 12:32:37 -08:00
Sameh Khamis ff0b4d8449 Merge branch 'main' into sameh/world-timeline-launches 2026-11-08 12:24:39 -08:00
Sameh Khamis 424bcb0843 Merge pull request #1512 from bilawalsidhu/sameh/world-clock
Keep one world clock, with a timeline to move it
2026-11-08 12:23:12 -08:00
Sameh Khamis 137639dd38 Merge branch 'sameh/world-clock' into sameh/world-timeline-launches 2026-11-08 12:13:46 -08:00
Sameh Khamis 84f3476dd3 Merge remote-tracking branch 'origin/main' into sameh/world-clock
# Conflicts:
#	index.html
#	scripts/package-boundaries.json
#	src/app/scene.js
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
#	src/layers/launches/ingestion.js
2026-11-08 12:13:41 -08:00
German Khokhlov b4556f9763 Test layers on one engine and share selection and shader helpers
Layer display tests no longer re-check Cesium primitives in every layer:
they run on the fake engine, and one Cesium translation check per ground
kind lives in the Cesium engine's ground tests. Their harnesses are shared,
and the fake-engine, Cesium-engine and flights tests that repeated the
contract suite are gone, 340 tests in all.

The press-travel measurement is shared by the selection dispatcher and
CCTV, which loses its ScreenSpaceEventHandler shim and world-click module.
ALPR, installations and bikeshare share one selection keeper and selected
marker write. Layers with their own click rules keep their handlers.

Navara's pulse is a class like the ground pass, the ground and imagery
passes share the surface uniforms, and the colour, log-depth and occlusion
GLSL lives in one chunk module, each caller keeping its depth bias. The
ground suite runs inside the contract suite (--only=ground:), and fixture
and script names say what they test.

Behaviour is unchanged on both engines.
2026-11-08 17:06:10 +04:00
German Khokhlov 7f478e3815 Remove always-true layer availability and leftover viewer paths
Every layer draws on both engines, so the per-engine availability
machinery always answered yes. The renderer manifest is now the id to
display map plus the wind flow backend; the catalog filter, the voice and
QA-probe refusals and the engine-kind checks go with it. The Navara
notice lists the same layers in the same words.

Paths that read the Cesium viewer behind an engine that is always there
are gone: the Recent Imagery viewer fallbacks, the shell's imagery-host
service (one shared host module remains), the engineless map controller,
viewer gates in voice actions, world packets and camera verbs, the
orientation controls' tracked-entity fallback, and the Bhote Koshi and
FIRMS Cesium-shaped paths. setCameraInputs, which nothing called, leaves
the contract; suspensions remain.

Smaller duplicates go too: one tracker-release helper for navigation and
location, the no-op frame latch, missing-engine guards, one name for the
injected display, flat bikeshare and installations displays, one overlay
host and binder, one name per world-overlay method, and stub-viewer
guards in the Cesium engine. Layers that still read the viewer keep the
shared init(viewer, context) entry.

Behaviour is unchanged on both engines. Viewer lines fall from 849 to
750 and engine-name literals from 32 to 27.
2026-11-08 17:06:01 +04:00
German Khokhlov 371d848eec Delete dead render code and consolidate QA helpers
Remove code that nothing in the app, the runners or CI uses:
- the one-off migration compare, look and bench scripts and the vector
  ground prototype they measured, with the bench tables in the docs;
- the unused Navara-named shared render modules (polygons, ground picks,
  mover bank, ground ring);
- the Cesium-shaped aircraft and vessel test renderers, and the trail,
  engineFromViewer and horizon-occluder helpers only they kept alive.
  Their tests now run on the production displays over the fake engine;
- Navara's unread diagnostics, fallback ports, dead exports and
  parameters, stale effect-chain anchors and the unread capability flags.

The QA scripts share one keyless guard and readiness wait, the session
harness reuses the strict harness's browser flags and block list, and the
harness honours QA_ANGLE and QA_RESERVED_PORTS. The bikeshare fixture
keeps only what its scenario reads. The docs name the engine picker where
it is (VISUAL PRESETS) and drop dated run results.

No behaviour changes on either engine. Navara layer renderer lines fall
from 189 to 0 and viewer lines from 891 to 849.
2026-11-08 17:05:38 +04:00
German Khokhlov 18f69c2699 Follow, credits, embed views and FPS through the engine on Navara
Five places still read the Cesium viewer and so did nothing, or the wrong
thing, on Navara. Each now goes through the globe contract, and Cesium
behaves as before.

- The Navara ground floor stands aside while the camera inputs are
  suspended, not only while they are switched off. The cockpit suspends
  them, so its eye 12 m up no longer jumps to the floor's 30 m between
  updates. A follow's own suspension keeps the floor on, as before.
- Follow state comes from engine.camera.following() and followChanged
  instead of viewer.trackedEntity. Enabling CCTV during a follow no longer
  flies away and ends it, a new follow stamps navigation, Director cards
  wait while a subject is followed, and an explicit navigation stops the
  follow on both engines.
- Navara registers every data credit, as Cesium does. The allowlist left
  out the layers that moved to Navara later (CCTV agencies, street-level
  imagery, GIBS, routing, bike feeds and others).
- Annotation links and embed mode attach on every engine; their camera
  flights use engine.camera.flyTo. Only Cesium adds the timer that keeps
  an inline panel drawing.
- The FPS readout counts engine.frames.postRender, so it shows on Navara.

Each fix has a test that fails without it.
2026-11-08 17:05:27 +04:00
Kaj ObstandClaude Opus 5.5 0f9592c77a changelog: Swedish (sv), #1630
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:47:03 +01:00
Kaj ObstandClaude Opus 5.5 fbd74bb80a i18n: add Swedish (sv)
Adds a complete Swedish catalog (2,961 messages) and registers sv in the
locale list, catalogs, language picker and package boundaries. Browsers set
to Swedish get it at once; ?lang=sv or the language picker choose it
elsewhere. Numbers and dates come out Swedish from Intl.

- docs/translations/sv.md: style (imperative without a pronoun, closed
  compounds, decimal comma, space before units and %) and the glossary.
- Entries that stay identical to English on purpose (codes such as HDG and
  FOV, product, place and source names) are listed under sv in
  same-as-english.json.
- The picker-order test places Svenska after Português.
- docs/CURRENT-STATE.md and docs/TRANSLATORS.md list the new language.

The translation comes from God's Eye View Sverige, a Swedish fork, and was
brought up to date with main (wildlife sightings, event age filter, traffic
styles and the rest).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:31:12 +01:00
Itallo BrandãoandClaude Opus 5.5 e631839a4e fix(transit): quote Halifax's Open Government Licence
The licence page refuses automated requests; its text was read in a browser.
Credit now uses the licence's own attribution statement.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:08:40 -03:00
Itallo Brandão 543c607b23 Merge remote-tracking branch 'origin/main' into feat/transit-more-cities 2026-11-08 02:57:49 -03:00
Itallo BrandãoandClaude Opus 5.5 923287eb92 chore(changelog): name the transit fragment after PR #1628
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:56:19 -03:00
Itallo BrandãoandClaude Opus 5.5 71b694ab62 feat(transit): add 32 more keyless cities across Europe, Asia, Oceania and North America
Prague, Warsaw, Kraków (buses and trams), Poznań, Gdańsk, Rzeszów, Vilnius,
Bucharest, Cluj-Napoca, Iași, Rome, Turin, Venice (buses and waterbuses),
Bordeaux, Montpellier, Rouen, Toulon, Le Havre, Tours, Málaga, Lisbon's city
Carris, Sendai, Johor Bahru, Kota Bharu, Adelaide, Atlanta, Denver, Edmonton,
Halifax and Victoria.

Each entry records its licence, or that none is stated and the Metro
Transit/OVapi basis it ships on. Montpellier and Toulon publish km/h and are
converted. Route ids are read as the rider-facing line; Prague, Bordeaux and
Le Havre trams and Venice waterbuses take their own mode. Adelaide needs
`Accept: */*` to get binary protobuf. Every feed was fetched through the dev
proxy on 2026-11-08.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:50:32 -03:00
Itallo BrandãoandClaude Opus 5.5 e0ba12e5a9 feat(transit): add Lisbon's Carris Metropolitana buses
Keyless GTFS-Realtime feed from Carris Metropolitana, documented in the
operator's public API repository. No licence is stated for the realtime
endpoint (the static GTFS is CC BY 4.0), so it is credited as a courtesy on
the same basis as the Metro Transit and OVapi feeds. The feed publishes km/h,
so a new `speedUnit` registry field lets the proxy convert to m/s. Route ids
wrap the GTFS route id in an area prefix and are read as the line number.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:38:24 -03:00
Denis Groshev a3902590cd Merge branch 'main' into denis/gev-news-layer 2026-11-07 23:19:12 +01:00
Itallo BrandãoandClaude Opus 5.5 584138decf Merge main into feat/shipwrecks
Keeps main's reformatted layer table in docs/CURRENT-STATE.md and adds the Shipwrecks row to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:37:28 -03:00
daika c5bdcdf37f chore(format): adopt responseCompression.test.mjs into the format scope 2026-11-08 04:02:47 +07:00
daika 83bd48102a Merge remote-tracking branch 'upstream/main' into rb/opensky-scope 2026-11-08 04:02:08 +07:00
Itallo BrandãoandClaude Opus 5.5 dfd4ba04f8 Merge main into feat/transit-icon-heading
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:50:40 -03:00
German Khokhlov e1095ee553 Document radial pads, draped trail reveals and input options
Describe the radial disc envelope, corridor backing and reveal control,
rectangular input pick windows and optional horizon ellipsoid. Record the
launch-pad and Transit restoration and the Street Level and CCTV callers.
Explain synchronous Transit and reveal construction and surface readiness
for classified ground materials after camera changes.
Cesium remains the appearance and interaction reference; this documentation
changes no behaviour. All eleven coupling counts remain unchanged.
2026-11-08 00:30:36 +04:00
German Khokhlov aa6959c97b Restore ground, picking and horizon parity through the globe contract
Draw launch-pad zones as radial ground discs with the original fill, rim,
transitions, emission and depth bias. Drape Transit history and its backing
on ground corridors. Keep the current age bands and once-per-second updates;
advance the active head with a reveal fraction and rebuild only at segment
crossings. Its wider dark edge and colour core blend once, as on Cesium.
Navara composes these looks after tone mapping.

Build reveal corridors and dynamic Transit history synchronously once shared
terrain bounds are loaded. Keep requesting frames until the visible globe
tiles and committed primitives are ready: classification needs current
surface depth after a camera move. Retain the reference radial material,
its alpha envelope and the head's material-only reveal updates.

Remove the empty Navara ground effect from the render chain when the last
shown set is hidden or destroyed. It otherwise remains as the insertion
anchor for later pulse effects after its packed textures have been disposed.
Cover reveal, destruction and replacement with fresh-state comparisons for
corridors, polygons, discs and cells. Cesium drawing is unchanged by this fix.

Keep the radial pixel check at a 500 metre radius and 4 kilometre view,
sample the brightest of six inner points around the native fill gap, and
sample across the rim width. Restore its view during arrival flights and
run it last among ground checks. Both suite launchers wait for the linked
view to arrive before checking. Keep these checks with the radial look.

Pass rectangular pick windows and drill limits through input subscriptions.
Street Level uses its 9 pixel window and CCTV uses its 14 pixel window and
six-result drill. Down picks stay synchronous, moves coalesce per frame and
Navara clicks keep the GPU answer from their gesture. Add a projector
ellipsoid option and use WGS84 radii reduced by 1,000 metres for Street Level.

These changes restore the original Cesium pad fill and ground trail rather
than the temporary ring and raised lines. Navara follows the same appearance
and horizon policy, and now honours explicit CPU pick windows. Existing
callers without these options retain their defaults. The four debts share
the contract, suite and engine implementations, so keep them together.

No coupling count grew or fell: renderer lines remain 0 and 189, external
Cesium imports 3, external viewer lines 891, engine-kind checks 1, engine
resolution calls 0, extension reads 3, ground passes 1, SDK loads 3, screen
handler files 4 and engine-name lines 32.

The contract and ground suites now wait until the camera has reached the
view in their link before checking, because the app's restore flight could
land in the middle of a check; that was also the cause of the intermittent
camera-events failure. The radial check uses a launch pad's size and samples
the fill at several inner points: Cesium's own old pad leaves the same gap in
a small ground disc's fill. The pulse-band check takes its base frame once
the surface has settled.

Checked in the browser, keyless: the contract suite passes on both engines
(84 and 85 checks), the ground suite passes on both, CCTV v2 passes 58
checks, and the space, transit, Street Level and CCTV engine switches pass.
The transit and Street Level scripts fail as they do before this change.
2026-11-08 00:30:36 +04:00
Denis Groshev d55acd2ac5 docs(changelog): add the #1557 fragment 2026-11-07 19:52:33 +01:00
Denis Groshev 72112ecb45 feat(cockpit): ride the selected vessel from its bridge
With no aircraft tracked, Cockpit can ride the selected AIS vessel. Entry
needs only Live AIS Vessels and a selected vessel; C, double-click and the
entry buttons (reading BRIDGE with a ship icon) enter it, while voice stays
aircraft-only.

The eye sits on the AIS position at a representative bridge height for the
vessel's type family, looks along true heading and moves along course over
ground. The last fix is dead-reckoned for up to 180 s between the layer's
refreshes, and on rivers and lakes the waterline is read from the rendered
surface around the ship rather than the geoid.

The bridge HUD shows speed and course over ground, a straight heading
tape, window mullions, the AIS destination as a reported voyage and a
heading-up radar of nearby ships (3 to 24 NM). The arrow keys look around,
Home or 0 looks ahead again. The vessel layer hides the ridden ship's
sprite and card, ignores scene clicks while held, and returns the camera
to the vessel's map view on exit.
2026-11-07 19:52:30 +01:00
German Khokhlov be08163caa Document segment batches and draped Wildlife privacy cells
Describe the segment batch lifecycle, per-item picks and styles, and the
single-primitive engine implementations. Document the restored Wildlife
fill and outline and replace the stale no-fill description.

The documentation follows Cesium's stem behavior and original draped
privacy appearance. No coupling count fell or grew; all eleven metrics
remain unchanged.
2026-11-07 22:47:24 +04:00
German Khokhlov db4bdba64f Restore Wildlife privacy fills through engine.ground
Draw distinct privacy cells in one ground set with taxon-coloured fills
at 25 percent opacity and two-pixel outlines at 80 percent opacity.
Restore the original Cesium ground appearance on both engines, replacing
the shared display's raised, depth-free outline. Ground classification
handles terrain, tiles and horizon occlusion; picks pass through.

Retain bounds-and-accent deduplication, unchanged-geometry reuse, selection,
wide-view removal and detach cleanup. Check the fill and outline on fake
and headless Cesium engines, and keep engine-switch ownership coverage.
Update the Wildlife switch check to read ground cells.

No coupling count fell or grew: all eleven metrics are unchanged, including
891 viewer lines and three external Cesium import files.

Checked in the browser, keyless: the Wildlife engine switch draws the cell
set on both engines; its keyboard step still meets the known Tab issue.
2026-11-07 22:47:24 +04:00
German Khokhlov fbadf792d8 Batch infrastructure and cable stems through engine.lines
Add copied, committed segment batches to the contract, Cesium, Navara and
the fake engine. Keep each segment's colour, width and pick identity while
drawing one primitive or overlay mesh per display. Preserve Cesium's stem
sizes, ground sampling, visible cohorts, update cadence and click targets.
Navara draws the batch after tone mapping through the existing overlay.

Cover disconnected geometry, picks, hidden replacement, empty batches and
cleanup in the contract suite and engine tests. Keep the infrastructure
and cable display assertions on fake and headless Cesium engines, and
read batch counts and pick identities in the browser checks.

No coupling count fell or grew: all eleven metrics are unchanged, including
891 viewer lines, three external Cesium import files and 189 layer Navara
render lines.

Checked in the browser, keyless: the contract suite passes on both engines,
with the new segment picking and pixel checks; the infrastructure and
submarine-cable engine switches, the cable overlay and infrastructure LOD
scripts pass.
2026-11-07 22:47:24 +04:00
Said Turksever 52187c91d1 Merge pull request #1506 from inoukt/fix/taiwan-highway-cctv-fallback
feat(cctv): expand Taiwan, Québec, Ottawa, and World cameras
2026-11-07 18:39:58 +00:00
German Khokhlov fd65288101 Cover raised imagery shells on the headless Cesium engine
Exercise the real shell primitives and materials over the headless engine.
Cover raised rectangle geometry, alpha and depth policy, camera lift, shell
ordering, upload readiness, hidden frames, detail replacement and moved
windows, render idling and resource cleanup. Drive the public imagery API
through detail bounds and staged source settlement as well. Supply only the
geometry and texture completion signals that require a renderer.

Preserve Cesium's reference behavior without changing runtime code. Navara
and the fake engine are unchanged. Include the new test in the format scope.

All eleven coupling counts remain unchanged; none fell or grew. Renderer
lines remain 0 and 189, external Cesium import files 3, viewer lines 891,
Navara kind checks 1, engine-sniff calls 0, extension reads 3, ground passes
1, SDK loads 3, screen-handler files 4 and engine-name lines 32.
2026-11-07 22:24:46 +04:00
German Khokhlov 7f3754d812 Serialize engine and shell transitions through the scene host
Move engine request ordering and the single rollback decision into the scene
host. Run handoff, renderer release, candidate acquisition, attachment,
restoration and settling in one transaction. Pass that transaction to nested
parts so unrelated calls wait without putting nested work behind itself.
Keep the engine-switch entry point as a delegating adapter.

Route direct shell attachment and detachment through the same host and remove
the shell queue. Serialize late part registration with attachment and release.
Keep failed-attachment cleanup and renderer release inside the transaction,
including failed restoration. Replacement applications share the transition
owner with fresh parts, and retired applications release their part registry.
A shell used alone creates its own scene host.

Preserve Cesium's picker and history ordering, single recovery, retained layer
session, cockpit restoration, hidden-tab settling and parked-renderer policy.
Navara keeps the same behavior. No user-visible change is intended on either
engine. Keep the existing behavioral assertions and cover concurrent operations,
failed cleanup followed by recovery, retired transaction handles, replacement
startup failure and parked expiry during a switch.

All eleven coupling counts remain unchanged; none fell or grew. Renderer lines
remain 0 and 189, external Cesium import files 3, viewer lines 891, Navara kind
checks 1, engine-sniff calls 0, extension reads 3, ground passes 1, SDK loads 3,
screen-handler files 4 and engine-name lines 32.

Checked on the keyless production build: the engine, Director and layer
handoff switches, app-once, and the rapid, hidden-tab, context-loss,
parked-release, failure-matrix and soak scenarios pass.
2026-11-07 22:16:37 +04:00
Said Turksever 084a63ab5f Merge upstream/main into fix/taiwan-highway-cctv-fallback 2026-11-07 17:54:15 +00:00
Said Turksever 6af14f7f13 fix(cctv): leave Iceland to its own pack, credit World and Canada packs
Main now ships an Iceland pack that loads the full live Vegagerðin list
(about 500 views) with its CC BY 4.0 credit. The World catalog's 166
Vegagerðin rows point at the same frames under different ids, so every one
of those cameras would be drawn twice. Drop them from the World export and
its loader policy; World is now 2,230 rows and the default catalog limit
7,350 (4,000 shared slots plus 3,350 reserved).

Register Data attribution entries for Ottawa, Québec, TripCheck and MLIT,
as every other CCTV pack has. MLIT's terms ask for the source in the form
"出典:国土交通省 川の防災情報" with the page link, and ODOT asks for
"Camera courtesy of ODOT"; TripCheck partner cameras keep their owner's
name in the label.

The Houston guard test from main checked the old 4,000 default ceiling;
check the 4,000 shared slots instead, like the other pack tests.
2026-11-07 17:53:55 +00:00
Said Turksever 8a14029501 Merge pull request #1619 from NeoXrated23/feat/giroc-camera-hls-playback
feat(cctv): add Giroc camera pack and support larger HLS segments
2026-11-07 17:52:27 +00:00
Said Turksever cc3d390911 Merge upstream/main into fix/taiwan-highway-cctv-fallback 2026-11-07 17:46:53 +00:00
Said Turksever b99c2f1ade Merge remote-tracking branch 'upstream/main' into pr1619-fix2 2026-11-07 17:43:20 +00:00
quanticsoul4772 6150e21ebb Merge branch 'main' into upstream/nominatim-cache-mode-windows 2026-11-07 09:38:35 -08:00
quanticsoul4772 a1d3003b57 Merge branch 'main' into upstream/tools-heading-wrap 2026-11-07 09:38:22 -08:00
German Khokhlov c1e857f798 Scope tracked readouts and selection context to each scene
Give each scene attachment its own tracked readout, follow subscription,
selected context, active card identity and label-subject cache. Bind layer
context publishers to that attachment and preserve the page selection slot
and existing selection and awareness event payloads. Mirror a clear to the
page only when it clears the page selection. Capture display context
services so retired callbacks cannot publish into a replacement attachment.
Route aircraft, satellite, Local ADS-B and ALPR label refreshes and annotation
card lookup to the attached readout. Remove Local ADS-B contexts before
dropping its outgoing renderer, and mirror context pruning to the page.

Carry the retained page selection through the layer session when an attachment
is released. Adopt it only if the page still selects the same record, seed the
incoming readout on initialization, and replace its retained carrier when the
new display registers its marker. Keep the original page selection timestamp
and avoid a duplicate page selection event. A clear between attachments wins,
and page teardown still clears the selection. Seed the incoming readout from
that retained record without validating the outgoing display's visibility:
FIRMS waits for camera restoration before registering its new display. Keep a
matching selected fire in the refresh set before pruning top-N contexts, so
weaker selected detections retain the page selection too.

Preserve Cesium's label text, selected-card policy, visual anchors and
follow-camera anti-jitter. Navara retains the same shared behavior. There is
no intended user-visible change on either engine. Tests cover simultaneous
follows, identical selection IDs, independent refresh and clear, stale
notifications, destruction, parked reattachment and real layer publication
on the fake and headless Cesium engines. The production aircraft tests also
cover grounded fleet and tracked models, the display floor on both sides of
the zoom boundary, follow-label refresh and cached position reads. The fire
regression exercises the application scene handoff, delayed viewport resume,
top-N pruning, unchanged selection timestamp and deliberate clear on both
engines. These fixes preserve the reference Cesium selection behavior.

All eleven coupling counts remain unchanged; none fell or grew. The counts
remain 0 and 189 renderer lines, 3 external Cesium import files, 891 viewer
lines, 1 Navara kind check, 0 engine-sniff calls, 3 extension reads, 1 ground
pass, 3 SDK loads, 4 screen-handler files and 32 engine-name lines.

Checked in the browser, keyless: the overlay baselines on both engines, the
ALPR and FIRMS fire-camera switches, and the layer handoff, Local ADS-B,
Contacts, space, infrastructure, CCTV, urban, transit, directions and
annotation engine switches pass. The tracking regression passes every check
that passes before this change.
2026-11-07 21:14:09 +04:00
manjunath22466 3ef3b9d555 Merge remote-tracking branch 'origin/main' into feat/viewport-context-decision-upstream 2026-11-07 21:52:46 +05:30
Said Turksever ca6eace79e Merge pull request #1617 from daikaginza/feat/cctv-iceland
feat(cctv): add Iceland (Vegagerðin / IRCA) road cameras as a keyless source pack
2026-11-07 15:34:07 +00:00
German Khokhlov 3814221cbc Scope world overlays to each scene attachment
Move the world overlay into scene.services.overlay. Each attachment owns its
sources, collision domains, paint lanes, canvases, observers, timers and
accessible action subscribers. Keep both canvases inside the engine container,
with detection beside the card root so its screen blend still reaches the
scene. Release shared font measurements only after the last host detaches.

Resolve retained layer publications against their attached engine, clear the
outgoing source before releasing its renderer, and reconnect the shell's action
subscription on attachment. Keep destroyed hosts inert and give parked engines
fresh services. Bind annotation rectangles and the existing tracked bridge to
their engine's overlay. The tracked bridge itself remains page-owned.

Preserve Cesium's projection, card placement, arbitration, fades, hit tests,
blend order and render demand. Navara keeps the same shared behaviour. No
user-visible change is intended. Exercise simultaneous hosts, either teardown,
stale handles, reattachment and a retained selected card on the fake and
headless Cesium engines. Update QA reads and package ownership for the service.

No coupling metric falls or grows: renderer lines remain 0 Cesium and 189
Navara; external Cesium import files 3; viewer lines 891; Navara kind checks 1;
engine sniff calls 0; extension reads 3; ground passes 1; SDK loads 3;
ScreenSpaceEventHandler files 4; engine-name lines 32.

Each host now takes the page's overlay ids from the host it replaces and gives
them back to a remaining host when destroyed, and the chrome's static overlay
placeholders are gone. The new hosts had left the ids on those empty
placeholders, so the voice pointer policy, the id selectors and the QA probes
that read #world-overlay-canvas missed the drawn canvas.

Checked in the browser, keyless: the overlay baselines on both engines, CCTV
v2 (58 checks), military names, and the engine, voice, ground-layer and Ghost
Maps switches pass. The transit script fails the same 25 checks as before.
2026-11-07 19:27:31 +04:00
Said Turksever 39d70fa1c0 fix(cctv): keep Icelandic labels intact and treat 'og' facings as ambiguous
- Label repeat-collapse uses letter lookarounds instead of ASCII-only \b, so
  'Þverá Árnessýslu' is no longer joined and 'séð séð' collapses.
- 'til austurs og vesturs' and 'í norður og suður' name two facings and keep
  the low-confidence pose.
- Fallback name uses the frame stem when the station number is missing.
- Tests: label and heading regressions, dedupe keeps the last row, cap clamp,
  high-confidence pose, frame redirect pin; file added to format scope.
- Credit names the dataset, links the terms and states no endorsement.
2026-11-07 14:38:52 +00:00
Said Turksever 8455c4bce1 Merge branch 'main' into feat/cctv-iceland 2026-11-07 14:38:51 +00:00
manjunath22466 2193c2116e Merge main into viewport diagnostics PR (#835) 2026-11-07 20:08:09 +05:30
Said Turksever 872bbefb63 docs(cctv): README HLS cap to 8 MiB; Giroc pack local-only note 2026-11-07 14:29:06 +00:00
Said Turksever 2213d3d8cf Merge remote-tracking branch 'upstream/main' into pr1619-fix 2026-11-07 14:28:44 +00:00
German Khokhlov 26597ad25c Decode the Bhote Koshi images once for every engine
The event's before and after images went to each engine's display as URLs,
so every engine switch fetched and decoded both again: the strict Nepal event
scenario saw three reads of each image where Cesium's kept providers made one.
The event now decodes the pair once with its own loader and hands the images
to each display, which takes a URL or a decoded image. The pair is dropped
with the rest of the event state on disable, and a failed load is retried on
the next attachment.

The wind engine-switch scenario waited for a diagnostics field the wind layer
lost when it moved onto engine.imagery; it now waits for imageryActive.

Checked in the browser, keyless: the Nepal event, wind and submarine-cable
engine switches pass.
2026-11-07 17:02:49 +04:00
German Khokhlov 0459ee6d58 Scope render demand to each scene attachment
Give each scene its own render governor, owner holds and recent idle requests.
Make the Cesium render backend own its request mode directly, so the governor
drives engine.render on both engines without a dependency back from Cesium.
Route layer requests through their attached engine and capture the attachment's
governor for camera motions, orbit, annotations and shell controls. Release
retained-layer holds before clearing the outgoing engine, and destroy the
governor after scene consumers have detached.

Preserve Cesium's owner-keyed holds, pre-install buffering, last-release settling
frame and one-shot requests during continuous rendering. Navara keeps the same
render policy. There is no intended visible change on either engine. Simultaneous
scenes no longer share holds, and an old captured service cannot release a
replacement attachment's hold. Cover both rules on fake and headless Cesium
engines, including an old orbit ending after reattachment.

Viewer-reference lines fall from 910 to 891, Navara-kind checks from 2 to 1,
and engine-name lines from 33 to 32. All other coupling counts stay unchanged.

Checked in the browser, keyless: qa-perf passes on both engines with the
same idle, active and release counts as before (24 checks on Cesium, 22 on
Navara).
2026-11-07 16:56:34 +04:00
German Khokhlov 4252ed0798 Scope pick ownership and selection dispatch to each scene
Create pick registries and selection dispatchers for each scene attachment,
publish them through scene.services, and release their predicates and input
subscriptions before parking the renderer. Resolve layer ownership with the
attached engine so identical layer IDs in simultaneous scenes cannot overwrite
or remove one another. Re-register retained ALPR and cyclone picks on attachment,
and resolve voice pointer picks against the engine that supplied the pick.

Preserve Cesium's selection, sibling ownership, inert trails, touch expansion,
drag and long-press rules on both engines. Extract the unchanged gesture
predicates so portable picking does not acquire a native input dependency.
Exercise scene isolation, teardown and reattachment on fake and headless Cesium
engines, retaining the existing layer assertions.

No coupling count falls or grows. Cesium and Navara renderer lines remain 0 and
189; external Cesium imports 3; viewer lines 910; Navara kind checks 2; engine
sniff calls 0; extension reads 3; ground passes 1; SDK loads 3; screen-handler
files 4; engine-name lines 33.
2026-11-07 16:27:43 +04:00
German Khokhlov 52c6f68bd5 Enforce engine SDK boundaries and rehearse every layer display
Restrict runtime SDK imports to their engine directories, including package
subpaths, re-exports and dynamic imports. Keep three measured, reasoned
exceptions for the Bhote Koshi property-record models and reject stale
exceptions. Move the Cesium annotation renderers and both wind adapters to
their engine folders, preserving their implementations and lazy loading.

Load all 35 manifest display entries on a fake engine with an unknown kind
and no native extensions. Draw small fixtures and verify resource and
subscription cleanup before destroying the engine. Name the three entries
without display factories as skips. Update the architecture, infrastructure
lifecycle and engine integration documentation to describe the code today.

Cesium behavior is unchanged, and Navara keeps its existing behavior. The
weather comparison harness supplies the Navara flow-mesh factory and accepts
the previous backend location for comparison runs. No user-visible strings
change.

Cesium layer-renderer lines fall from 14 to 0, Navara layer-renderer lines
from 314 to 189, outside-engine Cesium import files from 5 to 3, viewer lines
from 933 to 910, extension reads from 4 to 3, and engine-name lines from 36
to 33. No coupling count grows. Targeted checks pass: 845 tests, three named
rehearsal skips. Import directions, package boundaries, formatting and syntax
checks pass.

Checked in the browser, keyless: the contract suite passes on Navara and on
Cesium apart from the camera-events check that also fails intermittently
before; Navara annotations pass across engine switches; the ground-look
comparison is pixel-identical. qa-draw-tool's hidden-line check and the wind
engine switch fail the same way before this change.
2026-11-07 16:06:43 +04:00
Sameh Khamis 6cadfc9583 Merge pull request #1620 from bilawalsidhu/fix/cctv-initial-nearest-camera
CCTV opens on the camera nearest the viewer, not the first in the catalog
2026-11-07 04:04:38 -08:00
Sameh Khamis 2cac463c6f Changelog: CCTV opening camera 2026-11-07 03:52:54 -08:00
Sameh Khamis 479286a6a6 CCTV opens on the camera nearest the viewer, not the first in the catalog
Turning CCTV on picked the first catalog record, so at King's Cross it
showed a Sydney camera. enable() now picks the camera nearest the viewer.
Until someone chooses a camera, each settled move re-picks the nearest,
so a share link whose flight lands after CCTV turned on still opens
nearby.
2026-11-07 03:52:38 -08:00
German Khokhlov 656943641c Separate scene handoff and attachment from the page shell
Move the page handoff sequence into SceneSwitcher and include restoration
in the application's candidate attachment. Release a failed candidate once
before the request coordinator recovers the working engine. Keep address
ordering, parked renderers, retained layers and cockpit resumption intact.

Define the layer session and its field owners. Share its retention decision
across teardown paths. Separate layer scene binding from lifecycle policy,
and shell scene composition from page controls. Keep failed renderer
releases in the binding owner, with the existing retry behavior.

Give Director the scene and use the engine host for its canvas and pack
container. Keep the existing tracked-subject interaction guard so neither
engine's behavior changes. Pass startup chrome the first backend's explicit
welcome policy instead of testing for a viewer. Cesium remains the behavior
reference; this restructuring intentionally changes no visible behavior on
either engine.

Viewer references outside the Cesium engine fall from 948 to 933. All other
coupling counts stay unchanged. Scene service ownership, raw debug access,
and the remaining shell and host queues still need consolidation.

Checked in the browser, keyless, on the dev server: the engine switch,
Director engine switch, layer panel and layer visibility on both engines
pass, and 14 of 15 strict scenarios pass (rapid, hidden-tab, context-loss,
parked-release, Navara HD, failure matrix and soak among them). app-once
and the Director next-shot step fail the same way before this change.
2026-11-07 15:44:03 +04:00
German Khokhlov f385986383 Draw Bhote Koshi and awareness pulses through engine.ground
Render the Bhote Koshi event and locator through one ground and point display.
Keep Cesium's reveal paths, dashed halo, glowing core, polygon classification,
pick identities and independent point outline opacity. Add static pixel dash
masks, synchronous ground reveals after terrain bounds load, and per-point
outline colour so the shared display preserves those behaviours.

Add a ground pulse field with checked state on all three engines. Keep Cesium's
raised material disc, band equations, front widths, wake and rest timing and
reduced-motion behaviour. Navara now follows Cesium's raised pulse surface and
story styling, with ground effects composed after tone mapping. Move the wind
mesh shader into the Navara engine without changing its transparent pass.

Layer renderer lines fall from 210 to 14 for Cesium and from 794 to 314 for
Navara. Viewer lines fall from 953 to 948, extension reads from 6 to 4, and
engine-name lines from 37 to 36. No coupling count grows.

Checked in the browser, keyless: the contract suite, with its new pulse
pixel checks, passes on Navara and on Cesium apart from the camera-events
check that also fails intermittently before; the Nepal locator and Contacts
engine switches pass. The Nepal event and wind engine switches fail the same
way before this change. Side by side at the locator's overview, Cesium draws
the same pixels as before and Navara's river corridor now glows as Cesium's
does, where it was faint.
2026-11-07 15:06:26 +04:00
German Khokhlov dacfd33f10 Move shared follow offsets and Draw onto the globe contract
Share links read the current follow handle and displayed position through
engine.camera and restore their ENU offset with follow's swingTo option.
Cesium retains its first-frame wait, displayed-position cache, 60-degree
frame limit and range interpolation. Navara and the fake engine apply the
same swing. Cancellation, camera handoff and destruction release it; an
externally tracked Cesium entity keeps its existing handle.

Draw uses one surface with engine input, geodesic lines and point movers.
An exclusive pointer claim suppresses ambient subscriptions and deferred
picks across ownership changes. Cesium owns suspension and exact restoration
of its stock select and track actions. Camera gestures remain enabled.
Detaching releases the sketch and preview and retains shape and colour.
The separate Navara surface and Cesium data-source preview are removed.

Cesium remains the reference for clicked heights, line width and dash,
vertex size and outline, keyboard handling and completed annotations.
One deliberate visual fix is that the occluded rubber band uses the selected
colour instead of retaining the initial amber: it now identifies the same
sketch as its visible segment. Navara adopts Cesium's geodesic preview edges.
Finished annotation drapes are unchanged.

External Cesium import files fall from 7 to 5, viewer lines from 1005 to
953, and engine-name lines from 39 to 37. No coupling metric grows.

The full unit suite, import directions, package boundaries, formatting
and coupling checks pass.

Checked in the browser, keyless: the contract suite passes on Navara and on
Cesium apart from the camera-events check that also fails intermittently
before; qa-navara-draw passes; qa-draw-tool passes apart from its hidden-line
pixel check, which fails the same way before this change. Its destroy check
now expects no preview data source, since the preview has none.
2026-11-07 14:31:59 +04:00
German Khokhlov 910d693b94 Keep bounded imagery off Cesium's base layer and pins on drawn 3D tiles
Cesium treats the first imagery layer shown on the globe as its base
layer and stretches that layer's edge texels over the whole globe. With
no base map under it, a KMZ ground overlay turned the whole globe its
edge colour. The Cesium imagery adapter now keeps a floor under an
engine layer that covers less than the globe (a bounded rectangle, or
Web Mercator tiles, which stop at ±85.05°) whenever that layer would
otherwise be the base layer. The floor is one transparent pixel over the
globe at alpha 0, so Cesium draws nothing of it. It is placed when the
adapter adds or changes a layer, and again at each scene preUpdate,
before Cesium picks the base layer. When base imagery lies under the
layer, which is the app's normal state, there is no floor. A bounded
layer now draws only inside its rectangle, as the contract says.

When the globe is not the drawn surface, the import display now clamps
ground pins to the drawn scene, as Cesium's import did whenever the
globe was hidden. Before, it did this only for the map's own 3D tiles.
Each height query now leaves out the point sets of every imported file
on the engine, not only the file's own sets: Cesium's import kept all
imported pins out of height queries, so a higher pin from another file
never became the ground. The display also gives a footprint with its
points at the heights they are drawn at, and flying to a file frames
pins on raised tiles as Cesium did. Cesium's import clamped visible pins
with clampToHeightMostDetailed, which loads the finest tiles. The
display samples the tiles drawn now and samples them again after the
camera stops and while tiles load.

The KML reader's parity test against KmlDataSource now includes the
large-file QA's style case: a shared style with point, line and polygon
parts, inline overrides of each part, and named styles and an inline
StyleMap pair that are alike. The reader matches Cesium there.

QA scripts:
- qa-geo-import moves the camera over the files before it reads which
  pins show. Ground pins are hidden only beyond the horizon.
- qa-geo-import-large compares clamped pins by longitude and latitude.
  Cesium keeps a clamped billboard at its file height, while the import
  draws its pins on the sampled ground.
- qa-geo-import-regressions gives its 3D-tiles fixture a non-zero
  tileset geometricError, because Cesium never draws a tileset whose own
  error is 0. Its XYZ check now waits for the import to sample the ground
  again once the globe's tiles have loaded.

Known differences that remain: a KMZ ground overlay is now imagery, so
ground fills draw over it where the old ground rectangle drew over them;
right after an import on ellipsoid terrain, pins can sit on the coarse
globe mesh until the ground is sampled again.

The full unit suite and the boundary, format and coupling checks pass.
In the browser, keyless: qa-geo-import, qa-geo-import-large and
qa-navara-geo-import pass; qa-geo-import-regressions fails only its
portrait-flight check, which also fails before the refactor.
2026-11-07 14:11:03 +04:00
German Khokhlov f4b83a3a31 Draw CCTV and data packs through engine.meshes
Add indexed world triangles to the required globe contract on Cesium,
Navara and the fake engine. Own geometry, textures, visibility, depth tests,
pick identities and teardown inside each engine. Reused canvas images
request another frame until their new pixels upload. Move obstruction rays
and owned mesh, line and mover exclusions into the Cesium surface adapter.

Draw CCTV monitor planes and viewshed volumes through shared mesh handles.
Draw data-pack points, straight lines, raised polygons, image rectangles
and media captions through movers, lines and meshes. Delete the feature
renderers for each engine and retain decoded assets across scene rebinds.
Register the modules, dependency, format scope and reduced coupling baseline.

Cesium is the reference for geometry, colours, image placement, marker sizes,
caption offsets, picking and resource ownership. Navara now uses straight
pack lines and per-position polygon heights without edge densification.
Navara meshes draw in the transparent pass after the effects, with the
scene-depth test and the view's exposure undone, as the pack meshes did:
in the scene pass the landscape look's exposure washed a red pack image to
white. The scene-depth helper moves into the Navara engine, and the stems
helper that only its test still used is deleted. The mesh picker takes the
globe when Navara's depth read answers with the camera itself, which it
does right after another click and which hid a polygon from a quick second
click. The 256 px media caption is not pickable: its transparent square took
clicks meant for nearby pack features, and no action targets it. Annotation drapes remain on their existing
renderers because bounded building classification, animated fills and wire
cages need a separate contract extension to preserve Cesium behaviour.

Coupling falls from 344 to 210 Cesium layer-renderer lines, 1294 to 794 Navara
layer-renderer lines, 9 to 7 external Cesium import files, 1026 to 1005 viewer
lines, 9 to 6 extension reads and 42 to 39 engine-name lines. Other counts do
not grow. The full unit suite, import directions, package boundaries,
formatting and coupling checks pass. No user-visible strings change.

In the browser, keyless: the contract suite passes on Navara and on Cesium
apart from the camera-events check that also fails intermittently before
this change; CCTV v2 (58 checks), the CCTV and director engine switches pass,
with the pack image at the same pixel as before on both engines. The mesh
contract fixture now sits at 3 km, above the terrain Navara draws there. The
director interactions script starts at its view in the link so the app's
arrival flight no longer lands after the shot, and waits for the click's
asynchronous pick; its last step, the next-shot action, fails the same way
before this change.
2026-11-07 13:57:29 +04:00
NeoXrated23 9b65d41bab docs: add changelog entry for Giroc camera contribution 2026-11-07 11:48:07 +02:00
NeoXrated23 891eb56091 feat(cctv): add optional Giroc camera and support larger HLS segments 2026-11-07 11:46:48 +02:00
German Khokhlov 3907b7084e Read QA drawing evidence through the globe contract
Port the layer probes in twenty-six browser scripts to engine primitive
lists, drawn rows and layer diagnostics. Preserve Cesium as the reference
and keep the supported Navara paths on the same observations. This
changes QA observation only, with no application behaviour change.

Review the interrupted infrastructure group and retain its readiness,
drape, selection, label and floor checks. Match both installation outline
buffers. Frame each CCTV city before reading its visible coverage. Read
Radio colours and selected identities from actual mover writes. Retain
transit's GPU upload budgets using engine-owned line primitives rather
than substituting rebuild counts. Keep native inspection where a check
specifically measures GPU uploads, loaded meshes or ground readiness.

The preceding combined B/C commit owns all shared manifests, docs,
locale catalogs and the coupling baseline; this commit changes only its
own twenty-six QA scripts. Coupling remains unchanged at 344 Cesium
renderer lines, 1294 Navara renderer lines, 9 Cesium import files, 1026
viewer lines, 2 Navara kind checks, 0 engine sniff calls, 9 extension
reads, 1 ground pass, 3 SDK loads, 4 screen-handler files and 42 engine-name
lines.

The full unit suite and the boundary, format and coupling checks pass.
Browser validation remains to be run.

Browser checks to run
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
node scripts/qa-alpr-journey.mjs http://localhost:4291
node scripts/qa-alpr-journey.mjs 'http://localhost:4291/?engine=navara'
node scripts/qa-cables-overlay.mjs --url http://localhost:4291
node scripts/qa-cctv-v2.mjs --url http://localhost:4291 --fixture --terrain-globe
node scripts/qa-context-arrivals.mjs http://localhost:4291
node scripts/qa-directions.mjs --url http://localhost:4291
QA_BASE_URL=http://localhost:4291 node scripts/qa-draw-tool.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-hold.mjs
node scripts/qa-ghostmaps.mjs --url http://localhost:4291
node scripts/qa-gnss.mjs --url http://localhost:4291
node scripts/qa-height-datum.mjs --url http://localhost:4291
node scripts/qa-infra-lod.mjs --url http://localhost:4291
node scripts/qa-installation-navigation.mjs http://localhost:4291
node scripts/qa-military-names.mjs http://localhost:4291 --no-video
node scripts/qa-overpass-offload.mjs --url http://localhost:4291
node scripts/qa-radio.mjs --url http://localhost:4291
node scripts/qa-traffic-motion.mjs http://localhost:4291 --mode=osm
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit-recovery.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-transit.mjs
node scripts/qa-weather-teardown.mjs --url http://localhost:4291 --headless
for engine in cesium navara; do
  node scripts/qa-enrich-ambient.mjs --url http://localhost:4291 --engine=$engine
  QA_BASE_URL=http://localhost:4291 node scripts/qa-floor-verify.mjs --engine=$engine
  node scripts/qa-heading-b3.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-overlay-baseline.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-sprites-b5.mjs --url http://localhost:4291 --engine=$engine
  node scripts/qa-vessel-datum.mjs --url 'http://localhost:4291/?qaFeed=vessels:fixture' --engine $engine
done

The military-names driver exercises installation-polish; the transit
driver exercises transit-scenes. Use only the keyless server. Cases that
require photoreal tiles or unavailable providers remain unverified.
2026-11-07 12:45:21 +04:00
German Khokhlov 97c4dc76ce Move scene effects and imported geometry behind the globe contract
Move Cesium viewer, map, sky, projection, trail and sonar internals into
its engine. Drive styles, bloom, sharpening and contact sonar through
engine.render. Count camera-input suspensions across all three engines
and migrate the feature callers. Preserve Cesium's effects and input
behaviour; fix transit lens clicks on Cesium and make zero sharpening on
Navara disable sharpening, as it does on Cesium.

Parse imported files into plain records and draw them through movers,
lines, ground and imagery on both engines. Follow Cesium's KML style
precedence, preserve local KMZ images and frame the drawable records.
Open overlays use imagery when the engine has no native map-overlay API.
Run every imported-display case on fake and headless Cesium engines.

The shared import path retains these known differences from Cesium's old
DataSources: wall fills, extruded prisms and gx:LatLonQuad are undrawn;
raised polygon fills drape while outlines retain altitude; GroundOverlay
rotation, altitude and RGB tint are not applied; rhumb lines become great
circles. Ground heights use the loaded surface synchronously instead of
waiting for most-detailed sampling. Supported KMZ ground overlays now
draw on Navara too. Document the limits rather than claiming full parity.

Combine batches B and C because their disjoint path manifests cannot
place the shared boundary and format files in a later commit while
keeping B independently valid. This combined commit owns the shared
docs, package boundaries, format scope, locale catalogs and coupling
baseline. Remove five obsolete import strings from all eleven catalogs.
Make the load-time source scan include new files and skip deleted paths
so the same checks work before and after recording the moves.

Coupling: Navara renderer lines fall from 1695 to 1294, Cesium import
files from 25 to 9, viewer lines from 1177 to 1026, SDK loads from 4 to 3
and engine-name lines from 62 to 42. No count grows.

The full unit suite and the boundary, format and coupling checks pass.
Browser validation remains to be run.

Browser checks to run
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
node scripts/qa-contract-suite.mjs http://localhost:4291
node scripts/qa-navara-visual-styles.mjs http://localhost:4291
node scripts/qa-navara-bloom.mjs http://localhost:4291
node scripts/qa-navara-sonar.mjs http://localhost:4291
QA_BASE_URL=http://localhost:4291 node scripts/qa-visual-effects.mjs
QA_BASE_URL=http://localhost:4291 node scripts/qa-zoom-out-guard.mjs
node scripts/qa-sky-model-lighting.mjs --url http://localhost:4291
node scripts/qa-geo-import.mjs http://localhost:4291
node scripts/qa-geo-import-regressions.mjs http://localhost:4291
node scripts/qa-geo-import-large.mjs http://localhost:4291
node scripts/qa-navara-geo-import.mjs http://localhost:4291

Use the keyless server for every check. Photoreal-only cases remain
unverified; do not enable paid tiles to run them.
2026-11-07 12:45:21 +04:00
German Khokhlov c079b329d0 Remove the weather and wind imagery fallbacks
Require engine.imagery for weather and wind and load the wind flow backend
from the layer manifest. Preserve Cesium's staging, frame retention, shell
ordering, wind fade and streamlines; Navara follows the same display policy.
Remove obsolete rendering modules and their three translated status keys.
Run every weather and wind display case on both the fake engine and the
headless Cesium engine, keeping the existing behavioural assertions.

Coupling: Cesium import files fall from 27 to 25, viewer lines from 1232 to
1177 and engine-name lines from 63 to 62. The explicit Cesium wind flow
backend deliberately raises renderer lines from 330 to 344 and extension
reads from 8 to 9; every other count stays unchanged.

Browser checks to run
GOOGLE_MAPS_API_KEY= CESIUM_ION_TOKEN= npm run dev -- --port 4291
node scripts/qa-contract-suite.mjs http://localhost:4291
node scripts/qa-weather-compare.mjs shoot http://localhost:4291 --out=qa-shots/weather-after

Compare weather and wind against the previous capture on both engines.
2026-11-07 12:16:27 +04:00
German Khokhlov c73c5382d4 Draw weather, wind and Bhote Koshi through engine.imagery, one display each
engine.imagery gains what these rasters need, on every engine:
- geographic (EPSG:4326) tile schemes and a transform applied to each
  decoded tile (the satellite infrared mask);
- staged source changes, which keep the drawn source until the new one
  has loaded (the weather frame swap), and credit-only changes in place;
- raised layers that draw on a shell above terrain and 3D tiles, rising
  with the camera, with a detail image inside a rectangle. On Cesium this
  is the weather shell primitive, moved into the engine; on Navara the
  pixel's ray meets the ellipsoid grown by the height.

Rain radar, satellite clouds, lightning, the wind field and the Bhote
Koshi event and locator each draw through one render/display.js on both
engines; their Navara renderers and Navara's weather surface pass go.
Cesium's pixels are unchanged in every compared scene. On Navara the
Bhote Koshi images are no longer upside down. Two engine-specific parts
stay behind one backend option each: the wind's streamlines and the
Bhote Koshi story entities.

The coupling report now counts defineGroundEffect calls, not its own
declaration. Coupling: Navara renderer lines fall from 2304 to 1695,
Navara ground passes from 3 to 1 (the Global Context pulses), engine
kind checks from 5 to 2. The contract suite passes on both engines
(Cesium 71/71, Navara 74/74).
2026-11-07 11:14:50 +04:00
German Khokhlov b53b6a48a7 Load the layer displays lazily and rebase the main-chunk budget
Every layer display was imported statically from the manifest, so all of
them sat in the Cesium-mode main chunk. They are re-exported from one
module, src/app/layers/displays.js, which the manifest loads with one
import() before the catalog is built, on every engine; rendererFor stays
synchronous after that. One chunk beats per-family chunks here: every
start loads every display, and split chunks compress worse. CCTV's world
click gesture moves out of its display, which the app imported only for
it. Tests that build layers without the application await the loads.

Keyless, gzip-9: the main chunk falls from 2,113,641 to 2,066,560
bytes, below the 2,090,274 of the merge with main this branch carries.
A cold Cesium start loads about 7 KB more JS in total over four requests,
with the same time to a ready page. The budget's baseline moves to the
new size; it had never absorbed main's own growth.
2026-11-07 10:40:50 +04:00
Sameh Khamis c0a5d4141d Merge pull request #1616 from bilawalsidhu/fix/chat-tool-schema-patterns
fix(chat): send OpenAI chat tool schemas Chat Completions accepts
2026-11-06 22:21:05 -08:00
daika f5d5da7023 docs(changelog): add the Iceland CCTV pack fragment 2026-11-07 12:57:48 +07:00
daika 021e887f94 feat(cctv): add Iceland (Vegagerðin / IRCA) as a keyless CCTV source pack
One JSON list from gagnaveita.vegagerdin.is (about 500 camera views at roughly
170 road stations). Frames are JPEG stills in a single directory on
www.vegagerdin.is; only links inside that directory are registered, and the
media proxy pins the same directory.

The list decides ids, coordinates and which frame path is proxied, so it is
fetched over https with redirects refused and the body read capped at 2 MiB;
every rejection path releases the response body. Camera ids come from the frame
file name, which is unique per view in the live feed, and repeated frames
collapse before the cap.

The feed's view description (`Skyring`) is the label. An explicit Icelandic
facing ("til vesturs", "í suður", "séð austur") sets a high-confidence heading
(264 of 499 views); a view that states none, or two different ones, keeps the
id-hash low-confidence pose. A bare road direction ("Miklabraut austur") is not
read as a facing.

IRCA publishes road camera images under CC BY 4.0 and asks that the licensor is
acknowledged without implying endorsement; the attribution is in the Data
attribution popover and DATA_SOURCES.md, and every camera carries the licence
string. Default cap is 250 with anchors at Reykjavík and four regional centres so
a cap keeps every region. CCTV_ICELAND_ENABLED=0 turns the pack off.

Verified against the live feed through fetchCctvImageFromUpstream: 499 of 499
frames returned a JPEG, all distinct, all under 20 minutes old.
2026-11-07 12:57:08 +07:00
Sameh Khamis 8ec92eb48c docs(changelog): OpenAI chat tool patterns (#1616) 2026-11-06 21:52:03 -08:00
Sameh Khamis 5e42a83d88 fix(chat): send OpenAI chat tool schemas Chat Completions accepts
OpenAI refused every typed chat request with 400 invalid_function_parameters:
resolve_area's area-id pattern uses Unicode property classes (\p{L}), which
Chat Completions does not accept as a regex. OpenAI chat now leaves such
patterns out of the tool schemas it sends; each action still checks its
arguments against its own contract, and realtime voice keeps the patterns.
2026-11-06 21:51:46 -08:00
German Khokhlov 47d63128ff Bin Navara ground corridors set-wide and give corridors glow, pulse and dash
Navara's ground pass cut each corridor into chunks and tested them per
screen cell, so dense corridor sets were slow (10,000 cost about 150 ms a
frame). A corridor set is now cut into pieces along its great circles,
sorted into lon/lat tiles, each with a pyramid of bins listing the
segment runs within its margin; a pixel takes its tile and the finest
bin that covers its reach and tests only those runs. At 10,000 corridors
a frame now takes 11.8 ms still (Cesium 23 ms), at 15,000 17.9 ms. A
pixel corridor's width is measured across the line, so diagonal lines no
longer draw up to 1.4 times wider.

Pixel corridors take three looks, evaluated on the GPU from a time
uniform: Cesium's PolylineGlow falloff (glow), an alpha pulse and the
annotation route's flowing dash. While a shown set pulses or flows the
engine draws continuously, as Cesium did. Traffic's jam lines take the
glow and pulse again and the Directions route its flowing dash and render
hold, so on Cesium both match the drawing before they moved to the
contract pixel for pixel. The contract suite passes on both engines
(Cesium 69/69, Navara 72/72).
2026-11-07 09:50:49 +04:00
German Khokhlov 48885d9e61 Build the catalog from one layer manifest and attach one tools tree
src/app/layers/renderers.js now holds LAYER_MANIFEST, one entry per
layer in directory order: a display (the same render/display.js on every
engine, with the contract primitives it needs and, for CCTV and Global
Context, a Navara backend loaded by import()), a layer that draws itself
through the contract, or per-engine renderers for the layers not on a
display yet (weather, wind, Bhote Koshi, sea marks, ski pistes). A layer
is available where the engine has the primitives it needs. The Cesium
renderer table, CESIUM_BUILTIN, the registry and the Navara mode's map of
about 35 loaders go; the application loads a kind's renderers before it
builds the catalog.

The tools attach through one tree over the engine, in Cesium's order.
Each engine kind adds its own tools from one backend that reads its
extension once: the Cesium backend holds the viewer-only tools, the
Navara backend the annotation renderer, draw surface and file import
drawing.

Coupling: engine.extensions reads fall from 12 to 8, engine-name literal
lines from 73 to 66.
2026-11-07 09:41:02 +04:00
German Khokhlov f1ec9d4a22 Hand features the engine instead of guessing it from a viewer
No code outside src/globe calls isGlobeEngine or engineFromViewer any
more (14 calls left before this). The features that still sniffed take
the engine from their caller:

- Projection and surface helpers (focus de-emphasis, icon orientation,
  ground snap, the mesh floor sampler, ground picks) take the engine; the
  engine view's scene carries its projector instead of registering itself
  as a viewer.
- Credits go only through engine.credits, so annotations and layers
  share one OSM credit state on Cesium too (it could show twice).
- The annotation board and the resolver work on the engine: camera
  assists, visibility and picks go through engine.camera,
  engine.projector and src/geo, without Cesium.
- The Recent Imagery box tool has one implementation for both engines,
  on capture-phase canvas pointer events with the layer's engine.ground
  preview; the Navara copy goes. On Cesium the preview outline is solid
  now, as on Navara.
- Scene interactions take the engine, and the render governor has an
  engine variant beside the Cesium one.

Remaining engine-specific features stay behind explicit seams until the
contract covers them: the draw tool's Cesium surface (stock click actions
cannot be suspended), the annotation drapes (animated materials), share
follow, Cesium post effects and cyber sonar, and data packs (meshes).

Coupling: Cesium-importing files fall from 31 to 27, `viewer` lines
from 1367 to 1234, engine sniffing calls from 14 to 0.
2026-11-07 08:46:30 +04:00
German Khokhlov 406dd9122e Add raster imagery to the globe contract and draw FIRMS heat and Recent Imagery through it
engine.imagery draws raster layers over the globe: Web Mercator tile
templates and single images or canvases stretched over a rectangle, with
alpha, show, a left/right split against the engine's split position and
an order. A layer's source can be replaced in place.

- Cesium puts one ImageryLayer per layer on the globe's imagery stack,
  or on the shown 3D tileset's when the globe is hidden, and moves it
  when the surface changes; the Recent Imagery request gate moved here.
- Navara draws them in one screen pass after tone mapping that samples up
  to eight layers by geodetic position from mipmapped textures and a tile
  atlas, under the other drapes and engine.ground.
- The fake engine keeps each layer's state.

The FIRMS far-zoom heat raster and Recent Imagery now draw through it, so
FIRMS' heat seam and both its backends, and Recent Imagery's Cesium and
Navara renderers and tile plan, go. Against HEAD, Cesium's pixels are
identical in all four compared scenes; Navara's orbital heat changes by a
mean of 3.8 from new mipmapping, slightly towards Cesium. The contract
suite passes on both engines (Cesium 68/68, Navara 71/71).

engine.maps.addOverlay stays a Navara-only extension for Sea Marks and
Ski Pistes until imagery has a finer tile path for oblique global views.
2026-11-07 08:07:57 +04:00
German Khokhlov 1559bd57f4 Draw Street Level, traffic and directions through one display each
- Street Level: coverage sequences are ground corridor sets of up to
  120, draped on terrain and tiles as Cesium's ground polylines were;
  the marker and view cones are sprite movers turned to their compass
  bearing on screen and culled 1 km under WGS84. The layer no longer
  loads the Cesium SDK on Navara.
- Traffic: vehicles are two point mover sets (an outline disc under each
  fill) scaled and faded with Cesium's NearFarScalar maths; congestion
  heat lines are ground corridor sets draped on tiles. Heights, depth,
  readiness and the surface key go through the engine, and Navara no
  longer loads the Cesium SDK for traffic.
- Directions: the route is one ground corridor set in the annotation
  route's dash pattern, maneuvers are point movers and the A/B markers a
  disc and a letter sprite; the in-layer Navara branch and the Cesium
  point collection go.

Known gaps against Cesium, to close in the contract: the traffic heat
lines lose their glow and pulse and the route its flowing dashes
(no animated ground fills yet), and dense Street Level coverage on
Navara is costly as ground corridors (about 80 to 190 ms a frame at
6,000 to 14,400 sequences) until corridors are binned set-wide.

Coupling: Navara renderer lines fall from 3349 to 2767, Cesium-importing
files from 51 to 32, `viewer` lines from 1591 to 1373, Cesium SDK loads
on Navara from 6 to 4.
2026-11-07 08:03:25 +04:00
German Khokhlov 2fbdca62be Name the installations, Global Context and infrastructure displays in the current state 2026-11-07 07:14:06 +04:00
German Khokhlov 30c86a6001 Draw Mapped Installations, Global Context and the infrastructure layers through one display each
- Mapped Installations: one display holds the single copy of the logic
  the Cesium and Navara renderers each had (renderable records, painting,
  the selection fill, floors, named markers, selection). Discs are point
  movers, outlines ground corridors and the selection fill a ground
  polygon. Cesium's behaviour wins where they drifted: named areas draw
  their outlines (Navara dropped them), discs sit under the moving
  contacts, and outlines and the fill pick their site.
- Global Context: the range ring is a halo and a core line on
  engine.lines and the awareness logic no longer imports Cesium. The
  animated pulse disc, which no primitive draws yet, sits behind one
  seam, render/pulseDisc.js, with a Cesium and a Navara backend.
- Datacenters and dams: one display on movers, lines and ground sets
  with Cesium's LOD, motion probe, floor sampling and card cohort;
  localGeojsonCore.js keeps only the engine-free card and record code
  (1381 to 329 lines).

Coupling: layer renderer lines fall from 1011/4832 to 351/3349
(Cesium/Navara), `viewer` lines from 1745 to 1591,
ScreenSpaceEventHandler files from 8 to 6, Navara ground passes from 5
to 4.
2026-11-07 07:13:56 +04:00
German Khokhlov e18f12d34c Keep street maps legible on Navara after dusk
Navara lights the map tiles with the real sun and sky, and only the
landscape maps took the night ambient fill, so an OSM street map turned
black at local night where Cesium keeps it lit. Every base map now takes
the night fill; the landscape maps keep their orbital fill as well.
Daylight is unchanged.

This is why the strict acceptance's Navara pixel and first-frame checks
over Austin failed in evening runs: navara-hd, parked-release,
hidden-switch, context-loss, geo-import and scope saw a black frame, and
the soak and failure matrix waited for a first frame that never came.
At night with the fix all nine pass.
2026-11-07 06:29:33 +04:00
German Khokhlov 54fe0aea40 Draw nine layers' fills through engine.ground, one display each
Earthquakes, air quality, GNSS Integrity, fire perimeters, Prohibited &
Restricted Airspace, Active Fires, S2 GhostMaps, cyclone advisories and
submarine cables each have one render/display.js on the globe contract;
their render/cesium* and render/navara* files go. Their discs, cells,
polygons and corridors are engine.ground sets, so on Navara they come from
the one ground screen pass with exact colours instead of nine hand-written
classification passes and lit SDK vector layers. Cesium's look stays the
reference:

- Earthquake and air-quality discs have no outline (Cesium drew none on
  clamped ellipses); earthquake discs can now be picked on Navara too.
- Perimeters, drone zones and GhostMaps polygons outline their outer
  ring only, as Cesium did.
- GNSS cells stay out of the pick pass; cyclone cones, tracks and centres
  are culled by a port of Cesium's horizon occluder.
- Submarine cables are one ground corridor set, as Cesium batched them;
  landing points, tips and stems are movers and lines.
- FIRMS glows are a sprite set and its cells a ground set. The far-zoom
  heat raster, which no primitive draws yet, sits behind one seam,
  render/heatField.js, with a Cesium and a Navara backend.

The shared Navara ground helpers that only these layers used go
(navaraGroundCells, the cell and ground geometry, the zone renderers), as
does the ground prototype script.

Coupling: layer renderer lines fall from 1636/6953 to 1011/4832
(Cesium/Navara), Navara ground passes from 7 to 5, Cesium-importing files
from 65 to 53, engine.extensions reads from 26 to 19.
2026-11-07 06:20:48 +04:00
German Khokhlov 974a0b6361 Add ground fills to the globe contract
engine.ground draws sets of polygons (with holes), discs, cells and
corridors draped on the surface, with fill and outline colours, order and
picks, written in bulk and committed. Every engine implements it:

- Cesium batches each set into one GroundPrimitive for fills and one
  GroundPolylinePrimitive for outlines and pixel corridors, classified
  against terrain and 3D tiles; sets that are not pickable stay out of the
  pick pass.
- Navara draws them in one screen pass after tone mapping that finds each
  pixel's ground from the scene depth and tests the items in its screen
  cell; long rings are split into edge bins. Measured at Dubai against
  SDK vector layers, the pass matched Cesium's frame time up to 10,000
  polygons with a few megabytes instead of 70 to 146, drew exact colours
  and outlines, and hid reliably across an engine switch, where vector
  layers hidden with update({ show: false }) never hid. Dense corridors
  remain its limit (10,000 cost 135 ms a frame).
- The fake engine keeps the same registry and CPU picks.

The contract suite checks it on both engines (Cesium 66/66, Navara 69/69).
Earthquakes and fire perimeters have unwired ground prototypes whose
drawing matches today's Cesium one (0.998 on Cesium, 0.976 on Navara).
docs/GLOBE-PRIMITIVES.md describes the primitives and when to use each.
2026-11-07 05:14:42 +04:00
Enzo Abyie 36c29d6ab4 changelog: french catalog review 2026-11-06 20:14:15 -05:00
Enzo Abyie b86c650428 i18n(fr): typography, terminology and wording fixes 2026-11-06 20:14:03 -05:00
German Khokhlov 6dc643182e Count OSM Places on the movers set on both engines in the open layers scenario
OSM Places draws through one display on every engine now, so on Cesium its
points are a movers set too, not a data source's entities.
2026-11-07 04:36:31 +04:00
German Khokhlov dc1144b947 Draw radio, transit and CCTV through one display each
- Radio: stations, clusters, the selection and its brackets are point
  and sprite sets; clusters come from the layer's engine-neutral screen
  clusterer with Cesium's EntityCluster rules, and clicks go through the
  selection dispatcher. One style module holds the sizes and colours.
- Transit: vehicles are two sprite sets from an up-front atlas, rotated
  with Cesium's projected-rotation maths. The selected vehicle's trail is
  engine.lines in two lanes and 29 age bands, so trails are no longer
  draped over terrain until the contract has ground corridors. The
  Cesium contact trail renderer and its re-export go.
- CCTV: markers, wedges, the plane outline and the calibration gizmo
  (rings, arrows, handles, drags through engine.input) draw on the
  contract. The textured monitor plane and the viewshed volume, which no
  primitive expresses yet, sit behind one seam, render/meshes.js, with a
  Cesium backend and a Navara backend the Navara mode passes in.

Known gaps against Cesium: transit trails are not draped, and the
surveillance-lens click correction runs only where the engine reports
its style stage (Navara).

Coupling: layer renderer lines fall from 2638/8735 to 1636/6953
(Cesium/Navara), ScreenSpaceEventHandler files from 15 to 10, `viewer`
lines from 2014 to 1901.
2026-11-07 04:29:50 +04:00
German Khokhlov fc75e245a6 Assert Wildlife Sightings on both engines in the strict acceptance
qa-wildlife-switch.mjs serves four Salish Sea sightings (three from
iNaturalist, one obscured, and an Acartia orca) through the app's own
iNaturalist and Acartia proxies over faked upstream feeds, then on Cesium,
Navara, Cesium and Navara again checks the badge set and its positions,
picks, the density glow, the privacy cell outline and a clicked card.
Two of the switches are made from the keyboard and must keep the
selection; the last is made with the pointer on the Globe Engine picker,
whose click closes the card like any click outside it.
2026-11-07 03:52:50 +04:00
German Khokhlov 145adc08e0 Keep the tracked aircraft through a failed engine switch on Cesium
Cesium follows a display's sprite through a proxy Entity that mirrors the
native's readout fields, and gevRestoring was not one of them. A follow
the flights layer resumed on attach therefore read as new navigation to
the shell's tracked-entity watcher, which made the share restore of the
same attach count itself superseded and clear the selection. That is why
the aircraft was lost after a failed switch's rollback onto Cesium, and
after the retry. The proxy now shows the restoring mark while its follow
starts.

The flights renderer test runs a detach, a failed attach to a candidate,
the rollback and a retry on headless Cesium engines, and checks that the
aircraft stays tracked and no navigation is stamped.
2026-11-07 03:44:38 +04:00
German Khokhlov f06b93fbc9 Draw vessels, satellites, Space Missions, ALPR, bikeshare and OSM Places through one display each
Each layer now has one render/display.js on the globe contract, the same
factory on both engines; its render/cesium* and render/navara* files go.
Cesium's behaviour is the reference, and the logic that was written twice
is written once:

- AIS vessels: atlas chevrons and engine.lines trails. The display
  registers the gev-trail pick claim itself and its trailing visibility
  pass now also runs on Navara.
- Satellites: point banks per look, orbit rings as lines turned by their
  model matrix, and selection and follow through engine.camera.follow.
  The emulated _viewer is gone, and the tracked readout republishes card
  text on Navara too.
- Space Missions: dots, reticle, trajectories and tactical orbits as
  movers and lines. The layer keeps its own records instead of Entities
  and DataSources, and camera work goes through engine.camera. Clicks go
  through the selection dispatcher, and hover keeps Cesium's throttle. The
  pad zone is its rim only: the contract has no surface fill yet.
- ALPR, bikeshare and OSM Places: one display and, for bikeshare, one
  style module each; clicks through engine.input, no
  ScreenSpaceEventHandler or EntityCluster. ALPR keeps Navara's horizon
  cull, and OSM Places points sample the surface where Cesium clamped them.

Coupling: layer renderer lines fall from 4546/11106 to 2638/8735
(Cesium/Navara), `viewer` lines from 2209 to 2014, ScreenSpaceEventHandler
files from 20 to 15.
2026-11-07 03:34:06 +04:00
German Khokhlov 3e261d1baf Give the contract suite a clear pointer path and real canvases in the app
In the app the world overlay draws on a canvas of its own above the
globe, so letting every canvas take the pointer sent the suite's clicks
there. Only the engine's canvas takes it now. Without makeCanvas the
pick check's sprite atlas was a plain object no real engine draws, so its
sprites could not be picked; the page now hands the suite real 2D
canvases. --only=prefix,... runs a subset of the checks. With both, Cesium
passes 63 of 63 checks and Navara 66 of 66.
2026-11-07 02:39:02 +04:00
German Khokhlov 35e70ec280 Draw the aircraft family, Wildlife Sightings and the Cockpit through the contract
The layers that still had one Cesium and one Navara implementation, or
drew on Cesium only, now draw through engine.movers, engine.lines and
engine.camera on both engines, and application code holds the engine
instead of guessing it from a viewer.

- Aircraft family: flights, military and local ADS-B share one display
  (src/layers/aircraft/render/display.js). The Cesium and Navara
  renderers are gone. Where they differed, the display follows Cesium:
  - attach and visibility tracking, live disableDepthTestDistance
    changes, the tracked glyph's distance scale, model ids that change,
    local ADS-B markers hidden until shown and its UAT ring as a point;
  - the trail head's hidden alpha is 0.45, the contract's
    depthFailAlpha scaled by the head's 0.9.
  The tracked glyph stays over the fleet, as on Navara. The movers
  contract reports readiness per model and reads model axes as the
  glTF's own on every engine. Cesium's follow takes any native target
  through a proxy entity in the ENU frame and mirrors its readout fields.
- Wildlife Sightings draws badges and the density glow as sprite sets
  and its privacy cells as line outlines (no fill yet). It takes clicks
  from engine.input and its card anchors to the engine's canvas and
  camera, so the layer is in both engines' catalogs again and a switch
  keeps the session's layer set.
- Cockpit: cockpitHost takes the engine only. The ridden aircraft is the
  engine follow's target, hidden through its sprite; only the Cesium
  Viewer's stock double-click is still routed around.
- About forty camera, navigation, share-link, HUD, voice, detection and
  layer-lifecycle callers take the engine. isGlobeEngine and
  engineFromViewer calls outside src/globe fall from 51 to 14.

The coupling baseline drops accordingly: layer renderer lines from
5239/12331 to 4546/11106 (Cesium/Navara), `viewer` lines from 2424 to
2209, Cesium-importing files from 69 to 66.
2026-11-07 02:34:34 +04:00
Sameh Khamis 6e4da23cf7 Merge pull request #1609 from bilawalsidhu/feat/traffic-lines-toggle
Street Traffic: Lines and Dots toggles; hosts choose which road modes they offer
2026-11-06 13:46:00 -08:00
German Khokhlov 48988015f1 Give each engine its own extensions in place of the navaraViews map
What only one engine has now travels with the engine instead of a
WeakMap in navaraMode.js: engine.extensions.navara holds Navara's view,
its SDK classes and the daylight reset, engine.extensions.cesium the
Cesium viewer. The contract documents the field as outside the contract,
and the coupling report counts its reads outside src/globe in place of
the navaraViews reads (27, unchanged).
2026-11-07 01:28:51 +04:00
German Khokhlov 2213be6566 Draw movers and lines on Cesium and require them of every engine
Layers are to draw through the globe contract on both engines, so Cesium
gets back the movers and lines removed as unused:

- engine.movers draws sprite sets as a BillboardCollection (atlas cells
  cropped once per set), point sets as a PointPrimitiveCollection and
  model sets as one Model per instance. depthTest, disableWithinM, order,
  scaleByDistance, colorBlend, unlit and minimumPixelSize follow the
  contract; a model's matrix is written in place, so instances no longer
  share one scratch matrix.
- engine.lines draws each line as a polyline Primitive with width in CSS
  px, geodesic arcs, dashes, the tactical and arrow materials,
  depthFailAlpha and modelMatrix; a rebuild keeps its show, matrix,
  colour and place in order.
- The picker returns the pickId a layer wrote last, skips sets that are
  not pickable and instances behind the horizon, and falls through to
  what lies under them. Surface samples can exclude mover sets again.

movers and lines move from the optional capabilities into the required
contract shape, and the contract suite always runs their checks.
2026-11-07 01:24:59 +04:00
German Khokhlov 59a233db51 Skip the contract checks the running app drives itself
The app sets contact sonar on every frame (src/cyberSonarEngine.js), so a
value the contract suite sets never survives to its next frame inside the
app. qa-contract-suite.mjs reports such checks as skipped, naming their
owner, instead of failing them.
2026-11-07 01:24:50 +04:00
German Khokhlov 9daa4bd2db Measure engine coupling and run the contract suite on the real engines
scripts/report-engine-coupling.mjs counts the engine-specific code left
outside src/globe/cesium and src/globe/navara: layer renderer lines per
engine, Cesium imports, `viewer` mentions, engine-name checks, raw Navara
reads, Navara ground passes, Cesium SDK loads and ScreenSpaceEventHandler
users. engineCoupling.test.mjs fails npm test when a count grows past
scripts/engine-coupling-baseline.json, so each step towards drawing
through the globe contract can only lower them.

scripts/qa-contract-suite.mjs runs src/globe/testing/contractSuite.mjs in
headless Chrome on the Cesium and Navara of a keyless dev server, which
until now only ran on the fake engine. The page imports the suite from the
dev server, starts at a fixed view, waits for the camera to rest and lets
the pointer through the panels. Cesium passes 57 of 57 checks, Navara 64
of 67; docs/CURRENT-STATE.md names the three that fail.
2026-11-07 01:08:15 +04:00
German Khokhlov 92c8f8d9da Merge origin/main into german/navara
Main's camera, voice, Cockpit and layer work now runs on the globe
contract where this branch draws through it:

- The voice tilt to an angle, its pivot about the place under the
  crosshair and the frame limit from orbit run through the camera rig;
  src/globeInView.js measures a camera pose with src/geo instead of a
  Cesium camera. The free-look height limit and the globe-in-view
  recovery stay Cesium-only for now.
- Cockpit takes main's own-layer entry rule. Double-clicking the
  followed track goes through cockpitHost.routeDoubleClick, so it works
  on both engines.
- Space Missions' failed-entry rollback and GhostMaps' age filter run
  in the engine-neutral layer code. On Cesium an expired report is
  dropped in place; on Navara its product is redrawn.
- Wildlife Sightings joins the catalog Cesium-only: Navara leaves it
  out, and the shell builds its card only for the real layer, so a
  Navara page no longer fails to start on its stand-in.
- One instruction sentence is shorter, which keeps the Realtime prompt
  within its 55,500-character budget with the Sentinel-2 basemap.
2026-11-07 01:07:26 +04:00
Denis Groshev f4376f2f4f Merge branch 'main' into denis/gev-news-layer 2026-11-06 21:02:10 +01:00
Sameh Khamis 574430fb1d docs(changelog): Street Traffic Lines and Dots toggles (#1609) 2026-11-06 11:44:17 -08:00
Sameh Khamis 9c8bb206ef feat(traffic): Lines and Dots toggles; hosts choose which road modes they offer
createTrafficSource({ roadModes }) lets a host offer a subset of the TomTom,
OSM and Hybrid road modes (all three by default). Without OSM the layer never
draws simulated roads, and a single mode shows no road chip.

While flow is live the Street Traffic row offers Lines (every road with flow
as a steady green/amber/red line, from the heat-line renderer) and Dots (the
moving dots), saved in layer state and share links. Defaults: Lines off,
Dots on.
2026-11-06 11:37:57 -08:00
German Khokhlov c06eb89190 Merge branch 'main' into german/transit-bikeshare-empty-state 2026-11-06 23:37:24 +04:00
Mazeyar Moeini Feizabadi c56d5f2f06 Merge pull request #1578 from bilawalsidhu/german/space-missions-failed-entry
Roll back a Space Missions entry whose mission index fails to load

Thanks @gekh. Maintainer review fixes (separate commits): exit cancels the layer's own camera flights again, a failed index load shows a specific toast, tests and docs.
2026-11-06 20:26:55 +01:00
Mazeyar Moeini FeizabadiandClaude Opus 5.5 8da8c05eaf test(launches): keep the fixture's toast recorder for the failure-message test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:10:34 +01:00
Mazeyar Moeini Feizabadi 5dd7f41436 Merge branch 'pr-1578-new' into review/pr-1578
# Conflicts:
#	changelog.d/1578-space-missions-failed-entry.md
#	docs/CURRENT-STATE.md
#	src/layers/launches/controls.js
2026-11-06 20:10:01 +01:00
manjunath22466 4384e0d402 test(voice): pin crop-capture throws and defensive skips; tighten reason glosses
A throw from the crop capture now has a test that it closes its decision with an error outcome. Direct calls to the crop and image senders pin the channel-closed and pointer-scale-not-useful skips. The docs say a send decision requests an image, and when a pointer snapshot counts as already requested.
2026-11-07 00:39:26 +05:30
Mazeyar Moeini FeizabadiandClaude Opus 5.5 efa4374644 fix(launches): hand the camera over before stopping, and only blame a fresh index error
- releaseCameraOwnership marks the camera released before stopping, so a
  flight navigation asked to preserve survives; exit also releases it, so an
  inactive layer never cancels another owner's flight.
- The failure toast names the mission index only when its error is from the
  current Context attempt (stats.errorAt), not an earlier one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:28:56 +01:00
Mazeyar Moeini FeizabadiandClaude Opus 5.5 13219828e3 fix(launches): cancel mission flights on exit and name a failed index load
Review fixes on top of #1578:
- Exiting Space Missions cancels the layer's own camera flights again (entry,
  roster preview, mission focus); only a flight navigation started after
  releaseCameraOwnership() runs on.
- A failed first index load says so: "Space Missions unavailable: the
  mission index could not load. Try again later." on both the Context and
  Data Layers routes, instead of a generic transition toast. Translated.
- Regression tests for both; docs and changelog describe the real feedback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:20:11 +01:00
German Khokhlov 4c62435910 Drop the duplicate callout test and the card id cache 2026-11-06 22:01:39 +04:00
German Khokhlov ea76fc4bc0 Fold duplicate coverage tests into their neighbours 2026-11-06 22:01:39 +04:00
German Khokhlov 096b891099 Simplify failed-entry camera handling and tests 2026-11-06 22:01:39 +04:00
Mazeyar Moeini Feizabadi 3cd7ed68aa Merge remote-tracking branch 'origin/main' into review/pr-1578 2026-11-06 18:55:07 +01:00
Mazeyar Moeini Feizabadi 4effadd3e9 Merge pull request #1601 from bilawalsidhu/fix/street-level-keyless-default
fix(street-level): without a Mapillary key, start on a keyless source

Thanks @asturksever. Maintainer review fixes (separate commits): filter-only updates keep an explicitly chosen source, keyless Street Level QA gate updated, tests and docs.
2026-11-06 18:45:17 +01:00
manjunath22466 df37c9360d test(voice): pin diagnostics wiring and in-flight crop dedupe; tighten docs
Third review pass on #835. No runtime change.

- Controller test: viewport decisions reach the controller's debug log.
- Policy test: a second request for a snapshot still being captured is
  logged pointer-already-requested and is not captured twice.
- CURRENT-STATE: pointer-not-fresh also covers a missing snapshot,
  already-requested covers an earlier attempt that threw, and the image
  step runs when any tool output of the response was sent.
- Changelog: describe what is recorded rather than a fixed miscount.
2026-11-06 23:12:20 +05:30
Mazeyar Moeini Feizabadi ddc14c8f51 Merge remote-tracking branch 'origin/main' into review/pr-1601 2026-11-06 18:32:31 +01:00
Mazeyar Moeini FeizabadiandClaude Opus 5.5 f7b9a2e5af test(street-level): keyless QA branch handles a keyed bundle behind a keyless server
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:20:47 +01:00
manjunath22466 d0a612786a fix(voice): log a real send before bookkeeping; document decision reasons
Second review pass on #835's diagnostics. Behaviour is unchanged.

- Report viewport_context.result as soon as the send returns, so a throw
  in the retained-frame bookkeeping cannot log a real send as "error".
- List the decision reasons and when each field is present in
  CURRENT-STATE, note that the rate limit can split a pair, and say which
  codes are defensive.
- Changelog: the decision is logged when an entity-context answer reaches
  the image step, not for every answer.
2026-11-06 22:47:09 +05:30
Mazeyar Moeini FeizabadiandClaude Opus 5.5 8a7402d80c fix(street-level): keep explicit provider choices on filter-only updates
Review fixes on top of #1601:
- Only redirect a provider the params switch on to a keyless source, so a
  filter-only setParams (360°, SINCE) no longer moves an explicitly chosen
  keyless Mapillary to KartaView.
- Keyless QA gate: assert the new KartaView default and that the Mapillary
  chip explains its key instead of switching (it used to expect KEY REQUIRED).
- Tests: Graph-proxy keyReady, filter-only updates; shared test helper.
- README and CURRENT-STATE describe the keyless default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:14:16 +01:00
manjunath22466 207eb9805d Merge main into viewport diagnostics PR (#835)
Brings the branch up to main 72e2eba7. No conflicts; the net change against main is unchanged.
2026-11-06 22:38:20 +05:30
manjunath22466 27823d7d05 fix(voice): make viewport escalation diagnostics truthful and paired
Review follow-up for #835. What is captured, sent and kept is unchanged.

- Log nothing for tool results other than get_entity_context, so other
  tools stop spending the shared debug-log budget.
- Give every decision and outcome the same contextId and image kind, so
  overlapping requests pair up.
- Log why a crop decision did not send (stale, off ground, off screen,
  expired, pose changed, already requested) instead of leaving a lone
  "pointer-ground-crop" that reads as sent.
- Split capture-invalidated into session-reset, superseded, channel-lost
  and frame-stale.
- Close the pair with "error" when a step throws, then rethrow.
- Document the events in CURRENT-STATE and reword the changelog fragment.
2026-11-06 22:37:41 +05:30
German Khokhlov 72e2eba70d Merge pull request #1581 from bilawalsidhu/german/i18n-new-strings-fixes
Fix the translations of the voice geometry, fire filter, airspace and sea mark strings
2026-11-06 21:03:09 +04:00
German Khokhlov 41cc57d630 Merge branch 'main' into german/i18n-new-strings-fixes 2026-11-06 20:55:36 +04:00
Mazeyar Moeini Feizabadi 61dfdc771e Merge remote-tracking branch 'origin/main' into review/pr-1601 2026-11-06 17:54:23 +01:00
Mazeyar Moeini Feizabadi b35c810284 Merge pull request #1528 from dylanmaniatakes/codex/s2-ghostmaps-age-filter
feat: add adjustable age filtering across S2 GhostMaps

Thanks @dylanmaniatakes. Maintainer review fixes (separate commits): age-window narrowing/restore consistency, card kept across same-report reloads, date-only events cover their whole UTC day, and the age choice as a labelled Event age switch.
2026-11-06 17:52:28 +01:00
manjunath22466 15bda43a1e Merge main into viewport diagnostics PR (#835)
Brings the branch up to main 39d47a99. No conflicts; the net change against main is the same five files.
2026-11-06 22:20:08 +05:30
Mazeyar Moeini Feizabadi d5d824f705 Merge remote-tracking branch 'origin/main' into review/pr-1528 2026-11-06 17:40:33 +01:00
Mazeyar Moeini FeizabadiandClaude Opus 5.5 91ab9b5811 fix(ghostmaps): follow-up review fixes for the age switch
- Keep the open card across a widened reload only when its index still
  holds the same report; the server may answer a pinned version with newer
  bytes.
- Narrowing to a window the loaded data already covers clears a failed
  wider reload, so the row no longer stays in error.
- Label the every-age option "All" (contextual) so the six-option switch
  fits the row in every locale; tooltip and info line keep "All markers".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:37:29 +01:00
German Khokhlov d544613aae Merge pull request #847 from kvnloo/exp/cyclone-shared-context
feat(cyclones): publish explicit storm selection to shared context
2026-11-06 20:35:22 +04:00
Mazeyar Moeini FeizabadiandClaude Opus 5.5 c4298c4450 fix(ghostmaps): keep age-window changes consistent and make the age choice a switch
Review fixes on top of #1528:
- Narrowing the age window only removes markers; a product loaded with a
  narrower window is reloaded when the window widens again. Fixes markers
  missing after 7d -> 1d -> 7d and avoids reparsing every KMZ on narrowing.
- A wider window over the same edition keeps the open report card.
- Date-only event dates cover their whole UTC day, so 1d still shows
  yesterday's reports.
- The age options are one labelled "Event age" radio switch (row
  `segmented` control, as FIRMS confidence) instead of toggle chips mixed
  with the product chips.
- Un-nest the per-product age test; QA compares data-source counts with the
  live defaults instead of a hard-coded 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:25:01 +01:00
German Khokhlov c9f40a837c Tighten callout fade comments and docs 2026-11-06 20:20:11 +04:00
German Khokhlov 5c4918affb Tighten coverage prompt comments and docs 2026-11-06 20:20:11 +04:00
German Khokhlov 8917eebe12 Tighten failed-entry comments and docs 2026-11-06 20:20:11 +04:00
German Khokhlov 5bab5ec26d refactor(cyclones): drop defensive shared-context state and checks 2026-11-06 20:15:27 +04:00
German Khokhlov 5e64e599e1 docs(cyclones): tighten shared-context comments, changelog and notes 2026-11-06 20:07:56 +04:00
Mazeyar Moeini Feizabadi 7673351ce0 Merge remote-tracking branch 'origin/main' into review/pr-1528
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-06 17:02:29 +01:00
German Khokhlov 3a78f40024 Merge main into german/transit-bikeshare-empty-state
Conflict in src/layers/transit/queries.js: keep the branch's shared note with the gate's enter altitude and main's distinct region count.
2026-11-06 20:00:58 +04:00
German Khokhlov 910e63a56a Merge main into german/vessel-callout-under-card
Resolve addPaintItem by keeping both the collision reset from main and the
queued-card frame stamp from this branch.
2026-11-06 20:00:45 +04:00
German Khokhlov e1ea524b68 Merge main into german/space-missions-failed-entry 2026-11-06 20:00:21 +04:00
German Khokhlov ea22d16c1a Merge branch 'main' into exp/cyclone-shared-context 2026-11-06 19:44:54 +04:00
German Khokhlov 4e518821b5 fix(cyclones): name the classification and mark a stale cache in shared context
- Publish classificationName next to the raw NHC code; unknown codes stay
  unexpanded.
- Publish staleCacheFetchedAt only while the proxy serves its cache.
- Touch the shared store on clear only after this layer published, so a
  feed failure no longer evicts another layer's dangling selection.
- Drop the unreachable no-storm branch and the unused return value.
- Tests: split the heading fixture into due north and stationary, cover
  the unavailable and failure paths with the real store, and check that
  clear/disable leave another layer's selection alone; fold the fake-store
  test into the real-store one.
2026-11-06 19:43:04 +04:00
Said Turksever 3d6423c6f8 Merge pull request #870 from waar19/perf/fleet-tick-skip-hidden
perf(flights): skip dead-reckoning contacts that stay behind the horizon
2026-11-06 15:03:33 +00:00
Said Turksever 15fdf65a73 Merge branch 'main' into perf/fleet-tick-skip-hidden 2026-11-06 14:52:56 +00:00
German Khokhlov 90166024e9 Keep the cockpit camera steady near the ground on both engines
Cesium lifts a camera below its globe every frame, between the cockpit's 20 Hz updates, so an eye placed under the terrain (an aircraft reading below it on a cold floor cell) jumped up and down. The cockpit now also keeps its eye above the engine's own surface. Navara's ground floor stands aside while the camera inputs are off, since the cockpit places the camera itself.
2026-11-06 16:52:35 +04:00
German Khokhlov aca5317a2a Merge branch 'main' into exp/cyclone-shared-context 2026-11-06 16:03:13 +04:00
Said Turksever 8b1a573ef3 docs(transit): changelog fragment and 72-variant icon cache in CURRENT-STATE 2026-11-06 12:02:31 +00:00
Said Turksever 141181f83a docs(flights): changelog names the one-in-13-ticks rotation, about once a second 2026-11-06 12:02:30 +00:00
Said Turksever dda5ac6c00 Changelog: 1601 fragment 2026-11-06 11:59:31 +00:00
Denis Groshev 876e9b2e2f test: count News beside Wildlife Sightings
The catalog holds 39 layers, News ends the Events group just before
Wildlife, and the README names twenty-three layers.
2026-11-06 12:59:10 +01:00
Said Turksever ecbcc273dd Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-06 11:54:30 +00:00
Said Turksever 0dbbd9047e Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-06 11:54:29 +00:00
Said Turksever 579acf5081 Street Level: without a Mapillary key, light a keyless source instead of opening on KEY REQUIRED 2026-11-06 11:51:34 +00:00
Denis Groshev 2e87dd87eb Merge branch 'main' into denis/gev-news-layer
Wildlife Sightings (#1591) published share token 9, so News takes the
next free token, 00. The token tests read the ledger before both where
they were written against it, and the README counts both layers.
2026-11-06 12:33:13 +01:00
Denis Groshev 61424e4256 fix(news): keep a refused key refused during a slow read, and read Local when Conflicts fails
A slow re-read of a refused key read as unavailable until it landed; it
now stays refused. The first-read wait is 10 s, for a margin under the
browser's 15 s. A failed Conflicts read no longer stops Local's refresh,
which the server reads on its own. Tests cover GDELT's wait, the refused
key and a caller abort past the timeout, and fail rather than hang; the
docs say what a section serves meanwhile.
2026-11-06 12:02:58 +01:00
German Khokhlov 697b6b59e6 Repaint coverage rows after loads, nudges and cleared failures 2026-11-06 14:47:47 +04:00
German Khokhlov 1015b16664 Prune queued-card rows whose pooled item paints another entry
A pooled paint item reused by another card keeps a fresh frame stamp, so
rows for cards that stopped painting survived every prune and the index
grew for the whole session. The prune now also drops a row whose item
paints a different entry id or source, and a test pins the bound.
2026-11-06 14:45:37 +04:00
German Khokhlov 0de347c328 Leave other camera flights running when Space Missions stops without a replay 2026-11-06 14:45:36 +04:00
Denis Groshev c6ae4c44d7 fix(news): answer within the browser's wait on a first read
A country's first World News API read, or Liveuamap's, could take up to
20 s while the browser waits 15, so the panel showed the browser's own
"signal timed out". A read with nothing to serve yet is now waited for
at most 12 s, as GDELT's already was: past that the section answers
unavailable while the read goes on, and the next refresh is served from
it. A client timeout reads "News feed unavailable".
2026-11-06 11:41:37 +01:00
German Khokhlov 9825b02aab Add a Navara HD renderer choice, release a parked engine after three minutes, and cut Navara's memory
Navara now draws at CSS pixels without MSAA, like Cesium; Navara HD keeps the device pixel ratio and MSAA 4. Only one Navara variant is ever kept. A parked engine is released after three minutes and its WebGL context is lost. Three leaks that kept a released Navara alive are fixed, the canvas context drops its unused antialias buffer, cloud shadows use two 256 px cascades, and the tile cache is budgeted from the screen buffers.
2026-11-06 14:41:16 +04:00
Denis Groshev 525543d08a test(news): take Jordan as the sample country 2026-11-06 10:18:26 +01:00
Itallo BrandãoandClaude Opus 5.5 096b3c85bf Merge main into feat/shipwrecks; move shipwrecks to token 00
Main's wildlife-sightings layer took share-link token 9, so shipwrecks
takes the next free token, 00. Locale, docs, format-scope and voice
manifest conflicts keep both sides. Ledger and registry tests now model
the ledger before shipwrecks took 00, and layer counts include both
new layers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:52:49 -03:00
Itallo BrandãoandClaude Sonnet 5.5 433b336c2d feat(transit): add Szczecin buses and trams (ZDiTM, CC0)
Keyless GTFS-Realtime feed published by ZDiTM Szczecin under CC0 1.0.
Speeds are m/s (checked against vehicle movement), the file refreshes about
every 20 s, and the feed carries no route ids, so vehicles take the default
bus mode.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 02:50:42 -03:00
Denis Groshev 2fe0110f8d docs(changelog): add the #1593 fragment 2026-11-06 01:05:22 +01:00
Denis Groshev 427463cc45 fix(news): list Local newest first
Local ranked its reports by severity first. A country's own news now
carries no severity, so in a conflict area GDELT's reports (severity 1
and up) could crowd every one of its stories out of the 40 listed.
Local is now newest first; severity still shows by colour.

Also: readTitle reads Conflicts headlines only, the economy category's
comment and the attribution popover name World News API's category,
and KNOWN-ISSUES names Core's ids for World News API's categories.
2026-11-06 00:45:33 +01:00
Denis Groshev 82057cbb51 fix(news): keep World News API's own category for a country's news
The bundled rules read fighting and crises; in everyday headlines their
words mislead ("Kazakhstan Advances in Global Digital Rankings" read as
ground fighting). A country's own stories in Local now keep World News
API's category (politics, business, else other) and carry no severity.
Fighting in the area still shows through the Conflicts reports placed
there.
2026-11-06 00:45:33 +01:00
Denis Groshev 1e78ac931c Blame a GDELT export only while GDELT answers, and fix the review's notes
An export is given up on (never published, unreadable, or failing three
times) only while GDELT itself answers: its list of exports read, or
another export of the same round read. An outage that answers 404, or junk,
for everything now gives up on nothing, and a failing file earns one strike
a round rather than three within the first seconds' rounds.

Docs: News's share token is 9; territories without a country code are named
as themselves too; the changelog fragment credits one pull request. Unused
exports dropped from the rules and stale scaffolding from the ledger tests.
2026-11-06 00:45:33 +01:00
Denis Groshev 66ed0386a6 Put the News layer and Cockpit news entries in one changelog fragment
One pull request carries both, and a pull request number names one fragment.
2026-11-06 00:45:32 +01:00
Denis Groshev 71adf0d8cd Let a host turn Core's own news reading off
A host that serves news only from its own news server mounts the proxy with
`builtIn: false`: without GEV_NEWS_ORIGIN the routes then answer "not
configured", as before Core read any news itself.
2026-11-06 00:45:32 +01:00
Denis Groshev a5f9870179 Document the News layer's own sources, keys and limits
DATA_SOURCES, the README's layer and key tables, CURRENT-STATE and the
changelog describe the News layer as it now is: a configured news server,
or Core's own reading of GDELT (no key), Liveuamap (optional) and World
News API (Local, free plan backlink credited). .env.example gives the
download sizes and the daily read budget; KNOWN-ISSUES lists what machine
coding gets wrong and which disputed areas are named as themselves.
2026-11-06 00:45:32 +01:00
Denis Groshev 34c75ff5de Serve the News layer without a news server: GDELT, Liveuamap and World News API
With GEV_NEWS_ORIGIN unset, Core answers the News layer's routes itself.

Conflicts need no key: the last 24 h of GDELT 2.0 event exports, physical
violence only (CAMEO 18-20), one report per story (copies of one agency
story merged), placed where GDELT put it, a town the article's own words
name winning over a bare country, and titled by the article's page title
from the GKG export of the same quarter hour (the newest 3 h), else by its
address words. GDELT's actor coding is never shown. Exports are read up to
the newest GDELT lists; one that cannot be read is given up on without
holding the rest back, and an outage of two hours answers 502 rather than
an empty feed. With LIVEUAMAP_API_KEY, Liveuamap's mapped items join them.

Local reads World News API by the source country at the view centre
(WORLDNEWSAPI_KEY, at most WORLDNEWSAPI_DAILY_READS a day), with the
Conflicts reports placed inside the area. Disputed areas (11 from Natural
Earth, bundled) and territories without a country code are named as
themselves and read from no country's outlets; a country's outline leaves
them out.

Categories, relevance and severity come from shared rules; keyed answers
are cached and served while they refresh, and no key or address is logged.
2026-11-06 00:45:31 +01:00
Denis Groshev edb5837ae5 Name the Local area, say what each News section needs, and credit its sources
Local reads the area at the view centre (`/outline?at=`) and asks the feed
for its reports (`at`): the area is outlined faintly on the globe and named
at the top of the section ("News from Kazakhstan"; within 500 km at sea).
A disputed area is named as itself. A read that a newer look replaced, or
one left behind by a switch to Conflicts, is dropped, and a server without
the area route is asked once.

A section read from a key the server lacks says which key it needs
(503 `no_key`), that the key was refused (`key_rejected`) or that its
daily allowance is spent (`key_quota`); the other section keeps reading.
Liveuamap and World News API join the key registry.

Cards name the service a report came through ("via GDELT Project"), and
Liveuamap and Natural Earth are credited once their data shows. Economy is
a category of its own. Disputed-area labels are smaller and show the name
only; outlines keep up to 300 parts.
2026-11-06 00:45:31 +01:00
Denis Groshev fa736a7cd5 Count the shown News section in the panel header
With Local chosen, the header counted the Conflicts feed's cards instead of
the Local list's.
2026-11-06 00:45:30 +01:00
Denis Groshev 74d40c4b1a Show one-source events' pictures and play their clips in the News panel
A feed card shows its event's picture whatever the number of sources, so a
lone Telegram post opening with a video gets its preview; the globe's
picture cards still need two. An event picture may carry its post's clip (an
HTTPS MP4 or WebM file): the card marks it with a play sign, and opened, it
plays with controls and sound, loading nothing before. An expired link drops
the clip.
2026-11-06 00:45:30 +01:00
Denis Groshev f72f79f48b Split the News panel into Conflicts and Local
Conflicts is the hotspots' significant news, on the map whenever the layer is
on. Local lists every report within 500 km of the view centre below 3,000 km,
and the map shows those instead while it is chosen. The section replaces the
"Include minor news" and "Show event cards" toggles: picture cards are always
on, and Local is what reads minor news. The Conflicts feed reads the same
whichever section is shown.
2026-11-06 00:45:30 +01:00
Denis Groshev 4817d2a648 Let the News panel narrow the window to 12, 6, 3 or 1 hours
The panel's window label becomes a choice of the server's window and each
shorter one. The feed and map filter to it locally; the hotspot counts are
read again for it (/api/news/hotspots?hours=N, now on the allowlist). A
server names the longest window it serves as maxWindowHours.
2026-11-06 00:45:28 +01:00
Denis Groshev ee3593cfbd Ask for detailed news outlines and keep checked severities
Outlines move to geometry version 4: the news server now follows Natural
Earth's own border vertices instead of a coarse grid, and the new version is a
new cache entry. A severity a reviewer set (severityBy 'label') is kept as such
instead of reading as the rules'.
2026-11-06 00:45:27 +01:00
Denis Groshev 60fad38c3f Show news from the window the server serves
A host may serve fewer hours than the day. The hotspots answer may name its window
(`windowHours`, a whole number from 1 to 24); the layer keeps reports, the
feed, In view and the chosen event inside it, and the panel says
"Last 12 h" or "Last 24 h" accordingly. Without a window named, or with one
out of range, the layer keeps the full day as before.
2026-11-06 00:45:27 +01:00
Denis Groshev f3e206bf37 Draw news in GEV's own colours
The news layer brought its own ColorBrewer yellow, orange and red, solid
coloured marker discs, a coloured edge on every card, an amber developing
ring and pink disputed areas, which read as foreign next to GEV's cyan glass.

Severity now uses colours GEV already has (dim and primary text, the military
layers' amber, the Cockpit's emergency red), drawn as GEV's dark disc with a
severity rim and icon; attestation is still the rim's form. Cards lose the
coloured edge and name significant and major events in a mono badge;
attestation chips follow the status badge; outlines stay in the accent family
with disputed areas dotted in the primary text colour.
2026-11-06 00:45:25 +01:00
Denis Groshev e95ae7b80c Keep the news layer inside its day and its memory bounds
From review of the news layer:
- An event's other sides older than 24 hours (or undated) are no longer
  listed, and a report dated more than 15 minutes ahead is outside the
  window, as in the regional brief.
- Picture cards are one canvas per event in a bounded cache (24), repainted
  in place when the age changes, with each card's picture dropped with it;
  the age in the key had made a new canvas every minute.
- The feed list drops reports that leave the window and is capped at 600.
- "In view" is filtered from the reports the layer already holds; it reads
  the feed only with minor news on, once per refresh, never per pan.
- Infinite scroll restarts after a filter change or a failed read.
- The two filter selects stack, so "Significant and above" is not cut off.
- The timeout tests hold the event loop until aborted, so they pass on
  Node 22 as well.
2026-11-06 00:45:25 +01:00
Denis Groshev 138cc77a11 Export the news proxy so a host's local server can mount it
`./server/providers/news` (node) with its own package boundary, so a host composing its own local providers can serve /api/news from GEV_NEWS_ORIGIN as Core's standalone server does.
2026-11-06 00:45:25 +01:00
Denis Groshev 5bb489d1fb Put news on the globe, with a feed panel in the right rail
Events ▸ News draws the last 24 hours of reports from curated hotspots
(Ukraine–Russia, Israel–Gaza–Lebanon, Iran–Hormuz, Yemen–Red Sea, Sudan,
Sahel) and developing situations elsewhere, from any server that answers the
`/api/news` read routes.

- Globe: one marker per merged event, clamped to the ground and culled at the
  horizon; one beacon per hotspot when zoomed out and a pulsing beacon for a
  developing situation; hotspot outlines with disputed areas dotted and
  labelled by who administers and who claims them; picture cards for at most
  ten major or fast-growing events with two or more sources, never
  overlapping, switchable off.
- Panel: hotspot chips, a severity filter (significant and above by default),
  categories, minor news off by default, and a scrolling list of event cards
  with picture, headline, place, sources, age and the sides that reported it.
  "In view" lists what is near the map below 3,000 km once the camera
  settles. A card flies the globe to its event and opens it with a link to
  the publisher. Reduced motion stops the pulse and the flights.
- Core serves no news itself: the local server proxies GEV_NEWS_ORIGIN when it
  is set, and answers 503 unavailable otherwise, so the layer reads "not
  configured" once instead of retrying.
- Share token 9 (enabled-only); voice stays off for this layer.
2026-11-06 00:45:24 +01:00
Denis Groshev 2d9d518946 Add the news report model, markers and source
The pure half of a news layer: validated records for a news server's
`/api/news/hotspots|feed|outline` contract, a presentation model and canvas
markers and picture cards, and a fetch client.

- Colour carries severity only (0 no physical event, 1 minor, 2 significant,
  3 major), never a side; the category is the icon, the rim is attestation
  (confirmed, a claim, one side's own press), size grows with sources and
  markers fade with age.
- Cards say "Reported by {side} sources" and mark official channels; a
  machine-read English headline is labelled as such.
- `createNewsSource` reads same-origin routes with a 15 s timeout, a 4 MiB cap
  and record-by-record validation; `NEWS_API_PATHS` and `newsQueryAllowed`
  let a host proxy exactly what the client sends. A 503 marked unavailable
  means "not configured": no retries.
- Exported as `./layers/news/source`, with its own package boundary.
2026-11-06 00:45:22 +01:00
Denis Groshev dd83812a21 Hold Cockpit regional news to the last 24 hours, and tell its states apart
Regional News showed "REGIONAL NEWS UNAVAILABLE" above populated rows and a
static "RECENT" footer over months-old articles. The status banner kept
`display: grid` when hidden, every non-empty answer carried the unavailable
text, and nothing limited or sorted articles by age.

- One window (regionalNewsWindow.js, 24 h): the local proxy asks Google News
  with `when:1d` and GDELT for 24 h, drops undated, older or future-dated
  articles, sorts newest first, and reports newsWindowHours/newsFetchedAt.
- The Cockpit labels four states: fresh (banner hidden, "LAST 24 H"),
  kept after a failed refresh ("CACHED · REFRESH FAILED"), no news in the
  window, and unavailable. The footer lives in the news state, so it
  survives page and tab changes, and the client drops out-of-window rows.
- `.cockpit-brief-status[hidden]` now hides the banner.
2026-11-06 00:45:19 +01:00
German Khokhlov 7f8556c691 Merge branch 'main' into exp/cyclone-shared-context
# Conflicts:
#	src/app/constructCatalog.js
2026-11-05 23:01:23 +04:00
quanticsoul4772 f717f8a0d7 Merge branch 'main' into upstream/nominatim-cache-mode-windows 2026-11-05 10:23:39 -08:00
quanticsoul4772 d0bf21b8c3 Merge branch 'main' into upstream/tools-heading-wrap 2026-11-05 10:23:22 -08:00
German Khokhlov 90db4eb2c9 Name the real gate altitude and repaint coverage rows at once
The Transit prompt said "Fly below 3,000 km", but the closed gate opens at
2,850 km; it now names the enter threshold, as Bike Share does. When the
camera changes coverage, Transit and Bike Share ask the Layers panel to
repaint instead of leaving the old prompt up until the next 15 s / 60 s tick.
2026-11-05 21:21:03 +04:00
German Khokhlov da00b096d2 Fade a vessel's callout by its card's painted opacity
The callout now asks for the opacity its own card paints with this frame,
keyhole fade included, and fades out as the card fades in. A card faded out
at the keyhole rim leaves the callout readable, and a card fading out after
leaving its source still hides it. The vessel card id is cached on the
record, and the allocation gate covers the lookup.
2026-11-05 21:21:02 +04:00
German Khokhlov 655d93cc92 Keep a newer camera view when a failed Space Missions entry rolls back 2026-11-05 21:21:02 +04:00
German Khokhlov a6c64713ee Stop Navara's memory growth on engine switches, hide polygon zones reliably, and drop the Cesium camera rig
- Navara 0.2.1 never frees pick batch ids, and the mover banks rebuilt
  their instanced meshes on each attach, leaking about 260 KiB of
  WebAssembly memory per switch. A per-view pool hands released ids back
  out; the long soak now asserts native growth of at most 32 KiB per switch.
- Navara 0.2.1 sometimes drops the first show:false update on a new
  vector layer, so a zone turned off right after a switch back stayed
  drawn. Polygon displays hide by deleting their layers and rebuild them
  when shown.
- Camera verbs drive both engines through the globe contract; the Cesium
  viewer rig and the tests of states it can no longer reach are gone.
- Flights and military tracking share src/layers/aircraft/tracking.js.
- The geo import scenario reads movers only where the engine has them.
- Add the Navara section to docs/CURRENT-STATE.md and a changelog fragment.
2026-11-05 21:18:23 +04:00
Itallo BrandãoandClaude Opus 5.5 c73e4edce7 docs(changelog): add #1583 fragment
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:44:17 -03:00
quanticsoul4772andClaude Opus 5.5 df87419e64 test(tools): pin the raw near-360 satellite azimuths and list the tools with the heading helper
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 05:43:42 -08:00
quanticsoul4772andClaude Opus 5.5 e5ab5f945a test(nominatim): share one POSIX guard and say the cache is private only on POSIX
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 05:43:36 -08:00
German Khokhlov ed0ad9cf0f Add the changelog entry for #1581 2026-11-05 14:32:39 +04:00
German Khokhlov 7abf74518f Cut duplicated and dead code across the engines, the layers and the QA scripts
- src/geo drops the ported debug argument checks and the tests that
  bitExact already covers; package boundaries accept a directory entry,
  so src/geo/ is listed once per group.
- Cesium no longer carries movers and lines nobody draws with: they are
  optional engine capabilities. The fake engine and the Cesium camera use
  the shared cameraMath; unused contract tables, clock.ticked, the
  'finest' surface detail and Navara click statistics are gone.
- ALPR keeps one overlay core and one presentation model for both
  engines, bikeshare one station bookkeeping, the Navara ground effects
  one definition helper; image atlases, view centres, mover banks and
  colours are shared.
- The legacy switch route, the clock offset and rate controls and other
  unused exports are removed.
- The acceptance runs the Navara draw, geo import and scope checks;
  scenarios use the harness switch and page helpers; the main chunk
  check keeps the size budget only and runs once in CI.
2026-11-05 13:32:39 +04:00
German Khokhlov ee3466dff9 Keep the queued-card check off the per-frame allocation path
Writing a fractional alpha onto the overlay record for every paint item
boxed a heap number per item per frame. The allocation gate caught it:
the generic 60-entry workload went from 2,838 to 12,614 B/frame. The
record now keeps only the frame stamp and a pointer to its paint item,
and isOverlayEntryQueued multiplies the alphas when it is asked. The
gate is back to main's numbers on Node 24.
2026-11-05 13:14:00 +04:00
German Khokhlov c64ed40c62 Add the changelog entry for #1578 2026-11-05 12:54:36 +04:00
German Khokhlov 092e7e71a8 Add the changelog entry for #1577 2026-11-05 12:54:11 +04:00
German Khokhlov 652f3c8ea1 Hide a vessel's detection callout while its own card paints
With Vessels and Detection on, a vessel with an ambient card on the map
also got its detection callout next to the bracket. The callout lane
paints beneath the cards, so its text showed through the card's plate
and covered the speed and course.

The callout now gives way only to a card the shared overlay actually
paints in the same frame. The overlay paints part of the published
cards (shared budget, collisions, UI chrome), so a vessel whose card
was dropped keeps its callout.

The detection lane paints before the cards publish their paint rects,
so getOverlayPaintRect has nothing for the current frame there. The new
isOverlayEntryQueued answers from the paint queue that the overlay
solves before painting. Vessel detection objects name their card in
cardSourceId/cardEntryId.
2026-11-05 12:53:17 +04:00
German Khokhlov 25c33893f6 Add the changelog entry for #1576 2026-11-05 12:53:09 +04:00
German Khokhlov b6ad32009f Say why Transit and Bike Share rows are empty outside coverage
Over a city with no feed or system (London, Tokyo) both rows read
"ON · — · never", the same as a feed that never answered. Transit already
computed a coverage note but put it only in `coverage`, which the row does
not show; Bike Share reported nothing at all.

Both layers now report a `zoom-in` guidance status with a `statusMessage`:
"Fly below 3,000 km to a covered region" / "No feed here yet · N regions
available" for Transit, and "Fly below 48 km to a covered city" / "No bike
share here yet · N systems available" for Bike Share. A loading or failed
Bike Share city keeps its own text. Leaving coverage clears the layer's
last-update time, so the row no longer dates data it no longer draws.

The coverage-note translations move from the voice card into
`translateStatusMessage` (ui/statusText.js), which the Layers row and the
voice card now share.
2026-11-05 12:52:28 +04:00
German Khokhlov 5c04b12164 Roll back a Space Missions entry whose mission index fails to load
The launches layer swallowed every fetch error in its update, so the
manager treated a 429 or timeout on /api/launches as a successful start.
Context then committed the isolating mode with no missions: the other
layers stayed off, unrelated enables were refused, the camera stayed at
the full-globe view and the roster kept reading "loading".

Until the first index loads, the update now reports the failure. The
manager fails the enable closed and both entry routes roll back to the
pre-entry layers. The camera pose captured at entry is restored when the
mode is left before any index loaded, and the roster shows a failed
state instead of loading. A valid empty index still enters, and a
refresh failure after a loaded index keeps its records.
2026-11-05 12:43:49 +04:00
German Khokhlov 33a2876bc0 Fix the translations of the voice geometry, fire filter, airspace and sea mark strings 2026-11-05 12:36:13 +04:00
German Khokhlov c502404e1d Show installation and ALPR camera labels on Navara the way Cesium does
An installation card on Navara now names an unnamed feature and its
class in the UI language, and an ALPR camera keeps its context label as
English data that is translated when shown.
2026-11-05 12:29:45 +04:00
German Khokhlov 350a7f5abb Stop pointing comments and test headers at notes and QA scripts the repository does not carry 2026-11-05 12:08:35 +04:00
Itallo BrandãoandClaude Opus 5.5 1199fe05c7 fix(shipwrecks): give the era legend its labels and counts
The panel reads legend entries as label and count; the layer sent text, so each entry showed 'undefined'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 04:55:16 -03:00
Itallo BrandãoandClaude Opus 5.5 112ffd4386 feat(shipwrecks): period picker like air accidents, sharper icons
- Replace the four era chips with the start and end year pickers the
  air-accidents layer uses (the same RowRange control, ported as is);
  the era colours stay as a legend under the row.
- Rasterize the boat marker at 144 px so large markers stay sharp.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 04:49:36 -03:00
Itallo BrandãoandClaude Opus 5.5 dcd0f2cccd feat(layers): add shipwrecks layer (EMODnet + Wikidata)
A new Events layer maps shipwrecks with a card per wreck:
- EMODnet Human Activities heritage shipwrecks (about 6,500 in European
  waters, CC BY 4.0), skipping rows taken from wrecksite.eu.
- Wikidata shipwrecks with a Wikipedia article plus sinking events
  (CC0); an event folds into its ship for the date and death toll.
- A same-origin /api/shipwrecks proxy refreshes once a day and serves
  the last snapshot as stale for up to a week.

Markers are a boat icon coloured by era and sized by deaths; four era
chips filter the layer. The card follows the air-accidents layout
(deaths first). Text that names people is dropped; victims appear as
a count only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 04:42:59 -03:00
Itallo BrandãoandClaude Opus 5.5 6172be3995 feat(transit): give vehicle icons a readable front, and none without a course
At the ~20 px the fleet renders at, the bus, tram, metro and unknown
glyphs were near-symmetric boxes: rotated along the course, nothing showed
which end was the front. Each now has a front built into its outline (an
arrow-like nose for the bus, a pointed cab for the tram, a chamfered cab
for the metro), so it survives the tint, the halo and the sensor profiles,
which keep only the outline.

A vehicle with no known course sits at rotation 0, where a pointed glyph
would claim it is heading north. Those vehicles, fleet and selected alike,
now get a symmetric outline (rail and ferry included) and switch to the
pointed one once a course arrives. The raster cache doubles to a fixed 72
variants.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 03:39:15 -03:00
Itallo BrandãoandClaude Opus 5.5 b37025414d test(firms): pin the refresh call count and read sources from production
Review follow-up for #1552:
- environmentProviders: assert exactly 3 upstream calls per refresh again,
  so a returning fourth source would fail the test.
- firmsProxy: build the test's SOURCES from server/providers/firms.js, so
  there is one list instead of a mirrored copy.
- firmsProxy: drop the unused fourth count entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 02:57:27 -03:00
Rudy Celekli 79c867c8cb docs(changelog): add the perimeter size fragment for #1569 2026-11-04 22:14:16 -05:00
Rudy Celekli e2ccac56ae fix(perimeters): show a 0.25-acre fire as 0.25 ac, not 0 ac
The incident card rounded the WFIGS size to whole acres, so every fire
under half an acre read "0 ac". The live perimeter feed carries many of
these (0.01 to 0.37 acres). Sizes under one acre now keep the reported
hundredths.
2026-11-04 21:58:42 -05:00
German Khokhlov 0ee92eb864 Answer the Nepal event's Facebook embeds with inert stubs in its strict scenario, as its YouTube ones 2026-11-05 04:07:42 +04:00
German Khokhlov 909be62ae7 Assert the long soak's JS object growth from heap snapshots, and report compiled code, native memory and the used heap's slope apart 2026-11-05 03:38:23 +04:00
German Khokhlov 8bea1419f6 Cover drone airspace, OSM places, sea marks and ski pistes on both engines in the strict acceptance 2026-11-05 01:27:43 +04:00
German Khokhlov e1d3a588fc Drop the late feature changes of deleted Navara vector layers, which kept the view from going idle 2026-11-05 01:11:39 +04:00
German Khokhlov 843c4a111d Show drone airspace, OSM places, sea marks and ski pistes on Navara, and keep them across engine switches 2026-11-05 00:47:08 +04:00
quanticsoul4772 a1cee50d86 Merge branch 'main' into upstream/tools-heading-wrap 2026-11-04 12:46:59 -08:00
quanticsoul4772 c2a04c11b7 Merge branch 'main' into upstream/nominatim-cache-mode-windows 2026-11-04 12:46:44 -08:00
German Khokhlov 43b4e9348b Run the Global Context range pulses and their echoes on Navara, sharing the pulse clock with Cesium 2026-11-05 00:19:30 +04:00
German Khokhlov 727bb29cae Drape the Active Fires heatmap on Navara too, with the same hand-off fade to the markers 2026-11-05 00:11:32 +04:00
German Khokhlov 0c0df50a40 Merge origin/main into german/navara
Brings the Active Fires intensity work: the FRP filter, the confidence
switch, the ember-to-white-hot ramp and the far-zoom heatmap.

The filter, switch and ramp work on both engines. The heatmap drapes on
Cesium through the layer's Cesium renderer, which also fades the markers
across the hand-off; it goes with that renderer on an engine switch and is
built again on the next Cesium globe. Navara keeps heat cells for the far
bands, now coloured on the FRP ramp.
2026-11-04 23:20:12 +04:00
German Khokhlov db9bf64821 Merge origin/main into german/navara
Main's new layers (drone airspace, OSM places, sea marks, ski pistes) are
Cesium-only for now: they join the layer directory and the Cesium catalog,
are built afresh for each Cesium catalog, and show as unavailable on Navara.

The zoom-out guard is a scene part that attaches to Cesium globes only. The
awareness range pulses and their echoes draw on Cesium through the awareness
display's viewer; Navara keeps the plain range ring. Camera-input wakes go
through the display, and the analyst keeps the session-owned memory with
main's area presentation. A disposed voice runner aborts work in flight, and
an engine switch still keeps the last answer.

Modules main added that read Cesium while they load (zoomOutGuard,
awarenessEcho, the ground-sampling scratch in localGeojsonCore) now build
those values on first use or use src/geo, so a Navara page still starts
without the SDK. The OSM view box reads the engine's view rectangle.

Main grew its own main chunk by 93,873 bytes gzip-9 since the merge base, so
the baseline is raised to the merged size.
2026-11-04 23:11:31 +04:00
German Khokhlov a2f5b8fca3 Point the cited commits at the rewritten history and drop the registry rows for the local agent files 2026-11-04 22:47:46 +04:00
Itallo BrandãoandClaude Opus 5.5 b342e405d7 docs(changelog): add fragment for #1552
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 15:20:09 -03:00
Itallo BrandãoandClaude Opus 5.5 84c1fef8d3 fix(firms): stop requesting the retired Suomi NPP source
NASA stopped delivering Suomi NPP data on 2026-11-02 (FIRMS news page).
With a live MAP_KEY on 2026-11-04, VIIRS_SNPP_NRT came back ok with zero
rows while NOAA-20, NOAA-21 and MODIS returned 57k, 49k and 11k, so the
source only spent one transaction per refresh. The proxy now requests
NOAA-20, NOAA-21 and MODIS NRT. The "N" satellite label stays, so cached
or archived Suomi NPP rows still read SNPP.

Tests count sources from the list instead of assuming four, and a
regression test keeps the retired source out of the production list.

Refs #849

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 15:15:34 -03:00
quanticsoul4772 29a95211c5 Merge branch 'main' into upstream/tools-heading-wrap 2026-11-04 07:57:42 -08:00
German Khokhlov 06ae95ceb3 Keep scaling the shared atmosphere vectors in place, and take a rebuilt globe's base from the value they had before any scaling
Pointing the scaled uniforms at the scaler's own vectors (bc1b56b6) left the atmosphere unscaled: takram's materials read the AtmosphereParameters.DEFAULT vectors themselves, so the frame dimmed (luma 161 against 214) and qa-navara-bloom found nothing to bloom. The scaler writes those vectors in place again, and a page-wide map keeps each one's value from before it was first scaled: a globe built later on the page takes its base from there instead of from the scaled value. Bloom measures as before (214.4 → 240.3), and qa-strict-context-loss passes 3 of 3 under eight busy threads.
2026-11-04 19:53:33 +04:00
quanticsoul4772andClaude Opus 5.5 1c2cdc6775 changelog: Nominatim cache mode test on Windows (#1547)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 07:06:45 -08:00
quanticsoul4772andClaude Opus 5.5 4f43ded5da test(nominatim): check cache file modes only where POSIX modes exist
The disk-cache test asserts the cache directory is 0o700 and its files
0o600. Windows has no POSIX mode bits (the directory stats as 0o666), so
the test always failed there. Both checks now run only off Windows, as
localServices.test.mjs and mcpPanelKey.test.mjs already do for their
0o600 checks. The cache code is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 07:05:17 -08:00
German Khokhlov bc1b56b63a Scale the Navara atmosphere luminance without writing into takram's shared vectors, so a rebuilt globe is not drawn nine times too bright
The sky, aerial perspective and clouds materials hold the vectors of takram's AtmosphereParameters.DEFAULT, one per page. The look's scaler multiplied them in place and took its base from the first value it saw, so a Navara globe built later on the same page (after the parked one lost its WebGL context) could record an already scaled value as its base and draw the atmosphere scale² bright: a washed-out, nearly uniform frame. The fourth case of qa-strict-context-loss failed this way under CPU load (3 of 3 with eight busy threads; 3 of 3 pass now). Scaled uniforms now point at the scaler's own vectors, which it puts back each frame if a material wrote into them; the page's vectors are never written.
2026-11-04 19:04:48 +04:00
German Khokhlov b3efcfb7a2 Build the Navara weather shell without the Cesium SDK: angles from geo, the surface extent in radians
Since the weather layer stopped handing Navara the Cesium global, the shared shell read ContextLimits, Math and Rectangle from an undefined SDK and drew nothing on Navara (qa-weather-engine-switch timed out). On Navara the texture limit stays unset, as Cesium's never-created context left it.
2026-11-04 18:12:17 +04:00
German Khokhlov 1100cdcff5 Let the flights' fix history fill before the long soak measures the heap, and say why a context-loss case drew no frame
The long soak's heap slope counted the per-aircraft dead-reckoning history (POSITION_HISTORY_LIMIT fixes for 10 000 aircraft, about 14 MiB over the first 2.5 min of polls) as switch growth: a run with the same workload and no switches grows as much and then levels off. With --assert the soak now waits until the flights feed has been read more times than the history keeps before it switches; over 500 switches the heap then grows 1.07 KiB per switch.

qa-strict-context-loss records, only when no non-blank frame came, how many frames the running and the watched engine drew in 3 s and how much of the last one differs from its first pixel.
2026-11-04 17:48:46 +04:00
German Khokhlov f89aee1eac Load the Cesium SDK and its widget styles only when a Cesium globe or a feature that needs it first runs
index.html no longer loads /cesium/Cesium.js and widgets.css: the Cesium scene loads both before it builds its viewer. A cold Navara start requests nothing of the Cesium library again, and qa-strict-cold-chunks asserts that without the known-gap allowance. The strict harness puts an accessor in place of the Cesium global on every page and fails a scenario that reads it before Cesium.js has run; GEV_QA_CESIUM_TRAP=record lists every such read site instead.
2026-11-04 17:15:27 +04:00
German Khokhlov b8305de14b Run the Navara paths without the Cesium SDK, and load it on demand for the features that still build on it
Every Cesium read that a Navara page made was surveyed with the strict acceptance (GEV_QA_CESIUM_TRAP=record). Math on those paths now uses src/geo: screen annotations, weather and wind center readings, cyclone picks, scene data pack polygons and images (new navaraGeometry.js), the awareness subject, the orbit offset of camera verbs, voice frame_overhead, the agent world packet and Directions colours and ground picks. The celestial ring's Sun and Moon come from a bit-identical port of Cesium's Simon1994 model, JulianDate and TEME rotation (src/geo/simon1994.js, julianDate.js, transforms.js); a Cesium globe still applies its ICRF rotation once loaded.

Traffic, Street Level, the Nepal event and locator and geo import build on Cesium's math or entity model on either globe; their enable (or the import) now loads the SDK first through src/app/loadCesium.js, synchronously when it is already there. A production page guards the global so a missed read is reported and starts the load. No module reads Cesium while it loads (scripts/check-cesium-load-time.mjs, run by npm test). Cesium.js is still loaded by index.html here.
2026-11-04 17:15:21 +04:00
quanticsoul4772 13c3b011be Merge branch 'main' into upstream/tools-heading-wrap 2026-11-03 20:30:35 -08:00
daikaginza a64b41b3f0 Merge upstream/main 73c14ac74 2026-11-04 11:21:01 +07:00
daikaginza 0e0557e7f5 Merge upstream/main 73c14ac74 2026-11-04 11:21:01 +07:00
German Khokhlov f87da8523e Pin the page clock to an Austin afternoon in the Navara bloom and style checks
The look's sun follows the page clock, so the frame these checks compare was as bright as the hour they ran at: in the acceptance run the half-strength bloom changed the frame by 0.29 against 3.5 earlier, and at dusk the full bloom changes nothing (luma 13).
2026-11-04 00:59:58 +04:00
German Khokhlov 91685c94d9 Assert the soak's heap slope from switch 20 again
Measured over the second half the heap still grew about 20 KiB per switch in the acceptance run (117.8 to 123.8 MiB from switch 250 to 500), so the narrower window did not separate warm-up from growth; the original check stays.
2026-11-04 00:58:32 +04:00
German Khokhlov a56f2f9352 Assert the soak's heap slope over the second half of the run and report the warm-up slope beside it
Under the soak workload V8's compiled code and the layers' caches grow for about 250 switches and then the heap is flat (121.4, 121.7, 120.4, 119.1 MiB after 250 to 400 switches), so a line fitted from switch 20 measured the warm-up. Over the second half the threshold stays 10 KiB per switch; the run before the layer effect fix measures 19 KiB there.
2026-11-04 00:24:47 +04:00
German Khokhlov 43ab3ecaaf Run the Navara visual style, bloom, sonar, cockpit cloud and annotation checks in the strict acceptance 2026-11-04 00:20:13 +04:00
German Khokhlov ebc717df8b Dispose a Navara layer effect's pass when the layer detaches so engine switches stop retaining its material and textures
Layers re-create their ground effects on every attach. Navara's removePass leaves the EffectPass undisposed, so each switch kept its EffectMaterial in three's program and material caches, together with the uniforms' DataTextures and their Float32Array data (earthquakes: about 47 KB of buffers and one WebGL program per switch). engine.LayerEffectDesc now disposes the raw pass on delete, as the app's own effects do.
2026-11-04 00:07:15 +04:00
German Khokhlov 726e201314 Read a parked globe's WebGL context from the one it was built with instead of asking its canvas during a switch
Asking the canvas again during the switch made the reuse depend on getContext at that moment: with new contexts refused (qa-strict-failure-matrix, cold Navara to Cesium) the rollback to the parked Navara threw and left no globe.
2026-11-03 23:33:50 +04:00
German Khokhlov 90d64e6916 Mark a resumed or share-restored follow only while it starts so a later follow of the same subject is navigation again
The aircraft scene binding re-tracked with origin share-restore, which stayed on the entity: a later trackedEntityChanged for it (cockpit exit, refocus) never stamped navigation, and the aircraft lost its selection origin. Flights, military and satellites now set a transient gevRestoring mark around follow.start, and the aircraft binding resumes with the origin the aircraft was selected by.
2026-11-03 23:30:10 +04:00
German Khokhlov cbe1d841ea Dispose the Navara bloom and sharpen passes when their effect is deleted
EffectComposer.removePass does not dispose a pass. One withPassDisposal mixin now does it for the bloom, sharpen, styles and overlay effects.
2026-11-03 23:30:10 +04:00
German Khokhlov b49f9dddf0 Fail the acceptance when a production build throws on a cold start of either engine
qa-strict-prod-smoke opens the keyless vite preview on each engine, switches once and fails on any page error. qa-strict-cold-chunks reports Cesium.js at a cold Navara start as a known gap instead of a failure.
2026-11-03 23:21:10 +04:00
German Khokhlov 125fbc5a3b Load the Cesium SDK from index.html again, since engine-neutral modules read its global when they load
This reverts ae13a56f. In the production build every 'cesium' import is the global that Cesium.js defines; with the script loaded only when a Cesium globe started, modules evaluated at boot (cesiumConstants, annotationEngine) threw ReferenceError and neither engine started. A cold Navara start loading Cesium.js stays a known gap.
2026-11-03 23:21:00 +04:00
German Khokhlov 9cc108835e Bring comments in line with the single application and the Navara capabilities 2026-11-03 23:02:04 +04:00
German Khokhlov 8d7f055572 Format the files this branch wrote or broke with Prettier 2026-11-03 23:02:04 +04:00
German Khokhlov 227e3c2051 Serialize shell attach and detach so recovery waits for a failed attachment's cleanup 2026-11-03 22:56:24 +04:00
German Khokhlov ae13a56f6d Load the Cesium SDK and its widget styles only when a Cesium globe first starts 2026-11-03 22:56:24 +04:00
German Khokhlov 9f8381ff71 Check a parked renderer's real WebGL context before reuse and rebuild it when the context is lost 2026-11-03 22:56:24 +04:00
German Khokhlov 01ae045346 Keep a restored tracked aircraft when Cesium starts or resumes instead of treating it as new navigation 2026-11-03 22:56:24 +04:00
German Khokhlov 59a2e31aa8 Offer voice actions on whichever globe has the capability they need instead of refusing them without a Cesium viewer 2026-11-03 22:42:25 +04:00
Dylan Maniatakes 134597c3cf docs: add S2 age filter changelog for PR 1528 2026-11-03 13:23:40 -05:00
Dylan Maniatakes 36fceb5675 Add adjustable event age filtering to S2 GhostMaps 2026-11-03 13:15:33 -05:00
German Khokhlov b20579b06a Register the Navara Draw input in the package boundaries again 2026-11-03 22:03:25 +04:00
German Khokhlov a9282b30bb Expect the session's marks and files drawn on Navara as on Cesium in the one-application switch check 2026-11-03 21:52:42 +04:00
German Khokhlov 1a3f6f2486 Treat contacts with the Cyber sonar sweep on Navara behind an optional render.setContactSonar capability
The sprite and point mover shaders rate each contact by its anchor against the sweep with the cyberSonar.js sampler maths; the engine sonar scene feeds the theme, settings and shared sweep angle each drawn frame and keeps the fleet cadence only while the sweep runs over contacts.
2026-11-03 21:49:56 +04:00
German Khokhlov c2d7a6dbb0 Register the Navara style, bloom and cloud cover modules in the package boundaries and keep VisualEffects free of the globe contract 2026-11-03 21:49:50 +04:00
German Khokhlov 89339cdd96 Leave the scope mask off a globe whose container holds no DOM children 2026-11-03 21:47:13 +04:00
German Khokhlov 5983c663bd Put the DISPLAY scope mask over Navara, its terminus following the engine camera's height 2026-11-03 21:46:37 +04:00
German Khokhlov ef174b0ff7 Keep local directories out of the acceptance report 2026-11-03 21:45:20 +04:00
German Khokhlov 6ba67d01af Draw by hand on Navara: clicks, moves and the double click from the engine input, a dashed engine line and vertex dots as the preview, and depth picks at the camera treated as misses 2026-11-03 21:41:30 +04:00
German Khokhlov d8ef366ab1 Classify the Navara launch pad zone onto terrain and 3D tiles with Cesium's fill, rim and emission
The zone was a flat disc at ellipsoid height in Navara's opaque scene: it floated over low pads and sank under raised ground or 3D tiles, with hard edges and an unlit rim. A screen-space pass now paints the ground pixels within the radius of the pad from the scene depth, as Cesium's GroundPrimitive classifies them, using one style shared by both engines (LAUNCH_PAD_ZONE_STYLE).
2026-11-03 21:30:36 +04:00
German Khokhlov f0d38897f4 Drive the Navara volumetric clouds from the cockpit weather behind an optional render.setCloudCover capability
The cockpit's weather feed moves out of the Cesium cloud pass and serves both: Cesium keeps its own cloud canvas, an engine with its own clouds takes their cover from the same observations, with no second WebGL canvas.
2026-11-03 21:22:34 +04:00
German Khokhlov d8c63c69d8 Import GeoJSON, KML, KMZ and GPX files on Navara: shapes as clamped vector layers, pins as sprites, and the graphics it cannot draw counted and reported 2026-11-03 21:19:55 +04:00
German Khokhlov eee4f48745 Hide Navara infrastructure stems and data pack meshes behind walls, terrain and the Earth
Navara draws its transparent scene after the post effects into a target without the scene depth, so stems and pack polygons and images painted through 3D tiles. A scene-depth test now drops each of their fragments behind the depth Navara packs after its opaque pass; they stay in the transparent pass, so their look in the open is unchanged (scripts/qa-edge-occlusion.mjs compares it with the previous build). Wind flow was already hidden and is kept as a guard in the same scenario.
2026-11-03 21:15:03 +04:00
German Khokhlov c392d6fb50 Glow the Navara frame with the Display bloom behind an optional render.setBloom capability
postprocessing's BloomEffect runs after the overlays and before the visual styles, follows Cesium's eased strength and dead zone with its threshold set in display terms, and VisualEffects drives it when the engine has the bloom capability.
2026-11-03 21:11:53 +04:00
German Khokhlov 074f4af10e Release a running camera verb when a Navara scene is parked or torn down, as the Cesium scene does 2026-11-03 21:11:30 +04:00
German Khokhlov dcb8de9d2c Run the camera verbs on the globe contract so orbit, pan, tilt, rotate, globe rotation and the route dolly move a Navara camera as they move Cesium's
A motion drives one camera through a rig: a Cesium viewer's own camera, or any globe engine through camera.pose/setView/lookAt/release/transform/flightActive/following and frames.preRender. An engine rig binds its frame, canvas-input and follow listeners only while its motion runs. A finished once orbit now releases its lookAt lock, and an orbit armed through a flight keeps its nudge budget until it starts.
2026-11-03 21:11:30 +04:00
German Khokhlov 4c28cc0212 Draw the visual styles on Navara as one post pass behind an optional render.setStyle capability
CRT, NVG, FLIR, Anime and Noir run their Cesium shaders through a GLSL prelude with the exact sRGB transfer, crossfade in the stage order, fade the Navara stars with Anime, and report the visualStyles capability through engineCapabilities.
2026-11-03 21:02:50 +04:00
German Khokhlov 00dfed7959 Run the scenarios from the served clean checkout 2026-11-03 21:01:17 +04:00
German Khokhlov f7945d6844 Stop the runner's preview at the end and record only .env names and credential leaks in the served build 2026-11-03 21:00:58 +04:00
German Khokhlov 96b5b0bb1f Draw the session annotation board on Navara: draped areas and routes as clamped meshes, every other mark on the shared screen board over the engine's projector 2026-11-03 20:59:10 +04:00
German Khokhlov 5b7171f485 Pin that push-to-talk starts from the Navara canvas inside its renderer host 2026-11-03 20:40:34 +04:00
German Khokhlov f5c14ec30d Move the session annotation board, Draw and Import out of the Cesium tools into sessionTools.js and give Navara its tools seam 2026-11-03 20:37:59 +04:00
German Khokhlov dea10cac4f Give Local ADS-B the selectById hook the other selectable layers expose
It takes the ICAO a position row carries, in any case, or the marker id, and selects through the same path as a click; selectAircraft now delegates to it.
2026-11-03 20:36:10 +04:00
German Khokhlov 60a230b2ff Keep a bikeshare city's last known stations, marked stale, when it comes back into range during a source outage 2026-11-03 20:33:13 +04:00
German Khokhlov 057dbe24af Reframe a mission after a late active-satellite answer only while nobody has left its orbit view
A flyover, a launch-site view, a cancelled flyover or another mission now clears the pending orbit framing, so a late TLE lookup followed by a data refresh never takes the camera back.
2026-11-03 20:31:49 +04:00
German Khokhlov f0d7def9c7 Report an empty wind field from get_wind as no surface reading instead of a NaN speed 2026-11-03 20:29:44 +04:00
Said Turksever 2c936ae131 docs: correct hidden-contact rotation interval 2026-11-03 16:26:45 +00:00
Said Turksever 6dca8d943a Merge commit '2ebde97fb3af19402eab83fca2bfbec9ddfd166a' into easy-wins-20261103-pr766 2026-11-03 16:17:34 +00:00
Said Turksever 4e40594d4f Merge commit '2ebde97fb3af19402eab83fca2bfbec9ddfd166a' into easy-wins-20261103-pr762 2026-11-03 16:17:34 +00:00
Said Turksever b68cd25e9e Merge commit '2ebde97fb3af19402eab83fca2bfbec9ddfd166a' into easy-wins-20261103-pr870 2026-11-03 16:17:34 +00:00
Said Turksever 50cd03eb03 fix: resolve review findings and refresh PR #766 against main 2026-11-03 16:17:34 +00:00
Said Turksever b47c91965c fix: resolve review findings and refresh PR #762 against main 2026-11-03 16:17:33 +00:00
Said Turksever 90f9050b4c Merge commit '2ebde97fb3af19402eab83fca2bfbec9ddfd166a' into easy-wins-20261103-pr1524 2026-11-03 16:17:32 +00:00
Said Turksever 66a5c483c2 fix: resolve review findings and refresh PR #870 against main 2026-11-03 16:17:31 +00:00
Said Turksever 5f686b7289 fix: resolve review findings and refresh PR #1524 against main 2026-11-03 16:17:24 +00:00
German Khokhlov 1773494116 Run the layer acceptance under a default-deny network policy with per-layer assertion manifests and strict switch contracts
Every scenario runs through scripts/qa-strict-harness.mjs: each page, frame and worker gets an ordered block list before it runs (paid hosts and routes first, then only the preview, declared fixtures, keyless probes and cached free tile hosts), switches with real input on the picker, and records its assertions per layer and engine. The runner reads the catalog from the live page, fails any layer without an assertion on either engine, can build and serve a keyless checkout itself, and adds rapid, hidden-tab, context-loss, failure-injection, cold-chunk and soak scenarios. The main chunk gets a 1% size budget.
2026-11-03 20:15:50 +04:00
Said Turksever b7de7c1113 Merge branch 'main' into fix/taiwan-highway-cctv-fallback 2026-11-03 16:11:35 +00:00
German Khokhlov 3144169057 Draw the Navara CCTV frame and viewshed in the depth-tested opaque scene so walls hide them
Navara's transparent pass runs after the post effects without the scene depth, so a camera frame clamped in front of a wall still painted through it. scripts/qa-edge-cctv-occlusion.mjs serves a one-box tileset as a local fixture with no network and checks on both engines that the frame ends at the wall and is not visible behind it.
2026-11-03 20:11:23 +04:00
German Khokhlov 37ea4e1f93 Keep the six map-source tiles on one row and follow the engine picker in the map tray gate
Sentinel-2 made the desktop source row wrap to two rows; the row now lays out every tile in one row (three columns on a phone, as before). scripts/qa-map-source-tray.mjs read the controller's removed imagery internals and counted five tiles between OSM and the styles; it now reads the live base provider and counts the section's tiles and engine picker. The five failures left also fail on origin/main e5d15f24.
2026-11-03 20:00:55 +04:00
Said Turksever 44581dccaf Merge remote-tracking branch 'upstream/main' into pr1506-fix
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-03 15:57:37 +00:00
German Khokhlov 31b1f27a2e Wait for the traffic refinement task by task instead of for five milliseconds
The handoff test failed under a loaded test runner when the refined pass needed more yields than a fixed 5 ms timer allowed; it now yields until the refined paint lands (bounded by a task count, not wall time).
2026-11-03 19:55:33 +04:00
German Khokhlov f2117a98f2 Ask a parked Navara globe for no frames and guard parked engines in the switch gate
The Navara look stopped requesting frames once its globe is parked. scripts/qa-app-once.mjs now fails if anything moves a parked engine's camera or asks it to draw (its own resume aside).
2026-11-03 19:54:45 +04:00
German Khokhlov 40a81c58e6 Hold the shell's inputs between two engines instead of reaching released controllers
A visual choice made while no globe is attached (a style key, a Display control, a voice command) is queued and applied to the next engine's settings after it restored the address's view, so the person's choice wins; reads answer from the released settings. The globe reset and a location lookup started without a globe do nothing, and the map-source chips and camera buttons are unbound until the next globe binds them.
2026-11-03 19:54:45 +04:00
German Khokhlov df73c5b21a Break Contacts distance ties by identity so navigation reaches the same ID in any feed order 2026-11-03 19:50:25 +04:00
German Khokhlov c96d4ecfb1 Check that a superseded or cancelled launch flyover never takes the Navara camera back 2026-11-03 19:50:24 +04:00
German Khokhlov d5218c4722 Check that a late route answer and the previous engine's camera never take over a Directions route 2026-11-03 19:48:15 +04:00
German Khokhlov 4f8a9f2615 Cover a scene switch at a stopped satellite epoch with an edge-case test 2026-11-03 19:48:05 +04:00
German Khokhlov 1c09c90e13 Test that a late Nepal boundary after cancel leaves the live locator scene alone on both engines 2026-11-03 19:47:29 +04:00
German Khokhlov dae4011604 Pin that bikeshare systems sharing a station ID stay apart and an outage keeps the last status card 2026-11-03 19:47:04 +04:00
German Khokhlov 299a5943a0 Ignore a late image or error from a street-level provider the user switched away from 2026-11-03 19:46:31 +04:00
German Khokhlov f7cd6797ec Cover a vessel crossing an area cell during an AIS gap with an edge-case test 2026-11-03 19:46:13 +04:00
German Khokhlov 1910a3b2d5 Check that fires keep their observation sum and draw one marker kind across Navara LOD changes 2026-11-03 19:46:10 +04:00
German Khokhlov c45ec634ee Pin that an ALPR camera on a tile edge stays one record with its tagged azimuth 2026-11-03 19:45:59 +04:00
German Khokhlov d86047fe8f Release the shell from a candidate engine that fails after it attached
The shell's scene part detaches it too, so a candidate failing in a later part (a layer's drawing, Cockpit clouds) leaves the shell free for the working engine instead of refusing every later attach. StyleManager.attachScene releases what it built when it throws, and one detach runs at a time. The controls phase awaits its parts, and a failed attachment no longer forces handoff off for the parts it releases. scripts/qa-app-once.mjs now also rolls back from a Navara candidate whose layer attachment fails.
2026-11-03 19:45:34 +04:00
German Khokhlov 58c8a481ed Report an empty wind field as no reading instead of a NaN speed 2026-11-03 19:45:16 +04:00
German Khokhlov 63b2590b9f Cover an unplugged local receiver and local IDs in share links with edge-case tests 2026-11-03 19:44:55 +04:00
German Khokhlov 4a9b95e31b Pin that an empty installation answer is never reported as an unavailable source 2026-11-03 19:44:50 +04:00
German Khokhlov b977ef29e3 Split submarine cables at the antimeridian and keep their reference beside the crossing 2026-11-03 19:44:33 +04:00
German Khokhlov 293ffb2bd3 Pin that a stale GNSS zone and its refreshed neighbour stay apart and edge positions bin once 2026-11-03 19:44:16 +04:00
German Khokhlov b62f60482e Pin that a zero air-quality reading and a missing one give different cards 2026-11-03 19:43:42 +04:00
German Khokhlov b29e20632b Test that transit heading, data age and selection stay continuous through a stop, a turn and a feed outage 2026-11-03 19:43:37 +04:00
German Khokhlov 736ee8d694 Drop earthquakes older than the USGS feed window and pin duplicate and horizon edge cases 2026-11-03 19:43:07 +04:00
German Khokhlov 03cd379c0e Cover a military ICAO reported by both feeds with edge-case tests 2026-11-03 19:42:46 +04:00
German Khokhlov 57f7edc693 Test that a GhostMaps edition or engine change mid-parse shows only the selected edition 2026-11-03 19:42:10 +04:00
German Khokhlov c34a2c38fc Keep the analyst's last answer when the voice runner moves to another engine
A runner retired by an engine switch ends its in-flight writes but leaves the shared follow-up memory for the next engine's runner; ending the voice conversation still forgets it.
2026-11-03 19:41:12 +04:00
Said Turksever db8afbae81 fix(cctv): key-gate Ottawa, opt-in Québec video, keep shared slots
Review fixes for #1506.

- Ottawa: use the documented camera list and the city's Traffic Camera
  Images API. The pack is skipped with no request unless
  OTTAWA_CAMERA_CERTIFICATE is set; the certificate is added only at
  fetch time inside the pinned /opendata/ path, and each camera is
  fetched at most once a minute through the shared frame cache.
- Québec 511 video is opt-in (CCTV_QUEBEC_VIDEO_ENABLED=1) and loads
  only after the viewer presses Play; selection, auto-hop, scenes and
  voice no longer contact Québec 511. Credit splits locations (MTQ,
  CC BY 4.0) from video (Québec 511). Interrupted play() is no longer
  reported as an unavailable source; the per-frame listener runs only
  while a Québec camera is selected and the canvas rect is cached.
- Catalog cap: reserved Québec and World slots come only from capacity
  above the 4,000 shared slots, so a lower CCTV_MAX_SOURCES thins every
  pack instead of starving the existing ones. dev-fresh and
  .env.example default to 7,516.
- Throttled frames (World, Ottawa): inside the refresh floor with no
  cached frame the proxy serves the placeholder, never Street View; a
  failed fetch backs off for a minute; the cache is bounded by bytes.
- MJPEG first-frame extraction runs only for sources registered as
  MJPEG (Taiwan Highway Bureau), caps a frame at 2 MB, reads in linear
  time, accepts a CRLF preamble and a boundary that already starts with
  "--", and ends a part without Content-Length at the next delimiter.
  THB hosts are registered in the redirect pin list.
- Tests: World loader policy, Québec loader and opt-in, proxy cooldown
  without Street View, Ottawa signing, cap reservations at 7,516, 4,000
  and 48, overlay Play gate, refresh, visibility and teardown. Guard
  tests check the 4,000 shared slots again.
- Remove the unused standalone Québec preview module and public page.
- Docs: DATA_SOURCES, SECURITY, CURRENT-STATE, CCTV_INTEGRATION, README,
  .env.example, changelog fragment renamed to its full scope.
2026-11-03 15:31:08 +00:00
German Khokhlov 8e1a3bab3e Keep the debug handle off window between engines and recover the page after a failed recovery
The application's debug handle is one object, present on window only while a globe is attached, so scripts reading __godsEyeView?.engine.kind see no null engine mid-switch. A detached application has no live components: settle no longer waits for a frame of a missing engine, and an engine that runs again after a failed recovery hides the loading screen the error showed.
2026-11-03 19:10:47 +04:00
quanticsoul4772andClaude Opus 5.5 ccd83c5fa2 docs(tools): say heading values wrap into 0-359, not that 359.5 and up read 0
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 07:00:27 -08:00
quanticsoul4772andClaude Opus 5.5 d1ed2e8e6f changelog: tool headings wrap at 360 (#1524)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 06:49:55 -08:00
quanticsoul4772andClaude Opus 5.5 ae55963a0e fix(tools): wrap whole-degree headings at 360 to 0
The query tools rounded courses, headings, bearings and azimuths with
Number(value.toFixed(0)) and no wrap, so a direction of 359.5 degrees or
more came back as 360: find_vessel with course 359.7 and heading 359.6
returned course_deg 360 and heading_deg 360.

Round them through the shared roundHeadingDegrees helper the layers and
UI already use (src/data/headingDegrees.js), which wraps into 0 to 359:
course_deg and heading_deg (vessels), course_deg (aircraft), bearing_deg
(transit), heading_deg (cameras), and rise_azimuth_deg and azimuth_deg
(satellites). Other rounding is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 06:32:07 -08:00
German Khokhlov a99b862580 Create the application once per page and switch engines through the scene host
createStandaloneApplication opens its first globe, attaches the scene host and returns attachScene/detachScene: a switch hands the session off, parks the outgoing renderer and attaches the incoming one with its catalog to the parts the phases registered once. A failed candidate releases what it attached; the working engine attaches again on the same shell, voice, tools and stores. ?switch=legacy keeps the route that builds an application per engine for one release. scripts/qa-app-once.mjs now asserts identity, UI, tool and listener continuity, the rollback and the legacy route.
2026-11-03 18:19:51 +04:00
German Khokhlov 2bac200c01 Attach the tools, data and controls bound to one globe as scene parts
Each phase builds its session objects once (shell, layer manager and presentation, Director, annotation board, Draw, Import, voice, debug handle) and registers the parts bound to one globe: the shell's later attachments, Cockpit clouds, the catalog binding and sonar, the Director's scene, the Cesium or Navara tools and the debug handle's globe fields. A page with a scene host attaches them to every engine; a host that never switches attaches them once, as before. The shell releases its globe before the data layers detach, and the Navara notice goes with its engine.
2026-11-03 18:17:23 +04:00
German Khokhlov 061ed1b682 Attach the shell to another globe without disposing it
StyleManager.attachScene/detachScene rebuild only the globe-bound parts (share restoration, visual settings, layer bindings, Cockpit, the celestial ring, the overlay host, detection, map-source and camera-orientation controls) and point navigation, location search, HUD, share link and the display bindings at the next globe. dispose() revokes input synchronously, detaches the scene and only then nulls the stores and unmounts the views. The radio tuner listens on the globe container, and an engine's persistent notice can be withdrawn.
2026-11-03 18:02:46 +04:00
German Khokhlov d1fdb9eb45 Let the shell's share link, HUD, orbit and navigation follow another globe engine
The shell reads its viewer, engine and map stack from the attached scene. ShareLinkManager, IntelHUD and OrbitController gain attachEngine, NavigationController setScene and LocationNavigation attachScene/detachScene, so a switch can keep them and move only their camera listeners; between engines they write and read nothing. The voice runner and session voice modules are owned by their packages, which check:boundaries needs since the voice commit.
2026-11-03 17:48:08 +04:00
German Khokhlov 741b6f67eb Keep the Draw control and its chosen shape across engine switches
The session Draw handle keeps the shape and colour and the marks stay on the annotation board; the drawing tool (preview, click handler, Escape and Enter keys) belongs to the attached viewer, so a switch ends an open sketch and gives the pointer back.
2026-11-03 17:38:05 +04:00
German Khokhlov 17eea266f1 Keep imported files across engine switches and read them again on the next Cesium viewer
The Import list and each file's bytes belong to the page session; the data sources, pins, drop and gesture listeners belong to the attached viewer. Without a viewer the list stays and import or fly-to is refused.
2026-11-03 17:30:27 +04:00
German Khokhlov 111ba1626c Keep the annotation board across engine switches and draw it again on Cesium
The marks belong to the page session; only their drawing belongs to the attached viewer. A switch takes the drawing off, keeps the marks, and the next Cesium attachment draws them again. Without a viewer the board refuses new annotations instead of failing in the resolver.
2026-11-03 17:24:39 +04:00
German Khokhlov 05578c61b9 Keep an active voice session through engine switches
The page creates the voice once; each engine attachment only gives it that scene's action runner, pointer canvas and annotation events. Calls made during a switch wait for the incoming engine and are refused with ENGINE_SWITCHING when none attaches in time; results name the engine that answered, and the session hears engine-changed after a switch. The analyst's follow-up memory and layer enable times outlive one engine's runner. scripts/qa-voice-engine-switch.mjs drives a fake microphone and Realtime transport through the switches.
2026-11-03 17:20:40 +04:00
German Khokhlov 35ed499ae4 Keep the place search, language listener, logo gaze and Google key for the page session
An engine switch no longer recreates them: with a layer session they live until the page lifetime aborts; a host without one keeps the per-application lifetime.
2026-11-03 17:13:07 +04:00
German Khokhlov f0c2523ac4 Add the scene host that attaches engine-bound parts in order and detaches them in reverse 2026-11-03 17:09:53 +04:00
German Khokhlov b2cc00833d Measure session object identity and listener counts across engine switches
window.__gevSession names each session object by a stable instance id and counts applications, attachments and detaches. scripts/qa-app-once.mjs (report-only with --report for now) runs three round trips and a parked-renderer rollback on a keyless preview with paid map, model and voice endpoints blocked.
2026-11-03 17:08:22 +04:00
German Khokhlov 5564156c09 Assert that the traffic population after a globe round trip stays near the population before it
Both are read after the same settle delay; the population is checked again once the vehicle comparison is taken.
2026-11-03 16:24:08 +04:00
German Khokhlov eed8acc3f7 Keep the traffic population and vehicle heights after a globe round trip
The republish after a globe switch ran as a refinement ('Handoff refined'):
it sampled the incoming globe while it still showed coarse tiles and,
being a refinement, never refined again. Keyless Cesium's first terrain
tiles put Austin's streets near -2 200 m, so vehicles stayed there, and
the next globe weighted each road by the camera distance to those
waypoints (1.9 km became 4.1 km), cutting the population from about 620
to 260 until the camera moved.

The handoff is now a completed pass that refines once the incoming
surface has loaded, road density and the coverage radius are measured to
the road's coordinates on the local ground estimate instead of to the
last sampled waypoints, and flow that lands after a publication
re-allocates the retained roads' budgets instead of only recolouring
them.
2026-11-03 16:24:08 +04:00
German Khokhlov f4879fc24b Keep the Utilities panel's Directions row mounted across a globe switch 2026-11-03 15:36:11 +04:00
German Khokhlov 7f0572cf4c Run upstream's HUD summary and standalone startup tests against the engine-based HUD and application 2026-11-03 15:36:11 +04:00
German Khokhlov 1a1eef0e5c Merge origin/main into german/navara
Conflict resolutions:
- src/main.js: upstream's PHOTON_URL place-search setting is read once at
  module level and passed as geospatial: { endpoints: { photon } } by
  createApplication(), so every engine start of the switch flow gets it; the
  old single top-level createStandaloneApplication() call stays removed.
- src/layers/bikeshare/render/cesium/rendering.js: upstream's
  publishedCapacity fix applied with this branch's
  resolveCapacity(model, capacity, status) signature. The same fix is ported
  to src/layers/bikeshare/render/navara.js (record.publishedCapacity, status
  polls resolve from it), with a Navara regression test; upstream's
  proxyRoute test imports the moved render/cesium/rendering.js.
- src/layers/traffic/flow.js: upstream's nextTask() yield and current()
  check kept before the scene is taken from this branch's
  _trafficScene?.() ?? _viewer?.scene with its missing-scene return.
- scripts/package-boundaries.json, scripts/format-scope.json: union of both
  sides' file lists.
- src/voice/actionSchemas.test.mjs: both history comments kept, payload hash
  recomputed for the merged schemas (Sentinel-2 map stack enum plus the
  shorter layer alias hint).
- src/hudAltitudeDatum.test.mjs: this branch's engine-based IntelHUD
  construction with upstream's placeLabels summary context.
2026-11-03 15:36:06 +04:00
German Khokhlov a066a2d1ca Keep history entries untouched by unfinished globe picks and restore each visited entry's layers
A picked engine now lives in the switch until it runs; only then is the
address written, so a pick superseded by Back or one that fails rewrites no
entry. An engine-changing history visit holds the outgoing share link, which
neither restores nor writes the visited entry, and the running engine takes
the address back once the switch settles, restoring any entry visited while
it was held or not yet listening. Handed-off layers keep running where the
incoming address agrees with the outgoing one and follow a visited entry
where it differs. A failed recovery keeps the error on the loading screen
with an engine picker for a retry, and a failing reveal of the new globe no
longer blocks the cover removal or later requests.
2026-11-03 15:24:20 +04:00
German Khokhlov 9cc34e324f Stop scene attachments of Bhote Koshi, GhostMaps, cables and infrastructure waiting once their scene is released 2026-11-03 15:23:21 +04:00
German Khokhlov cf6d04b7db End the data startup wait when the application is torn down during a slow layer attachment 2026-11-03 15:23:21 +04:00
German Khokhlov 7fb11507d5 Bound the wait for layer attachments, park enables and explicit refreshes until the next scene binds 2026-11-03 15:23:20 +04:00
German Khokhlov 92666fdbaf fix(cyclones): preserve movement in selected voice context 2026-11-03 15:10:20 +04:00
German Khokhlov 53c114965d Write a machine acceptance report and stop early when no browser or preview is available
qa-all-layer-switches.mjs now checks once that Puppeteer can start Chrome and that the preview answers, and names PUPPETEER_EXECUTABLE_PATH and npx puppeteer browsers install chrome when it cannot. Each run writes <output>/acceptance.json with the commit, dirty files at both ends, Chrome, Node, base URL, times and, per scenario, status, exit code, duration, covered layer ids and the sha256 of its log. It replaces summary.json.
2026-11-03 15:05:37 +04:00
German Khokhlov 65d41b971c Check traffic vehicles one by one against a fixed dark ground after the globe round trip
Live Sentinel-2 imagery is bright at this altitude, so a fixed crop of white two-pixel vehicles changed anywhere from 14 to 300 pixels. The scenario now serves a uniform dark ground tile, waits for the layer to settle with vehicles in view, projects each vehicle to the window and requires 90% of those above the drawn terrain to change the picture where the layer places them; in simulation nothing else may change. Vehicles below the terrain and the population left after the round trip are reported, not asserted.
2026-11-03 15:05:36 +04:00
German Khokhlov 4d2b061e3b Hand the session off on Back/Forward and roll failed globe switches back to the last working engine
The engine switch moves out of main.js into src/standalone/engineSwitch.js: every
engine change in the page hands the session off (layers, Director, Contacts,
directions and satellite cameras), whether it came from the picker or from
history. Each attempt has a generation; a failure rolls back to the last engine
that started, requests made during a switch or recovery are reconciled once it
settles, and a picked engine adds its history entry only after it starts. The
first-frame wait only lifts the cover and never fails a started engine; a
hidden tab does not wait. Nothing starts after pagehide, and a failed recovery
leaves the picker usable.
2026-11-03 14:51:17 +04:00
German Khokhlov 64c5cd25e6 Document which inputs a warm cached renderer keeps from the application that built it 2026-11-03 14:47:36 +04:00
German Khokhlov 64759c552e Release the cockpit cloud WebGL context when its controller is destroyed 2026-11-03 14:47:36 +04:00
German Khokhlov 12cd73bed9 Skip CCTV scene work when its renderer was released during a health refresh 2026-11-03 14:47:35 +04:00
German Khokhlov baabff6acf Stop a Bhote Koshi attachment that resumes after its scene was released 2026-11-03 14:47:35 +04:00
German Khokhlov 2853938df7 Detach every layer from a scene even when one fails, and let in-flight attachments stop first 2026-11-03 14:47:35 +04:00
German Khokhlov dd040b4f87 Let a failed application teardown be retried instead of repeating its rejection 2026-11-03 14:47:35 +04:00
German Khokhlov 0c190c3a15 Format the layer lifecycle, standalone application and Bhote Koshi event with Prettier 2026-11-03 14:36:31 +04:00
InoukT fa4ce4b2a5 feat(cctv): add Quebec, Ottawa, and World feeds 2026-11-02 22:57:22 -05:00
German Khokhlov 62296a833c Exercise every catalog layer on both planets and verify live switch continuity 2026-11-03 05:22:02 +04:00
German Khokhlov 05b646daa3 Unsubscribe removed Navara effects and models from warm view frames 2026-11-03 05:16:20 +04:00
German Khokhlov 14f7b32537 Release Navara overlay targets and sprite images on repeated globe switches 2026-11-03 05:04:38 +04:00
Sameh Khamis bc61a39738 Merge branch 'sameh/world-clock' into sameh/world-timeline-launches 2026-11-02 17:01:07 -08:00
Sameh Khamis 9ab54675ba Merge remote-tracking branch 'origin/main' into sameh/world-clock
# Conflicts:
#	scripts/package-boundaries.json
2026-11-02 17:01:02 -08:00
German Khokhlov 160e445677 Keep authored scene playback, pack media and geographic interactions across globes 2026-11-03 04:49:46 +04:00
Sameh Khamis 3cecc8efbe Offer a minute a second as a play speed
Between real time and an hour a second there was nothing to watch a few
minutes of weather or a launch play out at. The speed menu now offers a
minute a second as its second choice.
2026-11-02 16:47:35 -08:00
German Khokhlov a49671a391 Preserve satellite tracking, orbit solutions and mission replay across globes 2026-11-03 04:36:49 +04:00
German Khokhlov 133ca14119 Keep CCTV playback and calibration while replacing globe projection surfaces 2026-11-03 04:25:02 +04:00
German Khokhlov 24190a3e35 Keep city stations, vehicle tracks and trail history across globe switches 2026-11-03 04:21:48 +04:00
German Khokhlov c4a97a653c Keep fire and camera selections bound to their source records 2026-11-03 04:18:53 +04:00
German Khokhlov b01f1ee7dc Preserve radio playback and imagery comparisons when switching planets 2026-11-03 04:16:00 +04:00
Sameh Khamis 8eab6a76a8 Drag the timeline along, and play through it smoothly
The zoomed track now drags: the handle still drags the time, while the
track around it drags the view along (or the time, with all of it shown),
a tap goes to the time under it, two fingers pinch it, and a sideways swipe
or a shift-wheel drags it too, the same with a mouse, a trackpad or a
finger. A view dragged off the handle stays put until the time moves.

Play moved in steps: the world clock told of its time four times a second,
so the handle and the globe jumped, and play stopped at every weather frame
while it drew, loaded ahead or not. The clock now tells of its time each
animation frame, and play no longer waits on the frame it is at: that
frame loads as play goes on, and play holds only at the next frame while
the one before it still loads. Weather loads its next frame ahead while the
world plays too, tells its listeners of frames as they change rather than
every frame, and the sun and moon move on each minute of world time.
2026-11-02 16:13:27 -08:00
German Khokhlov b98cbeebf2 Keep street imagery and picked coverage across Cesium and Navara 2026-11-03 04:13:26 +04:00
German Khokhlov 75df3389f3 Keep traffic vehicles and road flow across globe switches 2026-11-03 04:09:21 +04:00
German Khokhlov 4e09559337 Carry the Nepal event images, playback and media across globes 2026-11-03 03:57:18 +04:00
German Khokhlov 811bcaff85 Retain the Nepal locator presentation and draw it on either globe 2026-11-03 03:49:39 +04:00
German Khokhlov 605b9490f3 Move Directions routes and active camera flights between globes 2026-11-03 03:44:07 +04:00
German Khokhlov 5cea3e8cbb Retain the TeleGeography dataset and render cables and landings on Navara 2026-11-03 03:38:15 +04:00
German Khokhlov 2b4955c817 Draw and inspect the same wind forecast when switching globes 2026-11-03 03:34:25 +04:00
German Khokhlov d7b3d45135 Keep local ADS-B receivers and aircraft history alive across engine changes 2026-11-03 03:30:19 +04:00
German Khokhlov c1d829ef51 Draw local infrastructure on either globe from one retained dataset 2026-11-03 03:28:26 +04:00
German Khokhlov 868d061856 Preserve cyclone bulletins, forecast geometry and storm selection across engines 2026-11-03 03:21:58 +04:00
German Khokhlov cb350fa58d Share observed weather clocks and render radar, infrared and lightning on Navara 2026-11-03 03:18:59 +04:00
German Khokhlov 11992828fb Keep GhostMaps KMZ editions and selections across native globe renderers 2026-11-03 03:13:36 +04:00
Sameh Khamis 4fc9722356 Show a spinner by the time while play waits on data
Play holds while a layer loads the frame for its time, and the bar's only
sign of it was a faint pulse, so a stopped handle read as a stall. The
bubble now shows a small spinner beside the time while play is held. It
comes on once a hold lasts long enough to notice and stays through a run of
slow frames, so neither a quick frame nor the gaps between frames flash it.
2026-11-02 15:11:47 -08:00
German Khokhlov 87bbe14748 Keep Contacts, active layer intents and mapped sites across globe switches 2026-11-03 03:05:47 +04:00
German Khokhlov 81bcae907f Preserve AIS observations, selection and trail across globe changes 2026-11-03 02:53:10 +04:00
German Khokhlov e08ddd787b Preserve military aircraft and classification across renderer handoffs 2026-11-03 02:47:32 +04:00
German Khokhlov 1bda760763 Preserve fire incident geometry and selection across both globes 2026-11-03 02:40:36 +04:00
German Khokhlov b7431d7823 Use cockpit readiness for voice entry on either globe 2026-11-03 02:39:27 +04:00
German Khokhlov d7ab85f5c4 Keep Vue layer rows mounted through globe switches and rollback 2026-11-03 02:38:25 +04:00
German Khokhlov 2ee09c5911 Verify renderer startup rollback keeps the flight cockpit usable 2026-11-03 02:32:19 +04:00
German Khokhlov 1930ca247a Keep GNSS history across globe changes and share the Navara ground cell pass 2026-11-03 02:30:52 +04:00
German Khokhlov 6fe5bda028 Draw air quality on both globes from one retained station model 2026-11-03 02:25:10 +04:00
German Khokhlov 99bda8a35e Exercise camera handoff behavior in the location readout regression 2026-11-03 02:23:45 +04:00
German Khokhlov c0062c4d3d Retain live flights and earthquake observations across globe switches 2026-11-03 02:14:55 +04:00
German Khokhlov a0e5f04c30 Separate flight observations and tracking from the scene renderer 2026-11-03 02:04:41 +04:00
Sameh Khamis 287c881c17 Zoom the timeline track around the handle
With layers of very different reach on together, a month of launches and
a few hours of lightning, the short-range ticks were crushed at the end of
the one shared track. Scroll or pinch on the track now zooms it around the
handle, plus and minus zoom from the keyboard, and zero or a double-click
shows it all again. Everything still plays together; zooming only changes
what the track shows, and the view follows the handle as it plays, steps or
is left behind by now. A scrub's live edge goes by the stretch shown, so a
zoomed track can land minutes from now, and a double-click or pinch puts
back the time its first press moved.
2026-11-02 13:53:08 -08:00
Sameh Khamis 497016b084 Add the changelog entry for launches, speed and buffering on the timeline 2026-11-02 13:17:28 -08:00
Sameh Khamis 724109ee17 Merge branch 'sameh/world-clock' into sameh/world-timeline-launches 2026-11-02 13:17:08 -08:00
Sameh Khamis 09d833ed96 Add the changelog entry for the world clock and timeline 2026-11-02 13:17:06 -08:00
Sameh Khamis 0d96fe6acd Merge branch 'sameh/world-clock' into sameh/world-timeline-launches 2026-11-02 13:14:32 -08:00
Sameh Khamis 32e08f5043 Merge remote-tracking branch 'origin/main' into sameh/world-clock
# Conflicts:
#	index.html
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-02 13:14:32 -08:00
German Khokhlov 4bd7f97cc4 Isolate globe renderer suspension from the switch cache 2026-11-02 22:42:40 +04:00
German Khokhlov 73ea608881 Hide map panels when entering the cockpit 2026-11-02 22:21:45 +04:00
German Khokhlov 2087b55b47 Open the cockpit on a tracked Navara aircraft 2026-11-02 22:04:23 +04:00
InoukT 72ceeb2fce Merge branch 'main' into fix/taiwan-highway-cctv-fallback 2026-11-02 12:43:17 -05:00
German Khokhlov 8b179a7e01 Keep Navara cinematic clouds visible at flight altitudes 2026-11-02 21:27:29 +04:00
German Khokhlov 96ce795f8c Keep Navara terrain visible on night-side descents 2026-11-02 20:58:17 +04:00
Sameh Khamis 056b3b58c6 Wait for each frame while the world timeline plays, and quiet the bar
Play no longer runs ahead of the data. The world clock gains a wait, which
holds continuous play where it is while a layer loads what the time needs,
and a gate, which keeps play from passing a time a layer has yet to load.
Weather gates play at its next frame and waits there until the frame shows,
so every frame is seen however fast play runs; it waits no more than eight
seconds on a frame that never comes, and takes a wait only when its frames
change. Play pressed while a frame loads waits for it too. A scrub still
drops what loads and moves on.

The bar loses its long date and the dates at its ends: the time rides in a
bubble over the handle, which pulses while play waits, and the bar runs
left to right in every language. Play starts at real time.
2026-11-02 08:03:46 -08:00
Sameh Khamis d04e304502 Choose how fast the world timeline plays
A speed menu beside play offers real time, an hour, a day or a week a
second, a day a second to start with. A speed chosen while playing applies
at once; chosen while weather's own replay steps its frames, it takes
playback over from where it is. Play starts again from the top only at live
or at the very end, so a pause just before the end plays on. On phones the
menu sits beside the track, a full-size target.
2026-11-02 07:41:50 -08:00
Sameh Khamis e7973e81cf Stop zooming out while the globe is still in view
Cesium sets no limit, so zooming out left the globe a speck among the stars.
On the globe a hand now stops zooming out where the globe spans 30% of the
screen's height, which follows the screen's shape as the app's own
whole-globe view does. A camera already past it, after a flight or a turned
screen, stops there rather than jumping in at the next zoom. Flat maps keep
zooming out unlimited.
2026-11-02 07:27:53 -08:00
Sameh Khamis 0aa54dfa97 Bring launches up as the world time passes them, and play the whole track
Away from live, a launch shows from its lift-off on: its marker, label,
paths and orbit stay hidden while the world time is before it, so scrubbing
or playing brings the launches up in the order they flew. The selected
mission stays whenever it flies, and live shows every launch as before.

Play on the timeline runs the whole track in about a minute, from its start
when at live, and runs the clock itself past weather's own replay. It goes
live when it reaches now, or holds at an end that lies ahead of now. Going
live puts the rate back to 1.
2026-11-02 07:21:01 -08:00
InoukT 16965c4fbc docs(changelog): add Taiwan CCTV fallback entry 2026-11-02 10:00:13 -05:00
InoukT ed2aaa2c50 fix(cctv): add Taiwan Highway Bureau fallback 2026-11-02 09:58:57 -05:00
German Khokhlov 3a2a53aef9 Anchor Navara inspection sunlight to the restored view 2026-11-02 17:26:34 +04:00
German Khokhlov 0d15e80e28 Keep Navara satellite daylight through orbital zoom 2026-11-02 17:11:42 +04:00
German Khokhlov 3cbc17a926 Match Navara satellite landscapes to the Re:Earth Fuji view 2026-11-02 14:05:58 +04:00
German Khokhlov 5e658d8c9d Keep globe renderers warm across live engine switches 2026-11-02 13:45:23 +04:00
German Khokhlov f71c76c306 Restore engine-specific Cinematic sharpness across live switches 2026-11-02 13:19:21 +04:00
German Khokhlov b8f4d097c4 Merge main and switch Cesium and Navara in place through Vue
Preserve the camera, map source and layer selection across engine sessions. Restore the previous engine if startup fails and retain local history navigation.
2026-11-02 13:10:36 +04:00
Sameh Khamis 48093108b1 Make the world timeline a scrubber under the dock
The timeline shows whenever a layer with time to show is on, live or not.
Its track runs over the time those layers cover, reaching to now, with a
tick for each launch or frame and a handle to drag; arrows step between the
ticks, and a scrub to now goes live. The bar sits along the bottom of the
screen, and the dock and everything anchored above it rise by its height;
clean view, recording and the cockpit put it away with the dock.
2026-11-01 23:37:12 -08:00
German Khokhlov 1da65dc264 feat(ui): switch between Cesium and Navara in Visual Presets
Remember the chosen engine and restore the current view after navigation. Keep the standalone JavaScript UI and retire the Dioxus migration and Cesium removal milestones.
2026-11-02 11:29:55 +04:00
Sameh Khamis 9faec3028e Show the world time on the HUD clock
The HUD's UTC and collection readouts follow the world clock with the sky,
so away from live they read the time the globe is drawn at.
2026-11-01 23:17:53 -08:00
Sameh Khamis 3534398771 Move satellites, launches and the sky with the world time
Layers whose data has a time of its own follow the page's world clock from
the start; live feeds such as flights, military, vessels and transit stay
live. Launches register the span of the launches loaded, so turning them on
brings up the timeline. Each timed layer registers its coverage once and
reports none while off, telling the clock after each step has settled. The
launch feed keeps its own window, and an orbit ring is drawn again once the
world time has moved far from when it was drawn.
2026-11-01 22:53:51 -08:00
Sameh Khamis 690a6637e2 Show the timeline only while a layer with time to show is on
The bar now needs more than the world clock being away from live: a layer
that moves with the clock must be on with time to show. In Core that is
weather radar, satellite or lightning with frames; a host adds layers by
following them. Turning the last one off hides the bar; turning it back
on brings it back.

The world clock reports this as `supported` in its state. Weather products
register their frames as coverage one by one, suspended or not, so the way
back to live stays on screen with reduced motion. Weather refreshes and
satellite catalog changes tell the clock their coverage may have changed.
A coverage that throws counts as none, and the clock reads its state once
per notification round.
2026-11-01 22:36:45 -08:00
Sameh Khamis 434fc9602d Show a basic timeline while the world time is away from live
A bar above the command dock, like a video player's: play and pause on
the left, the world time's date and time in UTC beside it, and Live to
return. It stays hidden while live, so Core looks as before until weather
history is scrubbed or replayed, a host moves the clock, or a link made
away from live is opened. Adds the pause and play_arrow glyphs and the
strings in every catalog.
2026-11-01 21:55:33 -08:00
Sameh Khamis 32ec6ed436 Share links carry the world time when made away from live
Field 28, worldTime, holds the world time of a link made away from live:
zigzag seconds since 2026-01-01, like field 8, but it may lie ahead of the
link's creation (a forecast moment). Opening such a link pauses the world
clock at that time; a link without it leaves the clock alone, so a page
opens live and Back or Forward does not stop a replay. Times no JS Date
can hold are dropped.
2026-11-01 21:50:09 -08:00
Sameh Khamis 0c4da3cdb2 Keep one world clock that every timed layer reads
Core told time eight ways: Date.now() in most layers, JulianDate.now() for
flights and military, Cesium's clock frozen at page load for the sky, the
weather clock for observed imagery. They now read one clock,
src/time/worldClock.js, exported as ./time.

Live is a mode, not a timestamp: while live the clock is the wall clock,
so every layer draws as before. Away from live only the layers that follow
the clock move with it. The weather products follow it, as they followed
the weather transport; a host opts others in with follow(id).

- The weather clock takes its time from the world clock and drives its
  replay, stepping its frames as before.
- Satellites, launches, flights and military (still 30 s and 15 s behind),
  transit, GNSS, Recent Imagery, street level, the HUD and the celestial
  ring read worldTimeFor(id). Cadences stay on the wall clock.
- Cesium's sky (stars, sun, moon, atmosphere, lighting) draws at the world
  time each frame instead of the moment the page loaded.
- Weather frames and satellites declare coverage() for a host's timeline.
2026-11-01 21:47:32 -08:00
German Khokhlov dc7a7c4d53 perf(navara): skip redundant empty-scene resolves
Preserve pending clears, backgrounds, callbacks, shadows and real geometry. Verify exact pixels and the underground depth-clear path; add controlled original-render comparison modes.
2026-10-30 17:24:29 +04:00
German Khokhlov 15f1f9f5b6 perf(navara): submit stencil stages in one render
Preserve the native stencil draws and geometry MSAA while avoiding two intermediate MRT resolves per volume. Verify original pixels, holes, overlaps, ownership and controlled comparison modes.
2026-10-30 14:42:31 +04:00
German Khokhlov c76872dfaa perf(navara): resolve MSAA at geometry stages
Keep four-sample geometry and depth while using single-sample postprocessing buffers. Preserve forward and overlay coverage, skip empty transfers, and add matched pixel and regression checks.
2026-10-30 12:52:50 +04:00
German Khokhlov 8279af2e6d test(navara): record keyless W2 comparison and acceptance gaps 2026-10-29 21:39:20 +04:00
German Khokhlov 0df0fb8261 fix(navara): separate always-visible shader programs 2026-10-29 20:41:13 +04:00
German Khokhlov 8328aab779 fix(navara): batch contours and order ground overlays
Batch consecutive disjoint installation outlines to avoid three stencil draws
per feature. Preserve overlapping volumes, material order and local precision.
Delete the public effect handle correctly during teardown.

Keep imagery below ground classifications and glyphs in every activation order,
including minimal mode without tone mapping. Exclude fully offscreen ground
volumes from the screen candidate grid.

Full unit/allocation, boundaries, formatting and keyless builds pass. Browser
checks cover contour coverage, bounded colour differences, a missing-contour
negative control, real pass order, sprite alpha and FIRMS glows. Whole-wave
performance acceptance remains separate from this implementation checkpoint.
2026-10-29 20:27:24 +04:00
German Khokhlov 9ba5a3aba9 fix(navara): invalidate cached glyphs after context loss
Clear the display-resource signature used by static sprite reuse when the WebGL context is lost or restored. Cover both events and verify that caching resumes after the redraw.
2026-10-29 19:26:47 +04:00
German Khokhlov f59d134928 feat(navara): render clipped recent imagery comparisons 2026-10-29 18:30:49 +04:00
German Khokhlov 5a8bc33fbd feat(navara): render transit vehicles and ground trails 2026-10-29 17:51:17 +04:00
German Khokhlov b8560e5b26 refactor(transit): isolate native rendering from playback 2026-10-29 17:12:18 +04:00
German Khokhlov 694616d5c2 feat(navara): render CCTV media and calibration handles
Port camera markers, coverage, media planes, volumes and calibration to the
Navara renderer. Preserve shared source/media policy and add exact plane
picking, owned resource cleanup and idle hold handling.

Validate paired keyless dev/preview media and physical drags, native legacy,
engine contracts and full CPU/build gates. Record remaining shell, viewshed
browser and W2 performance gates; defer Dioxus and plan the transit port.
2026-10-29 16:53:27 +04:00
German Khokhlov 798d6636ab feat(navara): add finite visible-surface obstruction rays
Reuse retained tile triangles for CCTV's upcoming activation probe, with finite range, exclusions and honest misses. Correct nearest-hit ordering when the ray starts inside a mesh bound.

Fifteen focused surface tests and all full keyless unit/allocation, boundary, format and build gates pass. CCTV renderer integration and existing acceptance gaps remain documented.
2026-10-29 00:13:23 +04:00
German Khokhlov 93c71ffc86 refactor(cctv): isolate native rendering resources
Keep shared CCTV logic independent of Cesium, release the original renderer's resources, and guard retired initialization/media work. Preserve native marker, plane, volume, calibration and input behavior.

Record keyless dev/preview validation and retain unresolved shell idle/terrain and photoreal acceptance gaps. Routine QA builds use empty Google and Cesium credentials and block paid maps before navigation.
2026-10-29 00:06:22 +04:00
German Khokhlov f72bf6a774 fix(qa): keep routine map checks keyless
Record keyless launch instructions without changing app defaults.
Block Google tile and token fallback requests in routine CCTV checks;
add a guarded startup check for both engines.
2026-10-28 23:33:15 +04:00
German Khokhlov 311e178d0c refactor(cctv): isolate calibration handles and stop idle poll frames
Move native gizmo entities and input into an owned renderer. Restore the
captured camera-input flag, handle cancellation and duplicate presses, and
reject foreign handle identities. Preserve native geometry and picking.

Avoid no-op sync-chip mutations and let unchanged CCTV polls complete
without publishing scene changes. Keep health/UI polling and real geometry,
auto-hop and media updates. Extend browser idle samples across two polls.

Validated full Node24 unit/allocation, boundaries, scoped format, production,
panel and QA builds; native legacy parity, real media/GPU picks on dev and
preview, engine contracts and production shell. Retain failed evidence.
2026-10-28 22:44:27 +04:00
German Khokhlov a72cfc4098 fix(cctv): let unchanged image projections idle
Prove native geometry, real image and video pixels on dev and preview before renderer extraction. Scope fixture interception to API routes so geometry workers remain live.
2026-10-28 21:53:19 +04:00
German Khokhlov 46b3aceafc feat(alpr): render camera markers on Navara
Separate native scene ownership from shared ALPR source and selection state,
and add bounded Navara sprites with the existing direction/card overlay.
Preserve marker artwork, picking, floor retries and source attribution.

Fix lost post-render wakeups and redundant mobile rail mutations, and keep
HUD typewriter dimensions stable so static views settle without extra frames.
Add parser-fed dev/preview and legacy evidence, including retained failures.
Common W2 terrain and combined performance gates remain open.
2026-10-28 21:06:08 +04:00
German Khokhlov 9d587c1d37 feat(radio): render stations and clusters on Navara
Preserve native clustering, selection and direct-action playback through renderer ports. Record both-engine dev/preview, legacy, contract, shell and build evidence with remaining W2 limits.
2026-10-28 14:49:18 +04:00
German Khokhlov d82a6f1550 feat(bikeshare): render GBFS stations on Navara 2026-10-28 13:12:19 +04:00
German Khokhlov 1a694baf5a feat(installations): add Navara renderer 2026-10-28 12:18:03 +04:00
German Khokhlov 965a82144c refactor(installations): inject engine renderer adapter 2026-10-28 09:54:05 +04:00
German Khokhlov f4f0088814 refactor(installations): use globe camera contract 2026-10-28 09:50:17 +04:00
German Khokhlov c58bc9864b refactor(installations): make model engine neutral 2026-10-28 09:47:40 +04:00
German Khokhlov 5dd05a4a81 feat(firms): add Navara renderer checkpoint 2026-10-28 09:36:50 +04:00
German Khokhlov a9b3125a99 Extract the native FIRMS renderer and own its settle timer
Keep fire normalization, LOD, anchors, cards and selection portable while the native renderer owns ground cells, sprites, input and scene disposal. Cancel superseded/removed settle timers and unregister the original sprite collection.

Preserve all ten native raster pairs exactly in dev and preview; record source/click/focus/lifecycle, legacy Metal and full regression evidence with strict comparator sensitivity checks.
2026-10-27 14:40:08 +04:00
German Khokhlov a3152ab719 Add depth-classified earthquakes on Navara
Match native ground footprints, encoded alpha and source ordering on real terrain and photoreal tiles while excluding forward meshes. Cache static screen data and reduce large-disc edge work without changing GPU pixels.

Add deterministic lifecycle, source/share, foreground/horizon and three-context performance proof; restore credits for available Navara feeds and retain failure history in migration evidence.
2026-10-27 13:42:07 +04:00
German Khokhlov c89185f352 refactor(earthquakes): extract native renderer with identical ground-disc output
Keep refresh, cancellation and label policy engine-neutral; inject the original native entities at composition. Prove static geometry, analyst records, owner disposal and last-good refresh behavior. Native dev and QA-preview pass 18 checks each; all eight baseline PNGs remain byte-identical.
2026-10-27 10:56:41 +04:00
German Khokhlov e8870a4766 test(migration): establish W2 native oracle and release removed overlay identities
Add the sequential W2/completion plan and deterministic earthquake ground-raster, geometry, lifecycle and strict runtime QA. Prove the comparator rejects incomplete evidence and release deleted overlay entries from the collision arbiter. Record native dev/preview acceptance and full checkpoint gates.
2026-10-27 10:48:26 +04:00
German Khokhlov 0600964249 Complete phase 3 W1 acceptance and close T16 findings
Fix Navara model display tint, overlay ordering, follow gestures and resource cleanup.
Reduce repeated aircraft work and correct fixture enrichment responses.
Strengthen dev/preview QA, preserve failure history and document verified W1 results.

Validation: 7171 ordinary tests pass, one Windows-only skip; 15 allocation probes;
37 W1 acceptance steps; contracts, shell, four performance gates; 18 identical
Cesium baseline PNGs. Known pre-existing CCTV and live-source limits are recorded.
2026-10-26 23:23:25 +04:00
BreezeDelegate bbfb23a765 fix(ci): run allocation gates on Node 26 2026-10-25 03:15:11 +01:00
Said Turksever 11dd283712 Merge branch 'main' into codex/fix-tool-display-text 2026-10-24 23:43:46 +01:00
daikaginza 5f23c785ca Merge upstream main 9d3548c27 into fix/api-method-guard
# Conflicts:
#	docs/CURRENT-STATE.md
#	src/tooling/liveProviders.test.mjs
2026-10-24 13:21:09 +07:00
daikaginza 73f03771d8 Merge upstream main 9d3548c27 into feat/opensky-compress-cached-body
# Conflicts:
#	docs/CURRENT-STATE.md
#	src/tooling/liveProviders.test.mjs
2026-10-24 13:20:59 +07:00
Said TurkseverandClaude Opus 5.5 cbca6a228d fix(voice): keep canonical analyst IDs out of display-text normalization
compactAnalystItem normalized every string field, including `id`.
Bikeshare uses the raw station name as its ID, so a station with
repeated spaces or a name over 180 code units was registered as a
referent under a different key and a numbered fly_to_location reported
it as no longer loaded. Normalize display fields only; layerKey, id,
icao24, mmsi, noradId, registration and callsign pass through as the
layer reports them.

Adds an analyst_query -> referent -> fly_to_location test for
repeated-space and long station names, and documents the rule in
docs/CURRENT-STATE.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-23 12:31:18 +01:00
Said Turksever 445af0875b Merge remote-tracking branch 'origin/main' into fix/1280
# Conflicts:
#	docs/CURRENT-STATE.md
#	src/layers/alpr/records.js
2026-10-23 12:09:46 +01:00
daikaginza a160a9e953 Merge remote-tracking branch 'upstream/main' into feat/opensky-compress-cached-body
# Conflicts:
#	server/providers/aircraft/opensky.js
#	src/tooling/liveProviders.test.mjs
2026-10-23 13:30:47 +07:00
daikaginza 6a086cb16f Merge remote-tracking branch 'upstream/main' into fix/api-method-guard
# Conflicts:
#	src/tooling/liveProviders.test.mjs
2026-10-23 13:26:42 +07:00
Kevin Rajan deb02f635e Merge current upstream main into explicit cyclone context PR
Preserve reviewed selection event-ordering semantics and real-store tests.
Register the new app-layer wrapper in both package boundary sections.
2026-10-22 17:39:51 -05:00
German Khokhlov 766c786093 feat(launches): render on Navara 2026-10-23 01:19:57 +04:00
German Khokhlov 4baefeb8f9 refactor(launches): Cesium rendering behind a renderer interface 2026-10-23 00:32:10 +04:00
German Khokhlov 1839d62160 refactor(launches): geodesics on src/geo
Port native Vincenty geodesics and launch-path math with exact Cesium oracles.
Keep all existing launch path-test expectations unchanged.
2026-10-22 23:48:50 +04:00
German Khokhlov 6bdb995fe3 feat(satellites): render on Navara
Render satellite fleets and selected targets through mover banks and orbit lines.
Enable the celestial HUD on Navara and keep native follow explicitly in ENU.
Add deterministic interaction, share, orbit, motion and cleanup acceptance QA.
Record complete targeted acceptance and remaining shared cross-shell tile failures.
2026-10-22 23:41:14 +04:00
German Khokhlov 000ef99464 refactor(satellites): Cesium rendering behind a renderer interface
Move native points, synchronous orbit primitives, tracked entities, follow and picking into the Cesium renderer. Use appClock for satellite physical epochs and isolate the unchanged CelestialRing ephemeris. Synthetic core/dense and ISS/MEO/GEO snapshots match the baseline exactly; all gates pass. The pointer-ownership source assertion follows scene.pick into render/cesium.js; registry and import-boundary assertions now reflect the migrated satellite layer.
2026-10-22 22:37:09 +04:00
German Khokhlov 83fbe6949f fix(navara): disable redundant sun shadows in minimal look
Minimal inherited the sun light default of castShadow=true although the photoreal tiles carry baked lighting. Use the same no-shadow sun configuration as cinematic. The focused 3-run city gate improved from C4.6/N5.1 ms (1.1087) to C4.6/N4.9 ms (1.0652) with 12000 vessels. Full Node 24 tests and allocation gates, boundaries, format, build and panel build pass.
2026-10-22 13:38:22 +04:00
German Khokhlov c8a4f3ed98 fix(vessels): upload Navara chevrons and restore click focus
Fix delayed and growing mover bank commits, reject stale hidden model picks, and route world-focus through the engine. Strengthen vessel parity and performance checks without changing thresholds.
2026-10-22 12:31:59 +04:00
Said TurkseverandClaude Opus 5.5 d167cf0ccb Merge branch 'main' into codex/fix-tool-display-text
Resolve docs/CURRENT-STATE.md (and scripts/format-scope.json) by keeping both sides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 15:04:50 +01:00
Said Turksever c6decfced4 Merge latest main before publishing PR #847 fixes 2026-10-20 14:42:17 +01:00
Said Turksever b358b5dba1 fix(cyclones): distinguish deliberate clears from feed eviction 2026-10-20 14:17:39 +01:00
Said Turksever 1330605c18 Merge main into PR #847 preparation candidate 2026-10-20 14:14:38 +01:00
Kevin Rajan f05adb1f03 Merge current main into viewport diagnostics PR (#835)
Retain #837 pointer-crop behavior and #835 static escalation reason
codes, while preserving merged upstream voice controller changes.
Keep earlier independent reviewer provenance.
2026-10-20 01:49:23 -05:00
Said 3685c416f3 docs: record text fix 2026-10-20 01:21:41 +01:00
Said 586ed4f8ea fix(tools): bound untrusted display text at output boundaries 2026-10-20 01:21:04 +01:00
Said Turksever 9a5cafca27 Merge upstream/main into exp/cyclone-shared-context 2026-10-19 14:23:54 +01:00
daikaginza 6977770067 docs(changelog): move the entry into a changelog.d fragment
Main now takes pending entries as changelog.d/<pr>-<slug>.md
files (#787), so the entry leaves CHANGELOG.md for changelog.d/795-opensky-compress-cached-body.md.
2026-10-18 13:36:42 +07:00
daikaginza 2b7c4d1c65 Merge upstream/main 93ea088f4 2026-10-18 13:36:42 +07:00
daikaginza d3370d6bc4 docs(changelog): move the entry into a changelog.d fragment
Main now takes pending entries as changelog.d/<pr>-<slug>.md
files (#787), so the entry leaves CHANGELOG.md for changelog.d/794-api-method-guard.md.
2026-10-18 13:36:40 +07:00
daikaginza 1133e6327a Merge upstream/main 93ea088f4 2026-10-18 13:36:40 +07:00
German Khokhlov 69a78710cc feat(vessels): render on Navara (WIP: browser parity not yet run)
render/navara.js: one always-on-top engine.movers chevron bank (atlas of
every chevron icon at 32 px), MMSI pick ids, trail on engine.lines,
camera reads through the shared Navara layer view (extracted from the
aircraft renderer to src/layers/shared/render/navaraView.js, + camera
pickEllipsoid and canvas clientWidth). Registered in navaraMode.
QA: vessel qaDrawn; probe selected() falls back to getSelectedInfo;
qa-w1-layers vessel section; qa-vessels-engines side by side; --engine
for qa-vessel-cards/datum; qa-flights-engines military near pose 100 km.
Unit tests on the fake engine pass; browser runs NOT done (see progress).
2026-10-17 14:06:11 +04:00
German Khokhlov 620ada6df3 fix(globe): Navara picks rank as drawn and check model hits at their drawn radius
T11 review follow-up to ed9ea7e6. CPU hits rank as the engine draws them:
sets without a depth test over depth-tested ones, then by set order
(the tracked bank over the fleet, flights over local ADS-B), then depth —
so the CPU's top hit that checks a GPU answer is the instance drawn on
top. A model hit is no longer taken as answered: the model mover keeps
each instance's drawn diameter (after minimumPixelSize/maximumScale) and
the CPU checks the hit against its projected position within that radius
(+3 px); the CPU pick sees models at that radius too.
2026-10-17 13:26:42 +04:00
German Khokhlov 1fe8267550 refactor(vessels): Cesium rendering behind a renderer interface
src/layers/vessels/render/cesium.js holds the Cesium objects the layer
built itself, one for one: the translucent BillboardCollection of
chevrons (centred, screen-aligned, never depth-tested), the horizon
occluder, window projection, canvas size, viewer.trackedEntityChanged,
and the trail (data/trailRenderer's entity polyline, kept on Cesium: the
engine.lines Primitive path did not draw below the photoreal mesh in
round 6 — the plan's T13/T15 blocker; flights does the same).
The logic imports no Cesium: geo math, Rgba WHITE, the chevron artwork in
icons.js, clicks through the engine's selection dispatcher (own-rule
owner, same rule). Registered as CESIUM_LAYER_RENDERERS['ais-live-vessels'];
src/layers/vessels joins the boundary rule.

W1 replay vs 7c7422e2: vessels/flights/military/localAdsb counts, known
objects, screen points, requests and shot sha256 identical.
qa-vessel-cards --data synthetic 2/2 and qa-vessel-datum (?qaFeed=
vessels:fixture) 4/4, the same as 6eec8f6c.

Tests moved with the code (same meaning): cameraHandoff (the click body
is handleClick), inputOwnership (the guard precedes the click rule's
read of the pick), renderers.test (vessels in the extracted list),
importDirections (an unmoved example layer is satellites), the
ownership services fixture (renderer service).
2026-10-17 12:47:14 +04:00
German Khokhlov 6b27b21cc4 feat(data): an own-rule selection owner keeps its whole old click handler
Vessels' click rule (sibling ownership, cyclone background, card hit
test, unkeyed records) does not map onto the plain or look-under rules:
an owner with onClick(ctx) gets every click with the top pick, no gesture
rule (its old LEFT_CLICK handler), and is another layer to the rest.
2026-10-17 12:47:14 +04:00
Kevin Rajan 0e2ae76f0a test(cyclones): assert source-owned context label 2026-10-17 03:36:09 -05:00
Kevin Rajan 3a7c7b7c6a fix(cyclones): keep selected names source-owned for i18n 2026-10-17 03:36:07 -05:00
Kevin Rajan abaaf0c254 fix(cyclones): reconcile explicit shared selection with current main
Carry reviewer-verified stale A-to-B record eviction, explicit selection
and refresh semantics over latest i18n/cyclone source ownership.
2026-10-17 03:31:28 -05:00
German Khokhlov 6eec8f6cb0 fix(qa): floor verification finds a contact's model by its icao24
getNearby returns the contact's label as `id` (the callsign when it has
one) and its key as `icao24`; the drawn rows are keyed by icao24, so the
model lookup missed and every contact was measured at its sprite. Under a
3D model that sprite is hidden and keeps the pre-floor height by design
(T7): the "buried at -27 m" contacts on Cesium (the geoid at AUS) were
those hidden sprites, at HEAD and on 7c7422e2, whose script measured every
sprite. The Cesium report also gives each contact's model state and the
floor cell the layer reads.

AUS, 100 s after enabling flights, both engines: 3 low contacts, 0 buried;
N3752 / DAL848 drawn by their models at 130.2 / 135.4 m over a 123.5 /
128.6 m mesh, the DEM-prior cell warm (123.5 / 128.6 m).
2026-10-17 12:12:00 +04:00
German Khokhlov 71b7177dc0 fix(globe): Navara model tints are decoded from sRGB before mixing
The instance colour (sRGB, as Cesium takes colours: the military amber
#FFB800) was mixed into the linear material colour as is, so a tinted
model drew paler and brighter than Cesium's: the military footprint gate
read 124 % (more pixels cleared the contrast threshold over water) and
the median luminance +9 %. White (flights) is the same either way, which
is why the flights gate passed. The tint is now sRGBTransferEOTF-decoded,
as the sprite sets already decode theirs.

qa-flights-engines' model gate adds the highlights: the p90 luminance of
the model pixels within ±10 % of Cesium, with the near-white pixel count
reported (10b review low 2).

Models on, near pose (computed scale ratio ~1.0000 on both):
- military (from 100 km): footprint 9,902 vs 8,610 px (115 %), median
  luminance 158.9 vs 155.3, p90 190.9 vs 184.5 — 18/18;
- flights: footprint 104 %, median 189.8 vs 196.7, p90 223.1 vs 247.8
  (near-white px 83 vs 510: Cesium's white models saturate where Navara's
  tone mapping rolls them off) — 20/20.
Models off: flights 16/16, military 15/15. Side-by-sides regenerated.
2026-10-17 11:28:57 +04:00
German Khokhlov 5ff8c5d7a6 test(qa): floor verification reads the app's ground floor and measures models where drawn
- The DEM prior column read a fresh import of src/data/groundFloor.js, a
  module the app does not use for its layers (they read the scene's
  surface services), so it was null for every contact. It now reads
  window.__godsEyeView.surfaceServices.groundFloor. The terrain proxy
  answers 200 and the AUS cells resolve within one or two polls (123-128 m);
  the "proxy 503" reading in the task-11 report was this misread.
- A contact a 3D model draws is measured at the model's translation (the
  probe's drawn() reports it now): its sprite is hidden and keeps the
  pre-floor height by design (T7), so measuring the sprite reported a
  buried aircraft that is not drawn.
2026-10-17 11:02:52 +04:00
German Khokhlov ed9ea7e679 fix(globe): a Navara click never takes a stale GPU pick of a neighbour
Navara 0.2.1 can answer a click with the previous gesture's pick. The
check let that answer through when its instance was drawn within twice its
pick radius of the click — the followed aircraft at the centre beside the
clicked one (the 20/24 flights checklist run: a click selected the
previously followed aircraft). A GPU answer for a sprite or point set now
has to be drawn under the click (no slack) and be the instance the CPU
pick finds on top there (lines left out: they are not in the GPU pick);
otherwise the CPU pick answers. Models, which the CPU pick does not see,
are still taken as answered. The CPU drill takes `includeLines`.

Test: 20 gestures alternating a click on A and a click on its neighbour B
answered with A's stale pick — B every time. In the app, 40 scripted
click/follow/release gestures on Navara selected the clicked aircraft
every time with and without the fix (the stale answer is rare).
2026-10-17 11:02:52 +04:00
Kevin Rajan cb3b66e38b style(voice): apply exact Prettier 3.9.6 output to #835
Restore unchanged formatter script after ephemeral CI verification. Keep current-main-compatible reason instrumentation, pointer crop and capture cancellation tests.
2026-10-16 23:41:12 -05:00
Kevin Rajan d4980b1b77 test(voice): extract pinned formatting for #835 review repair (temporary) 2026-10-16 23:39:42 -05:00
Kevin Rajan 050f68ac03 test(voice): classify current-frame send and cancellation outcomes
Keep viewport and pointer-crop capture unchanged, but expose static reason codes for unavailable frames, invalidated turn captures and retained image sends. No image or user text in diagnostics.
2026-10-16 23:37:22 -05:00
Kevin Rajan 8c02f5852f fix(voice): preserve pointer crops in viewport escalation diagnostics
Reconcile origin #835 with merged voice core #837. Classify pointer crop separately from full screenshots, retain existing session/camera freshness guards, and log bounded static reason codes. Add dispatch regressions and changelog fragment.

Credit: asturksever identified the incompatibility; preserve #837 authorship.
2026-10-16 23:36:00 -05:00
Kevin Rajan a803be1af2 fix(cyclones): evict prior shared selection on storm switch
Regression: explicit storm A→B→A keeps one active record.
Reviewer finding: quanticsoul4772 (#847).
2026-10-16 23:01:44 -05:00
German Khokhlov ea1e2f9c04 feat(localAdsb): render on Navara
Task 12b. src/layers/localAdsb/render/navara.js implements the Local
ADS-B renderer interface of 12a on Navara:
- markers: the shared aircraft renderer's glyph sets in magenta #ff4fd8
  (class silhouette and scale, ×3 near / ×0.5 far, always on top), drawn
  under every sprite layer as Cesium draws the entities; each frame writes
  what Cesium's CallbackProperties gave (position, screen rotation,
  visibility) and the 2,000 km display distance;
- the 978 MHz UAT ring: its own sprite set, a white ring atlas tinted
  #ffb3ef, Cesium's 24 px point plus its 1.5 px outline (27 px), the same
  distance scale and distance, pickable as its aircraft;
- models (the Flights GLBs, magenta MIX tint), trail and head: the shared
  aircraft renderer's; the camera never follows a local aircraft.
Registered by registerNavaraLayerRenderers (import()); the Navara catalog
now offers flights, military and local ADS-B.

- ?qaFeed=local-adsb:fixture: the local receivers' answer in the server's
  format (src/bench/w1Feeds syntheticLocalReceivers: 40 aircraft near JFK,
  10 on 978 MHz UAT) through the layer's own decoder-feed session
  (sources.localAdsbFeeds, null by default: the layer makes its own).
- qa-w1-layers --layer=local-adsb: a click selects a marker and shows its
  card with the receiver line, the camera does not follow, Escape leaves
  the selection (it never answered Escape), empty map releases it.

qa-w1-layers local-adsb: Cesium 12/12, Navara 13/13, --compare parity
(global pose: the 2.27 px camera offset only). qa-flights-engines
--layer=local-adsb: 14/14 (fills exact; UAT rings side by side in
docs/migration/phase3/localAdsb).
2026-10-17 06:50:15 +04:00
German Khokhlov 65e30546dd fix(globe): Navara sprites keep a full rewrite when a commit follows it
A sprite/point set uploads its instances by update range. When the camera
went far enough to move the set's origin, update() rewrote every instance
(relative to the new origin) — and a commit later in the same frame
cleared the ranges and kept only its own, so the GPU never got the
rewrite: the instances not committed again drew offset by the origin move
(a military glyph at the orbit pose drew 45 px from its point after the
global → city → orbit sequence; the CPU picks, the probe and Cesium had it
in place). Ranges now add up until three uploads them (it clears them
after the upload).

Found by the military side-by-side (11b); flights' sprites had the same
exposure since 10b.
2026-10-17 06:49:53 +04:00
German Khokhlov 0b8cb855fa refactor(localAdsb): Cesium rendering behind a renderer interface
Task 12a. The Local ADS-B logic (index.js, models.js) no longer imports
Cesium. src/layers/localAdsb/render/cesium.js holds what it built, one
for one: the CustomDataSource of marker entities (billboard with
per-frame position/rotation/visibility CallbackProperties, the 2,000 km
DistanceDisplayCondition, the 978 MHz UAT ring as a PointGraphics), the
model PrimitiveCollection (models load, pose and leave through the shared
aircraft renderer's model part, adopting that collection), the selected
aircraft's trail (data/trailRenderer, as before) and live head segment,
and the camera reads. Registered for 'local-adsb' in the renderer
registry, created at init(viewer, {engine}).

- Clicks go through the engine's selection dispatcher (drill 8) instead of
  the layer's ScreenSpaceEventHandler, with the layer's own rule kept as is
  — a new `lookUnder` owner kind: its marker on top, or under picks no
  layer owns and under trail lines, is selected (and re-selected);
  another layer's contact on top, or empty map, releases only its own
  selection; a trail line alone does nothing; no gesture rule. To the
  other owners it is another layer, as its pick-owner registration made
  it before. (The dispatcher's `drillThrough` would have taken the marker
  from under another layer's contact, which the old handler never did.)
- models.js reads the camera through the renderer's view (frustum and
  horizon tests, ground snap owner) and draws through renderer.models
  (loadFleet/add/remove/destroy/setPose: the same MIX-tinted glTF options,
  the same HPR frame with the Flights nose offset). The shared aircraft
  renderer gains a loadModel seam (default Cesium.Model.fromGltfAsync).
- Cesium math → src/geo; the model tint is an Rgba equal to the old
  Cesium.Color; check-import-directions: src/layers/localAdsb/ joins the
  rendered layers.

Tests moved with the code, same meaning:
- index.test: the services fixture creates the layer's Cesium renderer and
  the selection dispatcher; the click harness scripts the picks on a fake
  engine registered for the fake viewer (was a fake ScreenSpaceEventHandler)
  and the app's pick owners (trails, a Flights contact) are registered in
  the pick registry (was an injected picking service).
- cockpitModels.test: the GLB loads are the aircraft renderer's loadModel
  calls (Cesium.Model.fromGltfAsync by default), each asynchronous:false.
- trackingClickGesture.test: the dispatcher's empty click goes to the
  plain owners (filter(plain)).
- renderers.test: local-adsb has an extracted Cesium renderer.
New: the dispatcher's look-under rule beside a plain owner (7 click cases).

W1 replay (flights, military, localAdsb) vs a fresh 7c7422e2 run: counts,
known objects, screen points, tracking poses, requests and every shot's
sha256 identical.
2026-10-17 05:50:30 +04:00
German Khokhlov 941b308c29 test(qa): floor verification logs each contact's probe answer and DEM prior (10b review 6)
On Navara each contact row now carries the surface probe's source and tile
size and the coarse DEM floor (cachedGroundFloor). The buried contacts
(SWA4871 60.5 m and AAL2592 -27 m against a 121-126 m mesh) have no DEM
prior (null: the terrain proxy answers 503 here) and the mesh floor sampler
latches nothing without one, so their sprite heights stay the barometric
fallback; the same rows bury on Cesium when the same contacts are low.
2026-10-17 05:34:41 +04:00
German Khokhlov 10d0e49ddb fix(globe): Navara aircraft models face, size and light as Cesium's (10b review 3)
The near-pose finding (Navara models faint, Cesium ~23 bright aircraft)
came from the side-by-side's frozen page clock: the fleet tick ran once per
pose, so Cesium never handed its aircraft to their models and drew 3×
glyphs, against real models on Navara. With the hand-off driven (the move
ends every 10 frames while a pose loads), both engines draw models, and
the comparison showed three real differences, all fixed:

- Orientation: Navara's glTF loader is three's GLTFLoader, which keeps
  the scene Y-up; the instance turn is now Cesium's whole axis correction
  (Y_UP_TO_Z_UP × Z_UP_TO_X_UP), not Z_UP_TO_X_UP alone. Every fleet model
  drew 90° off its heading.
- Size: minimumPixelSize is taken at Cesium's distance (Camera
  distanceToBoundingSphere: view depth less the radius at the model's own
  scale), in the mover and in ModelHandle.computedScale; the straight-line
  distance drew off-centre models up to 16 % larger.
- Colour: the instance colour tints by the set's blend amount (Cesium's
  colorBlendAmount) and its alpha is the model's opacity (Cesium: a
  model colour's alpha makes it translucent), not a blend weight; a model
  is translucent only while faded. An ambient share of the tinted surface
  (MODEL_AMBIENT 0.5, Cesium's image-based lighting) keeps a tinted
  aircraft near-white at a low sun (median luminance 139 → 190; Cesium 197).

qa-flights-engines (now --layer=flights|military) gates the near pose
with --models=on: computed scale within ±5 % per model (ratio 1.000007),
footprint against the same view with the layer off within ±20 % (1553 vs
1496 px, 104 %), median luminance within ±10 % (189.8 vs 196.7). The probe's
drawn() reports a model's computedScale. Flights models on: 19/19.
2026-10-17 05:23:50 +04:00
German Khokhlov 9dc446bd24 feat(military): render on Navara
Task 11b. src/layers/military/render/navara.js: the shared Navara aircraft
renderer with the military's sprite slot (SPRITE_LAYER_ORDER 'military',
under flights) and jet.glb's nose offset; registered by
registerNavaraLayerRenderers (now a table of lazily imported factories,
flights and military), so the Navara catalog offers military and the main
chunk stays without three/Navara.

- The follow passes between aircraft layers on Navara as on Cesium: the
  follow-handle → tracked-handle map and the "follow being started" handle
  are shared by every aircraft renderer of the page, so a layer whose follow
  another layer took reads the new owner's tracked handle (and its
  gevSelectionOrigin) — before, it read a bare follow handle and reported
  'programmatic'.
- trackById on a selected-but-not-followed contact follows it again (as
  flights since 10b; on Navara any camera command ends a follow).
- qaDrawn (the probe's drawn()) for military, shared with flights
  (src/layers/aircraft/qaDrawn.js).
- ?qaFeed=military:synthetic[:count]: seeded readsb rows (1 000 by
  default, 150 near JFK, 12 on the ground, 8 types), the first 25 hexes
  shared with the synthetic civil flights.
- qa-w1-layers: sections 4–6 read the --layer's module (were 'flights');
  section 5 keeps the queries beside the voice answers (the voice block
  overwrote them, so --compare never compared queries); for military: the
  follow passes military → flights → military, and flights leaves the 25
  shared aircraft to military while it is on (9 975 / 10 000 / 9 975).

qa-w1-layers --layer=military: Cesium 25/25, Navara 26/26, --compare parity
(queries included; the known 2.3 px camera offset at the global pose only).
2026-10-17 05:05:40 +04:00
German Khokhlov 3a0a3b0551 test(military): the colour constants equal the Cesium.Color values they replaced 2026-10-17 04:52:33 +04:00
German Khokhlov 7c0891deda refactor(military): Cesium rendering behind a renderer interface
Task 11a. The military flights logic no longer imports Cesium: it draws
through the shared aircraft renderer (src/layers/aircraft/render/cesium.js)
with the military's parameters in src/layers/military/render/cesium.js
(sprite-order slot 'military', jet.glb's 180° nose offset), registered for
'cesium' in the renderer registry and created at init(viewer, {engine}).
The same pure move as flights in 10a:

- Fleet: the BillboardCollection and its sprite-order slot (enable keeps
  restoreSpriteOrder, as before); sprites stay Billboard handles.
- Models: fleet/tracked/preload glTF loads (asynchronous:false, MIX tint at
  MODEL_COLOR_BLEND_AMOUNT, IR boost as `unlit` → the UNLIT CustomShader),
  the model collection, setPose. The per-class specs keep their own nose
  offset: setPose takes an optional offsetDeg (default the layer's), on
  both aircraft renderers (same arithmetic, test added).
- Tracked: the ENU billboard Entity with its position/colour/rotation
  callbacks (#FFD166, transparent amber once the model owns the visual),
  viewFrom, followed through viewer.trackedEntity + the one exact tracked
  frame; refocusTrackedById through follow.refocus.
- Trail: the entity trail and the floored live head segment.
- View: camera/scene reads, frame number, frustum and horizon tests,
  render requests, frame events.
- Clicks and Escape go through the engine's selection dispatcher (the
  ScreenSpaceEventHandler and keydown listener are gone); the cross-layer
  untrack watches the renderer's follow. Same rules: cockpit clicks are
  inert, the tracked visual/model is a no-op, another layer's pick is not
  empty space, an unowned pick is.
- Colours are engine-neutral Rgba (MIL_ICON_COLOR #FFB800,
  TRACKED_ICON_COLOR #FFD166, AMBER_TRANSPARENT), NearFarScalar →
  nearFarScale; Cesium math → src/geo.
- check-import-directions: src/layers/military/ joins the rendered layers
  (cesium only in render/cesium*.js).
- The military registry and the flights' duplicate suppression are
  untouched.

Source-text tests updated with the moved code, same meaning:
- militaryAwareness.test: military's hasContact guard reads fleet.visible().
- trackingClickGesture.test: the military gesture rules are the
  dispatcher's (selection gesture before select, click classifier before
  the empty-space clear), military answers the empty click with a
  user-origin clear — the same assertion flights already had.
- ownership.test: the services fixture names selection and renderer; the
  app-composed services build the layer's Cesium renderer.
- renderers.test: flights and military have extracted Cesium renderers.

New tests: the military logic over a plain-data renderer and a fake engine
(amber fleet, click-select, #FFD166 tracked glyph, no-op on the tracked
visual, empty-click and Escape release, the keyboard left on destroy); the
plain-data renderer moved to src/testSupport for both aircraft layers;
setPose with a per-model offset.

W1 replay vs 7c7422e2 (the 10a reference run): every layer's counts, known
objects, screen points, tracking pose after 2 s, requests and the
city/global/orbit shots (sha256) identical. qa-military-names (it checks
the installations layer): the same verdict as 7c7422e2 (the zoom-markers
assertion fails on both). Main chunk 2,890,763 B (−2.9 KB).
2026-10-17 04:52:15 +04:00
German Khokhlov e007350645 fix(ui): explicit navigation releases flights when an engine leaves out military awareness
On Navara the military-awareness module has no controls, so its
releaseCameraOwnership was skipped silently and a city pill, voice
fly_to_location or reset ended the follow but kept the aircraft selected.
The trackers it would release are released directly instead, as on
Cesium (selection and follow both cleared).
2026-10-17 03:59:51 +04:00
German Khokhlov 2da7b6d0d9 fix(ui): north-up and tilt keep an engine's own follow (Navara)
Inside a Navara follow the orientation controls frame the follow's target
and retarget its viewFrom instead of moving the camera, and the release
cancels only a running flight (a cancel is a camera command, which ends
the follow). Cesium's trackedEntity path is unchanged. Updated test:
cameraOrientationControls.test.mjs (orientation navigation starts a
flight before expecting its cancel; no flight, no cancel).
2026-10-17 03:48:12 +04:00
German Khokhlov dcfb1219d5 feat(globe): a follow handle names its target
FollowHandle.target() returns the followed point, checked by the contract
on both engines. The contract's model check now waits for ready() before
looking for the model's pixels: the magenta point and sprites could fill
the region before the glTF loaded (qa-globe-contract cesium 73/73,
navara 79/79).
2026-10-17 03:48:11 +04:00
German Khokhlov ba1191b2e5 test(qa): floor verification on both engines
qa-floor-verify --engine=cesium|navara: on an engine without a viewer the
camera pin, the tile readiness, the mesh probes (engine.surface
sampleHeightDetailed, tiles only, excluding the layer's mover sets) and the
sprite census (the probe's drawn()) go through the engine. The Cesium path
is unchanged. Live AUS traffic: Cesium FAIL as on 7c7422e2 (13 contacts at
the -27 m geoid fallback); Navara FAIL with 1 of 15 low contacts under the
mesh (68.6 m against 121.4 m). Task 10b, checklist item 7.
2026-10-17 03:20:44 +04:00
German Khokhlov 132a941e9a test(qa): sprite and enrichment checks on both engines through the probe
The QA probe gains drawn(layerId): what a layer draws per object (glyph
image, scale, shown, screen rotation, alpha, position; its 3D model's
scale, shown, ready), from the flights layer's qaDrawn(), which reads the
renderer's handles, so it is the same on every engine.

qa-sprites-b5 and qa-enrich-ambient take --engine=cesium|navara: without a
viewer they read the layer through the probe and move the engine camera;
the military rows/cluster are skipped where the engine has no military
layer. Cesium verdicts unchanged (sprites 9/9; enrich 9/2, the same E8/E10
failures as on 7c7422e2); Navara: sprites 7/7 (flights rows and 3D cluster),
enrich 9/2 (the same two). Task 10b, checklist item 7.
2026-10-17 03:15:12 +04:00
German Khokhlov 03a0ff3a01 test(qa): flights parity checklist on both engines, side by side
Task 10b:
- qa-w1-layers (flights): 3 focus (the selected object is the focus target,
  at the followed centre ≤ 3 px), 4 tracking (held ≤ 3 px for 5 s; wheeled
  in, ≥ 149 m), 5 the layer's queries (tracked info, contacts, nearby,
  detectable; compared by --compare), 6 share links (the followed object
  restored on the same and on the other engine). Clicks only on points the
  canvas takes (the Navara attribution card covered the old empty point).
  --compare reports the global pose's 2.3 px as the cameras' (Cesium
  projects its own nadir 2.27 px off centre at 15,000 km).
  Cesium 19/19, Navara 20/20, parity.
- qa-flights-engines (new): the same frozen synthetic fleet at the global,
  city, orbit and near (JFK, oblique) poses on both engines, UI hidden;
  side-by-side JPEGs (docs/migration/phase3/flights, models off and on),
  the same objects in the frame, screen points ≤ 2 px, glyph fill colours
  (brightest pixel at the glyph) within 3. 12/12 with models off and on.
- qa-labels: on an engine with flights but not military/satellites, the
  deterministic 12,000 / 5,200 field is all aircraft, so detection is
  gated on Navara too: 14/14 (Cesium 14/14 unchanged).
2026-10-17 02:35:32 +04:00
German Khokhlov b14bf7e013 feat(flights): render on Navara
Task 10b. src/layers/aircraft/render/navara.js implements the aircraft
renderer contract on the engine contract alone (no three, no Cesium):
- Fleet glyphs: banks of engine.movers sprite sets (one per distance scale
  and depth mode the handles ask for; flights: one, 'always', Cesium's
  NearFarScalar), growing by doubling, from a glyph atlas of every
  aircraftIcon() kind at the fleet (64 px) and tracked (192 px) rasters
  (data/aircraftIcons: AIRCRAFT_ICON_KINDS, aircraftIconCells,
  aircraftIconAtlasLayout, drawAircraftIconAtlas). Sprite handles keep the
  Billboard field vocabulary as setters; a frame's writes commit once at
  the end of the frame listener that made them (a microtask outside one).
- Models: banks of model mover sets per glTF and look; handles report
  ready from MoverSet.ready() and computedScale from minimumPixelSize kept
  on the CPU (radiusM in the spec, the class's nativeRadiusM); poses are
  Cesium's HPR frame (modelMatrix) turned by Navara's glTF axes
  (Z_UP_TO_X_UP: its loader is already Z-up; chosen side by side with
  Cesium among six candidates).
- Tracked glyph: a sprite whose position/colour/rotation callbacks are read
  every frame in preRender; follow = engine.camera.follow with the tracked
  readout's card as its label (gevFollowHandle); current() maps the follow
  back to the tracked handle, also while follow() raises followChanged.
- Trail and live head: engine.lines (alpha 0.85, depth-fail 0.4, geodesic)
  in the gev-trail pick namespace.
- View: camera/scene adapters over engine.camera.pose() (registered with
  the engine so shared helpers project through it), frustum and horizon
  tests, frame number, engine frame events.
flights/render/navara.js registers it for 'navara' from the Navara mode's
import(); the Navara catalog now offers Live Flights.

Also: tracking a contact that is selected but no longer followed follows it
again (a camera command ends a Navara follow; on Cesium a search flight
releases trackedEntity): trackById/select re-follows instead of only
re-publishing. Behaviour change on Cesium only in that case.

Tests: the renderer on the fake engine (contract, atlas, banks, per-frame
commits, growth, tracked callbacks, follow label, model pose, trails) and
the flights logic over it (fleet, click-select, follow, re-follow after a
camera command, release, no set left behind).
2026-10-17 02:15:04 +04:00
German Khokhlov 20e9d4a0fa fix(globe): Navara sprite atlases are sampled as Cesium samples billboards
Linear, without mipmaps: Cesium's billboard atlas has none, and a mipmapped
atlas greyed small glyphs Cesium draws at full colour (a 5 px aircraft at
15,000 km: 239 against Cesium's 255; now 255 on both). Task 10b colour
parity.
2026-10-17 02:15:04 +04:00
German Khokhlov 1dd3220812 fix(voice): Navara refusals reach speech, analyst queries and stop_tracking
Review of 10a (low):
- The spoken answer to set_layer_visibility on a layer this engine lacks is
  the refusal's own sentence (why, and the switch to ?engine=cesium), not
  only "Couldn't turn on".
- analyst_query naming layers the catalog left out (no viewer) refuses with
  UNAVAILABLE_ON_ENGINE and the layer ids instead of answering zero.
- stop_tracking skips layers the engine does not have, so with nothing
  tracked it is a no-op success on Navara (it reported the absent military
  and satellites layers as failures).
Tests for each (the old code fails the last two); Cesium unchanged.
2026-10-17 01:18:12 +04:00
German Khokhlov 443c581280 refactor(data): focus projects through the engine; readout follows a tracked visual's follow
Task 10b foundation.
- focusDeemphasis projects through the engine registered for the scene it
  is given (engineFromViewer(scene).projector), as iconOrientation already
  does, else Cesium's SceneTransforms. On Cesium the engine projector is
  SceneTransforms, so nothing changes there; on Navara the aircraft
  renderer's scene adapter is registered with the engine. Test with a fake
  engine.
- refreshTrackedReadout accepts a layer's tracked visual that names its
  follow handle (gevFollowHandle): engines without tracked entities
  republish the card through it. Test.
2026-10-17 01:16:59 +04:00
German Khokhlov 4dfcba8989 feat(globe): mover sets report ready(); Navara scales by distance as Cesium
Task 10b foundation.
- MoverSet.ready(): whether what the set draws has loaded (Navara: the
  model set's glTF, a URL atlas; Cesium: every instance's Model, the cropped
  atlas; parts without it draw at once). The aircraft renderer keeps the 2D
  glyph until a model set is ready, as the Cesium path waits on Model.ready.
  Contract suite: a drawn model set is ready() (both engines); unit test of
  the neutral store.
- Navara sprites and points interpolate scaleByDistance as Cesium's
  NearFarScalar (over the squared camera distance, eased by ^0.2) instead
  of linearly in distance, so aircraft glyphs have Cesium's size at every
  range; the CPU pick radius follows (distanceScale, tested equal to
  focusDeemphasis' port of czm_nearFarScalar). The contract text says so.
2026-10-17 01:16:59 +04:00
German Khokhlov d81ebf1268 build: the panel build leaves out the initial-chunk guard
The MCP panel build inlines every dynamic import into its one script, the
Navara mode included, so the guard that keeps Navara out of the app's
initial chunks failed it (npm run build:panel was red since d483e589). The
panel config filters the guard out, as phase 4 does; a test pins it.
build:panel is green and joins the gates. Phase-4 merge review item 2.
2026-10-17 01:09:58 +04:00
German Khokhlov 2ce1c55f69 build: the QA probe stays out of production builds
src/app/qaProbe.js was statically imported by standalone/application.js
and shipped in the main chunk (+639 B gzip). It now loads with import()
behind import.meta.env.DEV at the import site: production builds drop it
(main chunk 2,890,982 -> 2,889,432 B), the dev server still installs
window.__godsEyeView.layers (checked headless). Phase-4 merge review item 1.
2026-10-17 01:09:58 +04:00
German Khokhlov 7f13ef4a87 refactor(flights): Cesium rendering behind a renderer interface
Task 10a. The flights logic no longer imports Cesium: every Cesium object it
built moved one for one into the shared aircraft renderer
(src/layers/aircraft/render/cesium.js; contract in render/contract.js), with
the flights' parameters in src/layers/flights/render/cesium.js, registered
for 'cesium' in the renderer registry and created at init(viewer, {engine}).

- Fleet: BillboardCollection + sprite-order slot; sprites stay Billboard
  handles (field vocabulary documented for the Navara renderer).
- Models: fleet/tracked/preload glTF loads (asynchronous:false, MIX tint, IR
  boost as `unlit` → the UNLIT CustomShader), the model collection, setPose
  (same HPR frame with airplane.glb's 180° offset).
- Tracked: the ENU billboard Entity with its position/colour/rotation
  callbacks, viewFrom, followed through viewer.trackedEntity + the one exact
  tracked frame (kept as is; engine.camera.follow on Cesium would also end
  the follow on any camera command, a behaviour change).
- Trail: the entity trail (data/trailRenderer, unchanged: its Primitive
  predecessor failed below the mesh) and the live head segment.
- View: camera/scene reads, frame number, frustum and horizon tests,
  projection, render requests, frame events, evidence render-loop hold.
- Clicks and Escape go through the engine's selection dispatcher (the
  ScreenSpaceEventHandler and keydown listener are gone); the cross-layer
  untrack watches the renderer's follow.
- Colours are engine-neutral Rgba (src/layers/shared/color.js), bit-equal to
  the Cesium.Color constants they replace; NearFarScalar → nearFarScale.
- Cesium math in the logic → src/geo.
- check-import-directions: cesium only in render/cesium*.js and three/Navara
  only in render/navara*.js for flights, aircraft and shared; a layer's
  Navara renderer only through navaraMode's import().

Source-text tests updated with the moved code, same meaning:
- readLayerSource reads a layer's render/*.js and, for the aircraft layers,
  aircraft/render/*.js (GLB-load, tracked-entity, CallbackProperty checks).
- modelScale.test: _normalBillboardScaleByDistance reads nearFarScale(...).
- spriteOrder.test: flights' enable calls fleet.restoreOrderOnEnable(); the
  renderer wires it to restoreSpriteOrderOnEnable('flights', viewer).
- trackingClickGesture.test: the flights gesture rules are the dispatcher's
  (selection gesture before select, click classifier before the empty-space
  clear); flights answers the empty click with a user-origin clear.
- militaryAwareness.test: flights' hasContact guard reads fleet.visible().
- ownership.test: the services fixture names selection and renderer.

New tests: color, the Cesium aircraft renderer (contract, collections, pose,
loads, frustum/horizon, tracked entity, follow, trail head, adopted
collections), the flights logic over a plain-data renderer and a fake engine
(fleet, click-select, no-op on the tracked visual, empty-click release), the
boundary rule.

W1 replay vs 7c7422e2: counts, known objects, screen points (0 px), tracking
poses (0 m), requests and shots (same sha256) identical. Main chunk
2,890,982 B (+6.2 KB, +0.21 %: the renderer indirection).
2026-10-17 01:04:25 +04:00
German Khokhlov 8e702f0c3b feat(data): the selection dispatcher treats an unowned pick as empty space
Task 10a moves flights onto the dispatcher; its old handler deselected on a
pick nobody owns (an unclaimed entity), and only another layer's pick was not
empty space. The dispatcher now does the same (after drill-through owners had
their chance), and selectionDispatcherFor(engine) gives the application one
dispatcher per engine. Tests for both.
2026-10-17 00:20:18 +04:00
German Khokhlov 9f1a0611fa fix(ui): the "kept in the link" notice is readable on Navara
On Navara the engine's persistent notice came up right after the share
restore's NOT ON THIS ENGINE notice and replaced it at once, so the names
of the linked layers this engine lacks were never seen (and a later finite
notice would in turn have wiped the engine notice for good).

- ShellFeedback keeps a persistent notice that a finite one covers and
  brings it back when the finite one ends; a persistent notice that arrives
  under a live finite one waits for it.
- Notices take a dwellMs; "Kept in the link: …" stays 15 s.
- scripts/qa-engine-notice.mjs (headless): a Cesium share link with a layer
  Navara lacks, opened with ?engine=navara: notice shown with the layer
  name, still up 14 s later, then the engine notice is back. 4/4 (before
  the fix the notice never appeared).

T8-T9 review item 2.
2026-10-16 23:27:47 +04:00
German Khokhlov c493a8303b build: the QA fixture sources stay out of production builds
The ?qaFeed import is now gated by import.meta.env.DEV at its import site,
so Rollup drops the qaSources chunk (4.7 KB) from production builds instead
of shipping it unreachable. Dev behaviour is unchanged. (T8-T9 review item 3)
2026-10-16 23:18:33 +04:00
German Khokhlov bf46eb6d97 fix(voice): Navara refusals name the feature and the way back to Cesium
Review of T8-T9:
- The UNAVAILABLE_ON_ENGINE refusal now names the feature (UNAVAILABLE_FEATURES)
  and suggests the Cesium engine (?engine=cesium): "The cockpit is not
  available on the Navara engine yet. Switch to the Cesium engine
  (?engine=cesium) to use it." New fields: feature, suggestion.
- set_visual_style and set_post_processing refuse without a viewer
  (CESIUM_LOOK_ACTIONS): Navara has no visual styles, so they used to answer
  ok, change nothing and record a style get_current_view_state then reported.
- set_layer_visibility on a known layer the engine's catalog left out refuses
  with the same structured result instead of throwing "Unknown data layer".
  A name no engine knows still throws.
- Removed the dead summarizeEntity and the two helpers only it used.

Cesium (viewer present) is unchanged; tests cover each refusal and that the
style still applies with a viewer.
2026-10-16 23:17:36 +04:00
German Khokhlov 907cb2092d fix(globe): Navara redraws after a map change while its tiles arrive
Phase-4 QA: after a map-stack switch at the whole-globe view Navara
sometimes showed the new map only seconds later. Navara 0.2.1 reports idle
and stops drawing before a raster tile it fetched arrives, so the last
frame stays stale until something else asks for one. Every engine.maps
change (base imagery, terrain, photoreal tiles, their release) now asks for
a frame and keeps asking every 250 ms for 6 s, then the engine idles again.

scripts/qa-map-switch.mjs switches the stack (photoreal / Esri / OSM) at
18 000 km on each engine, waits for the tiles, hashes the picture, forces
frames and hashes again: a change means a stale picture. Before: Navara 3/8
switches drew the new picture by themselves; after: Navara 8/8, Cesium 8/8.
2026-10-16 23:00:15 +04:00
German Khokhlov ba039f7974 fix(app): the Navara notice no longer calls voice unavailable
Voice runs on Navara since 18b5475b (the viewer-only actions refuse with
UNAVAILABLE_ON_ENGINE); the notice still listed it as unavailable.
2026-10-16 22:53:35 +04:00
German Khokhlov 18b5475b8a feat(app): voice on the Navara engine
createViewerlessTools creates the voice commands with viewer: null on the
engine (tools.js passes it the voice options and place search), so
window.__gevVoiceCommands exists in ?engine=navara: the W1 tools drive the
engine and the viewer-only tools answer UNAVAILABLE_ON_ENGINE. The Cesium
path hands its engine to initGevVoiceCommands explicitly.

viewerlessTools.test.mjs: the Navara tools now return voice commands
(created without DOM through injected control and session stand-ins),
whose runner answers get_current_view_state on the engine and refuses
control_scene; the debug handle carries them and teardown removes them.
2026-10-16 22:52:21 +04:00
German Khokhlov bc281ab246 refactor(voice): W1 actions on the engine
The voice runner drives the globe engine (an `engine` option, by default
the one registered for the viewer) instead of the Cesium viewer:
track_entity, stop_tracking (camera.following().stop()), frame_overhead
(camera.flyToSphere with the pose heading), fly_to_location, zoom_to_globe
(follow released through camera.following()), adjust_camera_zoom,
next_iss_pass / next_satellite_pass, select_nearest_aircraft and the
analyst view context, get_current_view_state, get_entity_context
(projector.worldToWindow), the view target (picker.pickPosition, then
camera.pickEllipsoid, then picker.pickGlobe) and its moveEnd prewarm, and
the viewport samples. getBasemapLabelContext takes the engine or a viewer
with a registered engine (the HUD).

Without a viewer the tools that still need one (VIEWER_ONLY_ACTIONS:
control_scene, annotate_map, clear_annotations, fly_route, move_camera,
control_cockpit, set_context_mode, control_radio, control_cctv) answer a
structured refusal (code UNAVAILABLE_ON_ENGINE) instead of throwing.

Point-and-ask picks with engine.picker and keys the camera pose from
engine.camera.pose(); the viewport capture reads engine.host.canvas and
waits for the engine's postRender (both engines keep their drawing
buffer: Cesium's contextOptions, Navara's own WebGL2 canvas, S4).

Tests: gevActionsEngine.test.mjs runs each moved action on the fake
engine. gevActions.test.mjs keeps its expectations; its runner helper now
registers an engine for the test viewer (the Cesium engine over it, or a
fake engine at its positionCartographic) and the two harness cameras gain
roll and frustum for camera.pose().
2026-10-16 22:52:21 +04:00
German Khokhlov ec7dcc944e test(qa): an engine-neutral layer probe
- src/app/qaProbe.js → window.__godsEyeView.layers: list() (with
  `available`), unavailable(), available(id), enable/disable(id) (a user
  toggle through the layer manager), objects(id) → [{ id, label, position,
  screen }], screenOf(id, objectId), selected(id), following(), movers(),
  framesDrawn() - all through the engine and the catalog's availability, the
  same on Cesium and Navara. Installed after the tools.
- src/app/layers/qaSources.js: `?qaFeed=flights:synthetic:10000,
  vessels:fixture` (dev server only, allowQaRegistration) replaces those
  sources with seeded synthetic answers served through the live sources'
  own parsers (same snapshot shape; loaded with import() only then). The
  pure generators moved from src/tooling/w1Feeds.js to src/bench/w1Feeds.js
  (the browser graph may not reach src/tooling); the W1 recordings
  re-export them.
- scripts/qa-w1-layers.mjs: the parity checklist's frame, --layer
  --engine=cesium|navara with the fixture feeds, JSON per run and
  --compare (objects on screen per pose, 5 known objects within 2 px).
  Filled: 1 view, 8 idle, 9 errors/no leftover mover sets after 10
  on/off; 2-7 report TODO for the layer tasks.
- qa-perf, qa-labels, qa-overlay-baseline and qa-label-readability take
  --engine= and use the engine (camera, frames, render requests, tiles) and
  the probe (layers, following, movers) instead of the viewer; checks whose
  layers an engine lacks are SKIPPED with the reason (Navara today), as are
  qa-perf's animated-style checks there (Cesium post stages).

Verdicts, Cesium, before → after: qa-perf 24/24 → 24/24, qa-labels 14/14 →
14/14, qa-overlay-baseline (datacenters, firms, detection-50) 2 measured +
1 skipped → the same. Navara: qa-perf 14 passed, 3 skipped; qa-labels 3/3,
1 skipped; qa-overlay-baseline 3 skipped; qa-w1-layers flights: Cesium 5/5
(10 000 synthetic aircraft), Navara skipped (no flights renderer yet).
2026-10-16 22:52:18 +04:00
German Khokhlov 834d34296f test(overlays): an allocation workload through the Navara projector
Review of Task 7: every allocation workload ran the Cesium engine on a mock
viewer with identity matrices and the horizon test off, so the per-label
horizon test and the Navara projector were never in the gate. The new
`navara-projector` profile gives the host the Navara engine's projector
(cameraMath worldToWindow, viewProjection, horizonVisible, as
createNavaraEngine wires it) over a real WGS84 camera 2 000 km up, with 250
moving labels on a lon/lat grid under it and the horizon test on.

Node 24.14 (the calibrated runtime; CI's 24.14.0 matrix leg runs the gate
with GEV_REQUIRE_ALLOCATION_GATE=1): 10 905 B/frame median (generic 250
labels: 9 681), 43.6 B per candidate; budget 13 000 B/frame as the generic
saturated workload. All 14 workloads pass on 24.14.
2026-10-16 22:26:04 +04:00
German Khokhlov d6fe0f3805 fix(globe): the floor takes no tile coarser than 1 km
Review of Task 6: from 15 km with cold tiles Navara answers from 3.5-4 km
tiles (13.9 km at AUS), below the 4 km floor limit, 6-16 m off; the mesh
floor samples up to 25 km, so those would latch. MAX_FLOOR_TILE_SIZE_M is
now 1 km: from 800 m the answering tiles are 112-314 m. The 1.8 km tile
that read the Austin ground right under a 600 m camera is now a miss too,
which only leaves the DEM prior in place.

qa-surface-heights runs the 15 km pass first in a fresh page (cold), then
the 800 m pass in another, and keeps every answer the floor rule would
take while the tiles stream. Measured: 800 m 19/20 within 2 m of Cesium;
15 km cold 7/20 (the plan's gate of 15 assumed warm tiles - lod 'finest'
can only use what Navara has loaded, and at 15 km it loads 3.5-14 km
tiles), and the floor rule takes 0/20 of those; while streaming it took
answers more than 2 m off at 3 places: the Empire State spire (39 m, also
the settled answer) and two street points 2.2-2.3 m off.
2026-10-16 22:19:57 +04:00
German Khokhlov f0ce1b04c7 fix(globe): the Navara follow turns and dollies as Cesium's tracked camera
Review of Tasks 6-7: a 240 px drag right turned Navara's follow by -67.5°
where Cesium turns +119.5°, and a right drag did nothing (Cesium dollies,
1.95 Mm → 0.91 Mm). followGestures.js now follows Cesium's
ScreenSpaceCameraController for a tracked camera:
- left drag (rotate3D): heading += 1.77 × dx / width × 2π (clockwise for a
  drag right; 1.77 is Cesium's maximum rotate rate, which a tracked camera
  beyond a few metres always reaches) and the camera rises by
  1.77 × -dy / height × π; each move counts at most a tenth of the canvas
  (maximumMovementRatio);
- right drag (zoom3D): follow.js dollyOffset moves 5 × dy / height of the
  distance above the floor (closing) or of the range (opening), never
  inside it; no context menu while following.

The contract follow check turns the camera with a 240 px drag right and
asserts the heading turn against Cesium's rate (±15 %; the harness holds the
pointer before releasing it so Cesium adds no inertia), and dollies with a
right drag down (range below 80 %, never under 149 m). Cesium 73/73, Navara
79/79.
2026-10-16 22:14:02 +04:00
German Khokhlov b0cbed081b feat(app): layer catalog per engine
- src/app/layers/renderers.js: the layer renderer registry
  (registerLayerRenderers(engineKind, { [layerId]: factory }),
  rendererFor(layerId, engine), layerAvailableOn(layerId, engineKind)).
  Every application layer is registered for Cesium as CESIUM_BUILTIN (it
  still draws itself until its render/cesium.js); Navara factories come with
  the layer tasks. On it, the stable availability API phase 4 injects into
  its layer list: createLayerAvailability(engineKind) →
  { engine, isAvailable(id), available(), unavailable() } with { id, name }
  rows (src/app/layers/directory.js holds the ids and names in catalog
  order; a test keeps it equal to the constructed catalog).
- createApplicationCatalog({ engineKind, availability }) constructs only the
  layers the engine can show, in the same order (shared instances made
  once, on demand); the catalog carries `availability` and the others'
  metadata (`unavailableMetadata`). Cesium builds the same 30 layers in the
  same order as before; Navara builds none yet (no Navara renderers) - the
  standalone app now asks the catalog for it instead of making an empty one.
- catalogControlServices: a role whose layer the engine leaves out gets an
  empty stand-in (absentLayerControlServices folded in); any other missing
  layer is still a wiring error.
- LayerLifecycle(viewer, { engine }): every module call (init, enable,
  update, disable, destroy) gets { signal, engine } as its second argument;
  viewer is null on Navara.
- Links: a link that turns on layers this engine leaves out keeps them in
  the durable state (re-saved links carry them; back on Cesium they turn
  on) and activates nothing; a notice names them ("NOT ON THIS ENGINE · Kept
  in the link: …"). The Navara notice lists the engine's data layers from
  the registry.
- The dev launcher's CCTV wiring check matches the new catalog form.
2026-10-16 22:07:55 +04:00
German Khokhlov e1be2eac19 feat(app): detection and readout on Navara
The shell creates detection and the tracked readout on the engine, so the
Navara mode has them too (before: not created without a viewer). Detection
reads the camera height from the engine. The tracked readout follows
engine.camera.following(): a follow's plain-data label
(FollowOptions.label: { id, model, position }, the readout card's model
and anchor) or, on Cesium, the followed Entity and its gevLabelModel as
before (Cesium layers keep publishing the same). refreshTrackedReadout
takes a follow handle too. A bare viewer without a registered engine
(tests) is still read for its trackedEntity.

In the app on Navara: "[Detection] Initialized", "[TrackedReadout]
Initialized", and a follow with a label paints its card.
2026-10-16 21:14:59 +04:00
German Khokhlov 7d5a484796 refactor(overlays): layer labels through the engine
The world overlay host already projects through engine.projector and tests
the horizon with projector.horizonVisible; the last Cesium value on its
path, the snapshot of an entry's cullPosition, is now a geo Cartesian3.

Navara's horizonVisible is Cesium's EllipsoidalOccluder.isPointVisible:
tested against Cesium's own on 8 000 eye/point pairs (600 m to 20 000 km,
points under, on and over the ellipsoid; no disagreement) and at the limb
(a label 40 m under the ellipsoid just past the horizon is hidden, with its
cullPosition lifted 100 m it is shown). It ran per label per frame with
three new vectors each call; it now uses scratch vectors. Micro-measure
(500 labels per frame, no scavenge during the run): 7 941 B per frame
before, 51 after.

The W1 sources that projected with Cesium's SceneTransforms themselves -
the icon orientation's exact projection and the launch replay overlay - go
through the engine's projector when their scene or viewer has a registered
engine. Satellites, vessel labels and aircraft recession project nothing
themselves (the host does).

scripts/qa-overlay-engines.mjs publishes the same labels on both engines at
a city, an orbital and a limb pose and compares the painted rectangles:
worst 0 / 2 / 0 px; on both, the far-side label is not painted, the lifted
limb label is and the buried one is not.
2026-10-16 21:14:52 +04:00
German Khokhlov 311722e6de test(globe): the mover shader test expects decoded sRGB colours
68ff4e1c decodes instance colours in the fragment shader; its shader
test still matched the undecoded line.
2026-10-16 21:04:22 +04:00
German Khokhlov d7193e3b9e fix(globe): the CPU pick radius follows scaleByDistance
Review of Task 4: the CPU picks (Navara hover, drill, the stale-answer
check) took a sprite or point at its full sizePx however far it was, so a
set shrunk with scaleByDistance was picked well off its drawn edge. The
radius now scales by the set's scaleByDistance at the instance's camera
distance, linear between near and far as the Navara shader draws it.
2026-10-16 21:00:01 +04:00
German Khokhlov a4e753d550 fix(globe): Cesium picks fall through filtered results; no picks behind the horizon
Review of Task 4:
- a Cesium pick whose top result is filtered (a pickable: false set, the
  follow camera's proxy entity) took nothing; it now drills and answers the
  first result under it with an id (an object), else the first kept one
  (map tiles);
- billboards and points drawn without a depth test reach Cesium's GPU pick
  through the globe: mover instances behind the horizon (projector
  horizonVisible on the instance position) are left out of picks and
  drills, as Navara's CPU drill already did;
- the follow proxy entity (1 px point, cesium/follow.js) carries
  gevFollowProxy and is never a pick (review item 6).
2026-10-16 20:59:10 +04:00
German Khokhlov 4e195dac6b fix(globe): the Navara follow orbits on drag and zooms to the floor
Review of Task 5: while following on Navara a left drag panned the globe
about the ground point under the cursor (range 2,154 m → 54,136 m) and the
wheel zoomed toward that point (462 m after 40 notches, Cesium stops at
the 150 m floor). While a follow runs, Navara's spin, zoom and tilt are
now suspended (input.suspendCameraInputs, which gives the app's own
setCameraInputs choice back after the release) and followGestures.js turns
a drag into heading and pitch of the ENU offset (a canvas width is one
turn, its height half a turn; elevation kept within 2°-89°) and a wheel
notch into 20 % of its range, never inside the minimum range, as Cesium's
EntityView orbits the target. The offset is no longer read back from a
camera the follow alone moves.

Also (review item 7): a follow requests a render only when the target, the
offset or a gesture changed, so a still target lets Navara idle.

The contract follow check drags (range within ±5 %, target within 3 px)
and turns the wheel 40 notches (never below 149 m, ends within 5 % of
150 m) on both engines with real input (qa-globe-contract's pointer gains
drag and wheel); after stop it waits for Cesium's gesture inertia to end
before checking that the camera is free. Contract: Cesium 73/73, Navara
79/79.
2026-10-16 20:57:10 +04:00
German Khokhlov 68ff4e1c25 fix(globe): Navara overlay colours are sRGB
Review of Tasks 4-5: the overlay pass encodes linear to sRGB, and the
instance and line colours went in as if linear: grey 0.5 drew 187 (Cesium
128) and [1, 0.6, 0.2] drew 255/203/124 (Cesium 255/153/52). The sprite,
point and line shaders now decode them (sRGBTransferEOTF), as the sRGB
atlas texture already was.

A contract check draws mid-tone sprites, points and lines (grey 0.5 and
[1, 0.6, 0.2]) and reads their centres: within 3 per channel on every
engine. Navara 79/79.
2026-10-16 20:51:36 +04:00
German Khokhlov 645f042450 feat(globe): surface height quality
engine.surface.sampleHeightDetailed(place, { exclude, lod }) answers
{ height, source: 'tiles' | 'terrain', tileSizeM? } (src/globe/surfaceDetail.js).
On Navara tileSizeM is the extent of the tile that answered and
lod 'finest' takes the finest loaded tile, drawn or not; Cesium answers
tiles without a size, else the globe's terrain. acceptedSurfaceHeight()
turns an answer into a height or a miss: tiles larger than
MAX_FLOOR_TILE_SIZE_M (4 km) are too coarse, terrain is not the mesh.

The mesh floor sampler and the ground snap are engine-only now (a viewer
or scene reaches its engine through engineFromViewer, which also registers
the scene) and treat a coarse answer as a miss, as they treat undefined.
Measured on Navara at Austin: while the tiles stream, a 27.9 km tile reads
143.2 m where the ground is 122.5 m (a 1.8 km tile).

`exclude` takes the engine's own mover sets (MoverSet.id): Cesium hides a
set's primitive for the synchronous sample (Cesium matches excluded objects
per billboard, not per collection); Navara's ray only meets tiles.

"sampleHeight undefined in the app on Navara" did not reproduce: the
startup Austin sample is 121.9 m (tile 889 m) on Navara and 122.4 m on
Cesium; one ModelMesh class is patched.

S2 QA (scripts/qa-surface-heights.mjs, 20 places at JFK, SFO, AUS, roofs
and streets): Navara within 2 m of Cesium at 19/20 from 800 m and 18/20
from 15 km with lod 'finest' (gates 18 and 15); offsets per region
-0.21 / +0.4 / +0.1 m; the Empire State roof differs by 39 m (spire
geometry). Navara tile sizes at 800 m: 112-314 m.
2026-10-16 20:51:29 +04:00
German Khokhlov f640890cf6 feat(globe): follow camera on both engines
camera.follow({ position, viewFrom, minRange = 150, native? }) keeps a
moving target centred at an east-north-up offset the user may turn and
zoom; camera.following() and camera.followChanged report it; stop() frees
the camera and a camera command (setView, flyTo, flyToSphere, lookAt,
cancelFlight) ends it.

Probe ruling: Navara 0.2.1's cameraFollow holds the target but ignores the
user's drag and wheel, the aircraft sits ~13 px off centre with 7.6 px of
spread, and calling it without an offset each frame gives a NaN camera.
So Navara (and the fake engine) follow with src/globe/follow.js: each
frame, before Navara draws, the offset is re-read from where the user put
the camera relative to the previous target, kept outside the minimum
range and carried to the new target.

Cesium follows with viewer.trackedEntity: the layer's own Entity
(`native`) or a proxy Entity at position() with a 1 px transparent point,
since EntityView moves the camera only for an entity with a bounding
sphere. applyTrackedCameraFrame and the zoom limits moved to
src/globe/cesium/follow.js; src/data/trackedCamera.js keeps the pure parts
(the range clamp now lives in src/globe/follow.js). following() also
reports an entity the app tracked itself. Camera sensitivity claims made
on a viewer's Cesium camera now share the engine camera's ledger, and the
orientation controls and the world jump read the follow from the engine.

Contract (both engines): a target flying at 250 m/s stays within 3 px of
the centre for 2 s, zoomIn through it stops at 150 m, stop() and setView
end the follow. Cesium 71/71, Navara 77/77; shell 23/25/4.
2026-10-16 20:26:43 +04:00
German Khokhlov a89719aba9 build: package boundaries own the engine modules added since Task 1
check:boundaries failed from 76851ee9 on: vite-build did not own the
initial-chunk guard, and the layer and application packages did not own
src/time/clock.js and the movers, lines, picks and Navara overlay modules
they now reach.
2026-10-16 20:06:12 +04:00
German Khokhlov 39d522c9a9 perf(globe): less churn in movers and lines
Review of Tasks 2–3:
- the mover store's instance checks no longer build arrays or closures
  per set() (orientation and colour checked field by field), and dirty()
  returns one reused range object;
- Cesium models get their matrix and colour through scratch objects
  (Model.modelMatrix and Model.color copy what they are given) instead
  of a new Matrix4 and Color per placement;
- a destroyed MoverSet releases its store (count 0, empty instance
  arrays, no pick ids) so a set object a layer still holds keeps no
  memory, and the Cesium parts drop their billboard, point and model
  lists;
- (with the overlay commit) the Navara line viewport is updated in
  place.

Node micro-measure, 10 000 instances set + packed per frame: 0.27-0.29
ms, about 1.7 KB per frame (0.17 B per instance), as before. Contract:
Cesium 70/70, Navara 76/76.
2026-10-16 19:33:46 +04:00
German Khokhlov d483e5897e build: the initial-chunk guard covers Navara's whole dependency closure
Review of Task 0:
- build/navaraDependencies.js reads the dependency closure of
  @navaramap/three and @navaramap/three-default-plugin from node_modules
  (dependencies and peers) and drops what the app's other bundled
  dependencies reach too (Cesium is served as a script, so draco3d, which
  only Cesium shares, stays engine-only), plus three and postprocessing:
  88 packages today (the @navaramap/* packages, @takram/*, @react-three/
  fiber, @sparkjsdev/spark, astronomy-engine, d3*, draco3d, n8ao,
  three-stdlib, …). The browser build's initialChunkGuard fails on a
  module from any of them in an initial chunk. Tested on a fixture tree.
- check-main-chunk's budget measures the entry the page loads (the same
  chunk the composition check reads), falling back to phase 4's largest
  index-*.js only for a dist without a page.

Build clean: no Navara-only module in the initial chunks; main chunk
2,869,565 bytes (gzip 889,041).
2026-10-16 19:30:23 +04:00
German Khokhlov ae52695b15 test(qa): seeded synthetic feeds for the W1 baseline; requests compared
Review of Task 0: the recorded live feeds left vessels (no AIS key here)
and local ADS-B (no receiver) empty, and flights held one adsb.lol
viewport answer. src/tooling/w1Feeds.js builds seeded answers relative to
one frozen instant (2026-10-16T12:00Z): a global /api/flights OpenSky
snapshot (8 000 aircraft, 2 000 within 60 km of JFK), /api/vessels with
12 000 AIS rows worldwide and the rows within the city area (the area
answer first, the global one last as the replay's fallback), and
/api/local-receivers/aircraft with 40 aircraft near JFK on 1090 MHz and
978 MHz UAT. Tests read them back through the app's own parsers.
qa-w1-baseline --synthesize=DIR writes them beside the recorded
military, satellites and launches; a replay now logs every feed request
and how it was answered (exact / as another key / none) into
summary.json, and --compare diffs those too.

Replayed on d50eaa28 and HEAD: flights 8 000, military 154, local ADS-B
40, vessels 12 000, satellites 824, launches 24; the runs agree (counts,
known objects, screen points, tracking poses, requests). The Cesium
reference is re-recorded this way before Tasks 10a, 12a and 13a.
2026-10-16 19:28:12 +04:00
German Khokhlov 6436f580d9 test(globe): mover coverage on both engines; pickable: false honoured
Review of Tasks 2 and 4:
- A contract check draws, on both engines: an atlas cell switch, a
  rotation of π (a half-white cell flips from above to below the
  centre), scale 2 (about four times the area), scaleByDistance past
  `far` (about a quarter), setCount shrinking the set, depth modes for
  sprites 30 km under the ground ('always' and a disableWithinM past the
  camera draw), a point with its dark outline, and a glTF model
  (airplane.glb, minimumPixelSize 80, colorBlend replace) in magenta.
  'normal' and a 10 m disableWithinM hide the buried sprite on Navara;
  Cesium's billboards are not occluded by the app's photoreal tiles
  (globe hidden; measured), so those two are asserted on Navara only.
- pickable: false: Cesium's GPU pick sees every primitive, so the
  picker drops picks (and drill entries) of a set created with
  pickable: false; Navara only registers pickable sets. The contract's
  click check now clicks a pickable: false sprite on both engines.
- Navara resolves a GPU answer's batch id to the pick id when Navara
  answers (the pick's own frame), not when the engine's click consumes
  it: a set that reuses the instance in between still yields the
  instance that was under the pointer.

Contract: Cesium 70/70, Navara 76/76.
2026-10-16 19:20:47 +04:00
German Khokhlov 0957d9712b test(globe): pixel dashes at DPR 1 and 2
Review of Task 3 (DPI-independent dashes; the shader change landed with
the overlay commit): qa-globe-contract --dpr=2 runs the suite on a
high-DPI page, and a new check measures a 16/16 px dashed line along its
middle row in CSS pixels: the dashes must be regular, and on Navara
16/16 within 2 px. The harness prints what each engine draws.

Measured: Navara 16/16 (period 32) at DPR 1 and 2. Cesium draws
16 on / 16 off (PolylineDash dashLength 32, pattern 0xff00, uniforms
checked on the primitive) as 14 on / 2 off with a 16 px period at both
ratios — a Cesium-side difference to look at before a layer relies on
pixel dashes (none does yet). At DPR 2 the Navara harness's
stars-at-midnight check counts 4 star pixels (its detector is tuned for
DPR 1); every suite check passes at both ratios.
2026-10-16 19:14:52 +04:00
German Khokhlov 93866c42fc fix(globe): Navara lines are picked within 8 px of their segments
Review of Task 4: the CPU line pick (shared with the sprite drill, hover
and the synchronous pick) took half the drawn width + 3 px, so a 1-2 px
trail needed a 3.5-4 px aim. Now within 8 px of the projected segments,
or half the width + 3 px for wider lines. Tested at 7.5 and 8.5 px from
a 1 px line; the contract suite's line click passes on both engines.
2026-10-16 19:05:20 +04:00
German Khokhlov e804351205 fix(globe): Navara sprites, points and lines drawn after tone mapping
Review of Tasks 2–3: on Navara the overlays went through the look's
tone mapping and grading (magenta came out as 247/40/228, green as
147/228/89 in the cinematic look), where Cesium draws billboards and
polylines in the colours asked for.

src/globe/navara/overlay.js: a public Navara EffectDesc (gevOverlay)
inserted after toneMapping and before the gev sharpening, SMAA/FXAA and
final, whose pass renders one three scene over the tone-mapped input
buffer (in Navara's own Pass wrapper: Navara inserts a bare pass only
when it is an instance of its own postprocessing copy); it costs nothing
while the scene is empty. The chain has no depth buffer, so each overlay
material tests the scene depth itself (overlayDepth.js): its log depth
against Navara's MRT depth (RGBA packed), modes always / normal /
depth-fail, disableWithinM as a per-vertex escape. Sprites and points
(moverMesh.js) and line ribbons (lineMesh.js) live in that scene with
their per-frame work as overlay hooks; models stay Navara meshes, lit
and graded like the tiles. The sharpening now sits after the overlay
(Cesium's sharpen stage sharpens billboards too).

Also (review): Navara pixel dashes are measured in CSS pixels
(gl_FragCoord over uGevPixelRatio), and the line viewport uniform is
updated in place.

Measured: a magenta sprite draws 255/0/255. The contract suite's colour
checks are now within 40 of the asked colour on every channel on both
engines (were: dominant channels); Cesium 68/68, Navara 74/74 (its
sharpen-order check accepts the overlay between tone mapping and the
sharpening). Shell: cesium 23/23, cross 4/4; navara 23/25 — the two map
stack pictures from space fail on stddev 7.2/7.6 against 8, as on the
Task 3 commit run at the same hour (the cinematic sun follows the clock
into night over the reset view), not from this change.
2026-10-16 19:04:57 +04:00
German Khokhlov 3c2e924363 test(qa): headless by default
qa-directions, qa-transit, qa-transit-recovery and qa-wind-canvas
launched a visible Chrome window. They now run headless unless
QA_HEADFUL=1 (`headless: process.env.QA_HEADFUL === '1' ? false : true`,
the same change as on the phase-4 branch).
2026-10-16 18:59:37 +04:00
German Khokhlov 8812117013 fix(globe): Navara's shadowed sun leaked a light per frame
Navara 0.2.1's transparent pass refreshes its copy of every scene light
each frame with copy(light, true). For the sun's cascaded shadow lights
(three-csm CascadedDirectionalLights) that copy clones every cascade, so
each frame brought new DirectionalLights with new ids and three's
WebGLLights uniforms cache (keyed by id) grew by about 1.2 KB a frame in
the looks whose sun casts shadows (minimal, photoreal).

src/globe/navara/cascadedLightCopy.js patches the copy to update the
cascades it has in place (pose, colour, intensity, target, shadow
settings and camera) when the counts match, and keeps the rebuild
otherwise; internals.js finds the class among the view's scene lights
(private view._scenes.light) and applies it when the globe is built.
Tests on three-csm's own class: the original copy changes the light ids,
the patched one keeps the lights and takes the values.

Post-GC JS heap (CDP HeapProfiler.collectGarbage every 10 s, the app on
?engine=navara rendering continuously at 60 fps): minimal 4.33 → 0.20
MB/min over 70 s and 0.06 MB/min over 180 s; photoreal 4.34 → 0.19
MB/min; cinematic (no shadow) 0.34. Added to the upstream asks.
2026-10-16 18:41:20 +04:00
German Khokhlov c327a25744 feat(data): one selection dispatcher
src/data/selectionDispatcher.js: one engine click subscription with
picks (pick, drill 8), the press gesture (travel and duration, as
bindTrackingClickGesture) and one Escape listener for the layers that
move onto the engine. Owners register owns(id) (also in pickRegistry, so
layers still on their own handlers see the ownership) and onSelect /
onEmptyClick / onEscape. Rules of the existing handlers: another layer's
pick is not empty space; passThrough picks (trails) never take the
click; a drillThrough owner (local ADS-B) takes its object from under
another layer's; a drag selects nothing; deselection needs a clean click;
a pointer owned by a tool selects nothing. Tested on the fake engine:
own / foreign / empty / trail clicks, drill-through, drag and long press,
Escape, an owner without handlers, unregister and destroy. No layer uses
it yet (the *a tasks move them).
2026-10-16 18:25:12 +04:00
German Khokhlov b0cb8ce881 feat(globe): picks on input payloads
input.on('click'|'doubleClick'|'move', fn, { pick, drill }) delivers
{ …event, picked, drilled }: PickResult { id, owner, instance, primitive
(Cesium) } names the mover set (and instance) or line hit. One pick (and
one drill, as deep as the deepest listener) per event, shared by its
listeners; moves with picks are coalesced to one delivery per frame with
the latest position (src/globe/pickDelivery.js). Option checks in the
contract (normalizeInputOptions).

- Cesium: scene.pick / drillPick, and the picker names the owner: movers
  map billboards, points and models to their set and instance, lines
  their primitives to the line.
- Navara: a click with pick takes Navara's GPU pick of the same gesture
  (featureClick; its pick helper answers on the same pointerup, usually
  before the engine's click, so a fresh answer waits for it; otherwise
  the click waits up to 250 ms, then the CPU pick). Navara 0.2.1 answers
  clicks in quick succession with the first click's pick: a GPU answer
  whose instance is not drawn near the click is stale and the CPU pick
  answers. Hover, drill and the synchronous picker.pick/drillPick are CPU
  picks (src/globe/cpuPicks.js) of pickable mover sets and pickable lines
  (within half the width + 3 px), behind the globe excluded, nearest
  first; lines are picked on the CPU after an empty GPU answer.
  picker.clickStats() (Navara only) counts the answers and latencies.
- The fake engine picks its own movers and lines on the CPU unless
  scripted.
- The contract suite clicks with real pointer events (puppeteer) on both
  engines: a sprite is picked with its owner and instance, the map picks
  nothing, two sprites in one place drill both, a line is picked, a
  hover picks the sprite under it. Navara, 20 clicks: payload p50 2.8 ms,
  p95 4.1 ms after the click (puppeteer dispatch included), 20/20 GPU
  answers. The Cesium harness keeps the first-run card closed.
2026-10-16 18:25:12 +04:00
German Khokhlov 86aa0bc9ba feat(globe): dynamic lines on both engines
engine.lines.createLine({ id, widthPx, color, depthFailAlpha, dash
({ lengthPx, gapPx } | 'tactical'), arc ('none'|'geodesic'), modelMatrix,
pickId }) → a Line: setPositions (Cartesian3[] or a flat Float64Array),
setColor, setModelMatrix, show, destroy; lines.list() for QA. Options,
flat positions, geodesic densification (great circles of the geodetic
normals, height interpolated, one step per degree) and the registry are
engine-neutral (src/globe/lines.js); the fake engine keeps every line.

- Cesium: a Primitive of a PolylineGeometry (synchronous, rebuilt when
  the positions change) with a PolylineMaterialAppearance in an
  engine-owned collection; depthFailAlpha is depthFailAppearance with the
  alpha scaled; pixel dashes are PolylineDash (16-bit pattern); the
  tactical dash is the launches' selected-orbit shader under its own type
  (launches keep theirs until Task 15a moves them onto engine.lines).
- Navara: a public MeshDesc (transparent pass, unlit) holding a ribbon
  of one quad per segment, widened to CSS pixels in the vertex shader;
  a second copy drawn with GreaterDepth and the alpha scaled is the
  depth-fail part; dashes are cut in the fragment shader (pixels along
  the screen direction, or the tactical pattern over the fraction along
  the line); positions relative to the first point, the model matrix on
  the group. Geodesic arcs are densified on the CPU. Not pickable yet.
- The contract suite draws a three-point line on both engines: magenta
  along both segments, nothing off it, 1° east once turned by its model
  matrix, gone when hidden; a line 30 km under the ground shows only its
  depth-fail copy, dimmer than the line.

Main chunk 2,867,499 bytes (+7,197, +0.25 %: the Cesium lines).
2026-10-16 17:24:36 +04:00
German Khokhlov 0109097440 feat(globe): instanced movers on both engines
engine.movers.createSet({ id, kind: sprite|point|model, capacity, order,
depthTest, sprite|point|model, pickable }) → a MoverSet: set(index,
fields) stores an instance (position, screen rotation, orientation, rgba,
scale, atlas cell, show, pickId) in flat arrays, commit() uploads the
changed range once and requests one render, positionsView() reads the
positions, destroy() removes it; movers.list() for QA. The options, the
store and the registry are engine-neutral (src/globe/movers.js); the fake
engine keeps them for layer tests.

- Cesium: one primitive per set in an engine-owned PrimitiveCollection
  added with the first set, in `order`: sprites a BillboardCollection
  (one image per atlas cell, NearFarScalar, disableDepthTestDistance),
  points a PointPrimitiveCollection, models a Model per instance.
- Navara: a public MeshDesc per set. Sprites and points are one
  InstancedMesh of a quad, faced to the screen and sized in CSS pixels in
  the vertex shader from uniforms (the pick pass draws the same
  footprint), atlas uv, rotation, colour and scale per instance, drawn in
  the transparent pass unlit; offsets from an origin that follows the
  camera past max(1 km, 10 % of its height). Models: the glTF once, one
  InstancedMesh per mesh, minimumPixelSize/maximumScale on the CPU per
  drawn frame, colorBlend in the shader. Pickable sets register a
  PickableInstancedMeshWrapper; pickIds.js maps batch ids back to
  instances (used by Task 4).
- The contract suite draws three 24 px sprites on both engines: centred
  within 2 px of worldToWindow, hidden by show:false, recoloured, gone on
  destroy, and no frames added once the commit has drawn.
- qa-engine-bench --scenario=movers: 10 000 synthetic aircraft
  (src/bench/syntheticFlights.js, moved from the Navara spike with its
  test), every instance moved and committed every frame. p95 Navara
  (look=minimal) / Cesium: global 5.2/7.0 ms, city 5.6/9.9, orbit 6.0/7.4
  (0.74, 0.57, 0.81; gate 1.1). Cesium is measured without its 60 fps cap.

Main chunk 2,860,302 bytes (+9,706, +0.34 %: the Cesium movers).
2026-10-16 17:15:24 +04:00
German Khokhlov 4623c8b616 test(qa): a comparable W1 baseline from recorded feeds and a frozen clock
Review of Task 0: the live baseline could not be compared between
commits. qa-w1-baseline now records every /api response of each layer's
run (--record=DIR, DIR/<layer>.json), replays them through puppeteer
request interception (--replay=DIR; an /api request outside the
recording answers 503) with the page clock frozen at the recording's time
(evaluateOnNewDocument: Date.now and new Date(), hence JulianDate.now and
the app clock), and diffs two runs (--compare=A,B: counts, known objects'
positions within 1e-7° and 5 cm, screen points within 0.5 px, tracking
poses within 1e-6 relative). The rules live in src/tooling/w1Baseline.js
with tests.

Recorded once (qa-artifacts/w1-feeds, git-ignored), replayed on
d50eaa28 and on HEAD: the runs agree within the tolerances for flights,
military, satellites (counts, five known objects each, tracking poses)
and launches (count). Local ADS-B has no receiver and vessels no AIS key
here: both stay empty. This is the reference for the extraction tasks.
2026-10-16 16:37:43 +04:00
German Khokhlov 76851ee949 build: guard the initial chunks by module id; main-chunk check in CI
Review of Task 0:
- build/initialChunkGuard.js, a Rollup plugin in the browser build, fails
  the build when an initial chunk (an entry and what it imports
  statically) holds a module from node_modules/three, @navaramap/* or
  postprocessing. Checked by forcing it on satellite.js, which the main
  chunk does hold: the build fails naming the chunk and module.
  navaraAssets.test now pins both build plugins.
- scripts/check-main-chunk.mjs is one script for both main-chunk checks:
  the composition (text markers in the initial chunks, sizes) and, when
  scripts/main-chunk-baseline.json exists, the phase-4 branch's gzip-9
  budget, with its exports (checkMainChunk, measureMainChunk) so the two
  branches merge into this file. The composition test moved to
  src/tooling/mainChunkComposition.test.mjs to leave phase 4's
  mainChunk.test.mjs its name.
- CI runs npm run check:main-chunk after each npm run build (unit,
  browser-gate and Windows jobs).
2026-10-16 16:22:49 +04:00
German Khokhlov f7f4e9a45c fix(time): latch the Cesium clock at creation; document the clock's limits
Review of Tasks 0–1:
- The Cesium engine clock attaches its frame latch to viewer.clock.onTick
  when the engine is created, so it runs before onTick listeners added
  later (cameraVerbs installs its own after the engine exists). Before, it
  attached on first use and would have run after them. A viewer without a
  clock still latches lazily on the scene's preUpdate.
- GlobeClock lists what is not on the app clock yet: Cesium's viewer clock
  (its lighting and property reads at currentTime), the launch replay
  timeline and the mission orbit's GMST frame (Task 15a), the satellite
  cloud's SGP4 and orbit bake (Task 14a), and wall-clock throttles. The
  plan's T14a/T15a now carry those moves.
- freeze/offset/rate change now() at once and frameTime() from the next
  frame; QA helpers request a render after moving the clock.
2026-10-16 16:18:08 +04:00
German Khokhlov 7c7422e281 Merge migration/phase2-engine (4aaf9746) into migration/phase3-layers
The phase-2 review fixes: look internals in internals.js, the groundFloor
resample throttle, tile height selection, the sharpen mirror, QA tile
cache limits, engine.groundFloor diagnostics. In navara/index.js both
sides are kept: the look gets internals.js's luminance uniforms and reads
the sun's time from the engine clock's frame time, so engine.clock stays
the one time source for the sun.
2026-10-16 15:15:24 +04:00
German Khokhlov 4aaf974600 docs(migration): the review fixes, their measurements and QA 2026-10-16 15:06:15 +04:00
German Khokhlov ce985768bc test(qa): wheel floor against the floor's own ground; polite tile cache
The Navara wheel check measures clearance against engine.groundFloor's
ground (threshold 29 m: the floor keeps 30 m), so tiles streaming under a
still camera cannot flake it. The tile cache identifies itself with the
repository URL, keeps at most two misses in flight and merges identical
ones.
2026-10-16 15:06:15 +04:00
German Khokhlov da6141be4b perf(globe): lazy hidden tiles by tile extent; floor resample interval
sampleHeight collects hidden tiles only after the shown ones miss, with
no per-node allocation, and picks the finest by tile extent (Navara
exposes no geometric error), not per-mesh radius. The ground floor
resamples on a move only after 100 ms, and reports its own ground and
margin (engine.groundFloor, Navara-only diagnostics).
2026-10-16 15:06:15 +04:00
German Khokhlov 6dad3894bd fix(globe): sharpen sky mask past depth 0.99999, not >= 1
RGBA8 depth packing rounds near the far plane.
2026-10-16 15:06:15 +04:00
German Khokhlov 5188e8869d fix(globe): the cinematic look's atmosphere scale, warm-up and sunset clouds
- The takram uniform paths move to internals.js (forEachLuminanceUniform,
  loud in dev when a path is gone); look.js sets each factor to scale x
  the recorded base value, so shared or copied vectors are never scaled
  twice, and the stars keep the base value (own vector when shared).
  Stars at intensity 120: a starfield shows from space.
- Both night-side shader variants compile on the first two frames.
- The clouds hide once the sun sets at the camera (dark puffs at dusk).
- No allocation on look frames that change nothing (frozen updates, a
  scratch camera place from livePosition).
Gallery regenerated.
2026-10-16 15:06:15 +04:00
German Khokhlov bb4e6bafa4 perf(time): the app clock's frame latch without closure boxing
The clock keeps its numbers in object fields (stored in place) instead of
closure variables, and its default wall clock is one module-level
function. Micro-measure (Node, --expose-gc, 200k calls): the flights
render-time arithmetic allocates about 25-35 B per call with the app
clock against 66-102 B with JulianDate.now/addSeconds; an engine tick
(beginFrame + ticked) about 20-30 B.
2026-10-16 15:03:23 +04:00
German Khokhlov c33ee31efc refactor(layers): W1 timestamps without JulianDate
Flights and military keep their fix history in plain milliseconds
(timeMs, whole ms as the Date the JulianDate came from held) and read
"now" from the app clock: the render-delay time, bracketing,
extrapolation, the warm-up check and the trail seed are number
arithmetic (motion.js, tracking.js, snapshotRenderer.js), as is the turn
rate and the forward synthetic fix in data/motionModel.js, which no
longer imports Cesium. The JulianDate scratch fields in state.js and the
unused JulianDate.now() in queries.js go. Launches (mission frame, panel,
selection), the tracked satellite's SGP4 epoch and the celestial ring's
sun and moon read the app clock; where a Cesium property still takes a
JulianDate it is built from the app clock's now (as JulianDate.now did
from Date.now). Wall-clock throttles and feed receipt stamps stay on
Date.now; property reads at viewer.clock.currentTime (render, frozen at
boot) move with the render code in the *a tasks.

Tests that built JulianDate history fixtures now build ms ones with the
same instants (motionModel.test, flights.test, militaryFlights.test); new
tests show the military delayed position and the tracked satellite follow
the app clock (both fail on the previous code).
2026-10-16 15:01:46 +04:00
German Khokhlov f1a2df5cfe feat(time): the app clock on the engine
src/time/clock.js is the app's own time in ms since the epoch: by default
Date.now exactly (read at each call), with freeze/offset/rate/reset for QA
and tests, and one frame time latched per engine frame (beginFrame). The
contract gains engine.clock { now, frameTime, ticked }, in the fake engine
(a controllable wall clock, fake.advanceTime, fake.appClock), the contract
suite (one frame time for every listener of a frame, latched before
preUpdate, never decreasing), Cesium and Navara.

Cesium latches on the viewer clock's onTick, which CesiumWidget runs right
before scene.render, so scene listeners the app added before the engine
existed read the same frame time; the subscription appears on first use
and destroy removes it. The viewer clock itself is untouched: in the app
it never animates (shouldAnimate false, multiplier 1, its time frozen at
boot), and its time still drives Cesium's lighting as before. Navara
latches in the engine's handler of the view's preUpdate, and the look
reads the engine clock's frame time for the sun, so the app clock is the
one time source on Navara too.

src/time is a portable root in check-import-directions: no engine
packages, no browser globals.
2026-10-16 15:01:36 +04:00
German Khokhlov 27e28373d0 test(qa): W1 baseline and the main-chunk check
scripts/check-main-chunk.mjs (npm run check:main-chunk) finds the main
chunk of a build (the Vite manifest's entry, or the module script
dist/index.html loads), follows its static imports and fails when any of
them carries three or Navara markers; it prints the sizes. Base build:
main chunk 2,850,229 bytes (gzip 881,783), no static imports, clean.

scripts/qa-w1-baseline.mjs records Cesium mode for the W1 layers (flights,
military, local ADS-B, vessels, satellites, launches): counts, five known
objects with positions and screen points, JPEGs (path, size, sha256) at
the global / New York (JFK) / orbit poses, a tracked object's camera pose
after 2 s and what its share link restores. Live sources until the
fixture sources of Task 8.
2026-10-16 14:49:24 +04:00
German Khokhlov 26968f4903 fix(ui): the Cesium engine's render.sharpen() mirrors the sharpen stage
VisualEffects sends its sharpen state to engine.render.setSharpen when a
viewer exists too; the Cesium engine keeps it as state, and the Cesium
stage itself is unchanged.
2026-10-16 14:34:17 +04:00
German Khokhlov d50eaa28af test(qa): noon sky and stars, surface heights across engines, tile cache
qa-globe-contract checks the noon sky facing the horizon (daylight blue,
no star pixels) and stars at midnight; qa-engine-shell compares
sampleHeight at the Austin startup pose across engines and reports the
wheel floor's lowest frame. QA pages load OSM and keyless terrain tiles
through a disk cache with backoff on 429. Report updated.
2026-10-16 14:29:31 +04:00
German Khokhlov 1cca89adf2 fix(globe): Navara sampleHeight under a camera looking ahead
The ground under a tilted camera is outside the frustum, where Navara
hides its tiles, so the ray found nothing. When the shown tiles miss,
the loaded hidden ones answer, the finest first (within 1.5 times its
radius, which keeps out the quadtree parent). At the Austin startup
pose it agrees with Cesium within 0.12-1.04 m.
2026-10-16 14:29:31 +04:00
German Khokhlov a8a2ad5582 refactor(ui): engine-specific sharpen defaults instead of reading 49 as 80
The baseline and the style presets name ENGINE_SHARPEN_DEFAULT, which
resolves to 49 % on Cesium and 80 % on Navara. Share links and explicit
calls restore their value unchanged.
2026-10-16 14:29:31 +04:00
German Khokhlov abf16ae8af fix(globe): a daylight sky and no daytime stars in the cinematic look
The atmosphere draws physical radiance scaled for an exposure near 10,
while the baked Google tiles need exposure 1, so the noon sky came out
dark navy with the stars showing through. The cinematic look now scales
the atmosphere's radiance-to-luminance factors (sky, aerial perspective,
clouds) by 3, leaving the tiles alone, and shows its stars only from
space or with the sun below -6 degrees at the camera. Gallery
regenerated.
2026-10-16 14:29:31 +04:00
German Khokhlov bb8c0b83c4 feat(globe): Sharpen on Navara, on at 80 % by default
The contract gains render.setSharpen({ enabled, amount }) and
render.sharpen(). Navara draws it as a custom EffectDesc: Cesium's unsharp
mask (3x3 box blur, gamma 2.2 round trip so one amount matches Cesium)
right after tone mapping and before SMAA/FXAA, skipping sky and cloud
pixels by scene depth. The Cesium engine keeps the state only; Cesium mode
still sharpens with its own post stage, unchanged. The fake engine records
it and the contract suite checks it.

Without a viewer the Sharpen slider and toggle drive the engine with the
same slider-to-amount mapping. On Navara the defaults, presets and share
links read Cesium's 49 % as 80 %.
2026-10-16 13:30:56 +04:00
daikaginza 46717548c4 Merge remote-tracking branch 'upstream/main' into fix/api-method-guard
main renamed /api/opensky to /api/flights and /api/adsblol/mil to
/api/military, and each mount now also matches its /track route, handing it on
with next(). The GET check therefore sits after that hand-off, so a /track
request is never answered with the snapshot route's 405; a new test pins that,
and fails if the check is moved above the hand-off. Tests and notes use the new
paths.
2026-10-16 09:31:49 +07:00
daikaginza 85ce864bc4 Merge remote-tracking branch 'upstream/main' into feat/opensky-compress-cached-body
main renamed the flights proxy mount from /api/opensky to /api/flights. Git
took the new mount cleanly but left this branch's own tests and notes on the
old path, and the compression test then failed. They now use /api/flights.
2026-10-16 09:24:33 +07:00
German Khokhlov ac081a008d feat(globe): Navara's cinematic look by default
navaraScenePlan gains a pure `cinematic` plan, now the default of the app's
Navara mode (`?look=photoreal|minimal` picks the others): photoreal plus
volumetric clouds (low quality, layers from 1,500 m, no ground shadows),
ACES tone mapping, brighter stars and no sun shadow maps. From space, at
rest, the aerial perspective lights the globe from the atmosphere for a
real night side and terminator; it switches off below 1,000 km so the
Google tiles keep their baked light.

look.js builds the plan and keeps it current on frames Navara draws
anyway: the atmosphere's sun date follows the wall clock (the HUD's SUN
elevation), and the clouds hide above 4 km or over high ground in view,
parking their last image so the aerial perspective shows no ghosts.
2026-10-16 02:29:46 +04:00
German Khokhlov 177446c0be feat(globe): Navara's photoreal look by default
The Navara engine now builds its own photoreal scene (aerial perspective,
tone mapping, SMAA) instead of the minimal sky-and-sun look. It costs about
5 ms a frame (S5); minimal stays available for benches.
2026-10-15 22:55:31 +04:00
German Khokhlov 179c846f3e docs(globe): pickPosition reads the last drawn frame
Also corrects the separate drape pass cost in the phase 2 report to
0.8–0.95 ms, as measured in the spike.
2026-10-15 22:36:00 +04:00
German Khokhlov 80cc0b536e style: format the phase 2 files and add them to the format scope
Prettier on the 13 new test files that failed --check; format-scope.json
now lists every file phase 2 added (src/globe/**, engineChoice, navaraMode,
cameraAngles, the QA scripts and pages, build/navaraAssets and tests).
2026-10-15 22:34:25 +04:00
German Khokhlov 312ed416e9 test(globe): pick round-trip and setTerrain checks in the contract suite
Two engine-neutral checks: pickPosition then worldToWindow lands within
1.5 px of the screen point (over New York, straight down and tilted;
points with no surface are skipped, at least one must hit), and
maps.setTerrain resolves for keyless terrain and rejects an unknown kind.
The fake engine now rejects unknown terrain kinds, as both engines do.
The Navara contract page shows OSM on keyless terrain before the suite,
since depth picks need a drawn surface.
2026-10-15 22:34:25 +04:00
German Khokhlov b65dbab709 fix(app): Navara attributions clear of the panels; QA waits for tiles
Navara's attribution list is capped at 120 px, and the shell's data
credits register after the map's, so Google's copyright lines come first
and stay visible while the card no longer covers the left panels.

qa-engine-shell polls engine.surface.tilesLoaded() (true for 1.5 s, up to
90 s) before judging the picture and prints the load time per engine;
"wheel zoom stops above the ground" is a hard check on both engines
(height >= 20 m; on Navara >= 20 m above the sampled surface), and the
Navara known gap is gone.
2026-10-15 21:27:57 +04:00
German Khokhlov 8270172086 build: name the lazy Navara engine chunk navara-engine
chunkFileNames names the chunk holding src/globe/navara
navara-engine-[hash].js; chunking is unchanged (manualChunks would pull
modules shared with the Cesium entry into it and make the entry import
it). The Cesium main chunk stays 2,849,217 B.
2026-10-15 21:27:57 +04:00
German Khokhlov 64d3297301 fix(globe): keep the Navara camera above the ground
Navara 0.2.1's camera controller has no terrain or tile collision; its
only zoom limit, minimumZoomDistance, is a distance from the Earth's
centre, so a wheel zoom near the ground went through it (to -5.9 km).

groundFloor.js runs on the engine's preRender after the camera model
reads Navara's camera. While the camera is free (no flight, no lookAt
target) and below 20 km it samples the ground under it (tiles, else
terrain, else the ellipsoid; at most every 250 ms or 20 m), feeds
Navara's clamp (minimumZoomDistance = geocentric radius of the ground
point + 30 m, sent on changes over 10 m, reset to Navara's default
during flights and above 20 km) so the wheel stops smoothly, and lifts a
camera still below ground + 30 m straight up without camera.commanded.
It only lifts, so it cannot oscillate with the shell's ground guard,
whose lift is a flight the floor stands aside for.
2026-10-15 21:27:57 +04:00
German Khokhlov 4ded3fbf15 feat(app): run gev on Navara behind ?engine=navara
The page's engine is ?engine=cesium|navara, else localStorage gev.engine,
else Cesium (src/app/engineChoice.js). Cesium mode is unchanged.

In Navara mode (src/app/navaraMode.js, loaded with import() only then,
and loading the Navara globe the same way) the scene builds the Navara
globe on the app container with no viewer; map stacks go through
engine.maps (Google 3D from the browser key, else Esri imagery); only the
shell's own data credits are registered. The layer catalog is empty and
the controls get empty layer stand-ins. The shell does not create its
viewer-bound parts (cockpit view, celestial ring, detection, tracked
readout, post-process stages); the tools install the render governor on
the engine, skip the director, annotations, drawing, imagery box, scope
mask, voice and embedded views, skip the first-run welcome, and show
"Experimental Navara engine: layers, cockpit, director and voice are
unavailable". window.__godsEyeView.viewer is null.

Navara's attribution card is kept clear of the location tray. A few
shell paths became null-safe for the missing viewer, layers or stages
without changing Cesium behaviour.

scripts/qa-engine-shell.mjs checks both modes headless: boot, viewer,
console errors, map drawn, search and fly, north/tilt/zoom/reset, orbit,
HUD, share link restore and map stacks.
2026-10-15 20:56:16 +04:00
German Khokhlov 306e3e9467 build: reach Navara only through import() and ship its worker wasm
The import-direction check now tells dynamic imports apart
(analyzeModule(source, { dynamic: true })) and rejects any static import
or re-export of src/globe/navara/** from outside that directory, so the
Navara engine can only be a lazily loaded chunk of the app.

Navara's prebuilt workers load their wasm by file name next to
themselves, which the bundler cannot see; a build-only Rollup plugin
copies the missing ones (draco decoder, font worker) into assets/ when
the bundle includes Navara, and adds nothing to a build without it.
2026-10-15 20:56:06 +04:00
German Khokhlov 72fec292d4 feat(maps): map sources for the Navara engine
createDefaultMapSources takes the engine kind. On Navara, Google 3D is
available from the browser key alone (Navara builds the tiles itself and
has no ion route to them), so it is the default and the recovery stack
when a key is set; without one the default is Esri imagery. Bing (Cesium
ion imagery) has no Navara path and is unavailable with a reason. World
terrain still needs an ion token and falls back to the keyless terrain.
MapStackController passes the kind of the engine it is given; Cesium
sources are unchanged.
2026-10-15 20:55:59 +04:00
German Khokhlov d112ecf296 fix(globe): render holds and commanded on Navara
The render governor installs on a non-Cesium globe engine as its backend:
owner-counted holds drive engine.render.setContinuous and requests go to
engine.render.request, so orbit and every other per-frame owner keep
Navara drawing while held and let it idle once the last hold goes. Cesium
stays on the viewer path its engine delegates to.

A user gesture on Navara now ends a flight without raising
camera.commanded (the user is not a new camera owner). Headless Navara QA
checks both.
2026-10-15 20:06:13 +04:00
German Khokhlov c6e1b65c67 refactor(scenes): the director needs no viewer for its camera
Camera-gesture listeners attach to the engine's canvas and the
interaction availability check tolerates a missing viewer, so a director
built on an engine alone places, flies and stops scene cameras.
2026-10-15 19:49:24 +04:00
German Khokhlov 0b9deddd70 refactor(scenes): director flights use the globe engine
Scene flights, authored placement, flight cancellation and arrival
ownership run on the engine the composition hands the director (or the
one registered for its viewer). Tracking, interactions and data packs keep
the viewer until phase 1c.
2026-10-15 19:47:51 +04:00
German Khokhlov ec38e6ef16 refactor(ui): navigation, orientation, share links and HUD use the globe engine
The application shell keeps the page's globe engine and hands it to the
navigation controller, location navigation (search, city and POI flights,
orbit, globe reset), the tilt and north-up controls, share-link save and
restore, share restoration's first-gesture listeners and the HUD. The
viewer is passed only where entity tracking still lives on it (phase 1c)
and to the HUD's basemap label context (voice); every such use is null-safe.

Share links and captured shots write heading and roll as Cesium's camera
reports them (src/cameraAngles.js): the contract's [0, 2π) pose maps an
exact 0 back to Cesium's 2π, except the roll of a vertical view, so
links stay byte-identical (heading=360, roll=360) on every engine.

Source-reading tests follow the moved code with the same checks.
2026-10-15 19:47:51 +04:00
German Khokhlov 16e63f4c50 refactor(camera): use the globe engine
The startup fly-in, orbit, world-target focus, the arrival ground guard,
arrival ownership, camera sensitivity claims, landmark/viewport/globe
flights and place search (with its view bias and near-view recovery) take
the globe engine. Callers that still hold a viewer (voice, layers until
phase 1c) reach the same engine through engineFromViewer, and arrival
ownership is keyed by the engine whichever one a caller names.

The ground guard yields on camera.commanded instead of patching the
viewer camera itself; on Cesium that event hooks the same command methods
and tracking changes, so direct Cesium callers still end it. Landmark
framing reads the frustum fov from the pose's fovy and aspectRatio.

Tests that built viewer doubles now run on the fake engine (or on the
Cesium engine over the same double), with the same expectations.
2026-10-15 19:47:43 +04:00
German Khokhlov a3e929870d feat(globe): camera.commanded, picker.pickGlobe, pose.aspectRatio and isDestroyed
Shell navigation needs to know when a new owner takes the camera (the
arrival ground guard), where the globe surface is under a window position
(orientation pivot, view-centred place search), the frustum aspect ratio
(landmark framing) and whether the engine is gone. Each member has a
contract suite check and is implemented by the Cesium engine, the Navara
engine and the fake. On Cesium, commanded hooks the viewer camera's
command methods and trackedEntityChanged only while it has listeners, so
direct Cesium calls from layers still announce a new owner.
2026-10-15 19:19:41 +04:00
German Khokhlov 840e34a881 feat(globe): bring the Navara engine up to the shell contract
- camera.livePosition: one engine-owned Cartesian3, refreshed on read
  (the model replaces its state on every move); camera.height()
- render.request(reason) takes the diagnostic reason and ignores it
- maps.current() returns the sources as passed: the base imagery on the
  globe (none while tiles replace it) and the tiles shown
- maps.releasePhotorealTiles forgets a source and takes its tiles away
  if they show
- base imagery and photoreal share one switch order; tiles off brings
  back the last base imagery with its credit; imagery chosen while
  tiles show waits hidden, credit included
- Navara QA runs the contract's maps checks with a Google 3D tiles
  source on the dev server's browser key: 58/58
2026-10-15 19:08:47 +04:00
German Khokhlov a4ac0b9b0d fix(globe): harden the Navara engine after review
- maps: photoreal hides the base imagery layer instead of deleting it,
  and a new base layer goes in before the old one goes: deleting the
  last raster layer stops Navara 0.2.1 loading terrain at the current
  view, which left terrainHeight undefined under the tiles
- Google 3D tiles open Navara's attribution card so the tiles'
  copyright lines are on screen, as Cesium shows them; onScreen
  credits do the same
- setCameraInputs merges into the camera options set through the
  engine (new setCameraOptions extension) instead of resetting them
- createNavaraGlobe disposes the view and canvas if the engine throws
- the pin test checks the installed Navara version and catches
  bracketed private access
- QA: draw-match checks at the nadir clamp, after a Navara flight and
  after a mouse drag; terrain under photoreal set right away; the
  copyright lines on screen; create/destroy/create leaves no canvas,
  frames, animation frames, workers or console errors (54/54)
2026-10-15 19:04:07 +04:00
German Khokhlov 90c8e7763a feat(globe): implement the engine on Navara
src/globe/navara builds the GlobeEngine on a Navara 0.2.1 ThreeView. The
engine keeps the camera pose in src/geo values and pushes it to Navara's
setCamera in degrees (Cesium's heading/pitch/roll convention, S4);
whatever Navara moves itself (user input, its flyTo arc) is read back
from the drawn camera each frame. Projection, picks against the
ellipsoid and the view rectangle are geo math on that pose, with
Navara's fov kept on Cesium's 60° across the wider side.

- flights: Navara flyTo in ms with mapped easings, cancelFlight via an
  empty setCamera, user input cancels a flight as in Cesium
- events: moveStart/moveEnd (500 ms still)/changed from the pose
- render: forceUpdate and animation (render on demand, S5)
- surface: S2 CPU ray over 3D-tile meshes, Navara terrain fallback
- maps: OSM/Esri raster, Re:Earth or ion quantized-mesh terrain,
  Google 3D tiles with a browser key and Google's logo
- input: DOM pointer events with ScreenSpaceEventHandler semantics
- the three private Navara members used live in internals.js, pinned
  to 0.2.1 by a test

index.js builds the product view (MSAA 4, native pixel ratio, preserved
drawing buffer, effects=minimal) for a dynamic import. The contract
QA script gains --engine=navara on a dev-only harness page: 46/46
contract and 4/4 Navara checks; Cesium stays 53/53.
2026-10-15 19:03:58 +04:00
German Khokhlov b6ac0e9c99 build: add Navara for the experimental engine
Pins @navaramap/three and @navaramap/three-default-plugin 0.2.1 with
their peers three 0.185.1 and postprocessing 6.39.4 (mapillary-js keeps
its own nested three 0.134). The dev server leaves the Navara packages
and their wasm workers out of dependency pre-bundling so wasm-bindgen
resolves its .wasm next to the package. Nothing imports Navara yet: the
Cesium-mode main chunk is byte-identical (2,837,992 bytes, same hash).
2026-10-15 19:03:32 +04:00
German Khokhlov 6dbd9c983e refactor(maps): describe map sources to the engine
The map source controller always hands engine.maps an engine-neutral
description of the registry source: imagery kind/style/accessToken and
credit, photoreal kind and credentials, terrain kind/accessToken. The
default registry carries those beside its Cesium factories. Only a
Cesium engine also gets the prebuilt provider, tileset and cached terrain
(Cesium-mode behaviour, ordering and same-provider guard unchanged); for
any other engine the controller builds nothing, falls back by description
when a setter rejects, and has no tile-failure watch.
2026-10-15 19:01:00 +04:00
German Khokhlov ab0acb0e09 feat(globe): engine-agnostic maps checks and maps.current
The contract suite gains maps checks (opt-in with mapSources: two imagery
sources and a photoreal one the engine can show): setPhotorealTiles(null)
brings back the last base imagery, the last imagery or photoreal call
decides what shows, and a released photoreal source no longer shows or
counts. They observe the new maps.current(), the source objects on screen
as passed. The fake engine follows the same semantics; qa-globe-contract
runs the checks on the app engine with the startup tileset and reapplies
the app's map stack afterwards. The contract documents map sources as
engine-neutral descriptions whose prebuilt members only the engine kind
that built them uses.
2026-10-15 19:01:00 +04:00
German Khokhlov 12c9651a67 refactor(overlays): hand the shell's overlay host the engine, not the viewer
The controls pass the scene engine to the shell, which initializes the
world overlay on it; initWorldOverlay takes only a globe engine (a viewer
or null is refused), so the shell no longer needs a viewer for its
overlay. engineFromViewer only returns the engine registered for a viewer
(the scene engine until teardown) and never builds one, so no engine can
outlive its viewer without a destroy path. Overlay and detection tests
and the allocation worker build a Cesium engine on their mock viewers and
destroy it with them; frame allocation is unchanged.
2026-10-15 18:53:57 +04:00
German Khokhlov 3e3aa0bd9f refactor(maps): use the globe engine
MapSourceController (and MapStackController) take the page's globe
engine: a globe stack becomes engine.maps.setBaseImagery with the
controller's ready provider and credit, then setPhotorealTiles(null),
then setTerrain with the cached terrain; a 3D stack becomes
setPhotorealTiles with its tileset and credit; owned tilesets are
released through maps.releasePhotorealTiles before disposal. The
controller keeps its provider/terrain/tileset caches, construction and
tile-failure fallbacks, switch generations and error watch. The scene
builds its map stack controller with its engine; without an engine the
controller applies sources to the viewer as before. The photoreal-to-
globe sequence (imagery, credit, globe, tileset) is tested end to end on
the Cesium engine.
2026-10-15 18:40:19 +04:00
German Khokhlov 83c13f330d refactor(credits): use the globe engine
Map credits (maps/credits.js) are shown through a credits capability
(engine.credits.addStatic) instead of the scene's credit display; the
shared map scene takes the engine's credits, so the Cesium engine's maps
and its credits part share one owner. Data credits (registerDataCredits,
registerDynamicCredit, show/hideOsmCredit) take the globe engine and keep
accepting a viewer from layers that still hold one; the scene registers
the data credits on its engine. The Cesium credits part reads
Viewer.creditDisplay, else the scene frame state display it aliases.
2026-10-15 18:37:02 +04:00
German Khokhlov be56144d75 refactor(services): use the globe engine
groundSnap.heightFor and meshFloorSampler.sampleMeshFloorCells take the
globe engine: engine.surface.sampleHeight for the probe,
engine.surface.tilesLoaded (and, for mesh cells, maps.photorealActive)
for readiness, camera.height for the mesh sampler's altitude gate. Callers
that still hold a viewer or scene (the layers until phase 1c) keep the
unchanged Cesium path.
2026-10-15 18:34:39 +04:00
German Khokhlov c2a0d20f3d refactor(pick): use the globe engine
pickGroundPosition takes the globe engine (picker.pickPosition, then
camera.pickEllipsoid) and still accepts a Cesium viewer from callers that
hold one (layers until phase 1c), with one shared ground-or-ellipsoid
rule. isSurfacePick classifies engine pick results exactly as the Cesium
picks they wrap.
2026-10-15 18:32:45 +04:00
German Khokhlov aaa6074663 refactor(overlays): use the globe engine
The world overlay host takes its frames (frames.postRender), camera events
(camera.moveEnd), canvas size (host.size), projection matrix
(projector.viewProjection), horizon test (projector.horizonVisible) and
render requests (render.request('world-overlay')) from the globe engine.
initWorldOverlay accepts an engine or, for callers that still hold a
viewer, resolves the viewer's engine through engineFromViewer.

Contract: camera.position(result) becomes camera.livePosition, one
engine-owned object holding the current position, so per-frame readers
and paint lanes keep a live camera position without allocating; engine
events gain raiseValue for argument-list-free frame dispatch. Steady-state
frame allocation is unchanged (worker probe, 30 chunks: 2696/9536/38593
B/frame before and after; detection 473749 -> 408085).
2026-10-15 18:32:40 +04:00
German Khokhlov 58a196374f feat(globe): engine members and the viewer bridge for the shell
Contract: camera.position(result?) and camera.height() for per-frame
readers, render.request(reason?), maps.releasePhotorealTiles, sources
with a caller-owned ready provider, isGlobeEngine. engine.maps now hides
every tileset it has shown, lets the last imagery/photoreal call win, and
setPhotorealTiles(null) restores the last base imagery and its credit
instead of leaving the photoreal credit on a bare globe.

Cesium engine: reads the Viewer camera/canvas aliases, detaches bridged
events through the remover Cesium returns, raises frame timestamps
without allocating, re-aims the horizon occluder only when the camera
moved, and renders the scene directly when the governor governs another
viewer (or none yet). engineFromViewer returns the registered scene
engine of a viewer, else builds and caches one. The render governor stays
the Cesium render backend under engine.render and gains
isRenderGovernorInstalled.
2026-10-15 18:18:19 +04:00
German Khokhlov a5c98c6b07 feat(app): construct the globe engine with the viewer
The scene builds the page's Cesium engine right after the viewer
(src/app/globeEngine.js), names the startup Google tileset to it once
loaded, returns it beside the viewer and destroys it before the viewer.
The debug handle exposes it as window.__godsEyeView.engine. Nothing
consumes it yet; render holds taken before the governor is installed
apply at install.

qa-globe-contract now runs on the app's engine (self-built only when the
app has none); a throwaway engine checks creation and destroy leave the
scene untouched.
2026-10-15 18:02:11 +04:00
German Khokhlov ab2368f3f3 fix(globe): tighten the Cesium engine after review
Map sources are applied through one src/maps/sceneApply.js helper shared by MapSourceController and engine.maps (live layer kept for an unchanged provider, providers cached per source). The contract gains flyTo endTransform and rectangle destinations, flyToSphere easing, drillPick width/height and pick content, with Cesium's own flyToSphere range. Input skips listeners removed during dispatch and stays inert after destroy.
2026-10-15 17:55:00 +04:00
German Khokhlov d831cbd23a feat(globe): implement the engine on Cesium
createCesiumEngine wraps an existing viewer without changing it: camera, picker, projector, frames, render (through the render governor), surface, input, credits and maps, with geo values out and Cesium values in at the boundary. Map and surface code is shared with src/maps and services/groundSnap instead of copied. scripts/qa-globe-contract.mjs runs the contract suite against the engine in headless Chrome; the camera-events check now waits for the previous move to end, as Cesium's moveStart marks a move from rest.
2026-10-15 17:43:05 +04:00
German Khokhlov cd94a26b9f fix(globe): document Cesium's default view angles and check them
FlightOptions and setView now state Cesium's defaults for missing angles
(heading 0, pitch -pi/2, roll 0), enforced by a contract-suite check. The
suite also checks a horizon-bounded view rectangle and an undefined one
when the camera looks away from the Earth.
2026-10-15 17:21:31 +04:00
German Khokhlov 708c96b6f6 fix(globe): tighten the engine contract after review
Engine events get their own documented semantics (single registration,
branded instances); the fake follows Cesium's roll sign, angle ranges,
synchronous zero-duration flights, flyToSphere defaults and
computeViewRectangle. The contract adds viewProjection, horizonVisible,
flightActive, lookAtTransform/transform, zoomIn/zoomOut, input modifiers,
terrainHeight and clampToHeight, with geometric suite checks. Engine
import rules now follow each engine graph transitively.
2026-10-15 17:15:35 +04:00
German Khokhlov 528ec75f32 feat(globe): add the engine contract and its test suite
src/globe/contract.js defines the engine-neutral GlobeEngine capabilities
(JSDoc), createEngineEvent with Cesium.Event semantics and
assertGlobeEngine. src/globe/testing adds a geo-based fake engine for shell
tests and an engine-agnostic contract suite that the fake passes in Node.
check-import-directions keeps the contract free of cesium, three and
@navaramap, and each engine directory free of the other engine.
2026-10-15 17:01:08 +04:00
German Khokhlov f2fcd82290 test(qa): click street-level coverage at a verified pixel
The draped coverage line is about one pixel wide and can sit on a pixel
boundary: the probe hit at a fractional x while the click at the integer
pixel missed. Wait for tiles, click only at an integer pixel that picks the
line right before the click (off the centre column first), retry up to 4x.
2026-10-15 00:19:52 +04:00
German Khokhlov e3061872cc refactor(streetLevel,traffic,transit,firms,installations,radio,bikeshare,cables,detection): take vector maths from src/geo
Street level view/cameraFollow/groundClick/index, traffic footprint/viewport/ingestion, transit viewport/trails/state/rendering, FIRMS state/viewport, installations controls/viewport/state, radio queries/navigation/state, bikeshare viewport, submarine cable overlay and data/detection now use src/geo for Math, Cartesian2/3, Cartographic and WGS84 maths. Cesium stays for Rectangle, Ray, BoundingSphere, HeadingPitchRange, EasingFunction, EllipsoidGeodesic, EllipsoidalOccluder and the Ellipsoids passed into Cesium (pickEllipsoid, occluders). traffic/state.js keeps its Cesium lerp scratch (written per frame by Cesium.Cartesian3.lerp in animation.js; a geo scratch cost +4-7% there) and traffic/timing.js stays on Cesium (a test keeps it source-equivalent to model.parseRoads, outside this batch). street-level-layer, traffic-layer, installations-layer, bikeshare-layer and submarine-cables own the src/geo files.
2026-10-15 00:19:13 +04:00
German Khokhlov a915bdb3e5 refactor(launches,cctv,alpr): take vector maths from src/geo
Launches selection/replay/panel/policy/policyHelpers/state/camera, CCTV geometry/model/projection/navigation/cards/geometryQueue/state and ALPR model/visuals now use src/geo for Math, Cartesian2/3, Cartographic, Matrix3/4, Quaternion and ENU frames. launches/paths.js keeps Cesium (EllipsoidGeodesic). In camera.js, EllipsoidGeodesic, BoundingSphere.fromPoints/union and the BoundingSphere constructor stay on Cesium; the geo Cartographics passed into EllipsoidGeodesic are only read field by field and cloned there (computeProperties). The CCTV plane orientation stays a Cesium Quaternion and plane dimensions a Cesium Cartesian2 (entity ConstantProperty values). launches-layer and alpr-cameras own the src/geo files.
2026-10-15 00:18:35 +04:00
German Khokhlov ee8b94d987 refactor(flights,military,localAdsb): take vector maths from src/geo
Dead reckoning, the grounded display floor, snapshot rendering, queries and layer scratch state now use src/geo. Outputs are bit-identical to the previous batch (frozen-clock probe over flights, military, local ADS-B and orbits); per-frame paths are as fast as base 132b9e96 or faster (see docs/migration/phase1a-report.md).
2026-10-15 00:17:56 +04:00
German Khokhlov 2b83606765 refactor(vessels,satellites,awareness): take vector maths from src/geo
Moves the Cartesian3/Cartographic/Matrix3/Matrix4/Math calls of the vessel, satellite and Contacts (awareness) layers, plus flights controller/enrichment, to src/geo. The satellites-layer package boundary now owns the src/geo modules. Awareness keeps Cesium.Cartesian3.clone for the resolved subject position, which reaches the installations anchor (anchor.test pins a Cesium instance).
2026-10-15 00:17:17 +04:00
German Khokhlov 1a69368ae6 refactor(ui,voice,annotations,scenes): take vector maths from src/geo
Move the Cartesian3/Cartographic/Matrix4/Transforms/Ellipsoid/Math calls of
the second batch (cockpit camera, context and controller; camera orientation
controls; application shell; location navigation; the annotation resolver
and engine; voice actions and pointer pick; the scene director; the embed
app) to src/geo. Behaviour is unchanged: same argument order, same `result`
semantics.

Kept on Cesium: out-of-scope classes (HeadingPitchRange, BoundingSphere,
Intersect, EllipsoidalOccluder, EasingFunction, SceneTransforms, JulianDate),
WGS84 where it goes into pickEllipsoid or EllipsoidalOccluder,
Matrix4.IDENTITY handed to lookAtTransform, and the Cartesian2 window
positions that reach scene.pickPosition: Cesium keys its per-frame pick
cache on windowPosition.toString(), which a plain object would collapse to
one "[object Object]" key.

The Contacts-subject harness in gevActions.test stubbed
Cesium.Cartographic.fromCartesian to place its subject; it now builds a real
subject position at the same coordinates with geo.cartesian3.fromDegrees.
The cockpit-controls and scenes package-boundary groups now own the src/geo
files.
2026-10-15 00:16:38 +04:00
German Khokhlov d886be68a8 refactor(camera,data): take vector maths from src/geo
Move the Cartesian3/Cartographic/Matrix/Quaternion/Transforms/Math calls of
the first batch (src/data motion, anchor, tracked-camera and icon helpers;
the root camera, locations, HUD, share-link, world-focus, ground-guard,
orbit and cloud modules; services/groundSnap) to src/geo. Behaviour is
unchanged: same argument order, same `result` semantics.

Kept on Cesium: out-of-scope classes (JulianDate, Axis, HeadingPitchRange,
BoundingSphere, Rectangle, EasingFunction, EllipsoidalOccluder with its
WGS84 and seed vector, SceneTransforms) and Matrix4.IDENTITY wherever it is
handed straight to a Cesium camera call (locations.test pins its identity).

synthesizeForwardKinematicsFix now returns a plain position, so the two
snapshot renderers clone it with the static Cesium.Cartesian3.clone instead
of the instance method. The package-boundary groups that reach these
modules now own the src/geo files.
2026-10-15 00:15:59 +04:00
German Khokhlov 4a92ed8377 feat(geo): add src/geo, Cesium's vector maths without Cesium
An engine-neutral port of the CesiumJS 1.138 core maths gev uses: angle
maths, the WGS84 ellipsoid and geodetic coordinates, 2D/3D vectors, 3x3/4x4
matrices, quaternions and local east-north-up / heading-pitch-roll frames.
Formulas and operation order match Cesium, so outputs are bit-identical
(golden tests pin the centre, pole, -0 and quaternion branches).

- Values are plain-data instances of module-private constructors (own
  enumerable fields only), so geo's hot functions keep their own V8 hidden
  classes; matrices are plain column-major arrays Cesium accepts.
- Cesium's debug argument checks are ported with the same conditions, order
  and DeveloperError messages, gated on DEBUG (src/geo/debug.js): on in Node
  and the dev server, folded away by vite build, as Cesium's Build strips
  them. A release-parity test compares DEBUG=false geo with Build/Cesium.
- The import-direction check forbids cesium and @cesium/* in src/geo;
  CODE-BOUNDARIES.md and THIRD_PARTY_NOTICES.md document the port.
2026-10-15 00:15:20 +04:00
Wilmer AlzateandClaude Opus 5.5 81f49406b8 perf(flights): skip dead-reckoning contacts that stay behind the horizon
The fleet tick dead-reckoned, floored and distance-tested every contact on
every ~80 ms tick, then hid the ones behind the horizon. The worldwide feed
carries ~13k contacts; zoomed in, well under 1% are on screen, so nearly all
of that work produced a hidden billboard.

A contact that is already hidden and still behind the horizon at its last
position now re-reckons only on its turn in a staggered one-in-12-ticks
rotation (about once a second, HIDDEN_CONTACT_DR_TICKS). Anything that
position puts in view, and any contact with a fleet model, takes the full
pass immediately, so on-screen behaviour is unchanged. Hidden-inclusive
proximity queries (getNearby includeHidden, getAllPositions) read positions
at most about a second old, well inside the 30 s poll's uncertainty.

Measured in Chrome on a Radeon 780M, ~12.9k contacts, 56 on screen over
Colombia at 4 km, same view before and after:
  fleet tick   7.66 -> 2.03 ms/frame, longest tick 50.9 -> ~16 ms
  frame        20.6 -> 14.0 ms, 42.9 -> 57.8 FPS
Teleporting the camera to Madrid showed 975 contacts within 100 ms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 12:31:59 -05:00
Kevin Rajan 28f3243090 feat(cyclones): publish explicit selection to shared context 2026-09-29 00:20:16 -05:00
daikaginza e7dd71e9bc fix(server): read the whole q value, not a numeric prefix
Number.parseFloat accepts any valid numeric prefix, so `gzip;q=1oops` read as 1
and the out-of-range `gzip;q=2` was accepted — both contradicting the comment
two lines above, which promises that a quality this cannot read confidently
fails closed. Matching the qvalue grammar of RFC 9110 section 12.4.2 makes the
code do what it already claimed to do.

Thanks to kvnloo for catching it on review.
2026-09-29 11:13:36 +07:00
daikaginza 47b13ab18e docs(changelog): note OpenSky snapshot compression (#795) 2026-09-29 10:58:18 +07:00
daikaginza daed0be99a feat(server): compress the cached OpenSky snapshot for clients that accept gzip
The ~860 KB snapshot is held for an adaptive 9-300 s TTL and was served as
identity bytes to every request; no response in server/ set Content-Encoding or
Vary. Gzip it once per refresh beside the cached body, off the event loop, and
return the body and the header that names it together so the two can never
disagree. Live against an anonymous snapshot: 874,002 B to 258,012 B, 3.39x,
decompressing byte-identical to the uncompressed response.
2026-09-29 10:57:45 +07:00
daikaginza 2e5dfce1af fix(server): declare Allow: GET on the new 405 responses
RFC 9110 section 15.5.6 requires a 405 to name the methods it accepts, and
/api/radio/stations already does this in-tree. Thanks to kvnloo for catching it
on review. The same gap on the pre-existing handlers is tracked in #809 and is
deliberately left alone here.
2026-09-29 10:56:20 +07:00
daikaginza fb6b4e34f0 docs: changelog entry for the read-only proxy method contract (#794) 2026-09-29 10:52:57 +07:00
daikaginza b09db55149 fix(server): declare the method contract on the three read-only query proxies
/api/opensky, /api/adsblol/mil and /api/route answered any verb with the same
body while ignoring the request entirely. Measured against a running server
before the change: POST /api/opensky returned 200 with 833,759 bytes of live
state vectors, POST /api/adsblol/mil returned 47,372 bytes. Their siblings do
not: /api/geocode, /api/weather and /api/transit answer 405, and /api/overpass
is deliberately POST-only.

These three are read-only query proxies, so they now answer 405 to anything but
GET, using the wording and shape /api/geocode already uses. The refusal comes
first, before rate limiting and before any upstream call, which the tests
assert by stubbing fetch to throw.

Scope, stated plainly: this is a consistency and intent fix. No rate limiter
was being bypassed — /api/route already limits every request regardless of
verb, and the two aircraft routes have no limiter at all, only 9s and 12s
server-side caches that bound upstream cost for any method.

One existing fixture in src/search/geospatial.test.mjs built a request without
a method, which a real Node request always has; it now sets GET.

Verified on a running server after the change: GET still 200 on all three,
POST 405 on all three, /api/overpass and the other proxies unchanged.
Suite goes from 5090 to 5092 tests, 0 failures.
2026-09-29 10:52:26 +07:00
Kevin Rajan cacbbc972b chore: remove temporary formatter diagnostics 2026-09-28 20:57:48 -05:00
Kevin Rajan 989e740d6e style(voice): format viewport policy tests 2026-09-28 20:57:43 -05:00
Kevin Rajan 5195012c7f debug(format): print viewport policy canonical output 2026-09-28 20:55:51 -05:00
Kevin Rajan 86969f8a11 chore(format): adopt viewport policy tests 2026-09-28 20:53:15 -05:00
Kevin Rajan 3a40117e06 test(voice): pin viewport escalation reasons 2026-09-28 20:53:12 -05:00
Kevin Rajan 073ffedc1f feat(voice): route viewport decisions to diagnostics 2026-09-28 20:52:54 -05:00
Kevin Rajan 0c2e5727a5 feat(voice): reason-code viewport context escalation 2026-09-28 20:52:44 -05:00
monwissandClaude Opus 5.5 da689e03ac refactor(providers): move the tile budget helpers out of the TomTom module
utcDayKey, normalizeBudget and isOverBudget move unchanged to
src/data/tileBudget.js. Nothing in them was TomTom-specific; the next
keyed tile proxy (Xweather) counts a monthly allowance through the same
two functions. tomtomTiles.js re-exports them, so the traffic provider
and its tests keep their imports.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 11:34:40 -04:00
2122 changed files with 411406 additions and 69513 deletions

No files matched your search

+36 -6
View File
@@ -218,6 +218,7 @@ AISSTREAM_API_KEY=
# CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL # CCTV_TFL_MAX_SOURCES=250 # cap cameras loaded from TfL
# CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras # CCTV_ONTARIO_ENABLED=1 # set 0 to disable Ontario 511 cameras
# ONTARIO_511_API_KEY= # free developer key: sign up at 511on.ca/my511/register, then request it at 511on.ca/developers/doc # ONTARIO_511_API_KEY= # free developer key: sign up at 511on.ca/my511/register, then request it at 511on.ca/developers/doc
# OTTAWA_CAMERA_CERTIFICATE= # City of Ottawa Traffic Camera Images API certificate (free; request it via the form on traffic.ottawa.ca 'About the data'); without it the Ottawa pack is skipped
# CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511 # CCTV_ONTARIO_MAX_SOURCES=1000 # cap cameras loaded from Ontario 511
# CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack # CCTV_FINTRAFFIC_ENABLED=1 # set 0 to drop the Fintraffic (Finland) pack
# CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog # CCTV_FINTRAFFIC_MAX_SOURCES=300 # cap cameras loaded from the Fintraffic catalog
@@ -238,6 +239,8 @@ AISSTREAM_API_KEY=
# CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL # CCTV_CALGARY_ROWS_URL= # override the Open Calgary catalog URL
# CCTV_CATALONIA_ENABLED=1 # set 0 to disable the Servei Català de Trànsit cameras # CCTV_CATALONIA_ENABLED=1 # set 0 to disable the Servei Català de Trànsit cameras
# CCTV_CATALONIA_MAX_SOURCES=150 # cap cameras loaded from Catalonia (~130 exist today) # CCTV_CATALONIA_MAX_SOURCES=150 # cap cameras loaded from Catalonia (~130 exist today)
# CCTV_ICELAND_ENABLED=1 # set 0 to disable the Vegagerðin (Iceland) road cameras
# CCTV_ICELAND_MAX_SOURCES=250 # cap cameras loaded from Iceland (~500 views exist today)
# CCTV_HK_ENABLED=1 # set 0 to disable the Hong Kong Transport Department cameras # CCTV_HK_ENABLED=1 # set 0 to disable the Hong Kong Transport Department cameras
# CCTV_HK_MAX_SOURCES=1100 # cap cameras loaded from Hong Kong (~1,013 exist today); min 8, max 1200 # CCTV_HK_MAX_SOURCES=1100 # cap cameras loaded from Hong Kong (~1,013 exist today); min 8, max 1200
# CCTV_HK_CAMERAS_URL= # override the DATA.GOV.HK camera locations XML URL # CCTV_HK_CAMERAS_URL= # override the DATA.GOV.HK camera locations XML URL
@@ -268,15 +271,15 @@ AISSTREAM_API_KEY=
# CCTV_HOUSTON_CAMERAS_URL= # override the Houston TranStar catalog URL # CCTV_HOUSTON_CAMERAS_URL= # override the Houston TranStar catalog URL
# CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams # CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams
# CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL # CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL
# CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_OTTAWA_ENABLED=1 # set 0 to skip City of Ottawa cameras (also needs OTTAWA_CAMERA_CERTIFICATE)
# CCTV_OTTAWA_MAX_SOURCES=250 # cap Ottawa cameras (8-500, nearest downtown first)
# CCTV_QUEBEC_ENABLED=1 # set 0 to skip the Québec camera locations (MTQ, Montréal, scenic links)
# CCTV_QUEBEC_VIDEO_ENABLED= # off by default; set 1 to let viewers play Québec 511 clips in their browser after pressing Play (511 terms of use unconfirmed; see DATA_SOURCES.md)
# CCTV_WORLD_CAMERAS_ENABLED=1 # set 0 to skip the Oregon and MLIT (Japan) still cameras
# CCTV_MAX_SOURCES=7350 # catalog-wide ceiling: 4,000 slots shared round-robin, the rest reserved for Québec and World
# CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry
# CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured
# Optional: Data Library (DATA LAYERS ▸ Browse data library). Every entry is
# fetched live from its provider; nothing is hosted here. See docs/DATA-LIBRARY.md.
# GEV_LIBRARY_ENABLE= # comma-separated ids of operator opt-in entries to serve (packs use their host's own switch, e.g. CCTV_NYC_ENABLED)
# GEV_LIBRARY_DISABLE= # comma-separated ids to remove from this instance entirely
# Optional: comma-separated Overpass URLs; use only instances you run or pay for. # Optional: comma-separated Overpass URLs; use only instances you run or pay for.
# Also enables voice place search (osm_query, "hospitals in Kathmandu"). # Also enables voice place search (osm_query, "hospitals in Kathmandu").
OVERPASS_UPSTREAMS= OVERPASS_UPSTREAMS=
@@ -322,3 +325,30 @@ GEMINI_CHAT_MODEL=gemini-3.5-flash-lite
# Each reserves up to 40 stations within the existing 750-station directory. # Each reserves up to 40 stations within the existing 750-station directory.
# Restart after changing. Leave blank for the existing global selection. # Restart after changing. Leave blank for the existing global selection.
# RADIO_PRIORITY_COUNTRIES=IN,PL # RADIO_PRIORITY_COUNTRIES=IN,PL
# Optional: a news server for the News layer (its /api/news/hotspots, /feed
# and /outline routes), such as https://news.example. The local server
# forwards only the layer's own GET requests there, with a 15 s timeout. Set,
# it wins over the settings below. SERVER-SIDE ONLY.
# GEV_NEWS_ORIGIN=
#
# Without a news server Core reads the News layer itself. SERVER-SIDE ONLY.
# Conflicts need no key: the last 24 h of GDELT events (its 15-minute export
# files, about 100 KB each, about 10 MB a day; violence only, one report per
# story at GDELT's place for it), titled from GDELT's GKG files (about 6 MB
# each, about 23 MB an hour: the newest 3 h are read at start, then each new
# one). Optional: Liveuamap's mapped reports beside them (paid API,
# https://liveuamap.com/promo/api), read once every 5 minutes.
# LIVEUAMAP_API_KEY=
# Local: the day's news from the outlets of the country in view (free plan
# of 50 points a day, https://worldnewsapi.com/console/), read once per
# country every 30 minutes, about 1.5 points a read. Without it Local says
# "key required". Each country's news is kept in memory for at most 1 hour.
# World News API's terms (https://worldnewsapi.com/terms/) allow caching
# only with its prior written permission: get that before setting the key.
# The app's Data attribution popover credits World News API with a link.
# WORLDNEWSAPI_KEY=
# The most World News API reads a day (default 30, about 45 points, inside
# the free plan). After that Local says the daily limit is reached.
# Raise it on a paid plan.
# WORLDNEWSAPI_DAILY_READS=30
+4 -4
View File
@@ -53,16 +53,16 @@ jobs:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: npm run layer-token:check -- --base-ref origin/main run: npm run layer-token:check -- --base-ref origin/main
- name: Check Data Library manifests and share tokens
if: github.event_name == 'pull_request'
run: npm run library:check -- --base-ref origin/main
- name: Run unit tests - name: Run unit tests
run: npm test run: npm test
- name: Build production bundle - name: Build production bundle
run: npm run build run: npm run build
- name: Main chunk size budget
if: matrix.node == '24.22.2'
run: npm run check:main-chunk
street-level-browser: street-level-browser:
# Hermetic: every Mapillary request is answered from fixtures, so the # Hermetic: every Mapillary request is answered from fixtures, so the
# dummy token below never leaves the page. # dummy token below never leaves the page.
+40
View File
@@ -0,0 +1,40 @@
name: Announce merged PRs in Discord
on:
pull_request_target:
types: [closed]
permissions: {}
jobs:
announce:
if: github.repository == 'bilawalsidhu/gods-eye-view' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
# This uses the trusted base workflow and never checks out or runs PR code.
- name: Post to Discord contributions
shell: bash
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_CONTRIBUTIONS_WEBHOOK_URL }}
run: |
if [[ ! "$DISCORD_WEBHOOK_URL" =~ ^https://discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9_-]+$ ]]; then
echo '::error::Set DISCORD_CONTRIBUTIONS_WEBHOOK_URL to the Discord channel webhook URL.'
exit 1
fi
payload=$(jq -ce '
.pull_request | {
allowed_mentions: {parse: []},
embeds: [{
title: ("Merged #" + (.number | tostring) + ": " + (.title | .[0:115])),
url: .html_url,
footer: {text: ("By " + .user.login + " · into " + (.base.ref | .[0:100]))}
}]
}
' "$GITHUB_EVENT_PATH")
curl --silent --show-error --fail --output /dev/null \
--connect-timeout 10 --max-time 20 --retry 3 --retry-max-time 50 \
--header 'Content-Type: application/json' --data-binary "$payload" \
--url "$DISCORD_WEBHOOK_URL?wait=true"
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
- An application's own tool can take the pointer as Draw does, and its click
handlers can yield while a tool holds it, as the layers' do (`pointer` from
`createApplicationTools`) (samehkhamis, #1641).
## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language ## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language
- Show a Street Level view facing 359.5° or more as 0°. The viewer caption - Show a Street Level view facing 359.5° or more as 0°. The viewer caption
+3 -4
View File
@@ -128,9 +128,8 @@ prints anything; never fix that by renaming or deleting PR A's published token.
The highest-leverage places to jump in: The highest-leverage places to jump in:
- **🌆 Add a CCTV source pack.** Austin is the reference camera source. Adding another city means a clean public camera catalog with coordinates, attribution, and server-registered frame URLs (the proxy only fetches registered URLs — never client-supplied ones, see [SECURITY.md](SECURITY.md)). City packs are the best first lane. - **🌆 Add a CCTV source pack.** Austin is the reference camera source. Follow the [CCTV integration workflow](docs/CCTV_INTEGRATION.md) for source review, provider registration, proxy security, tests, and attribution. The proxy only fetches server-registered URLs — never client-supplied ones; see [SECURITY.md](SECURITY.md).
- **📚 Add a Data Library entry.** A regional or niche source that is not a core layer belongs in the Data Library: one JSON manifest pointing at a live API, provider-hosted file or tile endpoint, with its licence and attribution, and no JavaScript. See [docs/DATA-LIBRARY.md](docs/DATA-LIBRARY.md). Library entries use their own share-token ledger, not the layer tokens below. - **🛰️ Add or improve a data layer.** Layer factories live in `src/layers/<family>/`, with source, record, controller and renderer owners implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template, and see [Adding a new layer](#adding-a-new-layer) for what the PR needs.
- **🛰️ Add or improve a data layer.** Layer factories live in `src/layers/<family>/`, with source, record, controller and renderer owners implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template.
- **🎙️ Extend voice control.** Voice arguments are defined in `src/voice/actionSchemas.js`, with server-side descriptions in `server/providers/openai/tools.js` and client-side execution in `src/voice/gevActions.js`. Keep the tool surface tight and the responses honest (confirm only what actually happened). - **🎙️ Extend voice control.** Voice arguments are defined in `src/voice/actionSchemas.js`, with server-side descriptions in `server/providers/openai/tools.js` and client-side execution in `src/voice/gevActions.js`. Keep the tool surface tight and the responses honest (confirm only what actually happened).
- **🎨 Add a visual style.** Styles are GLSL post-process shaders in `src/styles/`. - **🎨 Add a visual style.** Styles are GLSL post-process shaders in `src/styles/`.
- **🐛 Fix bugs / improve the first-run experience.** See [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md). - **🐛 Fix bugs / improve the first-run experience.** See [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md).
@@ -229,7 +228,7 @@ ownership and adoption process.
1. Branch off `main`. 1. Branch off `main`.
2. Keep `npm run build`, `npm test`, and `npm run test:track` green and avoid new console errors, plus the [feature gate](#feature-regression-gates) for the area you touched. 2. Keep `npm run build`, `npm test`, and `npm run test:track` green and avoid new console errors, plus the [feature gate](#feature-regression-gates) for the area you touched.
3. If you change runtime behavior, update `docs/CURRENT-STATE.md` in the same PR, and add your changelog entry as `changelog.d/<pr-number>-<slug>.md` instead of editing `CHANGELOG.md` — see [changelog.d/README.md](changelog.d/README.md). Distinct filenames don't conflict; a release assembles the directory into `CHANGELOG.md`. A documentation claim that needs a regression test should assert against `docs/CURRENT-STATE.md` or `README.md`, which a PR still edits directly. 3. If you change runtime behavior, update `docs/CURRENT-STATE.md` in the same PR, and add your changelog entry as `changelog.d/<pr-number>-<slug>.md` instead of editing `CHANGELOG.md` — see [changelog.d/README.md](changelog.d/README.md). Distinct filenames don't conflict; a release assembles the directory into `CHANGELOG.md`. A documentation claim that needs a regression test should assert against `docs/CURRENT-STATE.md` or `README.md`, which a PR still edits directly.
4. If you add or change a data source, update [DATA_SOURCES.md](DATA_SOURCES.md) with its license and attribution (for a Data Library entry, `npm run library:docs` writes its row). **Don't add data you don't have the right to redistribute** — fetch it at runtime instead. 4. If you add or change a data source, update [DATA_SOURCES.md](DATA_SOURCES.md) with its license and attribution. **Don't add data you don't have the right to redistribute** — fetch it at runtime instead.
5. Describe what you changed and how you verified it (screenshots welcome for anything visual). 5. Describe what you changed and how you verified it (screenshots welcome for anything visual).
6. If you add a new layer, fill in the new-layer section of the PR template. See [Adding a new layer](#adding-a-new-layer). 6. If you add a new layer, fill in the new-layer section of the PR template. See [Adding a new layer](#adding-a-new-layer).
+83 -127
View File
@@ -5,7 +5,7 @@ God's Eye View's **code** is [MIT](LICENSE)-licensed. **The MIT grant covers the
How to read this: How to read this:
- **The non-permissive datasets are carved out, not omitted.** Some bundled data (e.g. TeleGeography, CC BY-NC-SA) isn't MIT-compatible. Rather than hide it, we **bundle it with a clear license carve-out** so the app works out of the box — but it stays under the provider's terms. - **The non-permissive datasets are carved out, not omitted.** Some bundled data (e.g. TeleGeography, CC BY-NC-SA) isn't MIT-compatible. Rather than hide it, we **bundle it with a clear license carve-out** so the app works out of the box — but it stays under the provider's terms.
- **If your use doesn't fit a dataset's license, remove that dataset.** Most importantly: TeleGeography is **NonCommercial** — commercial users must delete it (or license it from TeleGeography). It's one self-contained folder. - **If your use doesn't fit a dataset's license, remove that dataset.** Most importantly: TeleGeography is **NonCommercial** — commercial users must delete it (or license it from TeleGeography). It's one self-contained folder. **EOX Sentinel-2 cloudless 2020** is also **NonCommercial** (CC BY-NC-SA 4.0): remove that basemap or obtain a commercial licence from EOX.
- **Attribution is shown in-app** and listed here. Keep it intact. The required Google/Cesium credit renders on the on-globe credit line (bottom-left, `#cesium-credits`), and every per-layer credit below is registered into the expandable **"Data attribution"** lightbox on that line (`src/data/dataCredits.js` → `viewer.creditDisplay.addStaticCredit`). Both stay visible in clean-view and recording modes. - **Attribution is shown in-app** and listed here. Keep it intact. The required Google/Cesium credit renders on the on-globe credit line (bottom-left, `#cesium-credits`), and every per-layer credit below is registered into the expandable **"Data attribution"** lightbox on that line (`src/data/dataCredits.js` → `viewer.creditDisplay.addStaticCredit`). Both stay visible in clean-view and recording modes.
- **Bundled model attribution lives beside the model files.** [`public/models/README.md`](public/models/README.md) records each shipped model's creator, source, license, and modification status. - **Bundled model attribution lives beside the model files.** [`public/models/README.md`](public/models/README.md) records each shipped model's creator, source, license, and modification status.
@@ -30,15 +30,23 @@ How to read this:
| **adsb.lol point API** | Bounded live-flight fallback when OpenSky has no usable snapshot; GNSS Integrity layer (aircraft-reported NIC/NACp and GPS-loss integrity facts, via `/api/gnss-integrity`, 60 s server cache) | ODbL 1.0 | adsb.lol contributors; `api.adsb.lol/v2/lat/{lat}/lon/{lon}/dist/{radius}` | | **adsb.lol point API** | Bounded live-flight fallback when OpenSky has no usable snapshot; GNSS Integrity layer (aircraft-reported NIC/NACp and GPS-loss integrity facts, via `/api/gnss-integrity`, 60 s server cache) | ODbL 1.0 | adsb.lol contributors; `api.adsb.lol/v2/lat/{lat}/lon/{lon}/dist/{radius}` |
| **adsb.lol** | Military flights + aircraft traces | ODbL 1.0 | "adsb.lol" (ODbL) | | **adsb.lol** | Military flights + aircraft traces | ODbL 1.0 | "adsb.lol" (ODbL) |
| **adsbdb** (`api.adsbdb.com`) | Aircraft type, model name and registration by ICAO hex for enriched flights; airline and origin/destination airports for the tracked flight's callsign | No licence or rate limit is published for the service. Its own credits: aircraft data from PlaneBase, ICAO-to-N-number conversion by Guillaume Michel, and flight route data that "is the work of David Taylor, Edinburgh and Jim Mason, Glasgow, and may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh" | "adsbdb — api.adsbdb.com" (courtesy); route data credited to David Taylor, Edinburgh, and Jim Mason, Glasgow | | **adsbdb** (`api.adsbdb.com`) | Aircraft type, model name and registration by ICAO hex for enriched flights; airline and origin/destination airports for the tracked flight's callsign | No licence or rate limit is published for the service. Its own credits: aircraft data from PlaneBase, ICAO-to-N-number conversion by Guillaume Michel, and flight route data that "is the work of David Taylor, Edinburgh and Jim Mason, Glasgow, and may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh" | "adsbdb — api.adsbdb.com" (courtesy); route data credited to David Taylor, Edinburgh, and Jim Mason, Glasgow |
| **Planespotters.net** (`api.planespotters.net/pub/photos/hex/{hex}`) | One thumbnail photo of the tracked aircraft, by ICAO hex, in the CONTEXT panel's CONTACTS view under the tracked flight's name | [Planespotters photo API terms](https://www.planespotters.net/photo/api); each photo remains the copyright of its photographer. Thumbnails may be hotlinked on the condition that the photographer is named next to the photo and the photo links to its Planespotters page with a followable link | "Photo © {photographer}" beside every photo that names one, linked to its Planespotters page; "Planespotters.net" in Data attribution |
| **AISStream.io** | Live vessels (AIS) | Free, beta, no formal ToS; AIS is a public broadcast | "AISStream.io" (courtesy) | | **AISStream.io** | Live vessels (AIS) | Free, beta, no formal ToS; AIS is a public broadcast | "AISStream.io" (courtesy) |
| **CelesTrak** | Satellite TLEs (SGP4) | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" | | **CelesTrak** | Satellite TLEs (SGP4) and SATCAT records (object type, operational status, owner) that file each satellite under a class | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" |
| **The Space Devs — Launch Library 2 v2.3** | Recent launch, payload, stage, and recovery metadata for Space Missions (30d) | [The Space Devs terms of use](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_TSD.md#terms-of-use): data may be used and shared in any form; avoid forwarding it without added value; attribution is encouraged (not mandatory). [Official API limits](https://ll.thespacedevs.com/docs/): 15 unauthenticated calls/hour; optional token | "Launch Library 2 — The Space Devs" (courtesy attribution) | | **The Space Devs — Launch Library 2 v2.3** | Recent launch, payload, stage, and recovery metadata for Space Missions (30d) | [The Space Devs terms of use](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_TSD.md#terms-of-use): data may be used and shared in any form; avoid forwarding it without added value; attribution is encouraged (not mandatory). [Official API limits](https://ll.thespacedevs.com/docs/): 15 unauthenticated calls/hour; optional token | "Launch Library 2 — The Space Devs" (courtesy attribution) |
| **EOX Sentinel-2 cloudless 2020** | Optional satellite basemap in Cesium and Navara. Navara uses daylight for terrain inspection. | [EOX Maps](https://maps.eox.at/); [CC BY-NC-SA 4.0](https://cloudless.eox.at/license-non-commercial). Commercial use requires a commercial licence from EOX. | [EOxCloudless](https://cloudless.eox.at/) by [EOX IT Services GmbH](https://eox.at/) (Contains modified Copernicus Sentinel data 2020) |
| **Esri World Imagery** (ArcGIS Online tile service) | The keyless satellite basemap — the default landing when no Google/ion credential is configured, and the "Esri Satellite" map stack | [Esri Master Agreement](https://www.esri.com/en-us/legal/terms/full-master-agreement): the public World Imagery service is usable in public-facing apps with attribution; no key is required for this classic endpoint, but Esri governs and can change access — an app at scale should review current ArcGIS Location Platform terms | "Powered by Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community" (provider carries the service's own credit line) | | **Esri World Imagery** (ArcGIS Online tile service) | The keyless satellite basemap — the default landing when no Google/ion credential is configured, and the "Esri Satellite" map stack | [Esri Master Agreement](https://www.esri.com/en-us/legal/terms/full-master-agreement): the public World Imagery service is usable in public-facing apps with attribution; no key is required for this classic endpoint, but Esri governs and can change access — an app at scale should review current ArcGIS Location Platform terms | "Powered by Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community" (provider carries the service's own credit line) |
| **USGS** | Earthquakes | U.S. public domain | "Data courtesy of the U.S. Geological Survey" | | **USGS** | Earthquakes | U.S. public domain | "Data courtesy of the U.S. Geological Survey" |
| **Environment and Climate Change Canada** (MSC GeoMet OGC API) | Air quality (AQHI) — Canadian station observations | [ECCC Data Servers End-use Licence](https://eccc-msc.github.io/open-data/licence/readme_en/) (open, attribution required, no endorsement implied) | "Contains information licensed under the Environment and Climate Change Canada Data Servers End-use Licence" | | **Environment and Climate Change Canada** (MSC GeoMet OGC API) | Air quality (AQHI) — Canadian station observations | [ECCC Data Servers End-use Licence](https://eccc-msc.github.io/open-data/licence/readme_en/) (open, attribution required, no endorsement implied) | "Contains information licensed under the Environment and Climate Change Canada Data Servers End-use Licence" |
| **NASA GIBS** (WMTS tiles + Worldview Snapshots) and **NASA CMR** (granule catalog) | Recent Imagery layer: HLS S30/L30 (Sentinel-2, Landsat 8/9) and VIIRS true-colour tiles draped on the map, per-day thumbnails and PNG exports; the HLS granule catalog (dates, cloud, footprints) for a selected box | U.S. public domain (NASA data); keyless, browser-direct (CORS `*`); no bulk use — one 30-day catalog query per box, tiles bounded to the box, six tile requests in flight | "We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS)." — [gibs.earthdata.nasa.gov](https://gibs.earthdata.nasa.gov), [HLS](https://lpdaac.usgs.gov/products/hlss30v002/) | | **NASA GIBS** (WMTS tiles + Worldview Snapshots) and **NASA CMR** (granule catalog) | Recent Imagery layer: HLS S30/L30 (Sentinel-2, Landsat 8/9) and VIIRS true-colour tiles draped on the map, per-day thumbnails and PNG exports; the HLS granule catalog (dates, cloud, footprints) for a selected box | U.S. public domain (NASA data); keyless, browser-direct (CORS `*`); no bulk use — one 30-day catalog query per box, tiles bounded to the box, six tile requests in flight | "We acknowledge the use of imagery provided by services from NASA's Global Imagery Browse Services (GIBS), part of NASA's Earth Science Data and Information System (ESDIS)." — [gibs.earthdata.nasa.gov](https://gibs.earthdata.nasa.gov), [HLS](https://lpdaac.usgs.gov/products/hlss30v002/) |
| **NIFC WFIGS** (Wildland Fire Interagency Geospatial Services, ArcGIS feature service) | Fire Perimeters layer: current interagency wildfire incident perimeters, containment, and incident facts | U.S. public domain (interagency wildland-fire data published through the [NIFC Open Data portal](https://data-nifc.opendata.arcgis.com/)); fetched keyless through `/api/fire-perimeters` (5-minute server cache) with `exceededTransferLimit` paging | "Wildfire perimeters: National Interagency Fire Center (WFIGS)" | | **NIFC WFIGS** (Wildland Fire Interagency Geospatial Services, ArcGIS feature service) | Fire Perimeters layer: current interagency wildfire incident perimeters, containment, and incident facts | U.S. public domain (interagency wildland-fire data published through the [NIFC Open Data portal](https://data-nifc.opendata.arcgis.com/)); fetched keyless through `/api/fire-perimeters` (5-minute server cache) with `exceededTransferLimit` paging | "Wildfire perimeters: National Interagency Fire Center (WFIGS)" |
| **S2 Underground GhostMaps** (`github.com/s2underground/GhostMaps`) | S2 GhostMaps layer: the current edition of S2's KMZ intel products (Common Intelligence Picture, 30-day kinetic pull, Border Crisis Map, Iran and Yemen products, newest incident drop). Keyless. `/api/ghostmaps` reads the branch head from git's ref advertisement every 15 s (`GHOSTMAPS_POLL_SECONDS`, minimum 5 s), lists the tree through the GitHub REST API only when the head changes (optional `GITHUB_TOKEN`), downloads files by commit-pinned raw URLs into `.gev-cache/ghostmaps/` and pushes new catalogs to browsers over server-sent events | [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) (repository README: "S2 Underground © 2025 by S2A1"); non-commercial, attribution required; files are fetched at runtime, never stored in this repository | "S2 GhostMaps intel products: S2 Underground © by S2A1, CC BY-NC-SA 4.0" — shown in-app | | **S2 Underground GhostMaps** (`github.com/s2underground/GhostMaps`) | S2 GhostMaps layer: the current edition of S2's KMZ intel products (Common Intelligence Picture, 30-day kinetic pull, Border Crisis Map, Iran and Yemen products, newest incident drop). Keyless. `/api/ghostmaps` reads the branch head from git's ref advertisement every 15 s (`GHOSTMAPS_POLL_SECONDS`, minimum 5 s), lists the tree through the GitHub REST API only when the head changes (optional `GITHUB_TOKEN`), downloads files by commit-pinned raw URLs into `.gev-cache/ghostmaps/` and pushes new catalogs to browsers over server-sent events | [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) (repository README: "S2 Underground © 2025 by S2A1"); non-commercial, attribution required; files are fetched at runtime, never stored in this repository | "S2 GhostMaps intel products: S2 Underground © by S2A1, CC BY-NC-SA 4.0" — shown in-app |
| **News server** (`GEV_NEWS_ORIGIN`, optional; none by default) | News layer, both sections: the last 24 h of reports from many publishers. Set, the local server forwards the layer's `/api/news/hotspots`, `/feed` and `/outline` GET requests to it and Core reads none of the three sources below. Which providers stand behind its feed is the news server's to state | Each report stays its publisher's: headlines and summaries link to the original article, and pictures are hotlinked from the publisher's site (no referrer), never stored | Per report: publisher name and article link on every card; picture cards name the picture's publisher. The Data attribution popover states that places, categories, severity and English headlines are derived automatically and can be wrong |
| **GDELT 2.0 events and GKG** (`data.gdeltproject.org/gdeltv2/`; no key; used only without a news server) | News layer, Conflicts: the last 24 h of GDELT's 15-minute event exports (`*.export.CSV.zip`, about 100 KB each), read server-side as they appear. Physical violence only (CAMEO roots 18–20); one report per story, copies of one agency story merged; placed at GDELT's location for it (a town the article's words name beats a bare country). Titled by the article's page title from the GKG export of the same quarter hour (`*.gkg.csv.zip`, about 6 MB each) for the newest 3 h, else by the words of the article's address. GDELT's actor coding is never shown. Categories, relevance and severity by bundled rules | [GDELT Terms of Use](https://www.gdeltproject.org/about.html#termsofuse): unrestricted academic, commercial and governmental use, with citation and a link to the GDELT Project. Coded events, page titles and article links only; linked articles keep their publishers' terms | "GDELT Project" with a link to gdeltproject.org in the Data attribution popover; "via GDELT Project", the publisher's domain and the article link on every card |
| **Liveuamap** (`a.liveuamap.com/api`, `LIVEUAMAP_API_KEY`, optional, paid; used only without a news server) | News layer, Conflicts, beside GDELT: Liveuamap's worldwide mapped items of the last 24 h (`a=mpts`), read server-side every 5 minutes; categories and severity are read from the headline by bundled rules | Liveuamap API terms of the operator's own subscription. Headlines, places and links only; item pictures and clips are hotlinked from Liveuamap, never stored | "Liveuamap" with a link in the Data attribution popover once its items show; "Liveuamap" and the item's Liveuamap link on every card |
| **World News API** (`api.worldnewsapi.com/search-news`, `WORLDNEWSAPI_KEY`, optional; used only without a news server) | News layer, Local: the last 24 h of news by the source country at the view centre (country from the bundled Natural Earth admin-0 pack; none in a disputed area), read server-side once per country every 30 minutes, about 1.5 points a read, kept in memory for at most 1 hour and then deleted (a read that fails is served from that copy only within the hour); at most `WORLDNEWSAPI_DAILY_READS` reads a day (default 30), after which Local says the daily limit is reached. Each story keeps World News API's own category (politics, business, else other) with no severity of its own; sport, entertainment, lifestyle and travel left out | [World News API terms](https://worldnewsapi.com/terms/) of the operator's own plan. The terms allow caching only with World News API's prior written permission, and then for at most 1 hour: getting that permission before setting the key is the operator's responsibility. Titles and links only; article text and summaries are not kept or shown; pictures are hotlinked from the publisher | The "World News API" link to worldnewsapi.com, always in the Data attribution popover (a credit; the terms require a backlink only of invited free plans); "via World News API", the publisher's domain and the article link on every card |
| **EMODnet Human Activities — Ship wrecks** (`ows.emodnet-humanactivities.eu/wfs`, layer `emodnet:heritageshipwrecks`) | Shipwrecks layer: about 7,000 wrecks in European waters (name, sinking year and text, vessel type, least depth and its quality, position accuracy, period, cargo, protected status). Keyless WFS GeoJSON, fetched by `/api/shipwrecks` once a day; rows sourced from wrecksite.eu are skipped, and free text that refers to a person by a title or role is dropped by a keyword filter (a bare name can still pass) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (EMODnet catalogue record `e965088b-a265-4517-84df-c49b156af8a7`; data originator AND-International, compiled from SHOM, National Monuments Service, Historic England and the Oxford Roman Economy Project); fetched at runtime, not stored in this repository | "Shipwrecks (Europe): EMODnet Human Activities (data originator AND-International, …), CC BY 4.0" — shown in-app |
| **Wikidata — shipwrecks** (`query.wikidata.org/sparql`) | Shipwrecks layer: shipwreck items (Q852190) with a coordinate and at least one Wikipedia article, plus sinking events (Q906512, where the death toll lives): name, ship type, sinking date with its precision, deaths and survivors (counts only), description and article link. Keyless, fetched by `/api/shipwrecks` once a day | [CC0](https://creativecommons.org/publicdomain/zero/1.0/); fetched at runtime | "Shipwrecks (worldwide): Wikidata (CC0)" — shown in-app |
| **InciWeb** (inciweb.wildfire.gov) | Per-incident "InciWeb ↗" card links to official incident pages (narratives, evacuation/closure notices) | U.S. government public incident information. Links are matched best-effort by incident name and state, with incident page origin and update times checked on selection. Same-origin `/api/fire-perimeters/inciweb` routes cache the catalog for 1 hour and publication metadata for 30 minutes; linked pages remain InciWeb content | "Incident information: InciWeb (inciweb.wildfire.gov)" | | **InciWeb** (inciweb.wildfire.gov) | Per-incident "InciWeb ↗" card links to official incident pages (narratives, evacuation/closure notices) | U.S. government public incident information. Links are matched best-effort by incident name and state, with incident page origin and update times checked on selection. Same-origin `/api/fire-perimeters/inciweb` routes cache the catalog for 1 hour and publication metadata for 30 minutes; linked pages remain InciWeb content | "Incident information: InciWeb (inciweb.wildfire.gov)" |
| **FAA Special Use Airspace** (`services6.arcgis.com` FeatureServer `Special_Use_Airspace`) | Prohibited & Restricted Airspace layer: US prohibited (P) and restricted (R) special-use airspace polygons (not the UAS Facility Map or UAS flight restrictions), altitude limits from the `*_VAL`/`*_UOM`/`*_CODE` fields, times of use, and controlling agency | U.S. public domain (FAA aeronautical data published through the [FAA AIS Open Data portal](https://adds-faa.opendata.arcgis.com/)); fetched keyless through `/api/drone-airspace` (1-hour server cache). Informational only — not a substitute for official UAS flight-planning tools | "Prohibited & restricted airspace: FAA Special Use Airspace" | | **FAA Special Use Airspace** (`services6.arcgis.com` FeatureServer `Special_Use_Airspace`) | Prohibited & Restricted Airspace layer: US prohibited (P) and restricted (R) special-use airspace polygons (not the UAS Facility Map or UAS flight restrictions), altitude limits from the `*_VAL`/`*_UOM`/`*_CODE` fields, times of use, and controlling agency | U.S. public domain (FAA aeronautical data published through the [FAA AIS Open Data portal](https://adds-faa.opendata.arcgis.com/)); fetched keyless through `/api/drone-airspace` (1-hour server cache). Informational only — not a substitute for official UAS flight-planning tools | "Prohibited & restricted airspace: FAA Special Use Airspace" |
| **iNaturalist API** (`api.inaturalist.org/v2/observations`, `/v1/grid/{z}/{x}/{y}.grid.json`) | Wildlife Sightings layer: verifiable wildlife observed within the last 48 h (whales 7 d), by observation time, in the current view, with taxon, observation time, quality grade and eligible photo | Per-record licences; observations default CC BY-NC. Photos CC0/BY/BY-SA/BY-NC/BY-NC-SA shown; ND and all-rights-reserved photos link-only. Keyless via `/api/inaturalist/observations` (5-min cache; global sample 10 min; stale ≤ 1 h; 60 requests/min and 8,000/day upstream caps). **NonCommercial content:** commercial redistributors must filter to CC0/BY/BY-SA or remove the layer. No AI training on this content | "Wildlife sightings: iNaturalist contributors (licences per record; photos credited on each card)" | | **iNaturalist API** (`api.inaturalist.org/v2/observations`, `/v1/grid/{z}/{x}/{y}.grid.json`) | Wildlife Sightings layer: verifiable wildlife observed within the last 48 h (whales 7 d), by observation time, in the current view, with taxon, observation time, quality grade and eligible photo | Per-record licences; observations default CC BY-NC. Photos CC0/BY/BY-SA/BY-NC/BY-NC-SA shown; ND and all-rights-reserved photos link-only. Keyless via `/api/inaturalist/observations` (5-min cache; global sample 10 min; stale ≤ 1 h; 60 requests/min and 8,000/day upstream caps). **NonCommercial content:** commercial redistributors must filter to CC0/BY/BY-SA or remove the layer. No AI training on this content | "Wildlife sightings: iNaturalist contributors (licences per record; photos credited on each card)" |
@@ -65,9 +73,11 @@ How to read this:
| **DriveBC** (Government of British Columbia) | CCTV camera catalog + frames, British Columbia highways | [Open Government Licence – British Columbia](https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc) — attribution REQUIRED | "DriveBC. Contains information licensed under the Open Government Licence – British Columbia" | | **DriveBC** (Government of British Columbia) | CCTV camera catalog + frames, British Columbia highways | [Open Government Licence – British Columbia](https://www2.gov.bc.ca/gov/content/data/open-data/open-government-licence-bc) — attribution REQUIRED | "DriveBC. Contains information licensed under the Open Government Licence – British Columbia" |
| **Open Calgary** (`data.calgary.ca`) | CCTV camera catalog + frames, Calgary | [Open Government Licence – City of Calgary](https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – City of Calgary" | | **Open Calgary** (`data.calgary.ca`) | CCTV camera catalog + frames, Calgary | [Open Government Licence – City of Calgary](https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – City of Calgary" |
| **Servei Català de Trànsit** (+ Ajuntament de Barcelona, Ajuntament de Terrassa) | CCTV camera catalog + frames, Catalonia | [Llicència oberta d'ús d'informació - Catalunya](https://web.gencat.cat/ca/generalitat/dades-indicadors/dades-obertes/llicencies) — attribution REQUIRED | "Servei Català de Trànsit"; partner-supplied cameras also name their owner | | **Servei Català de Trànsit** (+ Ajuntament de Barcelona, Ajuntament de Terrassa) | CCTV camera catalog + frames, Catalonia | [Llicència oberta d'ús d'informació - Catalunya](https://web.gencat.cat/ca/generalitat/dades-indicadors/dades-obertes/llicencies) — attribution REQUIRED | "Servei Català de Trànsit"; partner-supplied cameras also name their owner |
| **Icelandic Road and Coastal Administration (Vegagerðin, IRCA)** | CCTV camera catalog + frames, Iceland road cameras | [CC BY 4.0](https://www.vegagerdin.is/vegagerdin/gagnasafn/vefthjonustur/terms-and-conditions) (IRCA terms of use, Act 45/2018) — attribution REQUIRED; no suggestion of IRCA endorsement; IRCA and State logos are excluded; real-time data is provided as is | "Icelandic Road and Coastal Administration (Vegagerðin)", CC BY 4.0 |
| **King County Road Services** (`services.arcgis.com`, King County GIS) | CCTV camera catalog + frames, unincorporated King County WA road network | [KCGIS Center terms](https://kingcounty.gov/en/dept/kcit/data-information-services/gis-center/about/terms-conditions-copyrights) — attribution required; public ArcGIS layer | "King County Road Services" (partner-city cameras name their owner) | | **King County Road Services** (`services.arcgis.com`, King County GIS) | CCTV camera catalog + frames, unincorporated King County WA road network | [KCGIS Center terms](https://kingcounty.gov/en/dept/kcit/data-information-services/gis-center/about/terms-conditions-copyrights) — attribution required; public ArcGIS layer | "King County Road Services" (partner-city cameras name their owner) |
| **WSDOT** (`data.wsdot.wa.gov`) | CCTV camera catalog + frames, Washington State highways | [WSDOT travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer) — public "low volume" feed; catalog cached, frames on WSDOT's own image host only | "Washington State Department of Transportation (Travel Center)" (courtesy) | | **WSDOT** (`data.wsdot.wa.gov`) | CCTV camera catalog + frames, Washington State highways | [WSDOT travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer) — public "low volume" feed; catalog cached, frames on WSDOT's own image host only | "Washington State Department of Transportation (Travel Center)" (courtesy) |
| **Hong Kong Transport Department / DATA.GOV.HK** | CCTV camera catalog + frames, Hong Kong traffic snapshot cameras | [DATA.GOV.HK Terms of Use](https://data.gov.hk/en/terms-and-conditions) — attribution REQUIRED | "Hong Kong Transport Department / DATA.GOV.HK" | | **Hong Kong Transport Department / DATA.GOV.HK** | CCTV camera catalog + frames, Hong Kong traffic snapshot cameras | [DATA.GOV.HK Terms of Use](https://data.gov.hk/en/terms-and-conditions) — attribution REQUIRED | "Hong Kong Transport Department / DATA.GOV.HK" |
| **Webcam Romania — Giroc** (`webcamromania.ro`, `w4.webcamromania.ro`) | Optional Giroc camera metadata, JPEG snapshot and live HLS video | [Provider terms](https://webcamromania.ro/termeni-si-conditii/): video belongs to the original camera operator; no open video redistribution licence asserted. The [camera page](https://webcamromania.ro/webcam-orase/webcam-giroc/) offers an embed. Local opt-in via `CCTV_SOURCES_FILE`; media is fetched at runtime, never bundled. | "Webcam Romania · webcamromania.ro"; source link and rights notice in the Data attribution popover |
| **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" | | **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" |
| **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) | | **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) |
| **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) | | **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) |
@@ -101,6 +111,40 @@ How to read this:
| **YRT/Viva** (rtu.york.ca) | Transit layer live vehicles, York Region ON | York Region Transit realtime data, first-party York Region infrastructure under the YRT Open Data Licence v2.0, which permits commercial use and makes attribution optional. Viva route ids are named by colour from York Region’s published GTFS. | "Contains public transit Information made available under YRT’s Open Data Licence" — the licence’s own wording, given though not required | | **YRT/Viva** (rtu.york.ca) | Transit layer live vehicles, York Region ON | York Region Transit realtime data, first-party York Region infrastructure under the YRT Open Data Licence v2.0, which permits commercial use and makes attribution optional. Viva route ids are named by colour from York Region’s published GTFS. | "Contains public transit Information made available under YRT’s Open Data Licence" — the licence’s own wording, given though not required |
| **Burlington Transit** (opendata.burlington.ca) | Transit layer live vehicles, Burlington ON | Burlington Transit realtime data under the City of Burlington Open Data Terms of Use, which permit any lawful purpose. The terms are revocable and must travel with the data, so the credit links them. Route ids are internal keys and are joined to the City’s published GTFS. | Credit to the City of Burlington plus a link to its Open Data Terms of Use, which the licence requires | | **Burlington Transit** (opendata.burlington.ca) | Transit layer live vehicles, Burlington ON | Burlington Transit realtime data under the City of Burlington Open Data Terms of Use, which permit any lawful purpose. The terms are revocable and must travel with the data, so the credit links them. Route ids are internal keys and are joined to the City’s published GTFS. | Credit to the City of Burlington plus a link to its Open Data Terms of Use, which the licence requires |
| **SMTR Rio de Janeiro** (dados.mobilidade.rio) | Transit layer live vehicles, Rio de Janeiro: municipal buses (SPPO) and the Mobi-Rio BRT, as two feeds | CC BY 4.0, stated on both data.rio catalog pages. Keyless GTFS-Realtime published by the Secretaria Municipal de Transportes, labelled beta, with a gateway limit of 5 requests a second and 60 a minute. Line names come from SMTR’s GTFS `routes.txt` (dados.mobilidade.rio/gtfs/schedule), under the same licence. | Attribution to SMTR / Prefeitura da Cidade do Rio de Janeiro under CC BY 4.0 | | **SMTR Rio de Janeiro** (dados.mobilidade.rio) | Transit layer live vehicles, Rio de Janeiro: municipal buses (SPPO) and the Mobi-Rio BRT, as two feeds | CC BY 4.0, stated on both data.rio catalog pages. Keyless GTFS-Realtime published by the Secretaria Municipal de Transportes, labelled beta, with a gateway limit of 5 requests a second and 60 a minute. Line names come from SMTR’s GTFS `routes.txt` (dados.mobilidade.rio/gtfs/schedule), under the same licence. | Attribution to SMTR / Prefeitura da Cidade do Rio de Janeiro under CC BY 4.0 |
| **ZDiTM Szczecin** (zditm.szczecin.pl) | Transit layer live vehicles, Szczecin, Poland: buses and trams as one feed | CC0 1.0, stated on the ZDiTM developer page: “The data are provided free of charge under the CC0 1.0 licence, for use in any context, with no prior notification required.” Keyless GTFS-Realtime; the page says one update a minute, measured at about every 20 seconds. The feed carries no route id, so every vehicle takes the default bus mode. | None required (CC0); credited to ZDiTM Szczecin as a courtesy |
| **Carris Metropolitana** (api.carrismetropolitana.pt) | Transit layer live vehicles, Lisbon Metropolitan Area, Portugal | Keyless GTFS-Realtime documented in the operator’s public API repository (github.com/carrismetropolitana/api). No licence is stated for the realtime endpoint; the operator’s static GTFS is CC BY 4.0. Included on the same basis as the Metro Transit and OVapi feeds and credited as a courtesy; it can be switched off if Carris objects. Speeds are published in km/h and converted to m/s by the proxy. Route ids are read as the line number per the operator’s `/v2/routes`. | Courtesy credit to Carris Metropolitana |
| **Prague PID** (api.golemio.cz) | Transit layer live vehicles, Prague and Central Bohemia, Czechia | CC BY (https://pid.cz/o-systemu/opendata/). Stated: “Data, která lze stáhnout přímo zde z webu, jsou stejně jako celý obsah webu opatřena licencí CC-BY” The PID open-data page lists this realtime feed and licenses the data it publishes under CC BY, crediting the author and any changes. The same page says data served from other sources follows that source’s licence; the feed is served by Golemio, the city’s data platform, which states none of its own. Keyless since the catalogue moved it off the X-Access-Token endpoint. Speeds are m/s, checked against movement on 2026-11-07. Route ids are `L` plus a number whose range gives the mode and the line (metro `L991` is A, train `L1004` is S4, ferry `L1801` is P1), read from PID’s GTFS routes.txt. | Pražská integrovaná doprava (PID) / Golemio |
| **Warsaw buses and trams** (mkuran.pl) | Transit layer live vehicles, Warsaw, Poland | Warsaw Open Data Project Usage Terms (https://mkuran.pl/gtfs/). Stated: “The City Hall requires a credit by providing the name Miasto Stołeczne Warszawa, data download date (use the timestamp from the FeedHeader) and link to http://api.um.warszawa.pl.” Positions come from the Warsaw City Hall API, repackaged as GTFS-RT by Mikołaj Kuranowski’s unofficial feed. The credit names the city, links its API and carries the date of the feed timestamp, the three things the terms ask for. Vehicle timestamps ran about two minutes old on 2026-11-07, which the delayed playback absorbs. | Miasto Stołeczne Warszawa (api.um.warszawa.pl) |
| **Bordeaux TBM** (bdx.mecatran.com) | Transit layer live vehicles, Bordeaux, France | Licence Ouverte / Etalab 2.0 (https://transport.data.gouv.fr/datasets/offres-de-services-bus-tram-et-scolaire-au-format-gtfs-netex-gtfs-rt-siri-lite). The quote is the licence field of the national access point’s dataset record (fr-lo is the Licence Ouverte), which asks only for credit. The `apiKey` in the URL is not a credential: it is the same fixed string printed in the open-data listing for everyone. Speeds are m/s. The trams (A–F) and BAT³ boats ride numbered route ids (tram A is `59`), mapped from TBM’s static GTFS; other numbered lines are buses. | Bordeaux Métropole (TBM) |
| **Sendai City Bus** (api-public.odpt.org) | Transit layer live vehicles, Sendai, Japan | CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). Served by the Open Data Challenge for Public Transportation (ODPT) public API, which needs no key for this feed; the catalogue records CC BY 4.0 as its licence. Vehicle timestamps ran about two minutes old on 2026-11-07. Speeds are m/s. | 仙台市交通局 (Sendai City Transportation Bureau) / ODPT |
| **Poznań buses and trams** (ztm.poznan.pl) | Transit layer live vehicles, Poznań, Poland | ZTM Poznań open-data terms (https://www.ztm.poznan.pl/otwarte-dane/dla-deweloperow/). Stated: “Treści udostępniane są bezwnioskowo i bezpłatnie, na zasadach powszechnego dostępu i stanowią źródło informacji.” Reuse is free on three conditions: name the source, give the date of the last update, and say the data were processed. The credit names ZTM, says processed, and carries the date of the feed timestamp as the update date. The feed carries no bearing, so heading comes from movement between fixes. | Zarząd Transportu Miejskiego w Poznaniu (processed) |
| **Gdańsk buses and trams** (ckan2.multimediagdansk.pl) | Transit layer live vehicles, Gdańsk, Poland | CC BY 4.0 (https://dane.gov.pl/pl/dataset/38599,rozklady-jazdy-i-lokalizacja-przystankow-gtfs-rt). The national catalogue (dane.gov.pl) lists this dataset under CC BY 4.0. No bearing; heading comes from movement. Speeds are m/s. | Zarząd Transportu Miejskiego w Gdańsku |
| **Edmonton Transit Service** (gtfs.edmonton.ca) | Transit layer live vehicles, Edmonton, AB | City of Edmonton Open Data Agreement (https://www.edmonton.ca/transportation/Web-version2.1-OpenDataAgreement.pdf). The agreement is the same municipal template as Mississauga and Burlington: a licence to use, modify and distribute for any lawful purpose, in any medium. It was read from search excerpts; the PDF itself would not extract here. Route ids are zero-padded. Speeds are m/s. | Contains information licensed under the Open Data Agreement – City of Edmonton |
| **Halifax Transit** (gtfs.halifax.ca) | Transit layer live vehicles, Halifax, NS | Open Government Licence – Halifax v1.0 (https://www.halifax.ca/home/open-data/open-data-license), modelled on OGL – Canada 2.0. Stated: “The information provider grants you a worldwide, royalty-free, perpetual, non-exclusive licence to use the Information, including for commercial purposes, subject to the terms below.” No logos or implied endorsement. Speeds are m/s. | Contains information licenced under the Open Government Licence – Halifax |
| **Victoria Regional Transit** (bct.tmix.se) | Transit layer live vehicles, Victoria, BC | BC Transit Open Data Terms of Use (https://www.bctransit.com/open-data/terms-of-use). Stated: “BC Transit grants you a limited, revocable and non-exclusive license to use, reproduce, and redistribute the Data in accordance with these Terms.” The grant is revocable, like the Ontario municipal templates. Route ids carry a `-VIC` system suffix. Speeds are m/s. | BC Transit |
| **Montpellier TaM** (gtfsproxy.e-tam.fr) | Transit layer live vehicles, Montpellier, France | ODbL (https://transport.data.gouv.fr/datasets/offre-de-transport-tam-en-temps-reel-gtfs-rt-urbain-et-suburbain). The quote is the dataset record’s licence field. ODbL asks for credit and share-alike on a derived database; the layer redistributes no database, it draws the positions. Speeds are km/h: readings ran 3.3 times the measured movement on 2026-11-07, so the proxy converts them. | Montpellier Méditerranée Métropole (TaM) |
| **Kraków buses** (gtfs.ztp.krakow.pl) | Transit layer live vehicles, Kraków, Poland | No licence stated (https://gtfs.ztp.krakow.pl/). ZTP publishes this feed keyless on its own GTFS server and lists it in the Mobility Database, but states no licence there or on dane.gov.pl. Shipped on the same basis as Metro Transit and OVapi; switch it off if ZTP objects. | Zarząd Transportu Publicznego w Krakowie |
| **Kraków trams** (gtfs.ztp.krakow.pl) | Transit layer live vehicles, Kraków, Poland | No licence stated (https://gtfs.ztp.krakow.pl/). Same server and basis as the bus feed. | Zarząd Transportu Publicznego w Krakowie |
| **Vilnius public transport** (stops.lt) | Transit layer live vehicles, Vilnius, Lithuania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2383). Published keyless by the city transport agency on stops.lt; no licence found. Shipped on the same basis as Metro Transit and OVapi. Answered 522 once on 2026-11-07 and normally since. | SĮ Susisiekimo paslaugos (stops.lt) |
| **Bucharest public transport** (gtfs.tpbi.ro) | Transit layer live vehicles, Bucharest, Romania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2104). Keyless public endpoint from the metropolitan transport authority; no licence found. Same basis as Metro Transit and OVapi. | TPBI (Transport Public București–Ilfov) |
| **Rome public transport** (romamobilita.it) | Transit layer live vehicles, Rome, Italy | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1776). Keyless file on the agency’s own site; the catalogue’s older `/sites/default/files/` path now 404s. No licence found; same basis as Metro Transit and OVapi. Fewer than one vehicle in five carries a bearing, so most headings come from movement. | Roma Servizi per la Mobilità |
| **Turin GTT** (percorsieorari.gtt.to.it) | Transit layer live vehicles, Turin, Italy | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2696). Official keyless endpoint; no licence found. Same basis as Metro Transit and OVapi. Route ids append `U` for urban lines. | Gruppo Torinese Trasporti (GTT) |
| **Venice ACTV buses** (tpl.actv.it) | Transit layer live vehicles, Venice, Italy | ACTV open-data catalogue (https://actv.avmspa.it/it/content/catalogo-dei-dati-metadati-e-banche-dati-actv). ACTV lists this feed in its own open-data catalogue; that page would not render for a script, so its licence is not quoted. Same basis as Metro Transit and OVapi. Route ids append a two-letter network code (`_UM`, `_ES`, `_UN`). | ACTV S.p.A. |
| **Venice ACTV waterbuses** (tpl.actv.it) | Transit layer live vehicles, Venice, Italy | ACTV open-data catalogue (https://actv.avmspa.it/it/content/catalogo-dei-dati-metadati-e-banche-dati-actv). Same catalogue and basis as the ACTV bus feed. | ACTV S.p.A. |
| **Adelaide Metro** (gtfs.adelaidemetro.com.au) | Transit layer live vehicles, Adelaide, SA | No licence stated for the realtime feed (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1772). Official keyless endpoint; the state catalogue page refused automated reads, so no licence is quoted. Same basis as Metro Transit and OVapi. Speeds are m/s. The server picks its encoding from Accept, hence the override. | Adelaide Metro |
| **MARTA buses** (gtfs-rt.itsmarta.com) | Transit layer live vehicles, Atlanta, GA | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-1601). Official keyless endpoint; no licence found. Same basis as Metro Transit and OVapi. Speeds are m/s. | MARTA |
| **Denver RTD** (open-data.rtd-denver.com) | Transit layer live vehicles, Denver, CO | RTD GTFS-realtime licence agreement (https://www.rtd-denver.com/business-center/open-spatial-data/gtfs-realtime-license-agreement). Stated: “RTD hereby grants you non-exclusive, limited and revocable rights to use, reproduce, and redistribute the Data”. Read 2026-11-09: a click-through agreement that RTD may change without notice; it permits redistribution, asks for no credit wording, and bars RTD trademarks from use with the data, so the credit stays plain text. The feed itself is keyless. Speeds are m/s. | Regional Transportation District (RTD) |
| **Cluj-Napoca CTP** (cluj-rt-feed.gtfs.ro) | Transit layer live vehicles, Cluj-Napoca, Romania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2614). Keyless; no licence found. Same basis as Metro Transit and OVapi. | Compania de Transport Public Cluj-Napoca |
| **Iași CTP** (iasi-rt-feed.gtfs.ro) | Transit layer live vehicles, Iași, Romania | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2118). Keyless; no licence found. Same basis as Metro Transit and OVapi. | Compania de Transport Public Iași |
| **Rzeszów city transport** (mpkrzeszow.pl) | Transit layer live vehicles, Rzeszów, Poland | MPK Rzeszów open data (https://www.mpkrzeszow.pl/otwartedane/). Listed on the operator’s open-data page, which names no licence. No bearing; heading comes from movement. | MPK Rzeszów |
| **Málaga EMT** (gtfs.emtmalaga.es) | Transit layer live vehicles, Málaga, Spain | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2213). Keyless; no licence found. Same basis as Metro Transit and OVapi. No bearing; heading comes from movement. | EMT Málaga |
| **Carris (Lisbon city)** (gateway.carris.pt) | Transit layer live vehicles, Lisbon, Portugal | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2930). Keyless official gateway; no licence found. Same basis as Metro Transit and OVapi. Carries neither bearing nor speed, and vehicle reports run about a minute old. Route ids append a variant. | Carris |
| **BAS.MY Johor Bahru** (api.data.gov.my) | Transit layer live vehicles, Johor Bahru, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Stated: “All GTFS Realtime feeds are licensed under CC BY 4.0.” Served keyless by Malaysia’s national open-data API, which caps GTFS Realtime at 4 requests a minute and refreshes positions every 30 seconds, so the proxy caches this feed for 30 seconds. Without the trailing slash the API redirects. Route ids append an operator code. | BAS.MY / data.gov.my |
| **BAS.MY Kota Bharu** (api.data.gov.my) | Transit layer live vehicles, Kota Bharu, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Same API, licence and rate limit as Johor Bahru. | BAS.MY / data.gov.my |
| **Rouen Astuce** (api.mrn.cityway.fr) | Transit layer live vehicles, Rouen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie). The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); and this is its own TCAR vehicle feed from that record; the suburban TAE and TNI networks are separate feeds and are not read. Route ids carry the network prefix (`TCAR:`); line 90 is the Métro (a street-running light rail, drawn as a tram) and 99 the Calypso boat. | Métropole Rouen Normandie (Astuce) |
| **Toulon Mistral** (feed-rdtpm-toulon.ratpdev.com) | Transit layer live vehicles, Toulon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-de-la-metropole-toulon-provence-mediterranee). Dataset record’s licence field quoted. Speeds are km/h: readings ran 3.5 times the measured movement on 2026-11-07, so the proxy converts them. Route ids are zero-padded. | Métropole Toulon Provence Méditerranée (Mistral) |
| **Le Havre LiA** (opendata.transports-lia.fr) | Transit layer live vehicles, Le Havre, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri). The network publishes under ODbL (dataset record quoted); and this is its own GTFS-RT vehicle feed from that record; route ids carry the network prefix `8-`. The layer draws positions and redistributes no database. Lines A and B are the tramway. | Le Havre Seine Métropole (LiA) |
| **Tours Fil Bleu** (data.filbleu.fr) | Transit layer live vehicles, Tours, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/fil-bleu-syndicat-des-mobilites-gtfs-gtfs-rt). Dataset record’s licence field quoted. | Fil Bleu – Syndicat des Mobilités de Touraine |
| **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster | | **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster |
| **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" | | **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" |
| **OpenSeaMap** (`tiles.openseamap.org`) | Sea Marks layer: raster overlay of buoys, lighthouses and ports (#10) | Chart tiles CC BY-SA 2.0; data ODbL 1.0 via OpenStreetMap | "Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)" | | **OpenSeaMap** (`tiles.openseamap.org`) | Sea Marks layer: raster overlay of buoys, lighthouses and ports (#10) | Chart tiles CC BY-SA 2.0; data ODbL 1.0 via OpenStreetMap | "Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)" |
@@ -122,7 +166,8 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
**Quebec is thinly represented**, and that is upstream rather than a filter here: the province runs its own Info-Smog program, so only a handful of Quebec stations appear in ECCC's national feed. The reporting station count also varies hour to hour as stations report or fall silent. **Quebec is thinly represented**, and that is upstream rather than a filter here: the province runs its own Info-Smog program, so only a handful of Quebec stations appear in ECCC's national feed. The reporting station count also varies hour to hour as stations report or fall silent.
- **Via Lietuva / Eismoinfo (Lithuania).** Public road-camera catalogs and periodically updated still images from [Eismoinfo](https://eismoinfo.lt/), credited to Via Lietuva / eismoinfo.lt in the app. **Off by default.** The camera endpoints are the eismoinfo.lt website backend and are not among the services listed in Via Lietuva's [open-data statement](https://eismoinfo.lt/eismoinfo-backend/content?id=44), whose conditions are attribution and reuse on the same terms. Until the statement is confirmed to cover the camera photos, the pack loads only when an operator sets `CCTV_LITHUANIA_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to Eismoinfo. `eismoinfo-backend/camera-info-table` is joined by ID to `layer-static-features/VKR`. Without `lks=true`, the latter returns WGS84 `[latitude, longitude]`; the table's `x/y` instead use LKS-94 metres. Only the official HTTPS `/eismoinfo-backend/image-provider/camera/last?id=…` image endpoint is registered. Catalogs use the shared 15-minute cache; frames are fetched on demand and never bundled or archived. These are periodic snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. `CCTV_LITHUANIA_MAX_SOURCES` defaults to 400. - **Via Lietuva / Eismoinfo (Lithuania).** Public road-camera catalogs and periodically updated still images from [Eismoinfo](https://eismoinfo.lt/), credited to Via Lietuva / eismoinfo.lt in the app. **Off by default.** The camera endpoints are the eismoinfo.lt website backend and are not among the services listed in Via Lietuva's [open-data statement](https://eismoinfo.lt/eismoinfo-backend/content?id=44), whose conditions are attribution and reuse on the same terms. Until the statement is confirmed to cover the camera photos, the pack loads only when an operator sets `CCTV_LITHUANIA_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to Eismoinfo. `eismoinfo-backend/camera-info-table` is joined by ID to `layer-static-features/VKR`. Without `lks=true`, the latter returns WGS84 `[latitude, longitude]`; the table's `x/y` instead use LKS-94 metres. Only the official HTTPS `/eismoinfo-backend/image-provider/camera/last?id=…` image endpoint is registered. Catalogs use the shared 15-minute cache; frames are fetched on demand and never bundled or archived. These are periodic snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. `CCTV_LITHUANIA_MAX_SOURCES` defaults to 400.
- **Freeway Bureau, MOTC (Taiwan).** National freeway cameras from the Freeway Bureau's keyless [CCTV static table](https://tisvcloud.freeway.gov.tw/history/motc20/CCTV.xml) in the MOTC 2.0 standard, refreshed daily and fetched at runtime (never bundled). Freeway Bureau traffic data is published under the [Open Government Data License, Taiwan (Version 1.0)](https://data.gov.tw/license), which permits reuse, including commercial reuse, with attribution; the app credits the Freeway Bureau, MOTC, names the dataset and links the licence, as its attribution exhibit asks. The table publishes each camera's MJPEG stream on one of four official camera servers (`cctvn`, `cctvn5`, `cctvc`, `cctvs` under `freeway.gov.tw`). The frame proxy relays single images, so each published stream URL is mapped to the same server's single-JPEG path for the same camera number (`/abs2mjpg/bmjpg` → `/abs2mjpg/jpg`, `/live-view/mjpg/video.cgi` → `/live-view/jpg/image.cgi`); only those exact HTTPS pairs are registered, and the still path is not itself listed in the table. These are on-demand snapshots, not continuous video. Camera headings are low-confidence estimates. No API key is required. The table host does not answer from some networks outside Taiwan (the camera servers do); there the pack is empty, logs one warning and retries hourly. On by default; `CCTV_TAIWAN_ENABLED=0` skips the pack, and `CCTV_TAIWAN_MAX_SOURCES` defaults to 600 (bounded to 1–2000). - **Freeway Bureau and Highway Bureau, MOTC (Taiwan).** National freeway cameras use the Freeway Bureau’s keyless [MOTC 2.0 CCTV table](https://tisvcloud.freeway.gov.tw/history/motc20/CCTV.xml), refreshed daily and fetched at runtime (never bundled). Freeway Bureau data follows the [Open Government Data License, Taiwan v1.0](https://data.gov.tw/license); the app credits the Bureau, names the dataset and links the licence. If that catalog is unreachable and no last-good source list exists, the app falls back to the Highway Bureau’s free [provincial-road CCTV XML feed](https://thbapp.thb.gov.tw/opendata/), with attribution as requested by the Bureau. Its MJPEG streams are accepted only from cctv-ss01 through cctv-ss08.thb.gov.tw over HTTPS; the frame proxy extracts one bounded JPEG per request. This fallback covers provincial roads, not national freeways. Neither feed requires an API key. CCTV_TAIWAN_ENABLED=0 disables both; CCTV_TAIWAN_MAX_SOURCES defaults to 600 (bounded to 1–2000).
- **Québec CCTV (MTQ / Québec 511, Montréal and scenic links).** `public/quebec-cameras.json` contains 680 MTQ positions from the official georeferenced WFS (CC BY 4.0), 426 Montréal camera locations derived from OSM (ODbL 1.0), and 14 scenic records with per-row provenance and accuracy. The MTQ video is **opt-in** (`CCTV_QUEBEC_VIDEO_ENABLED=1`): the CC BY 4.0 licence covers the camera locations only, and Québec 511's terms for embedding its clips on another site are unconfirmed, so by default MTQ cameras link to the Québec 511 viewer page instead. With the flag set, the 680 MTQ cameras play the operator's five-second MP4 clip directly in the viewer's browser from `www.quebec511.info/Carte/Fenetres/camera.ashx?id=<id>&format=mp4`, only after the viewer presses Play, so Québec 511 sees the viewer's IP address; Québec 511 refreshes those clips every few minutes. A second browser request supplies an approximate map still. The app does not proxy, store, or read video pixels, and no camera images are bundled. The selected-camera credit names both sources: locations MTQ under CC BY 4.0, video Québec 511. Montréal and scenic entries have provider-page links rather than direct frame URLs; their source-page terms apply. The video remains hosted by Québec 511 under its service terms. No API key is required; `CCTV_QUEBEC_ENABLED=0` disables the pack. Confirm Québec 511's terms of use before setting `CCTV_QUEBEC_VIDEO_ENABLED=1` on a public deployment.
- **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)). - **Google Maps Platform.** You supply your own API key and are bound by [Google's ToS](https://cloud.google.com/maps-platform/terms). Google Maps Content (tiles, geocodes, places) **may not be cached, stored, rehosted, or committed** — this app only ever uses it live, which is the compliant pattern. The "Google" attribution is displayed on the globe and must stay visible. Restrict your key (see [SECURITY.md](SECURITY.md)).
- **Sea Marks and Ski Pistes (#10).** Two keyless raster overlay layers under Infrastructure, OpenSeaMap sea marks and OpenSnowMap ski pistes, requested tile by tile as the camera views them, with no prefetch, bulk download or caching on our side (OpenSnowMap forbids bulk tile download without prior agreement). They drape on the globe on the OSM and Bing stacks and on the Google Photorealistic tileset on Google 3D (see `src/maps/imageryHost.js`). OpenRailwayMap and OpenInfraMap, also requested in #10, are not included: OpenRailwayMap's tile server only answers requests whose Referer is its own site, and OpenInfraMap serves vector tiles that need a vector rendering pipeline. - **Sea Marks and Ski Pistes (#10).** Two keyless raster overlay layers under Infrastructure, OpenSeaMap sea marks and OpenSnowMap ski pistes, requested tile by tile as the camera views them, with no prefetch, bulk download or caching on our side (OpenSnowMap forbids bulk tile download without prior agreement). They drape on the globe on the OSM and Bing stacks and on the Google Photorealistic tileset on Google 3D (see `src/maps/imageryHost.js`). OpenRailwayMap and OpenInfraMap, also requested in #10, are not included: OpenRailwayMap's tile server only answers requests whose Referer is its own site, and OpenInfraMap serves vector tiles that need a vector rendering pipeline.
@@ -163,13 +208,16 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
contributors, ODbL 1.0; GPSJAM for aggregation only. contributors, ODbL 1.0; GPSJAM for aggregation only.
- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session, with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it. - **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session (for the flights layer's enrichment, a lookup that failed temporarily, such as an outage, a timeout, a rate limit or an authorization refusal that may clear, may be asked again after a 60-second cool-down; an answer, found or not, and a permanent refusal such as a 404 are final; Local ADS-B lookups are asked once), with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it.
- **Planespotters photos.** When you track an aircraft, the browser asks `https://api.planespotters.net/pub/photos/hex/{hex}` directly (the API answers cross-origin, so no key and no server proxy are involved) for that aircraft only, once per hex per session, through the same enrichment queue as adsbdb, with an 8-second timeout. A miss is remembered for the session; a request that failed temporarily (an outage, the timeout or a rate limit) may be asked again after a 60-second cool-down while the aircraft stays tracked, as for the flights layer's other enrichment. Either way no photo shows meanwhile. The first photo whose image is an `https://` URL on `*.plnspttrs.net`, and whose page is an `https://www.planespotters.net` URL is shown in the CONTEXT panel's CONTACTS view, under the tracked flight's name (`src/ui/views/panels/AwarenessPanel.vue`), and nowhere else; a vessel, an installation, a military aircraft or a converted TR-3B has none, and a military aircraft or TR-3B is never looked up. Anything else is dropped, so the app never shows or links outside Planespotters (`src/data/planespottersPhoto.js`, also exported as `gods-eye-view/sources/planespotters` for servers that cache the lookup). The photo follows the attribution rules: the thumbnail is hotlinked from the URL the API gives (never copied or re-hosted), the photographer's name, when the answer gives one, is visible text next to it ("Photo © {photographer}"; a photo whose answer names no one shows no credit line), and the photo and credit are a plain link to the photo's Planespotters page with `target="_blank" rel="noopener"` and never `rel="nofollow"`. If the image fails to load, the photo and its credit are hidden.
- **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state. - **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state.
- **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover. - **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover.
- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does. - **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does.
- **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms. - **DelDOT live video cameras.** The keyless `tmc.deldot.gov/json/videocamera.json` catalog supplies active cameras and official `https://video.deldot.gov/live/…/playlist.m3u8` links. The local server transiently buffers bounded MPEG-TS segments in memory and deletes them on session release or idle expiry. No recordings are retained. `CCTV_DELDOT_ENABLED=0` disables the pack. Public availability is not a blanket redistribution license; deployment beyond personal local viewing needs confirmation of applicable DelDOT terms.
- **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch. - **Fintraffic weathercams.** The camera list is one keyless GeoJSON request to `tie.digitraffic.fi/api/weathercam/v1/stations` (809 stations / 2,275 presets), sent with the `Digitraffic-User: gods-eye-view` header the service asks clients to identify themselves with and with gzip; frames are stills on `weathercam.digitraffic.fi`, refreshed on each station's 600 s collection interval, which is also the client's ambient-still refresh cadence for this pack (the active camera keeps the usual 10-second refresh). One preset (a fixed camera view) is one camera; stations not `GATHERING` and presets not `inCollection` are dropped. Attribution is required by the CC BY 4.0 licence and is registered in the Data attribution popover. `CCTV_FINTRAFFIC_ENABLED=0` is the kill switch.
- **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/<id>.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel. - **DriveBC highway cameras.** The camera list comes from the keyless `https://www.drivebc.ca/api/webcams/` endpoint served by the DriveBC.ca site ([bcgov/DriveBC.ca](https://github.com/bcgov/DriveBC.ca)); frames come from `https://www.drivebc.ca/images/<id>.jpg`, built from the numeric camera id. DataBC's [DriveBC HighwayCams](https://catalogue.data.gov.bc.ca/dataset/bc-highwaycams) catalogue entry publishes the same cameras under the Open Government Licence – British Columbia, but its CSV still lists the retired `images.drivebc.ca` frame URLs, which now return a placeholder. The licence's attribution statement is required and is registered in the Data attribution popover. By default the 250 cameras nearest Vancouver and Victoria load; `CCTV_DRIVEBC_MAX_SOURCES` changes the cap and `CCTV_DRIVEBC_ENABLED=0` turns the pack off. Some cameras in the feed are supplied by partners (TransLink, the City of Vancouver, the City of Surrey, Parks Canada / Alberta Motor Association and others); the feed's per-camera credit is carried onto the camera record and shown beside the provider in the CCTV panel.
- **City of Ottawa traffic cameras.** Camera images come from the city's [Traffic Camera Images API](https://traffic.ottawa.ca/en/traffic-map-data-lists-and-resources/about-the-data), which requires an access certificate requested through the city's registration form and allows one certificate and user id to fetch a camera at most once every 60 seconds. Set `OTTAWA_CAMERA_CERTIFICATE`; without it the pack is skipped and makes no request. The camera list is the documented keyless `traffic.ottawa.ca/map/service/camera` JSON (camera number, English and French names, coordinates, owner). Frames are `traffic.ottawa.ca/opendata/camera?c=<number>`; the server adds the certificate and the user id `gev` at fetch time, so no catalog response carries the key, and serves each camera from one shared in-memory frame per minute. MTO cameras carried on the city feed keep their owner as a per-camera credit. The feed publishes no bearing, so headings use the shared id-hash fallback at low confidence, and no status field, so a camera the city takes offline still registers. Frames are never stored on disk. The city states no redistribution licence for the images; the certificate request asks how they will be used, so describe the deployment there. `CCTV_OTTAWA_MAX_SOURCES` caps the pack (default 250, nearest downtown) and `CCTV_OTTAWA_ENABLED=0` turns it off.
- **World cams image subset.** The normal CCTV catalog adds 1,176 Oregon TripCheck stills and 1,054 MLIT river cameras from the curated `config/cctv_sources.world-image-cameras.json` export; disable it with `CCTV_WORLD_CAMERAS_ENABLED=0`. The export's Vegagerðin rows are left out because the Iceland pack already loads those cameras. The server validates and pins each image URL to its provider path, fetches a frame only when a camera is viewed, and never stores or redistributes it. [ODOT allows businesses, private citizens and organizations to use TripCheck images and asks for credit](https://www.tripcheck.com/Pages/Frequently-Asked-Questions) ("Camera courtesy of ODOT"); TripCheck also carries partner cameras run by Oregon counties and cities, Caltrans and WSDOT, which keep their owner's name in the camera label. MLIT's [site terms](https://www.river.go.jp/kawabou/kwb_apend/html/caution.html) follow the Government of Japan Standard Terms of Use 2.0 (CC BY 4.0 compatible, commercial use allowed, source credit required) and ask tools not to collect data periodically, pointing regular collection to the river information data service; frames are therefore fetched only on view and refresh at most every ten minutes per camera. Both credits are registered in the Data attribution popover.
- **TxDOT ITS cameras.** The catalog comes from the keyless `its.txdot.gov/its/DistrictIts/GetCctvStatusListByDistrict` endpoint, one call per district. Frames come from the per-camera `GetCctvSnapshotByIcdId` endpoint, which answers JSON carrying a base64 JPEG rather than an image body; the proxy decodes it only for responses from the official TxDOT origin and validates the JPEG header before serving. Only cameras reporting `Device Online` are registered. Districts default to Austin and San Antonio (`CCTV_TXDOT_DISTRICTS`; 25 codes exist statewide); `CCTV_TXDOT_MAX_SOURCES` caps the pack and `CCTV_TXDOT_ENABLED=0` turns it off. Frames are fetched live at request time and are never stored or redistributed. - **TxDOT ITS cameras.** The catalog comes from the keyless `its.txdot.gov/its/DistrictIts/GetCctvStatusListByDistrict` endpoint, one call per district. Frames come from the per-camera `GetCctvSnapshotByIcdId` endpoint, which answers JSON carrying a base64 JPEG rather than an image body; the proxy decodes it only for responses from the official TxDOT origin and validates the JPEG header before serving. Only cameras reporting `Device Online` are registered. Districts default to Austin and San Antonio (`CCTV_TXDOT_DISTRICTS`; 25 codes exist statewide); `CCTV_TXDOT_MAX_SOURCES` caps the pack and `CCTV_TXDOT_ENABLED=0` turns it off. Frames are fetched live at request time and are never stored or redistributed.
- **Tallinn ristmikud.** Intersection camera stills are fetched live from `ristmikud.tallinn.ee` (`/last/camNNN.jpg`). The curated catalog (`config/cctv_sources.tallinn.json`) supplies coordinates and heading priors; only server-registered ristmikud HTTPS URLs are proxied. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_TALLINN_ENABLED=0`. - **Tallinn ristmikud.** Intersection camera stills are fetched live from `ristmikud.tallinn.ee` (`/last/camNNN.jpg`). The curated catalog (`config/cctv_sources.tallinn.json`) supplies coordinates and heading priors; only server-registered ristmikud HTTPS URLs are proxied. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_TALLINN_ENABLED=0`.
- **Tarktee / Transpordiamet.** Estonia road-weather camera locations and current JPEG URLs come from keyless DATEX2 endpoints on `tarktee.transpordiamet.ee` (`/api/v1/datex/roadCameraLocations` + `roadCameraImages`). Frame URLs rotate with each capture; the 15-minute CCTV source cache refreshes them. Only `https://tarktee.transpordiamet.ee/images/…` URLs are registered. Disable with `CCTV_TARKTEE_ENABLED=0`. - **Tarktee / Transpordiamet.** Estonia road-weather camera locations and current JPEG URLs come from keyless DATEX2 endpoints on `tarktee.transpordiamet.ee` (`/api/v1/datex/roadCameraLocations` + `roadCameraImages`). Frame URLs rotate with each capture; the 15-minute CCTV source cache refreshes them. Only `https://tarktee.transpordiamet.ee/images/…` URLs are registered. Disable with `CCTV_TARKTEE_ENABLED=0`.
@@ -178,6 +226,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Open Calgary traffic cameras.** Keyless. The camera list is the public Socrata dataset `k7p9-kppz` on `data.calgary.ca`; frames are stills on the city's own `trafficcam.calgary.ca` host, and only that host is registered. Most rows publish an `http://` frame URL and the host redirects those to HTTPS, so URLs are upgraded and pinned to that origin before registration. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `quadrant` field and the quadrant suffix on each camera name are Calgary's address grid, not a bearing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. The licence requires the attribution statement above. Disable with `CCTV_CALGARY_ENABLED=0`. - **Open Calgary traffic cameras.** Keyless. The camera list is the public Socrata dataset `k7p9-kppz` on `data.calgary.ca`; frames are stills on the city's own `trafficcam.calgary.ca` host, and only that host is registered. Most rows publish an `http://` frame URL and the host redirects those to HTTPS, so URLs are upgraded and pinned to that origin before registration. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `quadrant` field and the quadrant suffix on each camera name are Calgary's address grid, not a bearing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. The licence requires the attribution statement above. Disable with `CCTV_CALGARY_ENABLED=0`.
- **FL511 Florida cameras.** Off by default; set `CCTV_FLORIDA_ENABLED=1` to load them. The camera list is the keyless FL511 traffic camera layer on ArcGIS (`services.arcgis.com/.../FL511_Traffic_Cameras`), published from a Florida Division of Emergency Management account rather than by FL511 directly, and the service carries no licence text of its own. It is paged up to 2,000 rows at a time, ordered by the layer's object id and following ArcGIS's `exceededTransferLimit`, to cover the ~4,000-camera statewide set. Frames are stills on two Divas Cloud hosts (`images-dis.divas.cloud`, `snapshots.divas.cloud`); both are host-pinned and a row whose image resolves elsewhere is dropped. `CCTV_FLORIDA_MAX_SOURCES` caps the pack (default 500, anchored per region across the state). Each catalog page is read under a 2 MiB cap and redirects are refused. Frames are fetched live at request time and never stored or redistributed. FL511's terms (fl511.com/privacy#disclaimer) license its content for individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent. - **FL511 Florida cameras.** Off by default; set `CCTV_FLORIDA_ENABLED=1` to load them. The camera list is the keyless FL511 traffic camera layer on ArcGIS (`services.arcgis.com/.../FL511_Traffic_Cameras`), published from a Florida Division of Emergency Management account rather than by FL511 directly, and the service carries no licence text of its own. It is paged up to 2,000 rows at a time, ordered by the layer's object id and following ArcGIS's `exceededTransferLimit`, to cover the ~4,000-camera statewide set. Frames are stills on two Divas Cloud hosts (`images-dis.divas.cloud`, `snapshots.divas.cloud`); both are host-pinned and a row whose image resolves elsewhere is dropped. `CCTV_FLORIDA_MAX_SOURCES` caps the pack (default 500, anchored per region across the state). Each catalog page is read under a 2 MiB cap and redirects are refused. Frames are fetched live at request time and never stored or redistributed. FL511's terms (fl511.com/privacy#disclaimer) license its content for individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent.
- **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~130 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo or an explicit port, or pointing outside that host's frame directory, is rejected even on an allowed host; an SCT `sctidcam` must be a bare `.gif`/`.jpg` filename), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Camera ids are built from the SCT camera file or the partner image path, not the feed's `fid` (which embeds an upstream server-start stamp and changes on every upstream restart), and rows the feed lists more than once collapse to one camera. The feed carries no camera status field, so a camera its publisher has taken offline still registers and shows the publisher's own "image unavailable" still (SCT and Barcelona both serve one with HTTP 200), the same as other packs without a status field. Frames are fetched at request time and never stored or redistributed. The feed itself states no licence; the pack relies on the Generalitat de Catalunya open-data portal's terms for this dataset (the licence and attribution above). Disable with `CCTV_CATALONIA_ENABLED=0`. - **Catalonia (Servei Català de Trànsit) cameras.** The camera list is the keyless `https://www.gencat.cat/transit/opendata/cameres.xml` WFS/GML feed, fetched over https with redirects refused and the body read capped at 1 MiB, since the list decides ids, coordinates and which allowlisted frame path is proxied. It carries a `font` field naming the real publisher per camera; this pack keeps SCT's own highway cameras plus hotlinked Barcelona (IMI) and Terrassa municipal cameras (~130 total), each confirmed to return a real frame, so frame URLs are checked against a small host allowlist (`mct.gencat.cat`, `www.bcn.cat`, `emap.terrassa.cat`) rather than one pinned origin (a link carrying `user:pass@` userinfo or an explicit port, or pointing outside that host's frame directory, is rejected even on an allowed host; an SCT `sctidcam` must be a bare `.gif`/`.jpg` filename), and every non-SCT camera carries its real owner as a `credit` shown beside the shared `provider`. The feed carries no compass heading, so every camera loads at low heading confidence (same as TfL and Fintraffic). SCT's own feed link (`RenderService`) needed a workaround: it always 302s to a second endpoint (`TransitCamera`) on a Location the upstream hardcodes as plain http even when requested over https — refused by `fetchWithinHost`'s same-origin (scheme-included) redirect policy — and separately, `mct.gencat.cat`'s TLS setup fails the handshake with Node's fetch outright (`ERR_SSL_WRONG_SIGNATURE_TYPE`) even though other HTTP clients complete it. The loader therefore builds the `TransitCamera` URL itself, over plain http, from `RenderService`'s `sctidcam` id, rather than registering the (broken) upstream link — the only transport this one host actually serves to this runtime. Camera ids are built from the SCT camera file or the partner image path, not the feed's `fid` (which embeds an upstream server-start stamp and changes on every upstream restart), and rows the feed lists more than once collapse to one camera. The feed carries no camera status field, so a camera its publisher has taken offline still registers and shows the publisher's own "image unavailable" still (SCT and Barcelona both serve one with HTTP 200), the same as other packs without a status field. Frames are fetched at request time and never stored or redistributed. The feed itself states no licence; the pack relies on the Generalitat de Catalunya open-data portal's terms for this dataset (the licence and attribution above). Disable with `CCTV_CATALONIA_ENABLED=0`.
- **Iceland (Vegagerðin / IRCA) cameras.** The camera list is the keyless `https://gagnaveita.vegagerdin.is/api/vefmyndavelar2014_1` JSON feed (about 500 camera views at roughly 170 stations), fetched over https with redirects refused and the body read capped at 2 MiB, since the list decides ids, coordinates and which frame path is proxied. Frames are JPEG stills in `https://www.vegagerdin.is/vgdata/vefmyndavelar/`; only links inside that directory are registered (userinfo, explicit ports and encoded path separators are rejected). Camera ids come from the frame file name. The feed's `Skyring` view description is the camera label, and a stated facing ("til vesturs", "í suður", "séð austur") sets a high-confidence heading; a view that states none, or two different ones ("niður á veg", a bare road direction), takes the id-hash low-confidence pose. The feed has no elevation, so cameras carry a flat prior that the client's ground snap corrects. IRCA publishes road camera images under CC BY 4.0 and requires the licensor to be acknowledged without implying endorsement; the pack shows the attribution above (licensor, dataset, CC BY 4.0, terms link and no-endorsement note) in the Data attribution popover, and every camera record carries the licence string. IRCA describes the images as fetched several times per hour (a live frame's `Last-Modified` was about four minutes old on 2026-11-06) and the real-time data as provided as is, without warranty. Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_ICELAND_ENABLED=0`.
- **King County road cameras.** Keyless. The camera list is one public ArcGIS FeatureServer GeoJSON query (King County DOT Road Services, "King County and WSDOT Traffic Cameras"). The `outFields` list is explicit on purpose: the layer also carries asset-administration columns — including staff names — that are never fetched. Only live, active cameras with frames on the county's own `info.kingcounty.gov` image handler are registered (the feed writes `http://`; the handler serves HTTPS, so URLs are upgraded and pinned before registration). WSDOT-owned rows in the layer are skipped because their frames are WSDOT-hosted, outside the pinned image origin; partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner as the per-camera credit. The layer publishes no facing, so headings use the shared id-hash fallback at low confidence. Uniquely, the county publishes each camera's hardware `Manufacturer`/`Model` (Cohu 3950/3965, Bosch AUTODOME, Axis PTZ domes); both are carried on the source (and served by `/api/cctv/sources`) as `model` and `manufacturer` for identified-hardware consumers (vendor-generic strings such as `PTZ` name no model and are omitted). Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_KINGCOUNTY_ENABLED=0`. - **King County road cameras.** Keyless. The camera list is one public ArcGIS FeatureServer GeoJSON query (King County DOT Road Services, "King County and WSDOT Traffic Cameras"). The `outFields` list is explicit on purpose: the layer also carries asset-administration columns — including staff names — that are never fetched. Only live, active cameras with frames on the county's own `info.kingcounty.gov` image handler are registered (the feed writes `http://`; the handler serves HTTPS, so URLs are upgraded and pinned before registration). WSDOT-owned rows in the layer are skipped because their frames are WSDOT-hosted, outside the pinned image origin; partner-city cameras hosted on the county image handler (e.g. Newcastle) name their owner as the per-camera credit. The layer publishes no facing, so headings use the shared id-hash fallback at low confidence. Uniquely, the county publishes each camera's hardware `Manufacturer`/`Model` (Cohu 3950/3965, Bosch AUTODOME, Axis PTZ domes); both are carried on the source (and served by `/api/cctv/sources`) as `model` and `manufacturer` for identified-hardware consumers (vendor-generic strings such as `PTZ` name no model and are omitted). Frames are fetched at request time and never stored or redistributed. Disable with `CCTV_KINGCOUNTY_ENABLED=0`.
- **WSDOT highway cameras.** Keyless. The camera list is the Esri JSON layer `data.wsdot.wa.gov/travelcenter/Cameras.json` — the same layer WSDOT's own Travel Center map consumes (~1,700 point features, EPSG:3857 geometry converted to WGS84 on load; the body carries Windows-1252 bytes and is decoded as such). Only frames on WSDOT's own `images.wsdot.wa.gov` host are registered — the catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle, lodges, airports) whose own terms apply, and those are skipped rather than proxied. WSDOT's layer metadata marks the feed for "low volume" use (see WSDOT's [travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer)). The catalog is fetched once per CCTV catalog refresh (every 15 minutes), and a camera's frame is fetched only when that camera is viewed; frames are never stored or redistributed. The live feed's `CompassDirection` is a single cardinal (N/E/S/W), `B` (both directions — most rows), `O`, or null. A single cardinal gives a high-confidence heading (the diagonals NE/SE/SW/NW are mapped too should they appear); `B`, `O`, null, and any other code carry no single facing and use the shared id-hash fallback at low confidence. By default the 250 cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` changes the cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off. A keyed WSDOT Traveler Information API serves the same population for higher-volume use and is deliberately not required here. - **WSDOT highway cameras.** Keyless. The camera list is the Esri JSON layer `data.wsdot.wa.gov/travelcenter/Cameras.json` — the same layer WSDOT's own Travel Center map consumes (~1,700 point features, EPSG:3857 geometry converted to WGS84 on load; the body carries Windows-1252 bytes and is decoded as such). Only frames on WSDOT's own `images.wsdot.wa.gov` host are registered — the catalog also lists ~70 partner cameras (Oregon DOT, City of Seattle, lodges, airports) whose own terms apply, and those are skipped rather than proxied. WSDOT's layer metadata marks the feed for "low volume" use (see WSDOT's [travel information disclaimer](https://wsdot.wa.gov/about/policies/travel-information-disclaimer)). The catalog is fetched once per CCTV catalog refresh (every 15 minutes), and a camera's frame is fetched only when that camera is viewed; frames are never stored or redistributed. The live feed's `CompassDirection` is a single cardinal (N/E/S/W), `B` (both directions — most rows), `O`, or null. A single cardinal gives a high-confidence heading (the diagonals NE/SE/SW/NW are mapped too should they appear); `B`, `O`, null, and any other code carry no single facing and use the shared id-hash fallback at low confidence. By default the 250 cameras nearest Seattle and Spokane load; `CCTV_WSDOT_MAX_SOURCES` changes the cap (8–1,200) and `CCTV_WSDOT_ENABLED=0` turns the pack off. A keyed WSDOT Traveler Information API serves the same population for higher-volume use and is deliberately not required here.
- **Hong Kong Transport Department traffic snapshot cameras.** Keyless. The English camera list is the public DATA.GOV.HK XML at `static.data.gov.hk/td/traffic-snapshot-images/code/Traffic_Camera_Locations_En.xml`; frames are stills on `tdcctv.data.one.gov.hk`, and only that host is registered. Frame URLs are rebuilt from each camera key (`https://tdcctv.data.one.gov.hk/{KEY}.JPG`) rather than taken verbatim from the catalog. Stills were probed to answer directly on that origin with no redirect hop. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. By default nearly the full catalog loads (~1,013; pack cap 1,100), prioritized nearest Central, Tsim Sha Tsui, Sha Tin, Tsuen Wan, Tai Po, Fanling, Tuen Mun, Yuen Long and Tseung Kwan O so a lowered cap still spans Hong Kong Island, Kowloon and the New Territories. When the sum of packs exceeds the catalog-wide ceiling, shared round-robin keeps every lane represented. `CCTV_HK_MAX_SOURCES` (8–1200) changes the cap and `CCTV_HK_ENABLED=0` turns the pack off. The DATA.GOV.HK Terms of Use require attribution to the Government / Transport Department / DATA.GOV.HK. - **Hong Kong Transport Department traffic snapshot cameras.** Keyless. The English camera list is the public DATA.GOV.HK XML at `static.data.gov.hk/td/traffic-snapshot-images/code/Traffic_Camera_Locations_En.xml`; frames are stills on `tdcctv.data.one.gov.hk`, and only that host is registered. Frame URLs are rebuilt from each camera key (`https://tdcctv.data.one.gov.hk/{KEY}.JPG`) rather than taken verbatim from the catalog. Stills were probed to answer directly on that origin with no redirect hop. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. By default nearly the full catalog loads (~1,013; pack cap 1,100), prioritized nearest Central, Tsim Sha Tsui, Sha Tin, Tsuen Wan, Tai Po, Fanling, Tuen Mun, Yuen Long and Tseung Kwan O so a lowered cap still spans Hong Kong Island, Kowloon and the New Territories. When the sum of packs exceeds the catalog-wide ceiling, shared round-robin keeps every lane represented. `CCTV_HK_MAX_SOURCES` (8–1200) changes the cap and `CCTV_HK_ENABLED=0` turns the pack off. The DATA.GOV.HK Terms of Use require attribution to the Government / Transport Department / DATA.GOV.HK.
@@ -188,7 +237,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **Houston TranStar cameras.** Keyless. The camera list is the public `traffic.houstontranstar.org/data/layers/cctvSnapshots_json.js` catalog (~1,000 cameras across Greater Houston); frames are stills at `www.houstontranstar.org/snapshots/cctv/<id>.jpg`, synthesized from a validated numeric id rather than taken from the payload, and only that host is registered. Cameras whose catalog id is `0` (a different, non-snapshot class) and cameras outside Greater Houston are dropped. TranStar's `dir` field is the side of the freeway the camera sits on, not the way it faces: TranStar's own map shows it as "Traffic closest to the camera is traveling …" (South → eastbound, North → westbound, West → southbound, East → northbound). That travel direction (90°, 270°, 180°, 0°) seeds the heading as a low-confidence prior with the low-confidence pose; `None` or an unknown value uses the shared id-hash fallback at low confidence. Headings are corrected with the in-app calibration gizmo. Off by default. TranStar's [Content Usage terms](https://traffic.houstontranstar.org/disclaimer.aspx) prohibit copying or distributing the site's information, and external framing or linking to specific images, without TxDOT's prior written permission; its FAQ grants permission-free use only for the RSS feeds. This project does not hold that permission for downstream installations, so the pack loads only when an operator who holds TxDOT's written permission sets `CCTV_HOUSTON_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to TranStar. Once enabled, it loads the 250 cameras nearest downtown, Uptown, the Medical Center, IAH and Galveston; `CCTV_HOUSTON_MAX_SOURCES` (8–400) changes the cap. Snapshots update about every 3 minutes, which is also the ambient still cadence. Frames are fetched from TranStar and relayed to the browser on demand, buffered only transiently per request, sent with `Cache-Control: no-store`, and not archived. - **Houston TranStar cameras.** Keyless. The camera list is the public `traffic.houstontranstar.org/data/layers/cctvSnapshots_json.js` catalog (~1,000 cameras across Greater Houston); frames are stills at `www.houstontranstar.org/snapshots/cctv/<id>.jpg`, synthesized from a validated numeric id rather than taken from the payload, and only that host is registered. Cameras whose catalog id is `0` (a different, non-snapshot class) and cameras outside Greater Houston are dropped. TranStar's `dir` field is the side of the freeway the camera sits on, not the way it faces: TranStar's own map shows it as "Traffic closest to the camera is traveling …" (South → eastbound, North → westbound, West → southbound, East → northbound). That travel direction (90°, 270°, 180°, 0°) seeds the heading as a low-confidence prior with the low-confidence pose; `None` or an unknown value uses the shared id-hash fallback at low confidence. Headings are corrected with the in-app calibration gizmo. Off by default. TranStar's [Content Usage terms](https://traffic.houstontranstar.org/disclaimer.aspx) prohibit copying or distributing the site's information, and external framing or linking to specific images, without TxDOT's prior written permission; its FAQ grants permission-free use only for the RSS feeds. This project does not hold that permission for downstream installations, so the pack loads only when an operator who holds TxDOT's written permission sets `CCTV_HOUSTON_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to TranStar. Once enabled, it loads the 250 cameras nearest downtown, Uptown, the Medical Center, IAH and Galveston; `CCTV_HOUSTON_MAX_SOURCES` (8–400) changes the cap. Snapshots update about every 3 minutes, which is also the ambient still cadence. Frames are fetched from TranStar and relayed to the browser on demand, buffered only transiently per request, sent with `Cache-Control: no-store`, and not archived.
- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km<N>.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_ENABLED=0`. - **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km<N>.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_ENABLED=0`.
- **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals.
- **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All eighteen registered feeds, covering seventeen regions, are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington, YRT and Rio’s SMTR publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for. - **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All fifty-two registered feeds, covering forty-nine regions, are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington, YRT and Rio’s SMTR publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for.
- **Radio country coverage (optional).** Set `RADIO_PRIORITY_COUNTRIES=IN,PL` in the local `.env` and restart to include stations beyond the global click charts. The operator may choose up to eight unique ISO alpha-2 country codes; no key is needed. Each country adds one bounded query (at most 200 rows) and reserves up to 40 unique, usable stations inside the existing 750-station catalog. Country seeds precede specialist-tag seeds and global popularity fill; unused slots return to the shared pool. This is a coverage preference, not an exhaustive national directory or a guarantee of stations in every city. Countries with no eligible HTTPS MP3/AAC geolocated stations remain empty. An unset list preserves global selection. Country requests share the directory cache and three-worker pool; failed country queries are reported in coverage without blocking a healthy global refresh. Failed/degraded refreshes back off for one minute, with at most three mirrors per query. - **Radio country coverage (optional).** Set `RADIO_PRIORITY_COUNTRIES=IN,PL` in the local `.env` and restart to include stations beyond the global click charts. The operator may choose up to eight unique ISO alpha-2 country codes; no key is needed. Each country adds one bounded query (at most 200 rows) and reserves up to 40 unique, usable stations inside the existing 750-station catalog. Country seeds precede specialist-tag seeds and global popularity fill; unused slots return to the shared pool. This is a coverage preference, not an exhaustive national directory or a guarantee of stations in every city. Countries with no eligible HTTPS MP3/AAC geolocated stations remain empty. An unset list preserves global selection. Country requests share the directory cache and three-worker pool; failed country queries are reported in coverage without blocking a healthy global refresh. Failed/degraded refreshes back off for one minute, with at most three mirrors per query.
- **Radio Browser.** In every installation, including when `RADIO_PRIORITY_COUNTRIES` is unset, each request tries at most three mirror candidates (later fallback candidates are skipped), and failed or partial catalog refreshes back off for 60 seconds. A cold-start failure therefore continues returning 503 during that delay even if upstream recovers. `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; broad/specialist responses whose rows all fail the product's health policy do not count as successful queries. Optional priority-country outcomes are advisory: valid, in-limit responses with no eligible stations succeed as empty results, while failed or malformed country queries report unsuccessful coverage without degrading the global catalog. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty broad/specialist payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. - **Radio Browser.** In every installation, including when `RADIO_PRIORITY_COUNTRIES` is unset, each request tries at most three mirror candidates (later fallback candidates are skipped), and failed or partial catalog refreshes back off for 60 seconds. A cold-start failure therefore continues returning 503 during that delay even if upstream recovers. `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; broad/specialist responses whose rows all fail the product's health policy do not count as successful queries. Optional priority-country outcomes are advisory: valid, in-limit responses with no eligible stations succeed as empty results, while failed or malformed country queries report unsuccessful coverage without degrading the global catalog. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty broad/specialist payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply.
@@ -209,124 +258,6 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
--- ---
<!-- data-library:start (generated by npm run library:docs) -->
## Data Library entries (fetched at runtime — not stored in this repo)
Each Data Library entry is a manifest in `src/library/entries/` pointing at a live upstream its provider hosts; see [docs/DATA-LIBRARY.md](docs/DATA-LIBRARY.md). An entry appears on the map only after a viewer adds it from **DATA LAYERS ▸ Browse data library**, and its attribution is registered in the Data attribution popover while it is in use. This table is generated from the manifests: edit the manifest, then run `npm run library:docs`.
| Entry | How it is served | Licence / terms | Attribution |
| --- | --- | --- | --- |
| **Alaska Statewide Imagery (2023)** (`ak-statewide-imagery`, `apps.geo.fpac.usda.gov`) | tiles, bbox, browser-direct | [Public Domain (U.S. Government work)](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits) | "USDA / Alaska Geospatial Council" |
| **Amsterdam traffic and ANPR camera positions** (`amsterdam-traffic-cameras`, `api.data.amsterdam.nl`) | dataset, geojson, browser-direct; operator adds `amsterdam-traffic-cameras` to `GEV_LIBRARY_ENABLE` | [Gemeente Amsterdam open data: public unless stated otherwise; no named licence](https://schemas.data.amsterdam.nl/datasets/verkeersinformatiesystemen/dataset) | "Gemeente Amsterdam, verkeersinformatiesystemen register (entry contributed by @Maher-Reven)" |
| **Arkansas Statewide Imagery (2023)** (`ar-statewide-imagery`, `gis.arkansas.gov`) | tiles, bbox, browser-direct | [Public domain — Arkansas GIS Office open data](https://gis.arkansas.gov/) | "Arkansas GIS Office" |
| **Arizona Statewide Imagery (2017)** (`az-statewide-imagery`, `azgeo.az.gov`) | tiles, xyz, browser-direct; operator adds `az-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://azgeo.az.gov/) | "AZGEO" |
| **Bangor, Maine Orthoimagery** (`bangor-me`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `bangor-me` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.bangormaine.gov/) | "City of Bangor GIS" |
| **basemap.at Orthofoto** (`basemap-at-orthofoto`, `mapsneu.wien.gv.at`) | tiles, xyz, browser-direct | [CC BY 4.0](https://basemap.at/nutzungsbedingungen/) | "basemap.at — Datenquelle: basemap.at" |
| **Digitales Orthophoto Bayern (DOP 40 cm, open)** (`bayern-dop40`, `geoservices.bayern.de`) | tiles, bbox, browser-direct | [CC BY 4.0](https://geodaten.bayern.de/opengeodata/) | "© Bayerische Vermessungsverwaltung" |
| **Bismarck–Mandan, North Dakota Aerial Photography** (`bismarck-mandan-nd`, `ndgishub.nd.gov`) | tiles, bbox, browser-direct; operator adds `bismarck-mandan-nd` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.gis.nd.gov/) | "95West Aerial Mapping" |
| **TopPlusOpen basemap (BKG)** (`bkg-topplus-open`, `sgx.geodatenzentrum.de`) | tiles, xyz, browser-direct | [Datenlizenz Deutschland – Namensnennung 2.0](https://www.govdata.de/dl-de/by-2-0) | "© BKG, dl-de/by-2-0" |
| **Florida cameras (FL511)** (`cctv-florida-fl511`, `fl511.com`) | CCTV pack; operator sets `CCTV_FLORIDA_ENABLED=1` | [FL511 content: individual personal use only](https://fl511.com/privacy#disclaimer) | "Florida DOT — FL511.com" |
| **Lithuania road cameras (Via Lietuva)** (`cctv-lithuania`, `eismoinfo.lt`) | CCTV pack; operator sets `CCTV_LITHUANIA_ENABLED=1` | [Via Lietuva open-data statement; camera photo coverage unconfirmed](https://eismoinfo.lt/eismoinfo-backend/content?id=44) | "Via Lietuva / eismoinfo.lt" |
| **New York City cameras (NYC DOT TMC)** (`cctv-nyc-dot`, `webcams.nyctmc.org`) | CCTV pack; operator sets `CCTV_NYC_ENABLED=1` | [NYC DOT camera feed: data-sharing agreement required](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) | "NYC DOT Traffic Management Center" |
| **Connecticut Statewide Imagery (2019)** (`ct-statewide-imagery`, `cteco.uconn.edu`) | tiles, bbox, browser-direct; operator adds `ct-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://ct-deep-gis-open-data-website-ctdeep.hub.arcgis.com/) | "CT ECO — UConn CLEAR / CT DEEP" |
| **Dakota County, Minnesota Air Photo** (`dakota-county-mn`, `gisimg.co.dakota.mn.us`) | tiles, bbox, browser-direct; operator adds `dakota-county-mn` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://gis.co.dakota.mn.us/) | "Dakota County GIS" |
| **District of Columbia Statewide Imagery (2025)** (`dc-statewide-imagery`, `imagery.dcgis.dc.gov`) | tiles, bbox, browser-direct | [Open data — DC public domain terms](https://opendata.dc.gov/pages/terms-of-use) | "Office of the Chief Technology Officer" |
| **Delaware Statewide Imagery (2024)** (`de-statewide-imagery`, `imagery.firstmap.delaware.gov`) | tiles, bbox, browser-direct; operator adds `de-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://firstmap.delaware.gov/) | "This data is meant for use by requested parties. This data cannot be used without express consent.This data is meant for use by requested parties. This data cannot be used without express consent." |
| **Eesti ortofoto (Maa-amet)** (`ee-maa-amet-ortofoto`, `tiles.maaamet.ee`) | tiles, xyz, browser-direct | [Maa-amet open data licence (attribution)](https://geoportaal.maaamet.ee/eng/Spatial-Data/Orthophotos-p309.html) | "© Maa-amet (Estonian Land Board)" |
| **US power plants, 100 MW and up (EIA)** (`eia-power-plants`, `services2.arcgis.com`) | dataset, arcgis, browser-direct | [U.S. Energy Information Administration data: public domain](https://www.eia.gov/about/copyrights_reuse.php) | "U.S. Energy Information Administration (EIA-860, EIA-860M, EIA-923)" |
| **European seabed depth (EMODnet)** (`emodnet-bathymetry`, `tiles.emodnet-bathymetry.eu`) | tiles, xyz, browser-direct | [EMODnet data: free use with attribution](https://emodnet.ec.europa.eu/en/emodnet-data-policy) | "EMODnet Bathymetry Consortium — EMODnet Digital Bathymetry (DTM)" |
| **Superfund sites (US EPA)** (`epa-superfund-sites`, `geopub.epa.gov`) | dataset, arcgis, browser-direct | [U.S. EPA data: public domain](https://www.epa.gov/web-policies-and-procedures/epa-disclaimers) | "U.S. Environmental Protection Agency — Facility Registry Service (EnviroMapper)" |
| **Toxic release facilities (US EPA TRI)** (`epa-toxic-release-facilities`, `geopub.epa.gov`) | dataset, arcgis, browser-direct | [U.S. EPA data: public domain](https://www.epa.gov/web-policies-and-procedures/epa-disclaimers) | "U.S. Environmental Protection Agency — Toxics Release Inventory via the Facility Registry Service" |
| **Home energy ratings, England and Wales (EPC)** (`epc-england-wales`, `s3.us-west-2.amazonaws.com`) | dataset, geoparquet, browser-direct | [Open Government Licence v3.0 (EPC data); OS Open UPRN under OGL](https://epc.opendatacommunities.org/docs/copyright) | "Energy Performance of Buildings Data, DLUHC; geocoded by Addresscloud with OS Open UPRN — contains OS data © Crown copyright and database right; via Source Cooperative" |
| **US building footprints (FEMA USA Structures)** (`fema-usa-structures`, `services2.arcgis.com`) | dataset, arcgis, browser-direct | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | "FEMA Response Geospatial Office and Oak Ridge National Laboratory — USA Structures" |
| **Biodiversity records (GBIF)** (`gbif-occurrence-density`, `api.gbif.org`) | tiles, xyz, browser-direct | [GBIF data use agreement (records under CC0, CC BY or CC BY-NC)](https://www.gbif.org/terms) | "GBIF.org — Global Biodiversity Information Facility" |
| **Disaster alerts (GDACS)** (`gdacs-disaster-alerts`, `www.gdacs.org`) | dataset, geojson, browser-direct | [GDACS terms of use (free, attribution required)](https://www.gdacs.org/About/termofuse.aspx) | "GDACS — Global Disaster Alert and Coordination System (UN, European Commission)" |
| **Orthophotos Canton de Genève (SITG)** (`geneva-sitg-ortho`, `ge.ch`) | tiles, bbox, browser-direct | [SITG open data — free use with attribution](https://ge.ch/sitg/media/sitg/documents/CGU_donnees_libres.pdf) | "© SITG — Etat de Genève" |
| **Population density 2020 (SEDAC GPW)** (`gpw-population-density`, `gibs.earthdata.nasa.gov`) | tiles, xyz, browser-direct | [CC BY 4.0](https://sedac.ciesin.columbia.edu/data/collection/gpw-v4/terms-of-use) | "CIESIN, Columbia University — Gridded Population of the World v4.11, via NASA GIBS" |
| **Grand Forks, North Dakota Aerial Photography** (`grand-forks-nd`, `ndgishub.nd.gov`) | tiles, bbox, browser-direct; operator adds `grand-forks-nd` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.gis.nd.gov/) | "Grand Forks/East Grand Forks Metropolitan Planning Organization (MPO)" |
| **Japan standard map (GSI)** (`gsi-japan-map`, `cyberjapandata.gsi.go.jp`) | tiles, xyz, browser-direct | [GSI Tiles terms (free use with attribution)](https://maps.gsi.go.jp/development/ichiran.html) | "Geospatial Information Authority of Japan (GSI Tiles)" |
| **Japan aerial photos (GSI)** (`gsi-japan-photo`, `cyberjapandata.gsi.go.jp`) | tiles, xyz, browser-direct | [GSI Tiles terms (free use with attribution)](https://maps.gsi.go.jp/development/ichiran.html) | "Geospatial Information Authority of Japan (GSI Tiles)" |
| **Holocene volcanoes (Smithsonian GVP)** (`gvp-holocene-volcanoes`, `webservices.volcano.si.edu`) | dataset, geojson, proxied | [Smithsonian Terms of Use (personal, educational, non-commercial use; cite the Global Volcanism Program)](https://www.si.edu/termsofuse) | "Global Volcanism Program, Smithsonian Institution — Volcanoes of the World" |
| **Iowa Statewide Imagery (2023)** (`ia-statewide-imagery`, `ortho.gis.iastate.edu`) | tiles, bbox, browser-direct; operator adds `ia-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://geodata.iowa.gov/) | "USDA NAIP, Iowa State University GIS Facility" |
| **Ortofoto de Catalunya 25 cm** (`icgc-orto-catalunya`, `geoserveis.icgc.cat`) | tiles, xyz, browser-direct | [CC BY 4.0](https://www.icgc.cat/en/Ambits-tematics/Ortofoto) | "© Institut Cartogràfic i Geològic de Catalunya" |
| **Idaho Statewide Imagery (2023)** (`id-statewide-imagery`, `insideidaho.org`) | tiles, bbox, browser-direct | [Public domain / open — INSIDE Idaho terms](https://insideidaho.org/) | "United States Department of Agriculture, Farm Production and Conservation Business Center, Geospatial Enterprise Operations (FPAC-BC-GEO)" |
| **France aerial photos (IGN)** (`ign-orthophotos-france`, `data.geopf.fr`) | tiles, xyz, browser-direct | [Licence Ouverte / Etalab 2.0](https://www.etalab.gouv.fr/licence-ouverte-open-licence/) | "© IGN — Géoplateforme" |
| **Illinois Statewide Imagery** (`il-statewide-imagery`, `data.isgs.illinois.edu`) | tiles, bbox, browser-direct; operator adds `il-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://clearinghouse.isgs.illinois.edu/) | "Illinois State Geological Survey / IDOT" |
| **Maritime chokepoints (IMF PortWatch)** (`imf-portwatch-chokepoints`, `services9.arcgis.com`) | dataset, arcgis, browser-direct; operator adds `imf-portwatch-chokepoints` to `GEV_LIBRARY_ENABLE` | [IMF PortWatch terms; attribution required (reuse terms under review)](https://www.imf.org/external/terms.htm) | "IMF PortWatch (portwatch.imf.org)" |
| **Port disruptions (IMF PortWatch)** (`imf-portwatch-disruptions`, `services9.arcgis.com`) | dataset, arcgis, browser-direct; operator adds `imf-portwatch-disruptions` to `GEV_LIBRARY_ENABLE` | [IMF PortWatch terms; attribution required (reuse terms under review)](https://www.imf.org/external/terms.htm) | "IMF PortWatch (portwatch.imf.org), with GDACS event data" |
| **Indiana Statewide Imagery (2016)** (`in-statewide-imagery`, `di-ingov.img.arcgis.com`) | tiles, bbox, browser-direct | [CC0 1.0 Universal (public domain dedication)](https://creativecommons.org/publicdomain/zero/1.0/legalcode) | "Indiana Geographic Information Office (CC0)" |
| **Surface water occurrence (JRC)** (`jrc-surface-water`, `storage.googleapis.com`) | tiles, xyz, browser-direct | [Copernicus programme: free, without restriction of use](https://global-surface-water.appspot.com/download) | "Source: EC JRC/Google — Pekel et al. (2016), Global Surface Water" |
| **Norway counties and municipalities (Kartverket)** (`kartverket-admin-units`, `wms.geonorge.no`) | tiles, bbox, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway cadastral map (Kartverket)** (`kartverket-cadastre`, `wms.geonorge.no`) | tiles, bbox, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway sea depths (Kartverket)** (`kartverket-depth`, `wms.geonorge.no`) | tiles, bbox, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway hillshade (Kartverket)** (`kartverket-hillshade`, `wms.geonorge.no`) | tiles, bbox, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway nautical charts (Kartverket)** (`kartverket-nautical-charts`, `cache.kartverket.no`) | tiles, xyz, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway terrain relief (Kartverket DTM)** (`kartverket-terrain-relief`, `wms.geonorge.no`) | tiles, bbox, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway topographic map (Kartverket)** (`kartverket-topo`, `cache.kartverket.no`) | tiles, xyz, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway topographic map, greyscale (Kartverket)** (`kartverket-topo-grey`, `cache.kartverket.no`) | tiles, xyz, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Norway topographic raster map (Kartverket)** (`kartverket-toporaster`, `cache.kartverket.no`) | tiles, xyz, browser-direct | [CC BY 4.0 (Kartverket free products)](https://www.kartverket.no/api-og-data/vilkar-for-bruk) | "© Kartverket" |
| **Kansas Statewide Imagery (2021)** (`ks-statewide-imagery`, `services.kgs.ku.edu`) | tiles, bbox, browser-direct; operator adds `ks-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://www.kansasgis.org/) | "FSA, USDA," |
| **Kentucky Statewide Imagery** (`ky-statewide-imagery`, `kyraster.ky.gov`) | tiles, bbox, browser-direct | [Public domain with attribution](https://kygeonet.ky.gov/) | "KyFromAbove" |
| **Lake County, Indiana Aerial Imagery** (`lake-county-in`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `lake-county-in` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.lakecountyin.org/) | "Lake County Surveyor's Office" |
| **Lawrence, Kansas Aerial Photos** (`lawrence-ks`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `lawrence-ks` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://gis-lawrenceks.opendata.arcgis.com/) | "City of Lawrence GIS" |
| **Lee County, Florida Aerial Imagery** (`lee-county-fl`, `gisimageserver.leegov.com`) | tiles, bbox, browser-direct; operator adds `lee-county-fl` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.leegov.com/gis) | "Eagle View, Lee County Property Appraiser, Lee County GIS" |
| **Loudoun County, Virginia Imagery** (`loudoun-county-va`, `logis.loudoun.gov`) | tiles, bbox, browser-direct; operator adds `loudoun-county-va` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://geohub-loudoungis.opendata.arcgis.com/) | "Loudoun County GIS" |
| **Louisville / LOJIC, Kentucky Imagery** (`louisville-lojic-ky`, `services.lojic.org`) | tiles, bbox, browser-direct; operator adds `louisville-lojic-ky` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.lojic.org/) | "LOJIC, USDA-NAIP" |
| **Orthophoto officielle du Luxembourg** (`lu-ortho-latest`, `wmts1.geoportail.lu`) | tiles, xyz, browser-direct | [CC0 1.0 (Licence ouverte Luxembourg)](https://data.public.lu/en/datasets/orthophoto-officelle-du-grand-duche-de-luxembourg-edition-2023/) | "Administration du cadastre et de la topographie, Luxembourg (CC0)" |
| **Massachusetts Statewide Imagery** (`ma-statewide-imagery`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `ma-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://gis.data.mass.gov/) | "MassGIS, Commonwealth of Massachusetts" |
| **Geologic map (Macrostrat)** (`macrostrat-geology`, `tiles.macrostrat.org`) | tiles, xyz, browser-direct | [CC BY 4.0 (Macrostrat; source maps credited by their publishers)](https://macrostrat.org/about) | "Macrostrat — geologic maps from their original publishers" |
| **Global terrain shaded relief (Mapterhorn)** (`mapterhorn-terrain`, `tiles.mapterhorn.com`) | tiles, xyz, browser-direct | [Open data, per source (mostly CC BY 4.0 and public domain)](https://mapterhorn.com/attribution) | "© Mapterhorn and its open elevation sources" |
| **MARIS Imagery, Mississippi** (`maris-ms`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `maris-ms` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.gis.ms.gov/) | "MARIS" |
| **Maryland Statewide Imagery** (`md-statewide-imagery`, `mdgeodata.md.gov`) | tiles, bbox, browser-direct; operator adds `md-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://imap.maryland.gov/) | "Maryland iMAP" |
| **Maine Statewide Imagery** (`me-statewide-imagery`, `gis.maine.gov`) | tiles, bbox, browser-direct; operator adds `me-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://www.maine.gov/geolib/) | "USDA Aerial Photography Field Office" |
| **Michigan Statewide Imagery (2024)** (`mi-statewide-imagery`, `imagery.michigan.gov`) | tiles, bbox, browser-direct; operator adds `mi-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://gis-michigan.opendata.arcgis.com/) | "State of Michigan" |
| **Minnesota Statewide Imagery** (`mn-statewide-imagery`, `imageserver.gisdata.mn.gov`) | tiles, bbox, browser-direct | [Public domain / open — MnGeo terms](https://www.mngeo.state.mn.us/) | "MnGeo / State of Minnesota" |
| **Mountain Village, Colorado Orthoimagery** (`mountain-village-co`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `mountain-village-co` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.townofmountainvillage.com/) | "Town of Mountain Village" |
| **Mississippi Statewide Imagery** (`ms-statewide-imagery`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `ms-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://www.gis.ms.gov/) | "USDA-FSA-APFO" |
| **Montana Statewide Imagery (2023)** (`mt-statewide-imagery`, `gisservice.mt.gov`) | tiles, bbox, browser-direct | [Public domain — Montana State Library open data](https://msl.mt.gov/geoinfo/) | "Montana State Library" |
| **NAIP — National Agriculture Imagery Program** (`naip`, `imagery.nationalmap.gov`) | tiles, bbox, browser-direct | [Public Domain (U.S. Government work)](https://naip-usdaonline.hub.arcgis.com/) | "USDA NAIP" |
| **Earth at night (NASA Black Marble 2016)** (`nasa-black-marble`, `gibs.earthdata.nasa.gov`) | tiles, xyz, browser-direct | [NASA data: U.S. public domain; no endorsement implied](https://www.earthdata.nasa.gov/engage/open-data-services-software-policies) | "NASA Earth Observatory / GIBS — VIIRS Black Marble" |
| **NASA EONET open natural events** (`nasa-eonet-events`, `eonet.gsfc.nasa.gov`) | dataset, geojson, browser-direct | [NASA data: U.S. public domain; no endorsement implied](https://www.nasa.gov/nasa-brand-center/images-and-media/) | "NASA Earth Observatory Natural Event Tracker (EONET)" |
| **North Carolina Statewide Imagery (2023)** (`nc-statewide-imagery`, `services.nconemap.gov`) | tiles, bbox, browser-direct | [Public domain — NC OneMap terms of use](https://www.nconemap.gov/pages/terms) | "NC911, NCCGIA" |
| **North Central Texas (NCTCOG) Ortho** (`nctcog-tx`, `mapitimage.tarrantcounty.com`) | tiles, bbox, browser-direct; operator adds `nctcog-tx` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.nctcog.org/) | "North Central Texas Council of Governments" |
| **North Dakota Statewide Imagery (2013)** (`nd-statewide-imagery`, `ndgishub.nd.gov`) | tiles, xyz, browser-direct; operator adds `nd-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open state geodata — attribution requested](https://www.gis.nd.gov/) | "ND Dept. of Transportation, ND Dept. of Emergency Services, and the ND GIS Hub" |
| **New Orleans Regional Planning Commission Imagery** (`new-orleans-rpc-la`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `new-orleans-rpc-la` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.norpc.org/) | "Regional Planning Commission for Jefferson, Orleans, Plaquemines, St. Bernard, St. Charles, St. John the Baptist, St. Tammany, and Tangipahoa Parishes (RPC)" |
| **New Hampshire Statewide Imagery (2022)** (`nh-statewide-imagery`, `nhgeodata.unh.edu`) | tiles, bbox, browser-direct; operator adds `nh-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — NH GRANIT terms](https://www.granit.unh.edu/) | "NH GRANIT, University of New Hampshire" |
| **New Jersey Statewide Imagery (2015)** (`nj-statewide-imagery`, `maps.nj.gov`) | tiles, xyz, browser-direct; operator adds `nj-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — State of New Jersey provides data 'as is'](https://njgin.nj.gov/) | "Project definition, funding requests, and data distribution by NJ Office of Information Technology, Office of Geographic Information Systems (NJOIT - OGIS). Funding for the imagery project provided by" |
| **US tide stations (NOAA CO-OPS)** (`noaa-tide-stations`, `api.tidesandcurrents.noaa.gov`) | dataset, json, browser-direct | [NOAA data: U.S. public domain](https://tidesandcurrents.noaa.gov/disclaimers.html) | "NOAA Center for Operational Oceanographic Products and Services (CO-OPS)" |
| **Digitale Orthophotos NRW (DOP 10 cm)** (`nrw-dop`, `www.wms.nrw.de`) | tiles, bbox, browser-direct | [Datenlizenz Deutschland – Zero (public-domain-like)](https://www.govdata.de/dl-de/zero-2-0) | "Geobasis NRW (dl-de/zero-2-0)" |
| **NSW Imagery (Six Maps)** (`nsw-imagery`, `maps.six.nsw.gov.au`) | tiles, xyz, browser-direct | [CC BY 4.0](https://www.spatial.nsw.gov.au/products_and_services/web_services) | "© Spatial Services NSW" |
| **US bridges and their condition (NBI)** (`ntad-national-bridges`, `services.arcgis.com`) | dataset, arcgis, browser-direct | [U.S. government work: unrestricted public use](https://www.bts.gov/ntad) | "USDOT Bureau of Transportation Statistics — National Transportation Atlas Database, National Bridge Inventory" |
| **Northwest Arkansas (Benton & Washington County) Imagery** (`nwa-benton-washington-ar`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `nwa-benton-washington-ar` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://www.nwarpc.org/) | "NWARPC; Washington County; Benton County" |
| **US weather alerts with drawn areas (NWS)** (`nws-active-alerts`, `api.weather.gov`) | dataset, geojson, browser-direct | [NWS data: U.S. public domain](https://www.weather.gov/disclaimer) | "NOAA National Weather Service" |
| **New York Statewide Imagery (2019)** (`ny-statewide-imagery`, `orthos.its.ny.gov`) | tiles, xyz, browser-direct | [Public domain — NYS open data](https://data.gis.ny.gov/) | "USDA; NYS ITS Geospatial Services" |
| **Oakland County, Michigan Ortho Imagery** (`oakland-county-mi`, `gisservices.oakgov.com`) | tiles, bbox, browser-direct; operator adds `oakland-county-mi` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://oakgov.maps.arcgis.com/) | "Oakland County GIS" |
| **Ohio Statewide Imagery** (`oh-statewide-imagery`, `maps.ohio.gov`) | tiles, xyz, browser-direct | [Public domain — Ohio open data](https://maps.ohio.gov/) | "Ohio Geographically Referenced Information Program (OGRIP)" |
| **Seamarks (OpenSeaMap)** (`openseamap-seamarks`, `tiles.openseamap.org`) | tiles, xyz, browser-direct | [CC BY-SA 2.0; data © OpenStreetMap contributors (ODbL)](https://openseamap.org/index.php?id=imprint&L=1) | "© OpenSeaMap contributors, CC BY-SA; data © OpenStreetMap contributors" |
| **Oregon Statewide Imagery (2000)** (`or-statewide-imagery`, `imagery.oregonexplorer.info`) | tiles, bbox, browser-direct | [Public domain — Oregon open data](https://www.oregon.gov/geo/) | "Oregon, GEO, DAS" |
| **Pennsylvania Statewide Imagery** (`pa-statewide-imagery`, `imagery.pasda.psu.edu`) | tiles, bbox, browser-direct | [Public domain — PASDA open data](https://www.pasda.psu.edu/) | "PASDA / PAMAP, Commonwealth of Pennsylvania" |
| **Netherlands aerial photos (PDOK)** (`pdok-aerial-netherlands`, `service.pdok.nl`) | tiles, xyz, browser-direct | [CC BY 4.0](https://www.pdok.nl/introductie/-/article/luchtfoto-pdok) | "Beeldmateriaal Nederland via PDOK" |
| **Ortofotomapa Polski (GUGiK)** (`pl-ortofotomapa`, `mapy.geoportal.gov.pl`) | tiles, bbox, browser-direct | [Polish open geodata (free reuse, Prawo geodezyjne art. 40a)](https://www.geoportal.gov.pl/) | "© GUGiK / geoportal.gov.pl" |
| **PNOA Máxima Actualidad** (`pnoa-ma`, `www.ign.es`) | tiles, bbox, browser-direct | [CC BY 4.0 (compatible) — Orden FOM/2807/2015](https://www.scne.es/productos.html) | "PNOA © Instituto Geográfico Nacional de España" |
| **Portland Metro (RLIS), Oregon Aerial Photos** (`portland-metro-or`, `utility.arcgis.com`) | tiles, xyz, browser-direct; operator adds `portland-metro-or` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://rlisdiscovery.oregonmetro.gov/) | "Oregon Metro RLIS" |
| **Ortofotos de Portugal Continental (DGT)** (`pt-dgt-ortos`, `ortos.dgterritorio.gov.pt`) | tiles, bbox, browser-direct | [CC BY 4.0](https://www.dgterritorio.gov.pt/) | "© Direção-Geral do Território" |
| **Rhode Island Statewide Imagery (2019)** (`ri-statewide-imagery`, `tiles.arcgis.com`) | tiles, xyz, browser-direct; operator adds `ri-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — RIGIS terms](https://www.rigis.org/) | "URI EDC, RIGIS" |
| **Switzerland aerial photos (SWISSIMAGE 10 cm)** (`swisstopo-swissimage`, `wmts.geo.admin.ch`) | tiles, xyz, browser-direct | [swisstopo free geodata (OGD): free use, including commercial, with source credit](https://www.swisstopo.admin.ch/en/terms-of-use-free-geodata-and-geoservices) | "©swisstopo — SWISSIMAGE 10 cm" |
| **Texas Statewide Imagery (2022)** (`tx-statewide-imagery`, `imagery.geographic.texas.gov`) | tiles, bbox, browser-direct | [Public domain — TxGIO open data](https://geographic.texas.gov/) | "Texas Geographic Information Office (TxGIO)" |
| **Union County, Ohio Aerial Imagery** (`union-county-oh`, `www7.co.union.oh.us`) | tiles, bbox, browser-direct; operator adds `union-county-oh` to `GEV_LIBRARY_ENABLE` | [Local government open data — see publisher terms](https://union-county-auditor-unionco.hub.arcgis.com/) | "Union County / City of Marysville GIS" |
| **US rivers and lakes (USGS)** (`usgs-hydrography`, `basemap.nationalmap.gov`) | tiles, xyz, browser-direct | [USGS The National Map: U.S. public domain](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits) | "USGS The National Map — National Hydrography Dataset" |
| **US topographic map (USGS)** (`usgs-topo`, `basemap.nationalmap.gov`) | tiles, xyz, browser-direct | [USGS The National Map: U.S. public domain](https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits) | "USGS The National Map" |
| **Virginia Statewide Imagery** (`va-statewide-imagery`, `vginmaps.vdem.virginia.gov`) | tiles, xyz, browser-direct; operator adds `va-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — VGIN VBMP terms](https://vgin.vdem.virginia.gov/) | "Virginia Geographic Information Network (VGIN)" |
| **Orthofotomozaïek Vlaanderen (winter)** (`vlaanderen-omw`, `geo.api.vlaanderen.be`) | tiles, bbox, browser-direct | [Vlaanderen model licence for free reuse](https://www.vlaanderen.be/digitaal-vlaanderen/onze-oplossingen/orthofotomozaieken) | "© Digitaal Vlaanderen" |
| **Vermont Statewide Imagery** (`vt-statewide-imagery`, `maps.vcgi.vermont.gov`) | tiles, bbox, browser-direct | [Public domain — Vermont open data](https://vcgi.vermont.gov/) | "Vermont Center for Geographic Information (VCGI)" |
| **Orthophotos de Wallonie** (`wallonie-ortho`, `geoservices.wallonie.be`) | tiles, bbox, browser-direct | [Conditions ouvertes SPW (attribution)](https://geoportail.wallonie.be/catalogue-donnees) | "© Service public de Wallonie" |
| **Wisconsin Statewide Imagery (2020)** (`wi-statewide-imagery`, `di-wisdot.img.arcgis.com`) | tiles, bbox, browser-direct; operator adds `wi-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — WisconsinView / state open data](https://www.wisconsinview.org/) | "WisDOT - NAIP 2020" |
| **Global power plants (WRI)** (`wri-global-power-plants`, `raw.githubusercontent.com`) | dataset, csv, browser-direct | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | "World Resources Institute, Global Power Plant Database (2021)" |
| **West Virginia Statewide Imagery** (`wv-statewide-imagery`, `services.wvgis.wvu.edu`) | tiles, bbox, browser-direct; operator adds `wv-statewide-imagery` to `GEV_LIBRARY_ENABLE` | [Open — WV GIS Technical Center terms](https://wvgis.wvu.edu/) | "WV GIS Technical Center, West Virginia University" |
<!-- data-library:end -->
## Bundled snapshots ## Bundled snapshots
Static datasets shipped in the repo for an out-of-the-box experience. **None are MIT** — each keeps its own license (see the carve-out in [LICENSE](LICENSE)). Each folder also has its own provenance README. Static datasets shipped in the repo for an out-of-the-box experience. **None are MIT** — each keeps its own license (see the carve-out in [LICENSE](LICENSE)). Each folder also has its own provenance README.
@@ -338,10 +269,16 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **Datacenters** (4,352) | `datacenters/` | **ODbL 1.0** (OpenStreetMap extract) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) | | **Datacenters** (4,352) | `datacenters/` | **ODbL 1.0** (OpenStreetMap extract) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) |
| **Dams** (704) | `dams/` | **ODbL 1.0** (OpenInfraMap / OSM extract) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) + Open Infrastructure Map | | **Dams** (704) | `dams/` | **ODbL 1.0** (OpenInfraMap / OSM extract) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) + Open Infrastructure Map |
| **Military area names** (36,466) | `osm_military_names/` | **ODbL 1.0** (OpenStreetMap, via Overture Maps) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) + Overture Maps Foundation | | **Military area names** (36,466) | `osm_military_names/` | **ODbL 1.0** (OpenStreetMap, via Overture Maps) | ✅ (attribution + share-alike on data) | © OpenStreetMap contributors (shared credit) + Overture Maps Foundation |
| **Québec CCTV source catalog** (1,120 locations) | `public/quebec-cameras.json` | Mixed: MTQ locations CC BY 4.0; Montréal camera nodes ODbL 1.0; scenic records retain per-record provenance. No video files are bundled or cached. | Location data is per-record; video remains hosted by Québec 511 and its platform terms apply; scenic source pages may have separate terms | MTQ / Données Québec CC BY 4.0; © OpenStreetMap contributors (ODbL); scenic operator and position source shown in the selected-camera panel |
| **World camera image subset** (2,230 locations) | `config/cctv_sources.world-image-cameras.json` | Mixed: ODOT TripCheck public images (ODOT permits use with credit) and MLIT site content under terms compatible with CC BY 4.0. Frames are not bundled or persisted; the server holds a bounded in-memory cache. | Per-source terms apply; NZTA Journey Planner web-cache rows are excluded; Vegagerðin rows are served by the Iceland pack instead | Oregon Department of Transportation (Camera courtesy of ODOT) and Japan Ministry of Land, Infrastructure, Transport and Tourism (出典:国土交通省 川の防災情報) |
| **TeleGeography Submarine Cable Map** (712 cables + 1,917 landing points) | `telegeography_submarine_cables/` | **CC BY-NC-SA 3.0** | ❌ **NonCommercial — remove for commercial use** | "© TeleGeography — submarinecablemap.com" | | **TeleGeography Submarine Cable Map** (712 cables + 1,917 landing points) | `telegeography_submarine_cables/` | **CC BY-NC-SA 3.0** | ❌ **NonCommercial — remove for commercial use** | "© TeleGeography — submarinecablemap.com" |
| **UCS Satellite Database** UCS-Satellite-Database 5-1-2023 (3,564 records; operator, owner country, purpose) | `ucs_satellites/` | **Free, unrestricted use** (UCS database manual, section 3) | ✅ (acknowledgement requested) | "UCS Satellite Database (UCS-Satellite-Database 5-1-2023), https://www.ucsusa.org/satellite_database" |
| **Imaging sensor pack** (46 Earth-observation platforms: swath, resolution) | `imaging_sensors/` | Individual specification facts from operator and agency pages, cited per entry | ✅ (facts only) | "Imaging sensor swaths: operator and agency specifications" |
| **Natural Earth physical regions** (1,046 land + 292 marine named polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) | | **Natural Earth physical regions** (1,046 land + 292 marine named polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **DataSF Analysis Neighborhoods** (41 SF neighborhood polygons) | `neighborhoods/` | **PDDL 1.0** (public domain) | ✅ (no restrictions) | "City & County of San Francisco — DataSF" (courtesy — not legally required) | | **DataSF Analysis Neighborhoods** (41 SF neighborhood polygons) | `neighborhoods/` | **PDDL 1.0** (public domain) | ✅ (no restrictions) | "City & County of San Francisco — DataSF" (courtesy — not legally required) |
| **Natural Earth countries, states and provinces** (260 admin-0/map units + 4,587 admin-1 polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) | | **Natural Earth countries, states and provinces** (260 admin-0/map units + 4,587 admin-1 polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **Natural Earth disputed areas** (11 of the source's 99 features, hand-picked; [list](src/data/local_data/natural_earth/README.md#disputed-areas-disputed_areasjson)) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **Natural Earth populated places** (7,342 points: name, position, rank) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **US Census Bureau counties** (3,235 county polygons) | `us_census_counties/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) | | **US Census Bureau counties** (3,235 county polygons) | `us_census_counties/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **US Census Bureau places** (32,629 city, town and CDP polygons) | `us_census_places/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) | | **US Census Bureau places** (32,629 city, town and CDP polygons) | `us_census_places/` | **Public domain** | ✅ (no restrictions) | "U.S. Census Bureau" (courtesy — not legally required) |
| **Who's On First neighborhoods** (61,441 polygons) | `wof_neighborhoods/` | Per source: CC BY, ODC-By, Open Government licences, CC0, PDDL, public domain (folder `ATTRIBUTION.md`) | ✅ (attribution) | "Neighborhood boundaries: Who's On First and contributors"; per-source credits in the folder | | **Who's On First neighborhoods** (61,441 polygons) | `wof_neighborhoods/` | Per source: CC BY, ODC-By, Open Government licences, CC0, PDDL, public domain (folder `ATTRIBUTION.md`) | ✅ (attribution) | "Neighborhood boundaries: Who's On First and contributors"; per-source credits in the folder |
@@ -397,11 +334,20 @@ Bundled OSM-derived data is distributed under [ODbL 1.0](https://opendatacommons
> We acknowledge the use of data and/or imagery from NASA's Fire Information for Resource Management System (FIRMS) (https://earthdata.nasa.gov/firms), part of NASA's Earth Observing System Data and Information System (EOSDIS). > We acknowledge the use of data and/or imagery from NASA's Fire Information for Resource Management System (FIRMS) (https://earthdata.nasa.gov/firms), part of NASA's Earth Observing System Data and Information System (EOSDIS).
FIRMS active fires are **fetched live at runtime** (CC0 / U.S. public domain data): the FIRMS active fires are **fetched live at runtime** (CC0 / U.S. public domain data): the
`/api/firms` server-side proxy merges the three VIIRS NRT sources (NOAA-20, NOAA-21, `/api/firms` server-side proxy merges the two VIIRS NRT sources (NOAA-20, NOAA-21)
Suomi-NPP) and MODIS NRT (Terra + Aqua), clamped to the trailing 24 h, cached 30 min to respect the shared MAP_KEY and MODIS NRT (Terra + Aqua), clamped to the trailing 24 h, cached 30 min to respect the shared MAP_KEY
transaction quota. Requires a free `FIRMS_MAP_KEY` transaction quota. Requires a free `FIRMS_MAP_KEY`
(https://firms.modaps.eosdis.nasa.gov/api/map_key/); the layer is empty without it. (https://firms.modaps.eosdis.nasa.gov/api/map_key/); the layer is empty without it.
The former bundled 2026-05-25 snapshot was removed 2026-07-16. The former bundled 2026-05-25 snapshot was removed 2026-07-16. Suomi NPP (`VIIRS_SNPP_NRT`)
is not requested: NASA stopped delivering it on 2026-11-02.
### UCS Satellite Database (`ucs_satellites/`)
Operator, owner country and purpose for active satellites, keyed by NORAD number, from the Union of Concerned Scientists Satellite Database release **UCS-Satellite-Database 5-1-2023** (data current through 2023-05-01; https://www.ucsusa.org/satellite_database). The satellites layer joins it to CelesTrak positions and SATCAT records to file each satellite as a station, imaging, telecom, Starlink, probe or debris object, and to name its operator and owner. Section 3 of the UCS database manual: "The Database is free and its use is unrestricted. We request that its use be acknowledged and referenced in written materials. References should include the version of the Database that was used, which is indicated by the name of the Excel file, and a link to or URL for the webpage www.ucsusa.org/satellite_database." The app acknowledges it in the Data attribution popover. UCS has paused updates since this release; Starlink rows are dropped (Starlink is classed by name). Build: `scripts/build-ucs-satellites.py`; curation in the folder README.
### Imaging sensor pack (`imaging_sensors/`)
Swath widths and ground resolutions for 46 Earth-observation platforms (Landsat, Sentinel, MODIS, VIIRS, GOES, Himawari, Gaofen, CBERS, RADARSAT, TerraSAR-X, COSMO-SkyMed, ALOS, KOMPSAT, SPOT, Pléiades, WorldView, SkySat, PlanetScope, ICEYE, Capella, Umbra and others), compiled 2026-11-04 from the operator and agency pages cited per entry. The orbital view draws each satellite's sensor swath to scale and caps its zoom at that swath. Facts only; no text or images copied. A swath is confirmed on an operator, agency (ESA's eoPortal included) or academic page; resellers are not cited. The CEOS MIM Database (CC BY 4.0) is not yet used. Sources and unverified values: the folder README.
### Natural Earth physical regions (`natural_earth/`) ### Natural Earth physical regions (`natural_earth/`)
@@ -422,6 +368,16 @@ https://www.naturalearthdata.com/about/terms-of-use/). We credit anyway: "Made w
Earth". Registration in the in-app `dataCredits.js` attribution list ships with the resolver Earth". Registration in the in-app `dataCredits.js` attribution list ships with the resolver
wiring (see below). wiring (see below).
### Natural Earth populated places (`natural_earth/populated_places.json`)
Natural Earth 10m populated places (`ne_10m_populated_places_simple`, the same pinned
`nvkelso/natural-earth-vector` commit and SHA-256 as the admin-1 build reads), reduced to one
row per place: name, latitude and longitude rounded to 3 decimals, `scalerank` and `rank_max`
(7,342 places, 229 KB). From 8x the orbital view names the most prominent place within half
its width ("Sichuan, China · near Chengdu"), offline, with the states and provinces pack giving
the division. Public domain; credited "Made with Natural Earth" with the other packs. Rebuild:
`node scripts/build-admin-packs.mjs --only places`; provenance in the folder README.
### Sky background (`sky/`) ### Sky background (`sky/`)
Sources, licences, the changes CC BY asks to state, hashes and build commands Sources, licences, the changes CC BY asks to state, hashes and build commands
@@ -592,4 +548,4 @@ NOAA density imagery with the raw GLM product.
## In-app attribution ## In-app attribution
The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, adsbdb, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app. The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, adsbdb, Planespotters.net, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app.
+22 -15
View File
@@ -212,7 +212,7 @@ Keep the `env`: bash treats `UID` as read-only, so a plain `UID=1026` in front o
On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`. On SELinux hosts (Fedora, RHEL), compose mounts the checkout with `:z` so the container may read it; Docker may relabel the checkout for container access, which can take a moment on a large checkout. Without that, the start fails with `Cannot find module '/app/scripts/docker-start.mjs'`.
Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings refuses proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)). The map routes compare the browser's `Origin` with the `Host` the server sees, so the proxy must pass the original `Host` and, when it terminates TLS, `X-Forwarded-Proto`. Caddy does both by default; for nginx add `proxy_set_header Host $http_host;`, `proxy_set_header X-Forwarded-Proto $scheme;` and `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`. On an install outside Docker, that forwarding header also rate-limits the proxy's users apart from your own browser. Under the default Docker bridge network every client on the host, your browser included, reaches the container from its gateway address and shares one Overpass bucket. Behind a reverse proxy, put your keys in `.env` or pass them as environment variables - Provider Settings and the News layer refuse proxied requests on purpose - and add the hostname you serve it on, for example `GEV_ALLOWED_HOSTS=globe.example.lan` (see [Sharing an instance](#-sharing-an-instance)). The map routes compare the browser's `Origin` with the `Host` the server sees, so the proxy must pass the original `Host` and, when it terminates TLS, `X-Forwarded-Proto`. Caddy does both by default; for nginx add `proxy_set_header Host $http_host;`, `proxy_set_header X-Forwarded-Proto $scheme;` and `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`. On an install outside Docker, that forwarding header also rate-limits the proxy's users apart from your own browser. Under the default Docker bridge network every client on the host, your browser included, reaches the container from its gateway address and shares one Overpass bucket.
</details> </details>
@@ -236,8 +236,8 @@ the container.
### Path 4 — GitHub Codespaces (cloud, no local install) ### Path 4 — GitHub Codespaces (cloud, no local install)
Codespaces requires a GitHub account. Overpass queries and OpenAI voice token Codespaces requires a GitHub account. Overpass queries, the News layer and OpenAI
requests return HTTP 403 through its forwarded HTTPS URL, even with API keys voice token requests return HTTP 403 through its forwarded HTTPS URL, even with API keys
in `.env` and `GEV_ALLOWED_HOSTS` configured. Use a local setup for these features. in `.env` and `GEV_ALLOWED_HOSTS` configured. Use a local setup for these features.
The codespace runs the dev server in a cloud workspace, so Node and the dependencies The codespace runs the dev server in a cloud workspace, so Node and the dependencies
@@ -340,7 +340,9 @@ Choose a first-run mission, or try these in order. The GIFs show Google Photorea
7. **Talk to it** _(needs an OpenAI key)_: _"Take me to LAX and select the nearest airborne aircraft."_ 7. **Talk to it** _(needs an OpenAI key)_: _"Take me to LAX and select the nearest airborne aircraft."_
8. **Come home.** Hit **Reset Globe** — or just say _"zoom out to a globe view."_ 8. **Come home.** Hit **Reset Globe** — or just say _"zoom out to a globe view."_
**Keyboard:** `1`–`6` visual styles · `H` HUD · `D` detection · `C` cockpit · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe. Open **Visual Presets → Globe Engine** to switch between Cesium and experimental Navara. The globe switches in place at the current view and remembers your choice. A link with `?engine=cesium` or `?engine=navara` overrides the saved choice. A fresh visit starts with Cesium.
**Keyboard:** `1`–`6` visual styles · `H` HUD · `D` detection · `C` CCTV · `` ` `` frame rate · `Esc` out. Trackpad pinch zooms the globe.
--- ---
@@ -426,7 +428,7 @@ _Ask for radio near anywhere and the globe starts broadcasting — every station
## 🛰️ What's on the Globe ## 🛰️ What's on the Globe
Twenty-two layers and map sources. **Twenty have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.) Twenty-three layers and map sources. **Twenty-one have a keyless path.** Some offer additional capabilities with a provider key. (🟢 no key · 🟡 free key · 🔴 metered.)
| Layer | What you get | Source | Auth | | Layer | What you get | Source | Auth |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
@@ -436,15 +438,16 @@ Twenty-two layers and map sources. **Twenty have a keyless path.** Some offer ad
| 📡 **GNSS Integrity** | Regional navigation accuracy scores; anomalies do not confirm interference | adsb.lol, GPSJAM aggregation | 🟢 | | 📡 **GNSS Integrity** | Regional navigation accuracy scores; anomalies do not confirm interference | adsb.lol, GPSJAM aggregation | 🟢 |
| 🔥 **Fire Perimeters** | Incident boundaries and reported containment | WFIGS / NIFC | 🟢 | | 🔥 **Fire Perimeters** | Incident boundaries and reported containment | WFIGS / NIFC | 🟢 |
| 🚢 **Live Vessels** | Thousands of ships worldwide | AISStream | 🟡 | | 🚢 **Live Vessels** | Thousands of ships worldwide | AISStream | 🟡 |
| 🛰️ **Satellites** | 838-object catalog, color-coded by class with a live legend — the **DENSE** chip drops in the whole Starlink shell | CelesTrak | 🟢 | | 🛰️ **Satellites** | About 1,300 objects in six classes (Stations, Imaging, Telecom, Starlink, Probes, Debris) with a toggle and a live count each; Starlink loads its whole shell | CelesTrak | 🟢 |
| 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 |
| 📰 **News** | The last 24 hours of news in the right-rail **NEWS** panel, narrowed to 12, 6, 3 or 1 hours, with picture cards and clips. **Conflicts**: physical violence worldwide from GDELT's event files, one report per story at GDELT's place for it, plus Liveuamap's mapped items with a key. **Local**: the news of the country at the view centre, its outline and name on the globe; a disputed area of the bundled list, or a territory without a country code, is named as itself. Places and categories are machine-coded and can be wrong | GDELT · Liveuamap · World News API, or your own news server | 🟢 Conflicts · 🟡 Local (World News API) · 🔴 Liveuamap items |
| ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 |
| 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds |
| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed), opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent) and opt-in Houston (TranStar, enable only with TxDOT's written permission per TranStar's Content Usage terms). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📹 **CCTV Mesh** | up to 7,350 camera locations (4,000 shared round-robin slots, plus room for 1,120 Québec and 2,230 World image cameras) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Ottawa (City of Ottawa, free access certificate) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Iceland (Vegagerðin) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Québec (MTQ locations with Québec 511 links, Montréal locations, regional webcams) · Oregon (TripCheck) · Japan (MLIT river cameras) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed), opt-in Québec 511 in-browser video (embedding terms unconfirmed), opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent) and opt-in Houston (TranStar, enable only with TxDOT's written permission per TranStar's Content Usage terms). Positions are published or attributed; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
| 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 |
| 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key |
| 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 |
| 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, Rio de Janeiro (buses and BRT), and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 | | 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, Rio de Janeiro (buses and BRT), Szczecin, Lisbon (Carris Metropolitana and the city’s Carris), Prague, Warsaw, Kraków, Poznań, Gdańsk, Rzeszów, Vilnius, Bucharest, Cluj-Napoca, Iași, Rome, Turin, Venice (buses and waterbuses), Bordeaux, Montpellier, Rouen, Toulon, Le Havre, Tours, Málaga, Sendai, Johor Bahru, Kota Bharu, Adelaide, Atlanta, Denver, Edmonton, Halifax, Victoria, and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 |
| 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 | | 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 |
| 🧭 **Directions** | Click A and B on the globe for a street-following drive, walk or cycle route draped on the terrain with turn-by-turn steps — then FLY the camera along it. No key, no geocoder, no mic | OSRM on FOSSGIS servers (OpenStreetMap) | 🟢 | | 🧭 **Directions** | Click A and B on the globe for a street-following drive, walk or cycle route draped on the terrain with turn-by-turn steps — then FLY the camera along it. No key, no geocoder, no mic | OSRM on FOSSGIS servers (OpenStreetMap) | 🟢 |
| ⌖ **OSM Places** | Voice place search ("hospitals in Kathmandu") from OpenStreetMap, drawn as numbered points that follow-up questions can filter. Needs your own Overpass server (`OVERPASS_UPSTREAMS`); off otherwise | | ⌖ **OSM Places** | Voice place search ("hospitals in Kathmandu") from OpenStreetMap, drawn as numbered points that follow-up questions can filter. Needs your own Overpass server (`OVERPASS_UPSTREAMS`); off otherwise |
@@ -485,9 +488,9 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
> Street-level photos from any provider in one panel. This build ships **Mapillary** (a free client token), **Google Street View** (your Google Maps key), **KartaView** and **Panoramax** (no key). > Street-level photos from any provider in one panel. This build ships **Mapillary** (a free client token), **Google Street View** (your Google Maps key), **KartaView** and **Panoramax** (no key).
**Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it the panel reads **KEY REQUIRED**. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless. **Setup.** At [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), register an application with **Read** access and copy its **Client Token** (`MLY|…`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart. Without it Street Level starts on **KartaView** instead, and the Mapillary chip says which key to add. **Google Street View** uses `GOOGLE_MAPS_API_KEY` with the **Maps JavaScript API** enabled for it in Google Cloud; its chip starts off, since every panorama is billed to the key. **KartaView** and **Panoramax** need nothing: their public APIs are keyless.
**Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo. **Use.** Turn on **Street Level** under DATA LAYERS → Cameras, or with the pill in the STREET LEVEL panel header. Each provider has a chip in its own colour (Mapillary green), and one is lit at a time: lighting one switches the others off. Mapillary, which is free, is lit by default when its token is set; otherwise KartaView is. Zoom in to street level (below 60 km) for the coverage lines; from orbit the panel says to zoom in. The **IMAGERY** (ALL / 360° / FLAT) and **SINCE** filters apply to Mapillary, KartaView and Panoramax; Street View panoramas are all 360°, so with Street View lit the filters are hidden. Click a coverage line for its image cones and a cone to open the photo. Light **KARTAVIEW** (purple) and zoom in below about 6 km: its lines are the streets KartaView has photos of, and a click on one opens the photo nearest the click, flat or 360° (drag to look around), with cones along its drive; **‹ ›** or ← → step through the drive (Shift+← →, ↑ ↓ and + − look around a 360° photo; pinch zooms on a phone). Photos KartaView has archived are skipped, and where all are archived the panel says so. For KartaView, **SINCE** goes by upload date, as KartaView's own map does (it can filter by nothing else), and the panel notes it beside the SINCE value (ANY DATE (by upload date)); KartaView filters its lines on its own server, and a filtered area KartaView has not drawn before can take several seconds. Light **PANORAMAX** (orange) for the open, federated commons (strongest in France and Belgium): below about 60 km its lines are sequences. A click on a line opens that sequence's picture nearest the click without moving the camera, flat or 360°, with cones along the sequence around it; **‹ ›** or ← → step through it, and both filters apply. Its caption names the picture's authors (or the instance holding it) and licence (mostly CC BY-SA 4.0; pictures on the IGN instance are Licence Ouverte 2.0). On the **Google 3D** map (Google's terms keep Street View off other maps, where it does not appear), light **GOOGLE STREET VIEW** and click a street from below 3 km, or press **OPEN STREET VIEW** for the view centre, to open Google's panorama there; step along its arrows. Above the photo: **EXPAND** (full screen; Esc leaves) and **FIT / FILL** (Mapillary, and flat KartaView and Panoramax photos). The caption links to the image on the provider's site, and the globe credits each provider that is on ("© Mapillary contributors, CC BY-SA 4.0", "Street View imagery © Google", "© KartaView contributors, CC BY-SA 4.0", "© Panoramax contributors, licence per picture"). KartaView's own photo pages fail for signed-out visitors, so its link opens the full-size photo.
Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`. Share links carry the provider switches and filters (`0.m.0` Mapillary off, `0.m.0_0.r.google-1` Street View on, `0.m.0_0.r.google-0*kartaview-1*panoramax-0` KartaView on and `0.m.0_0.r.google-0*kartaview-0*panoramax-1` Panoramax on, as the app writes them (`0.m.0_0.r.kartaview-1` and `0.m.0_0.r.panoramax-1` work too; a link that leaves out `m` keeps Mapillary, the default, lit), `0.p.p` panoramas only, `0.s.365` the last year). To add a provider, implement the contract in `src/layers/streetLevel/registry.js` and register it in `src/app/layers/streetLevel.js`.
@@ -567,12 +570,14 @@ and configuration details.
No key needed for Wind, Rain radar, Satellite clouds, Lightning density, No key needed for Wind, Rain radar, Satellite clouds, Lightning density,
Cyclone advisories, Mapped ALPR Cameras, Transit, Directions, Delaware live Cyclone advisories, Mapped ALPR Cameras, Transit, Directions, Delaware live
video, and coordinate or bundled-place search. A layer row waiting on a key video, the News layer's Conflicts section, and coordinate or bundled-place
search. A layer row waiting on a key
names it. names it.
### Choose the capabilities you want ### Choose the capabilities you want
Seven keys. Five have a free tier, and the two 🔴 ones are metered: Nine keys. Six have a free tier. Of the three 🔴 ones, Google Maps and OpenAI are
metered and Liveuamap is a paid subscription:
| | Key | Why | Get it | | | Key | Why | Get it |
| --- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -583,6 +588,8 @@ Seven keys. Five have a free tier, and the two 🔴 ones are metered:
| 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free | | 🟡 | **NASA FIRMS** | 🔥 Live active fires | [firms.modaps.eosdis.nasa.gov](https://firms.modaps.eosdis.nasa.gov/api/map_key/) — free |
| 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [my.tomtom.com](https://my.tomtom.com/keys) — free tier available | | 🟡 | **TomTom** | 🚦 Live flow speeds and congestion colors for the simulated traffic layer | [my.tomtom.com](https://my.tomtom.com/keys) — free tier available |
| 🟡 | **Mapillary** | 📷 Street Level coverage and photos. A public client token by design (see SECURITY.md) | [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers) — register an app, copy its Client Token | | 🟡 | **Mapillary** | 📷 Street Level coverage and photos. A public client token by design (see SECURITY.md) | [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers) — register an app, copy its Client Token |
| 🟡 | **World News API** | 📰 News layer, Local: the news of the country in view. The free plan allows 50 points a day; `WORLDNEWSAPI_DAILY_READS` (default 30) keeps Core inside it. Core keeps each country's news for up to 1 hour, and World News API's [terms](https://worldnewsapi.com/terms/) allow caching only with its prior written permission, which is yours to get. The Data attribution popover credits it with a link | [worldnewsapi.com/console](https://worldnewsapi.com/console/) — free plan |
| 🔴 | **Liveuamap** | 📰 News layer, Conflicts: Liveuamap's mapped items beside GDELT's reports (🟢 Conflicts works without) | [liveuamap.com/promo/api](https://liveuamap.com/promo/api) — paid API |
![Diving from city-scale live congestion straight into an intersection's public camera](docs/media/05-traffic-to-cctv.gif) ![Diving from city-scale live congestion straight into an intersection's public camera](docs/media/05-traffic-to-cctv.gif)
@@ -639,7 +646,7 @@ and panel. DelDOT uses its official HTTPS HLS catalog links. Disable that
pack with `CCTV_DELDOT_ENABLED=0`. pack with `CCTV_DELDOT_ENABLED=0`.
The server allows two concurrent sessions. Each retains at most 12 segments The server allows two concurrent sessions. Each retains at most 12 segments
and 24 MiB in memory; individual downloads are capped at 4 MiB with a ten and 24 MiB in memory; individual downloads are capped at 8 MiB with a ten
second deadline. There are no segment files or ffmpeg processes. Redirects, second deadline. There are no segment files or ffmpeg processes. Redirects,
off-origin references, encrypted playlists and non-MPEG-TS segments are refused. off-origin references, encrypted playlists and non-MPEG-TS segments are refused.
Each decoder has its own client lease (at most eight per session), including Each decoder has its own client lease (at most eight per session), including
@@ -656,8 +663,8 @@ Honest numbers, roughly, as of mid-2026 — always check the provider pricing pa
| | Cost reality | | | Cost reality |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **🟢 Most layers** | **$0, no signup.** OpenSky anon, USGS, CelesTrak, adsb.lol, city CCTV, Radio Browser, GBFS, Launch Library 2, transit feeds, OSRM routing, NOAA and ECMWF weather, OpenStreetMap ALPR mapping, bundled datasets. | | **🟢 Most layers** | **$0, no signup.** OpenSky anon, USGS, CelesTrak, adsb.lol, city CCTV, Radio Browser, GBFS, Launch Library 2, transit feeds, OSRM routing, NOAA and ECMWF weather, OpenStreetMap ALPR mapping, GDELT news events, bundled datasets. |
| **🟡 The free-key tier** | **$0 with a signup.** AISStream, FIRMS, TomTom, OpenSky, plus Cesium ion for eligible personal/non-commercial use. Provider quotas and eligibility still apply. | | **🟡 The free-key tier** | **$0 with a signup.** AISStream, FIRMS, TomTom, OpenSky, World News API, plus Cesium ion for eligible personal/non-commercial use. Provider quotas and eligibility still apply. |
| **🗺️ Google 3D tiles** | **Free through an eligible Cesium ion Community account within its quota; metered through a direct Google key.** Use the direct route for Google place search or commercial deployment, verify current provider terms, and set budget alerts where billing is enabled. | | **🗺️ Google 3D tiles** | **Free through an eligible Cesium ion Community account within its quota; metered through a direct Google key.** Use the direct route for Google place search or commercial deployment, verify current provider terms, and set budget alerts where billing is enabled. |
| **🔴 OpenAI voice** | API-key sessions keep the live spend readout, STD/MINI model toggle, $2 warning, and **$5 hard cap**. ChatGPT OAuth is an explicit choice in Provider Settings. Its usage is tracked without an API-dollar estimate or spend cap. The voice context window stays deliberately short. | | **🔴 OpenAI voice** | API-key sessions keep the live spend readout, STD/MINI model toggle, $2 warning, and **$5 hard cap**. ChatGPT OAuth is an explicit choice in Provider Settings. Its usage is tracked without an API-dollar estimate or spend cap. The voice context window stays deliberately short. |
+4 -4
View File
@@ -69,20 +69,20 @@ browser-key fallback for existing single-key setups.
The data proxies under `server/providers/` are written so the browser cannot turn the server into an open relay: The data proxies under `server/providers/` are written so the browser cannot turn the server into an open relay:
- **No arbitrary-URL fetching.** The CCTV frame proxy fetches only server-registered camera/frame URLs — clients cannot pass an upstream URL to fetch (SSRF mitigation). Other proxies target fixed upstream hosts. - **No arbitrary-URL fetching.** The CCTV frame proxy fetches only server-registered camera/frame URLs — clients cannot pass an upstream URL to fetch (SSRF mitigation). Other proxies target fixed upstream hosts.
- **Live HLS is bounded and origin-pinned.** Registered HTTP(S) HLS sources use a Node puller that rejects redirects and off-origin playlist references, bounds playlists and segment bodies while streaming, and aborts timed-out or released sessions. At most two sessions retain 12 segments/24 MiB each, plus bounded download buffers. Stores are memory-only and idle sessions expire after 15 seconds. RTMP/ffmpeg execution is not enabled. Locally configured source URLs remain an operator trust boundary. - **Live HLS is bounded and origin-pinned.** Registered HTTP(S) HLS sources use a Node puller that rejects redirects and off-origin playlist references, bounds playlists to 256 KiB and segment bodies to 8 MiB while streaming, and aborts timed-out or released sessions. At most two sessions retain 12 segments/24 MiB each, plus bounded download buffers. Stores are memory-only and idle sessions expire after 15 seconds. RTMP/ffmpeg execution is not enabled. Locally configured source URLs remain an operator trust boundary.
- **Radio is not an audio relay.** `/api/radio/stations` contacts only allowlisted Radio Browser HTTPS hosts and paths, rejects redirects, rejects any hostname with a loopback/private/link-local/metadata/non-public A or AAAA result, and pins each TLS connection to a validated address. It returns normalized public HTTPS stream URLs; `/api/radio/click/:uuid` applies the same destination policy and accepts only station IDs from the current bounded catalog. The browser then connects directly to the broadcaster after an explicit playback action, so the broadcaster sees the listener's IP address. GEV never proxies, caches, records, or redistributes audio. - **Radio is not an audio relay.** `/api/radio/stations` contacts only allowlisted Radio Browser HTTPS hosts and paths, rejects redirects, rejects any hostname with a loopback/private/link-local/metadata/non-public A or AAAA result, and pins each TLS connection to a validated address. It returns normalized public HTTPS stream URLs; `/api/radio/click/:uuid` applies the same destination policy and accepts only station IDs from the current bounded catalog. The browser then connects directly to the broadcaster after an explicit playback action, so the broadcaster sees the listener's IP address. GEV never proxies, caches, records, or redistributes audio.
- **Québec 511 video plays in the browser, opt-in and after Play.** With `CCTV_QUEBEC_VIDEO_ENABLED=1`, `/api/cctv/sources` lists, for each MTQ camera, one URL built server-side from its numeric id on `https://www.quebec511.info/Carte/Fenetres/camera.ashx`; the overlay accepts no other host, path or format. The browser loads that clip only after the viewer presses Play for the selected camera, so Québec 511 sees the viewer's IP address, and the panel says so before Play. Selecting a camera by click, auto-hop, a scene or voice never loads it, and switching cameras stops playback. GEV never proxies, caches or reads the video's pixels. With the flag unset (the default) the browser makes no request to Québec 511.
- **No verbatim client headers upstream.** The CCTV media route is the one route that relays a request header (`Range`, for video seeking). It is parsed and canonicalized before it is forwarded: one `bytes=` range only, with every accepted form — explicit span, open-ended and suffix — bounded to the same 64 MiB ceiling the relay applies to a declared response body. Multi-range, malformed, inverted and non-`bytes` values are dropped and the request proceeds without a `Range`, as RFC 7233 §3.1 prescribes. Bounding the request bounds what is asked for: a response that declares no length — live streamed media, or a chunked body from an upstream that ignores the `Range` — has no ceiling. An upstream request the browser has stopped waiting for is cancelled rather than left running, whether the viewer leaves before the headers arrive or during the body. - **No verbatim client headers upstream.** The CCTV media route is the one route that relays a request header (`Range`, for video seeking). It is parsed and canonicalized before it is forwarded: one `bytes=` range only, with every accepted form — explicit span, open-ended and suffix — bounded to the same 64 MiB ceiling the relay applies to a declared response body. Multi-range, malformed, inverted and non-`bytes` values are dropped and the request proceeds without a `Range`, as RFC 7233 §3.1 prescribes. Bounding the request bounds what is asked for: a response that declares no length — live streamed media, or a chunked body from an upstream that ignores the `Range` — has no ceiling. An upstream request the browser has stopped waiting for is cancelled rather than left running, whether the viewer leaves before the headers arrive or during the body.
- **The Data Library proxy fetches manifest upstreams only.** `/api/library/data/<id>`, `/api/library/tile/<id>/<z>/<x>/<y>` and `/api/library/index/<id>` (a partitioned GeoParquet entry's file index, built from an S3 ListObjectsV2 listing and one ranged footer read per file, cached in memory for the entry's `cacheSeconds`) resolve the id against the validated manifests in `src/library/entries/`; the browser never supplies a URL, and every derived request must stay on the manifest's https host. Redirects are refused, dataset bodies are capped while streaming at the manifest's `maxBytes` (at most 32 MiB) and tiles at 2 MiB of `image/png`, `image/jpeg` or `image/webp`, responses are cached in memory only (at most 512 tiles), requests are rate-limited per client, and upstream error text is never returned. A keyed entry must be proxied; its key is read from this server's environment through the key registry and its routes refuse cross-site and forwarded requests like the other cost-bearing routes. Browser-direct entries are fetched by the browser with no cookies or referrer.
- **Transit fetches registered feeds only.** `/api/transit/vehicles/<id>` resolves the id against `src/data/transitFeeds.js`; the browser never supplies a URL, and a feed that is registered but disabled does not resolve at all. Redirects are followed manually and each hop is validated against the feed's own https origin before it is requested, so an off-origin or downgraded hop is refused rather than contacted. Bodies are capped at 8 MB, the protobuf is decoded server-side under entity-count and string-length ceilings, a differential feed is refused, and snapshots live 15 s in memory with no disk cache. A per-feed admission limiter and a failure cooldown ladder bound what this process can ask of any operator. - **Transit fetches registered feeds only.** `/api/transit/vehicles/<id>` resolves the id against `src/data/transitFeeds.js`; the browser never supplies a URL, and a feed that is registered but disabled does not resolve at all. Redirects are followed manually and each hop is validated against the feed's own https origin before it is requested, so an off-origin or downgraded hop is refused rather than contacted. Bodies are capped at 8 MB, the protobuf is decoded server-side under entity-count and string-length ceilings, a differential feed is refused, and snapshots live 15 s in memory with no disk cache. A per-feed admission limiter and a failure cooldown ladder bound what this process can ask of any operator.
- **Local receiver feeds are operator-configured and local-only.** `/api/local-receivers/aircraft` reads only the `aircraft.json` URLs in the server's `LOCAL_RECEIVER_FEEDS`; the browser never supplies an address. Each host must pass the shared tap address rule (`src/data/tapAddress.js`: loopback, RFC1918, `localhost`, `*.local`; no link-local, IPv6 or other names), the scheme must be http(s), the path must end in `aircraft.json`, and credentials, queries and fragments are refused. Invalid entries are logged at startup and never fetched. Reads use a 2 s timeout, refuse redirects, cap bodies at 2 MB and share one read per second; responses name each feed by band only and never carry upstream error text. - **Local receiver feeds are operator-configured and local-only.** `/api/local-receivers/aircraft` reads only the `aircraft.json` URLs in the server's `LOCAL_RECEIVER_FEEDS`; the browser never supplies an address. Each host must pass the shared tap address rule (`src/data/tapAddress.js`: loopback, RFC1918, `localhost`, `*.local`; no link-local, IPv6 or other names), the scheme must be http(s), the path must end in `aircraft.json`, and credentials, queries and fragments are refused. Invalid entries are logged at startup and never fetched. Reads use a 2 s timeout, refuse redirects, cap bodies at 2 MB and share one read per second; responses name each feed by band only and never carry upstream error text.
- **Voice place search runs only on the operator's Overpass.** `/api/osm/features` answers only when `OVERPASS_UPSTREAMS` is set and never falls back to a public instance. The client sends a curated preset id, a box, a mode and a limit; the server writes and quotes the Overpass query itself, so no client or model text reaches it. Boxes are capped (36 deg² for a count, 4 deg² for a listing), as are the feature limit, the Overpass timeout and memory, and the response bytes read. Each client (IPv6 clients keyed by /64, as on `/api/overpass`) runs one search at a time and two run at once in all, under a per-client rate limit (12/min) and a global one (30/min); a search gets 22 s across every configured endpoint in turn (Overpass itself is told 20 s), which ends before the voice answer stops waiting at 25 s. The route sits behind the cross-site gate below. Answers are cached in memory and on disk (`.gev-cache/osm-features/`, at most 400 entries and 256 MB, kept a day), keyed to the configured instance so a repointed `OVERPASS_UPSTREAMS` never serves the old one's answers; a malformed answer is never cached. - **Voice place search runs only on the operator's Overpass.** `/api/osm/features` answers only when `OVERPASS_UPSTREAMS` is set and never falls back to a public instance. The client sends a curated preset id, a box, a mode and a limit; the server writes and quotes the Overpass query itself, so no client or model text reaches it. Boxes are capped (36 deg² for a count, 4 deg² for a listing), as are the feature limit, the Overpass timeout and memory, and the response bytes read. Each client (IPv6 clients keyed by /64, as on `/api/overpass`) runs one search at a time and two run at once in all, under a per-client rate limit (12/min) and a global one (30/min); a search gets 22 s across every configured endpoint in turn (Overpass itself is told 20 s), which ends before the voice answer stops waiting at 25 s. The route sits behind the cross-site gate below. Answers are cached in memory and on disk (`.gev-cache/osm-features/`, at most 400 entries and 256 MB, kept a day), keyed to the configured instance so a repointed `OVERPASS_UPSTREAMS` never serves the old one's answers; a malformed answer is never cached.
- **Validated response formats.** Proxied bodies are relayed only in the format the route serves. CCTV frames and the Street View fallback accept JPEG, PNG, GIF, WebP and AVIF only, sent under their canonical type; any other type falls through to the next fallback. Live CCTV media must match its feed kind (`video/mp4`, `video/webm`, or `multipart/x-mixed-replace` with a boundary) before it is relayed or the camera counts as healthy, and its redirects stay on the registered origin. GBFS bodies must parse as JSON and are re-serialized as `application/json; charset=utf-8`. Camera catalog URLs are pinned by parsed URL — scheme, exact host, no credentials, default port, and a normalized path inside the agency's image directory — and a redirect on a pinned host must stay inside the same directory. Every `/api/cctv` route uses the keyless cross-site checks described below, so cameras stay reachable behind a reverse proxy; a request carrying forwarding headers never reaches the key-bearing Street View fallback and gets the placeholder frame instead, as when no key is configured. - **Validated response formats.** Proxied bodies are relayed only in the format the route serves. CCTV frames and the Street View fallback accept JPEG, PNG, GIF, WebP and AVIF only, sent under their canonical type; any other type falls through to the next fallback. The one exception is a source registered as an MJPEG still (the Taiwan Highway Bureau feeds): its `multipart/x-mixed-replace` answer is read only up to its first JPEG part, capped at 2 MB, and sent as `image/jpeg`; a multipart answer from any other camera falls through. A camera whose API needs an operator credential (the City of Ottawa certificate, `OTTAWA_CAMERA_CERTIFICATE`) has a keyless catalog URL; the frame proxy adds the credential only at fetch time, only for a URL inside that camera's pinned path, so `/api/cctv/sources` and `/api/cctv/stream` never carry it. Live CCTV media must match its feed kind (`video/mp4`, `video/webm`, or `multipart/x-mixed-replace` with a boundary) before it is relayed or the camera counts as healthy, and its redirects stay on the registered origin. GBFS bodies must parse as JSON and are re-serialized as `application/json; charset=utf-8`. Camera catalog URLs are pinned by parsed URL — scheme, exact host, no credentials, default port, and a normalized path inside the agency's image directory — and a redirect on a pinned host must stay inside the same directory. Every `/api/cctv` route uses the keyless cross-site checks described below, so cameras stay reachable behind a reverse proxy; a request carrying forwarding headers never reaches the key-bearing Street View fallback and gets the placeholder frame instead, as when no key is configured.
- **Consistent API response headers.** Every `/api` response, including errors, partial content, Host refusals and unknown routes, carries `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff` (`apiResponseHeadersPlugin` in `build/vite.js`, installed ahead of the Host check). A route that sets its own policy adds it as a further policy; it never replaces this one. - **Consistent API response headers.** Every `/api` response, including errors, partial content, Host refusals and unknown routes, carries `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff` (`apiResponseHeadersPlugin` in `build/vite.js`, installed ahead of the Host check). A route that sets its own policy adds it as a further policy; it never replaces this one.
- **Response-size caps and timeouts** on proxied responses. - **Response-size caps and timeouts** on proxied responses.
- **Sanitized errors** — internal error details are not echoed back to clients. - **Sanitized errors** — internal error details are not echoed back to clients.
- **Coalesced OAuth refresh** and cached successful responses only (OpenSky). - **Coalesced OAuth refresh** and cached successful responses only (OpenSky).
- **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing. - **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing.
- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles, and `/api/tomtom/*`, backed by its persistent daily tile budget) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an `<img>`/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. The keyless map routes (`/api/terrain/heights`, `/api/drone-airspace`, `/api/inaturalist/observations`, `/api/acartia/sightings`, `/api/overpass` and voice place search's `/api/osm/features`, which spends the same operator Overpass) use the same cross-site checks, minus the forwarding-header refusal: they guard only per-client quotas and caches, not a key or a bill, and the map and voice need them to work behind a reverse proxy. `/api/overpass` also admits at most 300 POSTs a minute per peer (IPv6 peers keyed by /64) and 900 a minute across all remote peers before it reads a body. A direct loopback request — a loopback socket with none of the forwarding headers `hasProxySignals` checks (`Forwarded`, `Via`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Real-IP`, `CF-Connecting-IP`, `CF-Ray`) — draws on its own per-peer admission window, so remote peers cannot exhaust the owner's admission. A reverse proxy on the same machine must add at least one of those headers (Caddy and Cloudflare do by default; nginx needs `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`, plus `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` so the `Origin` check matches), or its traffic is indistinguishable from the owner's and shares the owner's window. With one, its requests are then counted as remote, in one bucket per proxy socket — the forwarded client address is never trusted as identity — so all users behind one proxy share 300/min. The isolation needs a loopback socket: under the default Docker bridge network every host-side client, the owner included, arrives from the container gateway and is one remote peer. **Limit:** this isolates admission only. The upstream budget (90 requests/min per client key, 300/min across all clients) is shared, so once remote traffic has used the global upstream budget, an owner request that misses the cache can still get `429`, or `503` when remote misses hold every upstream concurrency slot; cache hits are unaffected. The CCTV routes (`/api/cctv/*`) use the same keyless checks; their one key-bearing step, the Street View fallback, is skipped for any request with forwarding headers. - **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles, `/api/tomtom/*`, backed by its persistent daily tile budget, and `/api/news/*`, which spends the World News API daily reads, Liveuamap reads and GDELT downloads) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an `<img>`/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. The keyless map routes (`/api/terrain/heights`, `/api/drone-airspace`, `/api/inaturalist/observations`, `/api/acartia/sightings`, `/api/overpass` and voice place search's `/api/osm/features`, which spends the same operator Overpass) use the same cross-site checks, minus the forwarding-header refusal: they guard only per-client quotas and caches, not a key or a bill, and the map and voice need them to work behind a reverse proxy. `/api/overpass` also admits at most 300 POSTs a minute per peer (IPv6 peers keyed by /64) and 900 a minute across all remote peers before it reads a body. A direct loopback request — a loopback socket with none of the forwarding headers `hasProxySignals` checks (`Forwarded`, `Via`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Real-IP`, `CF-Connecting-IP`, `CF-Ray`) — draws on its own per-peer admission window, so remote peers cannot exhaust the owner's admission. A reverse proxy on the same machine must add at least one of those headers (Caddy and Cloudflare do by default; nginx needs `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`, plus `proxy_set_header Host $http_host;` and `proxy_set_header X-Forwarded-Proto $scheme;` so the `Origin` check matches), or its traffic is indistinguishable from the owner's and shares the owner's window. With one, its requests are then counted as remote, in one bucket per proxy socket — the forwarded client address is never trusted as identity — so all users behind one proxy share 300/min. The isolation needs a loopback socket: under the default Docker bridge network every host-side client, the owner included, arrives from the container gateway and is one remote peer. **Limit:** this isolates admission only. The upstream budget (90 requests/min per client key, 300/min across all clients) is shared, so once remote traffic has used the global upstream budget, an owner request that misses the cache can still get `429`, or `503` when remote misses hold every upstream concurrency slot; cache hits are unaffected. The CCTV routes (`/api/cctv/*`) use the same keyless checks; their one key-bearing step, the Street View fallback, is skipped for any request with forwarding headers.
- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads and the Maps JavaScript API that Street View runs on, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `<style>` elements. - **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads and the Maps JavaScript API that Street View runs on, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `<style>` elements.
Allowing `maps.googleapis.com` in `script-src` also trusts its JSONP endpoints, which can bypass the script allowlist if an HTML-injection bug exists; this is an accepted limitation of loading the Maps JavaScript API, alongside the existing `'unsafe-eval'` allowance. Allowing `maps.googleapis.com` in `script-src` also trusts its JSONP endpoints, which can bypass the script allowlist if an HTML-injection bug exists; this is an accepted limitation of loading the Maps JavaScript API, alongside the existing `'unsafe-eval'` allowance.
+21 -16
View File
@@ -1,9 +1,9 @@
# Third-party software notices # Third-party software notices
The project depends on third-party npm packages whose own licenses apply. This The project depends on third-party npm packages whose own licenses apply. This
file records the packages added for the browser-local SDR feature, the file records the packages added for the browser-local SDR feature and the
Street Level viewer and the Data Library's GeoParquet reader; the complete resolved dependency inventory remains in Street Level viewer, and the CesiumJS code ported into `src/geo/`; the complete
`package-lock.json`. resolved dependency inventory remains in `package-lock.json`.
## Web RTL-SDR ## Web RTL-SDR
@@ -46,17 +46,22 @@ Street Level viewer and the Data Library's GeoParquet reader; the complete resol
- License: MIT - License: MIT
- License text: <https://github.com/mrdoob/three.js/blob/dev/LICENSE> - License text: <https://github.com/mrdoob/three.js/blob/dev/LICENSE>
## hyparquet ## CesiumJS (portions ported to src/geo)
- Package: `hyparquet` 1.31.2 (loaded on demand when a Data Library - Package: `cesium` 1.138.0 (`@cesium/engine` 22.3.0); the files in `src/geo/`
GeoParquet entry is switched on) are adapted from `@cesium/engine/Source/Core` (`Math.js`, `Ellipsoid.js`,
- Author: Hyperparam `scaleToGeodeticSurface.js`, `Cartographic.js`, `Cartesian2.js`,
- Source: <https://github.com/hyparam/hyparquet> `Cartesian3.js`, `Matrix3.js`, `Matrix4.js`, `Quaternion.js`,
- License: MIT `Transforms.js`, `BoundingSphere.js`, `EllipsoidGeodesic.js`, `JulianDate.js`,
`Simon1994PlanetaryPositions.js`) and say so in their first
## fzstd (hyparquet ZSTD decompressor) line. The changes: the classes' static functions became module functions,
vectors, geodetic positions and quaternions became plain-data instances of
- Package: `fzstd` 0.1.1 module-private constructors (own enumerable fields only, no methods),
- Author: Copyright (c) 2020 Arjun Barrett matrices plain arrays, and the ellipsoid a frozen object; the debug
- Source: <https://github.com/101arrowz/fzstd> argument checks were left out, as in Cesium's release build; only the subset
- License: MIT gev uses was kept.
- Author: Copyright 2011-2024 CesiumJS Contributors
- Source: <https://github.com/CesiumGS/cesium>
- License: Apache License 2.0
- License text: `node_modules/@cesium/engine/LICENSE.md` (shipped with the
dependency) and <https://www.apache.org/licenses/LICENSE-2.0>
+58
View File
@@ -0,0 +1,58 @@
// Build guard: the Cesium-mode page must not load three or Navara. The
// initial chunks are every entry chunk and what it imports statically;
// navaraMode.js reaches Navara only through import(), so three and
// @navaramap/* belong to lazy chunks. This checks the bundle's module ids
// (what Rollup put in each chunk), and fails the build otherwise.
/**
* Module ids that only the Navara engine may load, when no package list is
* given (the browser build passes the full dependency closure,
* navaraDependencies.js).
*/
export const ENGINE_ONLY_MODULES =
/[\\/]node_modules[\\/](?:three|@navaramap[\\/][^\\/]+|postprocessing)[\\/]/;
/**
* Engine-only modules in initial chunks of a Rollup bundle, sorted by chunk.
* @param {Record<string, { type: string, isEntry?: boolean, imports?: string[], moduleIds?: string[] }>} bundle
* @returns {{ chunk: string, module: string }[]}
*/
export function initialChunkViolations(bundle, pattern = ENGINE_ONLY_MODULES) {
const initial = new Set();
const visit = (fileName) => {
const chunk = bundle[fileName];
if (!chunk || chunk.type !== 'chunk' || initial.has(fileName)) return;
initial.add(fileName);
for (const imported of chunk.imports ?? []) visit(imported);
};
for (const [fileName, chunk] of Object.entries(bundle))
if (chunk.type === 'chunk' && chunk.isEntry) visit(fileName);
const violations = [];
for (const fileName of [...initial].sort())
for (const module of bundle[fileName].moduleIds ?? [])
if (pattern.test(module)) violations.push({ chunk: fileName, module });
return violations;
}
/** The Rollup plugin: fails generateBundle when an initial chunk holds three or Navara. */
export function initialChunkGuard({
log = console.log,
pattern = ENGINE_ONLY_MODULES,
} = {}) {
return {
name: 'gev-initial-chunk-guard',
generateBundle(_options, bundle) {
const violations = initialChunkViolations(bundle, pattern);
if (violations.length) {
this.error(
'Navara-only modules in the Cesium-mode initial chunks:\n' +
violations
.map(({ chunk, module }) => ` ${chunk}: ${module}`)
.join('\n'),
);
return;
}
log('[initial-chunks] no Navara-only modules in the initial chunks');
},
};
}
+65
View File
@@ -0,0 +1,65 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
/** Where @navaramap/three keeps its prebuilt worker assets, or null. */
function defaultAssetsDir() {
try {
const entry = fileURLToPath(import.meta.resolve('@navaramap/three'));
return path.join(path.dirname(entry), 'assets');
} catch {
return null;
}
}
/**
* Navara's prebuilt workers (@navaramap/three/dist/assets) load their wasm by
* file name next to themselves (`new URL('x-hash.wasm', self.location)`), a
* reference the bundler cannot see. When a build includes Navara (the
* ?engine=navara chunk), copy every such wasm the bundle lacks into
* `assets/` under its own name. A build without Navara gets none.
* @param {{ assetsDir?: string | null }} [options]
*/
export function navaraWorkerAssets({ assetsDir } = {}) {
return {
name: 'navara-worker-assets',
generateBundle(_options, bundle) {
const usesNavara = Object.values(bundle).some(
(file) => file.type === 'chunk' && file.code.includes('navara_wasm'),
);
if (!usesNavara) return;
const dir = assetsDir === undefined ? defaultAssetsDir() : assetsDir;
if (!dir || !existsSync(dir)) return;
for (const name of readdirSync(dir).sort()) {
if (!name.endsWith('.wasm')) continue;
const fileName = `assets/${name}`;
if (bundle[fileName]) continue;
this.emitFile({
type: 'asset',
fileName,
source: readFileSync(path.join(dir, name)),
});
}
},
};
}
const NAVARA_MODULE = /\/src\/globe\/navara\//;
/**
* Rollup chunkFileNames: the lazy chunk holding the Navara engine
* (src/globe/navara, with the Navara packages it pulls in) is named
* `navara-engine-[hash].js`; every other chunk keeps Vite's default name.
* Naming only (not manualChunks, which would pull modules shared with the
* Cesium entry into the Navara chunk), so the chunking is unchanged.
* @param {{ isEntry?: boolean, moduleIds?: string[] }} chunk
* @returns {string}
*/
export function navaraChunkFileNames(chunk) {
const navara =
!chunk.isEntry &&
(chunk.moduleIds ?? []).some((id) =>
NAVARA_MODULE.test(id.replaceAll('\\', '/')),
);
return navara ? 'assets/navara-engine-[hash].js' : 'assets/[name]-[hash].js';
}
+73
View File
@@ -0,0 +1,73 @@
// The packages only the Navara engine brings: the dependency closure of
// @navaramap/three and @navaramap/three-default-plugin (dependencies and
// peer dependencies, read from node_modules), without the packages the
// app's other bundled dependencies reach too (Cesium is external), plus
// three and postprocessing, which
// the Cesium-mode page never loads statically either. The initial-chunk
// guard (initialChunkGuard.js) fails the build when one of them is in an
// initial chunk.
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
export const NAVARA_ROOTS = Object.freeze([
'@navaramap/three',
'@navaramap/three-default-plugin',
]);
const ALWAYS_ENGINE_ONLY = Object.freeze(['three', 'postprocessing']);
// Not bundled: vite-plugin-cesium serves Cesium as a script, so what only
// Cesium reaches (draco3d, …) is not in the app's bundled graph.
const EXTERNAL = Object.freeze(['cesium']);
function readManifest(root, name) {
const file = path.join(root, 'node_modules', name, 'package.json');
return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null;
}
/** Every package reachable from `names` through installed manifests. */
export function dependencyClosure(root, names) {
const seen = new Set();
const queue = [...names];
while (queue.length) {
const name = queue.pop();
if (seen.has(name)) continue;
const manifest = readManifest(root, name);
if (!manifest) continue;
seen.add(name);
for (const next of Object.keys({
...manifest.dependencies,
...manifest.peerDependencies,
}))
queue.push(next);
}
return seen;
}
/** The engine-only package names, sorted. */
export function navaraOnlyPackages(root) {
const app = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
const navara = dependencyClosure(root, NAVARA_ROOTS);
const appRoots = Object.keys(app.dependencies ?? {}).filter(
(name) =>
!navara.has(name) &&
!ALWAYS_ENGINE_ONLY.includes(name) &&
!EXTERNAL.includes(name),
);
const shared = dependencyClosure(root, appRoots);
return [
...new Set([
...[...navara].filter((name) => !shared.has(name)),
...ALWAYS_ENGINE_ONLY,
]),
].sort();
}
/** A module-id pattern matching node_modules/<package>/ for the names. */
export function packagePattern(names) {
const escaped = names.map((name) =>
name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('/', '[\\\\/]'),
);
return new RegExp(
`[\\\\/]node_modules[\\\\/](?:${escaped.join('|')})[\\\\/]`,
);
}
+28
View File
@@ -6,6 +6,9 @@
* PANEL_BASE next to the app. * PANEL_BASE next to the app.
*/ */
import cesium from 'vite-plugin-cesium';
import { fileURLToPath } from 'node:url';
// The panel loads these paths (src/tools/globePanel.js); a test keeps the // The panel loads these paths (src/tools/globePanel.js); a test keeps the
// two in step. // two in step.
export const PANEL_BASE = '/panel/'; export const PANEL_BASE = '/panel/';
@@ -18,6 +21,27 @@ export function panelBuildConfig(config) {
return { return {
...config, ...config,
base: PANEL_BASE, base: PANEL_BASE,
plugins: (config.plugins ?? []).map((plugin) =>
plugin?.name === 'vite-plugin-cesium' ? cesium() : plugin,
),
resolve: {
...config.resolve,
alias: [
// The panel runs Cesium: the Navara mode and Navara's wind mesh
// (the panel's only paths to Navara and three) become stubs.
{
find: /^(?:\.\/|.*\/app\/)navaraMode\.js$|^.*\/globe\/navara\/windFlowMesh\.js$/,
replacement: fileURLToPath(
new URL('./panelNavara.js', import.meta.url),
),
},
...(Array.isArray(config.resolve?.alias)
? config.resolve.alias
: Object.entries(config.resolve?.alias ?? {}).map(
([find, replacement]) => ({ find, replacement }),
)),
],
},
build: { build: {
...config.build, ...config.build,
outDir: PANEL_OUT_DIR, outDir: PANEL_OUT_DIR,
@@ -26,6 +50,10 @@ export function panelBuildConfig(config) {
cssCodeSplit: false, cssCodeSplit: false,
rollupOptions: { rollupOptions: {
...config.build?.rollupOptions, ...config.build?.rollupOptions,
// The panel is a single chunk; its alias excludes the Navara engine.
plugins: (config.build?.rollupOptions?.plugins ?? []).filter(
(plugin) => plugin?.name !== 'gev-initial-chunk-guard',
),
output: { inlineDynamicImports: true }, output: { inlineDynamicImports: true },
}, },
}, },
+45
View File
@@ -0,0 +1,45 @@
// A Vite build through the panel alias resolves renderers.js's relative
// import of Navara's wind mesh to the stub (scripts/build-panel.mjs uses the
// same alias).
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { build } from 'vite';
import { panelBuildConfig } from './panel.js';
test('a Vite build resolves the relative wind-mesh import through the panel alias', async (t) => {
const root = mkdtempSync(join(tmpdir(), 'panel-alias-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(join(root, 'src/app/layers'), { recursive: true });
mkdirSync(join(root, 'src/globe/navara'), { recursive: true });
writeFileSync(
join(root, 'src/app/layers/renderers.js'),
"export const load = () => import('../../globe/navara/windFlowMesh.js');\n",
);
writeFileSync(
join(root, 'src/globe/navara/windFlowMesh.js'),
'export const real = true;\n',
);
const { resolve } = panelBuildConfig({ plugins: [] });
const [output] = await build({
configFile: false,
root,
logLevel: 'silent',
resolve,
build: {
outDir: join(root, 'out'),
write: false,
lib: {
entry: join(root, 'src/app/layers/renderers.js'),
formats: ['es'],
fileName: 'x',
},
rollupOptions: { output: { inlineDynamicImports: true } },
},
});
const [chunk] = output.output;
assert.match(chunk.code, /Navara is unavailable in the panel/);
assert.doesNotMatch(chunk.code, /real = true/);
});
+12
View File
@@ -0,0 +1,12 @@
// The panel host transports Cesium assets; Navara workers need a web page.
export function createNavaraScene() {
throw new Error('Navara is unavailable in the panel');
}
export function navaraToolBackend() {
throw new Error('Navara is unavailable in the panel');
}
export function createWindFlowMesh() {
throw new Error('Navara is unavailable in the panel');
}
+44
View File
@@ -0,0 +1,44 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createBrowserViteConfig } from './vite.js';
import { panelBuildConfig } from './panel.js';
test('panel HTML lists Cesium assets for the host resource loader', () => {
const config = panelBuildConfig(createBrowserViteConfig());
const plugin = config.plugins.find(
({ name }) => name === 'vite-plugin-cesium',
);
plugin.config(config, { command: 'build' });
assert.equal(typeof plugin.transformIndexHtml, 'function');
const tags = plugin.transformIndexHtml();
assert.ok(tags.some(({ attrs }) => attrs.src === '/panel/cesium/Cesium.js'));
assert.ok(
tags.some(
({ attrs }) => attrs.href === '/panel/cesium/Widgets/widgets.css',
),
);
});
test('the panel replaces Navara entry points and retains browser aliases', () => {
const browser = createBrowserViteConfig();
const config = panelBuildConfig(browser);
const alias = config.resolve.alias.find(({ find }) =>
find.test('./navaraMode.js'),
);
assert.ok(alias.replacement.endsWith('/build/panelNavara.js'));
// Navara's wind mesh imports three; the Cesium wind backend stays.
assert.equal(
'../../globe/navara/windFlowMesh.js'.replace(alias.find, alias.replacement),
alias.replacement,
);
assert.equal(alias.find.test('../../globe/cesium/windFlow.js'), false);
assert.ok(config.resolve.alias.includes(browser.resolve.alias[0]));
});
test('Three imports and requires share its ESM entry', () => {
const { resolve } = createBrowserViteConfig();
assert.deepEqual(resolve.dedupe, ['three']);
const alias = resolve.alias.find(({ find }) => find.test('three'));
assert.ok(alias.replacement.endsWith('/three/build/three.module.js'));
assert.equal(alias.find.test('three/addons/loaders/GLTFLoader.js'), false);
});
+50 -2
View File
@@ -5,6 +5,9 @@ import { VUE_PLUGIN_OPTIONS } from './vueOptions.js';
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js'; import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
import { embedFramingPlugin } from './embed-framing.js'; import { embedFramingPlugin } from './embed-framing.js';
import { panelBuildPlugin } from './panel.js'; import { panelBuildPlugin } from './panel.js';
import { navaraChunkFileNames, navaraWorkerAssets } from './navaraAssets.js';
import { initialChunkGuard } from './initialChunkGuard.js';
import { navaraOnlyPackages, packagePattern } from './navaraDependencies.js';
/** /**
* The repository root (this file lives at `<root>/build/vite.js`), as a * The repository root (this file lives at `<root>/build/vite.js`), as a
@@ -183,7 +186,9 @@ export function createBrowserViteConfig({
apiResponseHeadersPlugin(), apiResponseHeadersPlugin(),
// Next, so no provider route answers a Host the server does not allow. // Next, so no provider route answers a Host the server does not allow.
hostCheckPlugin(), hostCheckPlugin(),
cesium(), // Keep the SDK asset serving/copying and external imports, but let the
// scene load the script and stylesheet only when Cesium is requested.
{ ...cesium(), transformIndexHtml: undefined },
// Application views are Vue single-file components. // Application views are Vue single-file components.
vue(VUE_PLUGIN_OPTIONS), vue(VUE_PLUGIN_OPTIONS),
...plugins, ...plugins,
@@ -194,6 +199,20 @@ export function createBrowserViteConfig({
// A production build must not clean the dependency cache a running dev // A production build must not clean the dependency cache a running dev
// server is still serving optimized module URLs from. // server is still serving optimized module URLs from.
...(command === 'build' ? { cacheDir: 'node_modules/.vite-build' } : {}), ...(command === 'build' ? { cacheDir: 'node_modules/.vite-build' } : {}),
resolve: {
dedupe: ['three'],
alias: [
{
find: /^three$/,
replacement: fileURLToPath(
new URL(
'../node_modules/three/build/three.module.js',
import.meta.url,
),
),
},
],
},
optimizeDeps: { optimizeDeps: {
// First reached through the SDR worker or a dynamic import. Pre-bundle // First reached through the SDR worker or a dynamic import. Pre-bundle
// them at startup so first use cannot invalidate already-transformed // them at startup so first use cannot invalidate already-transformed
@@ -206,6 +225,18 @@ export function createBrowserViteConfig({
// Loaded on demand when the first street-level image opens. // Loaded on demand when the first street-level image opens.
'mapillary-js', 'mapillary-js',
], ],
// Navara's wasm-bindgen glue loads new URL('x_bg.wasm', import.meta.url)
// and its workers by URL; both must resolve next to the package's own
// files, not inside .vite/deps. Dev server only: the experimental
// Navara engine is a dynamic import outside the Cesium-mode bundle.
exclude: [
'@navaramap/three',
'@navaramap/three-default-plugin',
'@navaramap/engine',
'@navaramap/engine-api',
'@navaramap/engine-worker',
'@navaramap/engine-font-worker',
],
}, },
server: { server: {
host: host || 'localhost', host: host || 'localhost',
@@ -258,6 +289,23 @@ export function createBrowserViteConfig({
mapillaryToken ?? '', mapillaryToken ?? '',
), ),
}, },
build: { chunkSizeWarningLimit: 1500 }, build: {
chunkSizeWarningLimit: 1500,
// Build only: the wasm Navara's workers load by name (the Navara
// engine is a separate chunk loaded on ?engine=navara).
// The lazy chunk with the Navara engine is named navara-engine.
rollupOptions: {
// initialChunkGuard: no three/Navara module in the initial chunks.
plugins: [
navaraWorkerAssets(),
initialChunkGuard({
pattern: packagePattern(
navaraOnlyPackages(fileURLToPath(new URL('..', import.meta.url))),
),
}),
],
output: { chunkFileNames: navaraChunkFileNames },
},
},
}; };
} }
+3
View File
@@ -0,0 +1,3 @@
- Keep outside text in voice and MCP tool results to one line of 180
characters: ALPR tags, station names and other display fields (asturksever,
#1280).
@@ -0,0 +1,8 @@
- Expand CCTV coverage with a keyless Taiwan Highway Bureau fallback, 1,120
Québec camera locations, City of Ottawa traffic cameras (needs a free city
access certificate, `OTTAWA_CAMERA_CERTIFICATE`), and 2,230 curated
image-backed cameras from Oregon and Japan (InoukT, #1506).
- Québec 511 in-browser video is opt-in (`CCTV_QUEBEC_VIDEO_ENABLED=1`) and
loads only after the viewer presses Play; enforce provider-specific frame
refresh limits, and report recognized MLIT maintenance images as degraded
instead of healthy camera snapshots.
+5
View File
@@ -0,0 +1,5 @@
- One world clock now drives everything with a time of its own: the sky,
the HUD clock, satellites, launches and weather. A timeline along the
bottom of the screen, shown while one of those layers is on, plays, pauses
and scrubs it, with a tick for each launch or frame and a way back to
live. Share links made away from live carry the time (samehkhamis, #1512).
@@ -0,0 +1,7 @@
- The timeline brings launches up as the time passes them, offers play
speeds from real time to a week a second, and plays smoothly, holding only
when the next weather frame has yet to load, with a spinner by the time
while it waits. The time rides in a bubble over the handle. The track zooms
with a scroll or a pinch, drags along, and goes to the time under a tap, on
a phone as on a desktop. Zooming out on the globe stops while the globe is
still in view instead of leaving it a speck (samehkhamis, #1513).
+3
View File
@@ -0,0 +1,3 @@
- The agent and voice query tools wrap whole-degree directions into 0–359, so
a vessel course or heading, aircraft course, transit bearing, camera heading
or satellite azimuth that rounds to 360 reads 0 (@quanticsoul4772, #1524).
@@ -0,0 +1,4 @@
- Add an adjustable event-age filter across S2 GhostMaps products, defaulting
to seven days with 1-, 3-, 14- and 30-day choices and an All markers option.
Preserve undated features, save the selected window and expire old events
while the map stays open. Thanks @dylanmaniatakes (#1528).
@@ -0,0 +1,3 @@
- The Nominatim disk-cache test checks the cache directory and file modes
only where POSIX modes exist, so it no longer always fails on Windows
(quanticsoul4772, #1547).
+4
View File
@@ -0,0 +1,4 @@
- Stop requesting Suomi NPP in Active Fires. NASA stopped delivering its data
on November 2, 2026, so `VIIRS_SNPP_NRT` returned no rows while still costing
a MAP_KEY transaction on every refresh; the layer now merges VIIRS NOAA-20,
VIIRS NOAA-21 and MODIS (IMBrandao, #1552).
+10
View File
@@ -0,0 +1,10 @@
- Add Navara, an experimental three.js/WASM globe, as a second engine next to
Cesium, switchable live under VISUAL PRESETS → Globe Engine or with
`?engine=navara`. Navara draws at CSS pixels like Cesium; Navara HD
(`?engine=navara-hd`) draws at the screen's full resolution and needs about
1 GiB more on a Retina screen. Layers keep their data across a switch and
draw on both engines; Cesium loads only when a Cesium globe or a
Cesium-only feature needs it (German, @gekh, #1555).
- Keep the cockpit camera steady near the ground: it no longer jumps up and
down when an aircraft's reported altitude sits below the globe's terrain,
on either engine (German, @gekh, #1555).
+4
View File
@@ -0,0 +1,4 @@
- Ride the selected AIS vessel from its bridge: an eye height by vessel type,
dead reckoning between fixes, a bridge HUD with a 3–24 NM radar, look-around
with the arrow keys, and a waterline read from the scene so river boats sit on
the river rather than at sea level (Denis Groshev, @ZloyS, #1557).
+4
View File
@@ -0,0 +1,4 @@
- File satellites under six classes, Stations, Imaging, Telecom, Starlink,
Probes and Debris, each with a toggle; all but Starlink show by default.
Classes come from CelesTrak SATCAT, the UCS Satellite Database and name rules
(Denis Groshev, @ZloyS, #1558).
+4
View File
@@ -0,0 +1,4 @@
- Ride a tracked satellite in an orbital view: a track-up nadir camera, 1–8×
zoom, off-nadir tilt with an approximate swath, a CURRENTLY OVER line, and the
owner's flag, operator and TLE identity. `C` no longer enters or leaves first
person; double-click and the entry buttons do (Denis Groshev, @ZloyS, #1559).
+4
View File
@@ -0,0 +1,4 @@
- Zoom imaging satellites past 8×, up to 128×, as far as their sensor's
confirmed swath allows at the live altitude, with a to-scale swath strip, a
SENSOR step, mouse-wheel zoom, imagery pre-loaded ahead of the ground track and
the TLE's age (Denis Groshev, @ZloyS, #1560).
@@ -0,0 +1,3 @@
- Show a fire perimeter under one acre at its reported size. The incident card
rounded WFIGS sizes to whole acres, so a 0.25-acre fire read "0 ac"
(Rudy Celekli, @rudycelekli, #1569).
+5
View File
@@ -0,0 +1,5 @@
- Give transit vehicle icons a readable front: the bus, tram, metro and unknown
glyphs now carry a nose or cab in their outline, so the direction of travel
shows at fleet size and survives the sensor profiles. A vehicle with no known
course gets a symmetric outline instead of one that points north, and
switches to the pointed glyph once a course arrives (IMBrandao, #1574).
@@ -0,0 +1,4 @@
- Say why Transit and Bike Share are empty outside coverage. Over a city with
no feed or system (London, Tokyo) their Layers rows say so and count the
covered regions or systems, or say how low to fly, instead of `never`;
leaving coverage clears the last-update time (German, @gekh, #1576).
@@ -0,0 +1,3 @@
- A vessel whose ambient card is on the map no longer gets a Detection callout
showing through the card and covering its speed and course. A vessel whose
card the overlay did not paint keeps its callout (German, @gekh, #1577).
@@ -0,0 +1,4 @@
- Space Missions no longer locks the workspace when its mission index fails to
load (429, timeout or bad payload): entry rolls back to the previous layers
and camera view and a toast says the index could not load (German, @gekh,
#1578).
+4
View File
@@ -0,0 +1,4 @@
- Hold the orbital camera on a mapped site as the satellite passes over it,
from a click on its marker or its Live Signals row, and list nearby sites
and satellites in orbit's Live Signals, where a row focuses a site or rides
a satellite (Denis Groshev, @ZloyS, #1580).
@@ -0,0 +1,4 @@
- Fix the translations of the voice area cards, the fire confidence filter,
the drone airspace layer and Sea Marks: one word for "landmark" per
language, the airspace layer names the airspace again, and Traditional
Chinese follows Simplified Chinese again (German, @gekh, #1581).
+4
View File
@@ -0,0 +1,4 @@
- New Shipwrecks layer (Events): about 8,000 wrecks from EMODnet (CC BY 4.0)
and Wikidata (CC0). Each has a card with deaths, date, vessel, cause, depth,
position quality and a source link. Markers grow with the death toll, and
a start and end year picker filters by period (IMBrandao, #1583).
+29
View File
@@ -0,0 +1,29 @@
- Add the News layer (Events ▸ News) and its right-rail NEWS panel: the last
24 hours of news, narrowed in the panel to the last 12, 6, 3 or 1 hours with
the hotspot counts read for that window. Markers are coloured by severity
only, with hotspot beacons, outlines with disputed areas, picture cards for
major events and a continuous feed whose cards fly the globe to the event.
An open card plays the post's clip on request. The panel has two sections.
Conflicts lists the hotspots' news. Local, below 3,000 km, lists every
report in the area at the view centre, newest first, names and outlines that area on the
globe, and shows those reports on the map instead while it is chosen; a
disputed area is named as itself and read from no country's outlets. Set
`GEV_NEWS_ORIGIN` to a news server and the local server forwards the
layer's three read routes there. Unset, Core answers them itself:
Conflicts from the last 24 hours of GDELT 2.0 event exports with no key
(physical violence only, one report per story at GDELT's place for it,
titled from GDELT's GKG files for the newest 3 hours), plus Liveuamap's
mapped items with `LIVEUAMAP_API_KEY`; Local from World News API with
`WORLDNEWSAPI_KEY`, at most `WORLDNEWSAPI_DAILY_READS` reads a day
(default 30), each country's news kept for at most 1 hour. The routes
refuse cross-site and proxied requests. A section says when its key is
required, refused or out of its daily allowance. The Data attribution popover credits the GDELT
Project, World News API and, once its items show, Liveuamap
(Denis Groshev, @ZloyS, #1593).
- Cockpit Regional News holds its list to the last 24 hours, newest first, on
the server and in the client, and labels its four states apart: fresh
results with no banner and a `LAST 24 H` footer, earlier results kept after
a failed refresh near the same place as `CACHED · REFRESH FAILED`, `NO NEWS IN THE LAST 24 H`,
and `REGIONAL NEWS UNAVAILABLE`. The banner is now really hidden over a
populated list, and the footer no longer reverts to a static `RECENT` on a
page change (Denis Groshev, @ZloyS, #1593).
+5
View File
@@ -0,0 +1,5 @@
- Add Hebrew (he), the second right-to-left language: the title, rails, dock
and corner row mirror, numbers keep Western digits, and plurals use the
one/two/other Hebrew forms. Noto Sans Hebrew joins the font stacks and the
style, plural and glossary notes arrive in docs/translations/he.md
(Hebrew, @meirm, #1594).
@@ -0,0 +1,4 @@
- Without `OPENAI_API_KEY`, the mic now says the key is missing and to add it in
Provider Settings, instead of showing the raw server error with a hint to
check microphone permissions. `/api/realtime/token` returns
`code: "API_KEY_MISSING"` with its 503 (asturksever, #1599).
@@ -0,0 +1,4 @@
- Without a Mapillary token, Street Level starts on the first keyless source
(KartaView) instead of opening on Mapillary's `KEY REQUIRED` with nothing on
the globe. A share link that turns a source off is respected
(asturksever, #1601).
-29
View File
@@ -1,29 +0,0 @@
- Add the Data Library. New regional and niche sources no longer join the
Data Layers panel: **Browse data library** at its foot lists live entries
(a manifest each in `src/library/entries/`, fetched from the provider, never
bundled) with their licence, coverage and what this instance needs to serve
them, and **Add** puts one under a Library heading. Datasets (GeoJSON, CSV,
ArcGIS), raster tiles and host-layer packs are supported; selections persist
and travel in share links as `lib=`, separate from the layer tokens; one
generic `/api/library` proxy serves entries without CORS. Seeded with NASA
EONET events, EIA and WRI power plants, NWS alerts, NOAA tide stations,
Smithsonian volcanoes, NASA Black Marble, OpenSeaMap seamarks, and the
opt-in IMF PortWatch and CCTV packs, plus the City of Amsterdam's
traffic and ANPR camera positions (opt-in; contributed by @Maher-Reven in
#251). FEMA USA Structures and the
National Bridge Inventory load what is in view once you zoom in, and
Addresscloud's EPC ratings stream from Source Cooperative's GeoParquet on
S3, coloured A–G with live legend counts. Open tile overlays (geology,
biodiversity, population, surface water, national aerial photos and maps)
join GDACS alerts and EPA Superfund and TRI facilities. 64 aerial photo
layers from the Open Imagery Register (national programmes across Europe,
NSW, NAIP and US state, county and city services, WMS and ArcGIS export
included) show their resolution, true-ortho flag, years flown and refresh
cycle in Browse; aerial photos stay above every other drape, are clipped to
their coverage, and the providers' blank fill no longer whitens the globe.
Kartverket's open Norwegian maps (topographic, nautical charts, hillshade,
terrain relief, sea depths, boundaries and the cadastre) and Mapterhorn's
global terrain, drawn in the browser as shaded relief with an elevation
legend, are in the library too. Stat tiles head the dialog. A library layer's row has
Zoom to layer and Remove icons, and the dialog has Zoom and a confirmed
Remove all (asturksever, #1604).
@@ -0,0 +1,4 @@
- Street Traffic offers separate Lines and Dots toggles while live flow is on:
Lines draws each road with flow in green, amber or red, and Dots shows or
hides the moving dots. A host can choose which road modes it offers; without
OpenStreetMap the layer never draws simulated roads (#1609).
+2
View File
@@ -0,0 +1,2 @@
- French catalog review: missing narrow no-break spaces, apostrophes, one
mistranslated message and the Starlink shell wording (abiyeenzo, #1611).
@@ -0,0 +1,4 @@
- Typed chat on OpenAI answers again: OpenAI refused every message because
`resolve_area`'s area-id pattern uses Unicode classes Chat Completions does
not accept. Chat leaves those patterns out; area ids are still checked
before the tool runs (samehkhamis, #1616).
+12
View File
@@ -0,0 +1,12 @@
- Add Icelandic road cameras from the Icelandic Road and Coastal Administration
(Vegagerðin) as a CCTV pack. The camera list is one keyless JSON request to
`gagnaveita.vegagerdin.is/api/vefmyndavelar2014_1` (about 500 views at 166
stations); frames are JPEG stills in `www.vegagerdin.is/vgdata/vefmyndavelar/`,
and only links inside that directory are registered. The catalog is fetched
over https with redirects refused and a 2 MiB body cap. An explicit facing in
the view description ("til vesturs", "í suður", "séð austur") sets a
high-confidence heading; other views keep the low-confidence fallback pose.
IRCA publishes the images under CC BY 4.0, credited in the Data attribution
popover. Loads the 250 views nearest Reykjavík, Akureyri, Egilsstaðir,
Ísafjörður and Selfoss; `CCTV_ICELAND_MAX_SOURCES` changes that cap and
`CCTV_ICELAND_ENABLED=0` turns the pack off (daikaginza, #1617).
@@ -0,0 +1,5 @@
- Add an optional Giroc / Webcam Romania CCTV source pack with corrected
coordinates, live HLS, a snapshot fallback and provider attribution. Allow
HLS segments up to 8 MiB so high-resolution cameras whose segments exceed
4 MiB can play, while retaining the 24 MiB per-session cache and existing
origin and session limits (NeoXrated23, #1619).
+3
View File
@@ -0,0 +1,3 @@
- Turning CCTV on opens the camera nearest you instead of the first camera in
the catalog, which showed a Sydney camera in London. A share link opens on
the camera nearest the shared view once its flight lands (samehkhamis, #1620).
+14
View File
@@ -0,0 +1,14 @@
- Add 34 keyless feeds to the Transit layer, the first in Central and Eastern
Europe, Italy, Spain, France, Japan and Malaysia: Szczecin, Lisbon (Carris
Metropolitana and the city's Carris), Prague, Warsaw, Kraków, Poznań,
Gdańsk, Rzeszów, Vilnius, Bucharest, Cluj-Napoca, Iași, Rome, Turin, Venice
(buses and waterbuses), Bordeaux, Montpellier, Rouen, Toulon, Le Havre,
Tours, Málaga, Sendai, Johor Bahru, Kota Bharu, Adelaide, Atlanta, Denver,
Edmonton, Halifax and Victoria. Each feed records its licence, or that none
is stated, in the registry and DATA_SOURCES.md. Speeds published in km/h
(Lisbon, Montpellier, Toulon) are converted to m/s by the proxy; route ids
read as the line on the vehicle; Prague metro, trains, trams and ferries,
Bordeaux trams and boats, the Rouen Métro, Le Havre trams and Venice
waterbuses take their own mode. The Warsaw and Poznań credits carry the
data date their terms ask for, and the data.gov.my feeds are fetched at
most twice a minute to stay inside the API's rate limit (IMBrandao).
+3
View File
@@ -0,0 +1,3 @@
- Add Swedish (sv) to the language picker: the whole interface in Swedish,
with Swedish number and date formats and a style guide and glossary in
docs/translations/sv.md (Kaj Obst, @Kajjan7610, #1630).
@@ -0,0 +1,3 @@
- An application built with fewer layers no longer restores, saves or shares a
layer it does not have when a link or saved state from a full build names it
(samehkhamis, #1634).
+4
View File
@@ -0,0 +1,4 @@
- Draw rows take an application's own actions: `createApplicationTools({
drawMarkActions })` adds buttons to drawn areas' and lines' rows, each given
the shape's `[lon, lat]` geometry, with what it answers shown under the row.
The standalone build passes none and is unchanged (samehkhamis, #1636).
@@ -0,0 +1,4 @@
- Keep the orbital cockpit's sensor strip steady through a zoom step, load the
satellite owner flags at startup from one generated 15 KB sheet, and name
"Place names" in the loading status while the place-name packs load on first
lookup (Sameh Khamis, @samehkhamis, #1637).
@@ -0,0 +1,5 @@
- Voice on OpenAI connects again: OpenAI Realtime refused every session, so
the mic failed with "Failed to create Realtime token", because
`resolve_area`'s area-id pattern uses Unicode classes it no longer accepts.
Voice leaves those patterns out, as typed chat does; area ids are still
checked before the tool runs (avaldini, #1638).
@@ -0,0 +1,7 @@
- Keep buses and trains off rooftops and out of bridge decks on the photoreal
3D tiles: the ground floor probes the rendered mesh where each vehicle is,
keeps each reading at the point it was taken (up to sixteen a cell) and
answers from the reading nearest the vehicle, instead of one probe at each
110 m cell's centre that lifted every vehicle in the cell onto whatever lay
there. Transit probes each vehicle's latest point, and grounded flights read
the floor at their own position (Sameh Khamis, @samehkhamis, #1642).
@@ -0,0 +1,3 @@
- Street Traffic Lines build one set at a time while a city loads, instead of
one per paint, so panning with Lines on no longer piles up line builds
(Sameh Khamis, @samehkhamis, #1645).
+6
View File
@@ -0,0 +1,6 @@
- Show a photo of the tracked aircraft. Tracking a civil flight asks
Planespotters for that aircraft's photo, and the CONTEXT panel's CONTACTS view
shows it with the photographer's credit and a link to the photo's page. A
flight lookup (type, route or photo) that failed temporarily is now asked
again after a minute instead of staying empty for the session, and a callsign
change drops the previous callsign's route (Sameh Khamis, @samehkhamis, #1647).
+4
View File
@@ -0,0 +1,4 @@
- A FIRMS snapshot that ends at a set time (`at`, epoch ms, from a source that
serves one) is no longer labelled live: the Active Fires row reads
`NASA FIRMS · 24 h to 10-21 15:00 UTC`, and `get_active_fires` says "in the
24 hours to" that time and reports it as `window_end` (samehkhamis, #1650).
@@ -0,0 +1,7 @@
- A tracked flight's trail splits at feed gaps, impossible jumps and reports
without altitude instead of joining a day of history into one line, keeps
every history point, and follows the path the aircraft was drawn along so it
no longer cuts across turns. A tracked aircraft that stops reporting shows
STALE, keeps flying on its last heading for five minutes, then stops dimmed
as SIGNAL LOST and is kept ten minutes after its last report; aircraft on or
near the ground hold instead (Sameh Khamis, @samehkhamis, #1654).
+4
View File
@@ -0,0 +1,4 @@
- A long flight keeps its origin and destination when weather or airways take
it far off the great circle: the route check now allows 200 km or 15% of the
route's length, whichever is larger (623 km for JFK–SFO), while a short hop
still allows 200 km (Sameh Khamis, @samehkhamis, #1655).
@@ -0,0 +1,3 @@
- A tracked aircraft is dimmed as STALE while the whole flight feed is
stalled, as it is after a missed update, instead of staying at full
brightness (Sameh Khamis, @samehkhamis, #1656).
+3
View File
@@ -0,0 +1,3 @@
- A tracked aircraft's Planespotters photo is shown even when the answer names
no photographer, linked to its Planespotters page with no credit line,
instead of no photo at all (Sameh Khamis, @samehkhamis, #1657).
@@ -0,0 +1,5 @@
- Hide street-vehicle brackets and callouts behind buildings and terrain. A
Street Traffic contact behind a building, a 3D tile, a hill or a tunnel roof
now loses its bracket and "VEH-nnnn" callout along with its dot, instead of
having them painted over the facade; aircraft, vessels and satellites keep
their labels over the scene as before (Sameh Khamis, @samehkhamis, #1659).
@@ -0,0 +1,4 @@
- Keyless Gemini voice no longer blames the microphone. Without
`GEMINI_API_KEY`, the voice control shows only "Add a Gemini API key in
Provider Settings", without the microphone-permission hint, as OpenAI voice
already does (Sameh Khamis, @samehkhamis, #1660).
@@ -0,0 +1,4 @@
- Gemini voice messages follow a language change. A Gemini Live status or
error, such as "Gemini quota is unavailable", is shown in the new interface
language after you switch it, instead of staying in the old one, as OpenAI
voice messages already do (Sameh Khamis, @samehkhamis, #1661).
@@ -0,0 +1,36 @@
- Flights, Military Flights and AIS Vessels follow the world clock when their
source declares a timeline, that is, can answer a snapshot at a given time
(`source.timeline`, read by `readSnapshotTimeline` from
`gods-eye-view/sources/live`). Away from live they show the snapshot at the
clock's time and move on with it as it plays; back at live they poll as
before. A source's `timeline.subscribe` may also tell of a better answer
for a time it has answered; paused, the layer loads the time shown again.
A source with no timeline keeps them live, unchanged (Sameh Khamis,
@samehkhamis, #1662).
- AIS Vessels are keyed by the stable id their live source gives (`id` on a
vessel record), not by MMSI: vessels can share an MMSI, through spoofing,
reused numbers or defaults such as 0, and a number can move to another
hull. Two vessels sharing an MMSI are now two vessels, each selected,
followed and carded on its own. The MMSI is an attribute (`mmsi`), shown
when known and `--` otherwise, up to ten digits; looking a vessel up or
selecting it by MMSI (an older link, say) still finds a vessel with it, and
a vessel with no name is no longer named by its id. `vesselSnapshot` takes
`idFor(row)` for a source whose rows carry an id of their own. Tool and voice results carry the vessel's id beside its MMSI
(`id`, or `vesselId` where `id` is a display label), and `get_vessel_track`
also takes an `id`. AISStream has only MMSIs, so its vessels are keyed by
MMSI and nothing changes there (Sameh Khamis, @samehkhamis, #1662).
- A share link made while a vessel is selected (and the camera follows
nothing) now opens that vessel: the link names it, opens with the camera
where it was, looking at the vessel, and selects the vessel once the feed
has it, at the link's world time when it has one. A vessel id that is a
source's stable id, such as a UUID, travels as its 16 bytes in a new thing
field (13), so the link stays short (62 characters where a followed
aircraft's is 44). MMSI and other ids, and links already shared, are
unchanged; an app that predates the field opens such a link at the same
camera, without selecting the vessel. A camera that no follow offset can
carry (below the vessel's horizon, say) still makes a place view.
A shared flight, military flight, vessel or satellite that is not reporting
now says so in a sentence ("The shared vessel isn't reporting now — showing
where it was at 14:05"), with its id on a second line under it (ICAO,
NORAD, MMSI or vessel UUID) instead of at the head of the sentence (Sameh
Khamis, @samehkhamis, #1662).
+4
View File
@@ -0,0 +1,4 @@
- A weather feed can supply decoded frames to the weather layers. With an
optional `getFrameImage` or `getFrameTile`, Core draws the images and tiles
the feed returns instead of reading the image and tile routes; a feed
without them is unchanged (samehkhamis, #1665).
+4
View File
@@ -0,0 +1,4 @@
- Move the tile budget helpers `utcDayKey`, `normalizeBudget` and
`isOverBudget` out of the TomTom module into `src/data/tileBudget.js`, so
other keyed tile proxies can share them. `src/data/tomtomTiles.js`
re-exports them; behaviour does not change (monwiss, #762).
@@ -0,0 +1 @@
- Run the existing GC-bracketed allocation regression gates on Node 26 as well as Node 24. Keep the current byte budgets so CI catches allocation regressions on both supported runtimes (BreezeDelegate, #766; fixes #39).
+4
View File
@@ -0,0 +1,4 @@
- Answer `405 Method Not Allowed` with `Allow: GET` to anything but `GET` on
`/api/flights`, `/api/military` and `/api/route`, as the other query proxies
do. They answered every verb with the same body. The refusal comes before
rate limiting and any upstream call (daikaginza, #794).
@@ -0,0 +1,5 @@
- Compress the live flight snapshot on the wire. `/api/flights` gzips its
cached global snapshot and its regional adsb.lol fallback once per refresh
and sends that frame to clients that offer gzip: about 860 KB becomes about
250 KB. Other clients get the same bytes uncompressed, and both answers carry
`Vary: Accept-Encoding` (daikaginza, #795).
@@ -0,0 +1,7 @@
- The voice debug log now says why OpenAI Realtime did or did not get a
viewport screenshot or pointer crop. When an entity-context answer reaches
the image step, the log records a reason-coded decision and, when an image
was due, one outcome under the same id: sent, or why it was dropped (a stale
pointer, a camera move, a newer turn). What is captured, sent and kept is
unchanged, and no image data or user text is logged (Kevin Rajan, @kvnloo,
Manjunath, @manjunath22466, #835).
@@ -0,0 +1,3 @@
- Let the voice assistant read the storm you select in Cyclone advisories:
its official advisory, position, intensity and movement. Clearing the
selection and the storm leaving the feed are reported apart (kvnloo, #847).
+4
View File
@@ -0,0 +1,4 @@
- Reduce dead-reckoning work for flights already hidden behind the horizon.
Hidden contacts update on a staggered one-in-13-ticks rotation (about once a
second) while visible contacts and contacts with fleet models retain the full
update path (Wilmer Alzate, #870).
+23
View File
@@ -0,0 +1,23 @@
[
{
"id": "ro-giroc-webcamromania",
"name": "Giroc — Webcam Romania",
"city": "Giroc, Timiș",
"cityId": "giroc",
"provider": "Webcam Romania · webcamromania.ro",
"sourceKind": "configured",
"feedType": "hls",
"url": "https://w4.webcamromania.ro/giroc/index.m3u8",
"snapshotUrl": "https://webcamromania.ro/media/snapshots/01a0267e-d893-7203-a7eb-7ce3c8c99703/latest.jpg",
"lat": 45.70028947266781,
"lon": 21.233675914004227,
"headingDeg": 0,
"headingConfidence": "low",
"pitchDeg": -17,
"fovDeg": 74,
"rangeM": 700,
"mountHeightM": 24,
"groundElevationM": 87,
"license": "Public camera: https://webcamromania.ro/webcam-orase/webcam-giroc/ . Provider terms: https://webcamromania.ro/termeni-si-conditii/ . Video rights remain with the camera operator. Coordinates corrected by the contributor; direction, height, elevation and field of view are uncalibrated estimates."
}
]
File diff suppressed because it is too large. Load diff
+11
View File
@@ -188,6 +188,17 @@ state restoration, and release them after the consumers stop. The catalog
constructor (`application/layers`) also requires `wind`, `weather` and constructor (`application/layers`) also requires `wind`, `weather` and
`cyclones` sources with `getSnapshot` for the Weather layers, and owns the `cyclones` sources with `getSnapshot` for the Weather layers, and owns the
observed-history clock those layers share for the catalog's lifetime. observed-history clock those layers share for the catalog's lifetime.
A `weather` source may also supply decoded frames: an optional
`getFrameImage({ product, time, size, bbox, signal })` returns a whole-extent or
detail image and an optional `getFrameTile({ product, time, z, x, y, size,
signal })` returns one tile or null for a transparent tile, each as an
`ImageBitmap`, `ImageData`, canvas or `VideoFrame` of exactly the requested
size, which the layer draws once and closes. Without them the layers read the
image and tile routes (`WeatherFrameFeed` in `src/layers/weather/source.js`).
On globe surfaces, a source with `getFrameTile` currently requires Cesium for
radar, regional infrared and lightning. Navara pauses those products with
`This weather source requires Cesium`; decoded global infrared images and
the whole-frame images used over 3D tiles draw through the shared display.
`application/services` accepts boundary, terrain, regional-context, weather and `application/services` accepts boundary, terrain, regional-context, weather and
summary services; its weather service supplies cockpit local conditions, not the summary services; its weather service supplies cockpit local conditions, not the
+155
View File
@@ -0,0 +1,155 @@
# CCTV integration workflow
Use this workflow when adding a camera provider or repairing an existing feed.
The CCTV catalog comes through the local server; media normally does too. Keep
provider-specific URL policy on the server, including any validated browser-only
media URL permitted by the exception below.
## 1. Check the source before coding
- Identify the catalog endpoint and the actual frame or live-media endpoint.
They may be different hosts or use different formats.
- Read the provider's license, terms, attribution, rate limits, and access
requirements. Publicly reachable data is not automatically licensed for
reuse. Record whether the app may display it and whether any use is
restricted.
- Prefer a public, keyless feed. If credentials are required, keep them in a
server-only environment variable; never put a secret in browser code or a
Vite-exposed variable. A pack that needs a key is opt-in by that key: with
the variable unset it makes no request and logs once (Ontario 511 and
Ottawa). Keep the key out of catalog URLs and add it only at fetch time. Use
the provider's documented API, not a website endpoint that skips its key. If
access terms require an agreement or remain unconfirmed, make the pack
opt-in and ensure it makes no request by default.
- Confirm the feed can be fetched from the app's Node server environment. A URL
opening in a browser does not prove the server can reach it, and browser CORS
behavior does not diagnose a server-side fetch.
- Treat camera directories and aggregators such as [TW Live](https://tw.live/)
as discovery aids. Trace each listing to the original agency or camera owner;
their data and media terms govern reuse. For example, SkylineWebcams says
only camera hosts may embed its live feed and its terms restrict copying or
retransmitting footage, so do not proxy its stream without written
authorization ([FAQ](https://www.skylinewebcams.com/support/faq.html),
[terms](https://www.skylinewebcams.com/en/terms-of-use.html)).
- Decide whether the provider supplies still snapshots or continuous video.
Prefer stills unless live video is necessary and supported by the existing
proxy path.
## 2. Add and register the provider pack
Use the smallest existing provider as a template. Current owners are:
- `server/providers/cctv/sources.js` for standard open-data loaders.
- `server/providers/cctv/<provider>.js` for a provider with its own parser,
fallback, or more involved URL policy.
- `server/providers/cctv/catalog.js` to import the loader and add it to
`LIVE_PACKS`. Each pack is independently gated and the shared catalog cap
allocates space across packs.
- `server/providers/cctv/constants.js` for provider endpoints, caps, and
timeouts.
Return normalized camera records with stable unique IDs, readable names,
`city`/`cityId`, provider attribution, valid `lat`/`lon`, and the correct
`feedType`. Include camera heading only when the source actually provides a
bearing; descriptive direction text is not a numeric heading. Mark uncertain
heading data with the existing low-confidence conventions. Set `sourceKind` so
health and source diagnostics identify the provider. Apply a bounded per-pack
camera limit and geographic filtering where appropriate; avoid one provider
silently consuming the whole shared catalog cap.
Camera coordinates need their own verification. Prefer the original provider's
camera coordinates, then corroborate them against its location description,
route/mile marker, or official map. An aggregator's map dot, a town/county name,
or a geocoded attraction name is not a precise camera position. Do not pin a
camera to a city center or nearby landmark; if its actual location cannot be
verified, leave it out until it can. For manually curated coordinates, record
the evidence in the provider data or an adjacent comment. For example, TW Live
camera pages identify the originating agency; use that agency's catalog for
the camera point when available.
If the source becomes opt-in, use the catalog's opt-in gate (`envOptIn`) and
document the flag and reason in `DATA_SOURCES.md`. Otherwise use the normal
enabled-by-default flag pattern and document how to disable the pack.
## 3. Keep frame and media requests server-registered
The browser should request the local `/api/cctv/frame/:id` or
`/api/cctv/media/:id` endpoint. Do not pass an arbitrary upstream URL from the
browser, and do not fetch an unvalidated URL supplied by a catalog record.
Every provider must have a server-side rule that pins frame and media URLs to
the expected scheme, host, and path; use HTTPS where the provider supports it.
Use `pinnedCameraUrl()` for URLs that belong under a fixed provider directory,
or a stricter provider-specific validator when hosts or paths vary. Register
any new permitted image/media origin in the existing
`server/providers/cctv` media policy.
Catalog and media loaders must also:
- refuse unsafe schemes, credentials, unexpected ports, redirects outside the
registered origin/path, and malformed identifiers;
- set request deadlines and cap catalog and buffered frame bodies;
- parse only the expected format and validate coordinates before returning a
camera;
- handle offline cameras and partial provider failures without failing the
entire merged catalog; and
- avoid persistent frame storage unless the provider's terms explicitly allow
it and the design has been reviewed.
For live media, prefer the existing proxy and supported feed type (`mjpeg`,
`mp4`, `webm`, or `hls`). A direct browser-only video is a narrow fallback for
a source whose terms allow embedding when the browser can play it but the app
server cannot fetch it. Unconfirmed embedding terms make it opt-in like any
other pack, and the browser loads it only after an explicit play action, never
on selection, because the provider sees the viewer's IP address. Build its URL
from a validated provider ID on the server; never accept a browser-supplied
media URL. Render it in a native DOM
`<video>` without `crossOrigin`, and do not read its pixels into canvas, WebGL,
or a Cesium texture. If a map preview is needed, a second video request may be
paused as a DOM overlay at the camera location; label it as an approximate
still and scale it with map distance. If terms disallow embedding, use a source
link instead. Read [SECURITY.md](../SECURITY.md) before changing URL
validation, redirects, or proxy behavior.
When an image provider sets a minimum refresh interval, enforce it at the
server frame boundary as well as in the client. A bounded in-memory cache and
single-flight request per camera prevent projection, panel, and re-selection
paths from multiplying upstream requests; do not persist frames unless the
provider's terms explicitly allow it.
## 4. Add focused regression coverage
Add provider tests under `src/data/` following the existing CCTV test naming
and fixtures. Cover at least:
- representative valid catalog rows and their normalized output;
- missing or invalid IDs, coordinates, and frame URLs;
- duplicate records and the pack's cap/filter behavior;
- unsafe host/path, credentials, and redirect cases;
- timeout, oversized, malformed, and unavailable upstream responses; and
- the frame or live-media content type the provider actually serves.
Use `src/data/cctvProxy.test.mjs` for shared proxy/media behavior and
`src/data/cctvCatalogCap.test.mjs` for shared catalog allocation. For a
browser-only source, verify native playback, denied canvas pixel access, the
paused second-player overlay, and its cleanup separately from proxy tests. Run
the focused provider and proxy tests, then the standard checks from
[CONTRIBUTING.md](../CONTRIBUTING.md) that apply to the change. For runtime
verification, inspect `/api/cctv/sources`, `/api/cctv/health`, and the camera's
`/api/cctv/frame/:id` or `/api/cctv/stream/:id` response locally. Confirm a
successful frame is from the upstream camera by checking its content type and
`X-CCTV-Source`; a fallback image is not proof that the upstream feed works.
## 5. Update source documentation and submit
- Add the provider, endpoint, terms/license, required attribution, key status,
caching behavior, and any restrictions to `DATA_SOURCES.md`.
- Update the CCTV source section in `docs/CURRENT-STATE.md` when runtime
behavior, flags, caps, or fallback behavior changes.
- Register in-app attribution through the existing source/provider credit
fields; update `src/data/dataCredits.js` when the source needs a layer-level
credit.
- Add the required `changelog.d/<pr-number>-<slug>.md` entry for a runtime
change, and include the source, license, attribution, key/runtime-fetch
details, and verification evidence in the PR description.
- For a new layer, also follow the camera/source requirements in the
[new-layer PR checklist](../CONTRIBUTING.md#adding-a-new-layer).
+128 -15
View File
@@ -19,6 +19,17 @@ excluded. The formatter validates every entry before writing any file.
New world text labels must use the world-overlay host; Cesium text labels are forbidden. New world text labels must use the world-overlay host; Cesium text labels are forbidden.
## QA scripts
New `scripts/qa-*.mjs` scenarios use `scripts/qa/browser.mjs` for headless launch
and bounded shutdown, and `scripts/qa/ready.mjs` for application, camera and
frame readiness. Select the GPU backend with `QA_ANGLE`. Keep timed pauses only
when elapsed time is part of the assertion, and explain that measurement.
Read scene state through the globe contract or the QA probe; native reads belong
only in explicit engine-adapter checks. The counters in
`node scripts/report-qa-harness.mjs` must not grow; lower the baseline with
`--write-baseline` after removing duplication.
## Current component ownership ## Current component ownership
Package imports use `gods-eye-view`; `package.json` is the authoritative Package imports use `gods-eye-view`; `package.json` is the authoritative
@@ -30,8 +41,10 @@ export inventory. Use declared exports rather than reaching into internal files.
| `src/standalone/` | Select the default catalog, local sources and setup controls | | `src/standalone/` | Select the default catalog, local sources and setup controls |
| `src/ui/` | Navigation generations, restoration, visual state, panel snapshots and their subscriptions | | `src/ui/` | Navigation generations, restoration, visual state, panel snapshots and their subscriptions |
| `src/data/` | Lifecycle, context and feed state; legacy default-layer facades | | `src/data/` | Lifecycle, context and feed state; legacy default-layer facades |
| `src/layers/<family>/` | Source acquisition, records and Cesium resources with explicit controller/renderer owners | | `src/layers/<family>/` | Source acquisition, records and shared contract displays with explicit lifecycle owners |
| `src/sources/` | Portable protocols and source contracts; request state belongs to each factory instance | | `src/sources/` | Portable protocols and source contracts; request state belongs to each factory instance |
| `src/geo/` | Engine-neutral WGS84 maths, ported from Cesium, never imports Cesium |
| `src/globe/` | Engine-neutral globe contract and its test support; engines only in `globe/cesium`, `globe/navara` |
| `src/services/` | Supplied application operations; scene construction owns caches and cancellation | | `src/services/` | Supplied application operations; scene construction owns caches and cancellation |
| `src/voice/` | Portable action schemas/session, common controls, action execution and separate protocol adapters | | `src/voice/` | Portable action schemas/session, common controls, action execution and separate protocol adapters |
| `server/providers/` | Node route factories, process-scoped provider caches and shutdown cleanup | | `server/providers/` | Node route factories, process-scoped provider caches and shutdown cleanup |
@@ -53,18 +66,70 @@ See [application construction](APPLICATION.md) and the
2. `scripts/check-package-boundaries.mjs` builds every declared export without 2. `scripts/check-package-boundaries.mjs` builds every declared export without
app Vite configuration or environment files. `scripts/package-boundaries.json` app Vite configuration or environment files. `scripts/package-boundaries.json`
assigns each export exactly once and lists its owned modules and external assigns each export exactly once and lists its owned modules and external
dependencies. Unused imports still count. Node exports have only a `node` dependencies. Shared foundations use directory entries: `src/i18n/`,
`src/geo/` and `src/globe/`. An entry ending in `/` owns the files below it;
`src/globe/` excludes `cesium/`, `navara/` and `testing/`, which need separate
ownership. The import-direction gate still enforces engine rules.
Aggregate groups use directories where their module lists cover the directory;
small groups retain explicit snapshots outside the shared foundations.
Unused imports still count. Node exports have only a `node`
condition; browser groups cannot use build-only dependency exceptions. condition; browser groups cannot use build-only dependency exceptions.
Run `node scripts/check-package-boundaries.mjs --write` to regenerate each
group's `modules` list in `scripts/package-boundaries.json`, retaining useful
entries in order and appending newly reached paths in sorted order. Directory
entries stay directories while they own reached modules; they never expand
into file lists. Without `--write`, the check rejects both undeclared reached
modules and declarations no longer reached, printing additions and removals
for each changed group. A directory becomes stale only when no reached module
belongs to it. Review the manifest diff like a lockfile diff before accepting
changes to ownership.
Portable source graphs cannot reach application/rendering, Node, Cesium or Portable source graphs cannot reach application/rendering, Node, Cesium or
browser globals. This includes `sources/*`, dedicated `layers/*/source` exports, browser globals. This includes `sources/*`, dedicated `layers/*/source` exports,
flight/military/vessel record and ingestion exports, action schemas, the session flight/military/vessel record and ingestion exports, action schemas, the session
interface, lifecycle and feed state. The browser-global rule reserves platform interface, lifecycle and feed state, and every runtime file in `src/geo/` (the
names such as `document` and `window` in these modules; it is an architectural engine-neutral geometry ported from Cesium). The browser-global rule reserves
check, not a JavaScript sandbox. Common voice controls cannot depend on a platform names such as `document` and `window` in these modules; it is an
architectural check, not a JavaScript sandbox. The globe contract
(`src/globe/*.js` and `src/globe/testing/**`) cannot reach `cesium`,
`@cesium/*`, `three`, `postprocessing` or `@navaramap/*`, directly or through
helpers, nor import an engine implementation; the `src/globe/cesium/**` graph
reaches no Navara/three package or `src/globe/navara/**` module, and the
`src/globe/navara/**` graph no Cesium package or `src/globe/cesium/**` module,
through helpers too. Outside its own directory, `src/globe/navara/**` is
reached only through a literal `import()` (the app's `?engine=navara` mode), so
Navara stays out of the Cesium-mode bundle. Common voice controls cannot depend on a
Realtime protocol implementation. Negative fixtures cover indirect helpers, Realtime protocol implementation. Negative fixtures cover indirect helpers,
self-package imports, symlinks and unreachable files. self-package imports, symlinks and unreachable files.
Runtime SDK imports are restricted by directory, including unreachable modules:
`cesium` and `@cesium/*` belong in `src/globe/cesium/`; `three`,
`postprocessing` and `@navaramap/*` belong in `src/globe/navara/`. Package
subpaths, re-exports and literal dynamic imports obey the same rule, and the
`<script>` blocks of Vue components count as runtime modules. A module outside
the Navara engine reaches it only through `import()`: a static import of the
same specifier fails even beside a dynamic one. Tests and Node test support
retain their existing exemptions. The production script loader is
`src/app/loadCesium.js`; it imports no SDK package and needs no exception, and
it adds no `widgets.css` when the page's markup has already run `Cesium.js`
(the MCP panel). The panel build replaces `src/app/navaraMode.js` and
`src/globe/navara/windFlowMesh.js` with stubs (`build/panelNavara.js`), so the
panel carries no Navara or three. There is no blanket exception for layer
`render/` directories.
The temporary `engineImportExceptions` list in `scripts/package-boundaries.json`
contains exact file/package pairs with reasons. The checker reports the used
entries and rejects duplicates, missing reasons and unused entries. Its two
exceptions are the Bhote Koshi event and locator models in `src/data/`, which
still construct Cesium property records. Their drawing uses shared displays.
Exceptions do not relax the portable, contract or cross-engine graph checks.
Cesium annotation renderers live in `src/globe/cesium/annotations/`. Wind's
Cesium rendering (the particle canvas, its GPU path and the relief shading)
lives in `src/globe/cesium/`; `src/layers/wind/` keeps the data, model,
presentation and engine-neutral inspection. The remaining annotation migration
is separate debt.
Source-only factories use dedicated `layers/<family>/source` exports where Source-only factories use dedicated `layers/<family>/source` exports where
available, preserving the same factory implementation without loading rendering. available, preserving the same factory implementation without loading rendering.
Record normalization and endpoint policies have plain owners separate from Record normalization and endpoint policies have plain owners separate from
@@ -136,6 +201,9 @@ socket/watchdog on server close and re-reads configuration after restart.
The portable `gods-eye-view/sources/adsb-lol` export normalizes existing aircraft The portable `gods-eye-view/sources/adsb-lol` export normalizes existing aircraft
records without importing Node middleware or a renderer. Browser layer/controller records without importing Node middleware or a renderer. Browser layer/controller
separation is outside this server extraction. separation is outside this server extraction.
The portable `gods-eye-view/sources/planespotters` export reads a Planespotters
photo answer (upstream or a server's cached shape) into one host-checked,
credited thumbnail; it has no fetch, DOM or Node dependency.
## Place-search and routing providers ## Place-search and routing providers
@@ -163,7 +231,8 @@ local composition mounts them in the same order. Importing the entry performs
no acquisition; factory calls create independent cache state. no acquisition; factory calls create independent cache state.
`gods-eye-view/sources/space` owns only fixed upstream URL construction: the `gods-eye-view/sources/space` owns only fixed upstream URL construction: the
CelesTrak group/TLE query and Launch Library's recent 30-day detailed feed. CelesTrak group/TLE and SATCAT record queries and Launch Library's recent
30-day detailed feed.
Callers supply the group or end date and own validation, credentials, transport, Callers supply the group or end date and own validation, credentials, transport,
response limits and cache policy. The boundary gate checks this portable entry response limits and cache policy. The boundary gate checks this portable entry
separately from the Node providers. Satellite rendering and launch replay remain separately from the Node providers. Satellite rendering and launch replay remain
@@ -387,10 +456,12 @@ the caller.
## Visual effects ## Visual effects
`ui/effects` exports the effects controller and existing preset definitions. `ui/effects` exports the effects controller and existing preset definitions.
It owns shader stages and their clock, with explicit render ownership callbacks. It owns transitions and their clock, with explicit render ownership callbacks,
Construction installs no stages or frame callbacks. Stop animation before and writes styles, bloom and sharpening through `engine.render`. Construction
releasing UI consumers, then destroy to remove owned stages and restore the installs no stages or frame callbacks. Stop animation before releasing UI
borrowed bloom state. `ui/effects/bloom` exposes the pure intensity/version helpers consumers; controller destruction disables its effects. Cesium's
`src/globe/cesium/postStages.js` owns stage creation and destruction and restores
the borrowed bloom state when the engine is destroyed. `ui/effects/bloom` exposes the pure intensity/version helpers
without loading the renderer. UI presentation and product-action coordination without loading the renderer. UI presentation and product-action coordination
remain in their callers. remain in their callers.
@@ -502,7 +573,7 @@ perform no network requests and discover no additional modules.
`maps/controller` coordinates scene changes and lifetimes. `maps/imagery`, `maps/controller` coordinates scene changes and lifetimes. `maps/imagery`,
`maps/terrain` and `maps/3d` supply constructors; `maps/defaults` selects the `maps/terrain` and `maps/3d` supply constructors; `maps/defaults` selects the
standard sources and their setup/fallback policy. The standalone facade wires standard sources and their setup/fallback policy. The standalone facade wires
these to the application's render governor. these to their engine's render requests.
A registry supplies `sources`, `defaultId`, `unknownId` and optional `recoveryId`. A registry supplies `sources`, `defaultId`, `unknownId` and optional `recoveryId`.
Each source has a user-facing `descriptor`, availability/reason, and either an Each source has a user-facing `descriptor`, availability/reason, and either an
@@ -546,12 +617,22 @@ services into each family factory. Standalone provider selection lives in
`src/standalone/layerSources.js`. The construction export has its own checked `src/standalone/layerSources.js`. The construction export has its own checked
dependency graph, excluding standalone setup and compatibility layer instances. dependency graph, excluding standalone setup and compatibility layer instances.
`src/app/layers/renderers.js` is the layer manifest. Its lazy display loaders
resolve the shared factories exported by `displays.js` through `display`. All
five drawing primitives are required by the globe contract. The only display
backend options are wind streamlines, loaded from the engine folders. A missing wind backend leaves the raster field
available. Wildlife Sightings draws itself through the contract; Sea Marks and
Ski Pistes use their overlay controller. No layer display imports an engine SDK.
`displayRehearsal.test.mjs` loads every manifest display on the fake engine with
an unknown kind and no extensions, draws a small fixture and checks cleanup.
Both aircraft layers share the catalog's classification registry; launches use Both aircraft layers share the catalog's classification registry; launches use
its satellites and Contacts uses its aircraft, vessels and installations. Data its satellites and Contacts uses its aircraft, vessels and installations. Data
registration, controls and voice actions read those same instances. Destruction registration, controls and voice actions read those same instances. Destruction
remains the manager's responsibility; classification also observes application remains the manager's responsibility; classification also observes application
abort when startup has not reached registration. Scene engines remain page-owned, abort when startup has not reached registration. The session retains models while
so this change does not introduce multiple simultaneous viewers. scene attachment builds displays on the active engine. A parked engine can remain
allocated for a warm switch; its layer drawing is detached.
Direct `src/data` compatibility entries retain their old defaults and testing Direct `src/data` compatibility entries retain their old defaults and testing
exports. Browser regression probes use the registered instance's testing surface exports. Browser regression probes use the registered instance's testing surface
@@ -590,6 +671,23 @@ listener cleanup. Application data assembly constructs both owners explicitly;
`data/manager` is the compatibility facade for direct callers. The ordinary `data/manager` is the compatibility facade for direct callers. The ordinary
state subscriptions retain their existing event contract. state subscriptions retain their existing event contract.
`data/layerSceneBinding.js` owns renderer attach/detach, asynchronous settlement,
parking and failed-release retries. Lifecycle keeps visibility intentions and
refresh policy. `app/layerSession.js` defines the retained session fields and
handoff predicate. `standalone/sceneSwitcher.js` coordinates page handoff;
`ui/shellScene.js` owns scene-bound shell construction, while `applicationShell`
keeps page controls and policy. `app/sceneHost.js` owns the transition queue,
request ordering and recovery decision. Application acquisition, attachment,
restoration and renderer release run in that queue, and shell calls use it too.
Nested work receives an explicit transaction; outside calls wait for the whole
transition. A replacement application shares the transition owner with fresh
parts, and a shell used alone creates its own host. Pick ownership, selection
dispatch and render demand belong to
`scene.services`; render demand drives the engine contract, with no dependency
from the Cesium render backend to the governor. Overlay and tracked-readout
services still include module singletons; debug access still exposes the native
viewer and tileset.
## UI state owners ## UI state owners
- `ui/navigation` owns navigation generations, pending search presentation and - `ui/navigation` owns navigation generations, pending search presentation and
@@ -630,8 +728,9 @@ source-time fallback and model-owned ground policy. The renderer supplies that
ownership fact and handles stale-ground lifting, Cesium history and primitives; ownership fact and handles stale-ground lifting, Cesium history and primitives;
record reconciliation does not import the engine or application. record reconciliation does not import the engine or application.
`layers/vessels/records` owns plain AIS metadata, stable MMSI identity and bounded `layers/vessels/records` owns plain AIS metadata, identity by the stable id the
retention. `layers/vessels/ingestion` owns source requests and feed state through source gives (an MMSI only for a feed that has nothing else; the MMSI itself is
an attribute) and bounded retention. `layers/vessels/ingestion` owns source requests and feed state through
explicit operations. Both exports exclude Cesium, DOM and application assembly. explicit operations. Both exports exclude Cesium, DOM and application assembly.
The snapshot renderer applies record changes; rendering owns a weak map of The snapshot renderer applies record changes; rendering owns a weak map of
geometry and billboard resources used by cards, picking and trails. geometry and billboard resources used by cards, picking and trails.
@@ -763,3 +862,17 @@ CSS rather than mapping machine states to universal severity. CCTV owns
frame/source and calibration rules and their data attributes, while Weather frame/source and calibration rules and their data attributes, while Weather
owns coverage labels. The rail card keeps node identity, header focus, owns coverage labels. The rail card keeps node identity, header focus,
disclosure and teardown, and does not announce every card update. disclosure and teardown, and does not announce every card update.
## Engine implementation modules
Cesium's viewer construction, atmosphere compatibility, zoom-out guard, starry
sky, map scene application, imagery and terrain live in `src/globe/cesium/`.
The public viewer export still resolves through `package.json`. Celestial
rendering, screen-event constants, scene projection and the legacy trail
backend also live there. Sonar commands and post stages are engine internals;
feature controllers call `engine.render`.
Imported-file parsing (`geoFileImport.js`, `geoImportRecords.js`,
`geoImportKml.js`) returns plain records. `geoImportDisplay.js` consumes them
through ground, movers, lines and imagery. The UI owns file bytes, visibility
and attachment lifetime, with no DataSource or engine-specific import backend.
+1165 -365
View File
File diff suppressed because it is too large. Load diff
-183
View File
@@ -1,183 +0,0 @@
# Data Library
DATA LAYERS is the curated set of core layers. The Data Library holds
everything else: regional sources, niche themes and community contributions.
A viewer opens it from **DATA LAYERS ▸ Browse data library**, adds what they
want, and the entry appears in the panel under **Library**. Nothing in the
library is on until someone adds it.
Runtime behaviour is described in [CURRENT-STATE.md](CURRENT-STATE.md#data-library--october-21-2026).
This page is the authoring guide.
## The one rule: live sources only
A library entry points at data its provider hosts. That can be a query API, a
static file at a stable URL the provider controls, or a tile endpoint. The
data is never copied into this repository. If the upstream goes away, the
entry goes stale; this project never becomes the host of record. That is what
lets every self-hosted instance serve the whole library with no data of its
own.
It also keeps the existing rule in [CONTRIBUTING.md](../CONTRIBUTING.md): do
not add data you have no right to redistribute. A live source still needs a
licence that allows this use, and the manifest must say what it is.
## Kinds
| Kind | What it is | Renders through |
| --- | --- | --- |
| `dataset` | Vector features: `geojson`, `geojsonl`, `csv` (lat/lon columns), `json` (a record list with lat/lon fields), `arcgis` (a FeatureServer or MapServer layer, paged) or `geoparquet` (one file, or a partitioned set on S3) | The GeoJSON layer Datacenters and Dams use; view-bounded and GeoParquet sets draw as ground-clamped dots and polygons |
| `tiles` | Raster tiles: `xyz` with a `{z}/{x}/{y}` template (`{reverseY}` for TMS rows), or `bbox` with a `{bbox-epsg-3857}` template (WMS GetMap, ArcGIS `export`/`exportImage`) | The active map's imagery host (globe or Google 3D tiles) |
| `pack` | A source pack an existing host layer already loads (today: CCTV) | The host layer, switched on by its own environment variable |
A `dataset` or `tiles` entry needs no JavaScript. A `pack` documents a host
layer's opt-in source so viewers can see it and operators know the switch; the
host layer owns its code. Provider entries (code implementing a family
contract, such as a Street Level provider) are not supported yet and are
refused by the validator.
## Add an entry
1. Create `src/library/entries/<id>.json`. The id is 3–48 lowercase letters,
digits or dashes and is permanent.
2. Add one import line for it in `src/library/catalog.js` (keep the list in
file-name order).
3. For a `dataset` or `tiles` entry, run `npm run library:next -- <id>` and
append `["<id>", "<token>"]` to `src/library/ledger.json`. Tokens are
permanent: never change or reuse a row, even when an entry is removed.
Packs take no token.
4. Run `npm run library:docs` to regenerate the DATA_SOURCES.md table.
5. Run `npm run library:check -- --base-ref origin/main`,
`npm run library:probe`, `npm test` and `npm run qa:data-library`.
6. Open a pull request with a `changelog.d/` fragment.
Start new entries at `"status": "community"`. Maintainers move an entry to
`verified` after reviewing its licence and seeing it stay healthy; `stale` and
`archived` mark entries whose upstream has stopped answering. Archived entries
are hidden from Browse unless a viewer already added them.
## Manifest reference
```json
{
"id": "eia-power-plants",
"kind": "dataset",
"family": "infrastructure",
"title": "US power plants, 100 MW and up (EIA)",
"provider": "EIA",
"description": "Operating US power plants with at least 100 MW of capacity.",
"icon": "ϟ",
"maintainer": "github:your-handle",
"coverage": { "countries": ["US"], "bbox": [-180, 13, -64, 72] },
"source": {
"format": "arcgis",
"url": "https://services2.arcgis.com/…/Power_Plants_in_the_US/FeatureServer/0",
"query": { "where": "Total_MW >= 100", "outFields": "Plant_Name,PrimSource,Total_MW" },
"fetch": "browser-direct",
"cacheSeconds": 86400,
"maxFeatures": 4000,
"maxPages": 3
},
"display": {
"color": "#ffc23d",
"label": "Plant_Name",
"fields": ["PrimSource", "Total_MW"],
"rank": { "field": "Total_MW", "order": "desc" }
},
"licence": { "name": "EIA data: public domain", "url": "https://www.eia.gov/about/copyrights_reuse.php" },
"attribution": { "text": "U.S. Energy Information Administration", "url": "https://atlas.eia.gov/" },
"status": "community",
"reviewed": "2026-10-21"
}
```
Unknown fields are errors, so a typo cannot silently drop a cap or a licence.
| Field | Required | Notes |
| --- | --- | --- |
| `id`, `kind`, `family`, `title` (≤ 60), `provider` (≤ 32), `description` (≤ 240), `maintainer`, `status` | yes | `family` is one of movement, cameras, infrastructure, events, weather, environment, imagery. `provider` is the short name the panel row shows. |
| `coverage` | yes | `global: true`, ISO 3166-1 alpha-2 `countries`, and/or a `bbox` `[west, south, east, north]`. |
| `source.url` | yes | https, a real hostname, no credentials or fragment. |
| `source.format` | yes | Datasets: `geojson`, `geojsonl`, `csv`, `json`, `arcgis`, `geoparquet`. Tiles: `xyz` (`{z}`, `{x}` and `{y}` or `{reverseY}`) or `bbox` (one `{bbox-epsg-3857}`, filled with the tile's Web Mercator bounds in metres, as MapLibre does). Packs: `host`. |
| `source.records` | no | JSON only: the property path to the record list, such as `stations` for `{ "stations": [ … ] }`. Omit when the response is the list itself. |
| `source.fetch` | datasets, tiles | `browser-direct` when the upstream sends CORS headers and needs no key; otherwise `proxied`. |
| `source.query` | no | Query parameters added to the URL. For `arcgis`, `where` and `outFields`; geometry is always WGS84 GeoJSON. |
| `source.columns` | no | CSV or JSON: `{ "lat": "…", "lon": "…" }` when the fields are not lat/latitude/y and lon/lng/longitude/x. |
| `source.cacheSeconds` | no | 60–86,400, default 3,600. How long the proxy keeps a response. |
| `source.maxBytes` | no | Body cap, default 16 MiB, at most 32 MiB. |
| `source.maxFeatures` | no | Feature cap, default 10,000, at most 20,000. A capped dataset reads PARTIAL. |
| `source.maxPages` | no | ArcGIS pages to follow, default 5, at most 10. |
| `source.view` | no | `{ "maxAltitudeM": 300–50000, "sample": [w, s, e, n] }`. Makes a huge ArcGIS layer view-bounded: below that camera height it loads what is in view, one page at a time; above it the row asks the viewer to zoom in. Needs `arcgis` or `geoparquet`, and `browser-direct`. `sample` is where Zoom to layer lands and what `library:probe` queries. |
| `source.partitions` | no | GeoParquet only: `{ "listUrl": "<S3 ListObjectsV2 URL with prefix>", "keyBase": "<URL keys are relative to>" }`, both on the source host. Needs `source.view`. The library indexes each file by its footer bbox and reads only the files in view. |
| `source.minZoom`, `source.maxZoom` | no | Tiles only, 0–22. |
| `source.encoding` | no | Tiles (`xyz`): `terrarium` when the pixels are elevation as terrain-RGB (height = R × 256 + G + B / 256 − 32768 m). The browser draws them as shaded relief: one orange ramp in six elevation classes (legend in metres), lit from the north-west; sea (exactly 0 m) is transparent. |
| `source.tileSize` | no | Tiles: 256 (default) or 512 px. |
| `source.bounded` | no | Tiles with `coverage.bbox`: `true` requests tiles inside the bbox only. Required for aerial imagery, whose servers paint a fill (white, a flat ocean, a coarse world mosaic) everywhere they have no photos. A 4xx tile answer is treated as "no imagery here", never retried or counted as an error. |
| `source.key` | no | `{ "id": "<key registry id>", "in": "query" \| "header", "name": "…" }`. The id must exist in `src/keySetupCore.mjs`. A keyed entry must be `proxied`, so the key never reaches the browser. |
| `display.color`, `display.label`, `display.fields` (≤ 4, two shown) | no | Datasets only, except `opacity`. |
| `display.rank` | no | `{ "field": "…", "order": "desc" \| "asc" }`. Keeps the most important features when capped and lets the map prefer them at global zoom. Numbers or ISO dates. Without it, a zoomed-out view shows whichever features come first. |
| `display.colorBy` | no | View-bounded datasets: `{ "field": "…", "colors": { "<value>": "#rrggbb" }, "labels": { "<value>": "…" } }`, up to 12 values. Colours each feature by value; the row legend lists each label with the count in view. |
| `display.legend` | no | `[{ "label": "…", "color": "#rrggbb" }]`, up to 12: a static legend, such as a tile layer's colour scale. Take the colours from the provider's own legend or colour map. |
| `display.outline` | no | Tiles: `countries` outlines every part of each covered country, `coverage` only the parts inside `coverage.bbox`, `bbox` the bbox rectangle itself (a state, county or city inside a country). Use it for aerial imagery so its edge shows. |
| `display.aerial` | no | Tiles: `true` for aerial or satellite photography. It drapes above every other imagery layer, including core overlays, and stays there when another layer is switched on. Pair it with `source.bounded`. |
| `display.noDataColor` | no | Tiles: `#rrggbb` of the provider's no-imagery fill, drawn transparent (within a small tolerance for JPEG noise along its edge). Measure it from a tile outside the coverage; do not guess. Globe maps only: Google 3D tiles ignore it. |
| `display.opacity` | no | Tiles: (0, 1]. |
| `imagery` | no | Tiles: what a photo layer is. `type` (`ortho`, `satellite-mosaic`, `oblique`, `historic`), `resolution` as the provider publishes it ("10 cm (plains) / 25 cm (Alps)"), `resolutionM` (the finest, in metres), `trueOrtho`, and optional `vintage` ("2021–present"), `updateCycle`, `orthoQuality` and `registry` (an https link to where the metadata came from). Browse shows it as a spec list on the card. |
| `pack` | packs | `{ "host": "cctv", "env": "CCTV_EXAMPLE_ENABLED" }`. Packs are always `licence.operatorOptIn: true`. |
| `licence` | yes | `name`, an https `url`, and `operatorOptIn: true` when the terms need the operator's own agreement or consent. |
| `attribution` | yes | `text` and an optional https `url`; shown in the Data attribution popover while the entry is in use. |
| `regionsServed` | no | ISO codes, when the upstream only answers from some countries. |
| `reviewed` | no | `YYYY-MM-DD` of the last licence and endpoint review. |
## Aerial imagery from the Open Imagery Register
The aerial photo entries came from the [Open Imagery Register](https://imagery-registry.netlify.app/)
on 2026-10-23: every register source with a map preview, less the ones that
fail the library's rules. Each entry keeps the register's resolution, product
type, true-ortho flag, years flown and refresh cycle in `imagery`. Before an
entry was written, a tile inside its coverage was fetched in a browser from
the app's origin: it had to be a real photo (not a blank fill), and the
provider had to send CORS headers (or the entry would be `proxied`). The
fill a provider paints at its coverage corners became `display.noDataColor`.
Left out, with the reason:
| Register source | Why |
| --- | --- |
| NASA GIBS true colour | Already in core (Recent imagery). |
| Sentinel-2 cloudless (EOX) | CC BY-NC-SA: non-commercial. |
| SWISSIMAGE Bern and Marion County PMTiles | Extracts hosted by the register, not the provider. |
| Cabarrus County NC, Hawaii, Nashville, Sioux Falls | Credit a commercial imagery vendor (EagleView, Maxar WorldView, Pictometry, Nearmap). |
| DVRPC, Missouri, Washington | The preview serves a blank white tile across the coverage centre. |
| Sarasota County FL | The service now requires a token. |
Entries whose register licence reads "see publisher terms" or "verify portal
terms" are `licence.operatorOptIn: true`: an operator reads the publisher's
terms and lists the id in `GEV_LIBRARY_ENABLE`. Re-check an entry's terms and
drop the opt-in when its publisher states an open licence.
## Choosing a fetch mode
Use `browser-direct` when the upstream answers a browser on another origin
(`Access-Control-Allow-Origin`) and needs no key. Tile servers must send CORS
headers for WebGL to use them; without them, use `proxied`.
Use `proxied` otherwise. The proxy is generic: there is no per-entry server
code. It reaches only the manifest's host, refuses redirects, caps bodies,
caches per `cacheSeconds` and never returns upstream error text. The browser
names an entry id, never a URL, so the proxy cannot be pointed anywhere else.
## Operator switches
| Variable | Effect |
| --- | --- |
| `GEV_LIBRARY_ENABLE` | Comma-separated ids of `operatorOptIn` datasets and tiles to serve. |
| `GEV_LIBRARY_DISABLE` | Comma-separated ids to remove from this instance. |
| A pack's own variable | For example `CCTV_NYC_ENABLED=1`; Browse then shows the pack as switched on. |
## When an entry belongs in core instead
A library entry can be proposed for DATA LAYERS when it has global or
near-global coverage, is keyless or uses a key the registry already has, has
stayed `verified` with no stale period for about six months, and a core
maintainer will own it. Promotion and demotion are maintainer decisions.
+1 -1
View File
@@ -90,7 +90,7 @@ load, removes partial results and reports a stable error without exposing a URL.
Successful loading leaves the attribution cards visible with the assets. Successful loading leaves the attribution cards visible with the assets.
`createApplicationTools({ sceneDataPacks: { sources } })` and `createApplicationTools({ sceneDataPacks: { sources } })` and
`new SceneDirector(viewer, style, data, { dataPacks: { sources } })` accept a `new SceneDirector(scene, style, data, { dataPacks: { sources } })` accept a
registry of source functions. Each receives `{ path, signal, maxBytes }` and registry of source functions. Each receives `{ path, signal, maxBytes }` and
returns `{ bytes: Uint8Array, mimeType }`. Custom transports must honor the returns `{ bytes: Uint8Array, mimeType }`. Custom transports must honor the
signal, bound reads and return the real MIME type. They remain application code, signal, bound reads and return the real MIME type. They remain application code,
+660
View File
@@ -0,0 +1,660 @@
# Globe primitives
A layer draws through the globe-engine contract (`src/globe/contract.js`),
not through Cesium or Navara. The contract has five drawing primitives:
- `engine.movers`: things at points;
- `engine.lines`: lines in the air;
- `engine.ground`: fills on the ground;
- `engine.imagery`: rasters on the ground;
- `engine.meshes`: textured or coloured world triangles.
Each engine implements all five. Cesium's are in `src/globe/cesium/`,
Navara's in `src/globe/navara/`, and the fake engine's in
`src/globe/testing/fakeEngine.js`. The checks every engine must pass are in
`src/globe/testing/contractSuite/index.mjs`.
A new contract member lands in one change with its implementation on Cesium,
Navara and the fake engine, a behavioral check in the contract suite, and a
mutant that the check kills. Keep check names prefixed with the member they
exercise; optional behavior belongs in `engineCapabilities`.
## Movers: things at a point
`engine.movers.createSet({ id, kind, capacity, ... })` makes a set of up to
`capacity` instances drawn as one batch. Write them with
`set(index, fields)` and `setCount(n)`, then upload the changes with one
`commit()`.
There are three kinds:
- `sprite`: an image from an atlas, a square of `sizePx`, turned by
`rotation`;
- `point`: a filled disc with an outline;
- `model`: a glTF in metres, at least `minimumPixelSize` on screen.
Use movers for aircraft, vessels, satellites, cameras, stations: anything
that has a position and is drawn the same size on screen (or as a model).
Point instances may also set `outlineColor` independently of the fill,
so an expanding callout can fade its outline without changing its width.
Instances move every frame without a rebuild. `depthTest` decides whether
the scene hides them. A pick returns the instance's `pickId` when the set is
`pickable`. `ready(index?)` says whether a model's glTF or a sprite atlas has
loaded and `failed(index?)` whether it failed to: a failed set is not loaded
again, so a layer that retries makes a new set. Cesium loads a model per
instance: an index written with another `pickId` loads its own model, so an
instance moved into a failed index does not inherit the failure.
## Lines: polylines in the air
`engine.lines.createLine({ id, widthPx, color, ... })` makes one polyline.
You can change its positions, colour and model matrix at any time.
The width is in CSS pixels. The line follows the 3D positions it is given;
it does not follow the ground. Options:
- `arc: 'geodesic'` bends it along great circles;
- `dash` draws dashes;
- `depthFailAlpha` draws a dimmer copy where the scene hides it.
Use lines for trails, orbits, launch paths, a ring at a fixed height, and
leader lines.
## Ground: fills on the ground
`engine.ground.createSet({ id, kind, order, pickable, drape })` makes a set
of flat fills that drape over the terrain and the 3D tiles. Nothing is
extruded.
Work with a set like this:
- `replace(items)` checks and stores all its items at once;
- `commit()` draws them in place of the last ones;
- `show(visible)` shows or hides the whole set;
- `list()` names the sets.
Positions are `[longitude, latitude]` in degrees, as GeoJSON stores them.
Edges follow great circles.
There are four kinds:
| kind | item | for |
| ---------- | ------------------------------------------------------- | --------------------------------------------------------- |
| `polygon` | `rings: [outer, ...holes]`, `fill`, `outline` | perimeters, airspace, footprints, cones, KML polygons |
| `disc` | `center`, `radiusM` (metres along the ground) | earthquakes, air-quality stations, a pad zone |
| `cell` | `west`, `south`, `east`, `north` (meridians, parallels) | GNSS and FIRMS cells, privacy cells |
| `corridor` | `positions`, `color`, `widthPx` or `widthM` | routes, cables, roads, coverage lines, outlines in metres |
Item options:
- Every item has its own colours, rgba in 0..1; the alpha is the opacity.
- `fill` and `outline` (`{ color, widthPx }`) are optional; an outline runs
along every ring.
- Within a set, a later item draws on top. Between sets, a higher `order`
draws on top. All ground fills draw under movers, lines and models.
- `pickable: true` makes picks return an item's `pickId`, with `owner` (the
set) and `instance` (the item's index).
- `drape` is `'both'` (the default), `'terrain'` or `'tiles'`. Both engines
honour it: Navara tells 3D tiles from terrain by its globe depth, and its
CPU picks skip a set draped only on the surface not shown (terrain while
photoreal tiles show, tiles otherwise).
Discs with a fill may use `radial: { fillAlpha, rimAlpha, rimStart, rimEnd,
emission }`. Alpha multipliers are in 0..1; the radius fractions satisfy
`0 <= rimStart < rimEnd < 1`; emission is nonnegative. The rim grows with
smoothstep between the two radii, then fades smoothly to zero at the outer
edge. Space Missions uses a 500 m disc with alpha 0.105 inside and 0.72 at
the rim, transitions at 0.952 and 0.985, and emission 0.35. Cesium retains
its textured ground material and polygon depth bias. Navara composes the
same radial envelope in its ground pass after tone mapping.
An unpickable pixel corridor set with `reveal: true` accepts
`setReveal(fraction)` in 0..1. Each path is clipped at that fraction of its
length. Geometry stays intact: Cesium updates material uniforms and Navara
updates the corridor item texture. Hiding, showing and committing a new
path retain the fraction. A corridor may have `backing: { widthPx, color }`:
a wider edge with its own colour, with the core blended only once.
Transit drapes its history and active head through ground corridors. Its
completed runs retain their age bands, 5 px dark backing and 3 px colour,
with band updates once per displayed second. The active head is one backed
corridor revealed along the whole current segment; only a segment crossing
rebuilds its geometry. Transit uses dynamic sets so new history and head
corridors build synchronously once Cesium's terrain bounds are available.
The ground pass handles occlusion on both engines.
Cesium also builds reveal sets synchronously after loading those bounds.
For nonempty sets, `ready()` includes the visible globe's tile readiness:
classification needs the current surface depth as well as built geometry.
A `widthPx` corridor can also take the looks Cesium's ground polyline
materials gave it. The GPU works them out every frame from the time, so a
commit never rebuilds them:
| look | fields | Cesium's |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------- |
| `glow` | `power`, `taper` (1: none) | `PolylineGlow`'s `glowPower` and `taperPower` |
| `pulse` | `periodMs`, `min`, `max` | the alpha times a factor from `min` to `max` and back, a sine |
| `dash` | `cells`, `duty`, `speed`, `gapAlpha`, `emission` | the annotation route's `GevRouteFlow`: dashes flowing to the end |
- `glow` brightens the line toward white along its centre and fades it to
nothing at its edges.
- `pulse` follows the page's monotonic clock (`performance.now()`), as the
old materials did, not the app clock.
- `dash` lays `cells` dashes along the whole line. Each is lit for its first
`duty`, with soft edges of 0.08 of a cell, and brightened by `emission`
times its colour. Between them the alpha is times `gapAlpha`. The dashes
move toward the line's end at `speed` cells a second.
- A shown set with a `pulse` or a `dash` makes the engine draw a frame after
every frame, as Cesium's continuous render did. `list()` reports it as
`animated`. A `glow` alone does not animate.
Traffic's jam heat lines glow (power 0.25) and pulse (alpha 0.35 to 0.75,
1.63 s). The Directions route's 64 dashes flow at 0.55 cells a second.
A pixel corridor may instead use `dashPattern: { lengthPx, pattern: 255 }`,
a static 16-bit screen-space dash mask with transparent gaps, as Cesium's
PolylineDash material. It cannot combine with the other corridor looks.
Bhote Koshi uses an 18 px dash behind its 5 px glowing flood core.
Use `dynamic: true` on a set whose path changes each frame. Cesium waits
for its shared terrain-height bounds, then builds these small reveals
synchronously so successive frames do not discard unfinished worker jobs.
Other sets retain asynchronous building.
### Radial pulse fields
`engine.ground.createPulseField({ id, center, radiusM, heightM, color,
lineColor, haloColor, depth, lineWidth, haloWidth })` draws up to four
concentric pulse fronts. The centre is Earth-fixed metres; the radius is
metres along the surface. The disc lies on the ellipsoid raised by
`heightM` (default 0), with normal scene depth testing. Colours are rgba in
0..1, widths are CSS pixels, and `depth` is the inward band depth as a
fraction of the radius. A band's opacity falls quadratically to zero.
Call `draw({ radii, alphas, level })` with up to four radius fractions and
opacities in 0..1. Missing fronts are zero. `level` controls visibility;
the caller includes it in the alphas. The field starts hidden. `move(center)`
follows a new centre; `destroy()` releases it and is safe to repeat.
`ground.list()` includes it as `kind: 'pulse-field'`. The caller owns its
clock and render requests; the field starts no animation loop.
Global Context keeps its 2,500 m pulse height, fixed-width front and halo,
wake and rest windows, reduced-motion behaviour and 80 ms frame cadence.
Cesium retains one material disc, updating its uniforms and rotating for
small moves. Navara evaluates the same bands after tone mapping against a
raised ellipsoid and scene depth; its shader lives in
`src/globe/navara/ground/pulse.js`. The fake engine records the same state.
Bhote Koshi's event and locator share `src/layers/bhoteKoshi/render/story.js`:
the authored reveal properties feed ground corridors and polygons, and
point movers retain their independent fill and outline opacity. No story
backend is selected by engine. The authored model still uses its existing
property records; removing that model dependency is separate work.
### Ground drawing guarantees
Both engines preserve item order, fill and outline colours, pixel and metre
widths, visibility, and pick ownership. Non-pickable sets let picks through.
`ready()` reports when committed geometry is ready to draw. Animated corridor
looks use the shared ground time and request frames while shown.
Cesium translates ground items to classified fills and polylines. Navara
classifies visible scene depth and blends the ground in display space.
Navara picks ground items at the surface under the pointer. Packing, tiling
and bin sizes are implementation details in `src/globe/navara/ground/`.
## Imagery: rasters on the ground
`engine.imagery.createLayer({ id, source, alpha, show, split, order, raise })`
makes one raster layer draped over the terrain and the 3D tiles, under
every ground fill, mover, line and model. Its colours are the image's own,
blended by the texel's alpha times the layer's `alpha`.
There are two kinds of source:
| kind | source | for |
| ------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `tiles` | `url` with `{z}`, `{x}`, `{y}` (and `{s}` of `subdomains`), `minLevel`, `maxLevel`, `tileSize` | Recent Imagery's GIBS days, weather tiles, open overlays |
| `image` | `image`: a canvas, an ImageBitmap or image element, or `{ width, height, data }` RGBA bytes; or `url` | the FIRMS heat raster, a field image, event imagery |
Both take:
- `rectangle` (`{ west, south, east, north }` in degrees; west > east
crosses the antimeridian): where the layer draws, the globe when omitted;
- `credit`: HTML shown while the layer is.
Tiles are Web Mercator XYZ tiles, which stop at ±85.05°, or with
`scheme: 'geographic'` EPSG:4326 tiles (2×1 at level 0, y down from 90°N,
Cesium's `GeographicTilingScheme`). `opaque` tiles have no alpha channel,
and `maxRequests` caps the layer's tile requests in flight.
`transform(image)` turns each decoded tile (row 0 on the north edge) into
the canvas or image that draws: the weather's infrared mask. An image is
stretched over its rectangle in longitude and latitude, row 0 on the north
edge; one over the whole globe wraps across 180°.
`raise: heightM` puts an image layer on a shell that high above the
ellipsoid instead of draping it: it draws over terrain, 3D tiles and the
sky's edge, hidden only by what is nearer. The shell rises with the camera
(4 m per kilometre of camera height, at most 60 km, in 500 m steps) so the
coarse 3D tiles seen from far never hide it. Raised layers take image
sources only.
Work with a layer like this:
- `setSource(source)` draws another source in its place, at its place in
the order; the same canvas or bytes again draw their new pixels, and a
source that differs only in its `credit` keeps its pixels;
- `setSource(source, { staged: true, signal })` keeps the drawn source
until the new one is complete (the image loaded; the tiles in view in),
then swaps them. It resolves true then, and false when the new source
failed, `signal` aborted it, or another `setSource` replaced it first. A
hidden layer takes it at once. `staging()` says one is on its way;
- `setDetail({ image, rectangle } | null)` (raised layers): inside the
rectangle the layer draws the detail image in place of its own (a
sharper view of the area in view). The last detail draws until the new
one is uploaded; it resolves true once it draws. null clears it;
- `setAlpha(alpha)` fades it: at 0 it draws nothing but stays on its
surface, so it comes back without a gap (the FIRMS hand-off);
- `show(visible)` shows or hides it (Cesium unmaps a hidden layer's tiles);
- `setSplit('left' | 'right' | 'none')` puts it on one side of the split;
- `ready()` says the source is built (an image URL loaded); tiles arrive
after it;
- `destroy()`.
On the engine:
- `list()` names the layers in draw order, bottom first: a higher `order`
draws on top, then the later layer;
- `setSplitPosition(fraction)` sets where the split runs, a fraction of the
canvas width from the left (one per engine, Cesium's
`scene.splitPosition`); `splitPosition()` reads it;
- `surface()` says where imagery drapes now: `'globe'`, `'tiles'` (3D tiles)
or `'none'`.
### Imagery drawing guarantees
Both engines draw imagery below ground shapes, movers, lines and models,
in layer order. Each texel blends by its alpha times the layer alpha.
Visibility, split position, rectangle bounds and raised-shell height follow
the imagery contract. Replacing or destroying a source releases its owned
resources; stale loads cannot replace the current source.
Cesium translates sources to imagery providers and raised shells. Navara
samples imagery in its surface pass, including raised ellipsoid intersections.
The adapters own texture storage, tile selection and depth reconstruction.
Cesium moves draped layers between the visible globe and tileset surfaces;
without either surface, layers wait. Its split applies to globe imagery,
not raised shells. Navara draws the top eight visible imagery slots, with
detail images consuming a slot. Staged sources keep the current image visible
until their replacement is ready; a failed source leaves it intact.
Open map overlays use `engine.imagery` when `maps.addOverlay` is absent.
Navara's optional native raster path retains terrain detail, but does not
provide rectangle bounds, split, alpha, order, or drawing on 3D tiles.
### Who draws through imagery
- **FIRMS heat** (`src/layers/firms/render/display.js`): one image.
- **Recent Imagery** (`src/layers/recentImagery/render/display.js`): two
Web Mercator tile layers under a split.
- **Weather radar, satellite clouds and lightning**
(`src/layers/weather/render/display.js`): on the globe, the product's
geographic tiles (the regional infrared through its mask, a tile
transform), or the global infrared mosaic as one image; each new frame a
staged source; clouds, radar and lightning at orders 21, 22, 23. On 3D
tiles, a raised layer at the product's shell height
(`shellRendering.js` `WEATHER_SHELL_HEIGHTS`) with the detail window
around the view as its detail.
- **The wind field** (`src/layers/wind/render/display.js`): one image over
the globe at order 20, faded with the camera's height while streamlines
draw, raised at 5 km on 3D tiles. The streamlines stay each engine's,
behind the display's `flow` backend, loaded by the manifest:
`src/globe/cesium/windFlow.js` supplies Cesium's streamlines
(`src/layers/wind/rendering.js`), and `src/globe/navara/windFlow.js`
supplies Navara's flow mesh.
- **The Bhote Koshi event imagery** (`src/layers/bhoteKoshi/render/display.js`):
two `url` images over the event's rectangle at alpha 0.96, split left
and right at the engine's split position, order 15. The event's and the
locator's story graphics use the shared ground and mover display described
above.
- **Imported KML/KMZ ground images** (`src/data/geoImportDisplay.js`): one
image layer per rectangle at order 30, with the authored alpha capped at
0.99 as on Cesium; a tinted or rotated image is drawn on a canvas first,
and an absolute one is raised to its altitude.
- **Sea Marks and Ski Pistes** (`src/data/osmOverlays.js`): tile layers at
order 10 on engines without `maps.addOverlay`.
## Imported files
`geoFileImport.js` reads GeoJSON, GPX, KML and KMZ into the plain records in
`src/data/geoImportRecords.js`. `geoImportKml.js` resolves KML styles with
Cesium's precedence; its tests compare the records with `KmlDataSource`.
No import loads the Cesium SDK. One `geoImportDisplay.js` draws the records
on every engine: points on movers, ground paths and polygon fills on ground,
raised paths and outlines on lines, raised polygon fills on meshes, and packed
ground images on imagery.
Every owned id starts with `geo-import-`; the file controller owns visibility,
framing, refresh and destruction across engine switches.
Pins use composed square sprites to retain Cesium's pin tip, hot spot,
subregion, colour, scale and rotation; a KML heading turns with the camera's
heading, so it stays measured from north. Icon textures share a per-file
budget of 16 million pixels: many distinct styles get lower-resolution
textures at the same size. Ground heights come from the loaded surface,
refreshed after the camera or map settles, in batches of 256; on 3D tiles
only pins in the window are clamped to the tiles drawn. Ground pins ignore
scene depth and are culled beyond the horizon; absolute pins retain their
height.
Raised polygons (KML absolute or relative) draw their fills as meshes and
their outlines as lines at the vertices' heights. A ground overlay's colour
tints its image and its alpha fades it; a rotated overlay's image is turned
on a canvas over the box around it, as Cesium turns its rectangle in degrees,
and an absolute image or plain-colour overlay is a mesh at its altitude,
including negative heights (Cesium raises only absolute ones).
The shared display has known limits against Cesium: wall fills and extruded
prisms are omitted (their top geometry remains), and
`gx:LatLonQuad` and a rotated overlay whose box passes a pole are omitted.
These are counted as undrawn. Ground edges use great circles rather than
rhumb lines.
## Input ownership
`engine.input.on(type, listener, { pick, drill, pickWindow })` accepts
`pickWindow: { width, height }` with positive integer dimensions passed to
the picker. Omitted dimensions keep the engine default. Listeners share a
pick only when their windows match, and each receives its requested drill
limit. Pointer-down picks are synchronous. Moves, including drills, coalesce
to the latest position per frame. Navara clicks still use one GPU answer
from the gesture; a missing hit falls back to the requested CPU window.
Street Level requests 9 × 9 px; CCTV's gizmo requests its own window and
limit for presses and hover. Pointer ownership and removal still suppress
delivery while a pick is pending.
`engine.projector.horizonVisible(position, { ellipsoid })` accepts a plain
ellipsoid with positive finite `radii: { x, y, z }` in metres. Without the
option it uses WGS84. Street Level supplies WGS84 radii reduced by 1,000 m
so nearby cones remain visible below sea level. The shared math is tested
against the native ellipsoidal occluder, including points near the limb.
`engine.input.suspendCameraInputs()` disables camera gestures and returns an
idempotent resume function. Suspensions are counted: only the last resume
restores the state captured before the first suspension. This preserves an
already-disabled camera. `cameraInputsEnabled()` reads the current state.
Cockpit, imagery-box drawing, CCTV calibration and infrastructure flights use
these methods on every engine. Pointer claims remain separate from camera
input suspension.
Pick ownership belongs to the scene attachment, not to an engine implementation.
`scene.services.picking` owns its predicates and `scene.services.selection` owns
its shared click dispatcher. Layer entry points resolve these services using
their attached engine (`sceneServicesFor(engine)`); the pick-registry helper
functions take that engine as their final argument. Standalone entries acquire
the same services on first use. The application destroys them on detach and
creates fresh services when reattaching a parked engine. Identical layer IDs in
simultaneous scenes cannot replace or unregister one another's picks.
## Render demand
`scene.services.render` owns continuous-render holds and the last 16 idle
request reasons for one attachment. `holdContinuousRender(id)` and
`releaseContinuousRender(id)` are idempotent per owner; the last release
requests one settling frame. `governorRequestRender(reason)` forwards a
one-shot request even while another owner holds continuous rendering.
The tools call `install()` after scene consumers have registered their holds.
The governor drives `engine.render` on every engine. Cesium implements
`setContinuous` with its scene's `requestRenderMode` and disables automatic
simulation-time renders with `maximumRenderTimeChange = Infinity`. Engine
internals request their own scene directly and never import the governor.
Layer service helpers take their attached engine as the final argument;
attachment-bound callbacks can capture `renderGovernorFor(engine)` directly.
Destroying the scene clears its holds and makes the captured service inert.
A parked engine gets a fresh service when reattached.
## World overlays
`scene.services.overlay` owns one attachment's world labels and cards. It
keeps its sources, collision domains, paint lanes, canvases, frame listeners,
observers, timers and accessible action subscribers. `init()` installs its
DOM inside `engine.host.container`; the detection blend surface stays beside
the card root so it can blend against the scene.
Publish with `setEntries(sourceId, entries, options)`, `setVisible(sourceId,
visible)` and `clearSource(sourceId)`. Read painted rectangles with
`getPaintRect(sourceId, entryId)` and register custom painters with
`registerPaintLane(laneId, painter, options)`. `subscribeActions(listener)`
provides the accessible mirror; `activateAction(owner, key)` rejects an old
host's actions. The shell reconnects these operations on every attachment.
`worldOverlayFor(engine)` resolves the same service for standalone consumers.
The module helper functions take the engine first. Retained layer models use
`bindOverlayHost` with their attached-engine getter; injected hosts without a
`forEngine` resolver keep their own operations. Publications can buffer before
`init()`. Destruction is terminal, including before initialization; a parked
engine needs fresh scene services. An old paint-lane handle cannot mutate its
replacement. Font measurements are shared until the last live host releases
them.
`scene.services.trackedReadout` bridges the follow handle's label and selected
ALPR or Local ADS-B card into this overlay. `init()` subscribes to that engine's
follow changes and that attachment's context events; `refresh(entity)` republishes
changed label text, and `activeId()` resolves the card for annotation placement.
Positions stay live cached visual anchors. `trackedReadoutFor(engine)` resolves
the service; destruction removes its listeners and clears only its own source.
`scene.services.context` owns selection records and notifications. Layer publishers
use `bindOverlayHost` with their current engine, or a context service captured by
their display. It mirrors records to the page context slot and preserves the
`gev:entity-selected`, `gev:entity-selection-cleared` and aircraft awareness event
payloads for page consumers. A clear reaches the page only when that attachment
owns the page's selected record. Readouts listen only to their scene's event target.
Identical IDs in different scenes cannot share a card or clear one another's
selection. Retired context services cannot publish into a replacement attachment.
An engine handoff carries the page-selected record through the layer session.
The incoming context adopts it only while the page still selects that record;
a clear between attachments wins. Rebuilt markers replace the retained carrier
in the incoming readout without announcing another page selection.
Readout initialization reads the retained record without testing the outgoing
carrier's visibility; layers rebuild their carriers after camera restoration.
The page chrome and detection controller still have page lifetimes.
## Render effects
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
available on Cesium and Navara through `engine.render`. Cesium implements
styles, sharpening, borrowed bloom and the Anime Milky Way fade in
`src/globe/cesium/postStages.js`; contact sonar is in `sonar.js` and
`sonarCommands.js`. Cesium does not advertise `cloudCover`.
`VisualEffects` drives transitions through the render contract. UI teardown
stops animation and disables effects; engine destruction removes its stages
and restores the borrowed bloom state. Sharpen intensity zero disables
sharpening on both engines, following Cesium.
## Wildlife privacy cells
Wildlife privacy cells use one `cell` ground set for distinct bounds and taxon
accents. The fill is 25% opaque and the 2 px outline is 80% opaque, matching
Cesium's original ground geometry. Both drape on terrain and tiles and pass
picks through to the badges. Wide views and detach release the set.
## Batched line segments
`engine.lines.createSegments({ id, widthPx, color, order })` groups independent
straight segments in one batch. `replace(items)` copies pairs of Earth-fixed
endpoints and optional per-item `widthPx`, `color` and `pickId`; `commit()`
rebuilds the drawing once. Segments never connect to one another. `show()`
and `destroy()` affect the whole batch; an empty replacement clears it.
The batch shares `createLine`'s id namespace and `list()`, which includes its
endpoint count and committed `pickIds` in item order.
Cesium draws one synchronous primitive with per-instance colours and widths.
Navara draws one ribbon mesh in the overlay after tone mapping, with scene
depth and independent segment picks. Batches are solid and depth-tested;
curved, dashed, transformed and depth-fail lines still use `createLine`.
Infrastructure and cable references each use one batch for their visible
stems, preserving Cesium's colours, widths, label winners and update cadence.
Infrastructure excludes the whole stem batch while sampling its bases.
### Choosing a primitive
- **A position and an on-screen size:** a mover.
- **A line at given heights, or one that moves or is restyled every frame**
(trails, orbits, a ring at the terrain height): a line.
- **Anything that lies on the ground** and follows its relief:
- an area: `polygon`, or `disc` or `cell` when that is its shape (they
are exact and cheaper);
- a line on the ground: `corridor`, with `widthM` when its width is a
real width (an outline in metres) and `widthPx` otherwise.
- Group items that change together into one set, and replace them in bulk.
A ground set rebuilds on commit. Small paths revealed each frame use
`dynamic: true`; corridor looks animate without a rebuild, and radial
fronts use `ground.createPulseField`.
- **Pixels on the ground** (tiles, a raster, a photo): an imagery layer.
Fade it with `setAlpha`, and give a new raster to `setSource`.
## Meshes: world triangles
`engine.meshes.createMesh({ id, positions, indices, uv, image, color, show,
depthTest, pickId })` draws indexed triangles on both sides. Positions are
flat Earth-fixed xyz metres and indices name their vertices. Optional UVs run
from (0, 0) at the image's bottom left to (1, 1) at its top right. The engine
copies geometry arrays; the caller owns the canvas, image or video.
`update(patch)` replaces only supplied fields. Repeating `image` uploads new
canvas pixels; videos update on rendered frames. RGBA `color` multiplies the
image, white by default. `show()` reads visibility and `show(boolean)` changes
it. `depthTest` is `normal` or `always`; null `pickId` disables picking. Picks
return the authored id and the mesh id as owner. `destroy()` is idempotent;
`engine.meshes.list()` returns live ids in creation order.
Cesium draws a synchronous Primitive with a flat material, no back-face
culling and no depth writes. Colour, image and visibility changes retain the
primitive; geometry, pick-id and depth-test changes replace it. The engine
requests frames while the primitive or its image is preparing. Navara draws
local coordinates relative to a double-precision world origin in the shared
overlay pass after tone mapping and anti-aliasing, the pass that draws sprites
and lines, with scene depth and triangle picking. Their authored colours are
blended in display space.
Both engines own geometry, material and texture disposal. The fake engine
keeps the same state and tests triangle intersections for picks.
CCTV's `render/meshes.js` describes its live monitor quad and viewshed pyramid
through this primitive. Its markers and outlines remain movers and lines.
Obstruction probes use `engine.surface.raycast`; exclusions accept owned mesh,
line and mover handles on Cesium and restore their visibility after the query.
Data packs use one `src/scenes/dataPacks/display.js`: points on movers,
straight raised paths on lines, raised polygons and image rectangles on
meshes, and media markers with captions in a composed sprite. Polygon holes
and authored vertex heights are retained. Shared triangulation uses `earcut`,
with tests comparing its area to Cesium's polygon geometry. Images retain
Cesium's one-degree rectangle grid. Scene detach releases drawn resources and
pick targets; reattach redraws the retained decoded assets.
Annotation drapes still use their existing renderers. Their bounded building
classification volumes, animated fills and wire cages are not ordinary world
triangles; they need a classification contract before that migration can
preserve Cesium's behaviour.
## Shared follow offsets and manual drawing
`engine.camera.follow({ position, viewFrom, swingTo })` can swing from the
initial follow frame to `swingTo`, an east/north/up offset in metres. Each
frame turns at most 60 degrees and interpolates range, preserving Cesium's
tracked-camera crossing guard. `handle.retarget({ swingTo })` changes only
the pending swing; `null` cancels it. Stopping or replacing the follow, a
camera command or engine destruction ends it. Share links read the current
handle's target and label identity and the camera pose through the contract.
`engine.input.acquirePointer()` returns an exclusive claim, or `null` if
one is held. Pass it to `input.on(type, listener, { pointer: claim })` and
call `claim.release()` when done. Other screen subscriptions yield, including
picks that settle after ownership changes. Camera gestures stay enabled.
Cesium suspends its stock select and track clicks and restores the original
functions when the claim is released or the engine is destroyed.
The Draw tool uses one `drawSurface.js` on every engine: a dashed geodesic
line and a point mover set at the clicked heights. The dim, occluded band
uses the selected colour as well as the visible band. Finished geometry still
goes through the annotation board. Detaching the scene releases the claim,
listeners and preview while retaining the chosen shape and colour. The
page's pointer lease still arbitrates Draw against other tools.
## How to add an engine
Put the implementation in a new `src/globe/<name>/` folder. Keep SDK classes,
shaders, resource disposal and SDK workarounds there. Use
`src/globe/testing/fakeEngine.js` as the contract model, and
`src/globe/contract.js` as the authoritative member and data-shape reference.
Coordinates, colours, picks and display options crossing the boundary are plain
data; displays do not receive native viewers or SDK types.
Implement the required host, camera, picker, projector, frame, render, surface,
input, credit, clock and map interfaces, plus all five drawing primitives:
`movers`, `lines`, `ground`, `imagery` and `meshes`. `assertGlobeEngine(engine)`
checks these required interfaces. `engineCapabilities(engine)` reports only the
optional visualStyles, bloom, cloudCover and contactSonar groups. An optional
group must be complete or absent. Do not advertise a capability with a no-op implementation. Match Cesium's behavior where the
contract permits both engines to implement it.
Run `createContractSuite(engine, { waitFrames, ... })` from
`src/globe/testing/contractSuite.mjs`. Supply real pointer delivery, canvases and
pixel reads for browser checks; keep the fake-engine tests as fast contract
checks. Test ownership and repeated destruction as well as visible output.
The browser harness is `scripts/qa-contract-suite.mjs`; use
`--only=ground.,imagery.` for ground and imagery pixel checks.
Three reports keep migration debt from growing: `scripts/report-engine-coupling.mjs`
counts application coupling, `scripts/report-qa-harness.mjs` counts browser QA
duplication, and `src/globe/testing/contractCoverage.test.mjs` reports contract
members the fake-engine suite leaves uncalled. After removing coupling or moving
QA work to `scripts/qa-strict-harness.mjs` helpers, run the matching report with
`--write-baseline` and keep only decreases in its baseline. After adding behavioral
contract checks, remove the newly exercised names from
`src/globe/testing/contract-uncalled-baseline.json`; keep it sorted, and remove
members that no longer exist too. Run the matching tests with
`node --import=./build/registerVue.js --test` followed by their paths; the numeric
baselines reject growth, and the contract baseline also rejects stale entries.
The application currently registers two kinds: adding another still requires
registration in `ENGINE_KINDS`, scene construction/choice and the corresponding
QA harnesses. `assertGlobeEngine` currently validates that registration. The
folder and passing contract suite are the implementation boundary, not a claim
that arbitrary engine folders are auto-discovered today. Layers and their
fixtures should need no new drawing implementation.
The layer manifest (`src/app/layers/renderers.js`) loads shared factories through
`display`. Every valid engine implements all five primitives; the manifest has
no per-kind primitive lists or `needs` declarations. `backends` supplies optional
engine-specific display options, currently only wind's `flow`, loaded dynamically
from its engine folder. A missing wind backend leaves the shared raster field
available. Self-drawing layers and map overlays use the same contract and do not
register supported engine kinds in the manifest.
Run `src/app/layers/displayRehearsal.test.mjs`: it loads every manifest display
on the fake engine with an unknown kind and empty extensions. Its small fixtures
exercise attachment, drawing, hiding or release, and destruction; primitive
registries, frame/camera/input subscriptions, follow state and service records
must be clean before the engine itself is destroyed. New display entries need a
fixture. Wildlife Sightings and the two map overlays are explicit skips because
they have no display factory; their layer tests remain the coverage for those
controllers. Native wind streamlines, DOM rasterization and GPU pixels are not
covered by this rehearsal.
`node scripts/check-import-directions.mjs` enforces SDK ownership for static
imports, re-exports and literal dynamic imports, including package subpaths.
Cesium packages belong in `src/globe/cesium/`; three, postprocessing and Navara
packages belong in `src/globe/navara/`. Add the new SDK's ownership rule when
registering it. Contract graphs cannot reach any engine, and the existing
engines cannot reach one another. The temporary exact file/package exceptions
in `scripts/package-boundaries.json` must have reasons and must still be used;
the checker prints them. They are migration debt, not a pattern for new layers.
Run package boundaries, formatting, the full unit suite and the coupling report
as well; engine coupling counts may not grow.
+40 -23
View File
@@ -3,7 +3,7 @@
`gods-eye-view/infrastructure` exports `createInfrastructureLayers(services)`. `gods-eye-view/infrastructure` exports `createInfrastructureLayers(services)`.
It returns fresh datacenter and dam layers in that order, with the existing IDs, It returns fresh datacenter and dam layers in that order, with the existing IDs,
appearance, label budgets and bundled public datasets. Importing the module or appearance, label budgets and bundled public datasets. Importing the module or
calling the factory does not load data, create a viewer, or start the application. calling the factory does not load data, create an engine, or start the application.
The asset URLs resolve relative to the module; Vite rewrites them for production The asset URLs resolve relative to the module; Vite rewrites them for production
base paths. This browser source export expects a Vite-compatible asset build. base paths. This browser source export expects a Vite-compatible asset build.
@@ -21,7 +21,13 @@ const layers = createInfrastructureLayers({
governorRequestRender, governorRequestRender,
}); });
// Register these layers with the existing data-layer manager and viewer. // Attach each layer to the current engine, then enable it when requested.
for (const layer of layers) layer.init(null, { engine });
await layers[0].enable();
// On a scene switch, keep the parsed data and replace only its display.
for (const layer of layers) layer.detachRenderer();
for (const layer of layers) await layer.attachRenderer(null, { engine: nextEngine });
``` ```
| Function | Arguments / responsibility | | Function | Arguments / responsibility |
@@ -35,28 +41,39 @@ const layers = createInfrastructureLayers({
| `removeEntityContextsForLayer` | `(layerId)`; remove records owned by the layer on destruction or failed setup | | `removeEntityContextsForLayer` | `(layerId)`; remove records owned by the layer on destruction or failed setup |
| `governorRequestRender` | `(reason)`; request a frame through the existing render scheduler | | `governorRequestRender` | `(reason)`; request a frame through the existing render scheduler |
Each returned layer supports `init(viewer)`, `enable(viewer)`, `disable(viewer)`, Each returned session layer supports `init(null, { engine })`,
`update(viewer)`, `destroy(viewer)`, `getStats()`, `getLodDiagnostics()` and `attachRenderer(null, { engine, signal })`, `detachRenderer()`, `enable()`,
`getAnalystRecords()`. `disable()`, `update()`, `destroy()`, `getStats()`, `getLodDiagnostics()` and
Use the same viewer for the instance's entire lifetime. Each viewer/context/overlay `getAnalystRecords(limit)`. The first argument is retained for lifecycle
host permits one live instance per layer ID; destroy an old instance before compatibility; drawing uses the engine passed in the options. Attach once per
replacing it. Separate applications may use identical IDs with separate hosts. scene, detach before attaching another engine, and destroy when the session ends.
Callbacks must be ready before enable and remain usable through destruction. Separate applications may use identical IDs with separate context/overlay hosts.
Callbacks must remain usable through destruction.
Disable retains the loaded dataset for reuse and clears selection and listeners. `src/layers/infrastructure/index.js` owns the parsed dataset, shared in-flight
Concurrent enables share one load. Destroy is permanent, aborts pending fetches, load, enabled intention and selection across scene switches. Enable loads on
discards late results, and clears the owned data source, context records, overlays, first use. Disable hides the drawing and clears selection but retains parsed
input handler and listeners. The layer never creates an application context store, data. Detach destroys scene resources and retains the dataset and selected ID;
overlay host, viewer, or render scheduler. The consumer must resolve one compatible reattach redraws enabled data and restores selection when no other selection
Cesium installation shared with its viewer; it must not bundle separate copies. owns the context. The attachment signal cancels that scene's wait, not the
session's fetch. Destroy aborts the fetch, discards late results and releases
both data and drawing.
`gods-eye-view/infrastructure/geojson` exports the lower-level `src/layers/infrastructure/render/display.js` draws the same records on every
`createLocalGeoJsonLayer(options, services)` and existing infrastructure overlay engine: point movers for tips and pins, lines for stems, ground polygons for
helpers. `gods-eye-view/infrastructure/lod` exports the existing LOD policy helpers. footprints and ground corridors for paths. It owns the LOD walk, surface-height
Consumers use the package exports, which do not import standalone application refresh, picks, context handles and overlay publications. Destruction releases
globals. its primitives and frame, camera and input subscriptions. The layer never
creates an overlay host, engine or render scheduler and does not import Cesium.
`gods-eye-view/infrastructure/geojson` exports the overlay publication,
selection, label and error helpers in `src/data/localGeojsonCore.js`; it no
longer exports `createLocalGeoJsonLayer`. `gods-eye-view/infrastructure/lod`
exports the LOD policy helpers. The package exports do not import standalone
application globals.
Dataset files and source/license notices remain under `src/data/local_data/`. Dataset files and source/license notices remain under `src/data/local_data/`.
Their input counts are 4,352 datacenter features and 704 dam features. Cesium may Their input counts are 4,352 datacenter features and 704 dam features. The session
expand multipart geometries into multiple entities; entity and stem counts are splits multipart geometries using Cesium's stable repeated-id suffixes on every
different measurements. This change does not refresh or relicense the datasets. engine. Feature, mover and stem counts are different measurements. These files
and licenses are independent of the renderer implementation.
+106 -1
View File
@@ -1,6 +1,6 @@
# KNOWN ISSUES # KNOWN ISSUES
Updated: September 22, 2026 Updated: November 10, 2026
This file tracks active runtime issues only. This file tracks active runtime issues only.
@@ -10,6 +10,68 @@ For the roadmap and open backlog, see the repository issue tracker.
## Open ## Open
### Supplied decoded weather tiles require Cesium on globe surfaces
Status: Open (Cesium only)
Custom sources supplying individual decoded radar, regional infrared or
lightning tiles use Cesium's imagery provider scheduler
(`src/globe/cesium/weatherFrames.js`). Navara hides these controls on globe
surfaces and requests no supplied tiles there. Full-extent cloud images and
raised weather shells remain available on both engines. A full port needs
decoded tile callbacks in the imagery contract and the Navara adapter.
### Street-vehicle label depth occlusion requires Cesium
Status: Open (Cesium only)
GPU depth occlusion for street-vehicle brackets and callouts uses Cesium's
depth probe (`src/data/detection.js`). Navara retains the existing geometric
visibility checks, without the GPU depth result. The contract needs an
asynchronous depth-probe operation before this can run on both engines.
### Large raised KML polygons omit their fill
Status: Open (bounded triangulation)
On both engines, raised polygons with more than 32,768 vertices draw only
their outlines. Those with more than 128 rings draw neither fill nor
outlines. These limits bound main-thread triangulation and outline work;
the omitted geometry is counted as undrawn in the import result.
### Navara terrain tokens are not refreshed while the page stays open
Status: Open (reload required)
An expired terrain token is not refreshed during an open Navara session.
Reload the page to request a new terrain token.
### Navara does not fall back when a map source fails
Status: Open (manual map selection)
Navara does not report tile errors, so a failing map source does not trigger
fallback to another map. Select another map manually when tiles fail to load.
### Orbital imagery look-ahead is unavailable on Navara
Status: Open (Cesium only)
The orbital cockpit works on both engines. On Navara it loads imagery for
its current view but does not prefetch imagery ahead of the ground track.
At high optical zoom, imagery can arrive later than on Cesium. Navara's
package does not expose a raster prefetch operation sharing its tile cache
and request priorities.
### Ship bridge waterline uses loaded geometry on Navara
Status: Open (terrain fallback)
Navara's ship bridge samples loaded mesh geometry and detailed terrain for
the waterline. It does not request the finest mesh detail around the ship,
so the bridge height can differ while nearby tiles load. Cesium retains its
finest-detail mesh sampling.
### Wildlife grid relies on the iNaturalist UTFGrid cell schema ### Wildlife grid relies on the iNaturalist UTFGrid cell schema
Status: Open (guarded fallback) Status: Open (guarded fallback)
@@ -145,6 +207,49 @@ Status: Open (limits)
--- ---
### News layer without a news server
Status: Open (limits)
- GDELT's places and categories are machine-coded and approximate. A report can
be placed in the wrong town of the same name, or at a country an article only
names as a nationality ("Syrian refugees" placed in Syria).
- Copies of one agency story are merged, but the same event under different
wording stays as separate reports.
- Only the 11 disputed areas in the bundled list
(`src/data/local_data/natural_earth/README.md`) and the territories Natural
Earth gives no country code (Somaliland, Northern Cyprus, Siachen Glacier,
the Spratly Islands and a few more) are named as themselves; everywhere else
Natural Earth's default country lines apply, so a place in Western Sahara,
for example, is named with the country those lines put it in. GDELT places a whole city at one point: Jerusalem's falls
inside East Jerusalem, so it is named "Jerusalem" alone.
- A country's own stories in Local keep World News API's category (politics as
political, business as economy, else other) and carry no severity; fighting
there shows through the Conflicts reports placed in the area.
- When World News API takes 10 to 20 s to answer a country's first read, Local
reads unavailable until it is asked again (the next refresh, every 2 minutes
while the tab is shown, or the camera settling in another half-degree cell,
or a section switch), which is served from that read. A read over 20 s fails
and is tried again with the first ask a minute or more later.
- Local needs a World News API key. Its free plan allows 50 points a day; Core
stops at `WORLDNEWSAPI_DAILY_READS` reads (default 30) and Local then says the
daily limit is reached. A request that never reached World News API (offline)
is not counted; any answer from it is, a failing one too, and so is a
request that timed out.
- A country's news is kept for at most 1 hour (World News API's terms, which
also ask the operator for its written permission to cache at all). While
World News API cannot be read, Local in that country therefore reads
unavailable from an hour after its last good read.
- A story GDELT codes as violence whose address has no words, and whose page
title is not in yet (titles are read for the newest 3 hours only), is not
listed under Conflicts or counted in its hotspot: GDELT's coding alone is
wrong too often. It still shows in Local for its area, as "… reported in
<place>".
- `/api/news/*` refuses cross-site browser requests and requests carrying
reverse-proxy forwarding headers, like the other key-spending routes
(SECURITY.md): behind a reverse proxy the News layer reads unavailable.
---
## Closed / Intentional (for clarity) ## Closed / Intentional (for clarity)
### Proxy SSRF and error-surface hardening gaps ### Proxy SSRF and error-surface hardening gaps
+47 -15
View File
@@ -28,7 +28,11 @@ Following an aircraft is 42–50: 42 when the link is made from the app's own
follow camera with the fix taken as the link is made, 44 with a fix four follow camera with the fix taken as the link is made, 44 with a fix four
seconds older, and 50 after orbiting to another heading and pitch. A view in seconds older, and 50 after orbiting to another heading and pitch. A view in
any other style that keeps the style's own look adds only the style (two any other style that keeps the style's own look adds only the style (two
bytes): the 42-character followed aircraft in Noir is 44. bytes): the 42-character followed aircraft in Noir is 44. A selected vessel
named by its MMSI costs what an aircraft does (46 characters for the
44-character aircraft above); one named by a UUID costs 13 bytes more, as its
16 bytes travel raw (62 characters where the aircraft is 44, rather than about
88 with the UUID as text).
## Opening a short link ## Opening a short link
@@ -47,9 +51,22 @@ best effort: until the entity is found, and if it is not found in time, the
camera is where the link was made, looking at the entity's last-known fix, camera is where the link was made, looking at the entity's last-known fix,
and the app says that the entity isn't reporting and when the fix was taken. and the app says that the entity isn't reporting and when the fix was taken.
The share button makes a thing view while the camera follows an aircraft, The share button makes a thing view while the camera follows an aircraft,
military aircraft or satellite. A selected vessel is not followed (the camera military aircraft or satellite. While the camera follows nothing and a vessel
stays where the user puts it), so a link made then is a place view; vessel is selected, it makes a vessel thing view: the fix is the vessel's position
thing views open, but the app does not make them yet. as drawn (as of the world time while the vessel layer follows the world
clock, else as of its latest position report), and the offset is the one whose derived camera is
where the user's camera is (`followOffsetFor` in `src/share/follow.js`, the
inverse of `followCameraFor`, taken from the fix at the format's precision).
The opened camera is then within the offset's whole metres and degrees of
where the link was made, looking at the vessel. When no offset can carry the
camera, because it is not above the vessel's local horizontal (it would look
up at the vessel, or through the earth) or its range is outside 1 m to
10⁸ m, the link is a place view as before. A vessel is selected, not
followed, so the camera stays where the user puts it. Following an entity
takes priority over a selected vessel, and a followed entity the format
cannot name still makes a place view. Opening a vessel thing view with a
world time waits for the vessel layer's snapshot at that time and selects the
vessel once it has it.
## Payload ## Payload
@@ -106,7 +123,7 @@ probability of about 255 in 256 (many of them already fail as base64url).
| 12 | `panelState` | LEN | none | packed panel entries, see panels | | 12 | `panelState` | LEN | none | packed panel entries, see panels |
| 13 | thing | LEN | none | sub-message, see thing views | | 13 | thing | LEN | none | sub-message, see thing views |
| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | | 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) |
| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | | 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm, sentinel2 |
| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber | | 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber |
| 17 | `bloomIntensity` | uint | style | | | 17 | `bloomIntensity` | uint | style | |
| 18 | `bloomVersion` | uint | 2 | | | 18 | `bloomVersion` | uint | 2 | |
@@ -119,6 +136,7 @@ probability of about 255 in 256 (many of them already fail as base64url).
| 25 | `scopeFeatherPct` | uint | 100 | 0–200 | | 25 | `scopeFeatherPct` | uint | 100 | 0–200 |
| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus | | 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus |
| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs | | 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs |
| 28 | `worldTime` | epoch | none (live) | zigzag seconds since 2026-01-01T00:00:00Z |
Field 1 absent means `thing` when field 13 is present and `place` otherwise, Field 1 absent means `thing` when field 13 is present and `place` otherwise,
so v1 never writes it; a later view kind writes its code. so v1 never writes it; a later view kind writes its code.
@@ -132,6 +150,10 @@ style also extends the pinned style-defaults table.
`hudVariant` code 1 (`operator`) is retired: the app no longer offers that `hudVariant` code 1 (`operator`) is retired: the app no longer offers that
layout and restores it as `tactical`. The code stays reserved. layout and restores it as `tactical`. The code stays reserved.
Field 28 is the world time the link was made at, written only when it was away
from live. A link without it opens live. Unlike field 8 it may lie ahead of the
link's creation (a forecast moment), and it opens paused at that time.
Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
@@ -269,14 +291,23 @@ sub-message:
| 10 | fix alt | sint metres | 0 | | 10 | fix alt | sint metres | 0 |
| 11 | fix age | sint seconds the fix predates the created-at time | 0 | | 11 | fix age | sint seconds the fix predates the created-at time | 0 |
| 12 | observed-at | epoch, when the fix was taken; only without a created-at | none | | 12 | observed-at | epoch, when the fix was taken; only without a created-at | none |
| 13 | vessel id | UUID, 16 bytes (LEN) | |
The entity kind is the field that carries the id (its code in The entity kind is the field that carries the id (its code in
`SHARE_VIEW_ENTITY_KINDS`), so it costs nothing; exactly one of fields 1–4 is `SHARE_VIEW_ENTITY_KINDS`, or field 13 for a vessel), so it costs nothing;
present (two is malformed). Entity ids are the ones the layers look up: an exactly one of fields 1–4 and 13 is present (two is malformed). Entity ids are
aircraft's ICAO 24-bit address (six hex digits, stored as a number; other the ones the layers look up: an aircraft's ICAO 24-bit address (six hex
forms such as TIS-B `~abc123` as text), a vessel's MMSI and a satellite's digits, stored as a number; other forms such as TIS-B `~abc123` as text), a
NORAD catalog number (decimal numbers; an MMSI with a leading zero stays vessel's id (its MMSI, or a source's stable id, such as a UUID) and a
text). Ids are lowercase and match `[0-9a-z~_-]{1,16}`. satellite's NORAD catalog number (decimal numbers; an MMSI with a leading
zero stays text). Ids are lowercase and match `[0-9a-z~_-]{1,16}`, except a
vessel id that is a UUID (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, lowercase
hex): it is written as its 16 bytes in field 13 rather than as 36 characters
of text in field 3, after the other thing fields, and decodes as the
lowercase dashed UUID. Field 13 of any other length is malformed. A decoder
that predates field 13 skips it, finds no entity and opens the view as a
place at its derived camera (`unknown-entity-kind`), as for a new entity kind,
so the field needed no new version.
Fields 5–7 are the follow offset: the camera looks along (heading, pitch) at Fields 5–7 are the follow offset: the camera looks along (heading, pitch) at
the entity from `range` metres away, heading clockwise from north, in the the entity from `range` metres away, heading clockwise from north, in the
@@ -314,8 +345,9 @@ bytes, a check byte that does not match (`checksum`), an unknown version byte,
a truncated value, a varint over 8 bytes or above 2⁵³ − 1, an a truncated value, a varint over 8 bytes or above 2⁵³ − 1, an
overlong varint, a LEN longer than what remains (at any level, including overlong varint, a LEN longer than what remains (at any level, including
inside the thing sub-message), a reserved wire type, field number 0, a known inside the thing sub-message), a reserved wire type, field number 0, a known
field with the wrong wire type or repeated, two entity ids in one thing, a field with the wrong wire type or repeated, two entity ids in one thing
duplicate panel/option/style entry, non-ASCII text, and a latitude or (field 13 counts as one), a UUID vessel id that is not 16 bytes
(`bad-length`), a duplicate panel/option/style entry, non-ASCII text, and a latitude or
longitude (camera or fix) beyond ±90/±180. longitude (camera or fix) beyond ±90/±180.
Tolerated and reported in `unknownFields`: unknown fields at any level Tolerated and reported in `unknownFields`: unknown fields at any level
@@ -353,8 +385,8 @@ the app that can open it.
boolean takes the next flag bit instead, which costs nothing when it is at boolean takes the next flag bit instead, which costs nothing when it is at
its default. its default.
6. **New view kinds and entity kinds** get the next code. A new entity kind's 6. **New view kinds and entity kinds** get the next code. A new entity kind's
id takes the next free thing field (13 onwards), so older apps skip it and id takes the next free thing field (14 onwards; 13 is a vessel id that is
fall back to the camera. A thing that needs more than an id (for example a a UUID), so older apps skip it and fall back to the camera. A thing that needs more than an id (for example a
CCTV camera, a radio station or a transit vehicle) adds further thing CCTV camera, a radio station or a transit vehicle) adds further thing
fields the same way. fields the same way.
7. **A new layer option** needs an entry in `SHARE_VIEW_LAYER_OPTION_ABSENT` 7. **A new layer option** needs an entry in `SHARE_VIEW_LAYER_OPTION_ABSENT`
+1 -1
View File
@@ -270,7 +270,7 @@ and the development server's internal routes. See SECURITY.md.
| `get_aircraft_info` | `aircraft` | Aircraft type and registration, and flight route, from adsbdb | | `get_aircraft_info` | `aircraft` | Aircraft type and registration, and flight route, from adsbdb |
| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type; `group_by` and `stats` count and summarize every match | | `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type; `group_by` and `stats` count and summarize every match |
| `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name | | `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name |
| `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points | | `get_vessel_track` | `vessels` | Recent positions of one ship by vessel id or MMSI, thinned to 200 points |
| `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility | | `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility |
| `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a place or point now, highest first | | `satellites_overhead` | `satellites` | Satellites in a CelesTrak group above a place or point now, highest first |
| `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first | | `find_cctv_cameras` | `cctv` | Public cameras in an area, nearest first |
+5 -4
View File
@@ -1,6 +1,6 @@
# Translating God's Eye View # Translating God's Eye View
Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). Shipped languages: **English** (source and fallback), **Arabic** (`ar`), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Hebrew** (`he`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Swedish** (`sv`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`).
## How it works ## How it works
@@ -45,9 +45,10 @@ Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change,
1. Create `src/i18n/locales/<code>.json`, fill it from `npm run i18n -- --missing <code>` (start from `{}`), and translate every entry. 1. Create `src/i18n/locales/<code>.json`, fill it from `npm run i18n -- --missing <code>` (start from `{}`), and translate every entry.
2. Import it in `src/i18n/index.js` and add it to `CATALOGS`. 2. Import it in `src/i18n/index.js` and add it to `CATALOGS`.
3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`; the chrome mirrors through logical CSS properties (`inset-inline-*`, `margin-inline-*`), and `src/ui/styles/rtl.css` holds what those cannot express (see the Arabic notes). If the language should keep Western digits, map it in `INTL_TAGS` in `locale.js`. 3. Add the code to `LOCALES` and its own-language name to `LOCALE_NAMES` in `src/i18n/locale.js`. Right-to-left languages also go in `RTL_LOCALES`; the chrome mirrors through logical CSS properties (`inset-inline-*`, `margin-inline-*`), and `src/ui/styles/rtl.css` holds what those cannot express (see the Arabic notes). If the language should keep Western digits, map it in `INTL_TAGS` in `locale.js`.
4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json`. 4. Add `src/i18n/locales/<code>.json` next to `ru.json` in `scripts/package-boundaries.json` and in `scripts/format-scope.json`.
5. Write `docs/translations/<code>.md`: glossary, plural forms, units and style (copy the structure of `ru.md`). 5. Write `docs/translations/<code>.md`: glossary, plural forms, units and style (copy the structure of `ru.md`).
6. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths. 6. If the language is not written in Latin letters, also add it to `NON_LATIN_LOCALES` in `src/hudSummaryResponse.js` — that guard falls back to the translated text when a summary provider answers in English.
7. Run `npm run i18n` and `npm test`, then check the panels in the app at narrow widths.
## Rules ## Rules
@@ -63,4 +64,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth
## Language notes ## Language notes
Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one. Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/<code>.md`: [Arabic](./translations/ar.md), [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Hebrew](./translations/he.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Swedish](./translations/sv.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one.
+2 -2
View File
@@ -270,9 +270,9 @@ a feature supplying only its accent.
the tracked-entity fade is measured once the frame is in the document; the tracked-entity fade is measured once the frame is in the document;
the styles it injected now live in `src/ui/styles/annotations.css`), the the styles it injected now live in `src/ui/styles/annotations.css`), the
visible map target list (the scene chrome renders the buttons and status visible map target list (the scene chrome renders the buttons and status
the world overlay presents through `subscribeWorldOverlayActions()`, which the world overlay presents through `subscribeActions()`, which
the application shell publishes to the `gev-world-overlay-actions` store the application shell publishes to the `gev-world-overlay-actions` store
and answers with `activateWorldOverlayAction()`; the module needs no Vue and answers with `activateAction()`; the module needs no Vue
for the portable transit entry, and its card and detection canvases stay for the portable transit entry, and its card and detection canvases stay
painting surfaces it draws on, like the Cesium canvas), the Global painting surfaces it draws on, like the Cesium canvas), the Global
Context compass and contact bearings (`AwarenessDirection` teleports into Context compass and contact bearings (`AwarenessDirection` teleports into
+64
View File
@@ -0,0 +1,64 @@
# Hebrew (`he`)
Catalog: `src/i18n/locales/he.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
## Style
- Address the user with a masculine imperative, Google style: הסתר, הצג, נסה שוב, התקרב. Menu items and tooltips take the noun form: הסרת {name}, הרחבת {name}, שינוי הצבע של {name}.
- Sentence case: Hebrew has no case forms, so UPPERCASE English is normal sentence style here; short HUD labels stay short, favor an abbreviation over a wrapped line.
- Units follow the catalog: ק"מ, מ׳, קמ"ש, שנ׳, דק׳, שע׳; an abbreviation always ends in the geresh (׳). Instrument readouts and formatted shared-unit labels keep the Latin units (km, NM, FT, KTS, MW, KM/S); catalog prose outside those readouts keeps ק"מ/מ׳. Codes like HDG, FOV, CAL, SRC, REC stay exactly as in English — see `same-as-english.json`.
## Digits and direction
- Western digits 0–9 only, like every catalog: "5 מטוסים", "26.08.2026". Dates in scene labels use dd.mm.yyyy.
- `{direction}` values are adverbs (שמאלה, ימינה, ישר); maneuver templates are written to place them after the verb: פנה {direction} אל {road}.
- Values inserted by t() are isolated (U+2068…U+2069); do not add manual RTL marks — write the sentence as natural Hebrew and let isolation do the rest.
## Plurals
A `tn()` entry needs three forms under `Intl.PluralRules('he')`:
| Form | Counts | Example |
| ------- | ---------------------------- | -------------- |
| `one` | 1 and fractions below 1 | 1 מצלמה · 0.5 |
| `two` | 2 | 2 מצלמות |
| `other` | 0, 3, 10, 100, integers… | 5 מצלמות |
With {count} shown as a digit, `two` and `other` share the plural noun (2 מצלמות, 5 מצלמות); `one` takes the singular (1 מצלמה). For constructs that resist agreement with a noun (אין, הכול, זמין), phrase around the number: "{count} תחנות מדווחות" is fine, but prefer "ממתין: {count}" over a mismatched genitive chain.
## Glossary
| English | עברית |
| ---------------------------------------------- | ----------------------------------------------------- |
| layer / data layer | שכבה / שכבת נתונים |
| feed | פיד |
| source / data source / Provider Settings | מקור / מקור נתונים / הגדרות הספקים |
| LIVE / live data / LIVE TRACK | חי / נתונים חיים / מעקב חי |
| SIMULATED | מדומה |
| stale / degraded / unavailable | מיושן / מופחת / לא זמין |
| key (API key) / POWER UP | מפתח / שדרוג |
| globe / map / map source / basemap | גלובוס / מפה / מקור מפה / מפת בסיס |
| aircraft / flight / military flight | מטוס (כלי טיס in class lists) / טיסה / טיסה צבאית |
| vessel / ship / ships | אונייה / אוניות |
| satellite | לוויין |
| launch (rocket) / space mission | שיגור / משימת חלל |
| contact / tracking | מגע / עקיבה |
| installation / site | מתקן / אתר |
| camera / CCTV camera / ALPR camera | מצלמה / מצלמת CCTV / מצלמת ALPR |
| earthquake / active fire / cyclone | רעידת אדמה / שריפה פעילה / ציקלון |
| scene / shot | סצנה / שוט |
| briefing / panel / dock | תדריך / פאנל / עגינה |
| visual preset / style | ערכת ראייה / סגנון |
| Scope (circular viewport mask) / keyhole | סקופ / חור המנעול |
| heading (compass °) / course | כיוון / מהלך |
| HDG / BRG / CRS / FOV / DEST / SPD / RANGE | בלי שינוי — קודי מכשיר |
| advisory (NHC/CPHC cyclone) | עדכון; Advisory center — מרכז הסערה לפי העדכון |
| Street View (Google) | Street View, בלי תרגום |
| Street Level / Street Level layer | Street Level, בלי תרגום (שם שכבה) |
| WHOT/BHOT, Ironbow, CRT, NVG, FLIR | בלי שינוי — קודים ושמות מצבים |
| PREV / NEXT | הקודם / הבא |
| anchor / data pack / asset bundle (scene) | עוגן / חבילת נתונים / חבילת נכסים |
| forecast (weather) | תחזית |
| witness / geolocated source | עד ראייה / מקור מאוכן |
| contained (wildland fire) / FRP | הכלה / FRP, בלי שינוי |
+84
View File
@@ -0,0 +1,84 @@
# Swedish (`sv`)
Catalog: `src/i18n/locales/sv.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
Translation source: [God's Eye View Sverige](https://github.com/Kajjan7610/Gods-view-sweden) by Kaj Obst, November 2026.
## Style
- Standard Swedish as in Swedish software and on Swedish authority sites (SMHI, Trafikverket, Polisen).
- Actions and hints use the imperative without a pronoun: «Försök igen», «Klicka på en kamera för att aktivera den», «Välj en tagning först», «Tillåt mikrofonen och försök igen». Avoid «du»/«ni»; "your" becomes «egen» or is left out: «Egen RTL-SDR-mottagare», «Stationen ser din IP-adress» only where a person must be addressed.
- Sentence case, not Title Case: "Live Flights" → «Liveflyg», "Provider Settings" → «Leverantörsinställningar». UPPERCASE English stays UPPERCASE: «LIVEDATA AV», NOT AN ALL-CLEAR → «INTE FARA ÖVER».
- Compounds are written as one word, with a hyphen after a name, code or abbreviation: «datalager», «väderrutor», «TomTom-nyckel», «AIS-fartyg», «Street View-reserv». Never split a compound with a space.
- Short labels, chips and HUD readouts stay short; use a standard abbreviation with a period over a wrapped line: RÄCKV., HAST., NEDERB., ÖVR., MEDD., FÖRST., KAL. «Jämför» is JFR, without a period.
- The avionics codes HDG, BRG, CRS, DEST, SPD, ALT, GS, TRK, V/S, FOV and SQUAWK stay in English, as in the cockpit. Words in a sentence are translated: heading → «kurs», bearing → «bäring», range → «räckvidd».
- A dash between clauses is a spaced en dash « – ». Quotes are ”…” (Swedish closing quotes on both sides).
- Status and waiting messages: «Väntar på uppdatering», «Laddar …». Reasons after a dash stay lower case: «Scenen sparades inte – projektet är större än 5 MiB».
- A layer named in a sentence takes its panel name in quotes: «Lagret ”Liveflyg” är inte aktiverat».
- Rolling windows say their length: LAST YEAR → «SENASTE 12 MÅN.», LAST MONTH → «SENASTE 30 DAGARNA».
- Keyboard keys: Mellanslag, Backsteg, Skift; Esc, Enter, Tab and letter keys stay.
- Decimal comma in fixed text: «M4,4», «28,2710° N». Thousands separator is a space: «1 000 km». Formatted numbers come from `Intl` and are already Swedish.
- Units: km, m, km/h, m/s, kn (knop), ft (fot), hPa, MW, h, min, s, d (dygn, in short readouts); in UPPERCASE text KM, KM/H, KN, FT, MIN, H, D. A space between number and unit, also before %: «{value} m», «{count} min», «50 %». `°` sticks to the number.
- "ago": «för {count} min sedan», short «{count} min sedan». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «sedan» again.
- Compass points use O for öst and V for väst, as SMHI does: N, NNO, NO, ONO, O, OSO, SO, SSO, S, SSV, SV, VSV, V, VNV, NV, NNV; hemisphere letters N, S, O, V.
- Routing: «Sväng {direction} in på {road}», «Fortsätt {direction}», «Ta {exit} avfarten» with ordinals «2:a», «3:e».
## Plurals
A `tn()` entry needs two forms. `Intl.PluralRules('sv')` picks:
| Form | Counts | Example |
| ------- | --------------------------- | ----------- |
| `one` | 1 | 1 kamera |
| `other` | everything else: 0, 2, 1,5… | 3 kameror |
Abbreviated units and codes do not change: «{count} MEDD.».
## Glossary
| English | Svenska |
| ---------------------------------------------- | --------------------------------------------------------------- |
| layer / data layer | lager / datalager |
| feed / source / data source | flöde / källa / datakälla |
| LIVE (status) / live data | LIVE / livedata |
| SIMULATED | SIMULERAT |
| stale / degraded / unavailable | inaktuell / begränsad / otillgänglig |
| retry / retrying / retrying slowly | nytt försök / försöker igen / försöker igen med längre intervall |
| fallback | reserv |
| key (API key) / POWER UP / POWERED UP | nyckel / KOPPLA PÅ / PÅKOPPLAD |
| Provider Settings | Leverantörsinställningar |
| ON / OFF (state chips) | PÅ / AV |
| enable / disable | aktivera / inaktivera |
| globe / map / map source / basemap | glob / karta / kartkälla / bakgrundskarta |
| aircraft / flight / military flights | luftfartyg (short labels: flygplan) / flygning / militärflyg |
| vessel, ship | fartyg |
| satellite / shell (Starlink) | satellit / skal |
| launch (rocket) / launch site / space mission | uppskjutning / uppskjutningsplats / rymduppdrag |
| contact / tracking | kontakt / följning |
| installation / site | anläggning / plats |
| camera / CCTV camera | kamera / CCTV-kamera |
| earthquake / active fire / fire perimeter | jordbävning / aktiv brand / brandgräns |
| cyclone / advisory (NHC) / cone | cyklon / bulletin / kon |
| radio station / broadcast FM | radiostation / FM-radio |
| road / traffic flow | väg / trafikflöde |
| transit / bike share | kollektivtrafik / hyrcyklar |
| scene / shot / capture shot / camera move | scen / tagning / spara tagning / kameraåkning |
| anchor / data pack / bundle | ankare / datapaket / paket |
| briefing / panel / dock | genomgång / panel / docka |
| preset / visual preset | förval / visuellt förval |
| Scope (circular viewport mask) / keyhole | Scope / nyckelhål |
| overlay / detection overlay | överlägg / detekteringsöverlägg |
| Clean UI | Rent gränssnitt |
| Night vision / Thermal / Snow (styles) | Mörkerseende / Värmekamera / Snö |
| heading / altitude / ground speed | kurs / höjd / markfart |
| RANGE / PITCH / HGT (CCTV calibration chips) | RÄCKV. / LUTN. / HÖJD |
| PREV / NEXT / RESUME | FÖRRA / NÄSTA / FORTSÄTT |
| nearest (spatial) | närmaste |
| expand / collapse | fäll ut / fäll ihop |
| Street Level / Street View (Google) | Gatunivå / Street View |
| Recent Imagery | Senaste bilder |
| voice (control) / voice session | röststyrning / röstsession |
| click / tap / press a key | klicka / tryck / tryck på tangenten |
| card / label (on the map) / clear (layers) | kort / etikett / stäng av |
| frame (weather, video) / image | bildruta / bild |
| incident | händelse |
+2 -2
View File
@@ -21,12 +21,12 @@
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Noto+Sans+Arabic:wght@400;500;600;700&family=Noto+Sans+Hebrew:wght@400;500;600;700&display=swap" rel="stylesheet" />
<!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync <!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync
with the sources: src/materialSymbolsSubset.test.mjs fails when a source with the sources: src/materialSymbolsSubset.test.mjs fails when a source
names a glyph this list is missing, because a missing glyph does not draw names a glyph this list is missing, because a missing glyph does not draw
a box — the ligature never forms and the element renders the literal word. --> a box — the ligature never forms and the element renders the literal word. -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,bug_report,category,chevron_left,chevron_right,close,close_fullscreen,dark_mode,delete,draw,east,eco,egg_alt,flare,flight,handyman,layers,layers_clear,light_mode,local_fire_department,location_on,more_horiz,movie,my_location,navigation,normal,on,open_in_full,partly_cloudy_day,pentagon,pest_control,pets,public,question_mark,radar,radio,raven,right_panel_close,right_panel_open,rocket_launch,satellite_alt,search,set_meal,settings,skip_next,skip_previous,streetview,timeline,tune,umbrella,upload_file,videocam,view_in_ar,visibility,visibility_off,water_drop,zoom_in_map" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,bug_report,category,chevron_left,chevron_right,close,close_fullscreen,dark_mode,directions_boat,draw,east,eco,egg_alt,flare,flight,handyman,layers,layers_clear,light_mode,local_fire_department,location_on,more_horiz,movie,my_location,navigation,normal,on,open_in_full,partly_cloudy_day,pause,pentagon,pest_control,pets,play_arrow,public,question_mark,radar,radio,raven,right_panel_close,right_panel_open,rocket_launch,satellite_alt,search,set_meal,settings,skip_next,skip_previous,streetview,timeline,traffic,tune,umbrella,upload_file,videocam,view_in_ar,visibility,visibility_off,water_drop" rel="stylesheet" />
</head> </head>
<body> <body>
<div class="gev-view" data-gev-view="scene-chrome"></div> <div class="gev-view" data-gev-view="scene-chrome"></div>
+1121 -19
View File
File diff suppressed because it is too large. Load diff
+18 -11
View File
@@ -38,10 +38,6 @@
"i18n": "node scripts/i18n.mjs", "i18n": "node scripts/i18n.mjs",
"layer-token:next": "node scripts/next-layer-state-token.mjs", "layer-token:next": "node scripts/next-layer-state-token.mjs",
"layer-token:check": "node scripts/check-layer-state-tokens.mjs", "layer-token:check": "node scripts/check-layer-state-tokens.mjs",
"library:check": "node scripts/library.mjs check",
"library:next": "node scripts/library.mjs next",
"library:docs": "node scripts/library.mjs docs",
"library:probe": "node scripts/library.mjs probe",
"changelog": "node scripts/assemble-changelog.mjs --write", "changelog": "node scripts/assemble-changelog.mjs --write",
"changelog:check": "node scripts/assemble-changelog.mjs --check", "changelog:check": "node scripts/assemble-changelog.mjs --check",
"dev": "vite", "dev": "vite",
@@ -58,24 +54,28 @@
"qa:street-level:fixtures": "node scripts/qa-street-level.mjs --fixtures", "qa:street-level:fixtures": "node scripts/qa-street-level.mjs --fixtures",
"check:boundaries": "node scripts/check-import-directions.mjs && node scripts/check-package-boundaries.mjs", "check:boundaries": "node scripts/check-import-directions.mjs && node scripts/check-package-boundaries.mjs",
"qa:transit": "node scripts/qa-transit.mjs", "qa:transit": "node scripts/qa-transit.mjs",
"qa:data-library": "node scripts/qa-data-library.mjs", "mcp": "node server/mcp/stdio.js",
"mcp": "node server/mcp/stdio.js" "check:main-chunk": "node scripts/check-main-chunk.mjs"
}, },
"dependencies": { "dependencies": {
"@jtarrio/signals": "^0.10.0", "@jtarrio/signals": "^0.10.0",
"@jtarrio/webrtlsdr": "^3.0.6", "@jtarrio/webrtlsdr": "^3.0.6",
"@mapbox/vector-tile": "^3.0.0", "@mapbox/vector-tile": "^3.0.0",
"@meri-imperiumi/eccodes-wasm": "^2.48.2", "@meri-imperiumi/eccodes-wasm": "^2.48.2",
"@navaramap/three": "0.2.1",
"@navaramap/three-default-plugin": "0.2.1",
"cesium": "^1.124.0", "cesium": "^1.124.0",
"earcut": "^3.0.2",
"egm96-universal": "^1.1.1", "egm96-universal": "^1.1.1",
"fzstd": "0.1.1",
"hls.js": "^1.7.3", "hls.js": "^1.7.3",
"hyparquet": "1.31.2", "kdbush": "4.0.2",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"mgrs": "^2.1.0", "mgrs": "^2.1.0",
"pbf": "^5.1.2", "pbf": "^5.1.2",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"postprocessing": "6.39.4",
"satellite.js": "^6.0.2", "satellite.js": "^6.0.2",
"three": "0.185.1",
"vue": "^3.5.44", "vue": "^3.5.44",
"vue-i18n": "^11.4.15" "vue-i18n": "^11.4.15"
}, },
@@ -83,6 +83,7 @@
"@happy-dom/global-registrator": "^20.14.6", "@happy-dom/global-registrator": "^20.14.6",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.5.1", "@vue/test-utils": "^2.5.1",
"flag-icons": "7.5.0",
"happy-dom": "^20.14.6", "happy-dom": "^20.14.6",
"prettier": "3.9.6", "prettier": "3.9.6",
"puppeteer": "^25.16.0", "puppeteer": "^25.16.0",
@@ -96,7 +97,8 @@
"./infrastructure/geojson": "./src/data/localGeojsonCore.js", "./infrastructure/geojson": "./src/data/localGeojsonCore.js",
"./infrastructure/lod": "./src/data/localGeojsonLod.js", "./infrastructure/lod": "./src/data/localGeojsonLod.js",
"./application": "./src/app/application.js", "./application": "./src/app/application.js",
"./application/viewer": "./src/app/viewer.js", "./application/viewer": "./src/globe/cesium/viewer.js",
"./time": "./src/time/worldClock.js",
"./build/vite": { "./build/vite": {
"node": "./build/vite.js" "node": "./build/vite.js"
}, },
@@ -104,6 +106,7 @@
"node": "./server/providers/live.js" "node": "./server/providers/live.js"
}, },
"./sources/adsb-lol": "./src/data/adsbLolFallback.js", "./sources/adsb-lol": "./src/data/adsbLolFallback.js",
"./sources/planespotters": "./src/data/planespottersPhoto.js",
"./server/providers/places": { "./server/providers/places": {
"node": "./server/providers/places.js" "node": "./server/providers/places.js"
}, },
@@ -142,6 +145,9 @@
"./server/providers/regional": { "./server/providers/regional": {
"node": "./server/providers/regional.js" "node": "./server/providers/regional.js"
}, },
"./server/providers/news": {
"node": "./server/providers/news.js"
},
"./server/providers/openai": { "./server/providers/openai": {
"node": "./server/providers/openai.js" "node": "./server/providers/openai.js"
}, },
@@ -205,8 +211,8 @@
"./geography/neighborhoods": "./src/data/neighborhoodPolygons.js", "./geography/neighborhoods": "./src/data/neighborhoodPolygons.js",
"./maps/controller": "./src/maps/controller.js", "./maps/controller": "./src/maps/controller.js",
"./maps/catalog": "./src/maps/catalog.js", "./maps/catalog": "./src/maps/catalog.js",
"./maps/imagery": "./src/maps/imagery.js", "./maps/imagery": "./src/globe/cesium/mapImagery.js",
"./maps/terrain": "./src/maps/terrain.js", "./maps/terrain": "./src/globe/cesium/terrain.js",
"./maps/3d": "./src/maps/google3d.js", "./maps/3d": "./src/maps/google3d.js",
"./maps/defaults": "./src/maps/defaultSources.js", "./maps/defaults": "./src/maps/defaultSources.js",
"./voice/controller": "./src/voice/realtimeController.js", "./voice/controller": "./src/voice/realtimeController.js",
@@ -265,6 +271,7 @@
"./layers/gnss/source": "./src/layers/gnss/source.js", "./layers/gnss/source": "./src/layers/gnss/source.js",
"./layers/ghostmaps/source": "./src/layers/ghostMaps/source.js", "./layers/ghostmaps/source": "./src/layers/ghostMaps/source.js",
"./layers/drone-airspace/source": "./src/layers/droneAirspace/source.js", "./layers/drone-airspace/source": "./src/layers/droneAirspace/source.js",
"./layers/news/source": "./src/layers/news/source.js",
"./layers/firms/source": "./src/layers/firms/source.js", "./layers/firms/source": "./src/layers/firms/source.js",
"./layers/installations/source": "./src/layers/installations/source.js", "./layers/installations/source": "./src/layers/installations/source.js",
"./layers/launches/source": "./src/layers/launches/source.js", "./layers/launches/source": "./src/layers/launches/source.js",
+1
View File
@@ -0,0 +1 @@
{"source":"Revos addons/Qc cams/quebec_all_cameras.geojson","count":1120,"cameras":[{"id":"4103","kind":"traffic","name":"Route 117 à la jonction de la route 101 et à la hauteur du carrefour giratoire (Rouyn-Noranda)","nameEn":"Junction of route 117 and route 101 and at roundabout (Rouyn-Noranda)","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Abitibi-Témiscamingue","lat":48.26461,"lon":-79.01808,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=4103","notes":"Live since 2025/11/26"},{"id":"3820","kind":"traffic","name":"Aut. 20 à la hauteur de la montée Industrielle-et-Commerciale","nameEn":"Aut. 20 at montée Industrielle-et-Commerciale","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.43993,"lon":-68.47784,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3820","notes":"Live since 2018/12/12"},{"id":"3821","kind":"traffic","name":"Montée Industrielle-et-Commerciale à Rimouski","nameEn":"Montée Industrielle-et-Commerciale at Rimouski","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.44639,"lon":-68.48528,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3821","notes":"Live since 2018/12/12"},{"id":"3822","kind":"traffic","name":"Route 132 à la hauteur du Bic à la jonction de l'Aut. 20","nameEn":"Route 132 at Bic at the junction of Aut. 20","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.3775,"lon":-68.68694,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3822","notes":"Live since 2018/12/12"},{"id":"3834","kind":"traffic","name":"Route 132 à la hauteur de Saint-Fabien","nameEn":"Route 132 at Saint-Fabien","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.32927,"lon":-68.80262,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3834","notes":"Live since 2019/02/07"},{"id":"3835","kind":"traffic","name":"Route 132 à la hauteur de Cap-Chat (secteur Capucins)","nameEn":"Route 132 at Cap-Chat (Capucins sector)","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":49.04153,"lon":-66.86847,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3835","notes":"Live since 2019/02/07"},{"id":"3856","kind":"traffic","name":"Route 132 à l'est de Gros-Morne","nameEn":"Route 132, east of Gros-Morne","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":49.25302,"lon":-65.48355,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3856","notes":"Live since 2019/12/06"},{"id":"3836","kind":"traffic","name":"Route 132 à la hauteur de la baie du Petit Pabos","nameEn":"Route 132 at baie du Petit Pabos","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.37216,"lon":-64.58866,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3836","notes":"Live since 2019/02/07"},{"id":"3837","kind":"traffic","name":"Route 132 à la hauteur de Bonaventure","nameEn":"Route 132 at Bonaventure","category":"Highway traffic","operator":"Ministere des Transports et de la Mobilite durable (MTQ) - Quebec 511","region":"Bas-Saint-Laurent—Gaspésie—Îles-de-la-Madeleine","lat":48.03448,"lon":-65.46953,"accuracyM":0,"positionSource":"MTQ georeferenced point (WFS, EPSG:4326, 5 decimals)","viewerUrl":"https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3837","Line truncated
+57 -1
View File
@@ -7,13 +7,16 @@
* Natural Earth 10m admin-1 states and provinces (public domain). * Natural Earth 10m admin-1 states and provinces (public domain).
* src/data/local_data/us_census_counties/counties.json * src/data/local_data/us_census_counties/counties.json
* US Census Bureau cartographic boundary counties (public domain). * US Census Bureau cartographic boundary counties (public domain).
* src/data/local_data/natural_earth/populated_places.json
* Natural Earth 10m populated places as points, for the orbital view's
* "near <city>" (`src/data/placeBelow.js`; public domain).
* *
* Both sources are pinned by URL and SHA-256, so a rerun reproduces the packs * Both sources are pinned by URL and SHA-256, so a rerun reproduces the packs
* byte for byte. Downloads are cached outside the repository (default: the * byte for byte. Downloads are cached outside the repository (default: the
* system temp directory) and are never committed. * system temp directory) and are never committed.
* *
* Usage: * Usage:
* node scripts/build-admin-packs.mjs [--cache <dir>] [--only countries|admin1|counties] * node scripts/build-admin-packs.mjs [--cache <dir>] [--only countries|admin1|counties|places]
* *
* The provenance READMEs beside each pack record these parameters. * The provenance READMEs beside each pack record these parameters.
*/ */
@@ -101,6 +104,9 @@ export const PARAMS = Object.freeze({
elsewhereMinPopulation: 50_000, elsewhereMinPopulation: 50_000,
cityStateMaxKm2: 1_000, cityStateMaxKm2: 1_000,
}, },
// Every populated place, kept as a point: name, position and the two
// prominence ranks the orbital view picks "near <city>" by.
places: { decimals: 3 },
}); });
const args = process.argv.slice(2); const args = process.argv.slice(2);
@@ -688,6 +694,47 @@ async function buildAdmin1() {
}; };
} }
async function buildPlaces() {
const params = PARAMS.places;
const source = await fetchPinned('places');
const factor = 10 ** params.decimals;
const round = (value) => Math.round(value * factor) / factor;
const places = JSON.parse(source.bytes.toString('utf8'))
.features.map((f) => f.properties)
.filter(
(p) =>
p.name &&
Number.isFinite(p.latitude) &&
Number.isFinite(p.longitude) &&
Number.isFinite(p.scalerank),
)
.map((p) => [
p.name,
round(p.latitude),
round(p.longitude),
p.scalerank,
Number.isFinite(p.rank_max) ? p.rank_max : 0,
])
.sort((a, b) => a[3] - b[3] || b[4] - a[4] || a[0].localeCompare(b[0]));
return {
meta: {
title: 'Natural Earth populated places',
source: 'ne_10m_populated_places_simple',
url: SOURCES.places.url,
sha256: source.digest,
commit: NE_COMMIT,
license:
'Public domain (https://www.naturalearthdata.com/about/terms-of-use/)',
schema:
'places[]: [name, latitude, longitude, scalerank (0 most prominent .. 10), rank_max (population class, 0..14; higher is larger)]',
decimals: params.decimals,
curation: params,
script: 'scripts/build-admin-packs.mjs',
},
places,
};
}
async function buildCounties() { async function buildCounties() {
const params = PARAMS.counties; const params = PARAMS.counties;
const source = await fetchPinned('counties'); const source = await fetchPinned('counties');
@@ -768,4 +815,13 @@ if (isMain) {
'src/data/local_data/us_census_counties/counties.json', 'src/data/local_data/us_census_counties/counties.json',
await buildCounties(), await buildCounties(),
); );
if (!only || only === 'places') {
const relative = 'src/data/local_data/natural_earth/populated_places.json';
const pack = await buildPlaces();
const text = JSON.stringify(pack) + '\n';
await writeFile(path.join(ROOT, relative), text);
console.log(
`${relative}: ${pack.places.length} places, ${Buffer.byteLength(text)} bytes`,
);
}
} }
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* Build the owner flag sheet the orbital Cockpit marks a satellite's owner
* country with (`src/ui/ownerFlag.js`): every flag in OWNER_COUNTRY_CODES
* (`src/layers/satellites/classes.js`), drawn from the flag-icons 4:3 SVGs
* at twice the readout's 20 × 15 px, in one PNG the app loads at startup.
*
* src/data/local_data/owner_flags/owner_flags.png the sheet
* src/ui/ownerFlagSheet.js its layout (generated)
*
* Flag i (in the sorted code list) sits at column i % COLUMNS, row
* floor(i / COLUMNS), each cell followed by a GUTTER of transparent pixels. A rerun with the same flag-icons and sharp versions
* reproduces both files.
*
* Usage: node scripts/build-owner-flags.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as prettier from 'prettier';
import sharp from 'sharp';
import { OWNER_COUNTRY_CODES } from '../src/layers/satellites/classes.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const FLAGS_DIR = path.join(
path.dirname(require.resolve('flag-icons/package.json')),
'flags/4x3',
);
const FLAG_ICONS_VERSION = require('flag-icons/package.json').version;
/** One flag's cell, CSS pixels × 2. */
export const CELL = Object.freeze({ width: 40, height: 30 });
/**
* Transparent sheet pixels after each flag, so a browser smoothing a scaled
* flag (a 125% display) never reaches into its neighbour.
*/
export const GUTTER = 2;
export const COLUMNS = 16;
const SHEET = path.join(
ROOT,
'src/data/local_data/owner_flags/owner_flags.png',
);
const LAYOUT = path.join(ROOT, 'src/ui/ownerFlagSheet.js');
async function flagCell(code) {
const svg = await readFile(path.join(FLAGS_DIR, `${code.toLowerCase()}.svg`));
return sharp(svg, { density: 300 })
.resize(CELL.width, CELL.height, { fit: 'fill' })
.png()
.toBuffer();
}
const codes = [...OWNER_COUNTRY_CODES];
const rows = Math.ceil(codes.length / COLUMNS);
const pitchX = CELL.width + GUTTER;
const pitchY = CELL.height + GUTTER;
const cells = await Promise.all(codes.map(flagCell));
const sheet = await sharp({
create: {
width: COLUMNS * pitchX,
height: rows * pitchY,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 },
},
})
.composite(
cells.map((input, i) => ({
input,
left: (i % COLUMNS) * pitchX,
top: Math.floor(i / COLUMNS) * pitchY,
})),
)
.png({ compressionLevel: 9, palette: true, quality: 100, effort: 10 })
.toBuffer();
await writeFile(SHEET, sheet);
const layout = `// Generated by scripts/build-owner-flags.mjs from flag-icons ${FLAG_ICONS_VERSION}; do not edit.
/** The owner flag sheet's layout: flag i at column i % columns, row i / columns. */
export const OWNER_FLAG_SHEET = Object.freeze({
cellWidthPx: ${CELL.width},
cellHeightPx: ${CELL.height},
gutterPx: ${GUTTER},
columns: ${COLUMNS},
rows: ${rows},
codes: Object.freeze(${JSON.stringify(codes)}),
});
`;
await writeFile(
LAYOUT,
await prettier.format(layout, {
...(await prettier.resolveConfig(LAYOUT)),
filepath: LAYOUT,
}),
);
console.log(
`Wrote ${codes.length} flags (${COLUMNS} × ${rows}), ${(sheet.length / 1024).toFixed(1)} KB.`,
);
+257
View File
@@ -0,0 +1,257 @@
import assert from 'node:assert/strict';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
import * as geo from '../src/geo/index.js';
const directory = new URL('./fixtures/tiles3d/', import.meta.url);
const origin = { lon: -97.7431, lat: 30.2672 };
const transform = geo.transforms.eastNorthUpToFixedFrame(
geo.cartesian3.fromDegrees(origin.lon, origin.lat),
);
const box = (west, south, east, north, bottom, top) => [
[
[west, south, top],
[east, south, top],
[east, north, top],
[west, north, top],
],
[
[west, north, bottom],
[east, north, bottom],
[east, south, bottom],
[west, south, bottom],
],
[
[west, south, bottom],
[east, south, bottom],
[east, south, top],
[west, south, top],
],
[
[east, south, bottom],
[east, north, bottom],
[east, north, top],
[east, south, top],
],
[
[east, north, bottom],
[west, north, bottom],
[west, north, top],
[east, north, top],
],
[
[west, north, bottom],
[west, south, bottom],
[west, south, top],
[west, north, top],
],
];
// glTF is Y-up; the tileset transform is east/north/up after the 3D Tiles
// loader's Y-to-Z rotation. Untextured flat faces need no external assets.
function glb(faces) {
const positions = [],
normals = [];
for (const face of faces) {
const vertices = face.map(([x, y, z]) => [x, z, -y]);
const [a, b, c] = vertices;
const u = b.map((v, i) => v - a[i]);
const v = c.map((v, i) => v - a[i]);
const normal = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
const length = Math.hypot(...normal);
for (let i = 1; i < vertices.length - 1; i++)
for (const vertex of [a, vertices[i], vertices[i + 1]]) {
positions.push(...vertex);
normals.push(...normal.map((n) => n / length));
}
}
const bytes = Buffer.from(
new Float32Array([...positions, ...normals]).buffer,
);
const bounds = (fn) =>
[0, 1, 2].map((axis) => fn(...positions.filter((_, i) => i % 3 === axis)));
const json = Buffer.from(
JSON.stringify({
asset: { version: '2.0' },
scene: 0,
scenes: [{ nodes: [0] }],
nodes: [{ mesh: 0 }],
meshes: [
{
primitives: [{ attributes: { POSITION: 0, NORMAL: 1 }, material: 0 }],
},
],
materials: [
{
pbrMetallicRoughness: {
baseColorFactor: [0.5, 0.5, 0.5, 1],
metallicFactor: 0,
roughnessFactor: 1,
},
},
],
buffers: [{ byteLength: bytes.length }],
bufferViews: [0, 1].map((i) => ({
buffer: 0,
byteOffset: i * positions.length * 4,
byteLength: positions.length * 4,
target: 34962,
})),
accessors: [
{
bufferView: 0,
componentType: 5126,
count: positions.length / 3,
type: 'VEC3',
min: bounds(Math.min),
max: bounds(Math.max),
},
{
bufferView: 1,
componentType: 5126,
count: positions.length / 3,
type: 'VEC3',
},
],
}),
);
const padded = Buffer.alloc(Math.ceil(json.length / 4) * 4, 0x20);
json.copy(padded);
const result = Buffer.alloc(12 + 8 + padded.length + 8 + bytes.length);
result.writeUInt32LE(0x46546c67, 0);
result.writeUInt32LE(2, 4);
result.writeUInt32LE(result.length, 8);
result.writeUInt32LE(padded.length, 12);
result.writeUInt32LE(0x4e4f534a, 16);
padded.copy(result, 20);
result.writeUInt32LE(bytes.length, 20 + padded.length);
result.writeUInt32LE(0x004e4942, 24 + padded.length);
bytes.copy(result, 28 + padded.length);
return result;
}
function probe(x, y, z) {
const position = geo.matrix4.multiplyByPoint(transform, { x, y, z }, {});
const cartographic = geo.cartographic.fromCartesian(position);
return {
position,
cartographic,
lon: geo.math.toDegrees(cartographic.longitude),
lat: geo.math.toDegrees(cartographic.latitude),
height: cartographic.height,
};
}
export function buildTilesFixture() {
const roof = box(110, -60, 210, 60, 0, 60);
roof.shift();
roof.push(
[
[110, -60, 60],
[160, -60, 100],
[160, 60, 100],
[110, 60, 60],
],
[
[160, -60, 100],
[210, -60, 60],
[210, 60, 60],
[160, 60, 100],
],
[
[110, -60, 60],
[210, -60, 60],
[160, -60, 100],
],
[
[210, 60, 60],
[110, 60, 60],
[160, 60, 100],
],
);
const tiles = [
[
'plate',
box(-300, -300, 300, 300, -2, 0),
[0, 0, -1, 300, 0, 0, 0, 300, 0, 0, 0, 1],
],
[
'block50',
box(-210, -60, -110, 60, 0, 50),
[-160, 0, 25, 50, 0, 0, 0, 60, 0, 0, 0, 25],
],
[
'block200',
box(-50, 120, 50, 220, 0, 200),
[0, 170, 100, 50, 0, 0, 0, 50, 0, 0, 0, 100],
],
['roof', roof, [160, 0, 50, 50, 0, 0, 0, 60, 0, 0, 0, 50]],
];
const tileset = {
asset: { version: '1.1' },
geometricError: 1000,
root: {
transform: Array.from(transform),
boundingVolume: { box: [0, 0, 99, 300, 0, 0, 0, 300, 0, 0, 0, 101] },
geometricError: 1000,
refine: 'ADD',
children: tiles.map(([name, , box]) => ({
boundingVolume: { box },
geometricError: 0,
content: { uri: `${name}.glb` },
})),
},
extras: {
origin,
datum: 'WGS84 ellipsoid; local ENU heights in metres',
probes: {
plate: probe(0, -170, 0),
block50: probe(-160, 0, 50),
block200: probe(0, 170, 200),
roof: probe(180, 0, 84),
},
// More than one pass of surfacePoints, all away from the roof edges.
points: Array.from({ length: 40 }, (_, i) => {
const x = 140 + (i % 8) * 5;
return probe(
x,
-40 + Math.floor(i / 8) * 20,
100 - 0.8 * Math.abs(x - 160),
);
}),
},
};
const files = new Map(
tiles.map(([name, faces]) => [`${name}.glb`, glb(faces)]),
);
files.set(
'tileset.json',
Buffer.from(JSON.stringify(tileset, null, 2) + '\n'),
);
assert.ok(
[...files.values()].reduce((sum, b) => sum + b.length, 0) < 200_000,
);
return files;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const check = process.argv.includes('--check');
const files = buildTilesFixture();
if (!check) mkdirSync(directory, { recursive: true });
for (const [name, bytes] of files) {
const file = new URL(name, directory);
if (check)
assert.deepEqual(readFileSync(file), bytes, `${name} is reproducible`);
else writeFileSync(file, bytes);
}
console.log(
`${check ? 'Checked' : 'Generated'} ${files.size} fixture files (${[...files.values()].reduce((sum, b) => sum + b.length, 0)} bytes)`,
);
}
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
'''Maintainer-only UCS Satellite Database pack build (openpyxl).
Reads the UCS Excel release and writes the compact pack the satellites layer
loads: one row per satellite, keyed by NORAD number, with string-table indexes
for the repeated text columns. Starlink rows are left out: the layer files
every STARLINK object under its own class and operator without them.
Usage: python scripts/build-ucs-satellites.py "<path>/UCS-Satellite-Database 5-1-2023.xlsx"
'''
import argparse
import json
import logging
from pathlib import Path
import re
import openpyxl
LOG = logging.getLogger('build-ucs-satellites')
ROOT = Path(__file__).resolve().parent.parent
PACK = ROOT / 'src/data/local_data/ucs_satellites'
VERSION = 'UCS-Satellite-Database 5-1-2023'
DATA_CURRENT_TO = '2023-05-01'
URL = 'https://www.ucsusa.org/satellite_database'
# Output column -> spreadsheet header.
COLUMNS = {
'operator': 'Operator/Owner',
'operatorCountry': 'Country of Operator/Owner',
'purpose': 'Purpose',
}
STARLINK = re.compile(r'\bstarlink\b', re.I)
def clean(value: object) -> str:
'''One cell as tidy text; UCS's placeholders for nothing become ''.'''
text = re.sub(r'\s+', ' ', str(value or '')).strip()
return '' if text.lower() in ('', 'none', 'n/a') else text
def build(xlsx: Path) -> dict:
'''Read the release's first sheet into the pack the layer loads.'''
sheet = openpyxl.load_workbook(xlsx, read_only=True, data_only=True).worksheets[0]
rows = sheet.iter_rows(values_only=True)
header = {str(h).strip(): i for i, h in enumerate(next(rows)) if h}
names = [header['Current Official Name of Satellite'], header['Name of Satellite, Alternate Names']]
norad_col = header['NORAD Number']
tables = {key: [''] for key in COLUMNS}
index = {key: {'': 0} for key in COLUMNS}
records = {}
source_rows = skipped_starlink = invalid_norad = 0
for row in rows:
if not any(row):
continue
source_rows += 1
if any(STARLINK.search(clean(row[i])) for i in names):
skipped_starlink += 1
continue
try:
norad = int(float(row[norad_col]))
except (TypeError, ValueError):
invalid_norad += 1
continue
if norad <= 0:
invalid_norad += 1
continue
record = [norad]
for key, column in COLUMNS.items():
text = clean(row[header[column]])
if text not in index[key]:
index[key][text] = len(tables[key])
tables[key].append(text)
record.append(index[key][text])
# A duplicate NORAD number keeps the first row, as UCS lists it.
records.setdefault(norad, record)
ordered = [records[n] for n in sorted(records)]
return {
'meta': {
'source': 'Union of Concerned Scientists Satellite Database',
'version': VERSION,
'url': URL,
'dataCurrentTo': DATA_CURRENT_TO,
'license': 'Free, unrestricted use; acknowledgement requested (UCS database manual, section 3)',
'curation': {
'sourceRows': source_rows,
'starlinkRowsDropped': skipped_starlink,
'invalidNoradDropped': invalid_norad,
'duplicateNoradDropped': source_rows - skipped_starlink - invalid_norad - len(ordered),
'columns': COLUMNS,
},
'recordCount': len(ordered),
},
'fields': ['noradId', *COLUMNS],
'strings': tables,
'records': ordered,
}
def main() -> None:
'''Build the pack from the release given on the command line.'''
logging.basicConfig(level=logging.INFO, format='%(message)s')
parser = argparse.ArgumentParser(description='Build the UCS satellite pack from the UCS Excel release.')
parser.add_argument('xlsx', type=Path, help='UCS-Satellite-Database <version>.xlsx')
pack = build(parser.parse_args().xlsx)
PACK.mkdir(parents=True, exist_ok=True)
out = PACK / 'ucs_satellites.json'
# LF on every platform: the repository stores text with LF endings.
out.write_text(
json.dumps(pack, ensure_ascii=False, separators=(',', ':')) + '\n',
encoding='utf-8',
newline='\n',
)
LOG.info('%s: %d records, %d bytes', out.relative_to(ROOT), pack['meta']['recordCount'], out.stat().st_size)
if __name__ == '__main__':
main()
Loaded 100 of 2122 files, more files were not shown because too many files have changed in this diff. Show more