Commit Graph
100 Commits
Author SHA1 Message Date
Sameh Khamis ad9bd0f4f9 docs(changelog): 1696-i18n-entry 2026-11-12 22:07:30 -08:00
Sameh Khamis 5f96009b67 feat(i18n): an ./i18n entry for an application with its own language choice
startPageLocale, followPageLocale and switchLocale start, follow and switch
the page's language as the standalone application and the globe picker do,
for an application that offers the language elsewhere and passes
localePicker: false. The standalone application uses them.
2026-11-12 22:07:13 -08:00
Sameh Khamis 2bc3c1bed1 docs(changelog): 1695-vessel-photos 2026-11-12 20:11:20 -08:00
Sameh Khamis d5b030e7fa docs(changelog): 1694-cctv-globe-plane-clamp 2026-11-12 20:11:19 -08:00
Sameh Khamis 23aa1027fc docs(changelog): 1693-cctv-frame-no-flash 2026-11-12 20:11:17 -08:00
Sameh Khamis 347e77d1f6 feat(vessels): a selected vessel shows its Open Vessel Photos photo and MMSI
Selecting a vessel asks Open Vessel Photos (a free, keyless API) for its
photo by MMSI, once per MMSI. The CONTACTS view shows it under the vessel's
name and MMSI with the photographer, the photo's open licence and a link to
the vessel's page; only images on Open Vessel Photos' own host are shown, and
an answer about another MMSI is no photo. The vessel source exposes the lookup
as getEnrichment({kind: 'photo'}) and fetchOpenVesselPhoto for applications
with their own vessel source.
2026-11-12 20:10:30 -08:00
Sameh Khamis 2445af349d fix(cctv): on a globe map the camera plane ends where the view meets the ground
The activation probe raycast only the photoreal tiles. On a globe map the ray
missed, or ran on through the Earth and met a primitive thousands of
kilometres away, and the monitor plane stood at the camera's full range:
about a kilometre wide and hundreds of metres up. A hit at or beyond the range
now counts as none, and on a globe map the plane also clamps short of the
globe's terrain along the axis, or flat ground at the mount's ground while
that terrain is still loading, whichever is nearer.
2026-11-12 20:10:02 -08:00
Sameh Khamis 6e4df4c287 fix(cctv): a new camera's frame shows the moment it is decoded, without a fade
The preview settled when the image had loaded, so the panel's <img> took the
src while it was still decoding and then faded in over 180 ms: every camera
click lingered on an empty box. The preview now settles once the frame is
decoded (a rejected decode still shows the loaded frame, and a decode that
never settles shows it after one second), and the fade is gone.
2026-11-12 20:10:02 -08:00
Sameh Khamis 1d476c87ee fix(location): a POI key held with Cmd, Ctrl or Alt runs no POI
The points-of-interest keys (Q/W/E/R/T while a city is expanded) are a
second bare-key document listener, and it did not look at modifiers
either: Alt+R or a delivered Ctrl+R picked the fourth POI and flew the
camera there. It now leaves Cmd, Ctrl and Alt chords to the browser like
the application shortcuts; Shift still counts as the key. The docs and
the changelog entry say so.
2026-11-12 18:02:41 -08:00
Sameh Khamis d1966b9751 Clear a view-bounded layer's error when the camera returns to the drawn view
A failed request for the view in between left its error set, and the
early return for the already drawn view never cleared it, so the drawn
features showed with an error state.
2026-11-12 17:57:31 -08:00
Sameh Khamis 92d6188045 Stop a moved-on view from cancelling the GeoParquet index the next view needs
The browser shared one partition-index build per entry but ran it under
the first view's signal. A camera move cancels the old view before the
next one starts, so the next view was handed the same build, already
cancelled, and drew nothing (the view layer takes an AbortError for its
own cancellation) until the camera moved again. EPC hit this on its
first view after any quick move. The build now belongs to no view: each
view stops waiting on its own signal, and the build runs on for the next
view and every view after it.
2026-11-12 17:56:17 -08:00
Sameh Khamis 8cca78bcd5 Stop sharing a Data Library entry as on once its failed Remove switched it off
When Remove switched a layer off and then its destroy failed, the
manager published no visibility change, so the selection still listed
the entry as on: the link shared it as on and a reload restored it on
while its row read off. A failed Remove now drops the entry from what is
on and remembered whenever its layer ended up off.
2026-11-12 17:49:38 -08:00
Sameh Khamis 837eff7328 Let a share fit a full Data Library and show entries it cannot serve
A share's entries joined the library after the stored ones, and the
selection kept its first 64, so a link opened into a full library never
showed what it shared and the next hash write dropped its lib=. Now the
oldest entries the share leaves off make room for it, and their layers
are removed with them.

Browse counted an entry as added only while its layer was registered,
but an entry this instance cannot serve (switched off by the operator,
or its server unreachable) stayed in the selection and held one of the
64 places. A library full of them looked empty and refused every Add.
Such entries now show as added, so they can be removed one by one or
with Remove all.
2026-11-12 17:49:01 -08:00
Sameh Khamis 4fe8796e89 Swap refreshed Data Library features in place, on every tick
The live refresh had three faults. Its data age counted from when a
fetch finished, so with the manager ticking every cacheSeconds the next
tick found the data just short of due and data refreshed every other
tick. It switched the layer off and on to redraw, which removed the old
features before the new ones were built, closed an open feature card and,
if the rebuild failed, left the layer on but empty. And the tick the
manager runs right after enable refetched after a failed first load, so
a second failure switched the layer off instead of showing UNAVAILABLE.

The local GeoJSON layer gains replaceFeatures(): it builds the new set
while the old one stays drawn, removes the old source only once the new
one is in the scene, keeps a selected feature that is still in the new
set (same id, or same place and label), and on a failed rebuild puts the
old features, records and selection back and rejects. The library's
refresh uses it, counts age from the request, allows a little slack on
the tick, and does nothing until a first load has succeeded.
2026-11-12 17:47:29 -08:00
Sameh Khamis 1a657e2a64 Keep the library layer namespace in the layer lifecycle package
The core layer lifecycle imported the whole Data Library manifest
contract only to check the lib- prefix, so the layer-lifecycle package
owned a feature module. The namespace rule (prefix, entry id pattern,
the two id helpers) now lives in src/data/libraryLayerIds.js, which the
lifecycle owns; the manifest contract imports and re-exports it, so
library callers are unchanged.
2026-11-12 16:04:55 -08:00
Sameh Khamis 9e57fd84c8 Draw Data Library coverage outlines from the bundled Natural Earth countries
Imagery outlines fetched Natural Earth's 1:50m countries (about 3 MB)
from the natural-earth-vector repository's master branch on GitHub at
run time: a moving, rate-limited source whose failures silently removed
the outlines. Core already ships the pinned 1:10m countries pack, so
the outlines now read that, once, the first time an outline needs it.
2026-11-12 16:02:07 -08:00
Sameh Khamis 8d90cac7dc Keep lib= in the URL before the library starts and for entries it cannot serve
The share encoder wrote only registered entries. Until availability
loaded nothing was registered, so any hash write in the meantime (a
camera move during start-up) deleted lib= from the address bar, and an
entry this instance could not serve (its proxy unreachable) was dropped
for good: copying the link or reloading lost those entries. The URL now
carries what the page opened with until the library starts, and every
entry that is on afterwards, served here or not.
2026-11-12 15:59:14 -08:00
Sameh Khamis 2d5db9f63d Never let a share rewrite the Data Library entries remembered as on
Every restore wrote the selection it applied to browser storage, so
opening a link that switched no library entry on stored every saved
entry as off, and the next bare visit restored them off. Core layer
state never writes a recipient's preferences from a share. The library
now keeps what is remembered as on apart from what a share switches on
for the session: a share's entries still join the library, but only a
local restore and the viewer's own choices change what is remembered.
2026-11-12 15:58:22 -08:00
Sameh Khamis 53f0a32a1b Keep a Data Library entry whose layer fails to tear down
Remove dropped an entry from the selection and reported success even
when the manager could not disable or destroy its layer, leaving a
registered, possibly visible layer that Browse, share links and saved
state no longer knew. Remove now keeps the entry and reports failure,
so its row and Remove all still reach it and a later Remove can retry.
2026-11-12 15:57:02 -08:00
Sameh Khamis f33a6ada52 Refuse a Data Library entry past the 64-entry cap before it registers
Add registered and switched on an entry before the selection was cut to
its first 64, so a 65th was dropped from the selection but stayed on:
Browse showed it as not added, share links left it out and Remove all
missed it. Start-up registered a share's entries the same way past the
cap. Add now refuses an entry a full library cannot keep, and start-up
registers only what the selection keeps.
2026-11-12 15:53:29 -08:00
Sameh Khamis e25cce874d Read an ArcGIS load stopped at maxFeatures as truncated
Paging stopped once maxFeatures were read before it noted that the
upstream had more, so a page holding exactly the cap with
exceededTransferLimit set read as the whole dataset. FEMA USA
Structures (2,000 features, one page) never showed PARTIAL in a dense
view. Stopping at the cap or the last page with more upstream now marks
the load truncated.
2026-11-12 15:52:31 -08:00
Sameh Khamis 134239fa5a Read a view over an incomplete GeoParquet index as PARTIAL
The browser's partition index kept whether it was complete only to
schedule a rebuild and handed the partitions on without it, so a view
counted unread partitions from an index that might be missing some and
could read as complete, even empty, over an area it had never indexed.
A view over an incomplete index now reads PARTIAL until a rebuild
completes.
2026-11-12 15:51:52 -08:00
Sameh Khamis e05a9566e4 Retry a GeoParquet footer whose body fails mid-stream
Only a failed fetch() marked a footer read as worth retrying. A body cut
off after its headers arrived (a reset, the proxy's 20-second timeout)
was taken for an unparseable file: the partition was left out and the
index still read as complete, so the server cached it without that
partition for the entry's cacheSeconds (24 hours for EPC). Such a read
is now retried like any fetch failure; an oversized body is still final.
2026-11-12 15:50:46 -08:00
Sameh Khamis 7d91a1875b Cancel a view-bounded load when the camera returns to the drawn view
Moving from view A to B started B's request; moving back to A before it
finished returned early because A was already drawn, so B's request ran
on and its features replaced A's while the camera was at A. Returning to
the drawn view now cancels the request in between.
2026-11-12 15:49:56 -08:00
Sameh Khamis 0f13c0a1c2 Let a newer share switch off a library layer still switching on
A share restore skipped every entry whose settled state already matched
the share, but a layer still switching on for an earlier share reads as
off until it settles. Opening a second share in that window left the
first share's layer to come on anyway. The restore now compares with the
manager's effective visibility (the newest request), so the newer share
supersedes the earlier one.
2026-11-12 15:49:11 -08:00
Sameh Khamis d58ba23fba Refresh live Data Library datasets once older than cacheSeconds
A dataset layer loaded its features once and kept them until it was
removed: the parsed set survives disable/enable, so NWS alerts, GDACS and
EONET showed what they held when first switched on for the whole session.

A library dataset now asks the manager for a refresh every cacheSeconds.
Once its data is that old it loads again, and swaps the new set in only
when it is ready, so the old features stay drawn while the request runs
and stay if it fails. An enable after cacheSeconds also loads afresh.
Disable and destroy cancel a refresh in flight.
2026-11-12 15:48:14 -08:00
Sameh Khamis a105e819e0 fix(news): an application's picture cards stay at every height
Zoomed out, a hotspot's reports give way to its beacon, and the cards an application draws through pictureCards went with them; they came back only once the camera had stopped after zooming in. The cards now come from every report the close-up view would mark, at any height; the layer's own markers and beacons are unchanged.
2026-11-11 19:43:06 -08:00
Sameh Khamis 2b1784d7ed Merge remote-tracking branch 'upstream/main' into denis/news-world-clock 2026-11-11 15:17:44 -08:00
Sameh Khamis b7015f8819 Merge remote-tracking branch 'upstream/main' into denis/news-world-clock
# Conflicts:
#	docs/CURRENT-STATE.md
#	src/app/constructCatalog.js
2026-11-11 15:03:45 -08:00
Sameh Khamis 283890cee0 fix(rail): trip-update delays are as old as the server's read
The browser stamped every trip-updates answer as just read. While an
operator's feed fails, the rail proxy replays its last good copy for up to
ten minutes, and a cached copy can be a TTL old, so those delays never
reached RAIL_UPDATES_MAX_AGE_MS and a selected train's card showed them as
current. Delays now take their age from the snapshot's fetchedAt (capped
at now), as positions do in ingestion.js, so they stop applying 75 s after
the server last read the operator.
2026-11-11 14:57:02 -08:00
Sameh Khamis ace2160002 fix(rail): Montpellier tram speeds are converted from km/h
TaM publishes km/h where GTFS-Realtime asks m/s, and the Transit layer's
entry for the same feed already sets speedUnit: 'km/h'. The Rail entry did
not, so the proxy passed the readings through and the card multiplied them
by 3.6 again: a tram at 30 km/h read about 108 km/h. A registry test now
requires a Rail feed that Transit also reads to declare the same unit.
2026-11-11 14:54:01 -08:00
Sameh Khamis 0f18c03041 chore(news): credit the #1677 changelog fragment 2026-11-11 14:48:05 -08:00
Sameh Khamis 99a1e06b61 feat(news): an application may draw the News picture cards itself
createNewsLayer takes a pictureCards(cards, { selectedId }) function; createApplicationCatalog passes it through newsOptions. With it, the layer paints no picture cards of its own and hands the function one card per pictured event on the map, most severe then newest first, with the selected report's id; it hands no cards when the layer turns off.
2026-11-11 13:53:10 -08:00
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 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 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 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 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 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
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 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
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
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
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
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 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
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 6eb0729832 fix(firms): translate the snapshot-end label for Hebrew 2026-11-10 10:35:16 -08: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
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
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 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 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
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
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